diff options
author | Jan Pokorný <jpokorny@redhat.com> | 2013-11-18 11:14:43 +0100 |
---|---|---|
committer | Jan Pokorný <jpokorny@redhat.com> | 2013-11-18 11:14:43 +0100 |
commit | b182bb2e0a9266b313112d507e9803e5ef2de387 (patch) | |
tree | f348cb4d1b254d2e7fa8cc61a1f82d3b4a2e9e99 /plugin_registry.py | |
download | clufter-b182bb2e0a9266b313112d507e9803e5ef2de387.tar.gz clufter-b182bb2e0a9266b313112d507e9803e5ef2de387.tar.xz clufter-b182bb2e0a9266b313112d507e9803e5ef2de387.zip |
Initial commit
Signed-off-by: Jan Pokorný <jpokorny@redhat.com>
Diffstat (limited to 'plugin_registry.py')
-rw-r--r-- | plugin_registry.py | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/plugin_registry.py b/plugin_registry.py new file mode 100644 index 0000000..05369a5 --- /dev/null +++ b/plugin_registry.py @@ -0,0 +1,194 @@ +# -*- coding: UTF-8 -*- +# Copyright 2013 Red Hat, Inc. +# Part of clufter project +# Licensed under GPLv2 (a copy included | http://gnu.org/licenses/gpl-2.0.txt) +"""Easy (at least for usage) plugin framework""" +__author__ = "Jan Pokorný <jpokorny at redhat dot com>" + +import imp +import logging +from os import extsep, walk +from os.path import abspath, dirname, join, splitext +from contextlib import contextmanager +from sys import modules + +from .utils import classproperty, hybridproperty + +log = logging.getLogger(__name__) + + +class PluginRegistry(type): + """Core of simple plugin framework + + This ``superclass'' serves two roles: + + (1) metaclass for plugins (and its class hierarchy) + - only a tiny wrapper on top of `type` + + (2) base class for particular registries + + Both roles are combined due to a very tight connection. + + Inspired by http://eli.thegreenplace.net/2012/08/07/ (Fundamental...). + """ + _registries = set() # dynamic tracking of specific registries + + # + # these are relevant for use case (1) + # + + # non-API + + def __new__(registry, name, bases, attrs): + if '__metaclass__' not in attrs and (registry.use_local + or not registry.__module__.startswith(attrs['__module__'])): + # alleged end-use plugin + ret = registry.probe(name, bases, attrs) + else: + if registry not in PluginRegistry._registries: + log.debug("Registering `{0}' as registry" + .format(registry.registry)) + # specific registry not yet tracked + # (e.g., specific plugin was imported natively) + registry.setup() + PluginRegistry._registries.add(registry) + if registry.namespace not in modules: + # XXX hack to keep going in the test suite + __import__(registry.namespace) + + ret = super(PluginRegistry, registry).__new__(registry, name, + bases, attrs) + + return ret + + # + # these are relevant for both (1) + (2) + # + + @classmethod + def probe(registry, name, bases, attrs): + """Meta-magic to register plugin""" + try: + ret = registry._plugins[name] + log.info("Probe `{0}' plugin under `{1}' registry: already tracked" + .format(name, registry.registry)) + except KeyError: + log.debug("Probe `{0}' plugin under `{1}' registry: yet untracked" + .format(name, registry.registry)) + ret = super(PluginRegistry, registry).__new__(registry, name, + bases, attrs) + registry._plugins[name] = ret + finally: + if registry._path_context is not None: + registry._path_mapping[registry._path_context].add(name) + + return ret + + @hybridproperty + def name(this): + """Nicer access to __name__""" + return this.__name__ + + @classproperty + def registry(registry): + """Registry identifier""" + return registry.__name__ + + @classproperty + def namespace(registry): + """Absolute namespace possessed by the particular plugin registry + + For a plugin, this denotes to which registry/namespace it belongs. + """ + try: + return registry._namespace + except AttributeError: + registry._namespace = '.'.join((__package__, registry.__name__)) + return registry._namespace + + # + # these are relevant for use case (2) + # + + # non-API + + @classmethod + @contextmanager + def _path(registry, path): + """Temporary path context setup enabling safe sideways use""" + assert registry._path_context is None + registry._path_context = path + yield path, registry._path_mapping.setdefault(path, set()) + assert registry._path_context is not None + registry._path_context = None + + @classmethod + def _context(registry, paths): + """Iterate through the paths yielding context along + + Context is a pair `(path, list_of_per_path_tracked_plugins_so_far)`. + """ + if not isinstance(paths, (list, tuple)): + if paths is None: + return # explictly asked not to use even implicit path + paths = (paths, ) + + # inject implicit one + implicit = join(dirname(abspath(__file__)), registry.__name__) + paths = (lambda *x: x)(implicit, *paths) + + for path in paths: + with registry._path(path) as context: + yield context + + # API + + use_local = False + + @classmethod + def setup(registry, reset=False): + """Implicit setup upon first registry involvement or external reset""" + attrs = ('_path_context', None), ('_path_mapping', {}), ('_plugins', {}) + if reset: + map(lambda (a, d): setattr(registry, a, d), attrs) + else: + map(lambda (a, d): setattr(registry, a, getattr(registry, a, d)), + attrs) + if reset: + PluginRegistry._registries.discard(registry) + + @classmethod + def discover(registry, paths): + """Find relevant plugins available at the specified path(s) + + Returns `{plugin_name: plugin_cls}` mapping of plugins found. + """ + ret = {} + for path, path_plugins in registry._context(paths): + # skip if path already discovered (considered final) + if not len(path_plugins): + # visit *.py files within (and under) the path and probe them + for root, dirs, files in walk(path): + for f in files: + name, ext = splitext(f) + if not name.startswith('_') and ext == extsep + 'py': + mfile, mpath, mdesc = imp.find_module(name, [root]) + if not mfile: + log.debug("Omitting `{0}' at `{1}'" + .format(name, root)) + continue + mname = '.'.join((registry.namespace, name)) + try: + imp.load_module(mname, mfile, mpath, mdesc) + finally: + mfile.close() + path_plugins = registry._path_mapping[path] + + # filled as a side-effect of meta-magic, see `__new__` + ret.update({n: registry._plugins[n] for n in path_plugins}) + if registry.use_local: + # add "built-in" ones + ret.update({n: p for n, p in registry._plugins.iteritems() + if p.__module__ == registry.__module__}) + + return ret |