Source code for gather.api

"""Gather -- Collect all your plugins

Gather allows a way to register plugins.
It features the ability to register the plugins from any module,
in any package, in any distribution.
A given module can register plugins of multiple types.

In order to have anything registered from a package,
it needs to declare that it supports :code:`gather`
in its package metadata.

For example,
with
:code:`pyproject.toml`:

.. code::

    [project.entry-points.gather]
    ignored = "<ROOT_PACKAGE>"


The :code:`ROOT_PACKAGE` should point to the Python name of the package:
i.e., what users are expected to :code:`import` at the top-level.

Note that while having special facilities to run functions as subcommands,
Gather can be used to collect anything.
"""
import collections
import importlib.metadata
import sys

import attr
import venusian


def _get_modules():
    for entry_point in importlib.metadata.entry_points(group="gather"):
        module = importlib.import_module(entry_point.value)
        yield module


[docs] @attr.s(frozen=True) class Collector(object): """ A plugin collector. A collector allows to *register* functions or classes by modules, and *collect*-ing them when they need to be used. """ name = attr.ib(default=None) depth = attr.ib(default=1)
[docs] def register(self, name=None, transform=lambda x: x): """ Register a class or function Args: name (str): optional. Name to register the class or function as. (default is name of object) transform (callable): optional. A one-argument function. Will be called, and the return value used in collection. Default is identity function This is meant to be used as a decoator: .. code:: @COLLECTOR.register() def specific_subcommand(args): pass @COLLECTOR.register(name='another_specific_name') def main(args): pass """ def callback(scanner, inner_name, objct): ( """ Venusian_ callback, called from scan .. _Venusian: http://docs.pylonsproject.org/projects/""" """venusian/en/latest/api.html#venusian.attach """ ) tag = getattr(scanner, "tag", None) if tag is not self: return if name is None: effective_name = inner_name else: effective_name = name objct = transform(objct) scanner.registry[effective_name].add(objct) def attach(func): """Attach callback to be called when object is scanned""" venusian.attach(func, callback, depth=self.depth) return func return attach
[docs] def collect(self): """ Collect all registered functions or classes. Returns a dictionary mapping names to registered elements. """ def ignore_import_error(_unused): """ Ignore ImportError during collection. Some modules raise import errors for various reasons, and should be just treated as missing. """ if not issubclass(sys.exc_info()[0], ImportError): raise # pragma: no cover registry = collections.defaultdict(set) scanner = venusian.Scanner(registry=registry, tag=self) for module in _get_modules(): scanner.scan(module, onerror=ignore_import_error) return registry
[docs] def unique(mapping): """ Transform map to sets to map to single items. Raises a :code:`ValueError` if any of the values is not an iterable with exactly one item. Args: mapping: A mapping of keys to Iterables of 1 Returns: A mapping of keys to the single value """ ret = {} for key, value_set in mapping.items(): [value] = value_set ret[key] = value return ret
[docs] @attr.s(frozen=True) class Wrapper(object): """Add extra data to an object""" original = attr.ib() extra = attr.ib()
[docs] @classmethod def glue(cls, extra): """ Glue extra data to an object Args: extra: what to add Returns: callable: function of one argument that returns a :code:`Wrapped` This method is useful mainly as the :code:`transform` parameter of a :code:`register` call. """ def ret(original): """Return a :code:`Wrapper` with the original and extra""" return cls(original=original, extra=extra) return ret
__all__ = ["Collector", "unique", "Wrapper"]