From c562e62bac4b5d6855710569ef390f2a90ac99dd Mon Sep 17 00:00:00 2001 From: Chris Burroughs Date: Fri, 27 Mar 2026 15:52:39 -0400 Subject: [PATCH] pass thru --uploaded-prior-to on Pip v26.0 Pip 26.0 adds support for --uploaded-prior-to for filtering packages by their upload time to an index. See for details. This change passes through --uploaded-prior-to on recent enough Pip versions, and emits a warning on older ones. NOTE: When Pip is building an sdist, this setting also applies to build dependencies and it is thus easy when using far-in-the-past values to end up in a tangle of incompatible Python/setuptools versions. This is out of Pex's control, but might be the first thing you hit if you are like me and reach for old `cowsay` versions right away. closes #3128 --- pex/pip/tool.py | 21 +++++++ pex/resolve/configured_resolve.py | 1 + pex/resolve/lockfile/create.py | 3 + pex/resolve/resolver_configuration.py | 1 + pex/resolve/resolver_options.py | 16 +++++ pex/resolver.py | 6 ++ .../commands/test_lock_uploaded_prior_to.py | 58 +++++++++++++++++++ tests/resolve/test_resolver_options.py | 10 ++++ tests/test_pip.py | 15 ++++- 9 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tests/integration/cli/commands/test_lock_uploaded_prior_to.py diff --git a/pex/pip/tool.py b/pex/pip/tool.py index ff7978d48..743af863d 100644 --- a/pex/pip/tool.py +++ b/pex/pip/tool.py @@ -70,6 +70,7 @@ class PipArgs(object): indexes = attr.ib(default=None) # type: Optional[Sequence[Text]] find_links = attr.ib(default=None) # type: Optional[Iterable[Text]] network_configuration = attr.ib(default=None) # type: Optional[NetworkConfiguration] + uploaded_prior_to = attr.ib(default=None) # type: Optional[str] def iter(self, version): # type: (PipVersionValue) -> Iterator[str] @@ -123,6 +124,24 @@ def maybe_trust_insecure_host(url): yield "--timeout" yield str(network_configuration.timeout) + if self.uploaded_prior_to: + if version >= PipVersion.v26_0: + yield "--uploaded-prior-to" + yield self.uploaded_prior_to + else: + warn_msg = ( + "The `--uploaded-prior-to` was set but Pip v{THIS_VERSION} " + "does not support the `--uploaded-prior-to` option (which " + "is only available in Pip v{VERSION_26_0} and later " + "versions). Consequently, Pex is ignoring the " + "`--uploaded-prior-to` option for this particular Pip " + "invocation.".format( + THIS_VERSION=version, + VERSION_26_0=PipVersion.v26_0, + ) + ) + pex_warnings.warn(warn_msg) + class PackageIndexConfiguration(object): @staticmethod @@ -160,6 +179,7 @@ def create( use_pip_config=False, # type: bool extra_pip_requirements=(), # type: Tuple[Requirement, ...] keyring_provider=None, # type: Optional[str] + uploaded_prior_to=None, # type: Optional[str] ): # type: (...) -> PackageIndexConfiguration resolver_version = resolver_version or ResolverVersion.default(pip_version) @@ -178,6 +198,7 @@ def create( indexes=repos_configuration.indexes, find_links=repos_configuration.find_links, network_configuration=network_configuration, + uploaded_prior_to=uploaded_prior_to, ), env=cls._calculate_env( network_configuration=network_configuration, use_pip_config=use_pip_config diff --git a/pex/resolve/configured_resolve.py b/pex/resolve/configured_resolve.py index 18d6db756..abf9f12c9 100644 --- a/pex/resolve/configured_resolve.py +++ b/pex/resolve/configured_resolve.py @@ -189,6 +189,7 @@ def resolve( use_pip_config=resolver_configuration.use_pip_config, extra_pip_requirements=resolver_configuration.extra_requirements, keyring_provider=resolver_configuration.keyring_provider, + uploaded_prior_to=resolver_configuration.uploaded_prior_to, result_type=result_type, dependency_configuration=dependency_configuration, ) diff --git a/pex/resolve/lockfile/create.py b/pex/resolve/lockfile/create.py index 89a95f56c..3c6c852e6 100644 --- a/pex/resolve/lockfile/create.py +++ b/pex/resolve/lockfile/create.py @@ -557,6 +557,7 @@ def _create_lock_pip_download( use_pip_config=pip_configuration.use_pip_config, extra_pip_requirements=pip_configuration.extra_requirements, keyring_provider=pip_configuration.keyring_provider, + uploaded_prior_to=pip_configuration.uploaded_prior_to, dependency_configuration=dependency_configuration, ) except resolvers.ResolveError as e: @@ -594,6 +595,7 @@ def _create_lock_pip_reports( use_pip_config=pip_configuration.use_pip_config, extra_pip_requirements=pip_configuration.extra_requirements, keyring_provider=pip_configuration.keyring_provider, + uploaded_prior_to=pip_configuration.uploaded_prior_to, dependency_configuration=dependency_configuration, ) except resolvers.ResolveError as e: @@ -634,6 +636,7 @@ def create( use_pip_config=pip_configuration.use_pip_config, extra_pip_requirements=pip_configuration.extra_requirements, keyring_provider=pip_configuration.keyring_provider, + uploaded_prior_to=pip_configuration.uploaded_prior_to, ) configured_resolver = ConfiguredResolver(pip_configuration=pip_configuration) diff --git a/pex/resolve/resolver_configuration.py b/pex/resolve/resolver_configuration.py index a5716449f..72717ab0a 100644 --- a/pex/resolve/resolver_configuration.py +++ b/pex/resolve/resolver_configuration.py @@ -167,6 +167,7 @@ class PipConfiguration(object): use_pip_config = attr.ib(default=False) # type: bool extra_requirements = attr.ib(default=()) # type Tuple[Requirement, ...] keyring_provider = attr.ib(default=None) # type: Optional[str] + uploaded_prior_to = attr.ib(default=None) # type: Optional[str] @property def pip_configuration(self): diff --git a/pex/resolve/resolver_options.py b/pex/resolve/resolver_options.py index 82c78e82a..e1cbffc4a 100644 --- a/pex/resolve/resolver_options.py +++ b/pex/resolve/resolver_options.py @@ -207,6 +207,21 @@ def register( ), ) + parser.add_argument( + "--uploaded-prior-to", + dest="uploaded_prior_to", + type=str, + default=None, + help=( + "Configure Pip to 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. Only available in Pip v26.0 and later. " + "See: https://pip.pypa.io/en/stable/user_guide/#filtering-by-upload-time" + ), + ) + register_repos_options(parser) register_network_options(parser) @@ -893,6 +908,7 @@ def create_pip_configuration( use_pip_config=get_use_pip_config_value(options), extra_requirements=tuple(options.extra_pip_requirements), keyring_provider=options.keyring_provider, + uploaded_prior_to=options.uploaded_prior_to, ) diff --git a/pex/resolver.py b/pex/resolver.py index f43e23fe4..dca395233 100644 --- a/pex/resolver.py +++ b/pex/resolver.py @@ -1543,6 +1543,7 @@ def resolve( use_pip_config=False, # type: bool extra_pip_requirements=(), # type: Tuple[Requirement, ...] keyring_provider=None, # type: Optional[str] + uploaded_prior_to=None, # type: Optional[str] result_type=InstallableType.INSTALLED_WHEEL_CHROOT, # type: InstallableType.Value dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration ): @@ -1638,6 +1639,7 @@ def resolve( use_pip_config=use_pip_config, extra_pip_requirements=extra_pip_requirements, keyring_provider=keyring_provider, + uploaded_prior_to=uploaded_prior_to, ) requests = tuple( @@ -1934,6 +1936,7 @@ def download_requests( use_pip_config=False, # type: bool extra_pip_requirements=(), # type: Tuple[Requirement, ...] keyring_provider=None, # type: Optional[str] + uploaded_prior_to=None, # type: Optional[str] dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration ): # type: (...) -> Downloaded @@ -1946,6 +1949,7 @@ def download_requests( use_pip_config=use_pip_config, extra_pip_requirements=extra_pip_requirements, keyring_provider=keyring_provider, + uploaded_prior_to=uploaded_prior_to, ) build_requests, download_results = _download_internal( @@ -2021,6 +2025,7 @@ def reports( use_pip_config=False, # type: bool extra_pip_requirements=(), # type: Tuple[Requirement, ...] keyring_provider=None, # type: Optional[str] + uploaded_prior_to=None, # type: Optional[str] dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration ): # type: (...) -> Reports @@ -2033,6 +2038,7 @@ def reports( use_pip_config=use_pip_config, extra_pip_requirements=extra_pip_requirements, keyring_provider=keyring_provider, + uploaded_prior_to=uploaded_prior_to, ) pip_session = _PipSession( diff --git a/tests/integration/cli/commands/test_lock_uploaded_prior_to.py b/tests/integration/cli/commands/test_lock_uploaded_prior_to.py new file mode 100644 index 000000000..baf8d85fa --- /dev/null +++ b/tests/integration/cli/commands/test_lock_uploaded_prior_to.py @@ -0,0 +1,58 @@ +# Copyright 2026 Pex project contributors. +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +import os + +from pex.pep_440 import Version +from pex.resolve.lockfile import json_codec +from pex.typing import TYPE_CHECKING +from testing.cli import run_pex3 + +if TYPE_CHECKING: + from typing import Any + + +def test_uploaded_prior_to_filters_to_older_version(tmpdir): + # type: (Any) -> None + + lock_file = os.path.join(str(tmpdir), "cowsay.lock.json") + run_pex3( + "lock", + "create", + "cowsay", + "--pip-version", + "26.0", + "--uploaded-prior-to", + "2023-09-20", + "-o", + lock_file, + ).assert_success() + + lock = json_codec.load(lock_file) + assert 1 == len(lock.locked_resolves) + locked_resolve = lock.locked_resolves[0] + assert 1 == len(locked_resolve.locked_requirements) + assert Version("6.0") == locked_resolve.locked_requirements[0].pin.version + + +def test_uploaded_prior_to_far_future_allows_latest(tmpdir): + # type: (Any) -> None + + lock_file = os.path.join(str(tmpdir), "cowsay.lock.json") + run_pex3( + "lock", + "create", + "cowsay==6.1", + "--pip-version", + "26.0", + "--uploaded-prior-to", + "2063-04-05", + "-o", + lock_file, + ).assert_success() + + lock = json_codec.load(lock_file) + assert 1 == len(lock.locked_resolves) + locked_resolve = lock.locked_resolves[0] + assert 1 == len(locked_resolve.locked_requirements) + assert Version("6.1") == locked_resolve.locked_requirements[0].pin.version diff --git a/tests/resolve/test_resolver_options.py b/tests/resolve/test_resolver_options.py index 8d976259b..82aa6921f 100644 --- a/tests/resolve/test_resolver_options.py +++ b/tests/resolve/test_resolver_options.py @@ -420,3 +420,13 @@ def test_multiple_unnamed_indexes(parser): mac_scoped_repos = repos_configuration.scoped({"sys_platform": "darwin"}) assert [] == mac_scoped_repos.in_scope_indexes(ProjectName("torch")) assert [] == mac_scoped_repos.in_scope_find_links(ProjectName("torch")) + + +def test_resolver_uploaded_prior_to_passthru(parser): + # type: (ArgumentParser) -> None + resolver_options.register(parser) + + pip_configuration = compute_pip_configuration( + parser, args=["--pip-version", "26.0", "--uploaded-prior-to", "2015-10-21"] + ) + assert pip_configuration.uploaded_prior_to == "2015-10-21" diff --git a/tests/test_pip.py b/tests/test_pip.py index 17933b7ea..a8cac562c 100644 --- a/tests/test_pip.py +++ b/tests/test_pip.py @@ -22,7 +22,7 @@ from pex.pep_503 import ProjectName from pex.pex_warnings import PEXWarning from pex.pip.installation import _PIP, PipInstallation, get_pip -from pex.pip.tool import PackageIndexConfiguration, Pip +from pex.pip.tool import PackageIndexConfiguration, Pip, PipArgs from pex.pip.version import PipVersion, PipVersionValue from pex.resolve import abbreviated_platforms from pex.resolve.configured_resolver import ConfiguredResolver @@ -457,6 +457,19 @@ def test_keyring_provider( assert "does not support the `--keyring-provider` option" in message +def test_uploaded_prior_to_warning(): + # type: () -> None + args = PipArgs(uploaded_prior_to="2015-10-21") + + with warnings.catch_warnings(record=True) as events: + list(args.iter(PipVersion.v25_1)) + + assert len(events) == 1 + assert PEXWarning == events[0].category + message = str(events[0].message).replace("\n", " ") + assert "--uploaded-prior-to" in message + + @applicable_pip_versions def test_extra_pip_requirements_pip_not_allowed( create_pip, # type: CreatePip