Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bpo-42382: In importlib.metadata, EntryPoint objects now expose dist #23758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 31, 2020
Merged
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
11 changes: 3 additions & 8 deletions Doc/library/importlib.metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ Every distribution includes some metadata, which you can extract using the

>>> wheel_metadata = metadata('wheel') # doctest: +SKIP

The keys of the returned data structure [#f1]_ name the metadata keywords, and
their values are returned unparsed from the distribution metadata::
The keys of the returned data structure, a ``PackageMetadata``,
name the metadata keywords, and
the values are returned unparsed from the distribution metadata::

>>> wheel_metadata['Requires-Python'] # doctest: +SKIP
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
Expand Down Expand Up @@ -259,9 +260,3 @@ a custom finder, return instances of this derived ``Distribution`` in the


.. rubric:: Footnotes

.. [#f1] Technically, the returned distribution metadata object is an
:class:`email.message.EmailMessage`
instance, but this is an implementation detail, and not part of the
stable API. You should only use dictionary-like methods and syntax
to access the metadata contents.
185 changes: 119 additions & 66 deletions Lib/importlib/metadata.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import io
import os
import re
import abc
Expand All @@ -18,6 +17,7 @@
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
from typing import Any, List, Optional, Protocol, TypeVar, Union


__all__ = [
Expand All @@ -31,7 +31,7 @@
'metadata',
'requires',
'version',
]
]


class PackageNotFoundError(ModuleNotFoundError):
Expand All @@ -43,7 +43,7 @@ def __str__(self):

@property
def name(self):
name, = self.args
(name,) = self.args
return name


Expand All @@ -60,7 +60,7 @@ class EntryPoint(
r'(?P<module>[\w.]+)\s*'
r'(:\s*(?P<attr>[\w.]+))?\s*'
r'(?P<extras>\[.*\])?\s*$'
)
)
"""
A regular expression describing the syntax for an entry point,
which might look like:
Expand All @@ -77,6 +77,8 @@ class EntryPoint(
following the attr, and following any extras.
"""

dist: Optional['Distribution'] = None

def load(self):
"""Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise,
Expand Down Expand Up @@ -104,23 +106,27 @@ def extras(self):

@classmethod
def _from_config(cls, config):
return [
return (
cls(name, value, group)
for group in config.sections()
for name, value in config.items(group)
]
)

@classmethod
def _from_text(cls, text):
config = ConfigParser(delimiters='=')
# case sensitive: https://stackoverflow.com/q/1611799/812183
config.optionxform = str
try:
config.read_string(text)
except AttributeError: # pragma: nocover
# Python 2 has no read_string
config.readfp(io.StringIO(text))
return EntryPoint._from_config(config)
config.read_string(text)
return cls._from_config(config)

@classmethod
def _from_text_for(cls, text, dist):
return (ep._for(dist) for ep in cls._from_text(text))

def _for(self, dist):
self.dist = dist
return self

def __iter__(self):
"""
Expand All @@ -132,7 +138,7 @@ def __reduce__(self):
return (
self.__class__,
(self.name, self.value, self.group),
)
)


class PackagePath(pathlib.PurePosixPath):
Expand All @@ -159,6 +165,25 @@ def __repr__(self):
return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)


_T = TypeVar("_T")


class PackageMetadata(Protocol):
def __len__(self) -> int:
... # pragma: no cover

def __contains__(self, item: str) -> bool:
... # pragma: no cover

def __getitem__(self, key: str) -> str:
... # pragma: no cover

def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
"""
Return all values associated with a possibly multi-valued key.
"""


class Distribution:
"""A Python distribution package."""

Expand Down Expand Up @@ -210,9 +235,8 @@ def discover(cls, **kwargs):
raise ValueError("cannot accept context and kwargs")
context = context or DistributionFinder.Context(**kwargs)
return itertools.chain.from_iterable(
resolver(context)
for resolver in cls._discover_resolvers()
)
resolver(context) for resolver in cls._discover_resolvers()
)

@staticmethod
def at(path):
Expand All @@ -227,24 +251,24 @@ def at(path):
def _discover_resolvers():
"""Search the meta_path for resolvers."""
declared = (
getattr(finder, 'find_distributions', None)
for finder in sys.meta_path
)
getattr(finder, 'find_distributions', None) for finder in sys.meta_path
)
return filter(None, declared)

@classmethod
def _local(cls, root='.'):
from pep517 import build, meta

system = build.compat_system(root)
builder = functools.partial(
meta.build,
source_dir=root,
system=system,
)
)
return PathDistribution(zipfile.Path(meta.build_as_zip(builder)))

@property
def metadata(self):
def metadata(self) -> PackageMetadata:
"""Return the parsed metadata for this Distribution.

The returned object will have keys that name the various bits of
Expand All @@ -257,17 +281,22 @@ def metadata(self):
# effect is to just end up using the PathDistribution's self._path
# (which points to the egg-info file) attribute unchanged.
or self.read_text('')
)
)
return email.message_from_string(text)

@property
def name(self):
"""Return the 'Name' metadata for the distribution package."""
return self.metadata['Name']

@property
def version(self):
"""Return the 'Version' metadata for the distribution package."""
return self.metadata['Version']

@property
def entry_points(self):
return EntryPoint._from_text(self.read_text('entry_points.txt'))
return list(EntryPoint._from_text_for(self.read_text('entry_points.txt'), self))

@property
def files(self):
Expand Down Expand Up @@ -324,9 +353,10 @@ def _deps_from_requires_text(cls, source):
section_pairs = cls._read_sections(source.splitlines())
sections = {
section: list(map(operator.itemgetter('line'), results))
for section, results in
itertools.groupby(section_pairs, operator.itemgetter('section'))
}
for section, results in itertools.groupby(
section_pairs, operator.itemgetter('section')
)
}
return cls._convert_egg_info_reqs_to_simple_reqs(sections)

@staticmethod
Expand All @@ -350,6 +380,7 @@ def _convert_egg_info_reqs_to_simple_reqs(sections):
requirement. This method converts the former to the
latter. See _test_deps_from_requires_text for an example.
"""

def make_condition(name):
return name and 'extra == "{name}"'.format(name=name)

Expand Down Expand Up @@ -438,48 +469,69 @@ def zip_children(self):
names = zip_path.root.namelist()
self.joinpath = zip_path.joinpath

return dict.fromkeys(
child.split(posixpath.sep, 1)[0]
for child in names
)

def is_egg(self, search):
base = self.base
return (
base == search.versionless_egg_name
or base.startswith(search.prefix)
and base.endswith('.egg'))
return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)

def search(self, name):
for child in self.children():
n_low = child.lower()
if (n_low in name.exact_matches
or n_low.startswith(name.prefix)
and n_low.endswith(name.suffixes)
# legacy case:
or self.is_egg(name) and n_low == 'egg-info'):
yield self.joinpath(child)
return (
self.joinpath(child)
for child in self.children()
if name.matches(child, self.base)
)


class Prepared:
"""
A prepared search for metadata on a possibly-named package.
"""
normalized = ''
prefix = ''

normalized = None
suffixes = '.dist-info', '.egg-info'
exact_matches = [''][:0]
versionless_egg_name = ''

def __init__(self, name):
self.name = name
if name is None:
return
self.normalized = name.lower().replace('-', '_')
self.prefix = self.normalized + '-'
self.exact_matches = [
self.normalized + suffix for suffix in self.suffixes]
self.versionless_egg_name = self.normalized + '.egg'
self.normalized = self.normalize(name)
self.exact_matches = [self.normalized + suffix for suffix in self.suffixes]

@staticmethod
def normalize(name):
"""
PEP 503 normalization plus dashes as underscores.
"""
return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')

@staticmethod
def legacy_normalize(name):
"""
Normalize the package name as found in the convention in
older packaging tools versions and specs.
"""
return name.lower().replace('-', '_')

def matches(self, cand, base):
low = cand.lower()
pre, ext = os.path.splitext(low)
name, sep, rest = pre.partition('-')
return (
low in self.exact_matches
or ext in self.suffixes
and (not self.normalized or name.replace('.', '_') == self.normalized)
# legacy case:
or self.is_egg(base)
and low == 'egg-info'
)

def is_egg(self, base):
normalized = self.legacy_normalize(self.name or '')
prefix = normalized + '-' if normalized else ''
versionless_egg_name = normalized + '.egg' if self.name else ''
return (
base == versionless_egg_name
or base.startswith(prefix)
and base.endswith('.egg')
)


class MetadataPathFinder(DistributionFinder):
Expand All @@ -500,9 +552,8 @@ def find_distributions(cls, context=DistributionFinder.Context()):
def _search_paths(cls, name, paths):
"""Find metadata directories in paths heuristically."""
return itertools.chain.from_iterable(
path.search(Prepared(name))
for path in map(FastPath, paths)
)
path.search(Prepared(name)) for path in map(FastPath, paths)
)


class PathDistribution(Distribution):
Expand All @@ -515,9 +566,15 @@ def __init__(self, path):
self._path = path

def read_text(self, filename):
with suppress(FileNotFoundError, IsADirectoryError, KeyError,
NotADirectoryError, PermissionError):
with suppress(
FileNotFoundError,
IsADirectoryError,
KeyError,
NotADirectoryError,
PermissionError,
):
return self._path.joinpath(filename).read_text(encoding='utf-8')

read_text.__doc__ = Distribution.read_text.__doc__

def locate_file(self, path):
Expand All @@ -541,11 +598,11 @@ def distributions(**kwargs):
return Distribution.discover(**kwargs)


def metadata(distribution_name):
def metadata(distribution_name) -> PackageMetadata:
"""Get the metadata for the named package.

:param distribution_name: The name of the distribution package to query.
:return: An email.Message containing the parsed metadata.
:return: A PackageMetadata containing the parsed metadata.
"""
return Distribution.from_name(distribution_name).metadata

Expand All @@ -565,15 +622,11 @@ def entry_points():

:return: EntryPoint objects for all installed packages.
"""
eps = itertools.chain.from_iterable(
dist.entry_points for dist in distributions())
eps = itertools.chain.from_iterable(dist.entry_points for dist in distributions())
by_group = operator.attrgetter('group')
ordered = sorted(eps, key=by_group)
grouped = itertools.groupby(ordered, by_group)
return {
group: tuple(eps)
for group, eps in grouped
}
return {group: tuple(eps) for group, eps in grouped}


def files(distribution_name):
Expand Down
Loading