Skip to content

Commit

Permalink
Apply changes from importlib_metadata 7.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jaraco committed Mar 20, 2024
1 parent 8ad8898 commit 81b09e5
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 95 deletions.
155 changes: 123 additions & 32 deletions Lib/importlib/metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import re
import abc
Expand Down Expand Up @@ -26,7 +28,7 @@
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
from typing import Iterable, List, Mapping, Optional, Set, Union, cast
from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast

__all__ = [
'Distribution',
Expand Down Expand Up @@ -163,17 +165,17 @@ class EntryPoint:
value: str
group: str

dist: Optional['Distribution'] = None
dist: Optional[Distribution] = None

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

def load(self):
def load(self) -> Any:
"""Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise,
return the named object.
"""
match = self.pattern.match(self.value)
match = cast(Match, self.pattern.match(self.value))
module = import_module(match.group('module'))
attrs = filter(None, (match.group('attr') or '').split('.'))
return functools.reduce(getattr, attrs, module)
Expand Down Expand Up @@ -268,7 +270,7 @@ def __repr__(self):
"""
return '%s(%r)' % (self.__class__.__name__, tuple(self))

def select(self, **params):
def select(self, **params) -> EntryPoints:
"""
Select entry points from self that match the
given parameters (typically group and/or name).
Expand Down Expand Up @@ -304,19 +306,17 @@ def _from_text(text):
class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package"""

hash: Optional["FileHash"]
hash: Optional[FileHash]
size: int
dist: "Distribution"
dist: Distribution

def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
with self.locate().open(encoding=encoding) as stream:
return stream.read()
return self.locate().read_text(encoding=encoding)

def read_binary(self) -> bytes:
with self.locate().open('rb') as stream:
return stream.read()
return self.locate().read_bytes()

def locate(self) -> pathlib.Path:
def locate(self) -> SimplePath:
"""Return a path-like object for this path"""
return self.dist.locate_file(self)

Expand All @@ -330,6 +330,7 @@ def __repr__(self) -> str:


class DeprecatedNonAbstract:
# Required until Python 3.14
def __new__(cls, *args, **kwargs):
all_names = {
name for subclass in inspect.getmro(cls) for name in vars(subclass)
Expand All @@ -349,25 +350,48 @@ def __new__(cls, *args, **kwargs):


class Distribution(DeprecatedNonAbstract):
"""A Python distribution package."""
"""
An abstract Python distribution package.
Custom providers may derive from this class and define
the abstract methods to provide a concrete implementation
for their environment. Some providers may opt to override
the default implementation of some properties to bypass
the file-reading mechanism.
"""

@abc.abstractmethod
def read_text(self, filename) -> Optional[str]:
"""Attempt to load metadata file given by the name.
Python distribution metadata is organized by blobs of text
typically represented as "files" in the metadata directory
(e.g. package-1.0.dist-info). These files include things
like:
- METADATA: The distribution metadata including fields
like Name and Version and Description.
- entry_points.txt: A series of entry points as defined in
`the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_.
- RECORD: A record of files according to
`this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_.
A package may provide any set of files, including those
not listed here or none at all.
:param filename: The name of the file in the distribution info.
:return: The text if found, otherwise None.
"""

@abc.abstractmethod
def locate_file(self, path: Union[str, os.PathLike[str]]) -> pathlib.Path:
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
"""
Given a path to a file in this distribution, return a path
Given a path to a file in this distribution, return a SimplePath
to it.
"""

@classmethod
def from_name(cls, name: str) -> "Distribution":
def from_name(cls, name: str) -> Distribution:
"""Return the Distribution for the given package name.
:param name: The name of the distribution package to search for.
Expand All @@ -385,16 +409,18 @@ def from_name(cls, name: str) -> "Distribution":
raise PackageNotFoundError(name)

@classmethod
def discover(cls, **kwargs) -> Iterable["Distribution"]:
def discover(
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
) -> Iterable[Distribution]:
"""Return an iterable of Distribution objects for all packages.
Pass a ``context`` or pass keyword arguments for constructing
a context.
:context: A ``DistributionFinder.Context`` object.
:return: Iterable of Distribution objects for all packages.
:return: Iterable of Distribution objects for packages matching
the context.
"""
context = kwargs.pop('context', None)
if context and kwargs:
raise ValueError("cannot accept context and kwargs")
context = context or DistributionFinder.Context(**kwargs)
Expand All @@ -403,8 +429,8 @@ def discover(cls, **kwargs) -> Iterable["Distribution"]:
)

@staticmethod
def at(path: Union[str, os.PathLike[str]]) -> "Distribution":
"""Return a Distribution for the indicated metadata path
def at(path: str | os.PathLike[str]) -> Distribution:
"""Return a Distribution for the indicated metadata path.
:param path: a string or path-like object
:return: a concrete Distribution instance for the path
Expand All @@ -413,7 +439,7 @@ def at(path: Union[str, os.PathLike[str]]) -> "Distribution":

@staticmethod
def _discover_resolvers():
"""Search the meta_path for resolvers."""
"""Search the meta_path for resolvers (MetadataPathFinders)."""
declared = (
getattr(finder, 'find_distributions', None) for finder in sys.meta_path
)
Expand All @@ -424,7 +450,11 @@ def metadata(self) -> _meta.PackageMetadata:
"""Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of
metadata. See PEP 566 for details.
metadata per the
`Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_.
Custom providers may provide the METADATA file or override this
property.
"""
opt_text = (
self.read_text('METADATA')
Expand Down Expand Up @@ -454,6 +484,12 @@ def version(self) -> str:

@property
def entry_points(self) -> EntryPoints:
"""
Return EntryPoints for this distribution.
Custom providers may provide the ``entry_points.txt`` file
or override this property.
"""
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)

@property
Expand All @@ -466,6 +502,10 @@ def files(self) -> Optional[List[PackagePath]]:
(i.e. RECORD for dist-info, or installed-files.txt or
SOURCES.txt for egg-info) is missing.
Result may be empty if the metadata exists but is empty.
Custom providers are recommended to provide a "RECORD" file (in
``read_text``) or override this property to allow for callers to be
able to resolve filenames provided by the package.
"""

def make_file(name, hash=None, size_str=None):
Expand Down Expand Up @@ -497,7 +537,7 @@ def skip_missing_files(package_paths):

def _read_files_distinfo(self):
"""
Read the lines of RECORD
Read the lines of RECORD.
"""
text = self.read_text('RECORD')
return text and text.splitlines()
Expand Down Expand Up @@ -611,6 +651,9 @@ def _load_json(self, filename):
class DistributionFinder(MetaPathFinder):
"""
A MetaPathFinder capable of discovering installed distributions.
Custom providers should implement this interface in order to
supply metadata.
"""

class Context:
Expand All @@ -623,6 +666,17 @@ class Context:
Each DistributionFinder may expect any parameters
and should attempt to honor the canonical
parameters defined below when appropriate.
This mechanism gives a custom provider a means to
solicit additional details from the caller beyond
"name" and "path" when searching distributions.
For example, imagine a provider that exposes suites
of packages in either a "public" or "private" ``realm``.
A caller may wish to query only for distributions in
a particular realm and could call
``distributions(realm="private")`` to signal to the
custom provider to only include distributions from that
realm.
"""

name = None
Expand Down Expand Up @@ -658,11 +712,18 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]:

class FastPath:
"""
Micro-optimized class for searching a path for
children.
Micro-optimized class for searching a root for children.
Root is a path on the file system that may contain metadata
directories either as natural directories or within a zip file.
>>> FastPath('').children()
['...']
FastPath objects are cached and recycled for any given root.
>>> FastPath('foobar') is FastPath('foobar')
True
"""

@functools.lru_cache() # type: ignore
Expand Down Expand Up @@ -704,7 +765,19 @@ def lookup(self, mtime):


class Lookup:
"""
A micro-optimized class for searching a (fast) path for metadata.
"""

def __init__(self, path: FastPath):
"""
Calculate all of the children representing metadata.
From the children in the path, calculate early all of the
children that appear to represent metadata (infos) or legacy
metadata (eggs).
"""

base = os.path.basename(path.root).lower()
base_is_egg = base.endswith(".egg")
self.infos = FreezableDefaultDict(list)
Expand All @@ -725,7 +798,10 @@ def __init__(self, path: FastPath):
self.infos.freeze()
self.eggs.freeze()

def search(self, prepared):
def search(self, prepared: Prepared):
"""
Yield all infos and eggs matching the Prepared query.
"""
infos = (
self.infos[prepared.normalized]
if prepared
Expand All @@ -741,13 +817,28 @@ def search(self, prepared):

class Prepared:
"""
A prepared search for metadata on a possibly-named package.
A prepared search query for metadata on a possibly-named package.
Pre-calculates the normalization to prevent repeated operations.
>>> none = Prepared(None)
>>> none.normalized
>>> none.legacy_normalized
>>> bool(none)
False
>>> sample = Prepared('Sample__Pkg-name.foo')
>>> sample.normalized
'sample_pkg_name_foo'
>>> sample.legacy_normalized
'sample__pkg_name.foo'
>>> bool(sample)
True
"""

normalized = None
legacy_normalized = None

def __init__(self, name):
def __init__(self, name: Optional[str]):
self.name = name
if name is None:
return
Expand Down Expand Up @@ -777,7 +868,7 @@ class MetadataPathFinder(DistributionFinder):
@classmethod
def find_distributions(
cls, context=DistributionFinder.Context()
) -> Iterable["PathDistribution"]:
) -> Iterable[PathDistribution]:
"""
Find distributions.
Expand Down Expand Up @@ -810,7 +901,7 @@ def __init__(self, path: SimplePath) -> None:
"""
self._path = path

def read_text(self, filename: Union[str, os.PathLike[str]]) -> Optional[str]:
def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
with suppress(
FileNotFoundError,
IsADirectoryError,
Expand All @@ -824,7 +915,7 @@ def read_text(self, filename: Union[str, os.PathLike[str]]) -> Optional[str]:

read_text.__doc__ = Distribution.read_text.__doc__

def locate_file(self, path: Union[str, os.PathLike[str]]) -> pathlib.Path:
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
return self._path.parent / path

@property
Expand Down
Loading

0 comments on commit 81b09e5

Please sign in to comment.