From a474726776298d92c9ecbabd54dc89d05777a8ed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Feb 2021 09:58:19 -0500 Subject: [PATCH] Introduce SelectableGroups, created for the 3.x line to provide forward compatibilty to the new interfaces without sacrificing backward compatibility. --- CHANGES.rst | 18 +++++++++---- docs/using.rst | 4 +-- importlib_metadata/__init__.py | 46 +++++++++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2f5b1ec5..03fec086 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -v4.0.0 +v3.6.0 ====== * #284: Introduces new ``EntryPoints`` object, a tuple of @@ -13,10 +13,18 @@ v4.0.0 - Item access (e.g. ``eps[name]``) retrieves a single entry point by name. - ``entry_points()`` now returns an ``EntryPoints`` - object, but provides for backward compatibility with - a ``__getitem__`` accessor by group and a ``get()`` - method. + ``entry_points()`` now provides a future-compatible + ``SelectableGroups`` object that supplies the above interface + but remains a dict for compatibility. + + In the future, ``entry_points()`` will return an + ``EntryPoints`` object, but provide for backward + compatibility with a deprecated ``__getitem__`` + accessor by group and a ``get()`` method. + + If passing selection parameters to ``entry_points``, the + future behavior is invoked and an ``EntryPoints`` is the + result. Construction of entry points using ``dict([EntryPoint, ...])`` is now deprecated and raises diff --git a/docs/using.rst b/docs/using.rst index bdfe3e82..97941452 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -67,8 +67,8 @@ This package provides the following functionality via its public API. Entry points ------------ -The ``entry_points()`` function returns a sequence of all entry points, -keyed by group. Entry points are represented by ``EntryPoint`` instances; +The ``entry_points()`` function returns a collection of entry points. +Entry points are represented by ``EntryPoint`` instances; each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and a ``.load()`` method to resolve the value. There are also ``.module``, ``.attr``, and ``.extras`` attributes for getting the components of the diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index bd7d4d7e..c51c6528 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -187,6 +187,37 @@ def _from_text_for(cls, text, dist): return cls(ep._for(dist) for ep in EntryPoint._from_text(text)) +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 groups(self): + return self.keys() + + @property + def names(self): + return (ep.name for ep in self._all) + + @property + def _all(self): + return itertools.chain.from_iterable(self.values()) + + def select(self, **params): + if not params: + return self + return EntryPoints(self._all).select(**params) + + class LegacyGroupedEntryPoints(EntryPoints): """ Compatibility wrapper around EntryPoints to provide @@ -713,16 +744,25 @@ def version(distribution_name): return distribution(distribution_name).version -def entry_points(**params): +def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: """Return EntryPoint objects for all installed packages. - :return: EntryPoint objects for all installed packages. + For compatibility, returns ``SelectableGroups`` object unless + selection parameters are supplied. In the future, this function + will return ``LegacyGroupedEntryPoints`` instead of + ``SelectableGroups`` and eventually will only return + ``EntryPoints``. + + For maximum future compatibility, pass selection parameters + or invoke ``.select`` with parameters on the result. + + :return: EntryPoints or SelectableGroups for all installed packages. """ unique = functools.partial(unique_everseen, key=operator.attrgetter('name')) eps = itertools.chain.from_iterable( dist.entry_points for dist in unique(distributions()) ) - return LegacyGroupedEntryPoints(eps).select(**params) + return SelectableGroups.load(eps).select(**params) def files(distribution_name):