Skip to content

Commit e7d6e54

Browse files
authored
Merge pull request #9669 from uranusjr/ignore-invalid-name-dist-info
Ignore dist-info directories with invalid name
2 parents 550270c + e4349ae commit e7d6e54

File tree

4 files changed

+68
-20
lines changed

4 files changed

+68
-20
lines changed

news/7269.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Ignore ``.dist-info`` directories if the stem is not a valid Python distribution
2+
name, so they don't show up in e.g. ``pip freeze``.

src/pip/_internal/metadata/base.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1+
import logging
2+
import re
13
from typing import Container, Iterator, List, Optional
24

35
from pip._vendor.packaging.version import _BaseVersion
46

57
from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here.
68

9+
logger = logging.getLogger(__name__)
10+
711

812
class BaseDistribution:
13+
@property
14+
def location(self):
15+
# type: () -> Optional[str]
16+
"""Where the distribution is loaded from.
17+
18+
A string value is not necessarily a filesystem path, since distributions
19+
can be loaded from other sources, e.g. arbitrary zip archives. ``None``
20+
means the distribution is created in-memory.
21+
"""
22+
raise NotImplementedError()
23+
924
@property
1025
def metadata_version(self):
1126
# type: () -> Optional[str]
@@ -61,10 +76,37 @@ def get_distribution(self, name):
6176
"""Given a requirement name, return the installed distributions."""
6277
raise NotImplementedError()
6378

79+
def _iter_distributions(self):
80+
# type: () -> Iterator[BaseDistribution]
81+
"""Iterate through installed distributions.
82+
83+
This function should be implemented by subclass, but never called
84+
directly. Use the public ``iter_distribution()`` instead, which
85+
implements additional logic to make sure the distributions are valid.
86+
"""
87+
raise NotImplementedError()
88+
6489
def iter_distributions(self):
6590
# type: () -> Iterator[BaseDistribution]
6691
"""Iterate through installed distributions."""
67-
raise NotImplementedError()
92+
for dist in self._iter_distributions():
93+
# Make sure the distribution actually comes from a valid Python
94+
# packaging distribution. Pip's AdjacentTempDirectory leaves folders
95+
# e.g. ``~atplotlib.dist-info`` if cleanup was interrupted. The
96+
# valid project name pattern is taken from PEP 508.
97+
project_name_valid = re.match(
98+
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$",
99+
dist.canonical_name,
100+
flags=re.IGNORECASE,
101+
)
102+
if not project_name_valid:
103+
logger.warning(
104+
"Ignoring invalid distribution %s (%s)",
105+
dist.canonical_name,
106+
dist.location,
107+
)
108+
continue
109+
yield dist
68110

69111
def iter_installed_distributions(
70112
self,

src/pip/_internal/metadata/pkg_resources.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def from_wheel(cls, path, name):
2424
dist = pkg_resources_distribution_for_wheel(zf, name, path)
2525
return cls(dist)
2626

27+
@property
28+
def location(self):
29+
# type: () -> Optional[str]
30+
return self._dist.location
31+
2732
@property
2833
def metadata_version(self):
2934
# type: () -> Optional[str]
@@ -115,7 +120,7 @@ def get_distribution(self, name):
115120
return None
116121
return self._search_distribution(name)
117122

118-
def iter_distributions(self):
123+
def _iter_distributions(self):
119124
# type: () -> Iterator[BaseDistribution]
120125
for dist in self._ws:
121126
yield Distribution(dist)

tests/functional/test_freeze.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from doctest import ELLIPSIS, OutputChecker
66

77
import pytest
8+
from pip._vendor.packaging.utils import canonicalize_name
9+
from pip._vendor.pkg_resources import safe_name
810

911
from tests.lib import (
1012
_create_test_package,
@@ -128,26 +130,23 @@ def fake_install(pkgname, dest):
128130
)
129131
for pkgname in valid_pkgnames + invalid_pkgnames:
130132
fake_install(pkgname, script.site_packages_path)
133+
131134
result = script.pip('freeze', expect_stderr=True)
132-
for pkgname in valid_pkgnames:
133-
_check_output(
134-
result.stdout,
135-
'...{}==1.0...'.format(pkgname.replace('_', '-'))
136-
)
137-
for pkgname in invalid_pkgnames:
138-
# Check that the full distribution repr is present.
139-
dist_repr = '{} 1.0 ('.format(pkgname.replace('_', '-'))
140-
expected = (
141-
'...Could not generate requirement for '
142-
'distribution {}...'.format(dist_repr)
143-
)
144-
_check_output(result.stderr, expected)
145135

146-
# Also check that the parse error details occur at least once.
147-
# We only need to find one occurrence to know that exception details
148-
# are logged.
149-
expected = '...site-packages): Parse error at "...'
150-
_check_output(result.stderr, expected)
136+
# Check all valid names are in the output.
137+
output_lines = {line.strip() for line in result.stdout.splitlines()}
138+
for name in valid_pkgnames:
139+
assert f"{safe_name(name)}==1.0" in output_lines
140+
141+
# Check all invalid names are excluded from the output.
142+
canonical_invalid_names = {canonicalize_name(n) for n in invalid_pkgnames}
143+
for line in output_lines:
144+
output_name, _, _ = line.partition("=")
145+
assert canonicalize_name(output_name) not in canonical_invalid_names
146+
147+
# The invalid names should be logged.
148+
for name in canonical_invalid_names:
149+
assert f"Ignoring invalid distribution {name} (" in result.stderr
151150

152151

153152
@pytest.mark.git

0 commit comments

Comments
 (0)