diff options
Diffstat (limited to 'sleekxmpp/plugins/base.py')
-rw-r--r-- | sleekxmpp/plugins/base.py | 340 |
1 files changed, 271 insertions, 69 deletions
diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 561421d8..f08023ba 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -1,91 +1,293 @@ +# -*- 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. + + Part of SleekXMPP: The Sleek XMPP Library - See the file LICENSE for copying permission. + :copyright: (c) 2012 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ +import sys +import logging +import threading + + +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() -class base_plugin(object): +class PluginNotFound(Exception): + """Raised if an unknown plugin is accessed.""" + + +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) + + +def load_plugin(name, module=None): + """Find and import a plugin module so that it can be registered. + + This function is called to import plugins that have selected for + enabling, but no matching registered plugin has been found. + + :param str name: The name of the plugin. It is expected that + plugins are in packages matching their name, + even though the plugin class name does not + have to match. + :param str module: The name of the base module to search + for the plugin. """ + try: + if not module: + try: + module = 'sleekxmpp.plugins.%s' % name + __import__(module) + mod = sys.modules[module] + except: + module = 'sleekxmpp.features.%s' % name + __import__(module) + mod = sys.modules[module] + else: + __import__(module) + mod = sys.modules[module] + # Add older style plugins to the registry. + if hasattr(mod, name): + plugin = getattr(mod, name) + if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'): + plugin.name = name + # Mark the plugin as an older style plugin so + # we can work around dependency issues. + plugin.old_style = True + register_plugin(plugin, name) + except: + log.exception("Unable to load plugin: %s", name) + + +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. """ + top_level = False + if enabled is None: + enabled = set() + + with self._plugin_lock: + if name not in self._enabled: + enabled.add(name) + self._enabled.add(name) + if not self.registered(name): + load_plugin(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) + + if top_level: + for name in enabled: + if hasattr(self.plugins[name], 'old_style'): + # Older style plugins require post_init() + # to run just before stream processing begins, + # so we don't call it here. + pass + self.plugins[name].post_init() + + 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 + """ + 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): """ - Initialize plugin state, such as registering any stream or - event handlers, or new stanza types. + 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 |