55import sys
66import zipp
77import email
8+ import inspect
89import pathlib
910import operator
11+ import warnings
1012import functools
1113import itertools
1214import posixpath
13- import collections
15+ import collections . abc
1416
1517from ._compat import (
1618 NullFinder ,
2628from importlib import import_module
2729from importlib .abc import MetaPathFinder
2830from 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
158294class 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
673818def 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