diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index d6a0acf9cd8..30c514f7e59 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -257,6 +257,47 @@ e.g. http://example.com/constraints.txt, so that your organization can store and
serve them in a centralized place.
+.. _`Build Constraints`:
+
+Build Constraints
+-----------------
+
+.. versionadded:: 25.3
+
+Build constraints are a type of constraints file that applies only to isolated
+build environments used for building packages from source. Unlike regular
+constraints, which affect the packages installed in your environment, build
+constraints only influence the versions of packages available during the
+build process.
+
+This is useful when you need to constrain build dependencies
+(such as ``setuptools``, ``cython``, etc.) without affecting the
+final installed environment.
+
+Use build constraints like so:
+
+.. tab:: Unix/macOS
+
+ .. code-block:: shell
+
+ python -m pip install --build-constraint build-constraints.txt SomePackage
+
+.. tab:: Windows
+
+ .. code-block:: shell
+
+ py -m pip install --build-constraint build-constraints.txt SomePackage
+
+Example build constraints file (``build-constraints.txt``):
+
+.. code-block:: text
+
+ # Constrain setuptools version during build
+ setuptools>=45,<80
+ # Pin Cython for packages that use it to build
+ cython==0.29.24
+
+
.. _`Dependency Groups`:
diff --git a/news/13534.feature.rst b/news/13534.feature.rst
new file mode 100644
index 00000000000..6d7635ace48
--- /dev/null
+++ b/news/13534.feature.rst
@@ -0,0 +1,8 @@
+Add support for build constraints via the ``--build-constraint`` option. This
+allows constraining the versions of packages used during the build process
+(e.g., setuptools).
+
+When using ``--build-constraint``, you can no longer pass constraints to
+isolated build environments via the ``PIP_CONSTRAINT`` environment variable.
+To opt in to this behavior without specifying any build constraints, use
+``--use-feature=build-constraint``.
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 3a246a1e349..b78894d014b 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -11,7 +11,7 @@
from collections import OrderedDict
from collections.abc import Iterable
from types import TracebackType
-from typing import TYPE_CHECKING, Protocol
+from typing import TYPE_CHECKING, Protocol, TypedDict
from pip._vendor.packaging.version import Version
@@ -19,6 +19,7 @@
from pip._internal.cli.spinners import open_spinner
from pip._internal.locations import get_platlib, get_purelib, get_scheme
from pip._internal.metadata import get_default_environment, get_environment
+from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.logging import VERBOSE
from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.subprocess import call_subprocess
@@ -31,6 +32,10 @@
logger = logging.getLogger(__name__)
+class ExtraEnviron(TypedDict, total=False):
+ extra_environ: dict[str, str]
+
+
def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]:
return (a, b) if a != b else (a,)
@@ -101,8 +106,55 @@ class SubprocessBuildEnvironmentInstaller:
Install build dependencies by calling pip in a subprocess.
"""
- def __init__(self, finder: PackageFinder) -> None:
+ def __init__(
+ self,
+ finder: PackageFinder,
+ build_constraints: list[str] | None = None,
+ build_constraint_feature_enabled: bool = False,
+ constraints: list[str] | None = None,
+ ) -> None:
self.finder = finder
+ self._build_constraints = build_constraints or []
+ self._build_constraint_feature_enabled = build_constraint_feature_enabled
+ self._constraints = constraints or []
+
+ def _deprecation_constraint_check(self) -> None:
+ """
+ Check for deprecation warning: PIP_CONSTRAINT affecting build environments.
+
+ This warns when build-constraint feature is NOT enabled but regular constraints
+ match what PIP_CONSTRAINT environment variable points to.
+ """
+ if self._build_constraint_feature_enabled:
+ return
+
+ if self._build_constraints:
+ return
+
+ if not self._constraints:
+ return
+
+ if not os.environ.get("PIP_CONSTRAINT"):
+ return
+
+ pip_constraint_files = [
+ f for f in os.environ["PIP_CONSTRAINT"].split() if f.strip()
+ ]
+ if pip_constraint_files and pip_constraint_files == self._constraints:
+ deprecated(
+ reason=(
+ "Setting PIP_CONSTRAINT will not affect "
+ "build constraints in the future,"
+ ),
+ replacement=(
+ "to specify build constraints use --build-constraint or "
+ "PIP_BUILD_CONSTRAINT, to disable this warning without "
+ "any build constraints set --use-feature=build-constraint or "
+ 'PIP_USE_FEATURE="build-constraint"'
+ ),
+ gone_in="26.2",
+ issue=None,
+ )
def install(
self,
@@ -112,6 +164,8 @@ def install(
kind: str,
for_req: InstallRequirement | None,
) -> None:
+ self._deprecation_constraint_check()
+
finder = self.finder
args: list[str] = [
sys.executable,
@@ -167,6 +221,26 @@ def install(
args.append("--pre")
if finder.prefer_binary:
args.append("--prefer-binary")
+
+ # Handle build constraints
+ extra_environ: ExtraEnviron = {}
+ if self._build_constraint_feature_enabled:
+ args.extend(["--use-feature", "build-constraint"])
+
+ if self._build_constraints:
+ # Build constraints must be passed as both constraints
+ # and build constraints, so that nested builds receive
+ # build constraints
+ for constraint_file in self._build_constraints:
+ args.extend(["--constraint", constraint_file])
+ args.extend(["--build-constraint", constraint_file])
+
+ if self._build_constraint_feature_enabled and not self._build_constraints:
+ # If there are no build constraints but the build constraint
+ # feature is enabled then we must ignore regular constraints
+ # in the isolated build environment
+ extra_environ = {"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}}
+
args.append("--")
args.extend(requirements)
with open_spinner(f"Installing {kind}") as spinner:
@@ -174,6 +248,7 @@ def install(
args,
command_desc=f"pip subprocess to install {kind}",
spinner=spinner,
+ **extra_environ,
)
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 3519dadf13d..2367c816743 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -101,6 +101,18 @@ def check_dist_restriction(options: Values, check_target: bool = False) -> None:
)
+def check_build_constraints(options: Values) -> None:
+ """Function for validating build constraint options.
+
+ :param options: The OptionParser options.
+ """
+ if hasattr(options, "build_constraints") and options.build_constraints:
+ if not options.build_isolation:
+ raise CommandError(
+ "--build-constraint cannot be used with --no-build-isolation."
+ )
+
+
def _path_option_check(option: Option, opt: str, value: str) -> str:
return os.path.expanduser(value)
@@ -430,6 +442,21 @@ def constraints() -> Option:
)
+def build_constraint() -> Option:
+ return Option(
+ "--build-constraint",
+ dest="build_constraints",
+ action="append",
+ type="str",
+ default=[],
+ metavar="file",
+ help=(
+ "Constrain build dependencies using the given constraints file. "
+ "This option can be used multiple times."
+ ),
+ )
+
+
def requirements() -> Option:
return Option(
"-r",
@@ -1072,6 +1099,7 @@ def check_list_path_option(options: Values) -> None:
default=[],
choices=[
"fast-deps",
+ "build-constraint",
]
+ ALWAYS_ENABLED_FEATURES,
help="Enable new functionality, that may be backward incompatible.",
diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py
index dc1328ff019..31f22fb7914 100644
--- a/src/pip/_internal/cli/req_command.py
+++ b/src/pip/_internal/cli/req_command.py
@@ -8,6 +8,7 @@
from __future__ import annotations
import logging
+import os
from functools import partial
from optparse import Values
from typing import Any
@@ -44,6 +45,16 @@
logger = logging.getLogger(__name__)
+def should_ignore_regular_constraints(options: Values) -> bool:
+ """
+ Check if regular constraints should be ignored because
+ we are in a isolated build process and build constraints
+ feature is enabled but no build constraints were passed.
+ """
+
+ return os.environ.get("_PIP_IN_BUILD_IGNORE_CONSTRAINTS") == "1"
+
+
KEEPABLE_TEMPDIR_TYPES = [
tempdir_kinds.BUILD_ENV,
tempdir_kinds.EPHEM_WHEEL_CACHE,
@@ -132,12 +143,26 @@ def make_requirement_preparer(
"fast-deps has no effect when used with the legacy resolver."
)
+ # Handle build constraints
+ build_constraints = getattr(options, "build_constraints", [])
+ constraints = getattr(options, "constraints", [])
+ build_constraint_feature_enabled = (
+ hasattr(options, "features_enabled")
+ and options.features_enabled
+ and "build-constraint" in options.features_enabled
+ )
+
return RequirementPreparer(
build_dir=temp_build_dir_path,
src_dir=options.src_dir,
download_dir=download_dir,
build_isolation=options.build_isolation,
- build_isolation_installer=SubprocessBuildEnvironmentInstaller(finder),
+ build_isolation_installer=SubprocessBuildEnvironmentInstaller(
+ finder,
+ build_constraints=build_constraints,
+ build_constraint_feature_enabled=build_constraint_feature_enabled,
+ constraints=constraints,
+ ),
check_build_deps=options.check_build_deps,
build_tracker=build_tracker,
session=session,
@@ -221,20 +246,22 @@ def get_requirements(
Parse command-line arguments into the corresponding requirements.
"""
requirements: list[InstallRequirement] = []
- for filename in options.constraints:
- for parsed_req in parse_requirements(
- filename,
- constraint=True,
- finder=finder,
- options=options,
- session=session,
- ):
- req_to_add = install_req_from_parsed_requirement(
- parsed_req,
- isolated=options.isolated_mode,
- user_supplied=False,
- )
- requirements.append(req_to_add)
+
+ if not should_ignore_regular_constraints(options):
+ for filename in options.constraints:
+ for parsed_req in parse_requirements(
+ filename,
+ constraint=True,
+ finder=finder,
+ options=options,
+ session=session,
+ ):
+ req_to_add = install_req_from_parsed_requirement(
+ parsed_req,
+ isolated=options.isolated_mode,
+ user_supplied=False,
+ )
+ requirements.append(req_to_add)
for req in args:
req_to_add = install_req_from_line(
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 900fb403d6f..aa844613870 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -36,6 +36,7 @@ class DownloadCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.constraints())
+ self.cmd_opts.add_option(cmdoptions.build_constraint())
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.global_options())
@@ -81,6 +82,7 @@ def run(self, options: Values, args: list[str]) -> int:
options.editables = []
cmdoptions.check_dist_restriction(options)
+ cmdoptions.check_build_constraints(options)
options.download_dir = normalize_path(options.download_dir)
ensure_dir(options.download_dir)
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 1ef7a0f4410..550189c6d9e 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -87,6 +87,7 @@ class InstallCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.constraints())
+ self.cmd_opts.add_option(cmdoptions.build_constraint())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.pre())
@@ -303,6 +304,7 @@ def run(self, options: Values, args: list[str]) -> int:
if options.upgrade:
upgrade_strategy = options.upgrade_strategy
+ cmdoptions.check_build_constraints(options)
cmdoptions.check_dist_restriction(options, check_target=True)
logger.verbose("Using %s", get_pip_version())
diff --git a/src/pip/_internal/commands/lock.py b/src/pip/_internal/commands/lock.py
index e4a978d5aaa..b499a871bdb 100644
--- a/src/pip/_internal/commands/lock.py
+++ b/src/pip/_internal/commands/lock.py
@@ -59,6 +59,7 @@ def add_options(self) -> None:
)
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.constraints())
+ self.cmd_opts.add_option(cmdoptions.build_constraint())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.pre())
@@ -98,6 +99,8 @@ def run(self, options: Values, args: list[str]) -> int:
"without prior warning."
)
+ cmdoptions.check_build_constraints(options)
+
session = self.get_default_session(options)
finder = self._build_package_finder(
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index 61be254912f..2b2847805b7 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -60,6 +60,7 @@ def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.constraints())
+ self.cmd_opts.add_option(cmdoptions.build_constraint())
self.cmd_opts.add_option(cmdoptions.editable())
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.src())
@@ -101,6 +102,8 @@ def add_options(self) -> None:
@with_cleanup
def run(self, options: Values, args: list[str]) -> int:
+ cmdoptions.check_build_constraints(options)
+
session = self.get_default_session(options)
finder = self._build_package_finder(options, session)
diff --git a/tests/functional/test_build_constraints.py b/tests/functional/test_build_constraints.py
new file mode 100644
index 00000000000..2c3749d3ca6
--- /dev/null
+++ b/tests/functional/test_build_constraints.py
@@ -0,0 +1,176 @@
+"""Tests for the build constraints feature."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from tests.lib import PipTestEnvironment, TestPipResult, create_test_package_with_setup
+
+
+def _create_simple_test_package(script: PipTestEnvironment, name: str) -> Path:
+ """Create a simple test package with minimal setup."""
+ return create_test_package_with_setup(
+ script,
+ name=name,
+ version="1.0",
+ py_modules=[name],
+ )
+
+
+def _create_constraints_file(
+ script: PipTestEnvironment, filename: str, content: str
+) -> Path:
+ """Create a constraints file with the given content."""
+ constraints_file = script.scratch_path / filename
+ constraints_file.write_text(content)
+ return constraints_file
+
+
+def _run_pip_install_with_build_constraints(
+ script: PipTestEnvironment,
+ project_dir: Path,
+ build_constraints_file: Path,
+ extra_args: list[str] | None = None,
+ expect_error: bool = False,
+) -> TestPipResult:
+ """Run pip install with build constraints and common arguments."""
+ args = [
+ "install",
+ "--no-cache-dir",
+ "--build-constraint",
+ str(build_constraints_file),
+ "--use-feature",
+ "build-constraint",
+ ]
+
+ if extra_args:
+ args.extend(extra_args)
+
+ args.append(str(project_dir))
+
+ return script.pip(*args, expect_error=expect_error)
+
+
+def _assert_successful_installation(result: TestPipResult, package_name: str) -> None:
+ """Assert that the package was successfully installed."""
+ assert f"Successfully installed {package_name}" in result.stdout
+
+
+def _run_pip_install_with_build_constraints_no_feature_flag(
+ script: PipTestEnvironment,
+ project_dir: Path,
+ constraints_file: Path,
+) -> TestPipResult:
+ """Run pip install with build constraints but without the feature flag."""
+ return script.pip(
+ "install",
+ "--build-constraint",
+ str(constraints_file),
+ str(project_dir),
+ )
+
+
+def test_build_constraints_basic_functionality_simple(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
+ """Test that build constraints options are accepted and processed."""
+ project_dir = _create_simple_test_package(
+ script=script, name="test_build_constraints"
+ )
+ constraints_file = _create_constraints_file(
+ script=script, filename="constraints.txt", content="setuptools>=40.0.0\n"
+ )
+ result = _run_pip_install_with_build_constraints(
+ script=script, project_dir=project_dir, build_constraints_file=constraints_file
+ )
+ _assert_successful_installation(
+ result=result, package_name="test_build_constraints"
+ )
+
+
+@pytest.mark.network
+def test_build_constraints_vs_regular_constraints_simple(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
+ """Test that build constraints and regular constraints work independently."""
+ project_dir = create_test_package_with_setup(
+ script,
+ name="test_isolation",
+ version="1.0",
+ py_modules=["test_isolation"],
+ install_requires=["six"],
+ )
+ build_constraints_file = _create_constraints_file(
+ script=script, filename="build_constraints.txt", content="setuptools>=40.0.0\n"
+ )
+ regular_constraints_file = _create_constraints_file(
+ script=script, filename="constraints.txt", content="six>=1.10.0\n"
+ )
+ result = script.pip(
+ "install",
+ "--no-cache-dir",
+ "--build-constraint",
+ build_constraints_file,
+ "--constraint",
+ regular_constraints_file,
+ "--use-feature",
+ "build-constraint",
+ str(project_dir),
+ expect_error=False,
+ )
+ assert "Successfully installed" in result.stdout
+ assert "test_isolation" in result.stdout
+
+
+@pytest.mark.network
+def test_build_constraints_environment_isolation_simple(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
+ """Test that build constraints work correctly in isolated build environments."""
+ project_dir = _create_simple_test_package(script=script, name="test_env_isolation")
+ constraints_file = _create_constraints_file(
+ script=script, filename="build_constraints.txt", content="setuptools>=40.0.0\n"
+ )
+ result = _run_pip_install_with_build_constraints(
+ script=script,
+ project_dir=project_dir,
+ build_constraints_file=constraints_file,
+ extra_args=["--isolated"],
+ )
+ _assert_successful_installation(result=result, package_name="test_env_isolation")
+
+
+def test_build_constraints_file_not_found(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
+ """Test behavior when build constraints file doesn't exist."""
+ project_dir = _create_simple_test_package(
+ script=script, name="test_missing_constraints"
+ )
+ missing_constraints = script.scratch_path / "missing_constraints.txt"
+ result = _run_pip_install_with_build_constraints(
+ script=script,
+ project_dir=project_dir,
+ build_constraints_file=missing_constraints,
+ )
+ _assert_successful_installation(
+ result=result, package_name="test_missing_constraints"
+ )
+
+
+def test_build_constraints_without_feature_flag(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
+ """Test that --build-constraint automatically enables the feature."""
+ project_dir = _create_simple_test_package(script=script, name="test_no_feature")
+ constraints_file = _create_constraints_file(
+ script=script, filename="constraints.txt", content="setuptools==45.0.0\n"
+ )
+ result = _run_pip_install_with_build_constraints_no_feature_flag(
+ script=script, project_dir=project_dir, constraints_file=constraints_file
+ )
+ # Should succeed now that --build-constraint auto-enables the feature
+ assert result.returncode == 0
+ _assert_successful_installation(result=result, package_name="test_no_feature")
diff --git a/tests/unit/test_build_constraints.py b/tests/unit/test_build_constraints.py
new file mode 100644
index 00000000000..5da4c32844e
--- /dev/null
+++ b/tests/unit/test_build_constraints.py
@@ -0,0 +1,140 @@
+"""Tests for build constraints functionality."""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+from unittest import mock
+
+import pytest
+
+from pip._internal.build_env import SubprocessBuildEnvironmentInstaller, _Prefix
+from pip._internal.utils.deprecation import PipDeprecationWarning
+
+from tests.lib import make_test_finder
+
+
+class TestSubprocessBuildEnvironmentInstaller:
+ """Test SubprocessBuildEnvironmentInstaller build constraints functionality."""
+
+ @mock.patch.dict(os.environ, {}, clear=True)
+ def test_deprecation_check_no_pip_constraint(self) -> None:
+ """Test no deprecation warning is shown when PIP_CONSTRAINT is not set."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ constraints=["constraints.txt"],
+ build_constraint_feature_enabled=False,
+ )
+
+ # Should not raise any warning
+ installer._deprecation_constraint_check()
+
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": "constraints.txt"})
+ def test_deprecation_check_feature_enabled(self) -> None:
+ """
+ Test no deprecation warning is shown when
+ build-constraint feature is enabled
+ """
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ constraints=["constraints.txt"],
+ build_constraint_feature_enabled=True,
+ )
+
+ # Should not raise any warning
+ installer._deprecation_constraint_check()
+
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": "constraints.txt"})
+ def test_deprecation_check_constraint_mismatch(self) -> None:
+ """
+ Test no deprecation warning is shown when
+ PIP_CONSTRAINT doesn't match regular constraints.
+ """
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ constraints=["different.txt"],
+ build_constraint_feature_enabled=False,
+ )
+
+ # Should not raise any warning
+ installer._deprecation_constraint_check()
+
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": "constraints.txt"})
+ def test_deprecation_check_warning_shown(self) -> None:
+ """Test deprecation warning is shown when conditions are met."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ constraints=["constraints.txt"],
+ build_constraint_feature_enabled=False,
+ )
+
+ with pytest.warns(PipDeprecationWarning) as warning_info:
+ installer._deprecation_constraint_check()
+
+ assert len(warning_info) == 1
+ message = str(warning_info[0].message)
+ assert (
+ "Setting PIP_CONSTRAINT will not affect build constraints in the future"
+ in message
+ )
+ assert (
+ "to specify build constraints use "
+ "--build-constraint or PIP_BUILD_CONSTRAINT" in message
+ )
+
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": "constraint1.txt constraint2.txt"})
+ def test_deprecation_check_multiple_constraints(self) -> None:
+ """Test deprecation warning works with multiple constraints."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ constraints=["constraint1.txt", "constraint2.txt"],
+ build_constraint_feature_enabled=False,
+ )
+
+ with pytest.warns(PipDeprecationWarning):
+ installer._deprecation_constraint_check()
+
+ @mock.patch.dict(
+ os.environ, {"PIP_CONSTRAINT": "constraint1.txt constraint2.txt extra.txt"}
+ )
+ def test_deprecation_check_partial_match_no_warning(self) -> None:
+ """Test no deprecation warning is shown when only partial match."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ constraints=["constraint1.txt", "constraint2.txt"],
+ build_constraint_feature_enabled=False,
+ )
+
+ # Should not raise any warning since PIP_CONSTRAINT has extra file
+ installer._deprecation_constraint_check()
+
+ @mock.patch("pip._internal.build_env.call_subprocess")
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": "constraints.txt"})
+ def test_install_calls_deprecation_check(
+ self, mock_call_subprocess: mock.Mock, tmp_path: Path
+ ) -> None:
+ """Test install method calls deprecation check."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ constraints=["constraints.txt"],
+ build_constraint_feature_enabled=False,
+ )
+ prefix = _Prefix(str(tmp_path))
+
+ with pytest.warns(PipDeprecationWarning):
+ installer.install(
+ requirements=["setuptools"],
+ prefix=prefix,
+ kind="build dependencies",
+ for_req=None,
+ )
+
+ # Verify that call_subprocess was called (install proceeded after warning)
+ mock_call_subprocess.assert_called_once()