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 a6591b4b..866027a8 100644 --- a/nox/tox_to_nox.py +++ b/nox/tox_to_nox.py @@ -17,16 +17,30 @@ from __future__ import annotations import argparse +import os import pkgutil +import re +from configparser import ConfigParser +from pathlib import Path +from subprocess import check_output from typing import Any, Iterator 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: @@ -34,7 +48,7 @@ def wrapjoin(seq: Iterator[Any]) -> str: def fixname(envname: str) -> str: - 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" @@ -49,7 +63,64 @@ 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 = {} + + 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 + + if isinstance(section.get("commands"), str): + config[name]["commands"] = [ + wrapjoin(c.split()) + for c in section["commands"].strip().splitlines() + ] + + if isinstance(section.get("deps"), str): + 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) with open(args.output, "w") as outfile: diff --git a/noxfile.py b/noxfile.py index 45a98736..eecc0e6d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -46,6 +46,17 @@ def tests(session: nox.Session) -> None: *session.posargs, env={"COVERAGE_FILE": f".coverage.{session.python}"}, ) + # Test tox_to_nox with older tox + session.install("tox<4") + session.run( + "pytest", + "--cov=nox", + "--cov-config", + "pyproject.toml", + "--cov-report=", + "tests/test_tox_to_nox.py", + env={"COVERAGE_FILE": f".coverage.{session.python}.tox3"}, + ) session.notify("cover") diff --git a/pyproject.toml b/pyproject.toml index f9b8ea88..bc6d449c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ classifiers = [ [project.optional-dependencies] tox_to_nox = [ "jinja2", - "tox<4", + "tox", ] [project.urls] diff --git a/tests/test_tox_to_nox.py b/tests/test_tox_to_nox.py index c7b54850..47a2770f 100644 --- a/tests/test_tox_to_nox.py +++ b/tests/test_tox_to_nox.py @@ -18,9 +18,12 @@ import textwrap import pytest +from tox import __version__ as TOX_VERSION from nox import tox_to_nox +TOX4 = TOX_VERSION[0] == "4" + @pytest.fixture def makeconfig(tmpdir): @@ -277,6 +280,7 @@ 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(