Skip to content

Commit d2f2da9

Browse files
authored
Merge pull request #12463 from ichard26/errors/uninstall
Migrate uninstallation errors to diagnostic error format
2 parents e8e592a + 3b14deb commit d2f2da9

File tree

5 files changed

+57
-36
lines changed

5 files changed

+57
-36
lines changed

news/10421.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reword and improve presentation of uninstallation errors.

src/pip/_internal/cli/base_command.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
InstallationError,
2929
NetworkConnectionError,
3030
PreviousBuildDirError,
31-
UninstallationError,
3231
)
3332
from pip._internal.utils.filesystem import check_path_owner
3433
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
@@ -192,7 +191,6 @@ def exc_logging_wrapper(*args: Any) -> int:
192191
return PREVIOUS_BUILD_DIR_ERROR
193192
except (
194193
InstallationError,
195-
UninstallationError,
196194
BadCommand,
197195
NetworkConnectionError,
198196
) as exc:

src/pip/_internal/exceptions.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,6 @@ class InstallationError(PipError):
184184
"""General exception during installation"""
185185

186186

187-
class UninstallationError(PipError):
188-
"""General exception during uninstallation"""
189-
190-
191187
class MissingPyProjectBuildRequires(DiagnosticPipError):
192188
"""Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
193189

@@ -726,3 +722,45 @@ def from_config(
726722
exc_info = logger.isEnabledFor(VERBOSE)
727723
logger.warning("Failed to read %s", config, exc_info=exc_info)
728724
return cls(None)
725+
726+
727+
class UninstallMissingRecord(DiagnosticPipError):
728+
reference = "uninstall-no-record-file"
729+
730+
def __init__(self, *, distribution: "BaseDistribution") -> None:
731+
installer = distribution.installer
732+
if not installer or installer == "pip":
733+
dep = f"{distribution.raw_name}=={distribution.version}"
734+
hint = Text.assemble(
735+
"You might be able to recover from this via: ",
736+
(f"pip install --force-reinstall --no-deps {dep}", "green"),
737+
)
738+
else:
739+
hint = Text(
740+
f"The package was installed by {installer}. "
741+
"You should check if it can uninstall the package."
742+
)
743+
744+
super().__init__(
745+
message=Text(f"Cannot uninstall {distribution}"),
746+
context=(
747+
"The package's contents are unknown: "
748+
f"no RECORD file was found for {distribution.raw_name}."
749+
),
750+
hint_stmt=hint,
751+
)
752+
753+
754+
class LegacyDistutilsInstall(DiagnosticPipError):
755+
reference = "uninstall-distutils-installed-package"
756+
757+
def __init__(self, *, distribution: "BaseDistribution") -> None:
758+
super().__init__(
759+
message=Text(f"Cannot uninstall {distribution}"),
760+
context=(
761+
"It is a distutils installed project and thus we cannot accurately "
762+
"determine which files belong to it which would lead to only a partial "
763+
"uninstall."
764+
),
765+
hint_stmt=None,
766+
)

src/pip/_internal/req/req_uninstall.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from importlib.util import cache_from_source
66
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
77

8-
from pip._internal.exceptions import UninstallationError
8+
from pip._internal.exceptions import LegacyDistutilsInstall, UninstallMissingRecord
99
from pip._internal.locations import get_bin_prefix, get_bin_user
1010
from pip._internal.metadata import BaseDistribution
1111
from pip._internal.utils.compat import WINDOWS
@@ -61,7 +61,7 @@ def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
6161
6262
UninstallPathSet.add() takes care of the __pycache__ .py[co].
6363
64-
If RECORD is not found, raises UninstallationError,
64+
If RECORD is not found, raises an error,
6565
with possible information from the INSTALLER file.
6666
6767
https://packaging.python.org/specifications/recording-installed-packages/
@@ -71,17 +71,7 @@ def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
7171

7272
entries = dist.iter_declared_entries()
7373
if entries is None:
74-
msg = f"Cannot uninstall {dist}, RECORD file not found."
75-
installer = dist.installer
76-
if not installer or installer == "pip":
77-
dep = f"{dist.raw_name}=={dist.version}"
78-
msg += (
79-
" You might be able to recover from this via: "
80-
f"'pip install --force-reinstall --no-deps {dep}'."
81-
)
82-
else:
83-
msg += f" Hint: The package was installed by {installer}."
84-
raise UninstallationError(msg)
74+
raise UninstallMissingRecord(distribution=dist)
8575

8676
for entry in entries:
8777
path = os.path.join(location, entry)
@@ -509,11 +499,7 @@ def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
509499
paths_to_remove.add(f"{path}.pyo")
510500

511501
elif dist.installed_by_distutils:
512-
raise UninstallationError(
513-
f"Cannot uninstall {dist.raw_name!r}. It is a distutils installed "
514-
"project and thus we cannot accurately determine which files belong "
515-
"to it which would lead to only a partial uninstall."
516-
)
502+
raise LegacyDistutilsInstall(distribution=dist)
517503

518504
elif dist.installed_as_egg:
519505
# package installed by easy_install

tests/functional/test_uninstall.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ def test_basic_uninstall_distutils(script: PipTestEnvironment) -> None:
6565
result = script.pip(
6666
"uninstall", "distutils_install", "-y", expect_stderr=True, expect_error=True
6767
)
68+
assert "Cannot uninstall distutils-install 0.1" in result.stderr
6869
assert (
69-
"Cannot uninstall 'distutils-install'. It is a distutils installed "
70-
"project and thus we cannot accurately determine which files belong "
71-
"to it which would lead to only a partial uninstall."
70+
"It is a distutils installed project and thus we cannot accurately determine "
71+
"which files belong to it which would lead to only a partial uninstall."
7272
) in result.stderr
7373

7474

@@ -590,18 +590,16 @@ def test_uninstall_without_record_fails(
590590
installer_path.write_text(installer + os.linesep)
591591

592592
result2 = script.pip("uninstall", "simple.dist", "-y", expect_error=True)
593-
expected_error_message = (
594-
"ERROR: Cannot uninstall simple.dist 0.1, RECORD file not found."
595-
)
593+
assert "Cannot uninstall simple.dist 0.1" in result2.stderr
594+
assert "no RECORD file was found for simple.dist" in result2.stderr
596595
if not isinstance(installer, str) or not installer.strip() or installer == "pip":
597-
expected_error_message += (
598-
" You might be able to recover from this via: "
599-
"'pip install --force-reinstall --no-deps "
600-
"simple.dist==0.1'."
596+
hint = (
597+
"You might be able to recover from this via: "
598+
"pip install --force-reinstall --no-deps simple.dist==0.1"
601599
)
602600
elif installer:
603-
expected_error_message += f" Hint: The package was installed by {installer}."
604-
assert result2.stderr.rstrip() == expected_error_message
601+
hint = f"The package was installed by {installer}."
602+
assert f"hint: {hint}" in result2.stderr
605603
assert_all_changes(result.files_after, result2, ignore_changes)
606604

607605

0 commit comments

Comments
 (0)