Skip to content

Commit 8106aa0

Browse files
jaracoclaude
andcommitted
gh-160386: Apply changes from importlib_metadata 9.1
Syncs importlib_metadata 9.0.1 and 9.1.0 into importlib.metadata. Discovery now considers a path's children in reverse lexicographic order, so the greater of several metadata directories for one distribution wins and stale metadata from an unclean upgrade no longer shadows the version in use (gh-100666). Distribution.name and .version identify the distribution in the KeyError raised for a missing required field, and PathDistribution renders as its metadata path. EntryPoint validates its value on use rather than on construction. diagnose describes a distribution it cannot name instead of crashing. SimplePath.joinpath and __truediv__ take other positional-only. Prepared.normalize returns to the re.sub implementation, the hand-rolled version from gh-143658 having been reverted upstream. Removes _typing.py and its import. Its only export, md_none, lost its last call site in gh-143387, but the import survived that merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent d98f2cb commit 8106aa0

11 files changed

Lines changed: 253 additions & 73 deletions

File tree

‎Lib/importlib/metadata/__init__.py‎

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import sys
2323
import textwrap
2424
import types
25-
from collections.abc import Iterable, Mapping
25+
from collections.abc import Callable, Iterable, Mapping
2626
from contextlib import suppress
2727
from importlib import import_module
2828
from importlib.abc import MetaPathFinder
@@ -35,15 +35,14 @@
3535
from ._functools import method_cache, noop, pass_none, passthrough
3636
from ._itertools import always_iterable, bucket, unique_everseen
3737
from ._meta import PackageMetadata, SimplePath
38-
from ._typing import md_none
3938

4039
__all__ = [
4140
'Distribution',
4241
'DistributionFinder',
42+
'MetadataNotFound',
4343
'PackageMetadata',
4444
'PackageNotFoundError',
4545
'PackagePath',
46-
'MetadataNotFound',
4746
'SimplePath',
4847
'distribution',
4948
'distributions',
@@ -175,9 +174,11 @@ class EntryPoint:
175174
...
176175
ValueError: ('Invalid object reference...invalid-name...
177176
178-
The same thing happens on construction.
177+
Construction never raises; the value is validated only when
178+
accessed or loaded.
179179
180-
>>> EntryPoint(name=None, group=None, value='invalid-name')
180+
>>> ep = EntryPoint(name=None, group=None, value='invalid-name')
181+
>>> ep.load()
181182
Traceback (most recent call last):
182183
...
183184
ValueError: ('Invalid object reference...invalid-name...
@@ -213,7 +214,6 @@ class EntryPoint:
213214

214215
def __init__(self, name: str, value: str, group: str) -> None:
215216
vars(self).update(name=name, value=value, group=group)
216-
self.module
217217

218218
def load(self) -> Any:
219219
"""Load the entry point from its definition. If only a module
@@ -335,7 +335,7 @@ def __repr__(self):
335335
Repr with classname and tuple constructor to
336336
signal that we deviate from regular tuple behavior.
337337
"""
338-
return '%s(%r)' % (self.__class__.__name__, tuple(self))
338+
return f'{self.__class__.__name__}({tuple(self)!r})'
339339

340340
def select(self, **params) -> EntryPoints:
341341
"""
@@ -396,6 +396,25 @@ def __repr__(self) -> str:
396396
return f'<FileHash mode: {self.mode} value: {self.value}>'
397397

398398

399+
def _identify_dist(
400+
accessor: Callable[[Distribution], str],
401+
) -> Callable[[Distribution], str]:
402+
"""
403+
Decorate a required metadata accessor to identify the distribution
404+
in the error raised when its field is absent.
405+
"""
406+
407+
@functools.wraps(accessor)
408+
def wrapper(self: Distribution) -> str:
409+
try:
410+
return accessor(self)
411+
except KeyError as exc:
412+
(field,) = exc.args
413+
raise KeyError(f'Metadata field {field} not present in {self}') from None
414+
415+
return wrapper
416+
417+
399418
class Distribution(metaclass=abc.ABCMeta):
400419
"""
401420
An abstract Python distribution package.
@@ -556,6 +575,7 @@ def _ensure_metadata_present(self, text: str | None) -> str:
556575
raise MetadataNotFound('No package metadata was found.')
557576

558577
@property
578+
@_identify_dist
559579
def name(self) -> str:
560580
"""Return the 'Name' metadata for the distribution package."""
561581
return self.metadata['Name']
@@ -566,6 +586,7 @@ def _normalized_name(self):
566586
return Prepared.normalize(self.name)
567587

568588
@property
589+
@_identify_dist
569590
def version(self) -> str:
570591
"""Return the 'Version' metadata for the distribution package."""
571592
return self.metadata['Version']
@@ -707,7 +728,7 @@ def make_condition(name):
707728

708729
def quoted_marker(section):
709730
section = section or ''
710-
extra, sep, markers = section.partition(':')
731+
extra, _sep, markers = section.partition(':')
711732
if extra and markers:
712733
markers = f'({markers})'
713734
conditions = list(filter(None, [markers, make_condition(extra)]))
@@ -832,7 +853,7 @@ class FastPath:
832853
"""
833854

834855
@_clear_after_fork # type: ignore[misc]
835-
@functools.lru_cache()
856+
@functools.lru_cache
836857
def __new__(cls, root):
837858
return super().__new__(cls)
838859

@@ -885,14 +906,22 @@ def __init__(self, path: FastPath):
885906
From the children in the path, calculate early all of the
886907
children that appear to represent metadata (infos) or legacy
887908
metadata (eggs).
909+
910+
Children are considered in reverse lexicographic order, so when
911+
a path supplies more than one metadata directory for a
912+
distribution, as an interrupted or unclean upgrade may leave
913+
behind, the greater one is found first
914+
(python/cpython#100666). Because the order is lexicographic on
915+
the directory name and not on the parsed version, it only
916+
approximates a preference for the newer version.
888917
"""
889918

890919
base = os.path.basename(path.root).lower()
891920
base_is_egg = base.endswith(".egg")
892921
self.infos = FreezableDefaultDict(list)
893922
self.eggs = FreezableDefaultDict(list)
894923

895-
for child in path.children():
924+
for child in sorted(path.children(), key=str.lower, reverse=True):
896925
low = child.lower()
897926
if low.endswith((".dist-info", ".egg-info")):
898927
# rpartition is faster than splitext and suitable for this purpose.
@@ -958,15 +987,8 @@ def __init__(self, name: str | None):
958987
def normalize(name):
959988
"""
960989
PEP 503 normalization plus dashes as underscores.
961-
962-
Specifically avoids ``re.sub`` as prescribed for performance
963-
benefits (see python/cpython#143658).
964990
"""
965-
value = name.lower().replace("-", "_").replace(".", "_")
966-
# Condense repeats
967-
while "__" in value:
968-
value = value.replace("__", "_")
969-
return value
991+
return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
970992

971993
@staticmethod
972994
def legacy_normalize(name):
@@ -1017,6 +1039,16 @@ def __init__(self, path: SimplePath) -> None:
10171039
"""
10181040
self._path = path
10191041

1042+
def __str__(self) -> str:
1043+
"""
1044+
Render as the metadata path, identifying the distribution
1045+
even when its metadata cannot name it.
1046+
1047+
>>> str(Distribution.at('foo-1.0.dist-info'))
1048+
'foo-1.0.dist-info'
1049+
"""
1050+
return str(self._path)
1051+
10201052
def read_text(self, filename: str | os.PathLike[str]) -> str | None:
10211053
with suppress(
10221054
FileNotFoundError,
@@ -1060,7 +1092,7 @@ def _name_from_stem(stem):
10601092
filename, ext = os.path.splitext(stem)
10611093
if ext not in ('.dist-info', '.egg-info'):
10621094
return
1063-
name, sep, rest = filename.partition('-')
1095+
name, _sep, _rest = filename.partition('-')
10641096
return name
10651097

10661098

‎Lib/importlib/metadata/_adapters.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Message(email.message.Message):
5454
<BLANKLINE>
5555
"""
5656

57-
multiple_use_keys = set(
57+
multiple_use_keys = frozenset(
5858
map(
5959
FoldedCase,
6060
[

‎Lib/importlib/metadata/_meta.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ class SimplePath(Protocol):
5454
"""
5555

5656
def joinpath(
57-
self, other: str | os.PathLike[str]
57+
self, other: str | os.PathLike[str], /
5858
) -> SimplePath: ... # pragma: no cover
5959

6060
def __truediv__(
61-
self, other: str | os.PathLike[str]
61+
self, other: str | os.PathLike[str], /
6262
) -> SimplePath: ... # pragma: no cover
6363

6464
@property

‎Lib/importlib/metadata/_text.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,5 @@ def index(self, sub):
9595
return self.lower().index(sub.lower())
9696

9797
def split(self, splitter=' ', maxsplit=0):
98-
pattern = re.compile(re.escape(splitter), re.I)
98+
pattern = re.compile(re.escape(splitter), re.IGNORECASE)
9999
return pattern.split(self, maxsplit)

‎Lib/importlib/metadata/_typing.py‎

Lines changed: 0 additions & 15 deletions
This file was deleted.

‎Lib/importlib/metadata/diagnose.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import sys
22

3-
from . import Distribution
3+
from . import Distribution, MetadataNotFound
4+
5+
6+
def describe(dist: Distribution) -> str:
7+
"""
8+
Describe the distribution by name, or by location when the
9+
distribution is too invalid to name itself.
10+
11+
>>> describe(Distribution.at('missing-1.0.dist-info'))
12+
'missing-1.0.dist-info (no metadata)'
13+
"""
14+
try:
15+
return dist.name
16+
except MetadataNotFound:
17+
return f'{dist} (no metadata)'
18+
except KeyError:
19+
return f'{dist} (metadata without a name)'
420

521

622
def inspect(path):
@@ -9,7 +25,7 @@ def inspect(path):
925
if not dists:
1026
return
1127
print("Found", len(dists), "packages:", end=' ')
12-
print(', '.join(dist.name for dist in dists))
28+
print(', '.join(map(describe, dists)))
1329

1430

1531
def run():

‎Lib/test/test_importlib/metadata/_path.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
99

1010
if TYPE_CHECKING:
11-
from typing_extensions import Self
11+
from typing import Self
1212

1313

1414
class Symlink(str):

0 commit comments

Comments
 (0)