1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Provide a better error message when uninstalling packages without dist-info/RECORD

Fixes https://github.com/pypa/pip/issues/8954
This commit is contained in:
Miro Hrončok 2021-05-05 16:35:50 +02:00
parent e6414d6db6
commit f77649e841
3 changed files with 74 additions and 1 deletions

9
news/8954.feature.rst Normal file
View file

@ -0,0 +1,9 @@
When pip is asked to uninstall a project without the dist-info/RECORD file
it will no longer traceback with FileNotFoundError,
but it will provide a better error message instead, such as::
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'.
When dist-info/INSTALLER is present and contains some useful information, the info is included in the error message instead::
ERROR: Cannot uninstall foobar 0.1, RECORD file not found. Hint: The package was installed by rpm.

View file

@ -74,8 +74,27 @@ def uninstallation_paths(dist):
the .pyc and .pyo in the same directory.
UninstallPathSet.add() takes care of the __pycache__ .py[co].
If RECORD is not found, raises UninstallationError,
with possible information from the INSTALLER file.
https://packaging.python.org/specifications/recording-installed-packages/
"""
r = csv.reader(dist.get_metadata_lines('RECORD'))
try:
r = csv.reader(dist.get_metadata_lines('RECORD'))
except FileNotFoundError as missing_record_exception:
msg = 'Cannot uninstall {dist}, RECORD file not found.'.format(dist=dist)
try:
installer = next(dist.get_metadata_lines('INSTALLER'))
if not installer or installer == 'pip':
raise ValueError()
except (OSError, StopIteration, ValueError):
dep = '{}=={}'.format(dist.project_name, dist.version)
msg += (" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps {}'.".format(dep))
else:
msg += ' Hint: The package was installed by {}.'.format(installer)
raise UninstallationError(msg) from missing_record_exception
for row in r:
path = os.path.join(dist.location, row[0])
yield path

View file

@ -476,6 +476,51 @@ def test_uninstall_wheel(script, data):
assert_all_changes(result, result2, [])
@pytest.mark.parametrize('installer', [FileNotFoundError, IsADirectoryError,
'', os.linesep, b'\xc0\xff\xee', 'pip',
'MegaCorp Cloud Install-O-Matic'])
def test_uninstall_without_record_fails(script, data, installer):
"""
Test uninstalling a package installed without RECORD
"""
package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
result = script.pip('install', package, '--no-index')
dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info'
result.did_create(dist_info_folder)
# Remove RECORD
record_path = dist_info_folder / 'RECORD'
(script.base_path / record_path).unlink()
ignore_changes = [record_path]
# Populate, remove or otherwise break INSTALLER
installer_path = dist_info_folder / 'INSTALLER'
ignore_changes += [installer_path]
installer_path = script.base_path / installer_path
if installer in (FileNotFoundError, IsADirectoryError):
installer_path.unlink()
if installer is IsADirectoryError:
installer_path.mkdir()
else:
if isinstance(installer, bytes):
installer_path.write_bytes(installer)
else:
installer_path.write_text(installer + os.linesep)
result2 = script.pip('uninstall', 'simple.dist', '-y', expect_error=True)
expected_error_message = ('ERROR: Cannot uninstall simple.dist 0.1, '
'RECORD file not found.')
if not isinstance(installer, str) or not installer.strip() or installer == 'pip':
expected_error_message += (" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps "
"simple.dist==0.1'.")
elif installer:
expected_error_message += (' Hint: The package was installed by '
'{}.'.format(installer))
assert result2.stderr.rstrip() == expected_error_message
assert_all_changes(result.files_after, result2, ignore_changes)
@pytest.mark.skipif("sys.platform == 'win32'")
def test_uninstall_with_symlink(script, data, tmpdir):
"""