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..541fd852d14
--- /dev/null
+++ b/news/13534.feature.rst
@@ -0,0 +1,3 @@
+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) without affecting the final installation.
diff --git a/news/13534.removal.rst b/news/13534.removal.rst
new file mode 100644
index 00000000000..c8212bb15b2
--- /dev/null
+++ b/news/13534.removal.rst
@@ -0,0 +1,8 @@
+Deprecate the ``PIP_CONSTRAINT`` environment variable for specifying build
+constraints.
+
+Build constraints should now be specified using the ``--build-constraint``
+option or the ``PIP_BUILD_CONSTRAINT`` environment variable. When using build
+constraints, ``PIP_CONSTRAINT`` no longer affects isolated build environments.
+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..27475e449b2 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
@@ -28,8 +29,15 @@
from pip._internal.index.package_finder import PackageFinder
from pip._internal.req.req_install import InstallRequirement
+ class ExtraEnviron(TypedDict, total=False):
+ extra_environ: dict[str, str]
+
+
logger = logging.getLogger(__name__)
+# Global flag to track if deprecation warning has been shown
+_DEPRECATION_WARNING_SHOWN = False
+
def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]:
return (a, b) if a != b else (a,)
@@ -101,8 +109,50 @@ 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,
+ ) -> None:
self.finder = finder
+ self._build_constraints = build_constraints or []
+ self._build_constraint_feature_enabled = build_constraint_feature_enabled
+
+ def _deprecation_constraint_check(self) -> None:
+ """
+ Check for deprecation warning: PIP_CONSTRAINT affecting build environments.
+
+ This warns when build-constraint feature is NOT enabled and PIP_CONSTRAINT
+ is not empty, but only shows the warning once per process.
+ """
+ global _DEPRECATION_WARNING_SHOWN
+
+ if self._build_constraint_feature_enabled or self._build_constraints:
+ return
+
+ if _DEPRECATION_WARNING_SHOWN:
+ return
+
+ pip_constraint = os.environ.get("PIP_CONSTRAINT")
+ if not pip_constraint or not pip_constraint.strip():
+ return
+
+ _DEPRECATION_WARNING_SHOWN = True
+ deprecated(
+ reason=(
+ "Setting PIP_CONSTRAINT will not affect "
+ "build constraints in the future,"
+ ),
+ replacement=(
+ "to specify build constraints using --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 +162,8 @@ def install(
kind: str,
for_req: InstallRequirement | None,
) -> None:
+ self._deprecation_constraint_check()
+
finder = self.finder
args: list[str] = [
sys.executable,
@@ -167,6 +219,26 @@ def install(
args.append("--pre")
if finder.prefer_binary:
args.append("--prefer-binary")
+
+ # Handle build constraints
+ 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])
+
+ extra_environ: ExtraEnviron = {}
+ if self._build_constraint_feature_enabled and not self._build_constraints:
+ # If there are no build constraints but the build constraints
+ # 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 +246,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..6174d0d93ba 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -101,6 +101,32 @@ def check_dist_restriction(options: Values, check_target: bool = False) -> None:
)
+def check_build_constraints(options: Values) -> None:
+ """Function for validating build constraints 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."
+ )
+
+ # Import here to avoid circular imports
+ from pip._internal.network.session import PipSession
+ from pip._internal.req.req_file import get_file_content
+
+ session = PipSession()
+ try:
+ # Eagerly check build constraints file contents
+ # is valid so that we don't fail in when trying
+ # to check constraints in isolated build process
+ for constraint_file in options.build_constraints:
+ get_file_content(constraint_file, session)
+ finally:
+ session.close()
+
+
def _path_option_check(option: Option, opt: str, value: str) -> str:
return os.path.expanduser(value)
@@ -430,6 +456,21 @@ def constraints() -> Option:
)
+def build_constraints() -> 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 +1113,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..640ac9fb908 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,22 @@ def make_requirement_preparer(
"fast-deps has no effect when used with the legacy resolver."
)
+ # Handle build constraints
+ build_constraints = getattr(options, "build_constraints", [])
+ build_constraint_feature_enabled = (
+ "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,
+ ),
check_build_deps=options.check_build_deps,
build_tracker=build_tracker,
session=session,
@@ -221,20 +242,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..d8f8fbdacd5 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_constraints())
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..f545666269f 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_constraints())
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..71d22007f1f 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_constraints())
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..1a3287218bf 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_constraints())
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..5ed6dc954fb
--- /dev/null
+++ b/tests/functional/test_build_constraints.py
@@ -0,0 +1,171 @@
+"""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 _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
+ )
+ result.assert_installed(
+ "test-build-constraints", editable=False, without_files=["."]
+ )
+
+
+@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"],
+ )
+ result.assert_installed("test-env-isolation", editable=False, without_files=["."])
+
+
+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,
+ expect_error=True,
+ )
+ assert "Could not open requirements file" in result.stderr
+ assert "No such file or directory" in result.stderr
+
+
+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
+ result.assert_installed("test-no-feature", editable=False, without_files=["."])
diff --git a/tests/unit/test_build_constraints.py b/tests/unit/test_build_constraints.py
new file mode 100644
index 00000000000..719fd7ea714
--- /dev/null
+++ b/tests/unit/test_build_constraints.py
@@ -0,0 +1,138 @@
+"""Tests for build constraints functionality."""
+
+from __future__ import annotations
+
+import os
+import warnings
+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."""
+
+ def setup_method(self) -> None:
+ """Reset the global deprecation warning flag before each test."""
+ import pip._internal.build_env
+
+ pip._internal.build_env._DEPRECATION_WARNING_SHOWN = False
+
+ @mock.patch.dict(os.environ, {}, clear=True)
+ def test_deprecation_check_no_pip_constraint(self) -> None:
+ """Test no deprecation warning when PIP_CONSTRAINT is not set."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ build_constraint_feature_enabled=False,
+ )
+
+ # Should not raise any warning
+ installer._deprecation_constraint_check()
+
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": ""})
+ def test_deprecation_check_empty_pip_constraint(self) -> None:
+ """Test no deprecation warning for empty PIP_CONSTRAINT."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ build_constraint_feature_enabled=False,
+ )
+
+ # Should not raise any warning since PIP_CONSTRAINT is empty
+ installer._deprecation_constraint_check()
+
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": " "})
+ def test_deprecation_check_whitespace_pip_constraint(self) -> None:
+ """Test no deprecation warning for whitespace-only PIP_CONSTRAINT."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ build_constraint_feature_enabled=False,
+ )
+
+ # Should not raise any warning since PIP_CONSTRAINT is only whitespace
+ 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 when build-constraint feature is enabled."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ 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_warning_shown(self) -> None:
+ """Test deprecation warning emitted when PIP_CONSTRAINT is set
+ and build-constraint is not enabled."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ 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 using "
+ "--build-constraint or PIP_BUILD_CONSTRAINT" in message
+ )
+
+ @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 and proceeds with warning."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ 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()
+
+ @mock.patch.dict(os.environ, {"PIP_CONSTRAINT": "constraints.txt"})
+ def test_deprecation_check_warning_shown_only_once(self) -> None:
+ """Test deprecation warning is shown only once per process."""
+ finder = make_test_finder()
+ installer = SubprocessBuildEnvironmentInstaller(
+ finder,
+ build_constraint_feature_enabled=False,
+ )
+
+ with pytest.warns(PipDeprecationWarning):
+ installer._deprecation_constraint_check()
+
+ with warnings.catch_warnings(record=True) as warning_list:
+ warnings.simplefilter("always")
+ installer._deprecation_constraint_check()
+ assert len(warning_list) == 0