44import operator
55from abc import ABC , abstractmethod
66from collections import defaultdict
7+ from functools import partial
78from pathlib import Path
8- from typing import TYPE_CHECKING , Any , Callable , Sequence
9+ from typing import TYPE_CHECKING , Any , Callable , Sequence , cast
910
1011from packaging .requirements import Requirement
1112
1516from tox .tox_env .installer import Installer
1617from tox .tox_env .python .api import Python
1718from 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
2021if 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 :
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 combined_constraints = new_requirements + [c .removeprefix ("-c " ) for c in new_constraints ]
@@ -207,13 +230,18 @@ def _install_list_of_deps( # noqa: C901
207230 raise Recreate (msg ) # pragma: no branch
208231 new_deps = sorted (set (groups ["req" ]) - set (old or []))
209232 if new_deps : # pragma: no branch
233+ new_deps .extend (self .constraints .as_root_args )
210234 self ._execute_installer (new_deps , req_of_type )
211235 install_args = ["--force-reinstall" , "--no-deps" ]
212236 if groups ["pkg" ]:
237+ # we intentionally ignore constraints when installing the package itself
238+ # https://github.com/tox-dev/tox/issues/3550
213239 self ._execute_installer (install_args + groups ["pkg" ], of_type )
214240 if groups ["dev_pkg" ]:
215241 for entry in groups ["dev_pkg" ]:
216242 install_args .extend (("-e" , str (entry )))
243+ # we intentionally ignore constraints when installing the package itself
244+ # https://github.com/tox-dev/tox/issues/3550
217245 self ._execute_installer (install_args , of_type )
218246
219247 def _execute_installer (self , deps : Sequence [Any ], of_type : str ) -> None :
0 commit comments