diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5be101ae..f7dadd20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: matrix: os: [ubuntu-20.04, windows-latest, macos-latest] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + tox-version: ["latest", "<4"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -35,7 +36,33 @@ jobs: run: | python -m pip install --disable-pip-version-check . - name: Run tests on ${{ matrix.os }} - run: nox --non-interactive --error-on-missing-interpreter --session "tests-${{ matrix.python-version }}" -- --full-trace + run: nox --non-interactive --error-on-missing-interpreter --session "tests(python='${{ matrix.python-version }}', tox_version='${{ matrix.tox-version }}')" -- --full-trace + - name: Save coverage report + uses: actions/upload-artifact@v3 + with: + name: coverage + path: .coverage.* + + coverage: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install Nox-under-test + run: | + python -m pip install --disable-pip-version-check . + - name: Download individual coverage reports + uses: actions/download-artifact@v3 + with: + name: coverage + - name: Display structure of downloaded files + run: ls -aR + - name: Run coverage + run: nox --non-interactive --session "cover" lint: runs-on: ubuntu-latest diff --git a/nox/tox4_to_nox.jinja2 b/nox/tox4_to_nox.jinja2 new file mode 100644 index 00000000..e5a67d9b --- /dev/null +++ b/nox/tox4_to_nox.jinja2 @@ -0,0 +1,33 @@ +import nox + +{% for envname, envconfig in config.items()|sort: %} +@nox.session({%- if envconfig.base_python %}python='{{envconfig.base_python}}'{%- endif %}) +def {{fixname(envname)}}(session): + {%- if envconfig.description != '' %} + """{{envconfig.description}}""" + {%- endif %} + {%- set envs = envconfig.get('set_env', {}) -%} + {%- for key, value in envs.items()|sort: %} + session.env['{{key}}'] = '{{value}}' + {%- endfor %} + + {%- if envconfig.deps %} + session.install({{envconfig.deps}}) + {%- endif %} + + {%- if not envconfig.skip_install %} + {%- if envconfig.use_develop %} + session.install('-e', '.') + {%- else %} + session.install('.') + {%- endif -%} + {%- endif %} + + {%- if envconfig.change_dir %} + session.chdir('{{envconfig.change_dir}}') + {%- endif %} + + {%- for command in envconfig.commands %} + session.run({{command}}) + {%- endfor %} +{% endfor %} diff --git a/nox/tox_to_nox.py b/nox/tox_to_nox.py index 52a322f6..37e184f2 100644 --- a/nox/tox_to_nox.py +++ b/nox/tox_to_nox.py @@ -17,27 +17,40 @@ from __future__ import annotations import argparse +import os import pkgutil -from collections.abc import Iterator -from typing import Any +import re +from configparser import ConfigParser +from pathlib import Path +from subprocess import check_output +from typing import Any, Iterable import jinja2 import tox.config +from tox import __version__ as TOX_VERSION -_TEMPLATE = jinja2.Template( - pkgutil.get_data(__name__, "tox_to_nox.jinja2").decode("utf-8"), # type: ignore[union-attr] - extensions=["jinja2.ext.do"], -) +TOX4 = TOX_VERSION[0] == "4" +if TOX4: + _TEMPLATE = jinja2.Template( + pkgutil.get_data(__name__, "tox4_to_nox.jinja2").decode("utf-8"), # type: ignore[union-attr] + extensions=["jinja2.ext.do"], + ) +else: + _TEMPLATE = jinja2.Template( + pkgutil.get_data(__name__, "tox_to_nox.jinja2").decode("utf-8"), # type: ignore[union-attr] + extensions=["jinja2.ext.do"], + ) -def wrapjoin(seq: Iterator[Any]) -> str: + +def wrapjoin(seq: Iterable[Any]) -> str: """Wrap each item in single quotes and join them with a comma.""" return ", ".join([f"'{item}'" for item in seq]) def fixname(envname: str) -> str: """Replace dashes with underscores and check if the result is a valid identifier.""" - envname = envname.replace("-", "_") + envname = envname.replace("-", "_").replace("testenv:", "") if not envname.isidentifier(): print( f"Environment {envname!r} is not a valid nox session name.\n" @@ -58,7 +71,61 @@ def main() -> None: args = parser.parse_args() - config = tox.config.parseconfig([]) + if TOX4: + output = check_output(["tox", "config"], text=True) + original_config = ConfigParser() + original_config.read_string(output) + config: dict[str, dict[str, Any]] = {} + + for name, section in original_config.items(): + if name == "DEFAULT": + continue + + config[name] = dict(section) + # Convert set_env from string to dict + set_env = {} + for var in section.get("set_env", "").strip().splitlines(): + k, v = var.split("=") + if k not in ( + "PYTHONHASHSEED", + "PIP_DISABLE_PIP_VERSION_CHECK", + "PYTHONIOENCODING", + ): + set_env[k] = v + + config[name]["set_env"] = set_env + + config[name]["commands"] = [ + wrapjoin(c.split()) for c in section["commands"].strip().splitlines() + ] + + config[name]["deps"] = wrapjoin(section["deps"].strip().splitlines()) + + for option in "skip_install", "use_develop": + if section.get(option): + if section[option] == "False": + config[name][option] = False + else: + config[name][option] = True + + if os.path.isabs(section["base_python"]) or re.match( + r"py\d+", section["base_python"] + ): + impl = ( + "python" if section["py_impl"] == "cpython" else section["py_impl"] + ) + config[name]["base_python"] = impl + section["py_dot_ver"] + + change_dir = Path(section.get("change_dir")) + rel_to_cwd = change_dir.relative_to(Path.cwd()) + if str(rel_to_cwd) == ".": + config[name]["change_dir"] = None + else: + config[name]["change_dir"] = rel_to_cwd + + else: + config = tox.config.parseconfig([]) + output = _TEMPLATE.render(config=config, wrapjoin=wrapjoin, fixname=fixname) write_output_to_file(output, args.output) diff --git a/noxfile.py b/noxfile.py index 3ebed4b6..66bc2381 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,12 +31,29 @@ nox.options.sessions.append("conda_tests") -@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]) -def tests(session: nox.Session) -> None: +@nox.session +@nox.parametrize( + "python, tox_version", + [ + (python, tox_version) + for python in ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12") + for tox_version in ("latest", "<4") + ], +) +def tests(session: nox.Session, tox_version: str) -> None: """Run test suite with pytest.""" + # Because there is a dependency conflict between + # argcomplete and the latest tox (both depend on + # a different version of importlibmetadata for Py 3.7) + # pip installs tox 3 as the latest one for Py 3.7. + if session.python == "3.7" and tox_version == "latest": + return + session.create_tmp() # Fixes permission errors on Windows session.install("-r", "requirements-test.txt") session.install("-e", ".[tox_to_nox]") + if tox_version != "latest": + session.install(f"tox{tox_version}") session.run( "pytest", "--cov=nox", @@ -44,9 +61,10 @@ def tests(session: nox.Session) -> None: "pyproject.toml", "--cov-report=", *session.posargs, - env={"COVERAGE_FILE": f".coverage.{session.python}"}, + env={ + "COVERAGE_FILE": f".coverage.{session.python}.tox.{tox_version.lstrip('<')}" + }, ) - session.notify("cover") @nox.session(python=["3.7", "3.8", "3.9", "3.10"], venv_backend="conda") diff --git a/pyproject.toml b/pyproject.toml index 23b9e49f..76f7d1a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ [project.optional-dependencies] tox_to_nox = [ "jinja2", - "tox<4", + "tox", ] [project.urls] bug-tracker = "https://github.com/wntrblm/nox/issues" @@ -64,9 +64,6 @@ tox-to-nox = "nox.tox_to_nox:main" [tool.hatch] metadata.allow-ambiguous-features = true # disable normalization (tox-to-nox) for back-compat -[tool.ruff] -target-version = "py37" - [tool.ruff.lint] extend-select = [ "B", # flake8-bugbear @@ -85,6 +82,12 @@ ignore = [ "ISC001", # Conflicts with formatter ] +[tool.ruff] +target-version = "py37" + +[tool.isort] +profile = "black" + [tool.pytest.ini_options] minversion = "6.0" addopts = [ "-ra", "--strict-markers", "--strict-config" ] @@ -95,6 +98,7 @@ testpaths = [ "tests" ] [tool.coverage.run] branch = true +relative_files = true omit = [ "nox/_typing.py" ] [tool.coverage.report] diff --git a/tests/test_tox_to_nox.py b/tests/test_tox_to_nox.py index 76368553..11815b79 100644 --- a/tests/test_tox_to_nox.py +++ b/tests/test_tox_to_nox.py @@ -18,9 +18,14 @@ import textwrap import pytest +from tox import __version__ as TOX_VERSION tox_to_nox = pytest.importorskip("nox.tox_to_nox") +TOX4 = TOX_VERSION[0] == "4" +PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}" +PYTHON_VERSION_NODOT = PYTHON_VERSION.replace(".", "") + @pytest.fixture def makeconfig(tmpdir): @@ -40,9 +45,9 @@ def makeconfig(toxini_content): def test_trivial(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] - envlist = py27 + envlist = py{PYTHON_VERSION_NODOT} """ ) ) @@ -50,12 +55,12 @@ def test_trivial(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') - def py27(session): + @nox.session(python='python{PYTHON_VERSION}') + def py{PYTHON_VERSION_NODOT}(session): session.install('.') """ ).lstrip() @@ -65,9 +70,9 @@ def py27(session): def test_skipinstall(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] - envlist = py27 + envlist = py{PYTHON_VERSION_NODOT} [testenv] skip_install = True @@ -78,12 +83,12 @@ def test_skipinstall(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') - def py27(session): + @nox.session(python='python{PYTHON_VERSION}') + def py{PYTHON_VERSION_NODOT}(session): """ ).lstrip() ) @@ -92,9 +97,9 @@ def py27(session): def test_usedevelop(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] - envlist = py27 + envlist = py{PYTHON_VERSION_NODOT} [testenv] usedevelop = True @@ -105,12 +110,12 @@ def test_usedevelop(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') - def py27(session): + @nox.session(python='python{PYTHON_VERSION}') + def py{PYTHON_VERSION_NODOT}(session): session.install('-e', '.') """ ).lstrip() @@ -120,12 +125,12 @@ def py27(session): def test_commands(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] envlist = lint [testenv:lint] - basepython = python2.7 + basepython = python{PYTHON_VERSION} commands = python setup.py check --metadata --restructuredtext --strict flake8 \\ @@ -138,11 +143,11 @@ def test_commands(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') + @nox.session(python='python{PYTHON_VERSION}') def lint(session): session.install('.') session.run('python', 'setup.py', 'check', '--metadata', \ @@ -156,12 +161,12 @@ def lint(session): def test_deps(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] envlist = lint [testenv:lint] - basepython = python2.7 + basepython = python{PYTHON_VERSION} deps = flake8 gcp-devrel-py-tools>=0.0.3 @@ -172,11 +177,11 @@ def test_deps(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') + @nox.session(python='python{PYTHON_VERSION}') def lint(session): session.install('flake8', 'gcp-devrel-py-tools>=0.0.3') session.install('.') @@ -188,12 +193,12 @@ def lint(session): def test_env(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] envlist = lint [testenv:lint] - basepython = python2.7 + basepython = python{PYTHON_VERSION} setenv = SPHINX_APIDOC_OPTIONS=members,inherited-members,show-inheritance TEST=meep @@ -204,11 +209,11 @@ def test_env(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') + @nox.session(python='python{PYTHON_VERSION}') def lint(session): session.env['SPHINX_APIDOC_OPTIONS'] = \ 'members,inherited-members,show-inheritance' @@ -222,12 +227,12 @@ def lint(session): def test_chdir(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] envlist = lint [testenv:lint] - basepython = python2.7 + basepython = python{PYTHON_VERSION} changedir = docs """ ) @@ -236,11 +241,11 @@ def test_chdir(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') + @nox.session(python='python{PYTHON_VERSION}') def lint(session): session.install('.') session.chdir('docs') @@ -252,12 +257,12 @@ def lint(session): def test_dash_in_envname(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] envlist = test-with-dash [testenv:test-with-dash] - basepython = python2.7 + basepython = python{PYTHON_VERSION} """ ) ) @@ -265,11 +270,11 @@ def test_dash_in_envname(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') + @nox.session(python='python{PYTHON_VERSION}') def test_with_dash(session): session.install('.') """ @@ -277,15 +282,16 @@ def test_with_dash(session): ) +@pytest.mark.skipif(TOX4, reason="Not supported in tox 4.") def test_non_identifier_in_envname(makeconfig, capfd): result = makeconfig( textwrap.dedent( - """ + f""" [tox] envlist = test-with-& [testenv:test-with-&] - basepython = python2.7 + basepython = python{PYTHON_VERSION} """ ) ) @@ -293,11 +299,11 @@ def test_non_identifier_in_envname(makeconfig, capfd): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') + @nox.session(python='python{PYTHON_VERSION}') def test_with_&(session): session.install('.') """ @@ -315,12 +321,12 @@ def test_with_&(session): def test_descriptions_into_docstrings(makeconfig): result = makeconfig( textwrap.dedent( - """ + f""" [tox] envlist = lint [testenv:lint] - basepython = python2.7 + basepython = python{PYTHON_VERSION} description = runs the lint action now with an unnecessary second line @@ -331,11 +337,11 @@ def test_descriptions_into_docstrings(makeconfig): assert ( result == textwrap.dedent( - """ + f""" import nox - @nox.session(python='python2.7') + @nox.session(python='python{PYTHON_VERSION}') def lint(session): \"\"\"runs the lint action now with an unnecessary second line\"\"\" session.install('.')