Skip to content

Commit 893d315

Browse files
committed
Rename to --uploaded-prior-to, require remote index to have upload-time, pass to SubprocessBuildEnvironmentInstaller
1 parent b8f768d commit 893d315

File tree

15 files changed

+194
-73
lines changed

15 files changed

+194
-73
lines changed

docs/html/user_guide.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,66 @@ Example build constraints file (``build-constraints.txt``):
297297
cython==0.29.24
298298
299299
300+
.. _`Filtering by Upload Time`:
301+
302+
Filtering by Upload Time
303+
=========================
304+
305+
The ``--uploaded-prior-to`` option allows you to filter packages by their upload time
306+
to an index, only considering packages that were uploaded before a specified datetime.
307+
This can be useful for creating reproducible builds by ensuring you only install
308+
packages that were available at a known point in time.
309+
310+
.. tab:: Unix/macOS
311+
312+
.. code-block:: shell
313+
314+
python -m pip install --uploaded-prior-to=2025-03-16T00:00:00Z SomePackage
315+
316+
.. tab:: Windows
317+
318+
.. code-block:: shell
319+
320+
py -m pip install --uploaded-prior-to=2025-03-16T00:00:00Z SomePackage
321+
322+
The option accepts ISO 8601 datetime strings in several formats:
323+
324+
* ``2025-03-16`` - Date in local timezone
325+
* ``2025-03-16 12:30:00`` - Datetime in local timezone
326+
* ``2025-03-16T12:30:00Z`` - Datetime in UTC
327+
* ``2025-03-16T12:30:00+05:00`` - Datetime in UTC offset
328+
329+
For consistency across machines, use either UTC format (with 'Z' suffix) or UTC offset
330+
format (with timezone offset like '+05:00'). Local timezone formats may produce different
331+
results on different machines.
332+
333+
.. note::
334+
335+
This option only applies to packages from indexes, not local files. Local
336+
package files are allowed regardless of the ``--uploaded-prior-to`` setting.
337+
e.g., ``pip install /path/to/package.whl`` or packages from
338+
``--find-links`` directories.
339+
340+
This option requires package indexes that provide upload-time metadata
341+
(such as PyPI). If the index does not provide upload-time metadata for a
342+
package file, pip will fail immediately with an error message indicating
343+
that upload-time metadata is required when using ``--uploaded-prior-to``.
344+
345+
You can combine this option with other filtering mechanisms like constraints files:
346+
347+
.. tab:: Unix/macOS
348+
349+
.. code-block:: shell
350+
351+
python -m pip install -c constraints.txt --uploaded-prior-to=2025-03-16 SomePackage
352+
353+
.. tab:: Windows
354+
355+
.. code-block:: shell
356+
357+
py -m pip install -c constraints.txt --uploaded-prior-to=2025-03-16 SomePackage
358+
359+
300360
.. _`Dependency Groups`:
301361

302362

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ max-complexity = 33 # default is 10
268268
[tool.ruff.lint.pylint]
269269
max-args = 15 # default is 5
270270
max-branches = 28 # default is 12
271-
max-returns = 14 # default is 6
271+
max-returns = 15 # default is 6
272272
max-statements = 134 # default is 50
273273

274274
[tool.ruff.per-file-target-version]

src/pip/_internal/build_env.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ def install(
230230
# in the isolated build environment
231231
extra_environ = {"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}}
232232

233+
if finder.uploaded_prior_to:
234+
args.extend(["--uploaded-prior-to", finder.uploaded_prior_to.isoformat()])
233235
args.append("--")
234236
args.extend(requirements)
235237

src/pip/_internal/cli/cmdoptions.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from pip._internal.models.format_control import FormatControl
2929
from pip._internal.models.index import PyPI
3030
from pip._internal.models.target_python import TargetPython
31+
from pip._internal.utils.datetime import parse_iso_datetime
3132
from pip._internal.utils.hashes import STRONG_HASHES
3233
from pip._internal.utils.misc import strtobool
3334

@@ -833,31 +834,51 @@ def _handle_dependency_group(
833834
)
834835

835836

836-
def _handle_upload_before(
837+
def _handle_uploaded_prior_to(
837838
option: Option, opt: str, value: str, parser: OptionParser
838839
) -> None:
839840
"""
840-
Process a value provided for the --upload-before option.
841+
This is an optparse.Option callback for the --uploaded-prior-to option.
841842
842-
This is an optparse.Option callback for the --upload-before option.
843+
Parses an ISO 8601 datetime string. If no timezone is specified in the string,
844+
local timezone is used.
845+
846+
Note: This option only works with indexes that provide upload-time metadata
847+
as specified in the simple repository API:
848+
https://packaging.python.org/en/latest/specifications/simple-repository-api/
843849
"""
844850
if value is None:
845851
return None
846-
upload_before = datetime.datetime.fromisoformat(value)
847-
# Assume local timezone if no offset is given in the ISO string.
848-
if upload_before.tzinfo is None:
849-
upload_before = upload_before.astimezone()
850-
parser.values.upload_before = upload_before
852+
853+
try:
854+
uploaded_prior_to = parse_iso_datetime(value)
855+
# Use local timezone if no offset is given in the ISO string.
856+
if uploaded_prior_to.tzinfo is None:
857+
uploaded_prior_to = uploaded_prior_to.astimezone()
858+
parser.values.uploaded_prior_to = uploaded_prior_to
859+
except ValueError as exc:
860+
msg = (
861+
f"invalid --uploaded-prior-to value: {value!r}: {exc}. "
862+
f"Expected an ISO 8601 datetime string, "
863+
f"e.g '2023-01-01' or '2023-01-01T00:00:00Z'"
864+
)
865+
raise_option_error(parser, option=option, msg=msg)
851866

852867

853-
upload_before: Callable[..., Option] = partial(
868+
uploaded_prior_to: Callable[..., Option] = partial(
854869
Option,
855-
"--upload-before",
856-
dest="upload_before",
870+
"--uploaded-prior-to",
871+
dest="uploaded_prior_to",
857872
metavar="datetime",
858873
action="callback",
859-
callback=_handle_upload_before,
860-
help="Skip uploads after given time. This should be an ISO 8601 string.",
874+
callback=_handle_uploaded_prior_to,
875+
type="str",
876+
help=(
877+
"Only consider packages uploaded prior to the given date time. "
878+
"Accepts ISO 8601 strings (e.g., '2023-01-01T00:00:00Z'). "
879+
"Uses local timezone if none specified. Only effective when "
880+
"installing from indexes that provide upload-time metadata."
881+
),
861882
)
862883

863884
no_build_isolation: Callable[..., Option] = partial(

src/pip/_internal/cli/req_command.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
PackageFinder machinery and all its vendored dependencies, etc.
66
"""
77

8+
from __future__ import annotations
9+
810
import logging
911
import os
1012
from functools import partial
@@ -346,7 +348,6 @@ def _build_package_finder(
346348
session: PipSession,
347349
target_python: TargetPython | None = None,
348350
ignore_requires_python: bool | None = None,
349-
upload_before: datetime.datetime | None = None,
350351
) -> PackageFinder:
351352
"""
352353
Create a package finder appropriate to this requirement command.
@@ -367,5 +368,5 @@ def _build_package_finder(
367368
link_collector=link_collector,
368369
selection_prefs=selection_prefs,
369370
target_python=target_python,
370-
upload_before=upload_before,
371+
uploaded_prior_to=options.uploaded_prior_to,
371372
)

src/pip/_internal/commands/download.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def add_options(self) -> None:
4949
self.cmd_opts.add_option(cmdoptions.use_pep517())
5050
self.cmd_opts.add_option(cmdoptions.check_build_deps())
5151
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
52-
self.cmd_opts.add_option(cmdoptions.upload_before())
52+
self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
5353

5454
self.cmd_opts.add_option(
5555
"-d",
@@ -93,7 +93,6 @@ def run(self, options: Values, args: list[str]) -> int:
9393
session=session,
9494
target_python=target_python,
9595
ignore_requires_python=options.ignore_requires_python,
96-
upload_before=options.upload_before,
9796
)
9897

9998
build_tracker = self.enter_context(get_build_tracker())

src/pip/_internal/commands/index.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import datetime
1+
from __future__ import annotations
2+
3+
import json
24
import logging
35
from collections.abc import Iterable
46
from optparse import Values
@@ -38,7 +40,7 @@ def add_options(self) -> None:
3840
cmdoptions.add_target_python_options(self.cmd_opts)
3941

4042
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
41-
self.cmd_opts.add_option(cmdoptions.upload_before())
43+
self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
4244
self.cmd_opts.add_option(cmdoptions.pre())
4345
self.cmd_opts.add_option(cmdoptions.json())
4446
self.cmd_opts.add_option(cmdoptions.no_binary())
@@ -85,7 +87,6 @@ def _build_package_finder(
8587
session: PipSession,
8688
target_python: TargetPython | None = None,
8789
ignore_requires_python: bool | None = None,
88-
upload_before: datetime.datetime | None = None,
8990
) -> PackageFinder:
9091
"""
9192
Create a package finder appropriate to the index command.
@@ -103,7 +104,7 @@ def _build_package_finder(
103104
link_collector=link_collector,
104105
selection_prefs=selection_prefs,
105106
target_python=target_python,
106-
upload_before=upload_before,
107+
uploaded_prior_to=options.uploaded_prior_to,
107108
)
108109

109110
def get_available_package_versions(self, options: Values, args: list[Any]) -> None:
@@ -119,7 +120,6 @@ def get_available_package_versions(self, options: Values, args: list[Any]) -> No
119120
session=session,
120121
target_python=target_python,
121122
ignore_requires_python=options.ignore_requires_python,
122-
upload_before=options.upload_before,
123123
)
124124

125125
versions: Iterable[Version] = (

src/pip/_internal/commands/install.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def add_options(self) -> None:
207207
),
208208
)
209209

210-
self.cmd_opts.add_option(cmdoptions.upload_before())
210+
self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
211211
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
212212
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
213213
self.cmd_opts.add_option(cmdoptions.use_pep517())
@@ -342,7 +342,6 @@ def run(self, options: Values, args: list[str]) -> int:
342342
session=session,
343343
target_python=target_python,
344344
ignore_requires_python=options.ignore_requires_python,
345-
upload_before=options.upload_before,
346345
)
347346
build_tracker = self.enter_context(get_build_tracker())
348347

src/pip/_internal/commands/list.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,8 @@ def handle_pip_version_check(self, options: Values) -> None:
143143
super().handle_pip_version_check(options)
144144

145145
def _build_package_finder(
146-
self,
147-
options: Values,
148-
session: "PipSession",
149-
) -> "PackageFinder":
146+
self, options: Values, session: PipSession
147+
) -> PackageFinder:
150148
"""
151149
Create a package finder appropriate to this list command.
152150
"""

src/pip/_internal/commands/lock.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def add_options(self) -> None:
6565
self.cmd_opts.add_option(cmdoptions.src())
6666

6767
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
68+
self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
6869
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
6970
self.cmd_opts.add_option(cmdoptions.use_pep517())
7071
self.cmd_opts.add_option(cmdoptions.check_build_deps())

0 commit comments

Comments
 (0)