Skip to content

Commit 3c1d181

Browse files
authored
Merge pull request #9949 from hroncok/error_msg_missing_record
Provide a better error message when uninstalling packages without dist-info/RECORD
2 parents 57be6a7 + f77649e commit 3c1d181

File tree

3 files changed

+74
-1
lines changed

3 files changed

+74
-1
lines changed

news/8954.feature.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
When pip is asked to uninstall a project without the dist-info/RECORD file
2+
it will no longer traceback with FileNotFoundError,
3+
but it will provide a better error message instead, such as::
4+
5+
ERROR: Cannot uninstall foobar 0.1, RECORD file not found. You might be able to recover from this via: 'pip install --force-reinstall --no-deps foobar==0.1'.
6+
7+
When dist-info/INSTALLER is present and contains some useful information, the info is included in the error message instead::
8+
9+
ERROR: Cannot uninstall foobar 0.1, RECORD file not found. Hint: The package was installed by rpm.

src/pip/_internal/req/req_uninstall.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,27 @@ def uninstallation_paths(dist):
7474
the .pyc and .pyo in the same directory.
7575
7676
UninstallPathSet.add() takes care of the __pycache__ .py[co].
77+
78+
If RECORD is not found, raises UninstallationError,
79+
with possible information from the INSTALLER file.
80+
81+
https://packaging.python.org/specifications/recording-installed-packages/
7782
"""
78-
r = csv.reader(dist.get_metadata_lines('RECORD'))
83+
try:
84+
r = csv.reader(dist.get_metadata_lines('RECORD'))
85+
except FileNotFoundError as missing_record_exception:
86+
msg = 'Cannot uninstall {dist}, RECORD file not found.'.format(dist=dist)
87+
try:
88+
installer = next(dist.get_metadata_lines('INSTALLER'))
89+
if not installer or installer == 'pip':
90+
raise ValueError()
91+
except (OSError, StopIteration, ValueError):
92+
dep = '{}=={}'.format(dist.project_name, dist.version)
93+
msg += (" You might be able to recover from this via: "
94+
"'pip install --force-reinstall --no-deps {}'.".format(dep))
95+
else:
96+
msg += ' Hint: The package was installed by {}.'.format(installer)
97+
raise UninstallationError(msg) from missing_record_exception
7998
for row in r:
8099
path = os.path.join(dist.location, row[0])
81100
yield path

tests/functional/test_uninstall.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,51 @@ def test_uninstall_wheel(script, data):
476476
assert_all_changes(result, result2, [])
477477

478478

479+
@pytest.mark.parametrize('installer', [FileNotFoundError, IsADirectoryError,
480+
'', os.linesep, b'\xc0\xff\xee', 'pip',
481+
'MegaCorp Cloud Install-O-Matic'])
482+
def test_uninstall_without_record_fails(script, data, installer):
483+
"""
484+
Test uninstalling a package installed without RECORD
485+
"""
486+
package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
487+
result = script.pip('install', package, '--no-index')
488+
dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info'
489+
result.did_create(dist_info_folder)
490+
491+
# Remove RECORD
492+
record_path = dist_info_folder / 'RECORD'
493+
(script.base_path / record_path).unlink()
494+
ignore_changes = [record_path]
495+
496+
# Populate, remove or otherwise break INSTALLER
497+
installer_path = dist_info_folder / 'INSTALLER'
498+
ignore_changes += [installer_path]
499+
installer_path = script.base_path / installer_path
500+
if installer in (FileNotFoundError, IsADirectoryError):
501+
installer_path.unlink()
502+
if installer is IsADirectoryError:
503+
installer_path.mkdir()
504+
else:
505+
if isinstance(installer, bytes):
506+
installer_path.write_bytes(installer)
507+
else:
508+
installer_path.write_text(installer + os.linesep)
509+
510+
result2 = script.pip('uninstall', 'simple.dist', '-y', expect_error=True)
511+
expected_error_message = ('ERROR: Cannot uninstall simple.dist 0.1, '
512+
'RECORD file not found.')
513+
if not isinstance(installer, str) or not installer.strip() or installer == 'pip':
514+
expected_error_message += (" You might be able to recover from this via: "
515+
"'pip install --force-reinstall --no-deps "
516+
"simple.dist==0.1'.")
517+
elif installer:
518+
expected_error_message += (' Hint: The package was installed by '
519+
'{}.'.format(installer))
520+
assert result2.stderr.rstrip() == expected_error_message
521+
assert_all_changes(result.files_after, result2, ignore_changes)
522+
523+
479524
@pytest.mark.skipif("sys.platform == 'win32'")
480525
def test_uninstall_with_symlink(script, data, tmpdir):
481526
"""

0 commit comments

Comments
 (0)