diff --git a/README.rst b/README.rst index e8311d6..53786f7 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,15 @@ The ``tox-current-env`` plugin adds these options: This is useful for preparing the current environment for ``tox --current-env``. Use ``-`` for ``FILE`` to print to standard output. -It is possible to use the two printing options together, as long as the ``FILE`` is different. +``tox --print-dependency-groups-to=FILE`` + Instead of running any ``commands``, simply prints the names of the + `declared dependency_groups `_ + in ``dependency_groups`` to the specified ``FILE``. + This is useful for preparing the current environment for ``tox --current-env``. + Use ``-`` for ``FILE`` to print to standard output. + This option only exists with tox 4 and requires at least tox 4.22. + +It is possible to use the three printing options together, as long as the ``FILE`` is different. Invoking ``tox`` without any of the above options should behave as regular ``tox`` invocation without this plugin. Any deviation from this behavior is considered a bug. @@ -93,7 +101,7 @@ Usage ----- When the plugin is installed, -use ``tox`` with ``--current-env``, ``--print-deps-to`` or ``--print-extras-to`` +use ``tox`` with ``--current-env``, ``--print-deps-to``, ``--print-extras-to`` or ``--print-dependency-groups-to`` and all the other options as usual. Assuming your ``tox`` is installed on Python 3.7: @@ -153,6 +161,20 @@ To get a list of names of extras, run: py37: commands succeeded congratulations :) +To get a list of names of dependency groups, run: + +.. code-block:: console + + $ tox -e py37 --print-dependency-groups-to - + py37 create: /home/pythonista/projects/holy-grail/tests/.tox/py37 + py37 installed: ...you can see almost anything here... + py37 run-test-pre: PYTHONHASHSEED='3333333333' + group1 + ... + ___________________________________ summary ____________________________________ + py37: commands succeeded + congratulations :) + Caveats, warnings and limitations --------------------------------- @@ -167,6 +189,7 @@ The plugin is available also for tox 4. Differences in behavior between tox 3 an - The plugin does not check the requested Python version nor the environment name. If you let it run for multiple environments they'll all use the same Python. - Deprecated ``--print-deps-only`` option is no longer available. +- The ``--print-dependency-groups-to`` is only defined on tox 4. Use an isolated environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index d2a9f1d..9c0f003 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -56,13 +56,23 @@ def tox_add_option(parser): help="Don't run tests, only print the names of the required extras to the given file " + "(use `-` for stdout)", ) + parser.add_argument( + "--print-dependency-groups-to", + "--print-dependency-groups-to-file", + action="store", + type=argparse.FileType("w"), + metavar="FILE", + default=False, + help="Don't run tests, only print the names of the required dependency-groups to the given file " + + "(use `-` for stdout)", + ) @impl def tox_add_core_config(core_conf, state): opt = state.conf.options - if opt.current_env or opt.print_deps_to or opt.print_extras_to: + if opt.current_env or opt.print_deps_to or opt.print_extras_to or opt.print_dependency_groups_to: # We do not want to install the main package. # no_package is the same as skipsdist. loader = MemoryLoader(no_package=True) @@ -72,14 +82,14 @@ def tox_add_core_config(core_conf, state): opt.default_runner = "current-env" return - if getattr(opt.print_deps_to, "name", object()) == getattr( - opt.print_extras_to, "name", object() - ): + exclusive = [getattr(getattr(opt, o), "name", object()) + for o in ("print_deps_to", "print_extras_to", "print_dependency_groups_to")] + if len(exclusive) != len(set(exclusive)): raise RuntimeError( - "The paths given to --print-deps-to and --print-extras-to cannot be identical." + "The paths given to --print-*-to options cannot be identical." ) - if opt.print_deps_to or opt.print_extras_to: + if opt.print_deps_to or opt.print_extras_to or opt.print_dependency_groups_to: opt.default_runner = "print-env" return @@ -98,9 +108,8 @@ def tox_add_env_config(env_conf, state): if opt.current_env: allow_external_cmds = MemoryLoader(allowlist_externals=["*"], pass_env=["*"]) env_conf.loaders.insert(0, allow_external_cmds) - # For print-deps-to and print-extras-to, use empty - # list of commands so the tox does nothing. - if opt.print_deps_to or opt.print_extras_to: + # For print-*-to, use empty list of commands so that tox does nothing. + if opt.print_deps_to or opt.print_extras_to or opt.print_dependency_groups_to: empty_commands = MemoryLoader(commands=[], commands_pre=[], commands_post=[]) env_conf.loaders.insert(0, empty_commands) @@ -261,10 +270,21 @@ class PrintEnv(CurrentEnv): ) self.options.print_extras_to.flush() + if self.options.print_dependency_groups_to: + if "dependency_groups" not in self.conf: + raise RuntimeError( + "tox is too old to know about dependency_groups." + ) + print( + *self.conf["dependency_groups"], + sep="\n", + file=self.options.print_dependency_groups_to, + ) + self.options.print_dependency_groups_to.flush() + # https://github.com/fedora-python/tox-current-env/issues/75 return super().prepend_env_var_path() - @staticmethod def id(): return "print-env" diff --git a/tests/conftest.py b/tests/conftest.py index 9eaebd4..375f57d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ import os import shutil import pytest -from utils import FIXTURES_DIR, TOX4, modify_config, drop_unsupported_pythons +from utils import FIXTURES_DIR, TOX_VERSION, TOX4, modify_config, drop_unsupported_pythons @pytest.fixture(autouse=True) @@ -41,3 +41,16 @@ def print_deps_stdout_arg(request): def print_extras_stdout_arg(request): """Argument for printing extras to stdout""" return request.param + + +@pytest.fixture +def dependency_groups_support(): + """Support for dependency groups""" + if (TOX_VERSION.major, TOX_VERSION.minor) < (4, 22): + raise pytest.skip(reason="requires tox 4.22 or higher") + + +@pytest.fixture(params=("--print-dependency-groups-to-file=-", "--print-dependency-groups-to=-")) +def print_dependency_groups_stdout_arg(request, dependency_groups_support): + """Argument for printing dependency groups to stdout""" + return request.param diff --git a/tests/fixtures/pyproject.toml b/tests/fixtures/pyproject.toml index fed528d..007ed43 100644 --- a/tests/fixtures/pyproject.toml +++ b/tests/fixtures/pyproject.toml @@ -1,3 +1,6 @@ [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" + +[dependency-groups] +dg1 = ["build>=1"] diff --git a/tests/fixtures/tox.ini b/tests/fixtures/tox.ini index 66b7a70..7279724 100644 --- a/tests/fixtures/tox.ini +++ b/tests/fixtures/tox.ini @@ -11,6 +11,8 @@ deps = extras = dev full +dependency_groups = + dg1 commands = python -c 'import os, sys; print(os.path.realpath(sys.exec_prefix), "is the exec_prefix")' # we explicitly clear this because the inner tox does not need to know diff --git a/tests/test_integration_tox4.py b/tests/test_integration_tox4.py index f62348d..964b047 100644 --- a/tests/test_integration_tox4.py +++ b/tests/test_integration_tox4.py @@ -138,6 +138,30 @@ def test_print_extras(toxenv, print_extras_stdout_arg): ) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) +def test_print_dependency_groups(toxenv, print_dependency_groups_stdout_arg): + result = tox("-e", toxenv, print_dependency_groups_stdout_arg) + expected = textwrap.dedent( + f""" + dg1 + {tox_footer(toxenv)} + """ + ).lstrip() + assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted( + expected.splitlines() + ) + + +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) +def test_print_dependency_groups_empty(projdir, toxenv, print_dependency_groups_stdout_arg): + with modify_config(projdir / 'tox.ini') as config: + del config["testenv"]["dependency_groups"] + result = tox("-e", toxenv, print_dependency_groups_stdout_arg) + expected = [l.strip() for l in tox_footer(toxenv).splitlines() if l.strip()] + got = [l.strip() for l in prep_tox_output(result.stdout).splitlines() if l.strip()] + assert got == expected + + @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) @pytest.mark.parametrize("pre_post", ["pre", "post", "both"]) def test_print_extras_with_commands_pre_post(projdir, toxenv, pre_post, print_extras_stdout_arg): @@ -183,6 +207,17 @@ def test_allenvs_print_extras(print_extras_stdout_arg): assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted(expected) +def test_allenvs_print_dependency_groups(print_dependency_groups_stdout_arg): + result = tox(print_dependency_groups_stdout_arg) + expected = [] + for env in envs_from_tox_ini(): + expected.extend(("dg1", f"{env}: OK")) + expected.pop() # The last "py310: OK" is not there + expected.append(tox_footer(spaces=0)) + expected = ("\n".join(expected)).splitlines() + assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted(expected) + + @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_to_file(toxenv, tmp_path): depspath = tmp_path / "deps" @@ -203,6 +238,15 @@ def test_print_extras_to_file(toxenv, tmp_path): assert prep_tox_output(result.stdout) == expected +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) +def test_print_dependency_groups_to_file(toxenv, tmp_path, dependency_groups_support): + groupspath = tmp_path / "dependency_groups" + result = tox("-e", toxenv, "--print-dependency-groups-to", str(groupspath)) + assert sorted(groupspath.read_text().splitlines()) == ["dg1"] + expected = tox_footer(toxenv, spaces=0) + "\n" + assert prep_tox_output(result.stdout) == expected + + @pytest.mark.parametrize("option", ("--print-deps-to", "--print-deps-to-file")) def test_allenvs_print_deps_to_file(tmp_path, option): depspath = tmp_path / "deps" @@ -231,6 +275,20 @@ def test_allenvs_print_extras_to_file(tmp_path, option): assert prep_tox_output(result.stdout) == expected +@pytest.mark.parametrize("option", ("--print-dependency-groups-to", "--print-dependency-groups-to-file")) +def test_allenvs_print_dependency_groups_to_file(tmp_path, option, dependency_groups_support): + groupspath = tmp_path / "dependency_groups" + result = tox(option, str(groupspath)) + assert sorted(groupspath.read_text().splitlines()) == ( + ["dg1"] * len(envs_from_tox_ini()) + ) + expected = "" + for env in envs_from_tox_ini()[:-1]: + expected += f"{env}: OK\n" + expected += tox_footer(spaces=0) + "\n" + assert prep_tox_output(result.stdout) == expected + + def test_allenvs_print_deps_to_existing_file(tmp_path): depspath = tmp_path / "deps" depspath.write_text("nada") @@ -251,26 +309,28 @@ def test_allenvs_print_extras_to_existing_file(tmp_path): assert "full" in lines +def test_allenvs_print_dependency_groups_to_existing_file(tmp_path, dependency_groups_support): + groupspath = tmp_path / "dependency_groups" + groupspath.write_text("nada") + _ = tox("--print-dependency-groups-to", str(groupspath)) + lines = groupspath.read_text().splitlines() + assert "nada" not in lines + assert "dg1" in lines + + @pytest.mark.parametrize("deps_stdout", [True, False]) @pytest.mark.parametrize("extras_stdout", [True, False]) def test_allenvs_print_deps_to_file_print_extras_to_other_file( tmp_path, deps_stdout, extras_stdout ): if deps_stdout and extras_stdout: - pytest.xfail("Unsupported combination of parameters") + pytest.skip("Unsupported combination of parameters") depspath = "-" if deps_stdout else tmp_path / "deps" extraspath = "-" if extras_stdout else tmp_path / "extras" result = tox("--print-deps-to", str(depspath), "--print-extras-to", str(extraspath)) - if deps_stdout: - depslines = result.stdout.splitlines() - extraslines = extraspath.read_text().splitlines() - elif extras_stdout: - depslines = depspath.read_text().splitlines() - extraslines = result.stdout.splitlines() - else: - extraslines = extraspath.read_text().splitlines() - depslines = depspath.read_text().splitlines() + depslines = result.stdout.splitlines() if deps_stdout else depspath.read_text().splitlines() + extraslines = result.stdout.splitlines() if extras_stdout else extraspath.read_text().splitlines() assert "six" in depslines assert "py" in depslines @@ -283,6 +343,45 @@ def test_allenvs_print_deps_to_file_print_extras_to_other_file( assert "dev" not in depslines +@pytest.mark.parametrize("deps_stdout", [True, False]) +@pytest.mark.parametrize("extras_stdout", [True, False]) +@pytest.mark.parametrize("dependency_groups_stdout", [True, False]) +def test_allenvs_print_deps_to_file_print_extras_to_other_file_print_dependency_groups_to_other_file( + tmp_path, deps_stdout, extras_stdout, dependency_groups_stdout, dependency_groups_support +): + if deps_stdout + extras_stdout + dependency_groups_stdout > 1: + pytest.skip("Unsupported combination of parameters") + + depspath = "-" if deps_stdout else tmp_path / "deps" + extraspath = "-" if extras_stdout else tmp_path / "extras" + groupspath = "-" if dependency_groups_stdout else tmp_path / "dependency_groups" + result = tox("--print-deps-to", str(depspath), + "--print-extras-to", str(extraspath), + "--print-dependency-groups-to", str(groupspath)) + depslines = result.stdout.splitlines() if deps_stdout else depspath.read_text().splitlines() + extraslines = result.stdout.splitlines() if extras_stdout else extraspath.read_text().splitlines() + groupslines = result.stdout.splitlines() if dependency_groups_stdout else groupspath.read_text().splitlines() + + assert "six" in depslines + assert "py" in depslines + assert "full" in extraslines + assert "dev" in extraslines + assert "dg1" in groupslines + + assert "six" not in extraslines + assert "py" not in extraslines + assert "dg1" not in extraslines + + assert "full" not in depslines + assert "dev" not in depslines + assert "dg1" not in depslines + + assert "six" not in groupslines + assert "py" not in groupslines + assert "full" not in groupslines + assert "dev" not in groupslines + + def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): depsextraspath = tmp_path / "depsextras" result = tox( @@ -298,6 +397,36 @@ def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): assert "cannot be identical" in result.stderr +def test_print_deps_dependency_groups_to_same_file_is_not_possible(tmp_path, dependency_groups_support): + depsgroupspath = tmp_path / "depsgroups" + result = tox( + "-e", + NATIVE_TOXENV, + "--print-deps-to", + str(depsgroupspath), + "--print-dependency-groups-to", + str(depsgroupspath), + check=False, + ) + assert result.returncode > 0 + assert "cannot be identical" in result.stderr + + +def test_print_extras_dependency_groups_to_same_file_is_not_possible(tmp_path, dependency_groups_support): + extrasgroupspath = tmp_path / "extrasgroups" + result = tox( + "-e", + NATIVE_TOXENV, + "--print-extras-to", + str(extrasgroupspath), + "--print-dependency-groups-to", + str(extrasgroupspath), + check=False, + ) + assert result.returncode > 0 + assert "cannot be identical" in result.stderr + + def test_print_deps_extras_to_stdout_is_not_possible( tmp_path, print_deps_stdout_arg, @@ -314,6 +443,38 @@ def test_print_deps_extras_to_stdout_is_not_possible( assert "cannot be identical" in result.stderr +def test_print_deps_dependency_groups_to_stdout_is_not_possible( + tmp_path, + print_deps_stdout_arg, + print_dependency_groups_stdout_arg, +): + result = tox( + "-e", + NATIVE_TOXENV, + print_deps_stdout_arg, + print_dependency_groups_stdout_arg, + check=False, + ) + assert result.returncode > 0 + assert "cannot be identical" in result.stderr + + +def test_print_extras_dependency_groups_to_stdout_is_not_possible( + tmp_path, + print_extras_stdout_arg, + print_dependency_groups_stdout_arg, +): + result = tox( + "-e", + NATIVE_TOXENV, + print_extras_stdout_arg, + print_dependency_groups_stdout_arg, + check=False, + ) + assert result.returncode > 0 + assert "cannot be identical" in result.stderr + + @needs_all_pythons def test_regular_run(): result = tox()