1# -*- test-case-name: twisted.test.test_plugin -*- 2# Copyright (c) 2005 Divmod, Inc. 3# Copyright (c) Twisted Matrix Laboratories. 4# See LICENSE for details. 5 6""" 7Plugin system for Twisted. 8 9@author: Jp Calderone 10@author: Glyph Lefkowitz 11""" 12 13import os 14import sys 15 16from zope.interface import Interface, providedBy 17 18def _determinePickleModule(): 19 """ 20 Determine which 'pickle' API module to use. 21 """ 22 try: 23 import cPickle 24 return cPickle 25 except ImportError: 26 import pickle 27 return pickle 28 29pickle = _determinePickleModule() 30 31from twisted.python.components import getAdapterFactory 32from twisted.python.reflect import namedAny 33from twisted.python import log 34from twisted.python.modules import getModule 35 36 37 38class IPlugin(Interface): 39 """ 40 Interface that must be implemented by all plugins. 41 42 Only objects which implement this interface will be considered for return 43 by C{getPlugins}. To be useful, plugins should also implement some other 44 application-specific interface. 45 """ 46 47 48 49class CachedPlugin(object): 50 def __init__(self, dropin, name, description, provided): 51 self.dropin = dropin 52 self.name = name 53 self.description = description 54 self.provided = provided 55 self.dropin.plugins.append(self) 56 57 def __repr__(self): 58 return '<CachedPlugin %r/%r (provides %r)>' % ( 59 self.name, self.dropin.moduleName, 60 ', '.join([i.__name__ for i in self.provided])) 61 62 def load(self): 63 return namedAny(self.dropin.moduleName + '.' + self.name) 64 65 def __conform__(self, interface, registry=None, default=None): 66 for providedInterface in self.provided: 67 if providedInterface.isOrExtends(interface): 68 return self.load() 69 if getAdapterFactory(providedInterface, interface, None) is not None: 70 return interface(self.load(), default) 71 return default 72 73 # backwards compat HOORJ 74 getComponent = __conform__ 75 76 77 78class CachedDropin(object): 79 """ 80 A collection of L{CachedPlugin} instances from a particular module in a 81 plugin package. 82 83 @type moduleName: C{str} 84 @ivar moduleName: The fully qualified name of the plugin module this 85 represents. 86 87 @type description: C{str} or C{NoneType} 88 @ivar description: A brief explanation of this collection of plugins 89 (probably the plugin module's docstring). 90 91 @type plugins: C{list} 92 @ivar plugins: The L{CachedPlugin} instances which were loaded from this 93 dropin. 94 """ 95 def __init__(self, moduleName, description): 96 self.moduleName = moduleName 97 self.description = description 98 self.plugins = [] 99 100 101 102def _generateCacheEntry(provider): 103 dropin = CachedDropin(provider.__name__, 104 provider.__doc__) 105 for k, v in provider.__dict__.iteritems(): 106 plugin = IPlugin(v, None) 107 if plugin is not None: 108 # Instantiated for its side-effects. 109 CachedPlugin(dropin, k, v.__doc__, list(providedBy(plugin))) 110 return dropin 111 112try: 113 fromkeys = dict.fromkeys 114except AttributeError: 115 def fromkeys(keys, value=None): 116 d = {} 117 for k in keys: 118 d[k] = value 119 return d 120 121 122 123def getCache(module): 124 """ 125 Compute all the possible loadable plugins, while loading as few as 126 possible and hitting the filesystem as little as possible. 127 128 @param module: a Python module object. This represents a package to search 129 for plugins. 130 131 @return: a dictionary mapping module names to L{CachedDropin} instances. 132 """ 133 allCachesCombined = {} 134 mod = getModule(module.__name__) 135 # don't want to walk deep, only immediate children. 136 buckets = {} 137 # Fill buckets with modules by related entry on the given package's 138 # __path__. There's an abstraction inversion going on here, because this 139 # information is already represented internally in twisted.python.modules, 140 # but it's simple enough that I'm willing to live with it. If anyone else 141 # wants to fix up this iteration so that it's one path segment at a time, 142 # be my guest. --glyph 143 for plugmod in mod.iterModules(): 144 fpp = plugmod.filePath.parent() 145 if fpp not in buckets: 146 buckets[fpp] = [] 147 bucket = buckets[fpp] 148 bucket.append(plugmod) 149 for pseudoPackagePath, bucket in buckets.iteritems(): 150 dropinPath = pseudoPackagePath.child('dropin.cache') 151 try: 152 lastCached = dropinPath.getModificationTime() 153 dropinDotCache = pickle.load(dropinPath.open('r')) 154 except: 155 dropinDotCache = {} 156 lastCached = 0 157 158 needsWrite = False 159 existingKeys = {} 160 for pluginModule in bucket: 161 pluginKey = pluginModule.name.split('.')[-1] 162 existingKeys[pluginKey] = True 163 if ((pluginKey not in dropinDotCache) or 164 (pluginModule.filePath.getModificationTime() >= lastCached)): 165 needsWrite = True 166 try: 167 provider = pluginModule.load() 168 except: 169 # dropinDotCache.pop(pluginKey, None) 170 log.err() 171 else: 172 entry = _generateCacheEntry(provider) 173 dropinDotCache[pluginKey] = entry 174 # Make sure that the cache doesn't contain any stale plugins. 175 for pluginKey in dropinDotCache.keys(): 176 if pluginKey not in existingKeys: 177 del dropinDotCache[pluginKey] 178 needsWrite = True 179 if needsWrite: 180 try: 181 dropinPath.setContent(pickle.dumps(dropinDotCache)) 182 except OSError, e: 183 log.msg( 184 format=( 185 "Unable to write to plugin cache %(path)s: error " 186 "number %(errno)d"), 187 path=dropinPath.path, errno=e.errno) 188 except: 189 log.err(None, "Unexpected error while writing cache file") 190 allCachesCombined.update(dropinDotCache) 191 return allCachesCombined 192 193 194 195def getPlugins(interface, package=None): 196 """ 197 Retrieve all plugins implementing the given interface beneath the given module. 198 199 @param interface: An interface class. Only plugins which implement this 200 interface will be returned. 201 202 @param package: A package beneath which plugins are installed. For 203 most uses, the default value is correct. 204 205 @return: An iterator of plugins. 206 """ 207 if package is None: 208 import twisted.plugins as package 209 allDropins = getCache(package) 210 for dropin in allDropins.itervalues(): 211 for plugin in dropin.plugins: 212 try: 213 adapted = interface(plugin, None) 214 except: 215 log.err() 216 else: 217 if adapted is not None: 218 yield adapted 219 220 221# Old, backwards compatible name. Don't use this. 222getPlugIns = getPlugins 223 224 225def pluginPackagePaths(name): 226 """ 227 Return a list of additional directories which should be searched for 228 modules to be included as part of the named plugin package. 229 230 @type name: C{str} 231 @param name: The fully-qualified Python name of a plugin package, eg 232 C{'twisted.plugins'}. 233 234 @rtype: C{list} of C{str} 235 @return: The absolute paths to other directories which may contain plugin 236 modules for the named plugin package. 237 """ 238 package = name.split('.') 239 # Note that this may include directories which do not exist. It may be 240 # preferable to remove such directories at this point, rather than allow 241 # them to be searched later on. 242 # 243 # Note as well that only '__init__.py' will be considered to make a 244 # directory a package (and thus exclude it from this list). This means 245 # that if you create a master plugin package which has some other kind of 246 # __init__ (eg, __init__.pyc) it will be incorrectly treated as a 247 # supplementary plugin directory. 248 return [ 249 os.path.abspath(os.path.join(x, *package)) 250 for x 251 in sys.path 252 if 253 not os.path.exists(os.path.join(x, *package + ['__init__.py']))] 254 255__all__ = ['getPlugins', 'pluginPackagePaths'] 256