1
"""A flexible plugin framework.
3
Clear your mind of any previously defined concept of a plugin.
7
* Registry: stores a set of plugins
8
* Plugin: defines a set of behaviours
9
* Registry key: unique behavioural identifier
11
Types of definable behaviour:
15
3. Extension Point/Extender
17
A plugin can register any number of the above behaviour
22
When a plugin registers as a singleton for a key, it is saying "I provide the
23
behaviour", so when the registry is looked up for that key, the object is
24
returned. At this point, please consider that an ideal registry key may be an
25
Interface definition (formal or otherwise), so when you ask for the behaviour
26
by interface you are actually returned an object implementing that interface.
30
When a plugin defines a Feature, it is again saying "I provide the behaviour",
31
the difference with singleton is that many plugins can define a feature, and
32
these plugins are aggregated and can be looked up by registry key. The look up
33
returns a list of objects that claim to provide the key.
37
An extension point is identical to a feature except that the keys for it must
38
be predefined and are fixed. While a plugin may invent a feature and others
39
can join it, it is expected that whatever creates the registry formally
40
defines the extension points and they are then fixed. This can be used to
41
simulate the behaviour of traditional (Eclipse or Trac) extension points. The
42
plugin itself supplies the Extender (that which extends), while the registry
43
contains the Extension point itself (that which is to be extended).
49
a. First you will need a registry item:
53
b. now define a behavioural interface:
55
class IAmYellow(Interface):
57
"get the shade of yellow"
59
c. now write a class that implements this behaviour:
63
return 'light and greeny'
65
d. create an instance of the plugin
69
e. register it with the registry:
73
singletons=(IAmYellow,)
76
f. get the item from the registry at a later time:
78
plugin = reg.get_singleton(IAmYellow)
79
print plugin.get_shade()
83
* Attempting to register another plugin with a singleton of IAmYellow will
86
* Looking up a non-existent singleton will raise a SingletonError.
93
##############################################################################
98
func.__doc__ = getattr(cls, func.__name__).__doc__
102
class NamedSets(object):
104
The theory of the plugin architecture has its foundations
105
on this simple structure which is a simple collection of named sets.
107
Each key is associated to a set and the available operations are: to add
108
elements to the named set or to remove them.
110
def __getitem__(self, name):
112
Returns the named set.
114
@param name: the name of the set
115
@return: an iterator to the named set.
117
raise NotImplementedError
119
def add(self, name, value):
121
Add one one value to the named set.
123
@param name: the name of the set
124
@param value: the value to be added to the set
126
raise NotImplementedError
128
def remove(self, name, value):
130
Remove the `value` from the set named `name`.
132
@param name: the name of the set to remove the value from
133
@param value: the value to remove from the named set
135
raise NotImplementedError
139
Return a collection of the names of the existing sets.
141
return self.data.keys()
145
def __delitem__(self, name):
147
Remove the named set.
149
@param name: the name of the set to be removed.
155
return repr(self.data)
159
return len(self.data)
163
return iter(self.data)
167
class StrictNamedSets(NamedSets):
169
A strict named sets is a `NamedSets` that has fixed predefined sets.
171
In order to access a set, for adding or removing elements, you must
172
initialize it first. Trying to perform an operation on a undefined named
173
set will result in a `KeyError`.
175
def __init__(self, names=()):
177
Creates a strict named sets by providing an optional number of keys
180
@param names: the sets to initialize.
183
[self.init_set(name) for name in names]
186
def init_set(self, name):
188
Initializes a certain set.
190
pre-condition: key not in self.data
192
@param name: the name of the set
194
val = self.data[name] = set()
197
@copy_docs(NamedSets)
198
def __getitem__(self, name):
199
return self.data[name]
201
@copy_docs(NamedSets)
202
def add(self, key, value):
203
self.data[key].add(value)
205
@copy_docs(NamedSets)
206
def remove(self, key, value):
207
return self.data[key].remove(value)
210
class DynamicNamedSets(NamedSets):
212
In a dynamic named set the sets are created (empty sets) when you access
217
"""Creates an empty dynamic named sets object."""
220
@copy_docs(NamedSets)
221
def __getitem__(self, key):
222
return self.data.get(key, ())
224
@copy_docs(NamedSets)
225
def remove(self, key, value):
227
named_set = self.data[key]
228
value = named_set.remove(value)
229
# remove the set when it's empty.
230
if len(named_set) == 0:
237
@copy_docs(NamedSets)
238
def add(self, key, value):
240
vals = self.data[key]
242
vals = self.data[key] = set()
246
@copy_docs(NamedSets)
247
def __delitem__(self, key):
253
##############################################################################
256
class _PluginIterator(object):
257
def __init__(self, registry, real_iter, *args, **kwargs):
258
self.registry = registry
259
self.real_iter = real_iter
264
plugin = self.real_iter.next()
265
return plugin.get_instance(self.registry, *self.args, **self.kwargs)
271
return repr(self.real_iter)
273
class Plugin(object):
274
"""A possible implementation of a Plugin. A plugin holds an object.
275
When the 'get_instance' method is called, by suplying a registry, the
276
held object is returned. If you extend `Plugin` you can change this
277
by suplying one instance for an appropriate registry, or generating an
278
instance every time the method is called.
280
You can create a plugin's instance by issuing an `instance` or a `factory`
281
function. The factory function receives an argument, the context registry
282
and returns the object this plugin holds. If you use the factory it is
283
called only once, to set the holded object, when the `get_instance`
286
def __init__(self, instance=None, factory=None):
287
if factory is not None and not callable(factory):
288
raise TypeError("If you specify a factory it must be a callable object.", factory)
291
self.instance = instance
293
self.factory = factory
295
def get_instance(self, registry):
296
"""Returns the object associated with the `Plugin`."""
299
except AttributeError:
300
self.instance = self.factory(registry)
304
"""When this plugin contains a factory makes it regen the instance."""
307
def unplug(self, registry):
308
"""This method is called when the service is removed from the registry"""
310
##############################################################################
312
class ExtensionPointError(StandardError):
313
"""Raised when there's an error of some sort"""
315
class ExtensionPoint(object):
316
"""This class is based on Eclipse's plugin architecture. An extension
317
point is a class for defining a number of named sets, we'll address each
318
named list an extension. Conceptually an `ExtensionPoint` is a special
319
case of a `NamedList`, they have an equal interface.
321
In order to access extensions we have to initialize the `ExtensionPoint`
322
by calling the `init_extensions` method.
324
Before initializing the `ExtensionPoint` we can add objects in any
325
extensions. Objects added before initialization that are contained in an
326
extension not initialized will be silentely discarded.
328
After the `ExtensionPoint` is initialized, when objects are added to an
329
extension, they are activated, calling the protected method `_activate`.
330
The `_activate` method can be create to mutate objects when they are
331
inserted into the extension. Objects added to extensions before the
332
`ExtensionPoint` is initialized are only activated when the
333
`init_extensions` method is called.
336
"""Creates a new extension point object."""
337
self.lazy = DynamicNamedSets()
339
def _activate(self, extender):
341
This method is called when the object is placed in an initialized
346
def init_extensions(self, extension_points):
348
Initializes the valid extensions.
350
self.data = StrictNamedSets(extension_points)
352
for ext_pnt in self.lazy:
354
for extender in self.lazy[ext_pnt]:
355
self.data.add(ext_pnt, self._activate(extender))
361
@copy_docs(NamedSets)
362
def add(self, name, value):
363
"""Adds one more element to the extension point, or named list."""
365
self.data.add(name, self._activate(value))
366
except AttributeError:
367
self.lazy.add(name, value)
369
@copy_docs(NamedSets)
370
def __getitem__(self, key):
372
return self.data[key]
373
except AttributeError:
374
raise ExtensionPointError("Not initialized, run init() first")
376
get_extension_point = __getitem__
380
Verifies if the extension point was already initialized.
382
return hasattr(self, "data")
384
@copy_docs(NamedSets)
387
return self.data.keys()
389
raise ExtensionPointError("Not initialized, run init() first")
392
class PluginExtensionPoint(ExtensionPoint):
393
"""This is an `ExtensionPoint` prepared to hold `Plugin`s."""
394
def __init__(self, registry):
395
self._registry = weakref.ref(registry)
396
ExtensionPoint.__init__(self)
398
@copy_docs(ExtensionPoint)
399
def _activate(self, plugin):
400
# in this case we want to hold the actual instance and not the plugin
401
return plugin.get_instance(self._registry())
404
class FactoryDict(object):
406
A factory dict is a dictionary that creates objects, once, when they
407
are first accessed from a factory supplied at runtime.
408
The factory accepts one argument, the suplied key, and generates an object
409
to be held on the dictionary.
412
def __init__(self, factory):
414
Creates a `FactoryDict` instance with the appropriate factory
417
@param factory: the function that creates objects according to the
421
self.factory = factory
424
def __getitem__(self, key):
426
return self.data[key]
428
val = self.data[key] = self.factory(key)
432
def __delitem__(self, key):
440
return repr(self.data)
442
##############################################################################
443
## Use case of the classes defined above
445
class SingletonError(StandardError):
446
"""Raised when you there's a problem related to Singletons."""
448
class PluginEntry(object):
449
def __init__(self, plugin, features, singletons, extension_points, extends):
451
self.features = list(features)
452
self.singletons = list(singletons)
453
self.extends = dict(extends)
454
self.extension_points = list(extension_points)
456
def get_instance(self, *args, **kwargs):
457
return self.plugin.get_instance(*args, **kwargs)
460
class PluginFactoryCreator(object):
462
This is a factory of plugin factories.
463
Instances of this class are the factories needed on `Registry.register`,
464
where the only thing you change is the actual `Plugin` factory.
466
This class is needed when you need to specify a class that extends from
469
@param singletons: the singletons where the plugin will be registred
470
@param features: the features where the plugin will be registred
471
@param extends: the extension points the plugin will be registred
472
@param extension_points: the extension points this plugins defines
474
def __init__(self, plugin_factory):
475
self.plugin_factory = plugin_factory
477
def __call__(self, **kwargs):
478
singletons = kwargs.pop("singletons", ())
479
features = kwargs.pop("features", ())
480
extends = kwargs.pop("extends", ())
481
extension_points = kwargs.pop("extension_points", ())
483
if len(singletons) == len(features) == 0:
484
raise TypeError("You must specify at least one feature or one singleton key")
485
plugin = self.plugin_factory(**kwargs)
486
return plugin, features, singletons, extension_points, extends
489
# This is the default factory that uses the class Plugin
490
PluginFactory = PluginFactoryCreator(Plugin)
493
class Registry(object):
494
def __init__(self, plugin_factory=PluginFactory):
498
self.plugin_factory = plugin_factory
500
plugin_factory = lambda x: PluginExtensionPoint(self)
502
self.ext_points = FactoryDict(plugin_factory)
503
self.features = DynamicNamedSets()
506
def register(self, plugin, features, singletons, extension_points, extends):
508
Register a plugin with in features, singletons and extension points.
509
This method should not be handled directly, use 'register_plugin'
512
@param features: the features this plugin is associated with.
514
@param singletons: a list of singletons this plugin is registred to.
516
@param extension_points: a list of a tuple of two elements: the name
517
of the extension point and the extension points defined on that
520
@param extends: a list of a tuple of two elements: the name of an
521
extension point and the extension it should be registred.
523
# Check for singletons conflicts
524
# In this case we do not allow overriding an existing Singleton
525
for key in singletons:
527
val = self.singletons[key]
528
raise SingletonError(key)
532
for key in singletons:
533
self.singletons[key] = plugin
535
for feat in features:
536
self.features.add(feat, plugin)
538
# initialize all the extensions in each extension point
539
for holder_id, points in extension_points:
540
self.ext_points[holder_id].init_extensions(points)
542
extension_points = [name for name, points in extension_points]
544
for holder_id, extension_point in extends:
545
self.ext_points[holder_id].add(extension_point, plugin)
548
self.plugins[plugin] = PluginEntry(plugin, features, singletons,
549
extension_points, extends)
553
def get_plugin_from_singleton(self, singleton):
554
"""Returns the plugin associated with this singleton."""
556
return self.singletons[singleton]
558
raise SingletonError(singleton)
560
def unregister(self, plugin):
561
"""Removes a plugin from the registry."""
562
entry = self.plugins[plugin]
564
for key in entry.singletons:
565
del self.singletons[key]
567
for feat in entry.features:
568
self.features.remove(feat, plugin)
570
for holder_id in entry.extension_points:
571
del self.ext_points[holder_id]
573
for holder_id, ext_pnt in entry.extends.iteritems():
574
self.ext_points[holder_id].remove(ext_pnt, plugin)
576
del self.plugins[plugin]
580
def register_plugin(self, *args, **kwargs):
581
"""Register a new plugin."""
582
return self.register(*self.plugin_factory(*args, **kwargs))
584
def get_features(self, feature, *args, **kwargs):
585
return _PluginIterator(self, iter(self.features[feature]), *args, **kwargs)
587
def get_singleton(self, singleton, *args, **kwargs):
588
return self.get_plugin_from_singleton(singleton).get_instance(self, *args, **kwargs)
590
def get_extension_point(self, holder_id, extension_point):
591
return self.ext_points[holder_id].get_extension_point(extension_point)
593
def get_extension_point_def(self, holder_id):
594
return self.ext_points[holder_id].keys()
596
def _check_plugin(self, plugin):
597
entry = self.plugins[plugin]
598
if len(entry.features) == 0 and len(entry.singletons) == 0:
599
self.unregister(plugin)
601
def unregister_singleton(self, singleton):
603
plugin = self.singletons.pop(singleton)
604
entry = self.plugins[plugin]
605
entry.singletons.remove(singleton)
606
self._check_plugin(plugin)
609
raise SingletonError(singleton)
611
def unregister_feature(self, feature, plugin):
613
In order to remove a feature u must have the associated plugin.
615
self.features.remove(feature, plugin)
617
entry = self.plugins[plugin]
618
entry.features.remove(feature)
619
self._check_plugin(plugin)
622
return iter(self.plugins)
626
self.features.clear()
627
for plugin in self.plugins:
628
plugin.singeltons = []