Skip to content
Open
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
1 change: 1 addition & 0 deletions news/11849.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Warn that ``~``-prefixed leftover directories are safe to delete.
25 changes: 21 additions & 4 deletions src/pip/_internal/metadata/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,11 +622,28 @@ def iter_all_distributions(self) -> Iterator[BaseDistribution]:
flags=re.IGNORECASE,
)
if not project_name_valid:
logger.warning(
"Ignoring invalid distribution %s (%s)",
dist.canonical_name,
dist.location,
# Check the directory name rather than the distribution name,
# since the pkg_resources (default below 3.11) backend normalizes
# the leading tilde to a dash.
# TODO: use dist.canonical_name for this check once
# pkg_resources support is dropped (#13317).
info_location = dist.info_location
leftover_name = (
pathlib.Path(info_location).name if info_location else ""
)
if leftover_name.startswith("~"):
logger.warning(
"Ignoring incompletely removed distribution %s (%s); "
"'~'-prefixed leftover directories are safe to delete",
leftover_name,
dist.location,
)
else:
logger.warning(
"Ignoring invalid distribution %s (%s)",
dist.canonical_name,
dist.location,
)
continue
yield dist

Expand Down
16 changes: 16 additions & 0 deletions tests/functional/test_freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ def fake_install(pkgname: str, dest: str) -> None:
for pkgname in valid_pkgnames + invalid_pkgnames:
fake_install(pkgname, os.fspath(script.site_packages_path))

# Simulate a leftover of an interrupted uninstallation or upgrade, renamed
# to a tilde-prefixed name by pip's AdjacentTempDirectory.
leftover_dir = os.path.join(
os.fspath(script.site_packages_path), "~eftover-1.0.dist-info"
)
os.mkdir(leftover_dir)
with open(os.path.join(leftover_dir, "METADATA"), "w") as metadata_file:
metadata_file.write("Metadata-Version: 1.0\nName: leftover\nVersion: 1.0\n")

result = script.pip("freeze", expect_stderr=True)

# Check all valid names are in the output.
Expand All @@ -185,11 +194,18 @@ def fake_install(pkgname: str, dest: str) -> None:
for line in output_lines:
output_name, _, _ = line.partition("=")
assert canonicalize_name(output_name) not in canonical_invalid_names
assert "eftover" not in result.stdout

# The invalid names should be logged.
for name in canonical_invalid_names:
assert f"Ignoring invalid distribution {name} (" in result.stderr

# The tilde-prefixed leftover should be reported as incompletely removed.
assert (
"Ignoring incompletely removed distribution ~eftover-1.0.dist-info ("
in result.stderr
)


@pytest.mark.git
def test_freeze_editable_not_vcs(script: PipTestEnvironment) -> None:
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/metadata/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,29 @@ def test_trailing_slash_directory_metadata(
dist = get_directory_distribution(path)
assert dist.raw_name == dist.canonical_name == "foo"
assert dist.location == str(tmp_path)


def test_iter_all_distributions_warns_on_incomplete_removal(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
valid_info = tmp_path / "valid-1.0.dist-info"
valid_info.mkdir()
valid_info.joinpath("METADATA").write_text("Metadata-Version: 1.0\nName: valid\n")

leftover_info = tmp_path / "~eftover-1.0.dist-info"
leftover_info.mkdir()
leftover_info.joinpath("METADATA").write_text(
"Metadata-Version: 1.0\nName: leftover\n"
)

env = get_environment([os.fspath(tmp_path)])
with caplog.at_level(logging.WARNING):
dists = list(env.iter_all_distributions())

assert [dist.canonical_name for dist in dists] == ["valid"]
assert len(caplog.records) == 1
message = caplog.records[0].getMessage()
assert message.startswith(
"Ignoring incompletely removed distribution ~eftover-1.0.dist-info ("
)
assert "safe to delete" in message
25 changes: 25 additions & 0 deletions tests/unit/metadata/test_metadata_pkg_resources.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import email.message
import itertools
import logging
import os
from pathlib import Path
from typing import cast
from unittest import mock

Expand Down Expand Up @@ -124,3 +127,25 @@ def test_wheel_metadata_throws_on_bad_unicode() -> None:
with pytest.raises(UnsupportedWheel) as e:
metadata.get_metadata("METADATA")
assert "METADATA" in str(e.value)


def test_iter_all_distributions_warns_on_incomplete_removal(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
leftover_info = tmp_path / "~eftover-1.0.dist-info"
leftover_info.mkdir()
leftover_info.joinpath("METADATA").write_text(
"Metadata-Version: 1.0\nName: leftover\n"
)

env = Environment.from_paths([os.fspath(tmp_path)])
with caplog.at_level(logging.WARNING):
dists = list(env.iter_all_distributions())

assert dists == []
assert len(caplog.records) == 1
message = caplog.records[0].getMessage()
assert message.startswith(
"Ignoring incompletely removed distribution ~eftover-1.0.dist-info ("
)
assert "safe to delete" in message
Loading