Skip to content
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

Introduce resumable downloads with --resume-retries #12991

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0617d7c
Add support to resume incomplete download
yichi-yang Jul 17, 2022
a091ca1
Better incomplete download error message
yichi-yang Jul 17, 2022
dbc6a64
Add support for --resume-retries option
gmargaritis Oct 4, 2024
7e9ea50
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Oct 9, 2024
889ac6b
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Oct 15, 2024
0b86d14
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Oct 30, 2024
2cfd8fe
Add initial_progress to _raw_progress_bar
gmargaritis Oct 30, 2024
1a9c23b
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Nov 15, 2024
64bd385
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Dec 11, 2024
d265d53
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Dec 18, 2024
d4e2da2
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Dec 22, 2024
0dbb4bd
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Jan 6, 2025
9b0bb5d
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Jan 10, 2025
68a7b05
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Jan 11, 2025
a6576b3
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Jan 26, 2025
eb6a8db
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Jan 31, 2025
c05050f
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Feb 7, 2025
7639a05
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Feb 14, 2025
9539136
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Feb 21, 2025
d7942ef
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Mar 6, 2025
288bb86
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Mar 16, 2025
c93c72a
Add download status in IncompleteDownloadError
gmargaritis Mar 16, 2025
e2f2fc8
Use copy() for headers in _http_get_download
gmargaritis Mar 16, 2025
92eeba5
Fix length of error message in IncompleteDownloadError
gmargaritis Mar 16, 2025
c09d4c4
Remove returning bytes_received in _attempt_resume
gmargaritis Mar 16, 2025
ea20b76
Use Last-Modified header for conditional range requests
gmargaritis Mar 16, 2025
66f68ca
Enforce simultaneous use of 'range_start' and 'if_range' in _http_get…
gmargaritis Mar 16, 2025
e4974da
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Mar 22, 2025
11f293b
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Mar 24, 2025
8937c73
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Mar 26, 2025
ff2ccd2
Simplify code
ichard26 Mar 30, 2025
2808134
Nicer incomplete download error
ichard26 Mar 30, 2025
c146e81
Rework user-facing messaging
ichard26 Mar 30, 2025
1f65cc4
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Apr 3, 2025
eed205a
More refactoring
ichard26 Apr 4, 2025
616cde5
Reword retries flag CLI help
ichard26 Apr 4, 2025
7b1d0a3
Revert "Enforce simultaneous use of 'range_start' and 'if_range' in _…
ichard26 Apr 4, 2025
42af9a8
Please show me mercy, black
ichard26 Apr 5, 2025
9cc70e1
Merge branch 'main' into introduce-resuming-downloads
gmargaritis Apr 9, 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
3 changes: 3 additions & 0 deletions news/12991.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add support to enable resuming incomplete downloads.

Control the number of retry attempts using the ``--resume-retries`` flag.
15 changes: 12 additions & 3 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,17 @@ class PipOption(Option):
dest="retries",
type="int",
default=5,
help="Maximum number of retries each connection should attempt "
"(default %default times).",
help="Maximum attempts to establish a new HTTP connection. (default: %default)",
)

resume_retries: Callable[..., Option] = partial(
Option,
"--resume-retries",
dest="resume_retries",
type="int",
default=0,
help="Maximum attempts to resume or restart an incomplete download. "
"(default: %default)",
)

timeout: Callable[..., Option] = partial(
Expand Down Expand Up @@ -1077,7 +1086,6 @@ def check_list_path_option(options: Values) -> None:
help=("Enable deprecated functionality, that will be removed in the future."),
)


##########
# groups #
##########
Expand Down Expand Up @@ -1110,6 +1118,7 @@ def check_list_path_option(options: Values) -> None:
no_python_version_warning,
use_new_feature,
use_deprecated_feature,
resume_retries,
],
}

Expand Down
19 changes: 15 additions & 4 deletions src/pip/_internal/cli/progress_bars.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def _rich_download_progress_bar(
*,
bar_type: str,
size: Optional[int],
initial_progress: Optional[int] = None,
) -> Generator[bytes, None, None]:
assert bar_type == "on", "This should only be used in the default mode."

Expand All @@ -54,6 +55,8 @@ def _rich_download_progress_bar(

progress = Progress(*columns, refresh_per_second=5)
task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
if initial_progress is not None:
progress.update(task_id, advance=initial_progress)
with progress:
for chunk in iterable:
yield chunk
Expand Down Expand Up @@ -86,12 +89,13 @@ def _raw_progress_bar(
iterable: Iterable[bytes],
*,
size: Optional[int],
initial_progress: Optional[int] = None,
) -> Generator[bytes, None, None]:
def write_progress(current: int, total: int) -> None:
sys.stdout.write(f"Progress {current} of {total}\n")
sys.stdout.flush()

current = 0
current = initial_progress or 0
total = size or 0
rate_limiter = RateLimiter(0.25)

Expand All @@ -105,18 +109,25 @@ def write_progress(current: int, total: int) -> None:


def get_download_progress_renderer(
*, bar_type: str, size: Optional[int] = None
*, bar_type: str, size: Optional[int] = None, initial_progress: Optional[int] = None
) -> ProgressRenderer[bytes]:
"""Get an object that can be used to render the download progress.

Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
return functools.partial(
_rich_download_progress_bar, bar_type=bar_type, size=size
_rich_download_progress_bar,
bar_type=bar_type,
size=size,
initial_progress=initial_progress,
)
elif bar_type == "raw":
return functools.partial(_raw_progress_bar, size=size)
return functools.partial(
_raw_progress_bar,
size=size,
initial_progress=initial_progress,
)
else:
return iter # no-op, when passed an iterator

Expand Down
1 change: 1 addition & 0 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def make_requirement_preparer(
lazy_wheel=lazy_wheel,
verbosity=verbosity,
legacy_resolver=legacy_resolver,
resume_retries=options.resume_retries,
)

@classmethod
Expand Down
33 changes: 33 additions & 0 deletions src/pip/_internal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from pip._vendor.requests.models import Request, Response

from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -809,6 +810,38 @@ def __init__(
)


class IncompleteDownloadError(DiagnosticPipError):
"""Raised when the downloader receives fewer bytes than advertised
in the Content-Length header."""

reference = "incomplete-download"

def __init__(
self, link: "Link", received: int, expected: int, *, retries: int
) -> None:
# Dodge circular import.
from pip._internal.utils.misc import format_size

download_status = f"{format_size(received)}/{format_size(expected)}"
if retries:
retry_status = f"after {retries} attempts "
hint = "Use --resume-retries to configure resume attempt limit."
else:
retry_status = ""
hint = "Consider using --resume-retries to enable download resumption."
message = Text(
f"Download failed {retry_status}because not enough bytes "
f"were received ({download_status})"
)

super().__init__(
message=message,
context=f"URL: {link.redacted_url}",
hint_stmt=hint,
note_stmt="This is an issue with network connectivity, not pip.",
)


class ResolutionTooDeepError(DiagnosticPipError):
"""Raised when the dependency resolver exceeds the maximum recursion depth."""

Expand Down
8 changes: 6 additions & 2 deletions src/pip/_internal/models/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,9 @@ def __str__(self) -> str:
else:
rp = ""
if self.comes_from:
return f"{redact_auth_from_url(self._url)} (from {self.comes_from}){rp}"
return f"{self.redacted_url} (from {self.comes_from}){rp}"
else:
return redact_auth_from_url(str(self._url))
return self.redacted_url

def __repr__(self) -> str:
return f"<Link {self}>"
Expand All @@ -404,6 +404,10 @@ def __lt__(self, other: Any) -> bool:
def url(self) -> str:
return self._url

@property
def redacted_url(self) -> str:
return redact_auth_from_url(self.url)

@property
def filename(self) -> str:
path = self.path.rstrip("/")
Expand Down
Loading