Skip to content

Commit f787075

Browse files
committed
Merge branch 'main' into fscache
2 parents 0da7579 + 1e2381f commit f787075

File tree

6 files changed

+310
-32
lines changed

6 files changed

+310
-32
lines changed

CHANGES.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,56 @@
1+
v3.7.2
2+
======
3+
4+
* Cleaned up cruft in entry_points docstring.
5+
6+
v3.7.1
7+
======
8+
9+
* Internal refactoring to facilitate ``entry_points() -> dict``
10+
deprecation.
11+
12+
v3.7.0
13+
======
14+
15+
* #131: Added ``packages_distributions`` to conveniently
16+
resolve a top-level package or module to its distribution(s).
17+
18+
v3.6.0
19+
======
20+
21+
* #284: Introduces new ``EntryPoints`` object, a tuple of
22+
``EntryPoint`` objects but with convenience properties for
23+
selecting and inspecting the results:
24+
25+
- ``.select()`` accepts ``group`` or ``name`` keyword
26+
parameters and returns a new ``EntryPoints`` tuple
27+
with only those that match the selection.
28+
- ``.groups`` property presents all of the group names.
29+
- ``.names`` property presents the names of the entry points.
30+
- Item access (e.g. ``eps[name]``) retrieves a single
31+
entry point by name.
32+
33+
``entry_points`` now accepts "selection parameters",
34+
same as ``EntryPoint.select()``.
35+
36+
``entry_points()`` now provides a future-compatible
37+
``SelectableGroups`` object that supplies the above interface
38+
but remains a dict for compatibility.
39+
40+
In the future, ``entry_points()`` will return an
41+
``EntryPoints`` object, but provide for backward
42+
compatibility with a deprecated ``__getitem__``
43+
accessor by group and a ``get()`` method.
44+
45+
If passing selection parameters to ``entry_points``, the
46+
future behavior is invoked and an ``EntryPoints`` is the
47+
result.
48+
49+
Construction of entry points using
50+
``dict([EntryPoint, ...])`` is now deprecated and raises
51+
an appropriate DeprecationWarning and will be removed in
52+
a future version.
53+
154
v3.5.0
255
======
356

docs/using.rst

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,20 @@ This package provides the following functionality via its public API.
6767
Entry points
6868
------------
6969

70-
The ``entry_points()`` function returns a dictionary of all entry points,
71-
keyed by group. Entry points are represented by ``EntryPoint`` instances;
70+
The ``entry_points()`` function returns a collection of entry points.
71+
Entry points are represented by ``EntryPoint`` instances;
7272
each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and
7373
a ``.load()`` method to resolve the value. There are also ``.module``,
7474
``.attr``, and ``.extras`` attributes for getting the components of the
7575
``.value`` attribute::
7676

7777
>>> eps = entry_points()
78-
>>> list(eps)
78+
>>> sorted(eps.groups)
7979
['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation']
80-
>>> scripts = eps['console_scripts']
81-
>>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0]
80+
>>> scripts = eps.select(group='console_scripts')
81+
>>> 'wheel' in scripts.names
82+
True
83+
>>> wheel = scripts['wheel']
8284
>>> wheel
8385
EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')
8486
>>> wheel.module
@@ -180,6 +182,17 @@ function::
180182
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
181183

182184

185+
Package distributions
186+
---------------------
187+
188+
A convience method to resolve the distribution or
189+
distributions (in the case of a namespace package) for top-level
190+
Python packages or modules::
191+
192+
>>> packages_distributions()
193+
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}
194+
195+
183196
Distributions
184197
=============
185198

importlib_metadata/__init__.py

Lines changed: 178 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import sys
66
import zipp
77
import email
8+
import inspect
89
import pathlib
910
import operator
11+
import warnings
1012
import functools
1113
import itertools
1214
import posixpath
13-
import collections
15+
import collections.abc
1416

1517
from ._compat import (
1618
NullFinder,
@@ -26,7 +28,7 @@
2628
from importlib import import_module
2729
from importlib.abc import MetaPathFinder
2830
from itertools import starmap
29-
from typing import Any, List, Optional, TypeVar, Union
31+
from typing import Any, List, Mapping, Optional, TypeVar, Union
3032

3133

3234
__all__ = [
@@ -130,22 +132,19 @@ def _from_text(cls, text):
130132
config.read_string(text)
131133
return cls._from_config(config)
132134

133-
@classmethod
134-
def _from_text_for(cls, text, dist):
135-
return (ep._for(dist) for ep in cls._from_text(text))
136-
137135
def _for(self, dist):
138136
self.dist = dist
139137
return self
140138

141139
def __iter__(self):
142140
"""
143141
Supply iter so one may construct dicts of EntryPoints by name.
144-
145-
>>> eps = [EntryPoint('a', 'b', 'c'), EntryPoint('d', 'e', 'f')]
146-
>>> dict(eps)['a']
147-
EntryPoint(name='a', value='b', group='c')
148142
"""
143+
msg = (
144+
"Construction of dict of EntryPoints is deprecated in "
145+
"favor of EntryPoints."
146+
)
147+
warnings.warn(msg, DeprecationWarning)
149148
return iter((self.name, self))
150149

151150
def __reduce__(self):
@@ -154,6 +153,143 @@ def __reduce__(self):
154153
(self.name, self.value, self.group),
155154
)
156155

156+
def matches(self, **params):
157+
attrs = (getattr(self, param) for param in params)
158+
return all(map(operator.eq, params.values(), attrs))
159+
160+
161+
class EntryPoints(tuple):
162+
"""
163+
An immutable collection of selectable EntryPoint objects.
164+
"""
165+
166+
__slots__ = ()
167+
168+
def __getitem__(self, name): # -> EntryPoint:
169+
try:
170+
return next(iter(self.select(name=name)))
171+
except StopIteration:
172+
raise KeyError(name)
173+
174+
def select(self, **params):
175+
return EntryPoints(ep for ep in self if ep.matches(**params))
176+
177+
@property
178+
def names(self):
179+
return set(ep.name for ep in self)
180+
181+
@property
182+
def groups(self):
183+
"""
184+
For coverage while SelectableGroups is present.
185+
>>> EntryPoints().groups
186+
set()
187+
"""
188+
return set(ep.group for ep in self)
189+
190+
@classmethod
191+
def _from_text_for(cls, text, dist):
192+
return cls(ep._for(dist) for ep in EntryPoint._from_text(text))
193+
194+
195+
def flake8_bypass(func):
196+
is_flake8 = any('flake8' in str(frame.filename) for frame in inspect.stack()[:5])
197+
return func if not is_flake8 else lambda: None
198+
199+
200+
class Deprecated:
201+
"""
202+
Compatibility add-in for mapping to indicate that
203+
mapping behavior is deprecated.
204+
205+
>>> recwarn = getfixture('recwarn')
206+
>>> class DeprecatedDict(Deprecated, dict): pass
207+
>>> dd = DeprecatedDict(foo='bar')
208+
>>> dd.get('baz', None)
209+
>>> dd['foo']
210+
'bar'
211+
>>> list(dd)
212+
['foo']
213+
>>> list(dd.keys())
214+
['foo']
215+
>>> 'foo' in dd
216+
True
217+
>>> list(dd.values())
218+
['bar']
219+
>>> len(recwarn)
220+
1
221+
"""
222+
223+
_warn = functools.partial(
224+
warnings.warn,
225+
"SelectableGroups dict interface is deprecated. Use select.",
226+
DeprecationWarning,
227+
stacklevel=2,
228+
)
229+
230+
def __getitem__(self, name):
231+
self._warn()
232+
return super().__getitem__(name)
233+
234+
def get(self, name, default=None):
235+
flake8_bypass(self._warn)()
236+
return super().get(name, default)
237+
238+
def __iter__(self):
239+
self._warn()
240+
return super().__iter__()
241+
242+
def __contains__(self, *args):
243+
self._warn()
244+
return super().__contains__(*args)
245+
246+
def keys(self):
247+
self._warn()
248+
return super().keys()
249+
250+
def values(self):
251+
self._warn()
252+
return super().values()
253+
254+
255+
class SelectableGroups(dict):
256+
"""
257+
A backward- and forward-compatible result from
258+
entry_points that fully implements the dict interface.
259+
"""
260+
261+
@classmethod
262+
def load(cls, eps):
263+
by_group = operator.attrgetter('group')
264+
ordered = sorted(eps, key=by_group)
265+
grouped = itertools.groupby(ordered, by_group)
266+
return cls((group, EntryPoints(eps)) for group, eps in grouped)
267+
268+
@property
269+
def _all(self):
270+
"""
271+
Reconstruct a list of all entrypoints from the groups.
272+
"""
273+
return EntryPoints(itertools.chain.from_iterable(self.values()))
274+
275+
@property
276+
def groups(self):
277+
return self._all.groups
278+
279+
@property
280+
def names(self):
281+
"""
282+
for coverage:
283+
>>> SelectableGroups().names
284+
set()
285+
"""
286+
return self._all.names
287+
288+
def select(self, **params):
289+
if not params:
290+
return self
291+
return self._all.select(**params)
292+
157293

158294
class PackagePath(pathlib.PurePosixPath):
159295
"""A reference to a path in a package"""
@@ -310,7 +446,7 @@ def version(self):
310446

311447
@property
312448
def entry_points(self):
313-
return list(EntryPoint._from_text_for(self.read_text('entry_points.txt'), self))
449+
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
314450

315451
@property
316452
def files(self):
@@ -655,19 +791,28 @@ def version(distribution_name):
655791
return distribution(distribution_name).version
656792

657793

658-
def entry_points():
794+
def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
659795
"""Return EntryPoint objects for all installed packages.
660796
661-
:return: EntryPoint objects for all installed packages.
797+
Pass selection parameters (group or name) to filter the
798+
result to entry points matching those properties (see
799+
EntryPoints.select()).
800+
801+
For compatibility, returns ``SelectableGroups`` object unless
802+
selection parameters are supplied. In the future, this function
803+
will return ``EntryPoints`` instead of ``SelectableGroups``
804+
even when no selection parameters are supplied.
805+
806+
For maximum future compatibility, pass selection parameters
807+
or invoke ``.select`` with parameters on the result.
808+
809+
:return: EntryPoints or SelectableGroups for all installed packages.
662810
"""
663811
unique = functools.partial(unique_everseen, key=operator.attrgetter('name'))
664812
eps = itertools.chain.from_iterable(
665813
dist.entry_points for dist in unique(distributions())
666814
)
667-
by_group = operator.attrgetter('group')
668-
ordered = sorted(eps, key=by_group)
669-
grouped = itertools.groupby(ordered, by_group)
670-
return {group: tuple(eps) for group, eps in grouped}
815+
return SelectableGroups.load(eps).select(**params)
671816

672817

673818
def files(distribution_name):
@@ -687,3 +832,19 @@ def requires(distribution_name):
687832
packaging.requirement.Requirement.
688833
"""
689834
return distribution(distribution_name).requires
835+
836+
837+
def packages_distributions() -> Mapping[str, List[str]]:
838+
"""
839+
Return a mapping of top-level packages to their
840+
distributions.
841+
842+
>>> pkgs = packages_distributions()
843+
>>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
844+
True
845+
"""
846+
pkg_to_dist = collections.defaultdict(list)
847+
for dist in distributions():
848+
for pkg in (dist.read_text('top_level.txt') or '').split():
849+
pkg_to_dist[pkg].append(dist.metadata['Name'])
850+
return dict(pkg_to_dist)

0 commit comments

Comments
 (0)