From a091ca1a05ad4d227db886f05aadecb7a92532c8 Mon Sep 17 00:00:00 2001 From: Yichi Yang Date: Sun, 17 Jul 2022 12:06:21 -0700 Subject: [PATCH] Better incomplete download error message --- src/pip/_internal/exceptions.py | 33 +++++++++++++++++++++++++++ src/pip/_internal/network/download.py | 19 ++++----------- tests/unit/test_network_download.py | 3 ++- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 8ceb818a35d..b74c9fe8f7d 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -775,3 +775,36 @@ def __init__(self, *, distribution: "BaseDistribution") -> None: ), hint_stmt=None, ) + +class IncompleteDownloadError(DiagnosticPipError): + """Raised when the downloader receives fewer bytes than advertised + in the Content-Length header.""" + + reference = "incomplete-download-error" + + def __init__( + self, link: str, resume_incomplete: bool, resume_attempts: int + ) -> None: + if resume_incomplete: + message = ( + "Download failed after {} attempts because not enough bytes are" + " received. The incomplete file has been cleaned up." + ).format(resume_attempts) + hint = "Use --incomplete-download-retries to configure resume retry limit." + else: + message = ( + "Download failed because not enough bytes are received." + " The incomplete file has been cleaned up." + ) + hint = ( + "Use --incomplete-downloads=resume to make pip retry failed download." + ) + + super().__init__( + message=message, + context="File: {}\n" + "Resume failed download: {}\n" + "Resume retry limit: {}".format(link, resume_incomplete, resume_attempts), + hint_stmt=hint, + note_stmt="This is an issue with network connectivity, not pip.", + ) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index f237f8522ac..c1acca9c262 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -11,7 +11,7 @@ from pip._vendor.requests.models import Response from pip._internal.cli.progress_bars import get_download_progress_renderer -from pip._internal.exceptions import NetworkConnectionError +from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError from pip._internal.models.index import PyPI from pip._internal.models.link import Link from pip._internal.network.cache import is_from_cache @@ -229,21 +229,10 @@ def __call__(self, link: Link, location: str) -> Tuple[str, str]: content_file.write(chunk) if total_length is not None and bytes_received < total_length: - if self._resume_incomplete: - logger.critical( - "Failed to download %s after %d resumption attempts.", - link, - self._resume_attempts, - ) - else: - logger.critical( - "Failed to download %s." - " Set --incomplete-downloads=resume to automatically" - "resume incomplete download.", - link, - ) os.remove(filepath) - raise RuntimeError("Incomplete download") + raise IncompleteDownloadError( + str(link), self._resume_incomplete, self._resume_attempts + ) content_type = resp.headers.get("Content-Type", "") return filepath, content_type diff --git a/tests/unit/test_network_download.py b/tests/unit/test_network_download.py index 12845c277cd..1d7f6405f36 100644 --- a/tests/unit/test_network_download.py +++ b/tests/unit/test_network_download.py @@ -6,6 +6,7 @@ import pytest +from pip._internal.exceptions import IncompleteDownloadError from pip._internal.models.link import Link from pip._internal.network.download import ( Downloader, @@ -349,7 +350,7 @@ def test_downloader( if expected_bytes is None: remove = MagicMock(return_value=None) with patch("os.remove", remove): - with pytest.raises(RuntimeError): + with pytest.raises(IncompleteDownloadError): downloader(link, str(tmpdir)) # Make sure the incomplete file is removed remove.assert_called_once()