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
70 changes: 51 additions & 19 deletions Lib/importlib/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import sys
import textwrap
import types
from collections.abc import Iterable, Mapping
from collections.abc import Callable, Iterable, Mapping
from contextlib import suppress
from importlib import import_module
from importlib.abc import MetaPathFinder
Expand All @@ -35,15 +35,14 @@
from ._functools import method_cache, noop, pass_none, passthrough
from ._itertools import always_iterable, bucket, unique_everseen
from ._meta import PackageMetadata, SimplePath
from ._typing import md_none

__all__ = [
'Distribution',
'DistributionFinder',
'MetadataNotFound',
'PackageMetadata',
'PackageNotFoundError',
'PackagePath',
'MetadataNotFound',
'SimplePath',
'distribution',
'distributions',
Expand Down Expand Up @@ -175,9 +174,11 @@ class EntryPoint:
...
ValueError: ('Invalid object reference...invalid-name...

The same thing happens on construction.
Construction never raises; the value is validated only when
accessed or loaded.

>>> EntryPoint(name=None, group=None, value='invalid-name')
>>> ep = EntryPoint(name=None, group=None, value='invalid-name')
>>> ep.load()
Traceback (most recent call last):
...
ValueError: ('Invalid object reference...invalid-name...
Expand Down Expand Up @@ -213,7 +214,6 @@ class EntryPoint:

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

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

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


def _identify_dist(
accessor: Callable[[Distribution], str],
) -> Callable[[Distribution], str]:
"""
Decorate a required metadata accessor to identify the distribution
in the error raised when its field is absent.
"""

@functools.wraps(accessor)
def wrapper(self: Distribution) -> str:
try:
return accessor(self)
except KeyError as exc:
(field,) = exc.args
raise KeyError(f'Metadata field {field} not present in {self}') from None

return wrapper


class Distribution(metaclass=abc.ABCMeta):
"""
An abstract Python distribution package.
Expand Down Expand Up @@ -556,6 +575,7 @@ def _ensure_metadata_present(self, text: str | None) -> str:
raise MetadataNotFound('No package metadata was found.')

@property
@_identify_dist
def name(self) -> str:
"""Return the 'Name' metadata for the distribution package."""
return self.metadata['Name']
Expand All @@ -566,6 +586,7 @@ def _normalized_name(self):
return Prepared.normalize(self.name)

@property
@_identify_dist
def version(self) -> str:
"""Return the 'Version' metadata for the distribution package."""
return self.metadata['Version']
Expand Down Expand Up @@ -707,7 +728,7 @@ def make_condition(name):

def quoted_marker(section):
section = section or ''
extra, sep, markers = section.partition(':')
extra, _sep, markers = section.partition(':')
if extra and markers:
markers = f'({markers})'
conditions = list(filter(None, [markers, make_condition(extra)]))
Expand Down Expand Up @@ -832,7 +853,7 @@ class FastPath:
"""

@_clear_after_fork # type: ignore[misc]
@functools.lru_cache()
@functools.lru_cache
def __new__(cls, root):
return super().__new__(cls)

Expand Down Expand Up @@ -885,14 +906,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 Expand Up @@ -958,15 +987,8 @@ def __init__(self, name: str | None):
def normalize(name):
"""
PEP 503 normalization plus dashes as underscores.

Specifically avoids ``re.sub`` as prescribed for performance
benefits (see python/cpython#143658).
"""
value = name.lower().replace("-", "_").replace(".", "_")
# Condense repeats
while "__" in value:
value = value.replace("__", "_")
return value
return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')

@staticmethod
def legacy_normalize(name):
Expand Down Expand Up @@ -1017,6 +1039,16 @@ def __init__(self, path: SimplePath) -> None:
"""
self._path = path

def __str__(self) -> str:
"""
Render as the metadata path, identifying the distribution
even when its metadata cannot name it.

>>> str(Distribution.at('foo-1.0.dist-info'))
'foo-1.0.dist-info'
"""
return str(self._path)

def read_text(self, filename: str | os.PathLike[str]) -> str | None:
with suppress(
FileNotFoundError,
Expand Down Expand Up @@ -1060,7 +1092,7 @@ def _name_from_stem(stem):
filename, ext = os.path.splitext(stem)
if ext not in ('.dist-info', '.egg-info'):
return
name, sep, rest = filename.partition('-')
name, _sep, _rest = filename.partition('-')
return name


Expand Down
2 changes: 1 addition & 1 deletion Lib/importlib/metadata/_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class Message(email.message.Message):
<BLANKLINE>
"""

multiple_use_keys = set(
multiple_use_keys = frozenset(
map(
FoldedCase,
[
Expand Down
4 changes: 2 additions & 2 deletions Lib/importlib/metadata/_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ class SimplePath(Protocol):
"""

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

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

@property
Expand Down
2 changes: 1 addition & 1 deletion Lib/importlib/metadata/_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,5 @@ def index(self, sub):
return self.lower().index(sub.lower())

def split(self, splitter=' ', maxsplit=0):
pattern = re.compile(re.escape(splitter), re.I)
pattern = re.compile(re.escape(splitter), re.IGNORECASE)
return pattern.split(self, maxsplit)
15 changes: 0 additions & 15 deletions Lib/importlib/metadata/_typing.py

This file was deleted.

20 changes: 18 additions & 2 deletions Lib/importlib/metadata/diagnose.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import sys

from . import Distribution
from . import Distribution, MetadataNotFound


def describe(dist: Distribution) -> str:
"""
Describe the distribution by name, or by location when the
distribution is too invalid to name itself.

>>> describe(Distribution.at('missing-1.0.dist-info'))
'missing-1.0.dist-info (no metadata)'
"""
try:
return dist.name
except MetadataNotFound:
return f'{dist} (no metadata)'
except KeyError:
return f'{dist} (metadata without a name)'


def inspect(path):
Expand All @@ -9,7 +25,7 @@ def inspect(path):
if not dists:
return
print("Found", len(dists), "packages:", end=' ')
print(', '.join(dist.name for dist in dists))
print(', '.join(map(describe, dists)))


def run():
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_importlib/metadata/_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable

if TYPE_CHECKING:
from typing_extensions import Self
from typing import Self


class Symlink(str):
Expand Down
Loading
Loading