From 12b805ac1e4db2ee6bd17061cfbf0a2535662389 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 14 Jan 2025 21:38:07 -0500 Subject: [PATCH] feat: drop support for tox 3 (#910) * feat: drop support for tox 3 Signed-off-by: Henry Schreiner * Update ci.yml * tests: include Python version Signed-off-by: Henry Schreiner * fix: drop unused code Signed-off-by: Henry Schreiner --------- Signed-off-by: Henry Schreiner --- .github/workflows/ci.yml | 6 +- CONTRIBUTING.md | 2 +- nox/tox_to_nox.jinja2 | 36 --------- nox/tox_to_nox.py | 142 ++++++++++++++++-------------------- noxfile.py | 20 +---- pyproject.toml | 2 +- requirements-conda-test.txt | 2 +- tests/test_tox_to_nox.py | 46 +----------- 8 files changed, 73 insertions(+), 183 deletions(-) delete mode 100644 nox/tox_to_nox.jinja2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f32ff12..96709a96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,10 +54,8 @@ jobs: channels: conda-forge/label/python_rc,conda-forge - name: Install Nox-under-test (uv) run: uv pip install --system . - - name: Run tests on ${{ matrix.os }} (tox <4) - run: nox --non-interactive --error-on-missing-interpreter --session "tests(python='${{ matrix.python-version }}', tox_version='<4')" -- --full-trace - - name: Run tox-to-nox tests on ${{ matrix.os }} (tox latest) - run: nox --non-interactive --error-on-missing-interpreter --session "tests(python='${{ matrix.python-version }}', tox_version='latest')" -- tests/test_tox_to_nox.py --full-trace + - name: Run tests on ${{ matrix.os }} + run: nox --non-interactive --error-on-missing-interpreter --session "tests-${{ matrix.python-version }}" -- --full-trace - name: Save coverage report uses: actions/upload-artifact@v4 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6479a924..2448c5d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ To just check for lint errors, run: To run against a particular Python version: - nox --session "tests(python='3.12', tox_version='latest')" + nox --session tests-3.12 nox --session conda_tests nox --session mamba_tests nox --session micromamba_tests diff --git a/nox/tox_to_nox.jinja2 b/nox/tox_to_nox.jinja2 deleted file mode 100644 index 1e22ca89..00000000 --- a/nox/tox_to_nox.jinja2 +++ /dev/null @@ -1,36 +0,0 @@ -import nox - -{% for envname, envconfig in config.envconfigs.items()|sort: %} -@nox.session({%- if envconfig.basepython %}python='{{envconfig.basepython}}'{%- endif %}) -def {{fixname(envname)}}(session): - {%- if envconfig.description != '' %} - """{{envconfig.description}}""" - {%- endif %} - {%- set envs = dict(envconfig.setenv) -%} - {%- do envs.pop('PYTHONHASHSEED', None) -%} - {%- do envs.pop('TOX_ENV_DIR', None) -%} - {%- do envs.pop('TOX_ENV_NAME', None) -%} - {%- for key, value in envs.items()|sort: %} - session.env['{{key}}'] = '{{value}}' - {%- endfor %} - - {%- if envconfig.deps %} - session.install({{wrapjoin(envconfig.deps)}}) - {%- endif %} - - {%- if not envconfig.skip_install %} - {%- if envconfig.usedevelop %} - session.install('-e', '.') - {%- else %} - session.install('.') - {%- endif -%} - {%- endif %} - - {%- if envconfig.changedir and config.toxinidir.bestrelpath(envconfig.changedir) != '.' %} - session.chdir('{{config.toxinidir.bestrelpath(envconfig.changedir)}}') - {%- endif %} - - {%- for command in envconfig.commands %} - session.run({{wrapjoin(command)}}) - {%- endfor %} -{% endfor %} diff --git a/nox/tox_to_nox.py b/nox/tox_to_nox.py index 088f6664..48844544 100644 --- a/nox/tox_to_nox.py +++ b/nox/tox_to_nox.py @@ -21,13 +21,11 @@ import re import sys from configparser import ConfigParser -from importlib import metadata from pathlib import Path from subprocess import check_output from typing import Any, Iterable import jinja2 -import tox.config if sys.version_info < (3, 9): from importlib_resources import files @@ -42,22 +40,12 @@ def __dir__() -> list[str]: return __all__ -TOX_VERSION = metadata.version("tox") - -TOX4 = int(TOX_VERSION.split(".")[0]) >= 4 - DATA = files("nox") -if TOX4: - _TEMPLATE = jinja2.Template( - DATA.joinpath("tox4_to_nox.jinja2").read_text(encoding="utf-8"), - extensions=["jinja2.ext.do"], - ) -else: - _TEMPLATE = jinja2.Template( - DATA.joinpath("tox_to_nox.jinja2").read_text(encoding="utf-8"), - extensions=["jinja2.ext.do"], - ) +_TEMPLATE = jinja2.Template( + DATA.joinpath("tox4_to_nox.jinja2").read_text(encoding="utf-8"), + extensions=["jinja2.ext.do"], +) def wrapjoin(seq: Iterable[Any]) -> str: @@ -66,14 +54,11 @@ def wrapjoin(seq: Iterable[Any]) -> str: def fixname(envname: str) -> str: - """Replace dashes with underscores and check if the result is a valid identifier.""" - envname = envname.replace("-", "_").replace("testenv:", "") - if not envname.isidentifier(): - print( - f"Environment {envname!r} is not a valid nox session name.\n" - "Manually update the session name in noxfile.py before running nox." - ) - return envname + """ + Replace dashes with underscores. Tox 4+ requires valid identifiers for + names already. + """ + return envname.replace("-", "_").replace("testenv:", "") def write_output_to_file(output: str, filename: str) -> None: @@ -88,65 +73,60 @@ def main() -> None: args = parser.parse_args() - if TOX4: - output = check_output(["tox", "config"], text=True, encoding="utf-8") # noqa: S607 - 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() + output = check_output(["tox", "config"], text=True, encoding="utf-8") # noqa: S607 + 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( + [ + part + for dep in section["deps"].strip().splitlines() + for part in (dep.split() if dep.startswith("-r") else [dep]) ] + ) - config[name]["deps"] = wrapjoin([ - part for dep - in section["deps"].strip().splitlines() - for part in - (dep.split() if dep.startswith("-r") else [dep]) - ]) - - 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([]) + 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 output = _TEMPLATE.render(config=config, wrapjoin=wrapjoin, fixname=fixname) diff --git a/noxfile.py b/noxfile.py index 7c23c90f..75319631 100644 --- a/noxfile.py +++ b/noxfile.py @@ -38,28 +38,16 @@ ] -@nox.session -@nox.parametrize( - "python, tox_version", - [ - (python, tox_version) - for python in ALL_PYTHONS - for tox_version in ("latest", "<4") - ], -) -def tests(session: nox.Session, tox_version: str) -> None: +@nox.session(python=ALL_PYTHONS) +def tests(session: nox.Session) -> None: """Run test suite with pytest.""" - coverage_file = ( - f".coverage.{sys.platform}.{session.python}.tox{tox_version.lstrip('<')}" - ) + coverage_file = f".coverage.{sys.platform}.{session.python}" session.create_tmp() # Fixes permission errors on Windows session.install(*PYPROJECT["dependency-groups"]["test"], "uv") session.install("-e.[tox_to_nox]") - if tox_version != "latest": - session.install(f"tox{tox_version}") - extra_env = {"PYTHONWARNDEFAULTENCODING": "1"} if tox_version == "latest" else {} + extra_env = {"PYTHONWARNDEFAULTENCODING": "1"} session.run( "pytest", "--cov", diff --git a/pyproject.toml b/pyproject.toml index f0911e8e..c52a5e06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ optional-dependencies.tox_to_nox = [ "importlib-resources; python_version<'3.9'", "jinja2", - "tox", + "tox>=4", ] optional-dependencies.uv = [ "uv>=0.1.6", diff --git a/requirements-conda-test.txt b/requirements-conda-test.txt index 8dde50ea..8392016f 100644 --- a/requirements-conda-test.txt +++ b/requirements-conda-test.txt @@ -2,5 +2,5 @@ argcomplete >=1.9.4,<3.0 colorlog >=2.6.1,<7.0.0 jinja2 pytest -tox<4.0.0 +tox>=4.0.0 virtualenv >=20.14.1 diff --git a/tests/test_tox_to_nox.py b/tests/test_tox_to_nox.py index 24885e3c..17751466 100644 --- a/tests/test_tox_to_nox.py +++ b/tests/test_tox_to_nox.py @@ -28,7 +28,6 @@ tox_to_nox = pytest.importorskip("nox.tox_to_nox") -TOX4 = tox_to_nox.TOX4 PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}" PYTHON_VERSION_NODOT = PYTHON_VERSION.replace(".", "") @@ -289,44 +288,6 @@ def test_with_dash(session): ) -@pytest.mark.skipif(TOX4, reason="Not supported in tox 4.") -def test_non_identifier_in_envname( - makeconfig: Callable[[str], str], capfd: pytest.CaptureFixture[str] -) -> None: - result = makeconfig( - textwrap.dedent( - f""" - [tox] - envlist = test-with-& - - [testenv:test-with-&] - basepython = python{PYTHON_VERSION} - """ - ) - ) - - assert ( - result - == textwrap.dedent( - f""" - import nox - - - @nox.session(python='python{PYTHON_VERSION}') - def test_with_&(session): - session.install('.') - """ - ).lstrip() - ) - - out, _ = capfd.readouterr() - - assert ( - out == "Environment 'test_with_&' is not a valid nox session name.\n" - "Manually update the session name in noxfile.py before running nox.\n" - ) - - def test_descriptions_into_docstrings(makeconfig: Callable[[str], str]) -> None: result = makeconfig( textwrap.dedent( @@ -371,17 +332,16 @@ def test_commands_with_requirements(makeconfig: Callable[[str], str]) -> None: pytest pytest-cov aiohttp: -r requirements/aiohttp.txt - """ - ) + """) ) assert ( result - == textwrap.dedent(""" + == textwrap.dedent(f""" import nox - @nox.session(python='python3.13') + @nox.session(python='python{PYTHON_VERSION}') def aiohttp(session): session.install('pytest', 'pytest-cov', '-r', 'requirements/aiohttp.txt') session.install('-e', '.')