summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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