summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLance Stout <lancestout@gmail.com>2012-03-11 18:09:45 -0700
committerLance Stout <lancestout@gmail.com>2012-03-12 16:24:18 -0700
commit01b24999152c1eecceeac96c13f6e3e29ff10a6c (patch)
tree53eaaccadbff54410c6add62d63d3577603edee9
parent9f43d31bf511e0917f11b7edcac93921100383b7 (diff)
downloadslixmpp-01b24999152c1eecceeac96c13f6e3e29ff10a6c.tar.gz
slixmpp-01b24999152c1eecceeac96c13f6e3e29ff10a6c.tar.bz2
slixmpp-01b24999152c1eecceeac96c13f6e3e29ff10a6c.tar.xz
slixmpp-01b24999152c1eecceeac96c13f6e3e29ff10a6c.zip
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.
-rw-r--r--sleekxmpp/basexmpp.py98
-rw-r--r--sleekxmpp/plugins/__init__.py3
-rw-r--r--sleekxmpp/plugins/base.py288
3 files changed, 270 insertions, 119 deletions
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index f655783a..c0e5f9bd 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -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, {})
-
- # Load the plugin class from the module.
- self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
-
- # 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
-
- desc = (spec, self.plugin[plugin].description)
- log.debug("Loaded Plugin %s %s" % desc)
- except:
- log.exception("Unable to load plugin: %s", plugin)
+
+ # Use the global plugin config cache, if applicable
+ if not pconfig:
+ pconfig = self.plugin_config.get(plugin, {})
+
+ 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])
+
+ plugin_class = getattr(module, 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)
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index 6cb854ed..4fb41919 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -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
diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py
index 561421d8..447ffba7 100644
--- a/sleekxmpp/plugins/base.py
+++ b/sleekxmpp/plugins/base.py
@@ -1,91 +1,239 @@
+# -*- encoding: utf-8 -*-
+
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
+ sleekxmpp.plugins.base
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to client connections.
- See the file LICENSE for copying permission.
+ 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.
-
- 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.
-
- 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.
- """
+ 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)
+
+
+class PluginNotFound(Exception):
+ """Raised if an unknown plugin is accessed."""
+
+class PluginManager(object):
def __init__(self, xmpp, config=None):
+ #: 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()
+
+ #: 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.
"""
- Instantiate a new plugin and store the given configuration.
+ register_plugin(plugin)
+ if enable:
+ self.enable(plugin.name)
+
+ def enable(self, name, config=None, enabled=None):
+ """Enable a plugin, including any dependencies.
- Arguments:
- xmpp -- The main SleekXMPP instance.
- config -- A dictionary of configuration values.
+ :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'
- self.xmpp = xmpp
- self.config = config
- self.post_inited = False
- self.enable = config.get('enable', True)
- if self.enable:
- self.plugin_init()
+ for name in names:
+ self.enable(name, config.get(name, {}))
- def plugin_init(self):
+ 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
"""
- Initialize plugin state, such as registering any stream or
- event handlers, or new stanza types.
+ 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
+
+ #: 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 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