~ubuntu-branches/ubuntu/precise/pida/precise

« back to all changes in this revision

Viewing changes to pida/core/plugins.py

  • Committer: Bazaar Package Importer
  • Author(s): Jan Luebbe
  • Date: 2007-04-17 16:08:06 UTC
  • mfrom: (1.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20070417160806-3ttlb6igf94x9i03
Tags: 0.4.4-1
* New upstream release (closes: #419129)
* Add dependency on python-glade2 (closes: #418716)
* Update copyright

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""A flexible plugin framework.
 
2
 
 
3
Clear your mind of any previously defined concept of a plugin.
 
4
 
 
5
Key components:
 
6
 
 
7
    * Registry: stores a set of plugins
 
8
    * Plugin: defines a set of behaviours
 
9
    * Registry key: unique behavioural identifier
 
10
 
 
11
Types of definable behaviour:
 
12
 
 
13
    1. Singleton
 
14
    2. Feature
 
15
    3. Extension Point/Extender
 
16
 
 
17
A plugin can register any number of the above behaviour
 
18
types.
 
19
 
 
20
1. Singleton
 
21
 
 
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.
 
27
 
 
28
2. Feature
 
29
 
 
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.
 
34
 
 
35
3. Extension point
 
36
 
 
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).
 
44
 
 
45
Defining Plugins:
 
46
 
 
47
1. Singletons
 
48
 
 
49
a. First you will need a registry item:
 
50
 
 
51
    reg = Registry()
 
52
 
 
53
b. now define a behavioural interface:
 
54
 
 
55
    class IAmYellow(Interface):
 
56
        def get_shade():
 
57
            "get the shade of yellow"
 
58
 
 
59
c. now write a class that implements this behaviour:
 
60
 
 
61
    class Banana(object):
 
62
        def get_shade(self):
 
63
            return 'light and greeny'
 
64
 
 
65
d. create an instance of the plugin
 
66
 
 
67
    plugin = Banana()
 
68
 
 
69
e. register it with the registry:
 
70
 
 
71
    reg.register_plugin(
 
72
            instance=plugin,
 
73
            singletons=(IAmYellow,)
 
74
        )
 
75
 
 
76
f. get the item from the registry at a later time:
 
77
 
 
78
    plugin = reg.get_singleton(IAmYellow)
 
79
    print plugin.get_shade()
 
80
 
 
81
Things to note:
 
82
 
 
83
    * Attempting to register another plugin with a singleton of IAmYellow will
 
84
      fail.
 
85
 
 
86
    * Looking up a non-existent singleton will raise a SingletonError.
 
87
 
 
88
 
 
89
"""
 
90
 
 
91
import weakref
 
92
 
 
93
##############################################################################
 
94
## Core data types
 
95
 
 
96
def copy_docs(cls):
 
97
    def decorator(func):
 
98
        func.__doc__ = getattr(cls, func.__name__).__doc__
 
99
        return func
 
100
    return decorator
 
101
    
 
102
class NamedSets(object):
 
103
    """
 
104
    The theory of the plugin architecture has its foundations
 
105
    on this simple structure which is a simple collection of named sets.
 
106
    
 
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.
 
109
    """
 
110
    def __getitem__(self, name):
 
111
        """
 
112
        Returns the named set.
 
113
        
 
114
        @param name: the name of the set
 
115
        @return: an iterator to the named set.
 
116
        """
 
117
        raise NotImplementedError
 
118
    
 
119
    def add(self, name, value):
 
120
        """
 
121
        Add one one value to the named set.
 
122
        
 
123
        @param name: the name of the set
 
124
        @param value: the value to be added to the set
 
125
        """
 
126
        raise NotImplementedError
 
127
    
 
128
    def remove(self, name, value):
 
129
        """
 
130
        Remove the `value` from the set named `name`.
 
131
        
 
132
        @param name: the name of the set to remove the value from
 
133
        @param value: the value to remove from the named set
 
134
        """
 
135
        raise NotImplementedError
 
136
        
 
137
    def keys(self):
 
138
        """
 
139
        Return a collection of the names of the existing sets.
 
140
        """
 
141
        return self.data.keys()
 
142
    
 
143
    names = keys
 
144
    
 
145
    def __delitem__(self, name):
 
146
        """
 
147
        Remove the named set.
 
148
        
 
149
        @param name: the name of the set to be removed.
 
150
        """
 
151
        del self.data[name]
 
152
 
 
153
    @copy_docs(list)
 
154
    def __repr__(self):
 
155
        return repr(self.data)
 
156
 
 
157
    @copy_docs(list)
 
158
    def __len__(self):
 
159
        return len(self.data)
 
160
 
 
161
    @copy_docs(list)
 
162
    def __iter__(self):
 
163
        return iter(self.data)
 
164
 
 
165
 
 
166
 
 
167
class StrictNamedSets(NamedSets):
 
168
    """
 
169
    A strict named sets is a `NamedSets` that has fixed predefined sets.
 
170
    
 
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`.
 
174
    """
 
175
    def __init__(self, names=()):
 
176
        """
 
177
        Creates a strict named sets by providing an optional number of keys
 
178
        to define.
 
179
        
 
180
        @param names: the sets to initialize.
 
181
        """
 
182
        self.data = {}
 
183
        [self.init_set(name) for name in names]
 
184
        
 
185
 
 
186
    def init_set(self, name):
 
187
        """
 
188
        Initializes a certain set.
 
189
        
 
190
        pre-condition: key not in self.data
 
191
        
 
192
        @param name: the name of the set
 
193
        """
 
194
        val = self.data[name] = set()
 
195
        return val
 
196
 
 
197
    @copy_docs(NamedSets)
 
198
    def __getitem__(self, name):
 
199
        return self.data[name]
 
200
        
 
201
    @copy_docs(NamedSets)
 
202
    def add(self, key, value):
 
203
        self.data[key].add(value)
 
204
 
 
205
    @copy_docs(NamedSets)
 
206
    def remove(self, key, value):
 
207
        return self.data[key].remove(value)
 
208
 
 
209
 
 
210
class DynamicNamedSets(NamedSets):
 
211
    """
 
212
    In a dynamic named set the sets are created (empty sets) when you access
 
213
    them.
 
214
    """
 
215
    
 
216
    def __init__(self):
 
217
        """Creates an empty dynamic named sets object."""
 
218
        self.data = {}
 
219
        
 
220
    @copy_docs(NamedSets)
 
221
    def __getitem__(self, key):
 
222
        return self.data.get(key, ())
 
223
    
 
224
    @copy_docs(NamedSets)
 
225
    def remove(self, key, value):
 
226
        try:
 
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:
 
231
                del self.data[key]
 
232
            return value
 
233
            
 
234
        except KeyError:
 
235
            pass
 
236
    
 
237
    @copy_docs(NamedSets)
 
238
    def add(self, key, value):
 
239
        try:
 
240
            vals = self.data[key]
 
241
        except KeyError:
 
242
            vals = self.data[key] = set()
 
243
            
 
244
        vals.add(value)
 
245
 
 
246
    @copy_docs(NamedSets)
 
247
    def __delitem__(self, key):
 
248
        try:
 
249
            del self.data[key]
 
250
        except KeyError:
 
251
            pass
 
252
 
 
253
##############################################################################
 
254
## Base classes
 
255
 
 
256
class _PluginIterator(object):
 
257
    def __init__(self, registry, real_iter, *args, **kwargs):
 
258
        self.registry = registry
 
259
        self.real_iter = real_iter
 
260
        self.args = args
 
261
        self.kwargs = kwargs
 
262
    
 
263
    def next(self):
 
264
        plugin = self.real_iter.next()
 
265
        return plugin.get_instance(self.registry, *self.args, **self.kwargs)
 
266
    
 
267
    def __iter__(self):
 
268
        return self
 
269
    
 
270
    def __repr__(self):
 
271
        return repr(self.real_iter)
 
272
 
 
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.
 
279
    
 
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` 
 
284
    method is called.
 
285
    """
 
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)
 
289
            
 
290
        if factory is None:
 
291
            self.instance = instance
 
292
        else:
 
293
            self.factory = factory
 
294
            
 
295
    def get_instance(self, registry):
 
296
        """Returns the object associated with the `Plugin`."""
 
297
        try:
 
298
            return self.instance
 
299
        except AttributeError:
 
300
            self.instance = self.factory(registry)
 
301
            return self.instance
 
302
 
 
303
    def reset(self):
 
304
        """When this plugin contains a factory makes it regen the instance."""
 
305
        del self.instance
 
306
 
 
307
    def unplug(self, registry):
 
308
        """This method is called when the service is removed from the registry"""
 
309
 
 
310
##############################################################################
 
311
## Implementations
 
312
class ExtensionPointError(StandardError):
 
313
    """Raised when there's an error of some sort"""
 
314
    
 
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.
 
320
    
 
321
    In order to access extensions we have to initialize the `ExtensionPoint`
 
322
    by calling the `init_extensions` method.
 
323
    
 
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.
 
327
    
 
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.
 
334
    """
 
335
    def __init__(self):
 
336
        """Creates a new extension point object."""
 
337
        self.lazy = DynamicNamedSets()
 
338
    
 
339
    def _activate(self, extender):
 
340
        """
 
341
        This method is called when the object is placed in an initialized
 
342
        extension.
 
343
        """
 
344
        return extender
 
345
    
 
346
    def init_extensions(self, extension_points):
 
347
        """
 
348
        Initializes the valid extensions.
 
349
        """
 
350
        self.data = StrictNamedSets(extension_points)
 
351
        
 
352
        for ext_pnt in self.lazy:
 
353
            try:
 
354
                for extender in self.lazy[ext_pnt]:
 
355
                    self.data.add(ext_pnt, self._activate(extender))
 
356
                
 
357
            except KeyError:
 
358
                pass
 
359
        del self.lazy
 
360
       
 
361
    @copy_docs(NamedSets)
 
362
    def add(self, name, value):
 
363
        """Adds one more element to the extension point, or named list."""
 
364
        try:
 
365
            self.data.add(name, self._activate(value))
 
366
        except AttributeError:
 
367
            self.lazy.add(name, value)
 
368
 
 
369
    @copy_docs(NamedSets)
 
370
    def __getitem__(self, key):
 
371
        try:
 
372
            return self.data[key]
 
373
        except AttributeError:
 
374
            raise ExtensionPointError("Not initialized, run init() first")
 
375
 
 
376
    get_extension_point = __getitem__
 
377
    
 
378
    def has_init(self):
 
379
        """
 
380
        Verifies if the extension point was already initialized.
 
381
        """
 
382
        return hasattr(self, "data")
 
383
 
 
384
    @copy_docs(NamedSets)
 
385
    def keys(self):
 
386
        try:
 
387
            return self.data.keys()
 
388
        except:
 
389
            raise ExtensionPointError("Not initialized, run init() first")
 
390
        
 
391
 
 
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)
 
397
    
 
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())
 
402
 
 
403
 
 
404
class FactoryDict(object):
 
405
    """
 
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.
 
410
    """
 
411
    
 
412
    def __init__(self, factory):
 
413
        """
 
414
        Creates a `FactoryDict` instance with the appropriate factory
 
415
        function.
 
416
        
 
417
        @param factory: the function that creates objects according to the
 
418
        supplied key.
 
419
        """
 
420
        self.data = {}
 
421
        self.factory = factory
 
422
 
 
423
    @copy_docs(dict)
 
424
    def __getitem__(self, key):
 
425
        try:
 
426
            return self.data[key]
 
427
        except KeyError:
 
428
            val = self.data[key] = self.factory(key)
 
429
            return val
 
430
    
 
431
    @copy_docs(dict)
 
432
    def __delitem__(self, key):
 
433
        try:
 
434
            del self.data[key]
 
435
        except KeyError:
 
436
            pass
 
437
    
 
438
    @copy_docs(dict)
 
439
    def __repr__(self):
 
440
        return repr(self.data)
 
441
 
 
442
##############################################################################
 
443
## Use case of the classes defined above
 
444
 
 
445
class SingletonError(StandardError):
 
446
    """Raised when you there's a problem related to Singletons."""
 
447
 
 
448
class PluginEntry(object):
 
449
    def __init__(self, plugin, features, singletons, extension_points, extends):
 
450
        self.plugin = plugin
 
451
        self.features = list(features)
 
452
        self.singletons = list(singletons)
 
453
        self.extends = dict(extends)
 
454
        self.extension_points = list(extension_points)
 
455
    
 
456
    def get_instance(self, *args, **kwargs):
 
457
        return self.plugin.get_instance(*args, **kwargs)
 
458
 
 
459
 
 
460
class PluginFactoryCreator(object):
 
461
    """
 
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.
 
465
    
 
466
    This class is needed when you need to specify a class that extends from
 
467
    `Plugin`.
 
468
    
 
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
 
473
    """
 
474
    def __init__(self, plugin_factory):
 
475
        self.plugin_factory = plugin_factory
 
476
 
 
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", ())
 
482
            
 
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
 
487
 
 
488
 
 
489
# This is the default factory that uses the class Plugin
 
490
PluginFactory = PluginFactoryCreator(Plugin)
 
491
 
 
492
 
 
493
class Registry(object):
 
494
    def __init__(self, plugin_factory=PluginFactory):
 
495
        self.singletons = {}
 
496
        self.plugins = {}
 
497
        
 
498
        self.plugin_factory = plugin_factory
 
499
        
 
500
        plugin_factory = lambda x: PluginExtensionPoint(self)
 
501
        
 
502
        self.ext_points = FactoryDict(plugin_factory)
 
503
        self.features = DynamicNamedSets()
 
504
        
 
505
    
 
506
    def register(self, plugin, features, singletons, extension_points, extends):
 
507
        """
 
508
        Register a plugin with in features, singletons and extension points.
 
509
        This method should not be handled directly, use 'register_plugin'
 
510
        instead.
 
511
        
 
512
        @param features: the features this plugin is associated with.
 
513
        
 
514
        @param singletons: a list of singletons this plugin is registred to.
 
515
        
 
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
 
518
        extension point.
 
519
        
 
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.
 
522
        """
 
523
        # Check for singletons conflicts
 
524
        # In this case we do not allow overriding an existing Singleton
 
525
        for key in singletons:
 
526
            try:
 
527
                val = self.singletons[key]
 
528
                raise SingletonError(key)
 
529
            except KeyError:
 
530
                pass
 
531
    
 
532
        for key in singletons:
 
533
            self.singletons[key] = plugin
 
534
        
 
535
        for feat in features:
 
536
            self.features.add(feat, plugin)
 
537
        
 
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)
 
541
        
 
542
        extension_points = [name for name, points in extension_points]
 
543
        
 
544
        for holder_id, extension_point in extends:
 
545
            self.ext_points[holder_id].add(extension_point, plugin)
 
546
        
 
547
        
 
548
        self.plugins[plugin] = PluginEntry(plugin, features, singletons,
 
549
                                           extension_points, extends)
 
550
        
 
551
        return plugin
 
552
    
 
553
    def get_plugin_from_singleton(self, singleton):
 
554
        """Returns the plugin associated with this singleton."""
 
555
        try:
 
556
            return self.singletons[singleton]
 
557
        except KeyError:
 
558
            raise SingletonError(singleton)
 
559
    
 
560
    def unregister(self, plugin):
 
561
        """Removes a plugin from the registry."""
 
562
        entry = self.plugins[plugin]
 
563
        
 
564
        for key in entry.singletons:
 
565
            del self.singletons[key]
 
566
            
 
567
        for feat in entry.features:
 
568
            self.features.remove(feat, plugin)
 
569
            
 
570
        for holder_id in entry.extension_points:
 
571
            del self.ext_points[holder_id]
 
572
        
 
573
        for holder_id, ext_pnt in entry.extends.iteritems():
 
574
            self.ext_points[holder_id].remove(ext_pnt, plugin)
 
575
 
 
576
        del self.plugins[plugin]
 
577
        
 
578
        plugin.unplug(self)
 
579
    
 
580
    def register_plugin(self, *args, **kwargs):
 
581
        """Register a new plugin."""
 
582
        return self.register(*self.plugin_factory(*args, **kwargs))
 
583
 
 
584
    def get_features(self, feature, *args, **kwargs):
 
585
        return _PluginIterator(self, iter(self.features[feature]), *args, **kwargs)
 
586
    
 
587
    def get_singleton(self, singleton, *args, **kwargs):
 
588
        return self.get_plugin_from_singleton(singleton).get_instance(self, *args, **kwargs)
 
589
    
 
590
    def get_extension_point(self, holder_id, extension_point):
 
591
        return self.ext_points[holder_id].get_extension_point(extension_point)
 
592
    
 
593
    def get_extension_point_def(self, holder_id):
 
594
        return self.ext_points[holder_id].keys()
 
595
    
 
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)
 
600
    
 
601
    def unregister_singleton(self, singleton):
 
602
        try:
 
603
            plugin = self.singletons.pop(singleton)
 
604
            entry = self.plugins[plugin]
 
605
            entry.singletons.remove(singleton)
 
606
            self._check_plugin(plugin)
 
607
 
 
608
        except KeyError:
 
609
            raise SingletonError(singleton)
 
610
    
 
611
    def unregister_feature(self, feature, plugin):
 
612
        """
 
613
        In order to remove a feature u must have the associated plugin.
 
614
        """
 
615
        self.features.remove(feature, plugin)
 
616
        
 
617
        entry = self.plugins[plugin]
 
618
        entry.features.remove(feature)
 
619
        self._check_plugin(plugin)
 
620
 
 
621
    def __iter__(self):
 
622
        return iter(self.plugins)
 
623
 
 
624
    def clear(self):
 
625
        self.services = {}
 
626
        self.features.clear()
 
627
        for plugin in self.plugins:
 
628
            plugin.singeltons = []
 
629
            plugin.features = []
 
630
            plugin.unplug()
 
631
        self.plugins.clear()
 
632