summaryrefslogtreecommitdiffstats
path: root/plugin_registry.py
diff options
context:
space:
mode:
authorJan Pokorný <jpokorny@redhat.com>2013-11-18 11:14:43 +0100
committerJan Pokorný <jpokorny@redhat.com>2013-11-18 11:14:43 +0100
commitb182bb2e0a9266b313112d507e9803e5ef2de387 (patch)
treef348cb4d1b254d2e7fa8cc61a1f82d3b4a2e9e99 /plugin_registry.py
downloadclufter-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.py194
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