Introduce new plugin system.

The new system is backward compatible and will load older style plugins.

The new plugin framework allows plugins to track their dependencies, and
will auto-enable plugins as needed.

Dependencies are tracked via a class-level set named `dependencies` in
each plugin.

Plugin names are no longer tightly coupled with the plugin class name,
Pso EP8 style class names may be used.

Disabling plugins is now allowed, but ensuring proper cleanup is left to
the plugin implementation.

The use of a `post_init()` method is no longer needed for new style
plugins, but plugins following the old style will still require a
`post_init()` method.
This commit is contained in:
Lance Stout 2012-03-11 18:09:45 -07:00
parent 9f43d31bf5
commit 01b2499915
3 changed files with 265 additions and 114 deletions

View file

@ -31,6 +31,9 @@ from sleekxmpp.xmlstream import ET, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.features import *
from sleekxmpp.plugins import PluginManager, register_plugin
log = logging.getLogger(__name__)
@ -66,7 +69,7 @@ class BaseXMPP(XMLStream):
self.boundjid = JID(jid)
#: A dictionary mapping plugin names to plugins.
self.plugin = {}
self.plugin = PluginManager(self)
#: Configuration options for whitelisted plugins.
#: If a plugin is registered without any configuration,
@ -185,19 +188,18 @@ class BaseXMPP(XMLStream):
- The send queue processor
- The scheduler
"""
# The current post_init() process can only resolve a single
# layer of inter-plugin dependencies. However, XEP-0115 and
# plugins which depend on it exceeds this limit and can cause
# failures if plugins are post_inited out of order, so we must
# manually process XEP-0115 first.
if 'xep_0115' in self.plugin:
if not self.plugin['xep_0115'].post_inited:
self.plugin['xep_0115'].post_init()
name = 'xep_0115'
if not hasattr(self.plugin[name], 'post_inited'):
if hasattr(self.plugin[name], 'post_init'):
self.plugin[name].post_init()
self.plugin[name].post_inited = True
for name in self.plugin:
if not self.plugin[name].post_inited:
self.plugin[name].post_init()
if not hasattr(self.plugin[name], 'post_inited'):
if hasattr(self.plugin[name], 'post_init'):
self.plugin[name].post_init()
self.plugin[name].post_inited = True
return XMLStream.process(self, *args, **kwargs)
def register_plugin(self, plugin, pconfig={}, module=None):
@ -210,42 +212,41 @@ class BaseXMPP(XMLStream):
:param module: Optional refence to the module containing the plugin
class if using custom plugins.
"""
try:
# Import the given module that contains the plugin.
if not module:
try:
module = plugins
module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)])
except ImportError:
module = features
module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)])
if isinstance(module, str):
# We probably want to load a module from outside
# the sleekxmpp package, so leave out the globals().
module = __import__(module, fromlist=[plugin])
# Use the global plugin config cache, if applicable
if not pconfig:
pconfig = self.plugin_config.get(plugin, {})
# Use the global plugin config cache, if applicable
if not pconfig:
pconfig = self.plugin_config.get(plugin, {})
# Load the plugin class from the module.
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
if not self.plugin.registered(plugin):
# Use old-style plugin
try:
#Import the given module that contains the plugin.
if not module:
try:
module = sleekxmpp.plugins
module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)])
except ImportError:
module = sleekxmpp.features
module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)])
if isinstance(module, str):
# We probably want to load a module from outside
# the sleekxmpp package, so leave out the globals().
module = __import__(module, fromlist=[plugin])
# Let XEP/RFC implementing plugins have some extra logging info.
spec = '(CUSTOM) '
if self.plugin[plugin].xep:
spec = "(XEP-%s) " % self.plugin[plugin].xep
elif self.plugin[plugin].rfc:
spec = "(RFC-%s) " % self.plugin[plugin].rfc
plugin_class = getattr(module, plugin)
desc = (spec, self.plugin[plugin].description)
log.debug("Loaded Plugin %s %s" % desc)
except:
log.exception("Unable to load plugin: %s", plugin)
if not hasattr(plugin_class, 'name'):
plugin_class.name = plugin
register_plugin(plugin_class, name=plugin)
except:
log.exception("Unable to load plugin: %s", plugin)
return
self.plugin.enable(plugin, pconfig)
def register_plugins(self):
"""Register and initialize all built-in plugins.
@ -262,8 +263,7 @@ class BaseXMPP(XMLStream):
for plugin in plugin_list:
if plugin in plugins.__all__:
self.register_plugin(plugin,
self.plugin_config.get(plugin, {}))
self.register_plugin(plugin)
else:
raise NameError("Plugin %s not in plugins.__all__." % plugin)

View file

@ -6,6 +6,9 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import PluginManager, PluginNotFound, \
BasePlugin, register_plugin
__all__ = [
# Non-standard
'gmail_notify', # Gmail searching and notifications

View file

@ -1,91 +1,239 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
# -*- encoding: utf-8 -*-
See the file LICENSE for copying permission.
"""
sleekxmpp.plugins.base
~~~~~~~~~~~~~~~~~~~~~~
This module provides XMPP functionality that
is specific to client connections.
Part of SleekXMPP: The Sleek XMPP Library
:copyright: (c) 2012 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
import threading
import logging
class base_plugin(object):
log = logging.getLogger(__name__)
#: Associate short string names of plugins with implementations. The
#: plugin names are based on the spec used by the plugin, such as
#: `'xep_0030'` for a plugin that implements XEP-0030.
PLUGIN_REGISTRY = {}
#: In order to do cascading plugin disabling, reverse dependencies
#: must be tracked.
PLUGIN_DEPENDENTS = {}
#: Only allow one thread to manipulate the plugin registry at a time.
REGISTRY_LOCK = threading.RLock()
def register_plugin(impl, name=None):
"""Add a new plugin implementation to the registry.
:param class impl: The plugin class.
The implementation class must provide a :attr:`~BasePlugin.name`
value that will be used as a short name for enabling and disabling
the plugin. The name should be based on the specification used by
the plugin. For example, a plugin implementing XEP-0030 would be
named `'xep_0030'`.
"""
The base_plugin class serves as a base for user created plugins
that provide support for existing or experimental XEPS.
if name is None:
name = impl.name
with REGISTRY_LOCK:
PLUGIN_REGISTRY[name] = impl
if name not in PLUGIN_DEPENDENTS:
PLUGIN_DEPENDENTS[name] = set()
for dep in impl.dependencies:
if dep not in PLUGIN_DEPENDENTS:
PLUGIN_DEPENDENTS[dep] = set()
PLUGIN_DEPENDENTS[dep].add(name)
Each plugin has a dictionary for configuration options, as well
as a name and description.
The lifecycle of a plugin is:
1. The plugin is instantiated during registration.
2. Once the XML stream begins processing, the method
plugin_init() is called (if the plugin is configured
as enabled with {'enable': True}).
3. After all plugins have been initialized, the
method post_init() is called.
class PluginNotFound(Exception):
"""Raised if an unknown plugin is accessed."""
Recommended event handlers:
session_start -- Plugins which require the use of the current
bound JID SHOULD wait for the session_start
event to perform any initialization (or
resetting). This is a transitive recommendation,
plugins that use other plugins which use the
bound JID should also wait for session_start
before making such calls.
session_end -- If the plugin keeps any per-session state,
such as joined MUC rooms, such state SHOULD
be cleared when the session_end event is raised.
Attributes:
xep -- The XEP number the plugin implements, if any.
description -- A short description of the plugin, typically
the long name of the implemented XEP.
xmpp -- The main SleekXMPP instance.
config -- A dictionary of custom configuration values.
The value 'enable' is special and controls
whether or not the plugin is initialized
after registration.
post_initted -- Executed after all plugins have been initialized
to handle any cross-plugin interactions, such as
registering service discovery items.
enable -- Indicates that the plugin is enabled for use and
will be initialized after registration.
Methods:
plugin_init -- Initialize the plugin state.
post_init -- Handle any cross-plugin concerns.
"""
class PluginManager(object):
def __init__(self, xmpp, config=None):
"""
Instantiate a new plugin and store the given configuration.
#: We will track all enabled plugins in a set so that we
#: can enable plugins in batches and pull in dependencies
#: without problems.
self._enabled = set()
Arguments:
xmpp -- The main SleekXMPP instance.
config -- A dictionary of configuration values.
#: Maintain references to active plugins.
self._plugins = {}
self._plugin_lock = threading.RLock()
#: Globally set default plugin configuration. This will
#: be used for plugins that are auto-enabled through
#: dependency loading.
self.config = config if config else {}
self.xmpp = xmpp
def register(self, plugin, enable=True):
"""Register a new plugin, and optionally enable it.
:param class plugin: The implementation class of the plugin
to register.
:param bool enable: If ``True``, immediately enable the
plugin after registration.
"""
register_plugin(plugin)
if enable:
self.enable(plugin.name)
def enable(self, name, config=None, enabled=None):
"""Enable a plugin, including any dependencies.
:param string name: The short name of the plugin.
:param dict config: Optional settings dictionary for
configuring plugin behaviour.
"""
if enabled is None:
enabled = set()
with self._plugin_lock:
if name not in self._enabled:
enabled.add(name)
self._enabled.add(name)
plugin_class = PLUGIN_REGISTRY.get(name, None)
if not plugin_class:
raise PluginNotFound(name)
if config is None:
config = self.config.get(name, None)
plugin = plugin_class(self.xmpp, config)
self._plugins[name] = plugin
for dep in plugin.dependencies:
self.enable(dep, enabled=enabled)
plugin.plugin_init()
log.debug("Loaded Plugin: %s", plugin.description)
def enable_all(self, names=None, config=None):
"""Enable all registered plugins.
:param list names: A list of plugin names to enable. If
none are provided, all registered plugins
will be enabled.
:param dict config: A dictionary mapping plugin names to
configuration dictionaries, as used by
:meth:`~PluginManager.enable`.
"""
names = names if names else PLUGIN_REGISTRY.keys()
if config is None:
config = {}
self.xep = None
self.rfc = None
self.description = 'Base Plugin'
for name in names:
self.enable(name, config.get(name, {}))
def enabled(self, name):
"""Check if a plugin has been enabled.
:param string name: The name of the plugin to check.
:return: boolean
"""
return name in self._enabled
def registered(self, name):
"""Check if a plugin has been registered.
:param string name: The name of the plugin to check.
:return: boolean
"""
return name in PLUGIN_REGISTRY
def disable(self, name, _disabled=None):
"""Disable a plugin, including any dependent upon it.
:param string name: The name of the plugin to disable.
:param set _disabled: Private set used to track the
disabled status of plugins during
the cascading process.
"""
if _disabled is None:
_disabled = set()
with self._plugin_lock:
if name not in _disabled and name in self._enabled:
_disabled.add(name)
plugin = self._plugins.get(name, None)
if plugin is None:
raise PluginNotFound(name)
for dep in PLUGIN_DEPENDENTS[name]:
self.disable(dep, _disabled)
plugin.plugin_end()
if name in self._enabled:
self._enabled.remove(name)
del self._plugins[name]
def __keys__(self):
"""Return the set of enabled plugins."""
return self._plugins.keys()
def __getitem__(self, name):
"""
Allow plugins to be accessed through the manager as if
it were a dictionary.
"""
plugin = self._plugins.get(name, None)
if plugin is None:
raise PluginNotFound(name)
return plugin
def __iter__(self):
"""Return an iterator over the set of enabled plugins."""
return self._plugins.__iter__()
def __len__(self):
"""Return the number of enabled plugins."""
return len(self._plugins)
class BasePlugin(object):
#: A short name for the plugin based on the implemented specification.
#: For example, a plugin for XEP-0030 would use `'xep_0030'`.
name = ''
#: A longer name for the plugin, describing its purpose. For example,
#: a plugin for XEP-0030 would use `'Service Discovery'` as its
#: description value.
description = ''
#: Some plugins may depend on others in order to function properly.
#: Any plugin names included in :attr:`~BasePlugin.dependencies` will
#: be initialized as needed if this plugin is enabled.
dependencies = set()
def __init__(self, xmpp, config=None):
self.xmpp = xmpp
self.config = config
self.post_inited = False
self.enable = config.get('enable', True)
if self.enable:
self.plugin_init()
#: A plugin's behaviour may be configurable, in which case those
#: configuration settings will be provided as a dictionary.
self.config = config if config is not None else {}
def plugin_init(self):
"""
Initialize plugin state, such as registering any stream or
event handlers, or new stanza types.
"""
"""Initialize plugin state, such as registering event handlers."""
pass
def plugin_end(self):
"""Cleanup plugin state, and prepare for plugin removal."""
pass
def post_init(self):
"""Initialize any cross-plugin state.
Only needed if the plugin has circular dependencies.
"""
Perform any cross-plugin interactions, such as registering
service discovery identities or items.
"""
self.post_inited = True
pass
base_plugin = BasePlugin