-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpluginmanager.py
More file actions
598 lines (484 loc) · 23 KB
/
Copy pathpluginmanager.py
File metadata and controls
598 lines (484 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
#!/usr/bin/python
"""
The basic interface and implementation for a plugin system.
Also define the basic mechanism to add functionalities to the base
PluginManager. A few *principles* to follow in this case:
If the new functionalities do not overlap the ones already
implemented, then they must be implemented as a Decorator class of the
base plugin. This should be done by inheriting the
``PluginManagerDecorator``.
If this previous way is not possible, then the functionalities should
be added as a subclass of ``PluginManager``.
The first method is highly prefered since it makes it possible to have
a more flexible design where one can pick several functionalities and
litterally *add* them to get an object corresponding to one's precise
needs.
"""
import sys, os
import logging
import ConfigParser
import types
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT
from mplwidgets import BlankCanvas
# A forbiden string that can later be used to describe lists of
# plugins for instance (see ``ConfigurablePluginManager``)
PLUGIN_NAME_FORBIDEN_STRING=";;"
# prints all logged messages with level debug or higher
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(message)s',
filename='pluginlogger.log',
filemode='w')
class IPlugin(object):
"""Defines the basic interfaces for a plugin.
These interfaces are inherited by the *core* class of a plugin.
The *core* class of a plugin is then the one that will be notified the
activation/deactivation of a plugin via the ``activate/deactivate``
methods.
For simple (near trivial) plugin systems, one can directly use the
following interfaces.
When designing a non-trivial plugin system, one should create new
plugin interfaces that inherit the following interfaces.
"""
def __init__(self):
"""
Set the basic variables.
"""
self.is_activated = False
def activate(self):
"""
Called at plugin activation.
"""
self.is_activated = True
def deactivate(self):
"""
Called when the plugin is disabled.
"""
self.is_activated = False
class DialogPlugin(IPlugin):
"""Defines the interface for a plugin that pops up a dialog with an image"""
def create_window(self, rawframes, img, roi, name, path):
"""Create dialog and image inside it
**Inputs**
* img: 2d-array, containing the image data
* roi: tuple of slices, contains two slice objects, one for each
image axis. The tuple can be used as a 2D slice object.
* name: string, the name of the plugin
"""
self.window = PluginDialog(name)
# make ax more easily accessible
self.ax = self.window.ax
# call the user-implemented functionality
self.main(img[roi])
# show the window
self.window.show()
return self.window
def main(self, img):
"""This method is to be implemented by plugins, they do the work"""
pass
class VerboseDialogPlugin(IPlugin):
"""Defines the interface for a plugin that pops up a dialog with an image"""
def create_window(self, rawframes, img, roi, name, path):
"""Create dialog and image inside it
**Inputs**
* img: 2d-array, containing the image data
* roi: tuple of slices, contains two slice objects, one for each
image axis. The tuple can be used as a 2D slice object.
* name: string, the name of the plugin
"""
self.window = PluginDialog(name)
# make ax more easily accessible
self.ax = self.window.ax
# call the user-implemented functionality
self.main(rawframes, img, roi, name, path)
# show the window
self.window.show()
return self.window
def main(self, rawframes, img, roi, name, path):
"""This method is to be implemented by plugins, they do the work"""
pass
class PluginDialog(QDialog):
"""Handles the window that plugins can pop up"""
def __init__(self, name, parent=None):
super(PluginDialog, self).__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setWindowTitle(name)
layout = QVBoxLayout()
self.fig = BlankCanvas()
self.ax = self.fig.ax
self.toolbar = NavigationToolbar2QT(self.fig, self)
layout.addWidget(self.fig)
layout.addWidget(self.toolbar)
self.setLayout(layout)
class PluginInfo(object):
"""Gather some info about a plugin
This includes name, author,description, etc.
"""
def __init__(self, plugin_name, plugin_path):
"""Set the name and path of the plugin
The default values for other useful variables are set as well.
.. warning:: The ``path`` attribute is the full path to the
plugin if it is organised as a directory or the full path
to a file without the ``.py`` extension if the plugin is
defined by a simple file. In the later case, the actual
plugin is reached via ``plugin_info.path+'.py'``.
"""
self.name = plugin_name
self.path = plugin_path
self.author = "Unknown"
self.version = "?.?"
self.website = "None"
self.copyright = "Unknown"
self.description = ""
self.plugin_object = None
self.category = None
def _getIsActivated(self):
"""Return the activated state of the plugin object.
Makes it possible to define a property.
"""
return self.plugin_object.is_activated
is_activated = property(fget=_getIsActivated)
def setVersion(self, vstring):
"""Set the version of the plugin.
Used by subclasses to provide different handling of the
version number.
"""
self.version = vstring
class PluginManager(object):
"""Manage several plugins by ordering them in several categories.
The mechanism for searching and loading the plugins is already
implemented in this class so that it can be used directly (hence
it can be considered as a bit more than a mere interface)
The file describing a plugin should be written in the sytax
compatible with Python's ConfigParser module as in the following
example::
[Core Information]
Name= My plugin Name
Module=the_name_of_the_plugin_to_load_with_no_py_ending
[Documentation]
Description=What my plugin broadly does
Author= My very own name
Website= My very own website
Version=the_version_number_of_the_plugin
"""
def __init__(self, categories_filter={"Default":IPlugin}, \
directories_list=None, plugin_info_ext="yapsy-plugin"):
"""Initialize PluginManager.
Initialize the mapping of the categories and set the list of
directories where plugins may be. This can also be set by
direct call the methods:
- ``setCategoriesFilter`` for ``categories_filter``
- ``setPluginPlaces`` for ``directories_list``
- ``setPluginInfoExtension`` for ``plugin_info_ext``
You may look at these function's documentation for the meaning
of each corresponding arguments.
"""
self.setPluginInfoClass(PluginInfo)
self.setCategoriesFilter(categories_filter)
self.setPluginPlaces(directories_list)
self.setPluginInfoExtension(plugin_info_ext)
def setCategoriesFilter(self, categories_filter):
"""Set the categories of plugins to be looked for.
The ``categories_filter`` first defines the various categories
in which the plugins will be stored via its keys and it also
defines the interface tha has to be inherited by the actual
plugin class belonging to each category.
"""
self.categories_interfaces = categories_filter.copy()
# prepare the mapping from categories to plugin lists
self.category_mapping = {}
# also maps the plugin info files (useful to avoid loading
# twice the same plugin...)
self._category_file_mapping = {}
for categ in categories_filter.keys():
self.category_mapping[categ] = []
self._category_file_mapping[categ] = []
def setPluginInfoClass(self,picls):
"""Set the class that holds PluginInfo.
The class should inherit from ``PluginInfo``.
"""
self._plugin_info_cls = picls
def getPluginInfoClass(self):
"""Get the class that holds PluginInfo.
The class should inherit from ``PluginInfo``.
"""
return self._plugin_info_cls
def setPluginPlaces(self, directories_list):
"""Set the list of directories where to look for plugin places."""
if directories_list is None:
directories_list = [os.path.dirname(__file__)]
self.plugins_places = directories_list
def setPluginInfoExtension(self,plugin_info_ext):
"""Set the extension that identifies a plugin info file.
The ``plugin_info_ext`` is the extension that will have the
informative files describing the plugins and that are used to
actually detect the presence of a plugin (see
``collectPlugins``).
"""
self.plugin_info_ext = plugin_info_ext
def getCategories(self):
"""Return the list of all categories."""
return self.category_mapping.keys()
def getPluginsOfCategory(self,category_name):
"""Return the list of all plugins belonging to a category."""
return self.category_mapping[category_name]
def _gatherCorePluginInfo(self, directory, filename):
"""Gather the core information about a plugin.
The plugin is described by it's info file (found at
'directory/filename'), the core information is the plugin's
name and module to be loaded.
Return an instance of ``self.plugin_info_cls`` and the
config_parser used to gather the core data *in a tuple*, if the
required info could be localised, else return ``(None,None)``.
.. note:: This is supposed to be used internally by subclasses
and decorators.
"""
# now we can consider the file as a serious candidate
candidate_infofile = os.path.join(directory,filename)
# parse the information file to get info about the plugin
config_parser = ConfigParser.SafeConfigParser()
try:
config_parser.read(candidate_infofile)
except:
logging.debug("Could not parse the plugin file %s" \
%candidate_infofile)
return (None, None)
# check if the basic info is available
if not config_parser.has_section("Core"):
logging.debug("Plugin info file has no 'Core' section (in %s)" \
%candidate_infofile)
return (None, None)
if not config_parser.has_option("Core","Name") or not \
config_parser.has_option("Core","Module"):
logging.debug('Plugin info file has no "Name" or "Module" section (in %s)'\
%candidate_infofile)
return (None, None)
# check that the given name is valid
name = config_parser.get("Core", "Name")
name = name.strip()
if PLUGIN_NAME_FORBIDEN_STRING in name:
logging.debug("Plugin name contains forbiden character: %s (in %s)"\
%(PLUGIN_NAME_FORBIDEN_STRING, candidate_infofile))
return (None, None)
# start collecting essential info
plugin_info = self._plugin_info_cls(name,
os.path.join(directory, \
config_parser.get("Core", "Module")))
return (plugin_info,config_parser)
def gatherBasicPluginInfo(self, directory,filename):
"""Gather some basic documentation about the plugin.
This info is described by the plugin's info file
(found at 'directory/filename').
Return an instance of ``self.plugin_info_cls`` gathering the
required informations.
See also ``self._gatherCorePluginInfo``
"""
plugin_info,config_parser = self._gatherCorePluginInfo(directory, filename)
if plugin_info is None:
return None
# collect additional (but usually quite usefull) information
if config_parser.has_section("Documentation"):
if config_parser.has_option("Documentation","Author"):
plugin_info.author = config_parser.get("Documentation", "Author")
if config_parser.has_option("Documentation","Version"):
plugin_info.setVersion(config_parser.get("Documentation", "Version"))
if config_parser.has_option("Documentation","Website"):
plugin_info.website = config_parser.get("Documentation", "Website")
if config_parser.has_option("Documentation","Copyright"):
plugin_info.copyright = config_parser.get("Documentation", "Copyright")
if config_parser.has_option("Documentation","Description"):
plugin_info.description = config_parser.get("Documentation", "Description")
return plugin_info
def locatePlugins(self):
"""Walk through the plugins' places and look for plugins.
Return the number of plugins found.
"""
#print "%s.locatePlugins" % self.__class__
self._candidates = []
for directory in map(os.path.abspath,self.plugins_places):
# first of all, is it a directory :)
if not os.path.isdir(directory):
logging.debug("%s skips %s (not a directory)" \
%(self.__class__.__name__,directory))
continue
# iteratively walks through the directory
logging.debug("%s walks into directory: %s" \
%(self.__class__.__name__,directory))
for item in os.walk(directory):
dirpath = item[0]
for filename in item[2]:
# eliminate the obvious non plugin files
if not filename.endswith(".%s" % self.plugin_info_ext):
continue
candidate_infofile = os.path.join(dirpath,filename)
logging.debug('%s found a candidate: \
%s' % (self.__class__.__name__, \
candidate_infofile))
plugin_info = self.gatherBasicPluginInfo(dirpath,filename)
if plugin_info is None:
logging.debug("""Candidate rejected:
%s""" % candidate_infofile)
continue
# now determine the path of the file to execute,
# depending on wether the path indicated is a
# directory or a file
if os.path.isdir(plugin_info.path):
candidate_filepath = os.path.join(plugin_info.path, \
"__init__")
elif os.path.isfile(plugin_info.path+".py"):
candidate_filepath = plugin_info.path
else:
continue
self._candidates.append((candidate_infofile, \
candidate_filepath, plugin_info))
return len(self._candidates)
def loadPlugins(self, callback=None):
"""Load the candidate plugins.
These have been identified through a previous call to locatePlugins.
For each plugin candidate look for its category, load it and store it
in the appropriate slot of the ``category_mapping``.
If a callback function is specified, call it before every load
attempt. The ``plugin_info`` instance is passed as an argument to
the callback.
"""
if not hasattr(self, '_candidates'):
raise ValueError("locatePlugins must be called before loadPlugins")
for candidate_infofile, candidate_filepath, plugin_info in self._candidates:
# if a callback exists, call it before attempting to load
# the plugin so that a message can be displayed to the
# user
if callback is not None:
callback(plugin_info)
# now execute the file and get its content into a
# specific dictionnary
candidate_globals = {"__file__":candidate_filepath+".py"}
if "__init__" in os.path.basename(candidate_filepath):
sys.path.append(plugin_info.path)
try:
execfile(candidate_filepath+".py",candidate_globals)
except Exception,e:
logging.debug("Unable to execute the code in plugin: %s" \
%candidate_filepath)
logging.debug("\t The following problem occured: %s %s " \
%(os.linesep, e))
if "__init__" in os.path.basename(candidate_filepath):
sys.path.remove(plugin_info.path)
continue
if "__init__" in os.path.basename(candidate_filepath):
sys.path.remove(plugin_info.path)
# now try to find and initialise the first subclass of the correct
#plugin interface
for element in candidate_globals.values():
current_category = None
for category_name in self.categories_interfaces.keys():
try:
is_correct_subclass = issubclass(element, \
self.categories_interfaces[category_name])
except:
continue
if is_correct_subclass:
if element is not self.categories_interfaces[category_name]:
current_category = category_name
break
if current_category is not None:
if not (candidate_infofile in self._category_file_mapping[current_category]):
# we found a new plugin: initialise it and search for the next one
plugin_info.plugin_object = element()
plugin_info.category = current_category
self.category_mapping[current_category].append(plugin_info)
self._category_file_mapping[current_category].append(candidate_infofile)
current_category = None
break
# Remove candidates list since we don't need them any more and
# don't need to take up the space
delattr(self, '_candidates')
def collectPlugins(self):
"""Walk through the plugins' places and look for plugins.
Then for each plugin candidate look for its category, load it and
stores it in the appropriate slot of the category_mapping.
"""
self.locatePlugins()
self.loadPlugins()
def getPluginByName(self,name,category="Default"):
"""Get the plugin corresponding to a given category and name"""
if self.category_mapping.has_key(category):
for item in self.category_mapping[category]:
if item.name == name:
return item
return None
def activatePluginByName(self,name,category="Default"):
"""Activate a plugin corresponding to a given category + name."""
pta_item = self.getPluginByName(name,category)
if pta_item is not None:
plugin_to_activate = pta_item.plugin_object
if plugin_to_activate is not None:
logging.debug("Activating plugin: %s.%s"% (category,name))
plugin_to_activate.activate()
return plugin_to_activate
return None
def deactivatePluginByName(self,name,category="Default"):
"""Deactivate a plugin corresponding to a given category + name."""
if self.category_mapping.has_key(category):
plugin_to_deactivate = None
for item in self.category_mapping[category]:
if item.name == name:
plugin_to_deactivate = item.plugin_object
break
if plugin_to_deactivate is not None:
logging.debug("Deactivating plugin: %s.%s"% (category,name))
plugin_to_deactivate.deactivate()
return plugin_to_deactivate
return None
class PluginManagerDecorator(object):
"""Make it possible to add several responsibilities to a plugin manager.
This can de done in a more flexible way than by mere
subclassing. This is an implementation of the Decorator Design Patterns.
There is also an additional mechanism that allows for the
automatic creation of the object to be decorated when this object
is an instance of PluginManager (and not an instance of its
subclasses). This way we can keep the plugin managers creation
simple when the user don't want to mix a lot of 'enhancements' on
the base class.
"""
def __init__(self,decorated_object=None,
# The following args will only be used if we need to
# create a default PluginManager
categories_filter={"Default":IPlugin},
directories_list=[os.path.dirname(__file__)],
plugin_info_ext="yapsy-plugin"):
"""Mimics the PluginManager's __init__ method and wraps an
instance of this class into this decorator class.
- *If the decorated_object is not specified*, then we use the
PluginManager class to create the 'base' manager, and to do
so we will use the arguments: ``categories_filter``,
``directories_list``, and ``plugin_info_ext`` or their
default value if they are not given.
- *If the decorated object is given*, these last arguments are
simply **ignored** !
All classes (and especially subclasses of this one) that want
to be a decorator must accept the decorated manager as an
object passed to the init function under the exact keyword
``decorated_object``.
"""
if decorated_object is None:
logging.debug("Creating a default PluginManager instance to be decorated.")
decorated_object = PluginManager(categories_filter,
directories_list,
plugin_info_ext)
self._component = decorated_object
def __getattr__(self,name):
"""Decorator trick copied from:
http://www.pasteur.fr/formation/infobio/python/ch18s06.html
"""
return getattr(self._component,name)
def collectPlugins(self):
"""This function will usually be a shortcut.
Successively calls to ``self.locatePlugins`` and then
``self.loadPlugins`` can be made, which are
very likely to be redefined in each new decorator.
So in order for this to keep on being a "shortcut" and not a
real pain, I'm redefining it here.
"""
self.locatePlugins()
self.loadPlugins()