From f55beb7537c988e8f4ccc02c26a3328303e10ccf Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 8 Nov 2021 14:20:07 +0100 Subject: [PATCH 01/27] Rename module hooks to hooks3 --- src/tox_current_env/{hooks.py => hooks3.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/tox_current_env/{hooks.py => hooks3.py} (100%) diff --git a/src/tox_current_env/hooks.py b/src/tox_current_env/hooks3.py similarity index 100% rename from src/tox_current_env/hooks.py rename to src/tox_current_env/hooks3.py From a223dc5e970bd984322477d45ac86a2f3173826a Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 8 Nov 2021 14:21:14 +0100 Subject: [PATCH 02/27] New plugin version for tox 4 alpha --- src/tox_current_env/hooks.py | 6 + src/tox_current_env/hooks4.py | 227 ++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 src/tox_current_env/hooks.py create mode 100644 src/tox_current_env/hooks4.py diff --git a/src/tox_current_env/hooks.py b/src/tox_current_env/hooks.py new file mode 100644 index 0000000..96bc8be --- /dev/null +++ b/src/tox_current_env/hooks.py @@ -0,0 +1,6 @@ +from tox import __version__ as TOX_VERSION + +if TOX_VERSION[0] == "4": + from tox_current_env.hooks4 import * +else: + from tox_current_env.hooks3 import * diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py new file mode 100644 index 0000000..8e45556 --- /dev/null +++ b/src/tox_current_env/hooks4.py @@ -0,0 +1,227 @@ +import argparse +import platform +import sys +import warnings +from pathlib import Path +from typing import Set + +from tox.execute.api import Execute +from tox.execute.local_sub_process import LocalSubProcessExecuteInstance +from tox.plugin import impl +from tox.report import HandledError +from tox.tox_env.python.api import PythonInfo +from tox.tox_env.python.runner import PythonRun + + +@impl +def tox_register_tox_env(register): + register.add_run_env(CurrentEnv) + register.add_run_env(PrintEnv) + + +@impl +def tox_add_option(parser): + parser.add_argument( + "--current-env", + action="store_true", + dest="current_env", + default=False, + help="Run tests in current environment, not creating any virtual environment", + ) + parser.add_argument( + "--print-deps-only", + action="store_true", + dest="print_deps_only", + default=False, + help="Deprecated, equivalent to `--print-deps-to -`", + ) + parser.add_argument( + "--print-deps-to", + "--print-deps-to-file", + action="store", + type=argparse.FileType("w"), + metavar="FILE", + default=False, + 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=False, + help="Don't run tests, only print the names of the required extras to the given file " + + "(use `-` for stdout)", + ) + + +@impl +def tox_add_core_config(core_conf, config): + if config.options.current_env: + config.options.default_runner = "current-env" + return + + if config.options.print_deps_only: + warnings.warn( + "--print-deps-only is deprecated; use `--print-deps-to -`", + DeprecationWarning, + ) + if not config.options.print_deps_to: + config.options.print_deps_to = sys.stdout + else: + raise RuntimeError( + "--print-deps-only cannot be used together " "with --print-deps-to" + ) + + if getattr(config.options.print_deps_to, "name", object()) == getattr( + config.options.print_extras_to, "name", object() + ): + raise RuntimeError( + "The paths given to --print-deps-to and --print-extras-to cannot be identical." + ) + + if config.options.print_deps_to or config.options.print_extras_to: + config.options.default_runner = "print-env" + return + + # No options used - switch back to the standard runner + # Workaround for: https://github.com/tox-dev/tox/issues/2264 + config.options.default_runner = "virtualenv" + + +class CurrentEnv(PythonRun): + def __init__(self, create_args): + self._executor = None + self._installer = None + self._path = [] + super().__init__(create_args) + + @staticmethod + def id(): + return "current-env" + + @property + def _default_package_tox_env_type(self): + return None + + @property + def _external_pkg_tox_env_type(self): + return None + + @property + def _package_tox_env_type(self): + return None + + @property + def executor(self): + if self._executor is None: + self._executor = CurrentEnvRunExecutor(self.options.is_colored) + return self._executor + + def _get_python(self, base_python): + # TODO: Improve version check and error message + version_nodot = "".join(str(p) for p in sys.version_info[:2]) + base_python = base_python[0] + if not base_python.endswith(version_nodot): + raise HandledError( + "Python version mismatch. " + f"Current version: {sys.version_info[:2]}, " + f"Requested environment: {base_python}" + ) + + return PythonInfo( + implementation=sys.implementation, + version_info=sys.version_info, + version=sys.version, + is_64=(platform.architecture()[0] == "64bit"), + platform=platform.platform(), + extra={"executable": Path(sys.executable)}, + ) + + def create_python_env(self): + return None + + def env_bin_dir(self): + return Path(sys.prefix) / "bin" + + def env_python(self): + return sys.executable + + def env_site_package_dir(self): + return Path(sys.prefix) / "lib" + + @property + def installer(self): + if self._installer is None: + self._installer = DummyInstaller() + return self._installer + + def prepend_env_var_path(self): + return [self.env_bin_dir()] + + @property + def runs_on_platform(self): + return sys.platform + + +class CurrentEnvRunExecutor(Execute): + def build_instance( + self, + request, + options, + out, + err, + ): + # Disable check for the external commands, + # all of them are external for us. + request.allow = None + return LocalSubProcessExecuteInstance(request, options, out, err) + + +class DummyInstaller: + def install(self, *args): + return + + +class PrintEnv(CurrentEnv): + def __init__(self, create_args): + super().__init__(create_args) + + # As soon as this environment has enough info to do its job, + # do it and nothing more. + + if self.options.print_deps_to: + print( + *self.core["requires"], + *self.conf["deps"].lines(), + sep="\n", + file=self.options.print_deps_to, + ) + self.options.print_deps_to.flush() + + if self.options.print_extras_to: + if "extras" not in self.conf: + # Unfortunately, if there is skipsdist/no_package or skip_install + # in the config, this section is not parsed at all so we have to + # do it here manually to be able to read its content. + self.conf.add_config( + keys=["extras"], + of_type=Set[str], + default=set(), + desc="extras to install of the target package", + ) + print( + *self.conf["extras"], + sep="\n", + file=self.options.print_extras_to, + ) + self.options.print_extras_to.flush() + + # We are done + sys.exit(0) + + @staticmethod + def id(): + return "print-env" From bb79c6d40594ee89f84c58d3e1ef16ec9f129296 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 8 Nov 2021 14:35:20 +0100 Subject: [PATCH 03/27] Remove deprecated option --print-deps-only --- src/tox_current_env/hooks3.py | 2 +- src/tox_current_env/hooks4.py | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/tox_current_env/hooks3.py b/src/tox_current_env/hooks3.py index d52b680..62e956b 100644 --- a/src/tox_current_env/hooks3.py +++ b/src/tox_current_env/hooks3.py @@ -26,7 +26,7 @@ def tox_addoption(parser): action="store_true", dest="print_deps_only", default=False, - help="Deprecated, equivalent to `--print-deps-to -`", + help="Deprecated, equivalent to `--print-deps-to -`. Not available with tox 4.", ) parser.add_argument( "--print-deps-to", diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 8e45556..fd5f33b 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -28,13 +28,6 @@ def tox_add_option(parser): default=False, help="Run tests in current environment, not creating any virtual environment", ) - parser.add_argument( - "--print-deps-only", - action="store_true", - dest="print_deps_only", - default=False, - help="Deprecated, equivalent to `--print-deps-to -`", - ) parser.add_argument( "--print-deps-to", "--print-deps-to-file", @@ -63,18 +56,6 @@ def tox_add_core_config(core_conf, config): config.options.default_runner = "current-env" return - if config.options.print_deps_only: - warnings.warn( - "--print-deps-only is deprecated; use `--print-deps-to -`", - DeprecationWarning, - ) - if not config.options.print_deps_to: - config.options.print_deps_to = sys.stdout - else: - raise RuntimeError( - "--print-deps-only cannot be used together " "with --print-deps-to" - ) - if getattr(config.options.print_deps_to, "name", object()) == getattr( config.options.print_extras_to, "name", object() ): From 5d3802da93516daf70599ca88683f0fb18bde085 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 8 Nov 2021 15:17:15 +0100 Subject: [PATCH 04/27] Use sysconfig instead of hardcoded paths --- src/tox_current_env/hooks4.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index fd5f33b..543432d 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -1,6 +1,7 @@ import argparse import platform import sys +import sysconfig import warnings from pathlib import Path from typing import Set @@ -125,13 +126,13 @@ def create_python_env(self): return None def env_bin_dir(self): - return Path(sys.prefix) / "bin" + return Path(sysconfig.get_path("scripts")) def env_python(self): return sys.executable def env_site_package_dir(self): - return Path(sys.prefix) / "lib" + return Path(sysconfig.get_path("purelib")) @property def installer(self): From 46fd9f086514cfacc12b81a345b69c7d825fc86e Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 8 Nov 2021 15:26:00 +0100 Subject: [PATCH 05/27] Use the default installer (pip) even it fails in an offline env --- src/tox_current_env/hooks4.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 543432d..2618652 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -11,6 +11,7 @@ from tox.plugin import impl from tox.report import HandledError from tox.tox_env.python.api import PythonInfo +from tox.tox_env.python.pip.pip_install import Pip from tox.tox_env.python.runner import PythonRun @@ -137,7 +138,7 @@ def env_site_package_dir(self): @property def installer(self): if self._installer is None: - self._installer = DummyInstaller() + self._installer = Pip(self) return self._installer def prepend_env_var_path(self): @@ -162,11 +163,6 @@ def build_instance( return LocalSubProcessExecuteInstance(request, options, out, err) -class DummyInstaller: - def install(self, *args): - return - - class PrintEnv(CurrentEnv): def __init__(self, create_args): super().__init__(create_args) From d6cb357233813dbbc4a8b6bfb283c127a60920b2 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 13 Dec 2021 09:20:38 +0100 Subject: [PATCH 06/27] Own executor is not needed if we can override config --- src/tox_current_env/hooks4.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 2618652..8c78637 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -6,8 +6,9 @@ from pathlib import Path from typing import Set +from tox.config.loader.memory import MemoryLoader from tox.execute.api import Execute -from tox.execute.local_sub_process import LocalSubProcessExecuteInstance +from tox.execute.local_sub_process import LocalSubProcessExecutor from tox.plugin import impl from tox.report import HandledError from tox.tox_env.python.api import PythonInfo @@ -74,6 +75,14 @@ def tox_add_core_config(core_conf, config): config.options.default_runner = "virtualenv" +@impl +def tox_add_env_config(env_conf, config): + # This allows all external commands. + # All of them are extenal for us. + loader = MemoryLoader(allowlist_externals=["*"]) + config.core.loaders.insert(0, loader) + + class CurrentEnv(PythonRun): def __init__(self, create_args): self._executor = None @@ -100,7 +109,7 @@ def _package_tox_env_type(self): @property def executor(self): if self._executor is None: - self._executor = CurrentEnvRunExecutor(self.options.is_colored) + self._executor = LocalSubProcessExecutor(self.options.is_colored) return self._executor def _get_python(self, base_python): @@ -149,20 +158,6 @@ def runs_on_platform(self): return sys.platform -class CurrentEnvRunExecutor(Execute): - def build_instance( - self, - request, - options, - out, - err, - ): - # Disable check for the external commands, - # all of them are external for us. - request.allow = None - return LocalSubProcessExecuteInstance(request, options, out, err) - - class PrintEnv(CurrentEnv): def __init__(self, create_args): super().__init__(create_args) From ba4146c10d59fdb5e5a91aa8186d3067d949f6e3 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 13 Dec 2021 12:20:02 +0100 Subject: [PATCH 07/27] Empty list of commands means no need for SystemExit --- src/tox_current_env/hooks4.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 8c78637..c11e90e 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -79,8 +79,13 @@ def tox_add_core_config(core_conf, config): def tox_add_env_config(env_conf, config): # This allows all external commands. # All of them are extenal for us. - loader = MemoryLoader(allowlist_externals=["*"]) - config.core.loaders.insert(0, loader) + allow_external_cmds = MemoryLoader(allowlist_externals=["*"]) + config.core.loaders.insert(0, allow_external_cmds) + # For print-deps-to and print-extras-to, use empty + # list of commands. + if config.options.print_deps_to or config.options.print_extras_to: + empty_commands = MemoryLoader(commands=[]) + env_conf.loaders.insert(0, empty_commands) class CurrentEnv(PythonRun): @@ -192,9 +197,6 @@ def __init__(self, create_args): ) self.options.print_extras_to.flush() - # We are done - sys.exit(0) - @staticmethod def id(): return "print-env" From 07e8536934f6fba6eeda41c14b273b3a8fe0c6b6 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Tue, 14 Dec 2021 23:22:14 +0100 Subject: [PATCH 08/27] Add dummy installer so there is no chance to install packages --- src/tox_current_env/hooks4.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index c11e90e..21e0f7f 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -88,6 +88,13 @@ def tox_add_env_config(env_conf, config): env_conf.loaders.insert(0, empty_commands) +class Installer: + """Noop installer""" + + def install(self, *args, **kwargs): + return None + + class CurrentEnv(PythonRun): def __init__(self, create_args): self._executor = None @@ -151,9 +158,7 @@ def env_site_package_dir(self): @property def installer(self): - if self._installer is None: - self._installer = Pip(self) - return self._installer + return Installer() def prepend_env_var_path(self): return [self.env_bin_dir()] From 799e5ddda2c64ef00ec32840b7839e76421054ba Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Tue, 14 Dec 2021 23:22:56 +0100 Subject: [PATCH 09/27] No longer check Python versions --- src/tox_current_env/hooks4.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 21e0f7f..c2cd700 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -125,16 +125,6 @@ def executor(self): return self._executor def _get_python(self, base_python): - # TODO: Improve version check and error message - version_nodot = "".join(str(p) for p in sys.version_info[:2]) - base_python = base_python[0] - if not base_python.endswith(version_nodot): - raise HandledError( - "Python version mismatch. " - f"Current version: {sys.version_info[:2]}, " - f"Requested environment: {base_python}" - ) - return PythonInfo( implementation=sys.implementation, version_info=sys.version_info, From c5281c6243f09027e306e064404099127664bd4c Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Tue, 14 Dec 2021 23:23:41 +0100 Subject: [PATCH 10/27] Some more default options --- src/tox_current_env/hooks4.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index c2cd700..5f60705 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -55,34 +55,45 @@ def tox_add_option(parser): @impl def tox_add_core_config(core_conf, config): - if config.options.current_env: - config.options.default_runner = "current-env" + opt = config.options + + if opt.current_env or opt.print_deps_to or opt.print_extras_to: + # We do not want to install the main package. + # no_package is the same as skipsdist. + loader = MemoryLoader(no_package=True) + core_conf.loaders.insert(0, loader) + + if opt.current_env: + opt.default_runner = "current-env" return - if getattr(config.options.print_deps_to, "name", object()) == getattr( - config.options.print_extras_to, "name", object() + if getattr(opt.print_deps_to, "name", object()) == getattr( + opt.print_extras_to, "name", object() ): raise RuntimeError( "The paths given to --print-deps-to and --print-extras-to cannot be identical." ) - if config.options.print_deps_to or config.options.print_extras_to: - config.options.default_runner = "print-env" + if opt.print_deps_to or opt.print_extras_to: + opt.default_runner = "print-env" return # No options used - switch back to the standard runner # Workaround for: https://github.com/tox-dev/tox/issues/2264 - config.options.default_runner = "virtualenv" + opt.default_runner = "virtualenv" @impl def tox_add_env_config(env_conf, config): # This allows all external commands. # All of them are extenal for us. - allow_external_cmds = MemoryLoader(allowlist_externals=["*"]) - config.core.loaders.insert(0, allow_external_cmds) + # passenv is here because `TOX_TESTENV_PASSENV` + # no longer works in tox 4. + if config.options.current_env: + allow_external_cmds = MemoryLoader(allowlist_externals=["*"], passenv=["*"]) + env_conf.loaders.insert(0, allow_external_cmds) # For print-deps-to and print-extras-to, use empty - # list of commands. + # list of commands so the tox does nothing. if config.options.print_deps_to or config.options.print_extras_to: empty_commands = MemoryLoader(commands=[]) env_conf.loaders.insert(0, empty_commands) From 70a344a8f916a58dcd35a04edb74a2c73213bc95 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Tue, 14 Dec 2021 23:24:00 +0100 Subject: [PATCH 11/27] Fake Python environment --- src/tox_current_env/hooks4.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 5f60705..258f27b 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -1,7 +1,9 @@ import argparse +import os import platform import sys import sysconfig +import tempfile import warnings from pathlib import Path from typing import Set @@ -146,7 +148,19 @@ def _get_python(self, base_python): ) def create_python_env(self): - return None + # Fake Python environment just to make sure all possible + # commands like python or python3 works. + self.tempdir = tempfile.TemporaryDirectory() + for suffix in ( + "", + f"{sys.version_info.major}", + f"{sys.version_info.major}.{sys.version_info.minor}", + ): + os.symlink(sys.executable, Path(self.tempdir.name) / f"python{suffix}") + os.environ["PATH"] = ":".join((os.environ["PATH"], str(self.tempdir.name))) + + def _teardown(self): + del self.tempdir def env_bin_dir(self): return Path(sysconfig.get_path("scripts")) From dadd0a954b765875710b9ffc5948f3600b7d9808 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 3 Jan 2022 12:52:59 +0100 Subject: [PATCH 12/27] Use our own subprocess executor to alter env variables --- src/tox_current_env/hooks4.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 258f27b..a55c6f3 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -10,7 +10,10 @@ from tox.config.loader.memory import MemoryLoader from tox.execute.api import Execute -from tox.execute.local_sub_process import LocalSubProcessExecutor +from tox.execute.local_sub_process import ( + Execute, + LocalSubProcessExecuteInstance, +) from tox.plugin import impl from tox.report import HandledError from tox.tox_env.python.api import PythonInfo @@ -108,11 +111,28 @@ def install(self, *args, **kwargs): return None +class CurrentEnvLocalSubProcessExecutor(Execute): + def __init__(self, *args, **kwargs): + self.tempdir = kwargs.pop("tempdir") + super().__init__(*args, **kwargs) + + def build_instance( + self, + request, + options, + out, + err, + ): + request.env["PATH"] = ":".join((str(self.tempdir.name), request.env.get("PATH", ""))) + return LocalSubProcessExecuteInstance(request, options, out, err) + + class CurrentEnv(PythonRun): def __init__(self, create_args): self._executor = None self._installer = None self._path = [] + self.tempdir = tempfile.TemporaryDirectory() super().__init__(create_args) @staticmethod @@ -134,7 +154,7 @@ def _package_tox_env_type(self): @property def executor(self): if self._executor is None: - self._executor = LocalSubProcessExecutor(self.options.is_colored) + self._executor = CurrentEnvLocalSubProcessExecutor(self.options.is_colored, tempdir=self.tempdir) return self._executor def _get_python(self, base_python): @@ -150,14 +170,12 @@ def _get_python(self, base_python): def create_python_env(self): # Fake Python environment just to make sure all possible # commands like python or python3 works. - self.tempdir = tempfile.TemporaryDirectory() for suffix in ( "", f"{sys.version_info.major}", f"{sys.version_info.major}.{sys.version_info.minor}", ): os.symlink(sys.executable, Path(self.tempdir.name) / f"python{suffix}") - os.environ["PATH"] = ":".join((os.environ["PATH"], str(self.tempdir.name))) def _teardown(self): del self.tempdir From 14fd3c24c2f354584089419dcd039d8d487c6ebd Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 7 Feb 2022 14:24:47 +0100 Subject: [PATCH 13/27] Compatibility with Tox 4 beta 1 --- src/tox_current_env/hooks4.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index a55c6f3..7e4dddf 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -59,8 +59,8 @@ def tox_add_option(parser): @impl -def tox_add_core_config(core_conf, config): - opt = config.options +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: # We do not want to install the main package. @@ -89,17 +89,18 @@ def tox_add_core_config(core_conf, config): @impl -def tox_add_env_config(env_conf, config): +def tox_add_env_config(env_conf, state): + opt = state.conf.options # This allows all external commands. # All of them are extenal for us. # passenv is here because `TOX_TESTENV_PASSENV` # no longer works in tox 4. - if config.options.current_env: + if opt.current_env: allow_external_cmds = MemoryLoader(allowlist_externals=["*"], passenv=["*"]) 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 config.options.print_deps_to or config.options.print_extras_to: + if opt.print_deps_to or opt.print_extras_to: empty_commands = MemoryLoader(commands=[]) env_conf.loaders.insert(0, empty_commands) From 0e90086c0f148e9195cc0b4b68e17016908ab9eb Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Tue, 8 Feb 2022 12:10:01 +0100 Subject: [PATCH 14/27] Make __init__ methods noop and use env_dir instead of a temp dir --- src/tox_current_env/hooks4.py | 55 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 7e4dddf..6bf601d 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -113,10 +113,6 @@ def install(self, *args, **kwargs): class CurrentEnvLocalSubProcessExecutor(Execute): - def __init__(self, *args, **kwargs): - self.tempdir = kwargs.pop("tempdir") - super().__init__(*args, **kwargs) - def build_instance( self, request, @@ -124,7 +120,9 @@ def build_instance( out, err, ): - request.env["PATH"] = ":".join((str(self.tempdir.name), request.env.get("PATH", ""))) + request.env["PATH"] = ":".join( + (str(options._env.env_dir / "bin"), request.env.get("PATH", "")) + ) return LocalSubProcessExecuteInstance(request, options, out, err) @@ -133,7 +131,6 @@ def __init__(self, create_args): self._executor = None self._installer = None self._path = [] - self.tempdir = tempfile.TemporaryDirectory() super().__init__(create_args) @staticmethod @@ -155,7 +152,7 @@ def _package_tox_env_type(self): @property def executor(self): if self._executor is None: - self._executor = CurrentEnvLocalSubProcessExecutor(self.options.is_colored, tempdir=self.tempdir) + self._executor = CurrentEnvLocalSubProcessExecutor(self.options.is_colored) return self._executor def _get_python(self, base_python): @@ -169,17 +166,19 @@ def _get_python(self, base_python): ) def create_python_env(self): - # Fake Python environment just to make sure all possible - # commands like python or python3 works. + """Fake Python environment just to make sure all possible + commands like python or python3 works.""" + bindir = self.env_dir / "bin" + if not bindir.exists(): + os.mkdir(bindir) for suffix in ( "", f"{sys.version_info.major}", f"{sys.version_info.major}.{sys.version_info.minor}", ): - os.symlink(sys.executable, Path(self.tempdir.name) / f"python{suffix}") - - def _teardown(self): - del self.tempdir + symlink = bindir / f"python{suffix}" + if not symlink.exists(): + os.symlink(sys.executable, symlink) def env_bin_dir(self): return Path(sysconfig.get_path("scripts")) @@ -206,18 +205,6 @@ class PrintEnv(CurrentEnv): def __init__(self, create_args): super().__init__(create_args) - # As soon as this environment has enough info to do its job, - # do it and nothing more. - - if self.options.print_deps_to: - print( - *self.core["requires"], - *self.conf["deps"].lines(), - sep="\n", - file=self.options.print_deps_to, - ) - self.options.print_deps_to.flush() - if self.options.print_extras_to: if "extras" not in self.conf: # Unfortunately, if there is skipsdist/no_package or skip_install @@ -229,6 +216,24 @@ def __init__(self, create_args): default=set(), desc="extras to install of the target package", ) + + def create_python_env(self): + """We don't need any environment for this plugin""" + return None + + def prepend_env_var_path(self): + """Usage of this method for the core of this plugin is far from perfect + but this method is called every time even without recreated environment""" + if self.options.print_deps_to: + print( + *self.core["requires"], + *self.conf["deps"].lines(), + sep="\n", + file=self.options.print_deps_to, + ) + self.options.print_deps_to.flush() + + if self.options.print_extras_to: print( *self.conf["extras"], sep="\n", From b9030ccc0f892b91e5be67116a37f2fb415eb289 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Wed, 9 Feb 2022 11:17:30 +0100 Subject: [PATCH 15/27] Copy of the tests for Tox 4 --- tests/test_integration_tox4.py | 661 +++++++++++++++++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 tests/test_integration_tox4.py diff --git a/tests/test_integration_tox4.py b/tests/test_integration_tox4.py new file mode 100644 index 0000000..7660e5d --- /dev/null +++ b/tests/test_integration_tox4.py @@ -0,0 +1,661 @@ +import functools +import os +import pathlib +import re +import shutil +import subprocess +import sys +import textwrap +import warnings +import configparser +import contextlib + +from packaging.version import parse as ver + +import pytest + + +NATIVE_TOXENV = f"py{sys.version_info[0]}{sys.version_info[1]}" +NATIVE_SITE_PACKAGES = f"lib/python{sys.version_info[0]}.{sys.version_info[1]}/site-packages" +NATIVE_EXECUTABLE = str(pathlib.Path(sys.executable).resolve()) +FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" +DOT_TOX = pathlib.Path("./.tox") + + +def _exec_prefix(executable): + """Returns sys.exec_prefix for the given executable""" + cmd = (executable, "-c", "import sys; print(sys.exec_prefix)") + return subprocess.check_output(cmd, encoding="utf-8").strip() + + +NATIVE_EXEC_PREFIX = _exec_prefix(NATIVE_EXECUTABLE) +NATIVE_EXEC_PREFIX_MSG = f"{NATIVE_EXEC_PREFIX} is the exec_prefix" + + +@pytest.fixture(autouse=True) +def projdir(tmp_path, monkeypatch): + pwd = tmp_path / "projdir" + pwd.mkdir() + for fname in "tox.ini", "setup.py": + shutil.copy(FIXTURES_DIR / fname, pwd) + monkeypatch.chdir(pwd) + return pwd + + +@pytest.fixture(params=('--print-deps-only', '--print-deps-to-file=-', '--print-deps-to=-')) +def print_deps_stdout_arg(request): + """Argument for printing deps to stdout""" + 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 + + +@contextlib.contextmanager +def modify_config(tox_ini_path): + """Context manager that allows modifying the given Tox config file + + A statement like:: + + with prepare_config(projdir) as config: + + will make `config` a ConfigParser instance that is saved at the end + of the `with` block. + """ + config = configparser.ConfigParser() + config.read(tox_ini_path) + yield config + with open(tox_ini_path, 'w') as tox_ini_file: + config.write(tox_ini_file) + + +def tox(*args, quiet=True, **kwargs): + kwargs.setdefault("encoding", "utf-8") + kwargs.setdefault("stdout", subprocess.PIPE) + kwargs.setdefault("stderr", subprocess.PIPE) + kwargs.setdefault("check", True) + q = ("-q",) if quiet else () + try: + cp = subprocess.run((sys.executable, "-m", "tox") + q + args, **kwargs) + except subprocess.CalledProcessError as e: + print(e.stdout, file=sys.stdout) + print(e.stderr, file=sys.stderr) + raise + print(cp.stdout, file=sys.stdout) + print(cp.stderr, file=sys.stderr) + return cp + + +TOX_VERSION = ver(tox("--version").stdout.split(" ")[0]) + + +@functools.lru_cache(maxsize=8) +def is_available(python): + try: + subprocess.run((python, "--version")) + except FileNotFoundError: + return False + return True + + +needs_py367891011 = pytest.mark.skipif( + not all((is_available(f"python3.{x}") for x in range(6, 12))), + reason="This test needs python3.6, 3.7, 3.8, 3.9, 3.10 and 3.11 available in $PATH", +) + + + +def test_native_toxenv_current_env(): + result = tox("-e", NATIVE_TOXENV, "--current-env") + assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG + assert not (DOT_TOX / NATIVE_TOXENV / "lib").is_dir() + + +@needs_py367891011 +def test_all_toxenv_current_env(): + result = tox("--current-env", check=False) + assert NATIVE_EXEC_PREFIX_MSG in result.stdout.splitlines() + assert result.stdout.count("InterpreterMismatch:") >= 2 + assert result.returncode > 0 + + +@pytest.mark.parametrize("python", ["python3.4", "python3.5"]) +def test_missing_toxenv_current_env(python): + if is_available(python): + pytest.skip(f"Only works if {python} is not available in $PATH") + env = python.replace("python", "py").replace(".", "") + result = tox("-e", env, "--current-env", check=False) + assert f"InterpreterNotFound: {python}" in result.stdout + assert "Traceback" not in (result.stderr + result.stdout) + assert result.returncode > 0 + + +@needs_py367891011 +def test_all_toxenv_current_env_skip_missing(): + result = tox("--current-env", "--skip-missing-interpreters", check=False) + assert "InterpreterMismatch:" in result.stdout + assert "congratulations" in result.stdout + assert result.returncode == 0 + + +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +def test_print_deps(toxenv, print_deps_stdout_arg): + result = tox("-e", toxenv, print_deps_stdout_arg) + expected = textwrap.dedent( + f""" + six + py + ___________________________________ summary ____________________________________ + {toxenv}: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): + with modify_config(projdir / 'tox.ini') as config: + config["tox"]["minversion"] = "3.13" + result = tox("-e", toxenv, print_deps_stdout_arg) + expected = textwrap.dedent( + f""" + tox >= 3.13 + six + py + ___________________________________ summary ____________________________________ + {toxenv}: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + +@pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): + with modify_config(projdir / 'tox.ini') as config: + config["tox"]["requires"] = "\n setuptools > 30\n pluggy" + result = tox("-e", toxenv, print_deps_stdout_arg) + expected = textwrap.dedent( + f""" + setuptools > 30 + pluggy + six + py + ___________________________________ summary ____________________________________ + {toxenv}: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + +@pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +def test_print_deps_with_tox_minversion_and_requires(projdir, toxenv, print_deps_stdout_arg): + with modify_config(projdir / 'tox.ini') as config: + config["tox"]["minversion"] = "3.13" + config["tox"]["requires"] = "\n setuptools > 30\n pluggy" + result = tox("-e", toxenv, print_deps_stdout_arg) + expected = textwrap.dedent( + f""" + tox >= 3.13 + setuptools > 30 + pluggy + six + py + ___________________________________ summary ____________________________________ + {toxenv}: commands succeeded + congratulations :) + """ + ).lstrip() + 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( + "-e", toxenv, '--print-deps-only', + env={**os.environ, 'PYTHONWARNINGS': 'always'}, + ) + waring_text = ( + "DeprecationWarning: --print-deps-only is deprecated; " + + "use `--print-deps-to -`" + ) + assert waring_text in result.stderr + + +def test_allenvs_print_deps(print_deps_stdout_arg): + result = tox(print_deps_stdout_arg) + expected = textwrap.dedent( + """ + six + py + six + py + six + py + six + py + six + py + six + py + ___________________________________ summary ____________________________________ + py36: commands succeeded + py37: commands succeeded + py38: commands succeeded + py39: commands succeeded + py310: commands succeeded + py311: commands succeeded + congratulations :) + """ + ).lstrip() + 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 + dev + full + dev + full + ___________________________________ summary ____________________________________ + py36: commands succeeded + py37: commands succeeded + py38: commands succeeded + py39: commands succeeded + py310: commands succeeded + py311: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39", "py310", "py311"]) +def test_print_deps_to_file(toxenv, tmp_path): + depspath = tmp_path / "deps" + result = tox("-e", toxenv, "--print-deps-to", str(depspath)) + assert depspath.read_text().splitlines() == ["six", "py"] + expected = textwrap.dedent( + f""" + ___________________________________ summary ____________________________________ + {toxenv}: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + +@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39", "py310", "py311"]) +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" + result = tox(option, str(depspath)) + assert depspath.read_text().splitlines() == ["six", "py"] * 6 + expected = textwrap.dedent( + """ + ___________________________________ summary ____________________________________ + py36: commands succeeded + py37: commands succeeded + py38: commands succeeded + py39: commands succeeded + py310: commands succeeded + py311: commands succeeded + congratulations :) + """ + ).lstrip() + 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"] * 6 + expected = textwrap.dedent( + """ + ___________________________________ summary ____________________________________ + py36: commands succeeded + py37: commands succeeded + py38: commands succeeded + py39: commands succeeded + py310: commands succeeded + py311: 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") + result = tox("--print-deps-to", str(depspath)) + lines = depspath.read_text().splitlines() + assert "nada" not in lines + assert "six" in lines + 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", + NATIVE_TOXENV, + "--print-deps-only", + "--print-deps-to", + "foobar", + check=False, + ) + assert result.returncode > 0 + assert "cannot be used together" in result.stderr + + +@needs_py367891011 +def test_regular_run(): + result = tox() + lines = result.stdout.splitlines()[:6] + assert "/.tox/py36 is the exec_prefix" in lines[0] + assert "/.tox/py37 is the exec_prefix" in lines[1] + assert "/.tox/py38 is the exec_prefix" in lines[2] + assert "/.tox/py39 is the exec_prefix" in lines[3] + assert "/.tox/py310 is the exec_prefix" in lines[4] + assert "/.tox/py311 is the exec_prefix" in lines[5] + assert "congratulations" in result.stdout + for y in 6, 7, 8, 9, 10, 11: + for pkg in "py", "six", "test": + sitelib = DOT_TOX / f"py3{y}/lib/python3.{y}/site-packages" + assert sitelib.is_dir() + assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 + + +def test_regular_run_native_toxenv(): + result = tox("-e", NATIVE_TOXENV) + lines = sorted(result.stdout.splitlines()[:1]) + assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] + assert "congratulations" in result.stdout + for pkg in "py", "six", "test": + sitelib = ( + DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" + ) + assert sitelib.is_dir() + assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 + + +def test_regular_after_current_is_supported(): + result = tox("-e", NATIVE_TOXENV, "--current-env") + assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG + result = tox("-e", NATIVE_TOXENV) + assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout + assert "--recreate" not in result.stderr + + +def test_regular_after_killed_current_is_not_supported(): + # fake broken tox run + shutil.rmtree(DOT_TOX, ignore_errors=True) + (DOT_TOX / NATIVE_TOXENV / "bin").mkdir(parents=True) + (DOT_TOX / NATIVE_TOXENV / "bin" / "python").symlink_to(NATIVE_EXECUTABLE) + + result = tox("-e", NATIVE_TOXENV, check=False) + assert result.returncode > 0 + assert "--recreate" in result.stderr + + +def test_regular_after_first_print_deps_is_supported(print_deps_stdout_arg): + result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) + assert result.stdout.splitlines()[0] == "six" + result = tox("-e", NATIVE_TOXENV) + lines = sorted(result.stdout.splitlines()[:1]) + assert "--recreate" not in result.stderr + assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] + + # check that "test" was not installed to current environment + shutil.rmtree("./test.egg-info") + pip_freeze = subprocess.run( + (sys.executable, "-m", "pip", "freeze"), + encoding="utf-8", + stdout=subprocess.PIPE, + ).stdout.splitlines() + # XXX when this fails, recreate your current environment + assert "test==0.0.0" not in pip_freeze + + +def test_regular_recreate_after_current(): + result = tox("-e", NATIVE_TOXENV, "--current-env") + assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG + result = tox("-re", NATIVE_TOXENV) + assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout + assert "not supported" not in result.stderr + assert "--recreate" not in result.stderr + + +def test_current_after_regular_is_not_supported(): + result = tox("-e", NATIVE_TOXENV) + assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout + result = tox("-e", NATIVE_TOXENV, "--current-env", check=False) + assert result.returncode > 0 + assert "not supported" in result.stderr + + +def test_current_recreate_after_regular(): + result = tox("-e", NATIVE_TOXENV) + assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout + result = tox("-re", NATIVE_TOXENV, "--current-env") + assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG + + +def test_current_after_print_deps(print_deps_stdout_arg): + # this is quite fast, so we can do it several times + for _ in range(3): + result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) + assert "bin/python" not in result.stdout + assert "six" in result.stdout + result = tox("-re", NATIVE_TOXENV, "--current-env") + 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 + assert "six" in result.stdout + + result = tox("-re", NATIVE_TOXENV) + assert result.stdout.splitlines()[0] != NATIVE_EXEC_PREFIX_MSG + sitelib = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" + assert sitelib.is_dir() + assert len(list(sitelib.glob("test-*.dist-info"))) == 1 + + result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) + assert "bin/python" not in result.stdout + assert "six" in result.stdout + + +def test_print_deps_without_python_command(tmp_path, print_deps_stdout_arg): + bin = tmp_path / "bin" + bin.mkdir() + tox_link = bin / "tox" + tox_path = shutil.which("tox") + tox_link.symlink_to(tox_path) + env = {**os.environ, "PATH": str(bin)} + + result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg, env=env) + expected = textwrap.dedent( + f""" + six + py + ___________________________________ summary ____________________________________ + {NATIVE_TOXENV}: commands succeeded + congratulations :) + """ + ).lstrip() + assert result.stdout == expected + + +@pytest.mark.parametrize("flag", [None, "--print-deps-to=-", "--current-env"]) +def test_noquiet_installed_packages(flag): + flags = (flag,) if flag else () + result = tox("-e", NATIVE_TOXENV, *flags, quiet=False, check=False) + assert f"\n{NATIVE_TOXENV} installed: " in result.stdout + for line in result.stdout.splitlines(): + if line.startswith(f"{NATIVE_TOXENV} installed: "): + packages = line.rpartition(" installed: ")[-1].split(",") + break + + # default tox produces output sorted by package names + assert packages == sorted( + packages, key=lambda p: p.partition("==")[0].partition(" @ ")[0].lower() + ) + + # without a flag, the output must match tox defaults + if not flag: + assert len(packages) == 3 + assert packages[0].startswith("py==") + assert packages[1].startswith("six==") + assert packages[2].startswith(("test==", "test @ ")) # old and new pip + + # with our flags, uses the absolutely current environment by default, hence has tox + else: + assert len([p for p in packages if p.startswith("tox==")]) == 1 + assert all(re.match(r"\S+==\S+", p) for p in packages) + + +@pytest.mark.parametrize("flag", ["--print-deps-to=-", "--print-extras-to=-", "--current-env"]) +@pytest.mark.parametrize("usedevelop", [True, False]) +def test_self_is_not_installed(projdir, flag, usedevelop): + with modify_config(projdir / 'tox.ini') as config: + config['testenv']['usedevelop'] = str(usedevelop) + result = tox("-e", NATIVE_TOXENV, flag, quiet=False) + assert 'test==0.0.0' not in result.stdout + assert 'test @ file://' not in result.stdout + + +@pytest.mark.parametrize("usedevelop", [True, False]) +def test_self_is_installed_with_regular_tox(projdir, usedevelop): + with modify_config(projdir / 'tox.ini') as config: + config['testenv']['usedevelop'] = str(usedevelop) + result = tox("-e", NATIVE_TOXENV, quiet=False) + assert ('test==0.0.0' in result.stdout or + 'test @ file://' in result.stdout) From b0923ba7ea856fcd794c541a14b5e185bd57089f Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Thu, 10 Feb 2022 10:58:59 +0100 Subject: [PATCH 16/27] Improved tests and testing with tox 4 --- .github/workflows/main.yaml | 5 +- README.rst | 11 + setup.py | 2 +- tests/conftest.py | 36 ++ tests/test_integration.py | 327 ++++++------------- tests/test_integration_tox4.py | 578 ++++++++------------------------- tests/utils.py | 131 ++++++++ tox.ini | 6 +- 8 files changed, 412 insertions(+), 684 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/utils.py diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3cb28ed..a48da34 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -37,6 +37,9 @@ jobs: - py310-toxrelease - py310-toxmaster - py310-tox315 - + - py37-tox4 + - py38-tox4 + - py39-tox4 + - py310-tox4 # Use GitHub's Linux Docker host runs-on: ubuntu-latest diff --git a/README.rst b/README.rst index 2ccec46..35a85e0 100644 --- a/README.rst +++ b/README.rst @@ -157,6 +157,17 @@ To get a list of names of extras, run: Caveats, warnings and limitations --------------------------------- +tox 4 +~~~~~ + +The plugin is available also for tox 4. Differences in behavior between tox 3 and 4 are these: + +- ``--recreate`` is no longer needed when you switch from the plugin back to standard tox. Tox +detects it and handles the recreation automatically. +- 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. + Use an isolated environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/setup.py b/setup.py index a976155..9f18b8f 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def long_description(): packages=find_packages("src"), entry_points={"tox": ["current-env = tox_current_env.hooks"]}, install_requires=[ - "tox>=3.15,<4", + "tox>=3.15", "importlib_metadata; python_version < '3.8'" ], extras_require={ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b33178f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,36 @@ +import shutil + +import pytest +from utils import FIXTURES_DIR, TOX4 + + +@pytest.fixture(autouse=True) +def projdir(tmp_path, monkeypatch): + pwd = tmp_path / "projdir" + pwd.mkdir() + for fname in "tox.ini", "setup.py": + shutil.copy(FIXTURES_DIR / fname, pwd) + monkeypatch.chdir(pwd) + return pwd + + +if TOX4: + available_options = ("--print-deps-to-file=-", "--print-deps-to=-") +else: + available_options = ( + "--print-deps-only", + "--print-deps-to-file=-", + "--print-deps-to=-", + ) + + +@pytest.fixture(params=available_options) +def print_deps_stdout_arg(request): + """Argument for printing deps to stdout""" + 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 diff --git a/tests/test_integration.py b/tests/test_integration.py index f802403..5dcf3ba 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,120 +1,36 @@ -import functools import os -import pathlib import re import shutil import subprocess import sys import textwrap -import warnings -import configparser -import contextlib - -from packaging.version import parse as ver import pytest +from packaging.version import parse as ver - -NATIVE_TOXENV = f"py{sys.version_info[0]}{sys.version_info[1]}" -NATIVE_SITE_PACKAGES = f"lib/python{sys.version_info[0]}.{sys.version_info[1]}/site-packages" -NATIVE_EXECUTABLE = str(pathlib.Path(sys.executable).resolve()) -FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" -DOT_TOX = pathlib.Path("./.tox") - - -def _exec_prefix(executable): - """Returns sys.exec_prefix for the given executable""" - cmd = (executable, "-c", "import sys; print(sys.exec_prefix)") - return subprocess.check_output(cmd, encoding="utf-8").strip() - - -NATIVE_EXEC_PREFIX = _exec_prefix(NATIVE_EXECUTABLE) -NATIVE_EXEC_PREFIX_MSG = f"{NATIVE_EXEC_PREFIX} is the exec_prefix" - - -@pytest.fixture(autouse=True) -def projdir(tmp_path, monkeypatch): - pwd = tmp_path / "projdir" - pwd.mkdir() - for fname in "tox.ini", "setup.py": - shutil.copy(FIXTURES_DIR / fname, pwd) - monkeypatch.chdir(pwd) - return pwd - - -@pytest.fixture(params=('--print-deps-only', '--print-deps-to-file=-', '--print-deps-to=-')) -def print_deps_stdout_arg(request): - """Argument for printing deps to stdout""" - 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 - - -@contextlib.contextmanager -def modify_config(tox_ini_path): - """Context manager that allows modifying the given tox config file - - A statement like:: - - with prepare_config(projdir) as config: - - will make `config` a ConfigParser instance that is saved at the end - of the `with` block. - """ - config = configparser.ConfigParser() - config.read(tox_ini_path) - yield config - with open(tox_ini_path, 'w') as tox_ini_file: - config.write(tox_ini_file) - - -def tox(*args, quiet=True, **kwargs): - kwargs.setdefault("encoding", "utf-8") - kwargs.setdefault("stdout", subprocess.PIPE) - kwargs.setdefault("stderr", subprocess.PIPE) - kwargs.setdefault("check", True) - q = ("-q",) if quiet else () - try: - cp = subprocess.run((sys.executable, "-m", "tox") + q + args, **kwargs) - except subprocess.CalledProcessError as e: - print(e.stdout, file=sys.stdout) - print(e.stderr, file=sys.stderr) - raise - print(cp.stdout, file=sys.stdout) - print(cp.stderr, file=sys.stderr) - return cp - - -TOX_VERSION = ver(tox("--version").stdout.split(" ")[0]) - - -@functools.lru_cache(maxsize=8) -def is_available(python): - try: - subprocess.run((python, "--version")) - except FileNotFoundError: - return False - return True - - -needs_py3678910 = pytest.mark.skipif( - not all((is_available(f"python3.{x}") for x in range(6, 12))), - reason="This test needs python3.6, 3.7, 3.8, 3.9 and 3.10 available in $PATH", +from utils import ( + DOT_TOX, + NATIVE_EXEC_PREFIX_MSG, + NATIVE_EXECUTABLE, + NATIVE_SITE_PACKAGES, + NATIVE_TOXENV, + TOX_VERSION, + envs_from_tox_ini, + is_available, + modify_config, + needs_all_pythons, + tox, + tox_footer, ) - def test_native_toxenv_current_env(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG assert not (DOT_TOX / NATIVE_TOXENV / "lib").is_dir() -@needs_py3678910 +@needs_all_pythons def test_all_toxenv_current_env(): result = tox("--current-env", check=False) assert NATIVE_EXEC_PREFIX_MSG in result.stdout.splitlines() @@ -133,7 +49,7 @@ def test_missing_toxenv_current_env(python): assert result.returncode > 0 -@needs_py3678910 +@needs_all_pythons def test_all_toxenv_current_env_skip_missing(): result = tox("--current-env", "--skip-missing-interpreters", check=False) assert "InterpreterMismatch:" in result.stdout @@ -141,22 +57,20 @@ def test_all_toxenv_current_env_skip_missing(): assert result.returncode == 0 -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps(toxenv, print_deps_stdout_arg): result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) @pytest.mark.parametrize("pre_post", ["pre", "post", "both"]) def test_print_deps_with_commands_pre_post(projdir, toxenv, pre_post, print_deps_stdout_arg): with modify_config(projdir / 'tox.ini') as config: @@ -170,18 +84,16 @@ def test_print_deps_with_commands_pre_post(projdir, toxenv, pre_post, print_deps f""" six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected assert result.stderr == "" -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): - with modify_config(projdir / 'tox.ini') as config: + with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( @@ -189,18 +101,16 @@ def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): tox >= 3.13 six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): - with modify_config(projdir / 'tox.ini') as config: + with modify_config(projdir / "tox.ini") as config: config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( @@ -209,18 +119,18 @@ def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): pluggy six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) -def test_print_deps_with_tox_minversion_and_requires(projdir, toxenv, print_deps_stdout_arg): - with modify_config(projdir / 'tox.ini') as config: +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) +def test_print_deps_with_tox_minversion_and_requires( + projdir, toxenv, print_deps_stdout_arg +): + with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) @@ -231,30 +141,26 @@ def test_print_deps_with_tox_minversion_and_requires(projdir, toxenv, print_deps pluggy six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) 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 :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@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): with modify_config(projdir / 'tox.ini') as config: @@ -268,20 +174,20 @@ def test_print_extras_with_commands_pre_post(projdir, toxenv, pre_post, print_ex f""" dev full - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected assert result.stderr == "" -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_only_deprecated(toxenv): result = tox( - "-e", toxenv, '--print-deps-only', - env={**os.environ, 'PYTHONWARNINGS': 'always'}, + "-e", + toxenv, + "--print-deps-only", + env={**os.environ, "PYTHONWARNINGS": "always"}, ) waring_text = ( "DeprecationWarning: --print-deps-only is deprecated; " @@ -292,119 +198,69 @@ def test_print_deps_only_deprecated(toxenv): def test_allenvs_print_deps(print_deps_stdout_arg): result = tox(print_deps_stdout_arg) - expected = textwrap.dedent( - """ - six - py - six - py - six - py - six - py - six - py - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - congratulations :) - """ - ).lstrip() + expected = "" + for env in envs_from_tox_ini(): + expected += "six\npy\n" + expected += tox_footer(spaces=0) + "\n" 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 - dev - full - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - congratulations :) - """ - ).lstrip() + expected = "" + for env in envs_from_tox_ini(): + expected += "dev\nfull\n" + expected += tox_footer(spaces=0) + "\n" assert result.stdout == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39", "py310"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_to_file(toxenv, tmp_path): depspath = tmp_path / "deps" result = tox("-e", toxenv, "--print-deps-to", str(depspath)) assert depspath.read_text().splitlines() == ["six", "py"] expected = textwrap.dedent( f""" - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39", "py310"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) 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 :) + {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected -@pytest.mark.parametrize('option', ('--print-deps-to', '--print-deps-to-file')) +@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" result = tox(option, str(depspath)) assert depspath.read_text().splitlines() == ["six", "py"] * 5 expected = textwrap.dedent( - """ - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - congratulations :) + f""" + {tox_footer()} """ ).lstrip() assert result.stdout == expected -@pytest.mark.parametrize('option', ('--print-extras-to', '--print-extras-to-file')) +@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"] * 5 expected = textwrap.dedent( - """ - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - congratulations :) + f""" + {tox_footer()} """ ).lstrip() assert result.stdout == expected @@ -413,7 +269,7 @@ def test_allenvs_print_extras_to_file(tmp_path, option): def test_allenvs_print_deps_to_existing_file(tmp_path): depspath = tmp_path / "deps" depspath.write_text("nada") - result = tox("--print-deps-to", str(depspath)) + _ = tox("--print-deps-to", str(depspath)) lines = depspath.read_text().splitlines() assert "nada" not in lines assert "six" in lines @@ -423,7 +279,7 @@ def test_allenvs_print_deps_to_existing_file(tmp_path): 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)) + _ = tox("--print-extras-to", str(extraspath)) lines = extraspath.read_text().splitlines() assert "nada" not in lines assert "dev" in lines @@ -432,14 +288,15 @@ def test_allenvs_print_extras_to_existing_file(tmp_path): @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): +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)) + result = tox("--print-deps-to", str(depspath), "--print-extras-to", str(extraspath)) if deps_stdout: depslines = result.stdout.splitlines() extraslines = extraspath.read_text().splitlines() @@ -466,8 +323,10 @@ def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): result = tox( "-e", NATIVE_TOXENV, - "--print-deps-to", str(depsextraspath), - "--print-extras-to", str(depsextraspath), + "--print-deps-to", + str(depsextraspath), + "--print-extras-to", + str(depsextraspath), check=False, ) assert result.returncode > 0 @@ -475,9 +334,10 @@ def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): def test_print_deps_extras_to_stdout_is_not_possible( - tmp_path, - print_deps_stdout_arg, - print_extras_stdout_arg,): + tmp_path, + print_deps_stdout_arg, + print_extras_stdout_arg, +): result = tox( "-e", NATIVE_TOXENV, @@ -502,19 +362,17 @@ def test_print_deps_only_print_deps_to_file_are_mutually_exclusive(): assert "cannot be used together" in result.stderr -@needs_py3678910 +@needs_all_pythons def test_regular_run(): result = tox() lines = result.stdout.splitlines()[:5] - assert "/.tox/py36 is the exec_prefix" in lines[0] - assert "/.tox/py37 is the exec_prefix" in lines[1] - assert "/.tox/py38 is the exec_prefix" in lines[2] - assert "/.tox/py39 is the exec_prefix" in lines[3] - assert "/.tox/py310 is the exec_prefix" in lines[4] + for line, env in zip(lines, envs_from_tox_ini()): + assert f"/.tox/{env} is the exec_prefix" in line assert "congratulations" in result.stdout - for y in 6, 7, 8, 9, 10: + for env in envs_from_tox_ini(): + major, minor = re.match(r"py(\d)(\d+)", env).groups() for pkg in "py", "six", "test": - sitelib = DOT_TOX / f"py3{y}/lib/python3.{y}/site-packages" + sitelib = DOT_TOX / f"{env}/lib/python{major}.{minor}/site-packages" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 @@ -525,9 +383,7 @@ def test_regular_run_native_toxenv(): assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] assert "congratulations" in result.stdout for pkg in "py", "six", "test": - sitelib = ( - DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" - ) + sitelib = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 @@ -643,9 +499,7 @@ def test_print_deps_without_python_command(tmp_path, print_deps_stdout_arg): f""" six py - ___________________________________ summary ____________________________________ - {NATIVE_TOXENV}: commands succeeded - congratulations :) + {tox_footer(NATIVE_TOXENV)} """ ).lstrip() assert result.stdout == expected @@ -679,14 +533,16 @@ def test_noquiet_installed_packages(flag): assert all(re.match(r"\S+==\S+", p) for p in packages) -@pytest.mark.parametrize("flag", ["--print-deps-to=-", "--print-extras-to=-", "--current-env"]) +@pytest.mark.parametrize( + "flag", ["--print-deps-to=-", "--print-extras-to=-", "--current-env"] +) @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_not_installed(projdir, flag, usedevelop): - with modify_config(projdir / 'tox.ini') as config: - config['testenv']['usedevelop'] = str(usedevelop) + with modify_config(projdir / "tox.ini") as config: + config["testenv"]["usedevelop"] = str(usedevelop) result = tox("-e", NATIVE_TOXENV, flag, quiet=False) - assert 'test==0.0.0' not in result.stdout - assert 'test @ file://' not in result.stdout + assert "test==0.0.0" not in result.stdout + assert "test @ file://" not in result.stdout @pytest.mark.parametrize("externals", [None, "allowlist_externals", "whitelist_externals"]) @@ -705,8 +561,7 @@ def test_externals(projdir, externals): @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_installed_with_regular_tox(projdir, usedevelop): - with modify_config(projdir / 'tox.ini') as config: - config['testenv']['usedevelop'] = str(usedevelop) + with modify_config(projdir / "tox.ini") as config: + config["testenv"]["usedevelop"] = str(usedevelop) result = tox("-e", NATIVE_TOXENV, quiet=False) - assert ('test==0.0.0' in result.stdout or - 'test @ file://' in result.stdout) + assert "test==0.0.0" in result.stdout or "test @ file://" in result.stdout diff --git a/tests/test_integration_tox4.py b/tests/test_integration_tox4.py index 7660e5d..ccca154 100644 --- a/tests/test_integration_tox4.py +++ b/tests/test_integration_tox4.py @@ -1,381 +1,192 @@ -import functools import os -import pathlib import re import shutil -import subprocess -import sys import textwrap -import warnings -import configparser -import contextlib - -from packaging.version import parse as ver import pytest +from packaging.version import parse as ver - -NATIVE_TOXENV = f"py{sys.version_info[0]}{sys.version_info[1]}" -NATIVE_SITE_PACKAGES = f"lib/python{sys.version_info[0]}.{sys.version_info[1]}/site-packages" -NATIVE_EXECUTABLE = str(pathlib.Path(sys.executable).resolve()) -FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" -DOT_TOX = pathlib.Path("./.tox") - - -def _exec_prefix(executable): - """Returns sys.exec_prefix for the given executable""" - cmd = (executable, "-c", "import sys; print(sys.exec_prefix)") - return subprocess.check_output(cmd, encoding="utf-8").strip() - - -NATIVE_EXEC_PREFIX = _exec_prefix(NATIVE_EXECUTABLE) -NATIVE_EXEC_PREFIX_MSG = f"{NATIVE_EXEC_PREFIX} is the exec_prefix" - - -@pytest.fixture(autouse=True) -def projdir(tmp_path, monkeypatch): - pwd = tmp_path / "projdir" - pwd.mkdir() - for fname in "tox.ini", "setup.py": - shutil.copy(FIXTURES_DIR / fname, pwd) - monkeypatch.chdir(pwd) - return pwd - - -@pytest.fixture(params=('--print-deps-only', '--print-deps-to-file=-', '--print-deps-to=-')) -def print_deps_stdout_arg(request): - """Argument for printing deps to stdout""" - 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 - - -@contextlib.contextmanager -def modify_config(tox_ini_path): - """Context manager that allows modifying the given Tox config file - - A statement like:: - - with prepare_config(projdir) as config: - - will make `config` a ConfigParser instance that is saved at the end - of the `with` block. - """ - config = configparser.ConfigParser() - config.read(tox_ini_path) - yield config - with open(tox_ini_path, 'w') as tox_ini_file: - config.write(tox_ini_file) - - -def tox(*args, quiet=True, **kwargs): - kwargs.setdefault("encoding", "utf-8") - kwargs.setdefault("stdout", subprocess.PIPE) - kwargs.setdefault("stderr", subprocess.PIPE) - kwargs.setdefault("check", True) - q = ("-q",) if quiet else () - try: - cp = subprocess.run((sys.executable, "-m", "tox") + q + args, **kwargs) - except subprocess.CalledProcessError as e: - print(e.stdout, file=sys.stdout) - print(e.stderr, file=sys.stderr) - raise - print(cp.stdout, file=sys.stdout) - print(cp.stderr, file=sys.stderr) - return cp - - -TOX_VERSION = ver(tox("--version").stdout.split(" ")[0]) - - -@functools.lru_cache(maxsize=8) -def is_available(python): - try: - subprocess.run((python, "--version")) - except FileNotFoundError: - return False - return True - - -needs_py367891011 = pytest.mark.skipif( - not all((is_available(f"python3.{x}") for x in range(6, 12))), - reason="This test needs python3.6, 3.7, 3.8, 3.9, 3.10 and 3.11 available in $PATH", +from utils import ( + DOT_TOX, + NATIVE_EXEC_PREFIX_MSG, + NATIVE_SITE_PACKAGES, + NATIVE_TOXENV, + TOX_VERSION, + envs_from_tox_ini, + modify_config, + needs_all_pythons, + prep_tox_output, + tox, + tox_footer, ) - def test_native_toxenv_current_env(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG assert not (DOT_TOX / NATIVE_TOXENV / "lib").is_dir() -@needs_py367891011 -def test_all_toxenv_current_env(): - result = tox("--current-env", check=False) - assert NATIVE_EXEC_PREFIX_MSG in result.stdout.splitlines() - assert result.stdout.count("InterpreterMismatch:") >= 2 - assert result.returncode > 0 - - -@pytest.mark.parametrize("python", ["python3.4", "python3.5"]) -def test_missing_toxenv_current_env(python): - if is_available(python): - pytest.skip(f"Only works if {python} is not available in $PATH") - env = python.replace("python", "py").replace(".", "") - result = tox("-e", env, "--current-env", check=False) - assert f"InterpreterNotFound: {python}" in result.stdout - assert "Traceback" not in (result.stderr + result.stdout) - assert result.returncode > 0 - - -@needs_py367891011 -def test_all_toxenv_current_env_skip_missing(): - result = tox("--current-env", "--skip-missing-interpreters", check=False) - assert "InterpreterMismatch:" in result.stdout - assert "congratulations" in result.stdout - assert result.returncode == 0 - - -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps(toxenv, print_deps_stdout_arg): result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" + tox>={TOX_VERSION} six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() - assert result.stdout == expected + assert prep_tox_output(result.stdout) == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): - with modify_config(projdir / 'tox.ini') as config: + with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" - tox >= 3.13 + tox>=3.13 six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() - assert result.stdout == expected + assert prep_tox_output(result.stdout) == expected @pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): - with modify_config(projdir / 'tox.ini') as config: + with modify_config(projdir / "tox.ini") as config: config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" - setuptools > 30 + setuptools>30 pluggy + tox>={TOX_VERSION} six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() - assert result.stdout == expected + assert prep_tox_output(result.stdout) == expected @pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) -def test_print_deps_with_tox_minversion_and_requires(projdir, toxenv, print_deps_stdout_arg): - with modify_config(projdir / 'tox.ini') as config: +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) +def test_print_deps_with_tox_minversion_and_requires( + projdir, toxenv, print_deps_stdout_arg +): + with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" - tox >= 3.13 - setuptools > 30 + setuptools>30 pluggy + tox>=3.13 six py - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) + {tox_footer(toxenv)} """ ).lstrip() - assert result.stdout == expected + assert prep_tox_output(result.stdout) == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) 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 :) + {tox_footer(toxenv)} """ ).lstrip() - assert result.stdout == expected - - -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39"]) -def test_print_deps_only_deprecated(toxenv): - result = tox( - "-e", toxenv, '--print-deps-only', - env={**os.environ, 'PYTHONWARNINGS': 'always'}, - ) - waring_text = ( - "DeprecationWarning: --print-deps-only is deprecated; " - + "use `--print-deps-to -`" + assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted( + expected.splitlines() ) - assert waring_text in result.stderr def test_allenvs_print_deps(print_deps_stdout_arg): result = tox(print_deps_stdout_arg) - expected = textwrap.dedent( - """ - six - py - six - py - six - py - six - py - six - py - six - py - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - py311: commands succeeded - congratulations :) - """ - ).lstrip() - assert result.stdout == expected + expected = [] + for env in envs_from_tox_ini(): + expected.extend((f"tox>={TOX_VERSION}", "six", "py", 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) 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 - dev - full - dev - full - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - py311: commands succeeded - congratulations :) - """ - ).lstrip() - assert result.stdout == expected + expected = [] + for env in envs_from_tox_ini(): + expected.extend(("dev", "full", 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", ["py36", "py37", "py38", "py39", "py310", "py311"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_to_file(toxenv, tmp_path): depspath = tmp_path / "deps" result = tox("-e", toxenv, "--print-deps-to", str(depspath)) - assert depspath.read_text().splitlines() == ["six", "py"] - expected = textwrap.dedent( - f""" - ___________________________________ summary ____________________________________ - {toxenv}: commands succeeded - congratulations :) - """ - ).lstrip() - assert result.stdout == expected + assert sorted(depspath.read_text().splitlines()) == sorted( + [f"tox>={TOX_VERSION}", "six", "py"] + ) + expected = tox_footer(toxenv, spaces=0) + "\n" + assert prep_tox_output(result.stdout) == expected -@pytest.mark.parametrize("toxenv", ["py36", "py37", "py38", "py39", "py310", "py311"]) +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) 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 + assert sorted(extraspath.read_text().splitlines()) == sorted(["dev", "full"]) + 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')) +@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" result = tox(option, str(depspath)) - assert depspath.read_text().splitlines() == ["six", "py"] * 6 - expected = textwrap.dedent( - """ - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - py311: commands succeeded - congratulations :) - """ - ).lstrip() - assert result.stdout == expected + assert sorted(depspath.read_text().splitlines()) == sorted( + [f"tox>={TOX_VERSION}", "six", "py"] * 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 -@pytest.mark.parametrize('option', ('--print-extras-to', '--print-extras-to-file')) +@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"] * 6 - expected = textwrap.dedent( - """ - ___________________________________ summary ____________________________________ - py36: commands succeeded - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - py310: commands succeeded - py311: commands succeeded - congratulations :) - """ - ).lstrip() - assert result.stdout == expected + assert sorted(extraspath.read_text().splitlines()) == sorted( + ["dev", "full"] * 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") - result = tox("--print-deps-to", str(depspath)) + _ = tox("--print-deps-to", str(depspath)) lines = depspath.read_text().splitlines() assert "nada" not in lines assert "six" in lines @@ -385,7 +196,7 @@ def test_allenvs_print_deps_to_existing_file(tmp_path): 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)) + _ = tox("--print-extras-to", str(extraspath)) lines = extraspath.read_text().splitlines() assert "nada" not in lines assert "dev" in lines @@ -394,14 +205,15 @@ def test_allenvs_print_extras_to_existing_file(tmp_path): @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): +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)) + result = tox("--print-deps-to", str(depspath), "--print-extras-to", str(extraspath)) if deps_stdout: depslines = result.stdout.splitlines() extraslines = extraspath.read_text().splitlines() @@ -428,8 +240,10 @@ def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): result = tox( "-e", NATIVE_TOXENV, - "--print-deps-to", str(depsextraspath), - "--print-extras-to", str(depsextraspath), + "--print-deps-to", + str(depsextraspath), + "--print-extras-to", + str(depsextraspath), check=False, ) assert result.returncode > 0 @@ -437,9 +251,10 @@ def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): def test_print_deps_extras_to_stdout_is_not_possible( - tmp_path, - print_deps_stdout_arg, - print_extras_stdout_arg,): + tmp_path, + print_deps_stdout_arg, + print_extras_stdout_arg, +): result = tox( "-e", NATIVE_TOXENV, @@ -451,33 +266,17 @@ def test_print_deps_extras_to_stdout_is_not_possible( assert "cannot be identical" in result.stderr -def test_print_deps_only_print_deps_to_file_are_mutually_exclusive(): - result = tox( - "-e", - NATIVE_TOXENV, - "--print-deps-only", - "--print-deps-to", - "foobar", - check=False, - ) - assert result.returncode > 0 - assert "cannot be used together" in result.stderr - - -@needs_py367891011 +@needs_all_pythons def test_regular_run(): result = tox() - lines = result.stdout.splitlines()[:6] - assert "/.tox/py36 is the exec_prefix" in lines[0] - assert "/.tox/py37 is the exec_prefix" in lines[1] - assert "/.tox/py38 is the exec_prefix" in lines[2] - assert "/.tox/py39 is the exec_prefix" in lines[3] - assert "/.tox/py310 is the exec_prefix" in lines[4] - assert "/.tox/py311 is the exec_prefix" in lines[5] + lines = result.stdout.splitlines()[:5] + for line, env in zip(lines, envs_from_tox_ini()): + assert f"/.tox/{env} is the exec_prefix" in line assert "congratulations" in result.stdout - for y in 6, 7, 8, 9, 10, 11: + for env in envs_from_tox_ini(): + major, minor = re.match(r"py(\d)(\d+)", env).groups() for pkg in "py", "six", "test": - sitelib = DOT_TOX / f"py3{y}/lib/python3.{y}/site-packages" + sitelib = DOT_TOX / f"{env}/lib/python{major}.{minor}/site-packages" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 @@ -488,111 +287,11 @@ def test_regular_run_native_toxenv(): assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] assert "congratulations" in result.stdout for pkg in "py", "six", "test": - sitelib = ( - DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" - ) + sitelib = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 -def test_regular_after_current_is_supported(): - result = tox("-e", NATIVE_TOXENV, "--current-env") - assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG - result = tox("-e", NATIVE_TOXENV) - assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout - assert "--recreate" not in result.stderr - - -def test_regular_after_killed_current_is_not_supported(): - # fake broken tox run - shutil.rmtree(DOT_TOX, ignore_errors=True) - (DOT_TOX / NATIVE_TOXENV / "bin").mkdir(parents=True) - (DOT_TOX / NATIVE_TOXENV / "bin" / "python").symlink_to(NATIVE_EXECUTABLE) - - result = tox("-e", NATIVE_TOXENV, check=False) - assert result.returncode > 0 - assert "--recreate" in result.stderr - - -def test_regular_after_first_print_deps_is_supported(print_deps_stdout_arg): - result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) - assert result.stdout.splitlines()[0] == "six" - result = tox("-e", NATIVE_TOXENV) - lines = sorted(result.stdout.splitlines()[:1]) - assert "--recreate" not in result.stderr - assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] - - # check that "test" was not installed to current environment - shutil.rmtree("./test.egg-info") - pip_freeze = subprocess.run( - (sys.executable, "-m", "pip", "freeze"), - encoding="utf-8", - stdout=subprocess.PIPE, - ).stdout.splitlines() - # XXX when this fails, recreate your current environment - assert "test==0.0.0" not in pip_freeze - - -def test_regular_recreate_after_current(): - result = tox("-e", NATIVE_TOXENV, "--current-env") - assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG - result = tox("-re", NATIVE_TOXENV) - assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout - assert "not supported" not in result.stderr - assert "--recreate" not in result.stderr - - -def test_current_after_regular_is_not_supported(): - result = tox("-e", NATIVE_TOXENV) - assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout - result = tox("-e", NATIVE_TOXENV, "--current-env", check=False) - assert result.returncode > 0 - assert "not supported" in result.stderr - - -def test_current_recreate_after_regular(): - result = tox("-e", NATIVE_TOXENV) - assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout - result = tox("-re", NATIVE_TOXENV, "--current-env") - assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG - - -def test_current_after_print_deps(print_deps_stdout_arg): - # this is quite fast, so we can do it several times - for _ in range(3): - result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) - assert "bin/python" not in result.stdout - assert "six" in result.stdout - result = tox("-re", NATIVE_TOXENV, "--current-env") - 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 - assert "six" in result.stdout - - result = tox("-re", NATIVE_TOXENV) - assert result.stdout.splitlines()[0] != NATIVE_EXEC_PREFIX_MSG - sitelib = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" - assert sitelib.is_dir() - assert len(list(sitelib.glob("test-*.dist-info"))) == 1 - - result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) - assert "bin/python" not in result.stdout - assert "six" in result.stdout - - def test_print_deps_without_python_command(tmp_path, print_deps_stdout_arg): bin = tmp_path / "bin" bin.mkdir() @@ -604,58 +303,49 @@ def test_print_deps_without_python_command(tmp_path, print_deps_stdout_arg): result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg, env=env) expected = textwrap.dedent( f""" + tox>={TOX_VERSION} six py - ___________________________________ summary ____________________________________ - {NATIVE_TOXENV}: commands succeeded - congratulations :) + {tox_footer(NATIVE_TOXENV)} """ ).lstrip() - assert result.stdout == expected + assert prep_tox_output(result.stdout) == expected -@pytest.mark.parametrize("flag", [None, "--print-deps-to=-", "--current-env"]) -def test_noquiet_installed_packages(flag): +@pytest.mark.parametrize("flag", ["--print-deps-to=-", "--current-env"]) +def test_recreate_environment(flag): flags = (flag,) if flag else () + _ = tox("-e", NATIVE_TOXENV, check=False) result = tox("-e", NATIVE_TOXENV, *flags, quiet=False, check=False) - assert f"\n{NATIVE_TOXENV} installed: " in result.stdout - for line in result.stdout.splitlines(): - if line.startswith(f"{NATIVE_TOXENV} installed: "): - packages = line.rpartition(" installed: ")[-1].split(",") - break - - # default tox produces output sorted by package names - assert packages == sorted( - packages, key=lambda p: p.partition("==")[0].partition(" @ ")[0].lower() + assert f"{NATIVE_TOXENV}: recreate env because env type changed" in prep_tox_output( + result.stdout ) - # without a flag, the output must match tox defaults - if not flag: - assert len(packages) == 3 - assert packages[0].startswith("py==") - assert packages[1].startswith("six==") - assert packages[2].startswith(("test==", "test @ ")) # old and new pip - - # with our flags, uses the absolutely current environment by default, hence has tox - else: - assert len([p for p in packages if p.startswith("tox==")]) == 1 - assert all(re.match(r"\S+==\S+", p) for p in packages) - -@pytest.mark.parametrize("flag", ["--print-deps-to=-", "--print-extras-to=-", "--current-env"]) +@pytest.mark.parametrize( + "flag", ["--print-deps-to=-", "--print-extras-to=-", "--current-env"] +) @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_not_installed(projdir, flag, usedevelop): - with modify_config(projdir / 'tox.ini') as config: - config['testenv']['usedevelop'] = str(usedevelop) - result = tox("-e", NATIVE_TOXENV, flag, quiet=False) - assert 'test==0.0.0' not in result.stdout - assert 'test @ file://' not in result.stdout + with modify_config(projdir / "tox.ini") as config: + config["testenv"]["usedevelop"] = str(usedevelop) + _ = tox("-e", NATIVE_TOXENV, flag, quiet=False) + egg_link = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test.egg-link" + dist_info = ( + DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test-0.0.0.dist-info" + ) + assert not egg_link.exists() + assert not dist_info.exists() @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_installed_with_regular_tox(projdir, usedevelop): - with modify_config(projdir / 'tox.ini') as config: - config['testenv']['usedevelop'] = str(usedevelop) - result = tox("-e", NATIVE_TOXENV, quiet=False) - assert ('test==0.0.0' in result.stdout or - 'test @ file://' in result.stdout) + with modify_config(projdir / "tox.ini") as config: + config["testenv"]["usedevelop"] = str(usedevelop) + _ = tox("-e", NATIVE_TOXENV, "-v", quiet=False) + egg_link = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test.egg-link" + dist_info = ( + DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test-0.0.0.dist-info" + ) + to_test = egg_link if usedevelop else dist_info + assert to_test.exists() diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..b7a948f --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,131 @@ +import configparser +import contextlib +import functools +import os +import pathlib +import re +import subprocess +import sys +from configparser import ConfigParser + +import pytest +from packaging.version import parse as ver + +PYTHON_VERSION_DOT = f"{sys.version_info[0]}.{sys.version_info[1]}" +PYTHON_VERSION_NODOT = f"{sys.version_info[0]}{sys.version_info[1]}" +NATIVE_TOXENV = f"py{PYTHON_VERSION_NODOT}" +NATIVE_SITE_PACKAGES = f"lib/python{PYTHON_VERSION_DOT}/site-packages" +NATIVE_EXECUTABLE = str(pathlib.Path(sys.executable).resolve()) +FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" +DOT_TOX = pathlib.Path("./.tox") + + +def _exec_prefix(executable): + """Returns sys.exec_prefix for the given executable""" + cmd = (executable, "-c", "import sys; print(sys.exec_prefix)") + return subprocess.check_output(cmd, encoding="utf-8").strip() + + +NATIVE_EXEC_PREFIX = _exec_prefix(NATIVE_EXECUTABLE) +NATIVE_EXEC_PREFIX_MSG = f"{NATIVE_EXEC_PREFIX} is the exec_prefix" + + +def tox(*args, quiet=True, **kwargs): + kwargs.setdefault("encoding", "utf-8") + kwargs.setdefault("stdout", subprocess.PIPE) + kwargs.setdefault("stderr", subprocess.PIPE) + kwargs.setdefault("check", True) + kwargs.setdefault("cwd", os.getcwd()) + q = ("-q",) if quiet else () + env = dict(os.environ) + env.pop("TOX_WORK_DIR") + kwargs.setdefault("env", env) + try: + print("current", os.getcwd(), "running in", kwargs["cwd"]) + cp = subprocess.run((sys.executable, "-m", "tox") + q + args, **kwargs) + except subprocess.CalledProcessError as e: + print(e.stdout, file=sys.stdout) + print(e.stderr, file=sys.stderr) + raise + print(cp.stdout, file=sys.stdout) + print(cp.stderr, file=sys.stderr) + return cp + + +TOX_VERSION = ver(tox("--version").stdout.split(" ")[0].split("+")[0]) +TOX4 = TOX_VERSION.major == 4 +# if TOX4: +# DOT_TOX /= "4" + + +@contextlib.contextmanager +def modify_config(tox_ini_path): + """Context manager that allows modifying the given Tox config file + + A statement like:: + + with prepare_config(projdir) as config: + + will make `config` a ConfigParser instance that is saved at the end + of the `with` block. + """ + config = configparser.ConfigParser() + config.read(tox_ini_path) + yield config + with open(tox_ini_path, "w") as tox_ini_file: + config.write(tox_ini_file) + + +@functools.lru_cache(maxsize=8) +def is_available(python): + try: + subprocess.run((python, "--version")) + except FileNotFoundError: + return False + return True + + +@functools.lru_cache() +def envs_from_tox_ini(): + cp = ConfigParser() + cp.read(FIXTURES_DIR / "tox.ini") + return cp["tox"]["envlist"].split(",") + + +def tox_footer(envs=None, spaces=8): + if envs is None: + envs = envs_from_tox_ini() + elif isinstance(envs, str): + envs = [envs] + + default_indent = " " * spaces + + if TOX4: + result = "" + else: + result = "___________________________________ summary ____________________________________\n" + + for i, env in enumerate(envs): + if TOX4: + # Skip indentation for the first line + indent = default_indent if i > 0 else "" + result += f"{indent} {env}: OK\n" + else: + result += f"{default_indent} {env}: commands succeeded\n" + + result += f"{default_indent} congratulations :)" + + return result + + +def prep_tox_output(output): + """Remove time info from tox output""" + result = re.sub(r" \((\d+\.\d+|\d+) seconds\)", "", output) + result = re.sub(r" ✔ in (\d+\.\d+|\d+) seconds", "", result) + return result + + +needs_all_pythons = pytest.mark.skipif( + not all((is_available(f"python3.{x}") for x in range(6, 12))), + reason="This test needs all pythons from 3.6 to 3.11 available in $PATH", +) diff --git a/tox.ini b/tox.ini index ac9136d..9ed86b7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ # This information is repeated in .github/workflows/main.yaml # (see https://github.com/fedora-python/tox-github-action/issues/8) -envlist = {py36,py37,py38,py39,py310}-tox{release,master,315} +envlist = {py36,py37,py38,py39,py310}-tox{release,master,315},{py37,py38,py39,py310}-tox4 [testenv] extras = @@ -11,8 +11,10 @@ deps= tox315: tox >=3.15,<3.16 toxrelease: tox < 4 toxmaster: git+https://github.com/tox-dev/tox.git@legacy + tox4: git+https://github.com/tox-dev/tox.git@rewrite commands = - pytest -v {posargs} tests + !tox4: pytest -v {posargs} tests/test_integration.py + tox4: pytest -v {posargs} tests/test_integration_tox4.py [pytest] addopts = -nauto From 3e9cd587042cb6815873607355e04d351bf39978 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Thu, 10 Feb 2022 14:45:52 +0100 Subject: [PATCH 17/27] Remove unused imports --- src/tox_current_env/hooks4.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 6bf601d..3dec54c 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -3,21 +3,16 @@ import platform import sys import sysconfig -import tempfile -import warnings from pathlib import Path from typing import Set from tox.config.loader.memory import MemoryLoader -from tox.execute.api import Execute from tox.execute.local_sub_process import ( Execute, LocalSubProcessExecuteInstance, ) from tox.plugin import impl -from tox.report import HandledError from tox.tox_env.python.api import PythonInfo -from tox.tox_env.python.pip.pip_install import Pip from tox.tox_env.python.runner import PythonRun From 463c745f6ef997101f68ec67b0a555c8be06a67b Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Fri, 25 Feb 2022 09:32:23 +0100 Subject: [PATCH 18/27] Drop support for tox 3.15 --- .github/workflows/main.yaml | 15 +++++---------- tox.ini | 5 ++--- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index a48da34..dd64bee 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -22,21 +22,16 @@ jobs: # This information is repeated in tox.ini # (see https://github.com/fedora-python/tox-github-action/issues/8) # Generate it by: tox -l | sed "s/^/- /" - - py36-toxrelease + - py36-tox3 - py36-toxmaster - - py36-tox315 - - py37-toxrelease + - py37-tox3 - py37-toxmaster - - py37-tox315 - - py38-toxrelease + - py38-tox3 - py38-toxmaster - - py38-tox315 - - py39-toxrelease + - py39-tox3 - py39-toxmaster - - py39-tox315 - - py310-toxrelease + - py310-tox3 - py310-toxmaster - - py310-tox315 - py37-tox4 - py38-tox4 - py39-tox4 diff --git a/tox.ini b/tox.ini index 9ed86b7..11aa1eb 100644 --- a/tox.ini +++ b/tox.ini @@ -2,14 +2,13 @@ # This information is repeated in .github/workflows/main.yaml # (see https://github.com/fedora-python/tox-github-action/issues/8) -envlist = {py36,py37,py38,py39,py310}-tox{release,master,315},{py37,py38,py39,py310}-tox4 +envlist = {py36,py37,py38,py39,py310}-tox{3,master},{py37,py38,py39,py310}-tox4 [testenv] extras = tests deps= - tox315: tox >=3.15,<3.16 - toxrelease: tox < 4 + tox3: tox < 4 toxmaster: git+https://github.com/tox-dev/tox.git@legacy tox4: git+https://github.com/tox-dev/tox.git@rewrite commands = From 5d5286d4f01809a4a1f40548998d7346f509b35c Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Wed, 30 Nov 2022 12:24:38 +0100 Subject: [PATCH 19/27] Drop tox < 3.24, add Python 3.11, fix tox 4 compatibility --- .github/workflows/main.yaml | 20 ++++++++++++-------- README.rst | 10 +++++----- setup.py | 3 ++- src/tox_current_env/hooks3.py | 13 ++++--------- tests/fixtures/tox.ini | 2 +- tests/test_integration.py | 6 ------ tests/test_integration_tox4.py | 28 +++++++++++----------------- tests/utils.py | 7 ++++++- tox.ini | 6 +++--- 9 files changed, 44 insertions(+), 51 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index dd64bee..b64fd1b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -22,19 +22,23 @@ jobs: # This information is repeated in tox.ini # (see https://github.com/fedora-python/tox-github-action/issues/8) # Generate it by: tox -l | sed "s/^/- /" + - py36-tox324 - py36-tox3 - - py36-toxmaster + - py36-tox4 + - py37-tox324 - py37-tox3 - - py37-toxmaster - - py38-tox3 - - py38-toxmaster - - py39-tox3 - - py39-toxmaster - - py310-tox3 - - py310-toxmaster - py37-tox4 + - py38-tox324 + - py38-tox3 - py38-tox4 + - py39-tox324 + - py39-tox3 - py39-tox4 + - py310-tox324 + - py310-tox3 - py310-tox4 + - py311-tox324 + - py311-tox3 + - py311-tox4 # Use GitHub's Linux Docker host runs-on: ubuntu-latest diff --git a/README.rst b/README.rst index 35a85e0..0f49390 100644 --- a/README.rst +++ b/README.rst @@ -232,20 +232,20 @@ installs (a newer version of) ``tox`` and the missing packages into that environment and proxies all ``tox`` invocations trough that. Unfortunately, this is undesired for ``tox-current-env``. - 1. Starting with ``tox`` 3.23, it is possible to invoke it as - ``tox --no-provision`` to prevent the provision entirely. + 1. It is possible to invoke ``tox`` with ``--no-provision`` + to prevent the provision entirely. When requirements are missing, ``tox`` fails instead of provisioning. If a path is passed as a value for ``--no-provision``, the requirements will be serialized to the file, as JSON. - 2. Starting with ``tox`` 3.22, the requires, if specified, are included in the + 2. The requires, if specified, are included in the results of ``tox --print-deps-to``. This only works when they are installed (otherwise see the first point). 3. The minimal tox version, if specified, is included in the results of - ``tox --print-deps-to`` (as ``tox >= X.Y.Z``). + ``tox --print-deps-to``. This only works when the version requirement is satisfied (otherwise see the first point). -With ``tox >= 3.23``, the recommend way to handle this is: +The recommend way to handle this is: 1. Run ``tox --no-provision provision.json --print-deps-to=...`` or similar. 2. If the command fails, install requirements from ``provision.json`` to the diff --git a/setup.py b/setup.py index 9f18b8f..073588a 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def long_description(): packages=find_packages("src"), entry_points={"tox": ["current-env = tox_current_env.hooks"]}, install_requires=[ - "tox>=3.15", + "tox>=3.24", "importlib_metadata; python_version < '3.8'" ], extras_require={ @@ -42,6 +42,7 @@ def long_description(): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Testing", ], diff --git a/src/tox_current_env/hooks3.py b/src/tox_current_env/hooks3.py index 62e956b..33b0a9d 100644 --- a/src/tox_current_env/hooks3.py +++ b/src/tox_current_env/hooks3.py @@ -61,13 +61,9 @@ def _allow_all_externals(envconfig): setattr(envconfig, option, "*") break else: - # If none was set, we set one of them, preferably the new one: - if hasattr(envconfig, "allowlist_externals"): - envconfig.allowlist_externals = "*" - else: - # unless we need to fallback to the old and deprecated - # TODO, drop this when we drop support for tox < 3.18 - envconfig.whitelist_externals = "*" + # If none was set, we set the new one + envconfig.allowlist_externals = "*" + @tox.hookimpl def tox_configure(config): @@ -225,8 +221,7 @@ def tox_dependencies(config): """Get dependencies of tox itself, 'minversion' and 'requires' config options""" if config.minversion is not None: yield f"tox >= {config.minversion}" - # config does not have the "requires" attribute until tox 3.22: - yield from getattr(config, "requires", []) + yield from config.requires @tox.hookimpl diff --git a/tests/fixtures/tox.ini b/tests/fixtures/tox.ini index 0055b26..e218977 100644 --- a/tests/fixtures/tox.ini +++ b/tests/fixtures/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39,py310 +envlist = py36,py37,py38,py39,py310,py311 [testenv] deps = diff --git a/tests/test_integration.py b/tests/test_integration.py index 5dcf3ba..02e576b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,7 +6,6 @@ import textwrap import pytest -from packaging.version import parse as ver from utils import ( DOT_TOX, @@ -14,7 +13,6 @@ NATIVE_EXECUTABLE, NATIVE_SITE_PACKAGES, NATIVE_TOXENV, - TOX_VERSION, envs_from_tox_ini, is_available, modify_config, @@ -107,7 +105,6 @@ def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): assert result.stdout == expected -@pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): with modify_config(projdir / "tox.ini") as config: @@ -125,7 +122,6 @@ def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): assert result.stdout == expected -@pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion_and_requires( projdir, toxenv, print_deps_stdout_arg @@ -547,8 +543,6 @@ def test_self_is_not_installed(projdir, flag, usedevelop): @pytest.mark.parametrize("externals", [None, "allowlist_externals", "whitelist_externals"]) def test_externals(projdir, externals): - if externals == "allowlist_externals" and TOX_VERSION < ver("3.18"): - pytest.xfail("No support in old tox") with modify_config(projdir / 'tox.ini') as config: config['testenv']['commands'] = "echo assertme" if externals is not None: diff --git a/tests/test_integration_tox4.py b/tests/test_integration_tox4.py index ccca154..93594ad 100644 --- a/tests/test_integration_tox4.py +++ b/tests/test_integration_tox4.py @@ -4,14 +4,13 @@ import textwrap import pytest -from packaging.version import parse as ver from utils import ( DOT_TOX, NATIVE_EXEC_PREFIX_MSG, NATIVE_SITE_PACKAGES, NATIVE_TOXENV, - TOX_VERSION, + TOX_MIN_VERSION, envs_from_tox_ini, modify_config, needs_all_pythons, @@ -32,7 +31,7 @@ def test_print_deps(toxenv, print_deps_stdout_arg): result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" - tox>={TOX_VERSION} + tox>={TOX_MIN_VERSION} six py {tox_footer(toxenv)} @@ -57,7 +56,6 @@ def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): assert prep_tox_output(result.stdout) == expected -@pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): with modify_config(projdir / "tox.ini") as config: @@ -67,7 +65,7 @@ def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): f""" setuptools>30 pluggy - tox>={TOX_VERSION} + tox>={TOX_MIN_VERSION} six py {tox_footer(toxenv)} @@ -76,7 +74,6 @@ def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): assert prep_tox_output(result.stdout) == expected -@pytest.mark.xfail(TOX_VERSION < ver("3.22"), reason="No support in old tox") @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion_and_requires( projdir, toxenv, print_deps_stdout_arg @@ -117,7 +114,7 @@ def test_allenvs_print_deps(print_deps_stdout_arg): result = tox(print_deps_stdout_arg) expected = [] for env in envs_from_tox_ini(): - expected.extend((f"tox>={TOX_VERSION}", "six", "py", f"{env}: OK")) + expected.extend((f"tox>={TOX_MIN_VERSION}", "six", "py", f"{env}: OK")) expected.pop() # The last "py310: OK" is not there expected.append(tox_footer(spaces=0)) expected = ("\n".join(expected)).splitlines() @@ -140,7 +137,7 @@ def test_print_deps_to_file(toxenv, tmp_path): depspath = tmp_path / "deps" result = tox("-e", toxenv, "--print-deps-to", str(depspath)) assert sorted(depspath.read_text().splitlines()) == sorted( - [f"tox>={TOX_VERSION}", "six", "py"] + [f"tox>={TOX_MIN_VERSION}", "six", "py"] ) expected = tox_footer(toxenv, spaces=0) + "\n" assert prep_tox_output(result.stdout) == expected @@ -160,7 +157,7 @@ def test_allenvs_print_deps_to_file(tmp_path, option): depspath = tmp_path / "deps" result = tox(option, str(depspath)) assert sorted(depspath.read_text().splitlines()) == sorted( - [f"tox>={TOX_VERSION}", "six", "py"] * len(envs_from_tox_ini()) + [f"tox>={TOX_MIN_VERSION}", "six", "py"] * len(envs_from_tox_ini()) ) expected = "" for env in envs_from_tox_ini()[:-1]: @@ -303,7 +300,7 @@ def test_print_deps_without_python_command(tmp_path, print_deps_stdout_arg): result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg, env=env) expected = textwrap.dedent( f""" - tox>={TOX_VERSION} + tox>={TOX_MIN_VERSION} six py {tox_footer(NATIVE_TOXENV)} @@ -342,10 +339,7 @@ def test_self_is_not_installed(projdir, flag, usedevelop): def test_self_is_installed_with_regular_tox(projdir, usedevelop): with modify_config(projdir / "tox.ini") as config: config["testenv"]["usedevelop"] = str(usedevelop) - _ = tox("-e", NATIVE_TOXENV, "-v", quiet=False) - egg_link = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test.egg-link" - dist_info = ( - DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test-0.0.0.dist-info" - ) - to_test = egg_link if usedevelop else dist_info - assert to_test.exists() + result = tox("-e", NATIVE_TOXENV, "-v", quiet=False) + assert "test-0.0.0" in result.stdout + if usedevelop: + assert "test-0.0.0-0.editable" in result.stdout diff --git a/tests/utils.py b/tests/utils.py index b7a948f..dd53b31 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -52,7 +52,12 @@ def tox(*args, quiet=True, **kwargs): return cp -TOX_VERSION = ver(tox("--version").stdout.split(" ")[0].split("+")[0]) +TOX_MIN_VERSION = TOX_VERSION = ver(tox("--version").stdout.split(" ")[0].split("+")[0]) +# TOX_MIN_VERSION follows the same logic as tox itself +# see: https://github.com/tox-dev/tox/blob/6cf388db53402c2595ca8b673ddabece46f4d06e/src/tox/provision.py#L76-L84 +if TOX_MIN_VERSION.is_devrelease or TOX_MIN_VERSION.is_prerelease: + TOX_MIN_VERSION = ver(f"{TOX_VERSION.base_version}a0") + TOX4 = TOX_VERSION.major == 4 # if TOX4: # DOT_TOX /= "4" diff --git a/tox.ini b/tox.ini index 11aa1eb..f0a7167 100644 --- a/tox.ini +++ b/tox.ini @@ -2,15 +2,15 @@ # This information is repeated in .github/workflows/main.yaml # (see https://github.com/fedora-python/tox-github-action/issues/8) -envlist = {py36,py37,py38,py39,py310}-tox{3,master},{py37,py38,py39,py310}-tox4 +envlist = {py36,py37,py38,py39,py310,py311}-tox{324,3,4} [testenv] extras = tests deps= + tox324: tox >=3.24,<3.25 tox3: tox < 4 - toxmaster: git+https://github.com/tox-dev/tox.git@legacy - tox4: git+https://github.com/tox-dev/tox.git@rewrite + tox4: tox >=4,< 5 commands = !tox4: pytest -v {posargs} tests/test_integration.py tox4: pytest -v {posargs} tests/test_integration_tox4.py From f5beafc38c57c30f0b1cd6da97acf4088ee94f41 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Wed, 30 Nov 2022 14:27:10 +0100 Subject: [PATCH 20/27] Fix tests --- .github/workflows/main.yaml | 1 - tests/test_integration.py | 4 ++-- tox.ini | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b64fd1b..65b2455 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -24,7 +24,6 @@ jobs: # Generate it by: tox -l | sed "s/^/- /" - py36-tox324 - py36-tox3 - - py36-tox4 - py37-tox324 - py37-tox3 - py37-tox4 diff --git a/tests/test_integration.py b/tests/test_integration.py index 02e576b..8eb13b1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -240,7 +240,7 @@ def test_print_extras_to_file(toxenv, tmp_path): def test_allenvs_print_deps_to_file(tmp_path, option): depspath = tmp_path / "deps" result = tox(option, str(depspath)) - assert depspath.read_text().splitlines() == ["six", "py"] * 5 + assert depspath.read_text().splitlines() == ["six", "py"] * len(envs_from_tox_ini()) expected = textwrap.dedent( f""" {tox_footer()} @@ -253,7 +253,7 @@ def test_allenvs_print_deps_to_file(tmp_path, option): 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"] * 5 + assert extraspath.read_text().splitlines() == ["dev", "full"] * len(envs_from_tox_ini()) expected = textwrap.dedent( f""" {tox_footer()} diff --git a/tox.ini b/tox.ini index f0a7167..ceadfcb 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ # This information is repeated in .github/workflows/main.yaml # (see https://github.com/fedora-python/tox-github-action/issues/8) -envlist = {py36,py37,py38,py39,py310,py311}-tox{324,3,4} +envlist = py36-tox{324,3},{py37,py38,py39,py310,py311}-tox{324,3,4} [testenv] extras = From a72e7ba7138366237ea488f57113c69e23867e26 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 12 Dec 2022 12:38:11 +0100 Subject: [PATCH 21/27] Fix TOX_MIN_VERSION again --- tests/utils.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index dd53b31..f566763 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -52,15 +52,9 @@ def tox(*args, quiet=True, **kwargs): return cp -TOX_MIN_VERSION = TOX_VERSION = ver(tox("--version").stdout.split(" ")[0].split("+")[0]) -# TOX_MIN_VERSION follows the same logic as tox itself -# see: https://github.com/tox-dev/tox/blob/6cf388db53402c2595ca8b673ddabece46f4d06e/src/tox/provision.py#L76-L84 -if TOX_MIN_VERSION.is_devrelease or TOX_MIN_VERSION.is_prerelease: - TOX_MIN_VERSION = ver(f"{TOX_VERSION.base_version}a0") - +TOX_VERSION = ver(tox("--version").stdout.split(" ")[0].split("+")[0]) +TOX_MIN_VERSION = ver(f"{TOX_VERSION.major}.{TOX_VERSION.minor}") TOX4 = TOX_VERSION.major == 4 -# if TOX4: -# DOT_TOX /= "4" @contextlib.contextmanager From ad6bcf5c195b9d60182255a931a4fed79749b799 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 12 Dec 2022 12:38:26 +0100 Subject: [PATCH 22/27] Fix for commands_pre and commands_post for tox 4 --- src/tox_current_env/hooks4.py | 2 +- tests/test_integration_tox4.py | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 3dec54c..972cf03 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -96,7 +96,7 @@ def tox_add_env_config(env_conf, state): # 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: - empty_commands = MemoryLoader(commands=[]) + empty_commands = MemoryLoader(commands=[], commands_pre=[], commands_post=[]) env_conf.loaders.insert(0, empty_commands) diff --git a/tests/test_integration_tox4.py b/tests/test_integration_tox4.py index 93594ad..90ca700 100644 --- a/tests/test_integration_tox4.py +++ b/tests/test_integration_tox4.py @@ -40,6 +40,30 @@ def test_print_deps(toxenv, print_deps_stdout_arg): assert prep_tox_output(result.stdout) == expected +@pytest.mark.parametrize("toxenv", envs_from_tox_ini()) +@pytest.mark.parametrize("pre_post", ["pre", "post", "both"]) +def test_print_deps_with_commands_pre_post(projdir, toxenv, pre_post, print_deps_stdout_arg): + with modify_config(projdir / 'tox.ini') as config: + if pre_post == "both": + config["testenv"]["commands_pre"] = "echo unexpected" + config["testenv"]["commands_post"] = "echo unexpected" + else: + config["testenv"][f"commands_{pre_post}"] = "echo unexpected" + result = tox("-e", toxenv, print_deps_stdout_arg) + expected = textwrap.dedent( + f""" + tox>={TOX_MIN_VERSION} + six + py + {tox_footer(toxenv)} + """ + ).lstrip() + assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted( + expected.splitlines() + ) + assert result.stderr == "" + + @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): with modify_config(projdir / "tox.ini") as config: @@ -110,6 +134,29 @@ def test_print_extras(toxenv, print_extras_stdout_arg): ) +@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): + with modify_config(projdir / 'tox.ini') as config: + if pre_post == "both": + config["testenv"]["commands_pre"] = "echo unexpected" + config["testenv"]["commands_post"] = "echo unexpected" + else: + config["testenv"][f"commands_{pre_post}"] = "echo unexpected" + result = tox("-e", toxenv, print_extras_stdout_arg) + expected = textwrap.dedent( + f""" + dev + full + {tox_footer(toxenv)} + """ + ).lstrip() + assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted( + expected.splitlines() + ) + assert result.stderr == "" + + def test_allenvs_print_deps(print_deps_stdout_arg): result = tox(print_deps_stdout_arg) expected = [] From f71dd2a23810118eb7f35b38a583e69be29f12d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Mon, 12 Dec 2022 19:04:32 +0100 Subject: [PATCH 23/27] Skip tox 3/4 tests with tox 4/3 via pytest, not tox --- tests/{test_integration.py => test_integration_tox3.py} | 5 +++++ tests/test_integration_tox4.py | 5 +++++ tox.ini | 3 +-- 3 files changed, 11 insertions(+), 2 deletions(-) rename tests/{test_integration.py => test_integration_tox3.py} (99%) diff --git a/tests/test_integration.py b/tests/test_integration_tox3.py similarity index 99% rename from tests/test_integration.py rename to tests/test_integration_tox3.py index 8eb13b1..406c795 100644 --- a/tests/test_integration.py +++ b/tests/test_integration_tox3.py @@ -13,6 +13,7 @@ NATIVE_EXECUTABLE, NATIVE_SITE_PACKAGES, NATIVE_TOXENV, + TOX_VERSION, envs_from_tox_ini, is_available, modify_config, @@ -22,6 +23,10 @@ ) +if TOX_VERSION.major != 3: + pytest.skip("skipping tests for tox 3", allow_module_level=True) + + def test_native_toxenv_current_env(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG diff --git a/tests/test_integration_tox4.py b/tests/test_integration_tox4.py index 90ca700..1aee1e4 100644 --- a/tests/test_integration_tox4.py +++ b/tests/test_integration_tox4.py @@ -10,6 +10,7 @@ NATIVE_EXEC_PREFIX_MSG, NATIVE_SITE_PACKAGES, NATIVE_TOXENV, + TOX_VERSION, TOX_MIN_VERSION, envs_from_tox_ini, modify_config, @@ -20,6 +21,10 @@ ) +if TOX_VERSION.major != 4: + pytest.skip("skipping tests for tox 4", allow_module_level=True) + + def test_native_toxenv_current_env(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG diff --git a/tox.ini b/tox.ini index ceadfcb..d8463de 100644 --- a/tox.ini +++ b/tox.ini @@ -12,8 +12,7 @@ deps= tox3: tox < 4 tox4: tox >=4,< 5 commands = - !tox4: pytest -v {posargs} tests/test_integration.py - tox4: pytest -v {posargs} tests/test_integration_tox4.py + pytest -v {posargs} tests [pytest] addopts = -nauto From 028ee0cfb4ee6ada3a094bdd6d5f6c538f6a6fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 13 Dec 2022 16:56:33 +0100 Subject: [PATCH 24/27] Avoid race conditions when running tests with xdist --- tests/conftest.py | 6 +++++- tests/fixtures/tox.ini | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index b33178f..6efd787 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import os import shutil import pytest @@ -5,12 +6,15 @@ @pytest.fixture(autouse=True) -def projdir(tmp_path, monkeypatch): +def projdir(tmp_path, monkeypatch, worker_id): pwd = tmp_path / "projdir" pwd.mkdir() for fname in "tox.ini", "setup.py": shutil.copy(FIXTURES_DIR / fname, pwd) monkeypatch.chdir(pwd) + # https://github.com/pypa/pip/issues/5345#issuecomment-386424455 + monkeypatch.setenv("XDG_CACHE_HOME", + os.path.expanduser(f"~/.cache/pytest-xdist-{worker_id}")) return pwd diff --git a/tests/fixtures/tox.ini b/tests/fixtures/tox.ini index e218977..6d3f448 100644 --- a/tests/fixtures/tox.ini +++ b/tests/fixtures/tox.ini @@ -2,6 +2,8 @@ envlist = py36,py37,py38,py39,py310,py311 [testenv] +passenv = + XDG_CACHE_HOME deps = six py From c3497d2b2a69c82f17d04fcbc586d069871f0a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 14 Dec 2022 11:47:07 +0100 Subject: [PATCH 25/27] Don't assume the tests allways run via tox --- tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index f566763..ea180f4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -38,7 +38,7 @@ def tox(*args, quiet=True, **kwargs): kwargs.setdefault("cwd", os.getcwd()) q = ("-q",) if quiet else () env = dict(os.environ) - env.pop("TOX_WORK_DIR") + env.pop("TOX_WORK_DIR", None) kwargs.setdefault("env", env) try: print("current", os.getcwd(), "running in", kwargs["cwd"]) From 679fbc996ec9cc715da4a839af5043844d21837e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 14 Dec 2022 13:48:10 +0100 Subject: [PATCH 26/27] Always set passenv/pass_env to * --- README.rst | 16 +++++++--------- src/tox_current_env/hooks3.py | 5 +++++ src/tox_current_env/hooks4.py | 8 ++++---- tests/test_integration_tox3.py | 14 ++++++++++++++ tests/test_integration_tox4.py | 27 +++++++++++++++++++++++++++ tests/utils.py | 2 +- 6 files changed, 58 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 0f49390..560a5b7 100644 --- a/README.rst +++ b/README.rst @@ -208,15 +208,13 @@ forcefully killing it before it finished, uninstalling the plugin, and running ``tox``), you will get undefined results (such as installing packages from PyPI into your current environment). -Environment variables are not passed by default -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Although the plugin name suggests that current environment is used for tests, -it means the Python environment, not Shell. -If you want the tests to see environment variables of the calling process, -use the ``TOX_TESTENV_PASSENV`` environment variable. -Read `the documentation for passing environment variables to tox -`_. +Environment variables are passed by default +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since 0.0.9, all Shell environment variables are passed by default when using +this plugin. The `passenv` tox configuration is set to `*`. +Read `the documentation for more information about passing environment variables to tox +`_. tox provisioning ~~~~~~~~~~~~~~~~ diff --git a/src/tox_current_env/hooks3.py b/src/tox_current_env/hooks3.py index 33b0a9d..eca79f8 100644 --- a/src/tox_current_env/hooks3.py +++ b/src/tox_current_env/hooks3.py @@ -85,6 +85,11 @@ def tox_configure(config): for testenv in config.envconfigs: config.envconfigs[testenv].usedevelop = False _allow_all_externals(config.envconfigs[testenv]) + # Because tox 4 no longer reads $TOX_TESTENV_PASSENV, + # this plugin always passes all environment variables by default, + # even on tox 3. + # Unfortunately at this point the set contains actual values, not globs: + config.envconfigs[testenv].passenv |= set(os.environ.keys()) # When printing dependencies/extras we don't run any commands. # Unfortunately tox_runtest_pre/tox_runtest_post hooks don't use firstresult=True, diff --git a/src/tox_current_env/hooks4.py b/src/tox_current_env/hooks4.py index 972cf03..40a263b 100644 --- a/src/tox_current_env/hooks4.py +++ b/src/tox_current_env/hooks4.py @@ -87,11 +87,11 @@ def tox_add_core_config(core_conf, state): def tox_add_env_config(env_conf, state): opt = state.conf.options # This allows all external commands. - # All of them are extenal for us. - # passenv is here because `TOX_TESTENV_PASSENV` - # no longer works in tox 4. + # All of them are external for us. + # Because tox 4 no longer reads $TOX_TESTENV_PASSENV, + # this plugin always passes all environment variables by default. if opt.current_env: - allow_external_cmds = MemoryLoader(allowlist_externals=["*"], passenv=["*"]) + 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. diff --git a/tests/test_integration_tox3.py b/tests/test_integration_tox3.py index 406c795..0f5eb35 100644 --- a/tests/test_integration_tox3.py +++ b/tests/test_integration_tox3.py @@ -564,3 +564,17 @@ def test_self_is_installed_with_regular_tox(projdir, usedevelop): config["testenv"]["usedevelop"] = str(usedevelop) result = tox("-e", NATIVE_TOXENV, quiet=False) assert "test==0.0.0" in result.stdout or "test @ file://" in result.stdout + + +@pytest.mark.parametrize("passenv", [None, "different list", "__var", "*"]) +def test_passenv(projdir, passenv): + with modify_config(projdir / "tox.ini") as config: + config["testenv"]["commands"] = """python -c 'import os; print(os.getenv("__var"))'""" + if passenv is not None: + existing = config["testenv"].get("passenv", "") + " " + config["testenv"]["passenv"] = existing + passenv + env = {"__var": "assertme"} + result = tox("-e", NATIVE_TOXENV, "--current-env", env=env, quiet=False) + assert result.returncode == 0 + assert "\nassertme\n" in result.stdout + assert "\nNone\n" not in result.stdout diff --git a/tests/test_integration_tox4.py b/tests/test_integration_tox4.py index 1aee1e4..3ab9fbd 100644 --- a/tests/test_integration_tox4.py +++ b/tests/test_integration_tox4.py @@ -395,3 +395,30 @@ def test_self_is_installed_with_regular_tox(projdir, usedevelop): assert "test-0.0.0" in result.stdout if usedevelop: assert "test-0.0.0-0.editable" in result.stdout + + +@pytest.mark.parametrize("passenv", [None, "different list", "__var", "*"]) +def test_passenv(projdir, passenv): + with modify_config(projdir / "tox.ini") as config: + config["testenv"]["commands"] = """python -c 'import os; print(os.getenv("__var"))'""" + if passenv is not None: + existing = config["testenv"].get("passenv", "") + " " + config["testenv"]["passenv"] = existing + passenv + env = {"__var": "assertme"} + result = tox("-e", NATIVE_TOXENV, "--current-env", env=env, quiet=False) + assert result.returncode == 0 + assert "\nassertme\n" in result.stdout + assert "\nNone\n" not in result.stdout + + +@pytest.mark.parametrize("pass_env", [None, "different\nlist", "__var", "*"]) +def test_pass_env(projdir, pass_env): + with modify_config(projdir / "tox.ini") as config: + config["testenv"]["commands"] = """python -c 'import os; print(os.getenv("__var"))'""" + if pass_env is not None: + config["testenv"]["pass_env"] = pass_env + env = {"__var": "assertme"} + result = tox("-e", NATIVE_TOXENV, "--current-env", env=env, quiet=False) + assert result.returncode == 0 + assert "\nassertme\n" in result.stdout + assert "\nNone\n" not in result.stdout diff --git a/tests/utils.py b/tests/utils.py index ea180f4..377b2b3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -39,7 +39,7 @@ def tox(*args, quiet=True, **kwargs): q = ("-q",) if quiet else () env = dict(os.environ) env.pop("TOX_WORK_DIR", None) - kwargs.setdefault("env", env) + kwargs["env"] = {**env, **kwargs.get("env", {})} try: print("current", os.getcwd(), "running in", kwargs["cwd"]) cp = subprocess.run((sys.executable, "-m", "tox") + q + args, **kwargs) From 3438c5679a03f373c78d6421f68e58749a290efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 14 Dec 2022 14:53:08 +0100 Subject: [PATCH 27/27] Release 0.0.9 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 073588a..b0ba8ce 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def long_description(): author_email="miro@hroncok.cz", url="https://github.com/fedora-python/tox-current-env", license="MIT", - version="0.0.8", + version="0.0.9", package_dir={"": "src"}, packages=find_packages("src"), entry_points={"tox": ["current-env = tox_current_env.hooks"]},