From 6dd32b21ce04856a7e5cc6af7f4be95aa92709a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Fri, 11 Sep 2020 15:20:03 +0200 Subject: [PATCH] Add the --print-extras-to(-file) option --- README.rst | 44 ++++++++-- src/tox_current_env/hooks.py | 47 +++++++++-- tests/fixtures/setup.py | 8 +- tests/fixtures/tox.ini | 3 + tests/test_integration.py | 156 +++++++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 6242866..1721ca5 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ tox-current-env `tox `_ plugin to run tests in current Python environment --------------------------------------------------------------------------------------- -The ``tox-current-env`` plugin adds two options: +The ``tox-current-env`` plugin adds these options: ``tox --current-env`` Runs the tox testenv's ``commands`` in the current Python environment @@ -15,11 +15,21 @@ The ``tox-current-env`` plugin adds two options: (if ``tox`` is invoked from an Python 3.7 environment, any non 3.7 testenv will fail). ``tox --print-deps-to=FILE`` - Instead of running any ``commands``, - simply prints the declared dependencies in ``deps`` to the specified ``FILE``. - This is useful for preparing the current environment for the above. + Instead of running any ``commands``, simply prints the + `declared dependencies `_ + in ``deps`` to the specified ``FILE``. + This is useful for preparing the current environment for ``tox --current-env``. Use ``-`` for ``FILE`` to print to standard output. +``tox --print-extras-to=FILE`` + Instead of running any ``commands``, simply prints the names of the + `declared extras `_ + in ``extras`` to the specified ``FILE``. + 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. + 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. @@ -82,7 +92,10 @@ and ``pip``-installing locally: Usage ----- -When the plugin is installed, use ``tox`` with ``--current-env`` or ``--print-deps-to`` and all the other options as usual. Assuming your ``tox`` is installed on Python 3.7: +When the plugin is installed, +use ``tox`` with ``--current-env``, ``--print-deps-to`` or ``--print-extras-to`` +and all the other options as usual. +Assuming your ``tox`` is installed on Python 3.7: .. code-block:: console @@ -125,6 +138,21 @@ To get list of test dependencies, run: py37: commands succeeded congratulations :) +To get a list of names of extras, run: + +.. code-block:: console + + $ tox -e py37 --print-extras-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' + extra1 + extra2 + ... + ___________________________________ summary ____________________________________ + py37: commands succeeded + congratulations :) + Caveats, warnings and limitations --------------------------------- @@ -159,8 +187,8 @@ Don't mix current-env and regular tox runs Tox caches the virtualenvs it creates, and doesn't distinguish between regular virtualenvs and ``--current-env``. -Don't mix ``tox --current-env`` or ``tox --print-deps-to`` runs -and regular ``tox`` runs (without the flag). +Don't mix ``tox --current-env``, ``tox --print-deps-to`` or ``tox --print-extras-to`` +runs and regular ``tox`` runs (without the flags provided by this plugin). If you ever need to do this, use tox's ``--recreate/-r`` flag to clear the cache. The plugin should abort with a meaningful error message if this is detected, @@ -183,7 +211,7 @@ Read `the documentation for passing environment variables to tox Other limitations and known bugs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``installed:`` line in the output of ``tox --print-deps-to`` shows irrelevant output +The ``installed:`` line in the output of ``tox --print-deps-to``/``tox --print-extras-to`` shows irrelevant output (based on the content of the real or faked virtual environment). Regardless of any `Python flags `_ used in the shebang of ``tox``, diff --git a/src/tox_current_env/hooks.py b/src/tox_current_env/hooks.py index 3ea4f33..c59aef7 100644 --- a/src/tox_current_env/hooks.py +++ b/src/tox_current_env/hooks.py @@ -38,6 +38,16 @@ def tox_addoption(parser): help="Don't run tests, only print the dependencies to the given file " + "(use `-` for stdout)", ) + parser.add_argument( + "--print-extras-to", + "--print-extras-to-file", + action="store", + type=argparse.FileType('w'), + metavar="FILE", + default=None, + help="Don't run tests, only print the names of the required extras to the given file " + + "(use `-` for stdout)", + ) @tox.hookimpl @@ -55,11 +65,17 @@ def tox_configure(config): "--print-deps-only cannot be used together " + "with --print-deps-to" ) - if config.option.current_env or config.option.print_deps_to: + if config.option.current_env or config.option.print_deps_to or config.option.print_extras_to: config.skipsdist = True for testenv in config.envconfigs: config.envconfigs[testenv].whitelist_externals = "*" + if (getattr(config.option.print_deps_to, "name", object()) == + getattr(config.option.print_extras_to, "name", object())): + raise tox.exception.ConfigError( + "The paths given to --print-deps-to and --print-extras-to cannot be identical." + ) + return config @@ -97,16 +113,17 @@ def rm_venv(venv): def unsupported_raise(config, venv): if config.option.recreate: return - regular = not (config.option.current_env or config.option.print_deps_to) + regular = not (config.option.current_env or config.option.print_deps_to or config.option.print_extras_to) if regular and is_current_env_link(venv): if hasattr(tox.hookspecs, "tox_cleanup"): raise tox.exception.ConfigError( - "Looks like previous --current-env or --print-deps-to tox run didn't finish the cleanup. " + "Looks like previous --current-env, --print-deps-to or --print-extras-to tox run didn't finish the cleanup. " "Run tox run with --recreate (-r) or manually remove the environment in .tox." ) else: raise tox.exception.ConfigError( - "Regular tox run after --current-env or --print-deps-to tox run is not supported without --recreate (-r)." + "Regular tox run after --current-env, --print-deps-to or --print-extras-to tox run " + "is not supported without --recreate (-r)." ) elif config.option.current_env and is_proper_venv(venv): raise tox.exception.ConfigError( @@ -119,7 +136,7 @@ def tox_testenv_create(venv, action): """We create a fake virtualenv with just the symbolic link""" config = venv.envconfig.config create_fake_env = check_version = config.option.current_env - if config.option.print_deps_to: + if config.option.print_deps_to or config.option.print_extras_to: if is_any_env(venv): # We don't need anything return True @@ -130,7 +147,7 @@ def tox_testenv_create(venv, action): # because it's cheaper, faster and won't install stuff create_fake_env = True if check_version: - # With real --current-env, we check this, but not with --print-deps-to only + # With real --current-env, we check this, but not with --print-deps/extras-to only version_info = venv.envconfig.python_info.version_info if version_info is None: raise tox.exception.InterpreterNotFound(venv.envconfig.basepython) @@ -178,16 +195,30 @@ def tox_testenv_install_deps(venv, action): @tox.hookimpl def tox_runtest(venv, redirect): - """If --print-deps-to, prints deps instead of running tests""" + """If --print-deps-to, prints deps instead of running tests. + If --print-extras-to, prints extras instead of running tests. + Both options can be used together.""" config = venv.envconfig.config unsupported_raise(config, venv) + ret = None + if config.option.print_deps_to: print( *venv.get_resolved_dependencies(), sep="\n", file=config.option.print_deps_to, ) - return True + ret = True + + if config.option.print_extras_to: + print( + *venv.envconfig.extras, + sep="\n", + file=config.option.print_extras_to, + ) + ret = True + + return ret @tox.hookimpl diff --git a/tests/fixtures/setup.py b/tests/fixtures/setup.py index 4115873..1b58b71 100644 --- a/tests/fixtures/setup.py +++ b/tests/fixtures/setup.py @@ -1,3 +1,9 @@ from setuptools import setup -setup(name="test") +setup( + name="test", + extras_require={ + "dev": [], + "full": [], + } +) diff --git a/tests/fixtures/tox.ini b/tests/fixtures/tox.ini index 56979da..87b16cd 100644 --- a/tests/fixtures/tox.ini +++ b/tests/fixtures/tox.ini @@ -5,5 +5,8 @@ envlist = py36,py37,py38,py39 deps = six py +extras = + dev + full commands = python -c 'import os, sys; print(os.path.realpath(sys.exec_prefix), "is the exec_prefix")' diff --git a/tests/test_integration.py b/tests/test_integration.py index 213b450..5428d70 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -36,6 +36,12 @@ def print_deps_stdout_arg(request): return request.param +@pytest.fixture(params=('--print-extras-to-file=-', '--print-extras-to=-')) +def print_extras_stdout_arg(request): + """Argument for printing extras to stdout""" + return request.param + + def tox(*args, quiet=True, **kwargs): kwargs.setdefault("encoding", "utf-8") kwargs.setdefault("stdout", subprocess.PIPE) @@ -121,6 +127,21 @@ def test_print_deps(toxenv, print_deps_stdout_arg): assert result.stdout == expected +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +def test_print_extras(toxenv, print_extras_stdout_arg): + result = tox("-e", toxenv, print_extras_stdout_arg) + expected = textwrap.dedent( + f""" + dev + full + ___________________________________ summary ____________________________________ + {toxenv}: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + @pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) def test_print_deps_only_deprecated(toxenv): result = tox( @@ -157,6 +178,29 @@ def test_allenvs_print_deps(print_deps_stdout_arg): assert result.stdout == expected +def test_allenvs_print_extras(print_extras_stdout_arg): + result = tox(print_extras_stdout_arg) + expected = textwrap.dedent( + """ + dev + full + dev + full + dev + full + dev + full + ___________________________________ summary ____________________________________ + py36: commands succeeded + py37: commands succeeded + py38: commands succeeded + py39: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + @pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) def test_print_deps_to_file(toxenv, tmp_path): depspath = tmp_path / "deps" @@ -172,6 +216,21 @@ def test_print_deps_to_file(toxenv, tmp_path): assert result.stdout == expected +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +def test_print_extras_to_file(toxenv, tmp_path): + extraspath = tmp_path / "extras" + result = tox("-e", toxenv, "--print-extras-to", str(extraspath)) + assert extraspath.read_text().splitlines() == ["dev", "full"] + expected = textwrap.dedent( + f""" + ___________________________________ summary ____________________________________ + {toxenv}: commands succeeded + congratulations :) + """ + ).lstrip() + assert 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" @@ -190,6 +249,24 @@ def test_allenvs_print_deps_to_file(tmp_path, option): assert result.stdout == expected +@pytest.mark.parametrize('option', ('--print-extras-to', '--print-extras-to-file')) +def test_allenvs_print_extras_to_file(tmp_path, option): + extraspath = tmp_path / "extras" + result = tox(option, str(extraspath)) + assert extraspath.read_text().splitlines() == ["dev", "full"] * 4 + expected = textwrap.dedent( + """ + ___________________________________ summary ____________________________________ + py36: commands succeeded + py37: commands succeeded + py38: commands succeeded + py39: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + def test_allenvs_print_deps_to_existing_file(tmp_path): depspath = tmp_path / "deps" depspath.write_text("nada") @@ -200,6 +277,75 @@ def test_allenvs_print_deps_to_existing_file(tmp_path): assert "py" in lines +def test_allenvs_print_extras_to_existing_file(tmp_path): + extraspath = tmp_path / "extras" + extraspath.write_text("nada") + result = tox("--print-extras-to", str(extraspath)) + lines = extraspath.read_text().splitlines() + assert "nada" not in lines + assert "dev" in lines + assert "full" 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") + + 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() + + assert "six" in depslines + assert "py" in depslines + assert "full" in extraslines + assert "dev" in extraslines + + assert "six" not in extraslines + assert "py" not in extraslines + assert "full" not in depslines + assert "dev" not in depslines + + +def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): + depsextraspath = tmp_path / "depsextras" + result = tox( + "-e", + NATIVE_TOXENV, + "--print-deps-to", str(depsextraspath), + "--print-extras-to", str(depsextraspath), + 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, + print_extras_stdout_arg,): + result = tox( + "-e", + NATIVE_TOXENV, + print_deps_stdout_arg, + print_extras_stdout_arg, + check=False, + ) + assert result.returncode > 0 + assert "cannot be identical" in result.stderr + + def test_print_deps_only_print_deps_to_file_are_mutually_exclusive(): result = tox( "-e", @@ -319,6 +465,16 @@ def test_current_after_print_deps(print_deps_stdout_arg): assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG +def test_current_after_print_extras(print_extras_stdout_arg): + # this is quite fast, so we can do it several times + for _ in range(3): + result = tox("-e", NATIVE_TOXENV, print_extras_stdout_arg) + assert "bin/python" not in result.stdout + assert "full" in result.stdout + result = tox("-re", NATIVE_TOXENV, "--current-env") + assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG + + def test_regular_recreate_after_print_deps(print_deps_stdout_arg): result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) assert "bin/python" not in result.stdout