diff options
-rw-r--r-- | docs/changelog/1847.feature.rst | 2 | ||||
-rw-r--r-- | docs/changelog/1848.feature.rst | 2 | ||||
-rw-r--r-- | docs/changelog/1849.feature.rst | 1 | ||||
-rw-r--r-- | setup.cfg | 1 | ||||
-rw-r--r-- | src/tox/config/cli/parser.py | 2 | ||||
-rw-r--r-- | src/tox/execute/local_sub_process/read_via_thread_windows.py | 2 | ||||
-rw-r--r-- | src/tox/plugin/manager.py | 2 | ||||
-rw-r--r-- | src/tox/tox_env/api.py | 18 | ||||
-rw-r--r-- | src/tox/tox_env/python/api.py | 10 | ||||
-rw-r--r-- | src/tox/tox_env/python/virtual_env/api.py | 69 | ||||
-rw-r--r-- | src/tox/tox_env/python/virtual_env/package/api.py | 6 | ||||
-rw-r--r-- | tests/config/test_set_env.py | 4 | ||||
-rw-r--r-- | tests/session/cmd/test_show_config.py | 4 | ||||
-rw-r--r-- | tests/test_provision.py | 2 | ||||
-rw-r--r-- | tests/tox_env/python/virtual_env/test_package.py | 4 | ||||
-rw-r--r-- | tests/tox_env/python/virtual_env/test_virtualenv_api.py | 107 |
16 files changed, 200 insertions, 36 deletions
diff --git a/docs/changelog/1847.feature.rst b/docs/changelog/1847.feature.rst new file mode 100644 index 00000000..3be70211 --- /dev/null +++ b/docs/changelog/1847.feature.rst @@ -0,0 +1,2 @@ +Support the ``system_site_packages``/``sitepackages`` flag for virtual environment based tox environments - +by :user:`gaborbernat`. diff --git a/docs/changelog/1848.feature.rst b/docs/changelog/1848.feature.rst new file mode 100644 index 00000000..31000a91 --- /dev/null +++ b/docs/changelog/1848.feature.rst @@ -0,0 +1,2 @@ +Support the ``always_copy``/``alwayscopy`` flag for virtual environment based tox environments - +by :user:`gaborbernat`. diff --git a/docs/changelog/1849.feature.rst b/docs/changelog/1849.feature.rst new file mode 100644 index 00000000..add6fbfd --- /dev/null +++ b/docs/changelog/1849.feature.rst @@ -0,0 +1 @@ +Support the ``download`` flag for virtual environment based tox environments - by :user:`gaborbernat`. @@ -112,7 +112,6 @@ subtract_omit = */.tox/* [mypy] python_version = 3.6 -disallow_any_unimported = True disallow_any_generics = True disallow_subclassing_any = True disallow_untyped_calls = True diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py index b1804793..f47029ba 100644 --- a/src/tox/config/cli/parser.py +++ b/src/tox/config/cli/parser.py @@ -87,7 +87,7 @@ class HelpFormatter(ArgumentDefaultsHelpFormatter): text: str = super()._get_help_string(action) or "" # noqa if hasattr(action, "default_source"): default = " (default: %(default)s)" - if text.endswith(default): + if text.endswith(default): # pragma: no branch text = f"{text[: -len(default)]} (default: %(default)s -> from %(default_source)s)" return text diff --git a/src/tox/execute/local_sub_process/read_via_thread_windows.py b/src/tox/execute/local_sub_process/read_via_thread_windows.py index 9bec8962..1f4d29a1 100644 --- a/src/tox/execute/local_sub_process/read_via_thread_windows.py +++ b/src/tox/execute/local_sub_process/read_via_thread_windows.py @@ -15,7 +15,7 @@ class ReadViaThreadWindows(ReadViaThread): # pragma: win32 cover def __init__(self, file_no: int, handler: Callable[[bytes], None], name: str, drain: bool) -> None: super().__init__(file_no, handler, name, drain) self.closed = False - self._ov: Optional[_overlapped.Overlapped] = None # type: ignore[no-any-unimported] + self._ov: Optional[_overlapped.Overlapped] = None self._waiting_for_read = False def _read_stream(self) -> None: diff --git a/src/tox/plugin/manager.py b/src/tox/plugin/manager.py index ee3e42f8..55edb57c 100644 --- a/src/tox/plugin/manager.py +++ b/src/tox/plugin/manager.py @@ -21,7 +21,7 @@ from . import NAME, spec class Plugin: def __init__(self) -> None: - self.manager: pluggy.PluginManager = pluggy.PluginManager(NAME) # type: ignore[no-any-unimported] + self.manager: pluggy.PluginManager = pluggy.PluginManager(NAME) self.manager.add_hookspecs(spec) internal_plugins = ( diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py index fe8888a5..1bff9fec 100644 --- a/src/tox/tox_env/api.py +++ b/src/tox/tox_env/api.py @@ -210,10 +210,26 @@ class ToxEnv(ABC): set_env: SetEnv = self.conf["set_env"] for key in set_env: result[key] = set_env.load(key) - result["PATH"] = os.pathsep.join([str(i) for i in self._paths] + os.environ.get("PATH", "").split(os.pathsep)) + result["PATH"] = self.paths_env() self._env_vars = result return result + @property + def paths(self) -> List[Path]: + return self._paths + + @paths.setter + def paths(self, value: List[Path]) -> None: + self._paths = value + if self._env_vars is not None: # pragma: no branch # also update the environment variables with the new value + self._env_vars["PATH"] = self.paths_env() + + def paths_env(self) -> str: + # remove duplicates and prepend the tox env paths + values = dict.fromkeys(str(i) for i in self.paths) + values.update(dict.fromkeys(os.environ.get("PATH", "").split(os.pathsep))) + return os.pathsep.join(values) + def execute( self, cmd: Sequence[Union[Path, str]], diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py index f2950dd7..cc2ffd68 100644 --- a/src/tox/tox_env/python/api.py +++ b/src/tox/tox_env/python/api.py @@ -158,9 +158,13 @@ class Python(ToxEnv, ABC): with self._cache.compare(conf, Python.__name__) as (eq, old): if eq is False: # if changed create self.create_python_env() - self._paths = self.paths() + self.paths = self.python_env_paths() # now that the environment exist we can add them to the path super().setup() + @abstractmethod + def python_env_paths(self) -> List[Path]: + raise NotImplementedError + def setup_has_been_done(self) -> None: """called when setup is done""" super().setup_has_been_done() @@ -228,10 +232,6 @@ class Python(ToxEnv, ABC): raise NotImplementedError @abstractmethod - def paths(self) -> List[Path]: - raise NotImplementedError - - @abstractmethod def install_python_packages(self, packages: PythonDeps, of_type: str, no_deps: bool = False) -> None: raise NotImplementedError diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py index 5af0a504..fb28a5b7 100644 --- a/src/tox/tox_env/python/virtual_env/api.py +++ b/src/tox/tox_env/python/virtual_env/api.py @@ -10,6 +10,7 @@ from virtualenv.create.creator import Creator from virtualenv.run.session import Session from tox.config.cli.parser import DEFAULT_VERBOSITY, Parsed +from tox.config.loader.str_convert import StrConvert from tox.config.sets import CoreConfigSet, EnvConfigSet from tox.execute.api import Execute, Outcome, StdinSource from tox.execute.local_sub_process import LocalSubProcessExecutor @@ -25,9 +26,38 @@ class VirtualEnv(Python, ABC): def __init__( self, conf: EnvConfigSet, core: CoreConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler ) -> None: - self._virtualenv_session: Optional[Session] = None # type: ignore[no-any-unimported] + self._virtualenv_session: Optional[Session] = None super().__init__(conf, core, options, journal, log_handler) + def register_config(self) -> None: + super().register_config() + self.conf.add_config( + keys=["system_site_packages", "sitepackages"], + of_type=bool, + default=lambda conf, name: StrConvert().to_bool( + self.environment_variables.get("VIRTUALENV_SYSTEM_SITE_PACKAGES", "False") + ), + desc="create virtual environments that also have access to globally installed packages.", + ) + self.conf.add_config( + keys=["always_copy", "alwayscopy"], + of_type=bool, + default=lambda conf, name: StrConvert().to_bool( + self.environment_variables.get( + "VIRTUALENV_COPIES", self.environment_variables.get("VIRTUALENV_ALWAYS_COPY", "False") + ) + ), + desc="force virtualenv to always copy rather than symlink", + ) + self.conf.add_config( + keys=["download"], + of_type=bool, + default=lambda conf, name: StrConvert().to_bool( + self.environment_variables.get("VIRTUALENV_DOWNLOAD", "False") + ), + desc="true if you want virtualenv to upgrade pip/wheel/setuptools to the latest version", + ) + def default_pass_env(self) -> List[str]: env = super().default_pass_env() env.append("PIP_*") # we use pip as installer @@ -37,28 +67,37 @@ class VirtualEnv(Python, ABC): def default_set_env(self) -> Dict[str, str]: env = super().default_set_env() env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" - env["VIRTUALENV_NO_PERIODIC_UPDATE"] = "1" return env def build_executor(self) -> Execute: return LocalSubProcessExecutor(self.options.is_colored) @property - def session(self) -> Session: # type: ignore[no-any-unimported] + def session(self) -> Session: if self._virtualenv_session is None: - args = [ - "--clear", - "--no-periodic-update", - str(cast(Path, self.conf["env_dir"])), - ] - base_python: List[str] = self.conf["base_python"] - for base in base_python: - args.extend(["-p", base]) - self._virtualenv_session = session_via_cli(args, setup_logging=False) + self._virtualenv_session = session_via_cli( + [str(cast(Path, self.conf["env_dir"]))], + options=None, + setup_logging=False, + env=self.virtualenv_env_vars(), + ) return self._virtualenv_session + def virtualenv_env_vars(self) -> Dict[str, str]: + env = self.environment_variables.copy() + base_python: List[str] = self.conf["base_python"] + if "VIRTUALENV_CLEAR" not in env: + env["VIRTUALENV_CLEAR"] = "True" + if "VIRTUALENV_NO_PERIODIC_UPDATE" not in env: + env["VIRTUALENV_NO_PERIODIC_UPDATE"] = "True" + env["VIRTUALENV_SYSTEM_SITE_PACKAGES"] = str(self.conf["system_site_packages"]) + env["VIRTUALENV_COPIES"] = str(self.conf["always_copy"]) + env["VIRTUALENV_DOWNLOAD"] = str(self.conf["download"]) + env["VIRTUALENV_PYTHON"] = "\n".join(base_python) + return env + @property - def creator(self) -> Creator: # type: ignore[no-any-unimported] + def creator(self) -> Creator: return self.session.creator def create_python_env(self) -> None: @@ -79,10 +118,10 @@ class VirtualEnv(Python, ABC): extra_version_info=None, ) - def paths(self) -> List[Path]: + def python_env_paths(self) -> List[Path]: """Paths to add to the executable""" # we use the original executable as shims may be somewhere else - return list({self.creator.bin_dir, self.creator.script_dir}) + return list(dict.fromkeys((self.creator.bin_dir, self.creator.script_dir))) def env_site_package_dir(self) -> Path: return cast(Path, self.creator.purelib) diff --git a/src/tox/tox_env/python/virtual_env/package/api.py b/src/tox/tox_env/python/virtual_env/package/api.py index 8bf28c5b..b3a0a11b 100644 --- a/src/tox/tox_env/python/virtual_env/package/api.py +++ b/src/tox/tox_env/python/virtual_env/package/api.py @@ -90,7 +90,7 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend): ) -> None: VirtualEnv.__init__(self, conf, core, options, journal, log_handler) Frontend.__init__(self, *Frontend.create_args_from_folder(core["tox_root"])) - self._distribution_meta: Optional[PathDistribution] = None # type: ignore[no-any-unimported] + self._distribution_meta: Optional[PathDistribution] = None self._build_requires: Optional[Tuple[Requirement]] = None self._build_wheel_cache: Optional[WheelResult] = None self._backend_executor: Optional[LocalSubProcessPep517Executor] = None @@ -217,9 +217,7 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend): return self._package_dependencies @staticmethod - def discover_package_dependencies( # type: ignore[no-any-unimported] - meta: PathDistribution, extras: Set[str] - ) -> List[Requirement]: + def discover_package_dependencies(meta: PathDistribution, extras: Set[str]) -> List[Requirement]: result: List[Requirement] = [] requires = meta.requires or [] for req_str in requires: diff --git a/tests/config/test_set_env.py b/tests/config/test_set_env.py index df59830b..ef6ae8d3 100644 --- a/tests/config/test_set_env.py +++ b/tests/config/test_set_env.py @@ -44,9 +44,9 @@ def eval_set_env(tox_project: ToxProjectCreator) -> EvalSetEnv: def test_set_env_default(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) -> None: set_env = eval_set_env("") keys = list(set_env) - assert keys == ["PIP_DISABLE_PIP_VERSION_CHECK", "VIRTUALENV_NO_PERIODIC_UPDATE"] + assert keys == ["PIP_DISABLE_PIP_VERSION_CHECK"] values = [set_env.load(k) for k in keys] - assert values == ["1", "1"] + assert values == ["1"] def test_set_env_self_key(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) -> None: diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py index 7c196ee8..1cd3fcda 100644 --- a/tests/session/cmd/test_show_config.py +++ b/tests/session/cmd/test_show_config.py @@ -45,7 +45,7 @@ def test_show_config_commands(tox_project: ToxProjectCreator) -> None: ) outcome = project.run("c") outcome.assert_success() - env_config = outcome.state.tox_env("py").conf + env_config = outcome.env_conf("py") assert env_config["commands_pre"] == [Command(args=["python", "-c", 'import sys; print("start", sys.executable)'])] assert env_config["commands"] == [ Command(args=["pip", "config", "list"]), @@ -90,7 +90,7 @@ def test_pass_env_config_default(tox_project: ToxProjectCreator, stdout_is_atty: mocker.patch("sys.stdout.isatty", return_value=stdout_is_atty) project = tox_project({"tox.ini": ""}) outcome = project.run("c", "-e", "py", "-k", "pass_env") - pass_env = outcome.state.tox_env("py").conf["pass_env"] + pass_env = outcome.env_conf("py")["pass_env"] is_win = sys.platform == "win32" expected = ( (["COMSPEC"] if is_win else []) diff --git a/tests/test_provision.py b/tests/test_provision.py index c08024ae..365ac7a5 100644 --- a/tests/test_provision.py +++ b/tests/test_provision.py @@ -124,7 +124,7 @@ def test_provision_requires_ok( assert "py" in log_report["testenvs"] # recreate without recreating the provisioned env - provision_env = result_first.state.tox_env(".tox").conf["env_dir"] + provision_env = result_first.env_conf(".tox")["env_dir"] result_recreate_no_pr = proj.run("r", "--recreate", "--no-recreate-provision") result_recreate_no_pr.assert_success() assert prov_msg in result_recreate_no_pr.out diff --git a/tests/tox_env/python/virtual_env/test_package.py b/tests/tox_env/python/virtual_env/test_package.py index 378a6fb7..9ba1f14e 100644 --- a/tests/tox_env/python/virtual_env/test_package.py +++ b/tests/tox_env/python/virtual_env/test_package.py @@ -23,9 +23,9 @@ def test_tox_ini_package_type_valid(tox_project: ToxProjectCreator, pkg_type: st proj = tox_project({"tox.ini": f"[testenv]\npackage={pkg_type}"}) result = proj.run("c", "-k", "package_tox_env_type") result.assert_success() - res = result.state.tox_env("py").conf["package"] + res = result.env_conf("py")["package"] assert res is getattr(PackageType, pkg_type) - got_type = result.state.tox_env("py").conf["package_tox_env_type"] + got_type = result.env_conf("py")["package_tox_env_type"] assert got_type == "virtualenv-pep-517" diff --git a/tests/tox_env/python/virtual_env/test_virtualenv_api.py b/tests/tox_env/python/virtual_env/test_virtualenv_api.py new file mode 100644 index 00000000..8320b9dc --- /dev/null +++ b/tests/tox_env/python/virtual_env/test_virtualenv_api.py @@ -0,0 +1,107 @@ +import os + +import pytest +from pytest_mock import MockerFixture +from virtualenv import session_via_cli +from virtualenv.config.cli.parser import VirtualEnvOptions + +from tox.pytest import MonkeyPatch, ToxProject, ToxProjectCreator + + +@pytest.fixture() +def virtualenv_opt(monkeypatch: MonkeyPatch, mocker: MockerFixture) -> VirtualEnvOptions: + for key in os.environ: + if key.startswith("VIRTUALENV_"): # pragma: no cover + monkeypatch.delenv(key) # pragma: no cover + opts = VirtualEnvOptions() + mocker.patch( + "tox.tox_env.python.virtual_env.api.session_via_cli", + side_effect=lambda args, options, setup_logging, env: session_via_cli(args, opts, setup_logging, env), + ) + return opts + + +def test_virtualenv_default_settings(tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions) -> None: + proj = tox_project({"tox.ini": "[testenv]\npackage=skip"}) + result = proj.run("r", "-e", "py") + result.assert_success() + + conf = result.env_conf("py") + assert conf["system_site_packages"] is False + assert conf["always_copy"] is False + assert conf["download"] is False + + assert virtualenv_opt.clear is True + assert virtualenv_opt.system_site is False + assert virtualenv_opt.download is False + assert virtualenv_opt.copies is False + assert virtualenv_opt.no_periodic_update is True + assert virtualenv_opt.python == ["py"] + + +def test_virtualenv_flipped_settings( + tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch +) -> None: + proj = tox_project( + {"tox.ini": "[testenv]\npackage=skip\nsystem_site_packages=True\nalways_copy=True\ndownload=True"} + ) + monkeypatch.setenv("VIRTUALENV_CLEAR", "0") + + result = proj.run("r", "-e", "py") + result.assert_success() + + conf = result.env_conf("py") + assert conf["system_site_packages"] is True + assert conf["always_copy"] is True + assert conf["download"] is True + + assert virtualenv_opt.clear is False + assert virtualenv_opt.system_site is True + assert virtualenv_opt.download is True + assert virtualenv_opt.copies is True + assert virtualenv_opt.python == ["py"] + + +def test_virtualenv_env_ignored_if_set( + tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch +) -> None: + ini = "[testenv]\npackage=skip\nsystem_site_packages=True\nalways_copy=True\ndownload=True" + proj = tox_project({"tox.ini": ini}) + monkeypatch.setenv("VIRTUALENV_COPIES", "0") + monkeypatch.setenv("VIRTUALENV_DOWNLOAD", "0") + monkeypatch.setenv("VIRTUALENV_SYSTEM_SITE_PACKAGES", "0") + run_and_check_set(proj, virtualenv_opt) + + +def test_virtualenv_env_used_if_not_set( + tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch +) -> None: + proj = tox_project({"tox.ini": "[testenv]\npackage=skip"}) + monkeypatch.setenv("VIRTUALENV_COPIES", "1") + monkeypatch.setenv("VIRTUALENV_DOWNLOAD", "1") + monkeypatch.setenv("VIRTUALENV_SYSTEM_SITE_PACKAGES", "1") + run_and_check_set(proj, virtualenv_opt) + + +def run_and_check_set(proj: ToxProject, virtualenv_opt: VirtualEnvOptions) -> None: + result = proj.run("r", "-e", "py") + result.assert_success() + conf = result.env_conf("py") + assert conf["system_site_packages"] is True + assert conf["always_copy"] is True + assert conf["download"] is True + assert virtualenv_opt.system_site is True + assert virtualenv_opt.download is True + assert virtualenv_opt.copies is True + + +def test_honor_set_env_for_clear_periodic_update( + tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch +) -> None: + ini = "[testenv]\npackage=skip\nset_env=\n VIRTUALENV_CLEAR=0\n VIRTUALENV_NO_PERIODIC_UPDATE=0" + proj = tox_project({"tox.ini": ini}) + result = proj.run("r", "-e", "py") + result.assert_success() + + assert virtualenv_opt.clear is False + assert virtualenv_opt.no_periodic_update is False |