Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions pex/pip/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Comment on lines +132 to +143
Copy link
Copy Markdown
Member

@jsirois jsirois Mar 27, 2026

Choose a reason for hiding this comment

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

@cburroughs does the hard error on Pip's part when --uploaded-prior-to metadata is not available from the index suggest your warning should be an error as well? I meant to note this earlier in the review, but the Pip error prompts me to re-consider this. Can you think of a good + real reason to not hard error?. Hard errors are nice because you can relax them later and not break anyone. The reverse is not true, and I'm bound by Pex ethics to never break anyone.



class PackageIndexConfiguration(object):
@staticmethod
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions pex/resolve/configured_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
3 changes: 3 additions & 0 deletions pex/resolve/lockfile/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pex/resolve/resolver_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
16 changes: 16 additions & 0 deletions pex/resolve/resolver_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
)


Expand Down
6 changes: 6 additions & 0 deletions pex/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
58 changes: 58 additions & 0 deletions tests/integration/cli/commands/test_lock_uploaded_prior_to.py
Original file line number Diff line number Diff line change
@@ -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",
Comment on lines +23 to +24
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I assume this will fail on the py27 and py38 shards. I'd think you'd need a @pytest.mark.skipif and an appropriate condition guarding this test and the test below.

Copy link
Copy Markdown
Member

@jsirois jsirois Mar 27, 2026

Choose a reason for hiding this comment

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

Aha, even fails on new Pythons:

 pip: ERROR: Index http://127.0.0.1:49291/root/pypi/+simple/cowsay/ does not provide upload-time metadata.

N.B.: I use --devpi for tests by default for less flake / better netizen. You can bypass this in a test with one of these approaches:

  • @pytest.fixture
    def devpi_clean_env():
    # type: () -> Mapping[str, Any]
    # These will be set when tests are run with --devpi, and we want to unset them to
    # ensure our Pex command line config above is what is used.
    return dict(
    _PEX_USE_PIP_CONFIG=None,
    PIP_INDEX_URL=None,
    PIP_TRUSTED_HOST=None,
    )
  • # This test has problems completing its resolve just using --devpi, so we ensure PyPI is
    # also used.
    "--use-pip-config",
    env=make_env(PIP_EXTRA_INDEX_URL=PYPI),

And the devpi support is an open issue: devpi/devpi#1061

"--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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why? I'm not seeing how this tests your Pex integration. It seems to test Pip.

# 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
10 changes: 10 additions & 0 deletions tests/resolve/test_resolver_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is fine, but I view unit tests as useless in the best case, and detrimental to refactors in the worst when you have an IT that covers like you do.

# 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"
15 changes: 14 additions & 1 deletion tests/test_pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading