Skip to content

Commit b509c70

Browse files
committed
Add '[testenv] constraints' setting
This allows users to override constraints files for all installations. Without this, it is not possible to use constraints files that include the package under test without introducing a separate pre-tox processing step to scrub said package from the constraints file. Signed-off-by: Stephen Finucane <[email protected]>
1 parent 02d34ea commit b509c70

File tree

6 files changed

+85
-18
lines changed

6 files changed

+85
-18
lines changed

docs/changelog/3350.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added ``constraints`` to allow specifying constraints files for all dependencies.

docs/config.rst

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -942,15 +942,15 @@ Python run
942942
:keys: deps
943943
:default: <empty list>
944944

945-
Name of the Python dependencies. Installed into the environment prior to project after environment creation, but
945+
Python dependencies. Installed into the environment prior to project after environment creation, but
946946
before package installation. All installer commands are executed using the :ref:`tox_root` as the current working
947947
directory. Each value must be one of:
948948

949949
- a Python dependency as specified by :pep:`440`,
950950
- a `requirement file <https://pip.pypa.io/en/stable/user_guide/#requirements-files>`_ when the value starts with
951-
``-r`` (followed by a file path),
951+
``-r`` (followed by a file path or URL),
952952
- a `constraint file <https://pip.pypa.io/en/stable/user_guide/#constraints-files>`_ when the value starts with
953-
``-c`` (followed by a file path).
953+
``-c`` (followed by a file path or URL).
954954

955955
If you are only defining :pep:`508` requirements (aka no pip requirement files), you should use
956956
:ref:`dependency_groups` instead.
@@ -977,6 +977,21 @@ Python run
977977
-r requirements.txt
978978
-c constraints.txt
979979
980+
.. note::
981+
982+
:ref:`constraints` is the preferred way to specify constraints files since they will apply to package dependencies
983+
also.
984+
985+
.. conf::
986+
:keys: constraints
987+
:default: <empty list>
988+
:version_added: 4.28.0
989+
990+
`Constraints files <https://pip.pypa.io/en/stable/user_guide/#constraints-files>`_ to use during package and
991+
dependency installation. Provided constraints files will be used when installing package dependencies and any
992+
additional dependencies specified in :ref:`deps`, but will not be used when installing the package itself.
993+
Each value must be a file path or URL.
994+
980995
.. conf::
981996
:keys: use_develop, usedevelop
982997
:default: false
@@ -1210,7 +1225,6 @@ Pip installer
12101225
This command will be executed only if executing on Continuous Integrations is detected (for example set environment
12111226
variable ``CI=1``) or if journal is active.
12121227

1213-
12141228
.. conf::
12151229
:keys: pip_pre
12161230
:default: false
@@ -1227,7 +1241,7 @@ Pip installer
12271241

12281242
If ``constrain_package_deps`` is true, then tox will create and use ``{env_dir}{/}constraints.txt`` when installing
12291243
package dependencies during ``install_package_deps`` stage. When this value is set to false, any conflicting package
1230-
dependencies will override explicit dependencies and constraints passed to ``deps``.
1244+
dependencies will override explicit dependencies and constraints passed to :ref:`deps`.
12311245

12321246
.. conf::
12331247
:keys: use_frozen_constraints

src/tox/tox.schema.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,14 @@
289289
},
290290
"deps": {
291291
"type": "string",
292-
"description": "Name of the python dependencies as specified by PEP-440"
292+
"description": "python dependencies with optional version specifiers, as specified by PEP-440"
293+
},
294+
"constraints": {
295+
"type": "array",
296+
"items": {
297+
"type": "string"
298+
},
299+
"description": "constraints to apply to installed python dependencies"
293300
},
294301
"dependency_groups": {
295302
"type": "array",

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import operator
55
from abc import ABC, abstractmethod
66
from collections import defaultdict
7+
from functools import partial
78
from pathlib import Path
8-
from typing import TYPE_CHECKING, Any, Callable, Sequence
9+
from typing import TYPE_CHECKING, Any, Callable, Sequence, cast
910

1011
from packaging.requirements import Requirement
1112

@@ -15,7 +16,7 @@
1516
from tox.tox_env.installer import Installer
1617
from tox.tox_env.python.api import Python
1718
from tox.tox_env.python.package import EditableLegacyPackage, EditablePackage, SdistPackage, WheelPackage
18-
from tox.tox_env.python.pip.req_file import PythonDeps
19+
from tox.tox_env.python.pip.req_file import PythonConstraints, PythonDeps
1920

2021
if TYPE_CHECKING:
2122
from tox.config.main import Config
@@ -52,6 +53,7 @@ class Pip(PythonInstallerListDependencies):
5253

5354
def _register_config(self) -> None:
5455
super()._register_config()
56+
root = self._env.core["toxinidir"]
5557
self._env.conf.add_config(
5658
keys=["pip_pre"],
5759
of_type=bool,
@@ -65,6 +67,13 @@ def _register_config(self) -> None:
6567
post_process=self.post_process_install_command,
6668
desc="command used to install packages",
6769
)
70+
self._env.conf.add_config(
71+
keys=["constraints"],
72+
of_type=PythonConstraints,
73+
factory=partial(PythonConstraints.factory, root),
74+
default=PythonConstraints("", root),
75+
desc="constraints to apply to installed python dependencies",
76+
)
6877
self._env.conf.add_config(
6978
keys=["constrain_package_deps"],
7079
of_type=bool,
@@ -110,6 +119,10 @@ def install(self, arguments: Any, section: str, of_type: str) -> None:
110119
logging.warning("pip cannot install %r", arguments)
111120
raise SystemExit(1)
112121

122+
@property
123+
def constraints(self) -> PythonConstraints:
124+
return cast("PythonConstraints", self._env.conf["constraints"])
125+
113126
def constraints_file(self) -> Path:
114127
return Path(self._env.env_dir) / "constraints.txt"
115128

@@ -122,15 +135,24 @@ def use_frozen_constraints(self) -> bool:
122135
return bool(self._env.conf["use_frozen_constraints"])
123136

124137
def _install_requirement_file(self, arguments: PythonDeps, section: str, of_type: str) -> None: # noqa: C901
138+
new_requirements: list[str] = []
139+
new_constraints: list[str] = []
140+
125141
try:
126142
new_options, new_reqs = arguments.unroll()
127143
except ValueError as exception:
128144
msg = f"{exception} for tox env py within deps"
129145
raise Fail(msg) from exception
130-
new_requirements: list[str] = []
131-
new_constraints: list[str] = []
132146
for req in new_reqs:
133147
(new_constraints if req.startswith("-c ") else new_requirements).append(req)
148+
149+
try:
150+
_, new_reqs = self.constraints.unroll()
151+
except ValueError as exception:
152+
msg = f"{exception} for tox env py within constraints"
153+
raise Fail(msg) from exception
154+
new_constraints.extend(new_reqs)
155+
134156
constraint_options = {
135157
"constrain_package_deps": self.constrain_package_deps,
136158
"use_frozen_constraints": self.use_frozen_constraints,
@@ -159,6 +181,7 @@ def _install_requirement_file(self, arguments: PythonDeps, section: str, of_type
159181
raise Recreate(msg)
160182
args = arguments.as_root_args
161183
if args: # pragma: no branch
184+
args.extend(self.constraints.as_root_args)
162185
self._execute_installer(args, of_type)
163186
if self.constrain_package_deps and not self.use_frozen_constraints:
164187
# when we drop Python 3.8 we can use the builtin `.removeprefix`
@@ -215,13 +238,18 @@ def _install_list_of_deps( # noqa: C901
215238
raise Recreate(msg) # pragma: no branch
216239
new_deps = sorted(set(groups["req"]) - set(old or []))
217240
if new_deps: # pragma: no branch
241+
new_deps.extend(self.constraints.as_root_args)
218242
self._execute_installer(new_deps, req_of_type)
219243
install_args = ["--force-reinstall", "--no-deps"]
220244
if groups["pkg"]:
245+
# we intentionally ignore constraints when installing the package itself
246+
# https://github.com/tox-dev/tox/issues/3550
221247
self._execute_installer(install_args + groups["pkg"], of_type)
222248
if groups["dev_pkg"]:
223249
for entry in groups["dev_pkg"]:
224250
install_args.extend(("-e", str(entry)))
251+
# we intentionally ignore constraints when installing the package itself
252+
# https://github.com/tox-dev/tox/issues/3550
225253
self._execute_installer(install_args, of_type)
226254

227255
def _execute_installer(self, deps: Sequence[Any], of_type: str) -> None:

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,26 @@ def _is_url_self(self, url: str) -> bool:
5656
def _pre_process(self, content: str) -> ReqFileLines:
5757
for at, line in super()._pre_process(content):
5858
if line.startswith("-r") or (line.startswith("-c") and line[2].isalpha()):
59-
found_line = f"{line[0:2]} {line[2:]}"
59+
found_line = f"{line[0:2]} {line[2:]}" # normalize
6060
else:
6161
found_line = line
6262
yield at, found_line
6363

6464
def lines(self) -> list[str]:
6565
return self._raw.splitlines()
6666

67-
@staticmethod
68-
def _normalize_raw(raw: str) -> str:
67+
@classmethod
68+
def _normalize_raw(cls, raw: str) -> str:
6969
# a line ending in an unescaped \ is treated as a line continuation and the newline following it is effectively
7070
# ignored
7171
raw = "".join(raw.replace("\r", "").split("\\\n"))
7272
# for tox<4 supporting requirement/constraint files via -rreq.txt/-creq.txt
73-
lines: list[str] = [PythonDeps._normalize_line(line) for line in raw.splitlines()]
73+
lines: list[str] = [cls._normalize_line(line) for line in raw.splitlines()]
7474
adjusted = "\n".join(lines)
7575
return f"{adjusted}\n" if raw.endswith("\\\n") else adjusted # preserve trailing newline if input has it
7676

77-
@staticmethod
78-
def _normalize_line(line: str) -> str:
77+
@classmethod
78+
def _normalize_line(cls, line: str) -> str:
7979
arg_match = next(
8080
(
8181
arg
@@ -138,6 +138,23 @@ def factory(cls, root: Path, raw: object) -> PythonDeps:
138138
return cls(raw, root)
139139

140140

141+
class PythonConstraints(PythonDeps):
142+
@classmethod
143+
def _normalize_raw(cls, raw: str) -> str:
144+
# a line ending in an unescaped \ is treated as a line continuation and the newline following it is effectively
145+
# ignored
146+
raw = "".join(raw.replace("\r", "").split("\\\n"))
147+
# for tox<4 supporting requirement/constraint files via -rreq.txt/-creq.txt
148+
lines: list[str] = [cls._normalize_line(line) for line in raw.splitlines()]
149+
150+
if any(line.startswith("-") for line in lines):
151+
msg = "only constraints files or URLs can be provided"
152+
raise ValueError(msg)
153+
154+
adjusted = "\n".join([f"-c {line}" for line in lines])
155+
return f"{adjusted}\n" if raw.endswith("\\\n") else adjusted # preserve trailing newline if input has it
156+
157+
141158
ONE_ARG = {
142159
"-i",
143160
"--index-url",

src/tox/tox_env/python/runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ def register_config(self) -> None:
3434
super().register_config()
3535
root = self.core["toxinidir"]
3636
self.conf.add_config(
37-
keys="deps",
37+
keys=["deps"],
3838
of_type=PythonDeps,
3939
factory=partial(PythonDeps.factory, root),
4040
default=PythonDeps("", root),
41-
desc="Name of the python dependencies as specified by PEP-440",
41+
desc="python dependencies with optional version specifiers, as specified by PEP-440",
4242
)
4343
self.conf.add_config(
4444
keys=["dependency_groups"],

0 commit comments

Comments
 (0)