Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions news/8954.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 20 additions & 1 deletion src/pip/_internal/req/req_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/functional/test_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down