01b2499915
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.
239 lines
7.6 KiB
Python
239 lines
7.6 KiB
Python
# -*- encoding: utf-8 -*-
|
|
|
|
"""
|
|
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
|
|
|
|
|
|
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'`.
|
|
"""
|
|
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.
|
|
"""
|
|
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 = {}
|
|
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
|
|
|
|
#: 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.
|
|
"""
|
|
pass
|
|
|
|
|
|
base_plugin = BasePlugin
|