root/branches/stable-2.1/lib/pwiki/PluginManager.py @ 275

Revision 275, 16.0 kB (checked in by mbutscher, 4 years ago)

branches/stable-2.1:
* Internal: Bug fixed: Error in error handling for plugins
* Internal: Bug fixed: Some functions in plugins were not registered

for calling

* Bug fixed: WikidPad can't be closed if volume access lost during session

branches/mbutscher/work:
* Internal: Bug fixed: Error in error handling for plugins
* Internal: Bug fixed: Some functions in plugins were not registered

for calling

* Grammar change to forbid bold and italics to span over a heading
* Bug fixed: WikidPad can't be closed if volume access lost during session

Line 
1from __future__ import with_statement
2
3import os, sys, traceback, os.path, imp
4
5# sys.path.append(ur"C:\Daten\Projekte\Wikidpad\Next20\extensions")
6
7import wx
8
9import Utilities
10
11from .StringOps import mbcsEnc, pathEnc
12
13
14
15
16"""The PluginManager and PluginAPI classes implement a generic plugin framework.
17   Plugin apis are created by the PluginManager and can be used to call all
18   installed plugins for that api at once. The PluginManager loads plugins from
19   specified directories and manages them.
20   
21   Example code:
22   
23   pm = PluginManager(["dir1, "dir2"])
24   api = pm.registerSimplePluginAPI(("myAPI",1), ["init", "call1", "call2", "exit"])
25   pm.loadPlugins(["notloadme", "andnotme"])
26   api.init("hello")
27   api.exit()
28   
29   Plugin modules must expose the member WIKIDPAD_PLUGIN to be valid plugins.
30   Otherwise the PluginManager will not register them. Moreover WIKIDPAD_PLUGIN
31   must be a sequence container type that contains the descriptors for the apis
32   it implements. For example in the above case a valid module would have the
33   following member:
34   
35   WIKIDPAD_PLUGIN = (("myAPI",1),)
36   
37   or
38   
39   WIKIDPAD_PLUGIN = [("myAPI",1)]
40   
41   then it defines all functions of the api it supports
42   
43   def init():
44       pass
45   
46   def call1():
47       pass
48       
49   ...
50   
51   Descriptors are searched for with 'in' so any non-mutable instance should work.
52   
53   Plugin functions also can return values. The api wrapper object will collect
54   all return values and return a list of these. If you know that there will be
55   only one return value, you can unpack it directly to a variable with the
56   following syntax:
57   a, = api.call1()
58   """
59
60
61class SimplePluginAPI(object):
62    """encapsulates a single plugin api and stores the functions of various
63       plugins implementing that api. It takes a unique descriptor and a list
64       of function names at creation time. For each function name a member is
65       created that calls the registered plugin functions. The descriptor must
66       appear in the WIKIDPAD_PLUGIN sequence object of the any module to have
67       the module registered. After that the module just has to implement a
68       subset of the api's functions to be registered."""
69
70    def __init__(self, descriptor, functions):
71        self.descriptor = descriptor
72        self._functionNames = functions
73        self._plugins = {}
74        for f in self._functionNames:
75            pluginlist = []
76            self._plugins[f] = pluginlist
77            helper = self._createHelper( pluginlist )
78            setattr(self,f, helper)
79
80    def getFunctionNames(self):
81        return self._functionNames
82
83    def hasFunctionName(self, fctName):
84        return fctName in self._functionNames
85
86
87    @staticmethod
88    def _createHelper(funcList):
89        return lambda *args, **kwargs: [fun(*args, **kwargs) for fun in funcList]
90
91
92    def registerModule(self, module):
93        registered = False
94        if self.descriptor in module.WIKIDPAD_PLUGIN:
95            for f in self._functionNames:
96                if hasattr(module, f):
97                    self._plugins[f].append(getattr(module,f))
98                    registered = True
99            if not registered:
100                sys.stderr.write("plugin " + module.__name__ + " exposes " +
101                        str(self.descriptor) +
102                        " but does not support any interface methods!")
103        return registered
104
105#     def deleteModule(self, module):
106#         for f in self._functionNames:
107#             if hasattr(module, f):
108#                 self._plugins[f].remove(getattr(module,f))
109
110
111class WrappedPluginAPI(object):
112    """
113    Constructor takes as keyword arguments after descriptor names.
114   
115    The keys of the arguments are the function names exposed as attributes by
116    the API object. The values can be either:
117        None  to call function of same name in module(s) as SimplePluginAPI
118            does
119        a string  to call function of this name in module(s)
120        a wrapper function  to call with module object and parameters from
121                original function call
122    """
123
124    def __init__(self, descriptor, **wrappedFunctions):
125        self.descriptor = descriptor
126        self._functionNames = wrappedFunctions.keys()
127        self._wrappedFunctions = wrappedFunctions
128        self._plugins = {}
129        for f in self._functionNames:
130            pluginlist = [] # List containing either modules if wrappedFunctions[f]
131                    # is not None or functions if wrappedFunctions[f] is None
132            self._plugins[f] = pluginlist
133            helper = self._createHelper(wrappedFunctions[f], pluginlist)
134            setattr(self,f, helper)
135           
136    def getFunctionNames(self):
137        return self._functionNames
138
139    def hasFunctionName(self, fctName):
140        return fctName in self._functionNames
141
142
143    @staticmethod
144    def _createHelper(wrapFct, list):
145        if wrapFct is None or isinstance(wrapFct, (str, unicode)):
146            return lambda *args, **kwargs: [fun(*args, **kwargs) for fun in list]
147        else:
148            return lambda *args, **kwargs: [wrapFct(module, *args, **kwargs)
149                    for module in list]
150
151
152    def registerModule(self, module):
153        if not self.descriptor in module.WIKIDPAD_PLUGIN:
154            return False
155
156        registered = False
157        for f in self._functionNames:
158            if self._wrappedFunctions[f] is None:
159                if hasattr(module, f):
160                    self._plugins[f].append(getattr(module,f))
161                    registered = True
162            elif isinstance(self._wrappedFunctions[f], (str, unicode)):
163                realF = self._wrappedFunctions[f]
164                if hasattr(module, realF):
165                    self._plugins[f].append(getattr(module,realF))
166                    registered = True
167            else:
168                self._plugins[f].append(module)
169                # An internal wrapper function doesn't count as "registered"
170#                 registered = True
171
172        if not registered:
173            sys.stderr.write("plugin " + module.__name__ + " exposes " +
174                    str(self.descriptor) +
175                    " but does not support any interface methods!")
176
177        return registered
178
179
180
181class PluginAPIAggregation(object):
182    def __init__(self, *apis):
183        self._apis = apis
184
185        fctNames = set()
186        for api in self._apis:
187            fctNames.update(api.getFunctionNames())
188       
189        for f in list(fctNames):
190            funcList = [getattr(api, f) for api in apis if api.hasFunctionName(f)]
191            setattr(self, f, PluginAPIAggregation._createHelper(funcList))
192
193
194    @staticmethod
195    def _createHelper(funcList):
196        return lambda *args, **kwargs: reduce(lambda a, b: a+list(b),
197                [fun(*args, **kwargs) for fun in funcList])
198
199
200
201
202
203class PluginManager(object):
204    """manages all PluginAPIs and plugins."""
205    def __init__(self, directories):
206        self.pluginAPIs = {}  # Dictionary {<type name>:<verReg dict>}
207                # where verReg dict is list of tuples (<version No>:<PluginAPI instance>)
208        self.plugins = {} 
209        self.directories = directories
210       
211    def registerSimplePluginAPI(self, descriptor, functions):
212        api = SimplePluginAPI(descriptor, functions)
213        self.pluginAPIs[descriptor] = api
214        return api
215
216
217    def registerWrappedPluginAPI(self, descriptor, **wrappedFunctions):
218        api = WrappedPluginAPI(descriptor, **wrappedFunctions)
219        self.pluginAPIs[descriptor] = api
220        return api
221       
222   
223    @staticmethod
224    def combineAPIs(*apis):
225        return PluginAPIAggregation(*apis)
226
227
228#         name, versionNo = descriptor[:2]
229#
230#         if not self.pluginAPIs.has_key(name):
231#             verReg = []
232#             self.pluginAPIs[name] = verReg
233#         else:
234#             verReg = self.pluginAPIs[name]
235#
236#         verReg.append(versionNo, api)
237#         return api
238
239
240#     def deletePluginAPI(self, name):
241#         del self.pluginAPIs[name]
242#         
243#     def getPluginAPIVerReg(self, name):
244#         return self.pluginAPIs[name]
245
246
247    def _processDescriptors(self, descriptors):
248        """
249        Find all known plugin API descriptors and return those
250        of each type with highest version number.
251        Returns a list of descriptor tuples.
252        """
253        found = {}
254        for d in descriptors:
255            name, versionNo = d[:2]
256            if not (name, versionNo) in self.pluginAPIs:
257                continue
258
259            if versionNo > found.get(name, 0):
260                found[name] = versionNo
261       
262        return found.items()
263
264
265    def registerPlugin(self, module):
266        registered = False
267       
268        for desc in self._processDescriptors(module.WIKIDPAD_PLUGIN):
269            registered |= self.pluginAPIs[desc].registerModule(module)
270
271#         for api in self.pluginAPIs.itervalues():
272#             registered |= api.registerModule(module)
273
274        if registered:
275            self.plugins[module.__name__] = module
276
277        return registered
278
279#     def deletePlugin(self, name):
280#         if name in self.plugins:
281#             module = self.plugins[name]
282#             for api in self.pluginAPIs.itervalues():
283#                 api.deleteModule(module)
284#             del self.plugins[name]
285       
286    def loadPlugins(self, excludeFiles):
287        """load and register plugins with apis. the directories in the list
288           directories are searched in order for all files ending with .py or
289           all directories. These are assumed to be possible plugins for
290           WikidPad. All such files and directories are loaded as modules and if
291           they have the WIKIDPAD_PLUGIN variable, are registered.
292           
293           Files and directories given in exludeFiles are not loaded at all. Also
294           directories are searched in order for plugins. Therefore plugins
295           appearing in earlier directories are not loaded from later ones."""
296        import imp
297        exclusions = excludeFiles[:]
298        for dirNum, directory in enumerate(self.directories):
299            sys.path.append(os.path.dirname(directory))
300            if not os.access(mbcsEnc(directory, "replace")[0], os.F_OK):
301                continue
302            files = os.listdir(directory)
303            for name in files:
304                try:
305                    module = None
306                    fullname = os.path.join(directory, name)
307                    ( moduleName, ext ) = os.path.splitext(name)
308                    if name in exclusions:
309                        continue
310                    if os.path.isfile(fullname) and ext == '.py':
311                        with open(fullname) as f:
312                            packageName = "cruelimportExtensionsPackage%i_%i" % \
313                                    (id(self), dirNum)
314
315                            module = imp.new_module(packageName)
316                            module.__path__ = [directory]
317                            sys.modules[packageName] = module
318
319                            module = imp.load_module(packageName + "." + moduleName, f,
320                                    mbcsEnc(fullname)[0], (".py", "r", imp.PY_SOURCE))
321                    if module and hasattr(module, "WIKIDPAD_PLUGIN"):
322                        if self.registerPlugin(module):
323                            exclusions.append(name)
324                except:
325                    traceback.print_exc()
326            del sys.path[-1]
327         
328    def importDirectory(self, name, add_to_sys_modules = False):
329        name = mbcsEnc(name, "replace")[0]
330        try:
331            module = __import__(name)
332        except ImportError:
333            return None
334        if not add_to_sys_modules:
335            del sys.modules[name]
336        return module
337
338
339#     def importCode(self,code,name,add_to_sys_modules = False):
340#         """
341#         Import dynamically generated code as a module. code is the
342#         object containing the code (a string, a file handle or an
343#         actual compiled code object, same types as accepted by an
344#         exec statement). The name is the name to give to the module,
345#         and the final argument says wheter to add it to sys.modules
346#         or not. If it is added, a subsequent import statement using
347#         name will return this module. If it is not added to sys.modules
348#         import will try to load it in the normal fashion.
349#
350#         import foo
351#
352#         is equivalent to
353#
354#         foofile = open("/path/to/foo.py")
355#         foo = importCode(foofile,"foo",1)
356#
357#         Returns a newly generated module.
358#         """
359#         import imp
360#
361#         module = imp.new_module(name)
362#
363#         exec code in module.__dict__
364#         if add_to_sys_modules:
365#             sys.modules[name] = module
366#
367#         return module
368
369
370
371class InsertionPluginManager:
372    def __init__(self, byKeyDescriptions):
373        """
374        byKeyDescriptions -- sequence of tuples as returned by
375            describeInsertionKeys() of a plugin
376        """
377        self.byKeyDescriptions = byKeyDescriptions
378
379        # (<insertion key>, <import type>) tuple to initialized handler
380        # this is only filled on demand
381        self.ktToHandlerDict = {}
382
383        # Build ktToDescDict meaning
384        # (<insertion key>, <import type>) tuple to description tuple dictionary
385        ktToDescDict = {}
386        for keyDesc in self.byKeyDescriptions:
387            key, etlist, factory = keyDesc[:3]
388            for et in etlist:
389                ktToDescDict[(key, et)] = keyDesc[:3]
390
391        # (<insertion key>, <import type>) tuple to description tuple dictionary
392        self.ktToDescDict = ktToDescDict
393       
394        # Contains all handler objects for which taskStart() was called and
395        # respective taskEnd() wasn't called yet.
396        # Dictionary {id(handler): handler}
397
398        self.startedHandlers = {}
399
400    def getHandler(self, exporter, exportType, insKey):
401        """
402        Return the appropriate handler for the parameter combination or None
403        exporter -- Calling exporter object
404        exportType -- string describing the export type
405        insKey -- insertion key
406        """
407        result = self.ktToHandlerDict.get((insKey, exportType), 0) # Can't use None here
408
409        if result == 0:
410            keyDesc = self.ktToDescDict.get((insKey, exportType))
411            if keyDesc is None:
412                result = None
413            else:
414                key, etlist, factory = keyDesc
415                try:
416                    obj = factory(wx.GetApp())
417                except:
418                    traceback.print_exc()
419                    obj = None
420
421                for et in etlist:
422                    self.ktToHandlerDict[(key, et)] = obj
423               
424                result = obj
425       
426        if result is not None and not id(result) in self.startedHandlers:
427            # Handler must be started before it can be used.
428            try:
429                result.taskStart(exporter, exportType)
430                self.startedHandlers[id(result)] = result
431            except:
432                traceback.print_exc()
433                result = None
434
435        return result
436
437
438    def taskEnd(self):
439        """
440        Call taskEnd() of all created handlers.
441        """
442        for handler in self.startedHandlers.values():
443            try:
444                handler.taskEnd()
445            except:
446                traceback.print_exc()
447           
448        self.startedHandlers.clear()
449
450
451
452def getSupportedExportTypes(mainControl, continuousExport, guiParent=None):
453    import Exporters
454   
455    result = {}
456   
457    for ob in Exporters.describeExporters(mainControl):   # TODO search plugins
458        for tp in ob.getExportTypes(guiParent, continuousExport):
459            result[tp[0]] = (ob,) + tuple(tp)
460
461    return result
462
Note: See TracBrowser for help on using the browser.