Skip to content

Implement --upload-before #12717

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,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 = 14 # default is 6
max-statements = 134 # default is 50

######################################################################################
Expand Down
29 changes: 29 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False

import datetime
import importlib.util
import logging
import os
Expand Down Expand Up @@ -741,6 +742,34 @@ def _handle_no_cache_dir(
help="Ignore the Requires-Python information.",
)


def _handle_upload_before(
option: Option, opt: str, value: str, parser: OptionParser
) -> None:
"""
Process a value provided for the --upload-before option.

This is an optparse.Option callback for the --upload-before option.
"""
if value is None:
return None
upload_before = datetime.datetime.fromisoformat(value)
# Assume local timezone if no offset is given in the ISO string.
if upload_before.tzinfo is None:
upload_before = upload_before.astimezone()
Comment on lines +756 to +759
Copy link
Member

@pradyunsg pradyunsg May 21, 2024

Choose a reason for hiding this comment

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

We should use the utc timezone here, unconditionally. That's what we'll be comparing against coming out of PyPI's responses AFAICT.

parser.values.upload_before = upload_before


upload_before: Callable[..., Option] = partial(
Option,
"--upload-before",
dest="upload_before",
metavar="datetime",
action="callback",
callback=_handle_upload_before,
help="Skip uploads after given time. This should be an ISO 8601 string.",
)

no_build_isolation: Callable[..., Option] = partial(
Option,
"--no-build-isolation",
Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
PackageFinder machinery and all its vendored dependencies, etc.
"""

import datetime
import logging
from functools import partial
from optparse import Values
Expand Down Expand Up @@ -306,6 +307,7 @@ def _build_package_finder(
session: PipSession,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
upload_before: Optional[datetime.datetime] = None,
) -> PackageFinder:
"""
Create a package finder appropriate to this requirement command.
Expand All @@ -326,4 +328,5 @@ def _build_package_finder(
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
upload_before=upload_before,
)
2 changes: 2 additions & 0 deletions src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.upload_before())

self.cmd_opts.add_option(
"-d",
Expand Down Expand Up @@ -94,6 +95,7 @@ def run(self, options: Values, args: List[str]) -> int:
session=session,
target_python=target_python,
ignore_requires_python=options.ignore_requires_python,
upload_before=options.upload_before,
)

build_tracker = self.enter_context(get_build_tracker())
Expand Down
5 changes: 5 additions & 0 deletions src/pip/_internal/commands/index.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import logging
from optparse import Values
from typing import Any, Iterable, List, Optional
Expand Down Expand Up @@ -33,6 +34,7 @@ def add_options(self) -> None:
cmdoptions.add_target_python_options(self.cmd_opts)

self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.upload_before())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
Expand Down Expand Up @@ -81,6 +83,7 @@ def _build_package_finder(
session: PipSession,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
upload_before: Optional[datetime.datetime] = None,
) -> PackageFinder:
"""
Create a package finder appropriate to the index command.
Expand All @@ -98,6 +101,7 @@ def _build_package_finder(
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
upload_before=upload_before,
)

def get_available_package_versions(self, options: Values, args: List[Any]) -> None:
Expand All @@ -113,6 +117,7 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No
session=session,
target_python=target_python,
ignore_requires_python=options.ignore_requires_python,
upload_before=options.upload_before,
)

versions: Iterable[Version] = (
Expand Down
2 changes: 2 additions & 0 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def add_options(self) -> None:
),
)

self.cmd_opts.add_option(cmdoptions.upload_before())
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
Expand Down Expand Up @@ -329,6 +330,7 @@ def run(self, options: Values, args: List[str]) -> int:
session=session,
target_python=target_python,
ignore_requires_python=options.ignore_requires_python,
upload_before=options.upload_before,
)
build_tracker = self.enter_context(get_build_tracker())

Expand Down
4 changes: 3 additions & 1 deletion src/pip/_internal/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ def handle_pip_version_check(self, options: Values) -> None:
super().handle_pip_version_check(options)

def _build_package_finder(
self, options: Values, session: "PipSession"
self,
options: Values,
session: "PipSession",
) -> "PackageFinder":
"""
Create a package finder appropriate to this list command.
Expand Down
7 changes: 6 additions & 1 deletion src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.upload_before())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.progress_bar())

Expand Down Expand Up @@ -104,7 +105,11 @@ def add_options(self) -> None:
def run(self, options: Values, args: List[str]) -> int:
session = self.get_default_session(options)

finder = self._build_package_finder(options, session)
finder = self._build_package_finder(
options=options,
session=session,
upload_before=options.upload_before,
)

options.wheel_dir = normalize_path(options.wheel_dir)
ensure_dir(options.wheel_dir)
Expand Down
16 changes: 16 additions & 0 deletions src/pip/_internal/index/package_finder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Routines related to PyPI, indexes"""

import datetime
import enum
import functools
import itertools
Expand Down Expand Up @@ -104,6 +105,7 @@ class LinkType(enum.Enum):
format_invalid = enum.auto()
platform_mismatch = enum.auto()
requires_python_mismatch = enum.auto()
upload_too_late = enum.auto()


class LinkEvaluator:
Expand All @@ -125,6 +127,7 @@ def __init__(
target_python: TargetPython,
allow_yanked: bool,
ignore_requires_python: Optional[bool] = None,
upload_before: Optional[datetime.datetime] = None,
) -> None:
"""
:param project_name: The user supplied package name.
Expand All @@ -142,6 +145,7 @@ def __init__(
:param ignore_requires_python: Whether to ignore incompatible
PEP 503 "data-requires-python" values in HTML links. Defaults
to False.
:param upload_before: If set, only allow links prior to the given date.
"""
if ignore_requires_python is None:
ignore_requires_python = False
Expand All @@ -151,6 +155,7 @@ def __init__(
self._ignore_requires_python = ignore_requires_python
self._formats = formats
self._target_python = target_python
self._upload_before = upload_before

self.project_name = project_name

Expand All @@ -169,6 +174,11 @@ def evaluate_link(self, link: Link) -> Tuple[LinkType, str]:
reason = link.yanked_reason or "<none given>"
return (LinkType.yanked, f"yanked for reason: {reason}")

if link.upload_time is not None and self._upload_before is not None:
if link.upload_time > self._upload_before:
reason = f"Upload time {link.upload_time} after {self._upload_before}"
return (LinkType.upload_too_late, reason)

if link.egg_fragment:
egg_info = link.egg_fragment
ext = link.ext
Expand Down Expand Up @@ -593,6 +603,7 @@ def __init__(
format_control: Optional[FormatControl] = None,
candidate_prefs: Optional[CandidatePreferences] = None,
ignore_requires_python: Optional[bool] = None,
upload_before: Optional[datetime.datetime] = None,
) -> None:
"""
This constructor is primarily meant to be used by the create() class
Expand All @@ -614,6 +625,7 @@ def __init__(
self._ignore_requires_python = ignore_requires_python
self._link_collector = link_collector
self._target_python = target_python
self._upload_before = upload_before

self.format_control = format_control

Expand All @@ -630,6 +642,7 @@ def create(
link_collector: LinkCollector,
selection_prefs: SelectionPreferences,
target_python: Optional[TargetPython] = None,
upload_before: Optional[datetime.datetime] = None,
) -> "PackageFinder":
"""Create a PackageFinder.

Expand All @@ -638,6 +651,7 @@ def create(
:param target_python: The target Python interpreter to use when
checking compatibility. If None (the default), a TargetPython
object will be constructed from the running Python.
:param upload_before: If set, only find links prior to the given date.
"""
if target_python is None:
target_python = TargetPython()
Expand All @@ -654,6 +668,7 @@ def create(
allow_yanked=selection_prefs.allow_yanked,
format_control=selection_prefs.format_control,
ignore_requires_python=selection_prefs.ignore_requires_python,
upload_before=upload_before,
)

@property
Expand Down Expand Up @@ -714,6 +729,7 @@ def make_link_evaluator(self, project_name: str) -> LinkEvaluator:
target_python=self._target_python,
allow_yanked=self._allow_yanked,
ignore_requires_python=self._ignore_requires_python,
upload_before=self._upload_before,
)

def _sort_links(self, links: Iterable[Link]) -> List[Link]:
Expand Down
13 changes: 13 additions & 0 deletions src/pip/_internal/models/link.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import functools
import itertools
import logging
Expand Down Expand Up @@ -190,6 +191,7 @@ class Link:
"requires_python",
"yanked_reason",
"metadata_file_data",
"upload_time",
"cache_link_parsing",
"egg_fragment",
]
Expand All @@ -201,6 +203,7 @@ def __init__(
requires_python: Optional[str] = None,
yanked_reason: Optional[str] = None,
metadata_file_data: Optional[MetadataFile] = None,
upload_time: Optional[datetime.datetime] = None,
cache_link_parsing: bool = True,
hashes: Optional[Mapping[str, str]] = None,
) -> None:
Expand All @@ -222,6 +225,8 @@ def __init__(
no such metadata is provided. This argument, if not None, indicates
that a separate metadata file exists, and also optionally supplies
hashes for that file.
:param upload_time: upload time of the file, or None if the information
is not available from the server.
:param cache_link_parsing: A flag that is used elsewhere to determine
whether resources retrieved from this link should be cached. PyPI
URLs should generally have this set to False, for example.
Expand Down Expand Up @@ -253,6 +258,7 @@ def __init__(
self.requires_python = requires_python if requires_python else None
self.yanked_reason = yanked_reason
self.metadata_file_data = metadata_file_data
self.upload_time = upload_time

self.cache_link_parsing = cache_link_parsing
self.egg_fragment = self._egg_fragment()
Expand Down Expand Up @@ -281,6 +287,12 @@ def from_json(
if metadata_info is None:
metadata_info = file_data.get("dist-info-metadata")

upload_time: Optional[datetime.datetime]
if upload_time_data := file_data.get("upload-time"):
upload_time = datetime.datetime.fromisoformat(upload_time_data)
else:
upload_time = None

# The metadata info value may be a boolean, or a dict of hashes.
if isinstance(metadata_info, dict):
# The file exists, and hashes have been supplied
Expand All @@ -306,6 +318,7 @@ def from_json(
yanked_reason=yanked_reason,
hashes=hashes,
metadata_file_data=metadata_file_data,
upload_time=upload_time,
)

@classmethod
Expand Down