-
-
Notifications
You must be signed in to change notification settings - Fork 31k
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
bpo-43428: Sync with importlib_metadata 3.7. #24782
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from itertools import filterfalse | ||
|
||
|
||
def unique_everseen(iterable, key=None): | ||
"List unique elements, preserving order. Remember all elements ever seen." | ||
# unique_everseen('AAAABBBCCDAABBB') --> A B C D | ||
# unique_everseen('ABBCcAD', str.lower) --> A B C D | ||
seen = set() | ||
seen_add = seen.add | ||
if key is None: | ||
for element in filterfalse(seen.__contains__, iterable): | ||
seen_add(element) | ||
yield element | ||
else: | ||
for element in iterable: | ||
k = key(element) | ||
if k not in seen: | ||
seen_add(k) | ||
yield element |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,20 +4,24 @@ | |
import csv | ||
import sys | ||
import email | ||
import inspect | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. inspect is used only for flake8 related utility and moving it to a local import can save little time. I might be benchmarking import time wrong with importtime so please verify my results.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in python/importlib_metadata@15312a7. |
||
import pathlib | ||
import zipfile | ||
import operator | ||
import warnings | ||
import functools | ||
import itertools | ||
import posixpath | ||
import collections | ||
import collections.abc | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in python/importlib_metadata@4ae9584. |
||
|
||
from ._itertools import unique_everseen | ||
|
||
from configparser import ConfigParser | ||
from contextlib import suppress | ||
from importlib import import_module | ||
from importlib.abc import MetaPathFinder | ||
from itertools import starmap | ||
from typing import Any, List, Optional, Protocol, TypeVar, Union | ||
from typing import Any, List, Mapping, Optional, Protocol, TypeVar, Union | ||
|
||
|
||
__all__ = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be updated with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added to python/importlib_metadata@7bdeaa4, to be included in a future sync. |
||
|
@@ -120,18 +124,19 @@ def _from_text(cls, text): | |
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): | ||
""" | ||
Supply iter so one may construct dicts of EntryPoints easily. | ||
Supply iter so one may construct dicts of EntryPoints by name. | ||
""" | ||
msg = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The deprecation might be noted in docs since this was a valid usage before. The deprecation message is slightly unclear to me since
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This deprecation is unrelated to the introduction of
Previously, an Oh, I see. It's unclear from a user's perspective what Additionally, in python/importlib_metadata@bf777ae, I've added documentation for |
||
"Construction of dict of EntryPoints is deprecated in " | ||
"favor of EntryPoints." | ||
) | ||
warnings.warn(msg, DeprecationWarning) | ||
return iter((self.name, self)) | ||
|
||
def __reduce__(self): | ||
|
@@ -140,6 +145,143 @@ def __reduce__(self): | |
(self.name, self.value, self.group), | ||
) | ||
|
||
def matches(self, **params): | ||
attrs = (getattr(self, param) for param in params) | ||
return all(map(operator.eq, params.values(), attrs)) | ||
|
||
|
||
class EntryPoints(tuple): | ||
""" | ||
An immutable collection of selectable EntryPoint objects. | ||
""" | ||
|
||
__slots__ = () | ||
|
||
def __getitem__(self, name): # -> EntryPoint: | ||
try: | ||
return next(iter(self.select(name=name))) | ||
except StopIteration: | ||
raise KeyError(name) | ||
|
||
def select(self, **params): | ||
return EntryPoints(ep for ep in self if ep.matches(**params)) | ||
|
||
@property | ||
def names(self): | ||
return set(ep.name for ep in self) | ||
|
||
@property | ||
def groups(self): | ||
""" | ||
For coverage while SelectableGroups is present. | ||
>>> EntryPoints().groups | ||
set() | ||
""" | ||
return set(ep.group for ep in self) | ||
|
||
@classmethod | ||
def _from_text_for(cls, text, dist): | ||
return cls(ep._for(dist) for ep in EntryPoint._from_text(text)) | ||
|
||
|
||
def flake8_bypass(func): | ||
is_flake8 = any('flake8' in str(frame.filename) for frame in inspect.stack()[:5]) | ||
return func if not is_flake8 else lambda: None | ||
|
||
|
||
class Deprecated: | ||
jaraco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Compatibility add-in for mapping to indicate that | ||
mapping behavior is deprecated. | ||
|
||
>>> recwarn = getfixture('recwarn') | ||
>>> class DeprecatedDict(Deprecated, dict): pass | ||
>>> dd = DeprecatedDict(foo='bar') | ||
>>> dd.get('baz', None) | ||
>>> dd['foo'] | ||
'bar' | ||
>>> list(dd) | ||
['foo'] | ||
>>> list(dd.keys()) | ||
['foo'] | ||
>>> 'foo' in dd | ||
True | ||
>>> list(dd.values()) | ||
['bar'] | ||
>>> len(recwarn) | ||
1 | ||
""" | ||
|
||
_warn = functools.partial( | ||
warnings.warn, | ||
"SelectableGroups dict interface is deprecated. Use select.", | ||
DeprecationWarning, | ||
stacklevel=2, | ||
) | ||
|
||
def __getitem__(self, name): | ||
self._warn() | ||
return super().__getitem__(name) | ||
|
||
def get(self, name, default=None): | ||
flake8_bypass(self._warn)() | ||
return super().get(name, default) | ||
|
||
def __iter__(self): | ||
self._warn() | ||
return super().__iter__() | ||
|
||
def __contains__(self, *args): | ||
self._warn() | ||
return super().__contains__(*args) | ||
|
||
def keys(self): | ||
self._warn() | ||
return super().keys() | ||
|
||
def values(self): | ||
self._warn() | ||
return super().values() | ||
|
||
|
||
class SelectableGroups(dict): | ||
""" | ||
A backward- and forward-compatible result from | ||
entry_points that fully implements the dict interface. | ||
""" | ||
|
||
@classmethod | ||
def load(cls, eps): | ||
by_group = operator.attrgetter('group') | ||
ordered = sorted(eps, key=by_group) | ||
grouped = itertools.groupby(ordered, by_group) | ||
return cls((group, EntryPoints(eps)) for group, eps in grouped) | ||
|
||
@property | ||
def _all(self): | ||
""" | ||
Reconstruct a list of all entrypoints from the groups. | ||
""" | ||
return EntryPoints(itertools.chain.from_iterable(self.values())) | ||
|
||
@property | ||
def groups(self): | ||
return self._all.groups | ||
|
||
@property | ||
def names(self): | ||
""" | ||
for coverage: | ||
>>> SelectableGroups().names | ||
set() | ||
""" | ||
return self._all.names | ||
|
||
def select(self, **params): | ||
if not params: | ||
return self | ||
return self._all.select(**params) | ||
|
||
|
||
class PackagePath(pathlib.PurePosixPath): | ||
"""A reference to a path in a package""" | ||
|
@@ -296,7 +438,7 @@ def version(self): | |
|
||
@property | ||
def entry_points(self): | ||
return list(EntryPoint._from_text_for(self.read_text('entry_points.txt'), self)) | ||
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) | ||
|
||
@property | ||
def files(self): | ||
|
@@ -485,15 +627,22 @@ class Prepared: | |
""" | ||
|
||
normalized = None | ||
suffixes = '.dist-info', '.egg-info' | ||
suffixes = 'dist-info', 'egg-info' | ||
exact_matches = [''][:0] | ||
egg_prefix = '' | ||
versionless_egg_name = '' | ||
|
||
def __init__(self, name): | ||
self.name = name | ||
if name is None: | ||
return | ||
self.normalized = self.normalize(name) | ||
self.exact_matches = [self.normalized + suffix for suffix in self.suffixes] | ||
self.exact_matches = [ | ||
self.normalized + '.' + suffix for suffix in self.suffixes | ||
] | ||
legacy_normalized = self.legacy_normalize(self.name) | ||
self.egg_prefix = legacy_normalized + '-' | ||
self.versionless_egg_name = legacy_normalized + '.egg' | ||
|
||
@staticmethod | ||
def normalize(name): | ||
|
@@ -512,8 +661,9 @@ def legacy_normalize(name): | |
|
||
def matches(self, cand, base): | ||
low = cand.lower() | ||
pre, ext = os.path.splitext(low) | ||
name, sep, rest = pre.partition('-') | ||
# rpartition is faster than splitext and suitable for this purpose. | ||
pre, _, ext = low.rpartition('.') | ||
name, _, rest = pre.partition('-') | ||
return ( | ||
low in self.exact_matches | ||
or ext in self.suffixes | ||
|
@@ -524,12 +674,9 @@ def matches(self, cand, base): | |
) | ||
|
||
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) | ||
base == self.versionless_egg_name | ||
or base.startswith(self.egg_prefix) | ||
and base.endswith('.egg') | ||
) | ||
|
||
|
@@ -551,8 +698,9 @@ def find_distributions(cls, context=DistributionFinder.Context()): | |
@classmethod | ||
def _search_paths(cls, name, paths): | ||
"""Find metadata directories in paths heuristically.""" | ||
prepared = Prepared(name) | ||
return itertools.chain.from_iterable( | ||
path.search(Prepared(name)) for path in map(FastPath, paths) | ||
path.search(prepared) for path in map(FastPath, paths) | ||
) | ||
|
||
|
||
|
@@ -617,16 +765,28 @@ def version(distribution_name): | |
return distribution(distribution_name).version | ||
|
||
|
||
def entry_points(): | ||
def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This represents a change in signature for the public function that selectors can be passed directly like below which is not possible before the update. There are no separate docs for
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In python/importlib_metadata@bb24370, I expanded the docs to address this concern. It should be much clearer now what versions support the selection interface. Please take a look and let me know if that doesn't fully address the concern. |
||
"""Return EntryPoint objects for all installed packages. | ||
|
||
:return: EntryPoint objects for all installed packages. | ||
Pass selection parameters (group or name) to filter the | ||
result to entry points matching those properties (see | ||
EntryPoints.select()). | ||
|
||
For compatibility, returns ``SelectableGroups`` object unless | ||
selection parameters are supplied. In the future, this function | ||
will return ``EntryPoints`` instead of ``SelectableGroups`` | ||
even when no selection parameters are supplied. | ||
|
||
For maximum future compatibility, pass selection parameters | ||
or invoke ``.select`` with parameters on the result. | ||
|
||
:return: EntryPoints or SelectableGroups for all installed packages. | ||
""" | ||
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} | ||
unique = functools.partial(unique_everseen, key=operator.attrgetter('name')) | ||
eps = itertools.chain.from_iterable( | ||
dist.entry_points for dist in unique(distributions()) | ||
) | ||
return SelectableGroups.load(eps).select(**params) | ||
|
||
|
||
def files(distribution_name): | ||
|
@@ -646,3 +806,19 @@ def requires(distribution_name): | |
packaging.requirement.Requirement. | ||
""" | ||
return distribution(distribution_name).requires | ||
|
||
|
||
def packages_distributions() -> Mapping[str, List[str]]: | ||
""" | ||
Return a mapping of top-level packages to their | ||
distributions. | ||
|
||
>>> pkgs = packages_distributions() | ||
>>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values()) | ||
True | ||
""" | ||
pkg_to_dist = collections.defaultdict(list) | ||
for dist in distributions(): | ||
for pkg in (dist.read_text('top_level.txt') or '').split(): | ||
pkg_to_dist[pkg].append(dist.metadata['Name']) | ||
return dict(pkg_to_dist) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be good to document this as newly added in Python 3.10
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 690775a.