Rename to slixmpp
[slixmpp.git] / slixmpp / plugins / base.py
blob9694a414775c704f2fdcff25e0fa392f4db1be58
1 # -*- encoding: utf-8 -*-
3 """
4 slixmpp.plugins.base
5 ~~~~~~~~~~~~~~~~~~~~~~
7 This module provides XMPP functionality that
8 is specific to client connections.
10 Part of Slixmpp: The Slick XMPP Library
12 :copyright: (c) 2012 Nathanael C. Fritz
13 :license: MIT, see LICENSE for more details
14 """
16 import sys
17 import copy
18 import logging
19 import threading
22 if sys.version_info >= (3, 0):
23 unicode = str
26 log = logging.getLogger(__name__)
29 #: Associate short string names of plugins with implementations. The
30 #: plugin names are based on the spec used by the plugin, such as
31 #: `'xep_0030'` for a plugin that implements XEP-0030.
32 PLUGIN_REGISTRY = {}
34 #: In order to do cascading plugin disabling, reverse dependencies
35 #: must be tracked.
36 PLUGIN_DEPENDENTS = {}
38 #: Only allow one thread to manipulate the plugin registry at a time.
39 REGISTRY_LOCK = threading.RLock()
42 class PluginNotFound(Exception):
43 """Raised if an unknown plugin is accessed."""
46 def register_plugin(impl, name=None):
47 """Add a new plugin implementation to the registry.
49 :param class impl: The plugin class.
51 The implementation class must provide a :attr:`~BasePlugin.name`
52 value that will be used as a short name for enabling and disabling
53 the plugin. The name should be based on the specification used by
54 the plugin. For example, a plugin implementing XEP-0030 would be
55 named `'xep_0030'`.
56 """
57 if name is None:
58 name = impl.name
59 with REGISTRY_LOCK:
60 PLUGIN_REGISTRY[name] = impl
61 if name not in PLUGIN_DEPENDENTS:
62 PLUGIN_DEPENDENTS[name] = set()
63 for dep in impl.dependencies:
64 if dep not in PLUGIN_DEPENDENTS:
65 PLUGIN_DEPENDENTS[dep] = set()
66 PLUGIN_DEPENDENTS[dep].add(name)
69 def load_plugin(name, module=None):
70 """Find and import a plugin module so that it can be registered.
72 This function is called to import plugins that have selected for
73 enabling, but no matching registered plugin has been found.
75 :param str name: The name of the plugin. It is expected that
76 plugins are in packages matching their name,
77 even though the plugin class name does not
78 have to match.
79 :param str module: The name of the base module to search
80 for the plugin.
81 """
82 try:
83 if not module:
84 try:
85 module = 'slixmpp.plugins.%s' % name
86 __import__(module)
87 mod = sys.modules[module]
88 except ImportError:
89 module = 'slixmpp.features.%s' % name
90 __import__(module)
91 mod = sys.modules[module]
92 elif isinstance(module, (str, unicode)):
93 __import__(module)
94 mod = sys.modules[module]
95 else:
96 mod = module
98 # Add older style plugins to the registry.
99 if hasattr(mod, name):
100 plugin = getattr(mod, name)
101 if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'):
102 plugin.name = name
103 # Mark the plugin as an older style plugin so
104 # we can work around dependency issues.
105 plugin.old_style = True
106 register_plugin(plugin, name)
107 except ImportError:
108 log.exception("Unable to load plugin: %s", name)
111 class PluginManager(object):
112 def __init__(self, xmpp, config=None):
113 #: We will track all enabled plugins in a set so that we
114 #: can enable plugins in batches and pull in dependencies
115 #: without problems.
116 self._enabled = set()
118 #: Maintain references to active plugins.
119 self._plugins = {}
121 self._plugin_lock = threading.RLock()
123 #: Globally set default plugin configuration. This will
124 #: be used for plugins that are auto-enabled through
125 #: dependency loading.
126 self.config = config if config else {}
128 self.xmpp = xmpp
130 def register(self, plugin, enable=True):
131 """Register a new plugin, and optionally enable it.
133 :param class plugin: The implementation class of the plugin
134 to register.
135 :param bool enable: If ``True``, immediately enable the
136 plugin after registration.
138 register_plugin(plugin)
139 if enable:
140 self.enable(plugin.name)
142 def enable(self, name, config=None, enabled=None):
143 """Enable a plugin, including any dependencies.
145 :param string name: The short name of the plugin.
146 :param dict config: Optional settings dictionary for
147 configuring plugin behaviour.
149 top_level = False
150 if enabled is None:
151 enabled = set()
153 with self._plugin_lock:
154 if name not in self._enabled:
155 enabled.add(name)
156 self._enabled.add(name)
157 if not self.registered(name):
158 load_plugin(name)
160 plugin_class = PLUGIN_REGISTRY.get(name, None)
161 if not plugin_class:
162 raise PluginNotFound(name)
164 if config is None:
165 config = self.config.get(name, None)
167 plugin = plugin_class(self.xmpp, config)
168 self._plugins[name] = plugin
169 for dep in plugin.dependencies:
170 self.enable(dep, enabled=enabled)
171 plugin._init()
173 if top_level:
174 for name in enabled:
175 if hasattr(self.plugins[name], 'old_style'):
176 # Older style plugins require post_init()
177 # to run just before stream processing begins,
178 # so we don't call it here.
179 pass
180 self.plugins[name].post_init()
182 def enable_all(self, names=None, config=None):
183 """Enable all registered plugins.
185 :param list names: A list of plugin names to enable. If
186 none are provided, all registered plugins
187 will be enabled.
188 :param dict config: A dictionary mapping plugin names to
189 configuration dictionaries, as used by
190 :meth:`~PluginManager.enable`.
192 names = names if names else PLUGIN_REGISTRY.keys()
193 if config is None:
194 config = {}
195 for name in names:
196 self.enable(name, config.get(name, {}))
198 def enabled(self, name):
199 """Check if a plugin has been enabled.
201 :param string name: The name of the plugin to check.
202 :return: boolean
204 return name in self._enabled
206 def registered(self, name):
207 """Check if a plugin has been registered.
209 :param string name: The name of the plugin to check.
210 :return: boolean
212 return name in PLUGIN_REGISTRY
214 def disable(self, name, _disabled=None):
215 """Disable a plugin, including any dependent upon it.
217 :param string name: The name of the plugin to disable.
218 :param set _disabled: Private set used to track the
219 disabled status of plugins during
220 the cascading process.
222 if _disabled is None:
223 _disabled = set()
224 with self._plugin_lock:
225 if name not in _disabled and name in self._enabled:
226 _disabled.add(name)
227 plugin = self._plugins.get(name, None)
228 if plugin is None:
229 raise PluginNotFound(name)
230 for dep in PLUGIN_DEPENDENTS[name]:
231 self.disable(dep, _disabled)
232 plugin._end()
233 if name in self._enabled:
234 self._enabled.remove(name)
235 del self._plugins[name]
237 def __keys__(self):
238 """Return the set of enabled plugins."""
239 return self._plugins.keys()
241 def __getitem__(self, name):
243 Allow plugins to be accessed through the manager as if
244 it were a dictionary.
246 plugin = self._plugins.get(name, None)
247 if plugin is None:
248 raise PluginNotFound(name)
249 return plugin
251 def __iter__(self):
252 """Return an iterator over the set of enabled plugins."""
253 return self._plugins.__iter__()
255 def __len__(self):
256 """Return the number of enabled plugins."""
257 return len(self._plugins)
260 class BasePlugin(object):
262 #: A short name for the plugin based on the implemented specification.
263 #: For example, a plugin for XEP-0030 would use `'xep_0030'`.
264 name = ''
266 #: A longer name for the plugin, describing its purpose. For example,
267 #: a plugin for XEP-0030 would use `'Service Discovery'` as its
268 #: description value.
269 description = ''
271 #: Some plugins may depend on others in order to function properly.
272 #: Any plugin names included in :attr:`~BasePlugin.dependencies` will
273 #: be initialized as needed if this plugin is enabled.
274 dependencies = set()
276 #: The basic, standard configuration for the plugin, which may
277 #: be overridden when initializing the plugin. The configuration
278 #: fields included here may be accessed directly as attributes of
279 #: the plugin. For example, including the configuration field 'foo'
280 #: would mean accessing `plugin.foo` returns the current value of
281 #: `plugin.config['foo']`.
282 default_config = {}
284 def __init__(self, xmpp, config=None):
285 self.xmpp = xmpp
286 if self.xmpp:
287 self.api = self.xmpp.api.wrap(self.name)
289 #: A plugin's behaviour may be configurable, in which case those
290 #: configuration settings will be provided as a dictionary.
291 self.config = copy.copy(self.default_config)
292 if config:
293 self.config.update(config)
295 def __getattr__(self, key):
296 """Provide direct access to configuration fields.
298 If the standard configuration includes the option `'foo'`, then
299 accessing `self.foo` should be the same as `self.config['foo']`.
301 if key in self.default_config:
302 return self.config.get(key, None)
303 else:
304 return object.__getattribute__(self, key)
306 def __setattr__(self, key, value):
307 """Provide direct assignment to configuration fields.
309 If the standard configuration includes the option `'foo'`, then
310 assigning to `self.foo` should be the same as assigning to
311 `self.config['foo']`.
313 if key in self.default_config:
314 self.config[key] = value
315 else:
316 super(BasePlugin, self).__setattr__(key, value)
318 def _init(self):
319 """Initialize plugin state, such as registering event handlers.
321 Also sets up required event handlers.
323 if self.xmpp is not None:
324 self.xmpp.add_event_handler('session_bind', self.session_bind)
325 if self.xmpp.session_bind_event.is_set():
326 self.session_bind(self.xmpp.boundjid.full)
327 self.plugin_init()
328 log.debug('Loaded Plugin: %s', self.description)
330 def _end(self):
331 """Cleanup plugin state, and prepare for plugin removal.
333 Also removes required event handlers.
335 if self.xmpp is not None:
336 self.xmpp.del_event_handler('session_bind', self.session_bind)
337 self.plugin_end()
338 log.debug('Disabled Plugin: %s' % self.description)
340 def plugin_init(self):
341 """Initialize plugin state, such as registering event handlers."""
342 pass
344 def plugin_end(self):
345 """Cleanup plugin state, and prepare for plugin removal."""
346 pass
348 def session_bind(self, jid):
349 """Initialize plugin state based on the bound JID."""
350 pass
352 def post_init(self):
353 """Initialize any cross-plugin state.
355 Only needed if the plugin has circular dependencies.
357 pass
360 base_plugin = BasePlugin