Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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.
23 changes: 19 additions & 4 deletions src/pip/_internal/metadata/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,26 @@ 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 backend normalizes the leading tilde
Comment thread
2ykwang marked this conversation as resolved.
Outdated
# to a dash.
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