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:
parent
e6414d6db6
commit
f77649e841
9
news/8954.feature.rst
Normal file
9
news/8954.feature.rst
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue