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:
parent
9f43d31bf5
commit
01b2499915
3 changed files with 265 additions and 114 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue