diff options
author | Masen Furer <m_github@0x26.net> | 2023-01-25 11:24:56 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-25 11:24:56 -0800 |
commit | 8736549a48c8467045ea2a56edddc9d4b17a4546 (patch) | |
tree | d073a977b0871a017a2899f64216bcfc1f5ebc1c /src | |
parent | d291752f6fb45a70415e45d92e0ade3023fec392 (diff) | |
download | tox-git-8736549a48c8467045ea2a56edddc9d4b17a4546.tar.gz |
Enforce constraints during install_package_deps (#2888)
Fix https://github.com/tox-dev/tox/issues/2386
Diffstat (limited to 'src')
-rw-r--r-- | src/tox/pytest.py | 1 | ||||
-rw-r--r-- | src/tox/tox_env/python/pip/pip_install.py | 51 |
2 files changed, 51 insertions, 1 deletions
diff --git a/src/tox/pytest.py b/src/tox/pytest.py index ae211252..fb83721c 100644 --- a/src/tox/pytest.py +++ b/src/tox/pytest.py @@ -525,6 +525,7 @@ __all__ = ( "LogCaptureFixture", "TempPathFactory", "MonkeyPatch", + "SubRequest", "ToxRunOutcome", "ToxProject", "ToxProjectCreator", diff --git a/src/tox/tox_env/python/pip/pip_install.py b/src/tox/tox_env/python/pip/pip_install.py index 2136e862..fcd445a5 100644 --- a/src/tox/tox_env/python/pip/pip_install.py +++ b/src/tox/tox_env/python/pip/pip_install.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging from collections import defaultdict +from pathlib import Path from typing import Any, Callable, Sequence from packaging.requirements import Requirement @@ -38,6 +39,18 @@ class Pip(Installer[Python]): post_process=self.post_process_install_command, desc="command used to install packages", ) + self._env.conf.add_config( + keys=["constrain_package_deps"], + of_type=bool, + default=True, + desc="If true, apply constraints during install_package_deps.", + ) + self._env.conf.add_config( + keys=["use_frozen_constraints"], + of_type=bool, + default=False, + desc="Use the exact versions of installed deps as constraints, otherwise use the listed deps.", + ) if self._with_list_deps: # pragma: no branch self._env.conf.add_config( keys=["list_dependencies_command"], @@ -81,6 +94,17 @@ class Pip(Installer[Python]): logging.warning(f"pip cannot install {arguments!r}") raise SystemExit(1) + def constraints_file(self) -> Path: + return Path(self._env.env_dir) / "constraints.txt" + + @property + def constrain_package_deps(self) -> bool: + return bool(self._env.conf["constrain_package_deps"]) + + @property + def use_frozen_constraints(self) -> bool: + return bool(self._env.conf["use_frozen_constraints"]) + def _install_requirement_file(self, arguments: PythonDeps, section: str, of_type: str) -> None: try: new_options, new_reqs = arguments.unroll() @@ -90,7 +114,16 @@ class Pip(Installer[Python]): new_constraints: list[str] = [] for req in new_reqs: (new_constraints if req.startswith("-c ") else new_requirements).append(req) - new = {"options": new_options, "requirements": new_requirements, "constraints": new_constraints} + constraint_options = { + "constrain_package_deps": self.constrain_package_deps, + "use_frozen_constraints": self.use_frozen_constraints, + } + new = { + "options": new_options, + "requirements": new_requirements, + "constraints": new_constraints, + "constraint_options": constraint_options, + } # if option or constraint change in any way recreate, if the requirements change only if some are removed with self._env.cache.compare(new, section, of_type) as (eq, old): if not eq: # pragma: no branch @@ -100,9 +133,16 @@ class Pip(Installer[Python]): missing_requirement = set(old["requirements"]) - set(new_requirements) if missing_requirement: raise Recreate(f"requirements removed: {' '.join(missing_requirement)}") + old_constraint_options = old.get("constraint_options") + if old_constraint_options != constraint_options: + msg = f"constraint options changed: old={old_constraint_options} new={constraint_options}" + raise Recreate(msg) args = arguments.as_root_args if args: # pragma: no branch self._execute_installer(args, of_type) + if self.constrain_package_deps and not self.use_frozen_constraints: + combined_constraints = new_requirements + [c.lstrip("-c ") for c in new_constraints] + self.constraints_file().write_text("\n".join(combined_constraints)) @staticmethod def _recreate_if_diff(of_type: str, new_opts: list[str], old_opts: list[str], fmt: Callable[[str], str]) -> None: @@ -155,10 +195,19 @@ class Pip(Installer[Python]): self._execute_installer(install_args, of_type) def _execute_installer(self, deps: Sequence[Any], of_type: str) -> None: + if of_type == "package_deps" and self.constrain_package_deps: + constraints_file = self.constraints_file() + if constraints_file.exists(): + deps = [*deps, f"-c{constraints_file}"] + cmd = self.build_install_cmd(deps) outcome = self._env.execute(cmd, stdin=StdinSource.OFF, run_id=f"install_{of_type}") outcome.assert_success() + if of_type == "deps" and self.constrain_package_deps and self.use_frozen_constraints: + # freeze installed deps for use as constraints + self.constraints_file().write_text("\n".join(self.installed())) + def build_install_cmd(self, args: Sequence[str]) -> list[str]: try: cmd: Command = self._env.conf["install_command"] |