Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e1e72e7
Implement --upload-before
uranusjr May 20, 2024
c8b2481
Fix merge errors and rename to "exclude-newer-than"
notatallshaw Aug 6, 2025
a53d08a
Make common parse_iso_time
notatallshaw Aug 6, 2025
6db6c94
Add documentation on how to specify explicit timezone
notatallshaw Aug 6, 2025
c0ec2ec
Add exclude-newer tests
notatallshaw Aug 6, 2025
2bf1d3a
Pass exclude-newer-than to isolated build install
notatallshaw Aug 6, 2025
d5adbda
NEWS ENTRY
notatallshaw Aug 6, 2025
c374b2c
Fix linting
notatallshaw Aug 6, 2025
bc162b2
Merge branch 'main' into exclude-newer-than
notatallshaw Aug 8, 2025
72f363d
Add helpful error message on incorrect datetime format
notatallshaw Aug 9, 2025
181a7ca
Merge branch 'main' into exclude-newer-than
notatallshaw Aug 9, 2025
007caf6
Update tests/functional/test_exclude_newer.py
notatallshaw Aug 15, 2025
b53c5e8
Merge branch 'main' into exclude-newer-than
notatallshaw Aug 15, 2025
0f1bc46
Add `--no-deps` to request installs to not download unneeded packages
notatallshaw Aug 19, 2025
ad90024
Remove excessive functional tests
notatallshaw Aug 19, 2025
6ff91a4
Clean up test_finder tests
notatallshaw Aug 19, 2025
fbe923d
Update `test_handle_exclude_newer_than_naive_dates` comparison
notatallshaw Aug 19, 2025
703cdc4
Improve parameter formatting of `test_handle_exclude_newer_than_with_…
notatallshaw Aug 19, 2025
6cf2bec
Get exclude_newer_than from option
notatallshaw Aug 19, 2025
4713c6d
Add exclude-newer-than to the lock command
notatallshaw Aug 19, 2025
841ae12
Remove change in list, links, and wheel
notatallshaw Aug 19, 2025
61ec9b0
Update docs and news items to make clear index needs to provide `uplo…
notatallshaw Aug 19, 2025
e1f274a
Change name to uploaded prior to
notatallshaw Aug 21, 2025
e592e95
Add `--uploaded-prior-to` to the user guide
notatallshaw Aug 21, 2025
3cc912c
Merge branch 'main' into exclude-newer-than
notatallshaw Aug 29, 2025
fc40558
Merge branch 'main' into exclude-newer-than
notatallshaw Sep 5, 2025
afb6f2d
Merge branch 'main' into exclude-newer-than
notatallshaw Sep 25, 2025
b5e4923
Fix type hint error in `make_test_link_evaluator`
notatallshaw Oct 4, 2025
8be9e32
Merge branch 'main' into exclude-newer-than
notatallshaw Oct 5, 2025
659d538
Merge branch 'main' into exclude-newer-than
notatallshaw Oct 14, 2025
64f4529
Do not allow indexes which don't provide `upload-time` if `--uploaded…
notatallshaw Oct 18, 2025
a3b3ac1
Implement build constraints
notatallshaw Aug 9, 2025
b8f3513
Add build constraints tests
notatallshaw Aug 9, 2025
4e3aecd
Add build constraints to user guide
notatallshaw Aug 9, 2025
b6b6a7f
NEWS ENTRY
notatallshaw Aug 9, 2025
30fd4e1
Imply using new behavior when build constraints are provided without …
notatallshaw Aug 9, 2025
f1831e7
Update src/pip/_internal/cli/req_command.py
notatallshaw Aug 14, 2025
50e45d2
Update src/pip/_internal/build_env.py
notatallshaw Aug 14, 2025
9465e3e
Fix linting
notatallshaw Aug 19, 2025
8524919
Fix test
notatallshaw Aug 19, 2025
5453030
Consistently use "build constraints" in variables and documentation
notatallshaw Aug 19, 2025
0de1334
Simplify deprecation warning
notatallshaw Aug 19, 2025
b9a21b5
Only emit pip constraint deprecation warning once
notatallshaw Aug 19, 2025
97b6de1
Move `ExtraEnviron` into type checking block
notatallshaw Aug 19, 2025
707c449
Use standard `assert_installed` in functional tests for build constra…
notatallshaw Aug 20, 2025
c5e19c2
Eagerly assert build constraints files
notatallshaw Aug 20, 2025
c655049
Add deprecation news item.
notatallshaw Aug 20, 2025
830324b
Remove pointless check for `_PIP_IN_BUILD_IGNORE_CONSTRAINTS` in `_de…
notatallshaw Aug 20, 2025
1d679b8
Exit `_deprecation_constraint_check` early when build constraints pre…
notatallshaw Aug 20, 2025
84c955b
Remove superfluous `constraints` parameter
notatallshaw Aug 21, 2025
8e595f7
Use with to close pip session.
notatallshaw Oct 11, 2025
b7ca0bf
Remove deprication supression logic
notatallshaw Oct 11, 2025
6183877
Update tests
notatallshaw Oct 11, 2025
9587d29
fix lint
notatallshaw Oct 11, 2025
3f0350e
Avoid `pip install --dry-run` downloading full wheels
pelson Jul 15, 2025
8c5db4d
Refine the news
pelson Sep 24, 2025
b7e7ecf
Update news/12603.feature.rst
notatallshaw Oct 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions docs/html/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,108 @@ 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


.. _`Filtering by Upload Time`:


Filtering by Upload Time
=========================

The ``--uploaded-prior-to`` option allows you to filter packages by their upload time
to an index, only considering packages that were uploaded before a specified datetime.
This can be useful for creating reproducible builds by ensuring you only install
packages that were available at a known point in time.

.. tab:: Unix/macOS

.. code-block:: shell

python -m pip install --uploaded-prior-to=2025-03-16T00:00:00Z SomePackage

.. tab:: Windows

.. code-block:: shell

py -m pip install --uploaded-prior-to=2025-03-16T00:00:00Z SomePackage

The option accepts ISO 8601 datetime strings in several formats:

* ``2025-03-16`` - Date in local timezone
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In pypi-timemachine all date/times are specified in UTC. It is hard to imagine a use case where using anything else is a good idea.

Is this a case of following the ISO standard in pip is harmful to inexperienced users?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously discussed: #13520 (comment)

* ``2025-03-16 12:30:00`` - Datetime in local timezone
* ``2025-03-16T12:30:00Z`` - Datetime in UTC
* ``2025-03-16T12:30:00+05:00`` - Datetime in UTC offset

For consistency across machines, use either UTC format (with 'Z' suffix) or UTC offset
format (with timezone offset like '+05:00'). Local timezone formats may produce different
results on different machines.

.. note::

This option only applies to packages from indexes, not local files. Local
package files are allowed regardless of the ``--uploaded-prior-to`` setting.
e.g., ``pip install /path/to/package.whl`` or packages from
``--find-links`` directories.

This option requires package indexes that provide upload-time metadata
(such as PyPI). If the index does not provide upload-time metadata for a
package file, pip will fail immediately with an error message indicating
that upload-time metadata is required when using ``--uploaded-prior-to``.

You can combine this option with other filtering mechanisms like constraints files:

.. tab:: Unix/macOS

.. code-block:: shell

python -m pip install -c constraints.txt --uploaded-prior-to=2025-03-16 SomePackage

.. tab:: Windows

.. code-block:: shell

py -m pip install -c constraints.txt --uploaded-prior-to=2025-03-16 SomePackage


.. _`Dependency Groups`:


Expand Down
1 change: 1 addition & 0 deletions news/12603.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When PEP-658 metadata is available, full distribution files are no longer downloaded when using ``pip lock`` or ``pip install --dry-run``.
2 changes: 2 additions & 0 deletions news/13520.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``--uploaded-prior-to`` option to only consider packages uploaded prior to
a given datetime when the ``upload-time`` field is available from an index.
3 changes: 3 additions & 0 deletions news/13534.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions news/13534.removal.rst
Original file line number Diff line number Diff line change
@@ -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``.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ max-complexity = 33 # default is 10
[tool.ruff.lint.pylint]
max-args = 15 # default is 5
max-branches = 28 # default is 12
max-returns = 13 # default is 6
max-returns = 15 # default is 6
max-statements = 134 # default is 50

[tool.ruff.per-file-target-version]
Expand Down
70 changes: 68 additions & 2 deletions src/pip/_internal/build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
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

from pip import __file__ as pip_location
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
Expand All @@ -28,6 +29,10 @@
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__)


Expand Down Expand Up @@ -101,8 +106,44 @@ 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.
"""
if self._build_constraint_feature_enabled or self._build_constraints:
return

pip_constraint = os.environ.get("PIP_CONSTRAINT")
if not pip_constraint or not pip_constraint.strip():
return

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,
Expand All @@ -112,6 +153,8 @@ def install(
kind: str,
for_req: InstallRequirement | None,
) -> None:
self._deprecation_constraint_check()

finder = self.finder
args: list[str] = [
sys.executable,
Expand Down Expand Up @@ -167,6 +210,28 @@ def install(
args.append("--pre")
if finder.prefer_binary:
args.append("--prefer-binary")
if finder.uploaded_prior_to:
args.extend(["--uploaded-prior-to", finder.uploaded_prior_to.isoformat()])

# 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)

Expand All @@ -178,6 +243,7 @@ def install(
args,
command_desc=f"installing {kind}{identify_requirement}",
spinner=spinner,
**extra_environ,
)


Expand Down
88 changes: 88 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from pip._internal.models.format_control import FormatControl
from pip._internal.models.index import PyPI
from pip._internal.models.target_python import TargetPython
from pip._internal.utils.datetime import parse_iso_datetime
from pip._internal.utils.hashes import STRONG_HASHES
from pip._internal.utils.misc import strtobool

Expand Down Expand Up @@ -101,6 +102,29 @@ 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

# Eagerly check build constraints file contents
# is valid so that we don't fail in when trying
# to check constraints in isolated build process
with PipSession() as session:
for constraint_file in options.build_constraints:
get_file_content(constraint_file, session)


def _path_option_check(option: Option, opt: str, value: str) -> str:
return os.path.expanduser(value)

Expand Down Expand Up @@ -430,6 +454,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",
Expand Down Expand Up @@ -796,6 +835,54 @@ def _handle_dependency_group(
help="Ignore the Requires-Python information.",
)


def _handle_uploaded_prior_to(
option: Option, opt: str, value: str, parser: OptionParser
) -> None:
"""
This is an optparse.Option callback for the --uploaded-prior-to option.

Parses an ISO 8601 datetime string. If no timezone is specified in the string,
local timezone is used.

Note: This option only works with indexes that provide upload-time metadata
as specified in the simple repository API:
https://packaging.python.org/en/latest/specifications/simple-repository-api/
"""
if value is None:
return None

try:
uploaded_prior_to = parse_iso_datetime(value)
# Use local timezone if no offset is given in the ISO string.
if uploaded_prior_to.tzinfo is None:
uploaded_prior_to = uploaded_prior_to.astimezone()
parser.values.uploaded_prior_to = uploaded_prior_to
except ValueError as exc:
msg = (
f"invalid --uploaded-prior-to value: {value!r}: {exc}. "
f"Expected an ISO 8601 datetime string, "
f"e.g '2023-01-01' or '2023-01-01T00:00:00Z'"
)
raise_option_error(parser, option=option, msg=msg)


uploaded_prior_to: Callable[..., Option] = partial(
Option,
"--uploaded-prior-to",
dest="uploaded_prior_to",
metavar="datetime",
action="callback",
callback=_handle_uploaded_prior_to,
type="str",
help=(
"Only consider packages uploaded prior to the given date time. "
"Accepts ISO 8601 strings (e.g., '2023-01-01T00:00:00Z'). "
"Uses local timezone if none specified. Only effective when "
"installing from indexes that provide upload-time metadata."
),
)

no_build_isolation: Callable[..., Option] = partial(
Option,
"--no-build-isolation",
Expand Down Expand Up @@ -1072,6 +1159,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.",
Expand Down
Loading