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
10 changes: 9 additions & 1 deletion importlib_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,14 +890,22 @@ def __init__(self, path: FastPath):
From the children in the path, calculate early all of the
children that appear to represent metadata (infos) or legacy
metadata (eggs).

Children are considered in reverse lexicographic order, so when
a path supplies more than one metadata directory for a
distribution, as an interrupted or unclean upgrade may leave
behind, the greater one is found first
(python/cpython#100666). Because the order is lexicographic on
the directory name and not on the parsed version, it only
approximates a preference for the newer version.
"""

base = os.path.basename(path.root).lower()
base_is_egg = base.endswith(".egg")
self.infos = FreezableDefaultDict(list)
self.eggs = FreezableDefaultDict(list)

for child in path.children():
for child in sorted(path.children(), key=str.lower, reverse=True):
low = child.lower()
if low.endswith((".dist-info", ".egg-info")):
# rpartition is faster than splitext and suitable for this purpose.
Expand Down
5 changes: 5 additions & 0 deletions newsfragments/+prefer-greater-version.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Where a path supplies more than one metadata directory for a distribution,
as an interrupted or unclean upgrade may leave behind, discovery now prefers
the greater one by reverse lexicographic ordering of the directory names, so
the stale metadata no longer shadows the version in use
(python/cpython#100666).
70 changes: 70 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,76 @@ def test_diagnose_reports_invalid(self):
assert 'nameless-1.0.dist-info (metadata without a name)' in report


class MultipleVersionsTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
"""
An unclean upgrade can leave more than one metadata directory for a
distribution in one path. Prefer the greater one.

Ref python/cpython#100666.
"""

# Deliberately not in version order; the preference follows the
# lexicographic ordering of the directory names, which only
# approximates the ordering of the versions they name.
versions = '1.12.1', '1.13.1', '1.9.0', '2.0.0'
expected = '2.0.0', '1.9.0', '1.13.1', '1.12.1'

@staticmethod
def make_pkg(name, version, files=None):
"""
Create metadata for a dist-info package at the indicated version.
"""
return {
f'{name}-{version}.dist-info': (
{'METADATA': f'Name: {name}\nVersion: {version}\n'}
if files is None
else files
),
}

def build_versions(self, *versions):
for ver in versions:
fixtures.build_files(self.make_pkg('foo', ver), self.site_dir)

def test_greater_version_preferred(self):
"""
Retrieving by name resolves to the greater version.
"""
self.build_versions(*self.versions)
assert version('foo') == self.expected[0]

def test_discovery_order(self):
"""
Preferring one version doesn't hide the others from discovery,
which reports them greatest first.
"""
self.build_versions(*self.versions)
found = tuple(dist.version for dist in Distribution.discover(name='foo'))
assert found == self.expected

def test_greater_version_preferred_in_eggs(self):
"""
Legacy egg-info metadata gets the same preference.
"""
fixtures.build_files(
{
f'foo-{ver}.egg-info': {'PKG-INFO': f'Name: foo\nVersion: {ver}\n'}
for ver in self.versions
},
self.site_dir,
)
assert version('foo') == self.expected[0]

def test_validity_preferred_to_version(self):
"""
A valid dist is still preferred to an invalid one of a greater
version, the condition that prompted the report.
"""
self.build_versions('1.12.1')
fixtures.build_files(self.make_pkg('foo', '2.0.0', files={}), self.site_dir)
assert version('foo') == '1.12.1'


class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
@staticmethod
def pkg_with_non_ascii_description(site_dir):
Expand Down
Loading