Skip to content

Commit a43427f

Browse files
🐛 fix(pip): skip constrain_package_deps when constraints is set (#3948)
When both `constraints` and `constrain_package_deps = true` are configured, tox passes two `-c` flags to pip during `install_package_deps`: one for the user's constraints file and one for the auto-generated constraints file. 🔧 pip merges constraints from multiple files via specifier intersection, so overlapping packages with different versions produce unsatisfiable constraints and a resolver error. The `constraints` option already applies to both `install_deps` and `install_package_deps` phases, making `constrain_package_deps` redundant when it is set. This change skips generating and applying the auto-generated constraints file whenever user constraints are configured. The three affected code paths (non-frozen write, frozen write, and package_deps application) all gate on a new `_has_constraints` property that checks whether the user provided constraint content. Users who currently set both options will see the same effective behavior, minus the conflicting duplicate `-c` flag. If they relied on auto-constraining for packages absent from their constraints file, they should add those pins to their constraints file directly. Fixes #3945 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 27b68b3 commit a43427f

6 files changed

Lines changed: 77 additions & 6 deletions

File tree

docs/changelog/3945.bugfix.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When the ``constraints`` configuration option is set, ``constrain_package_deps`` and ``use_frozen_constraints`` are now
2+
ignored. Previously, both the user-provided constraints file and the auto-generated constraints file were passed to pip
3+
during ``install_package_deps``, which could cause resolver conflicts when the same package appeared in both files - by
4+
:user:`gaborbernat`. (:issue:`3945`)

docs/how-to/usage.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,13 @@ error when package dependencies conflict with test dependencies.
763763
For stronger guarantees, set ``use_frozen_constraints = true`` to generate constraints from the exact installed versions
764764
(via ``pip freeze``). This catches incompatibilities with any previously installed dependency.
765765

766+
.. note::
767+
768+
When :ref:`constraints` is set, ``constrain_package_deps`` and ``use_frozen_constraints`` have no effect. The
769+
:ref:`constraints` option already applies to both ``install_deps`` and ``install_package_deps`` phases, so the
770+
auto-generated constraints file is not created. If you need to pin specific dependency versions during package
771+
installation, add them to your constraints file directly.
772+
766773
.. note::
767774

768775
Constraint files are a subset of requirement files. You can pass a constraint file wherever a requirement file is

docs/onboarding.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,8 @@ The concrete installer (`tox_env/python/pip/ <https://github.com/tox-dev/tox/blo
746746
has the following features:
747747

748748
- **Incremental installs** compare new vs cached requirements and only install changes.
749-
- It supports ``pip_pre``, constraints, and ``use_frozen_constraints``.
749+
- It supports ``pip_pre``, constraints, and ``use_frozen_constraints``. When the ``constraints`` option is set, it takes
750+
precedence over ``constrain_package_deps``/``use_frozen_constraints``.
750751
- The ``{packages}`` placeholder in ``install_command`` is replaced with actual arguments.
751752
- See :ref:`exec-execution` for how installation commands are executed.
752753

docs/reference/config.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2068,6 +2068,9 @@ Pip installer
20682068
package dependencies during ``install_package_deps`` stage. When this value is set to false, any conflicting package
20692069
dependencies will override explicit dependencies and constraints passed to :ref:`deps`.
20702070

2071+
This option has no effect when :ref:`constraints` is set, as the constraints option already applies to package
2072+
dependency installation.
2073+
20712074
.. conf::
20722075
:keys: use_frozen_constraints
20732076
:default: false
@@ -2076,7 +2079,7 @@ Pip installer
20762079
When ``use_frozen_constraints`` is true, then tox will use the ``list_dependencies_command`` to enumerate package
20772080
versions in order to create ``{env_dir}{/}constraints.txt``. Otherwise the package specifications explicitly listed
20782081
under ``deps`` (or in requirements / constraints files referenced in ``deps``) will be used as the constraints. If
2079-
``constrain_package_deps`` is false, then this setting has no effect.
2082+
``constrain_package_deps`` is false or :ref:`constraints` is set, then this setting has no effect.
20802083

20812084
********************
20822085
User configuration

src/tox/tox_env/python/pip/pip_install.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def _install_requirement_file(self, arguments: PythonDeps, section: str, of_type
200200
if args: # pragma: no branch
201201
args.extend(self.constraints.as_root_args)
202202
self._execute_installer(args, of_type)
203-
if self.constrain_package_deps and not self.use_frozen_constraints:
203+
if self.constrain_package_deps and not self.use_frozen_constraints and not self._has_constraints:
204204
combined_constraints = new_requirements + [c.removeprefix("-c ") for c in new_constraints]
205205
self.constraints_file().write_text("\n".join(combined_constraints))
206206

@@ -288,8 +288,12 @@ def _apply_force_deps(self, deps: Sequence[Requirement]) -> list[str]:
288288
forced: dict[str, Requirement] = {r.name: r for r in getattr(self._env.options, "force_dep", [])}
289289
return [str(forced.get(dep.name, dep)) for dep in deps]
290290

291+
@property
292+
def _has_constraints(self) -> bool:
293+
return bool(self.constraints.lines())
294+
291295
def _execute_installer(self, deps: Sequence[Any], of_type: str) -> None:
292-
if of_type == "package_deps" and self.constrain_package_deps:
296+
if of_type == "package_deps" and self.constrain_package_deps and not self._has_constraints:
293297
constraints_file = self.constraints_file()
294298
if constraints_file.exists():
295299
deps = [*deps, f"-c{constraints_file}"]
@@ -298,8 +302,12 @@ def _execute_installer(self, deps: Sequence[Any], of_type: str) -> None:
298302
outcome = self._env.execute(cmd, stdin=StdinSource.OFF, run_id=f"install_{of_type}")
299303
outcome.assert_success()
300304

301-
if of_type == "deps" and self.constrain_package_deps and self.use_frozen_constraints:
302-
# freeze installed deps for use as constraints
305+
if (
306+
of_type == "deps"
307+
and self.constrain_package_deps
308+
and self.use_frozen_constraints
309+
and not self._has_constraints
310+
):
303311
self.constraints_file().write_text("\n".join(self.installed()))
304312

305313
def build_install_cmd(self, args: Sequence[str]) -> list[str]:

tests/tox_env/python/pip/test_pip_install.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,54 @@ def test_constrain_package_deps(
457457
assert not constraints_file.exists()
458458

459459

460+
@pytest.mark.parametrize("use_frozen_constraints", [True, False])
461+
def test_constraints_option_disables_constrain_package_deps(
462+
tox_project: ToxProjectCreator,
463+
demo_pkg_inline: Path,
464+
use_frozen_constraints: bool,
465+
) -> None:
466+
toml = (demo_pkg_inline / "pyproject.toml").read_text()
467+
proj = tox_project({
468+
"pyproject.toml": toml.replace("requires = []", 'requires = ["setuptools"]')
469+
+ '\n[project]\nname = "demo"\nversion = "0.1"\ndependencies = ["foo > 2"]',
470+
"build.py": (demo_pkg_inline / "build.py").read_text(),
471+
"constraints.txt": "coo==1.2.3",
472+
"tox.ini": (
473+
"[testenv]\npackage=wheel\n"
474+
"constrain_package_deps = true\n"
475+
f"use_frozen_constraints = {use_frozen_constraints}\n"
476+
"deps = coo==1.2.3\n"
477+
"constraints = constraints.txt"
478+
),
479+
})
480+
execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
481+
result = proj.run("r")
482+
result.assert_success()
483+
484+
constraints_file = proj.path / ".tox" / "py" / "constraints.txt"
485+
assert not constraints_file.exists()
486+
487+
for call in execute_calls.call_args_list:
488+
if call[0][3].run_id == "install_package_deps":
489+
cmd = call[0][3].cmd
490+
assert f"-c{constraints_file}" not in cmd
491+
assert "-c" in cmd
492+
assert "constraints.txt" in cmd
493+
494+
exp_run_ids = ["install_deps"]
495+
exp_run_ids.extend([
496+
"install_requires",
497+
"_optional_hooks",
498+
"get_requires_for_build_wheel",
499+
"build_wheel",
500+
"install_package_deps",
501+
"install_package",
502+
"_exit",
503+
])
504+
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
505+
assert run_ids == exp_run_ids
506+
507+
460508
def test_pip_resolution_env_var_change_reinstalls(tox_project: ToxProjectCreator) -> None:
461509
proj = tox_project({
462510
"tox.ini": """

0 commit comments

Comments
 (0)