summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMasen Furer <m_github@0x26.net>2023-01-25 11:24:56 -0800
committerGitHub <noreply@github.com>2023-01-25 11:24:56 -0800
commit8736549a48c8467045ea2a56edddc9d4b17a4546 (patch)
treed073a977b0871a017a2899f64216bcfc1f5ebc1c /src
parentd291752f6fb45a70415e45d92e0ade3023fec392 (diff)
downloadtox-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.py1
-rw-r--r--src/tox/tox_env/python/pip/pip_install.py51
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"]