| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.test.test_plugin -*- | |
| 2 # Copyright (c) 2005 Divmod, Inc. | |
| 3 # Copyright (c) 2007 Twisted Matrix Laboratories. | |
| 4 # See LICENSE for details. | |
| 5 | |
| 6 """ | |
| 7 Plugin system for Twisted. | |
| 8 | |
| 9 @author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} | |
| 10 @author: U{Glyph Lefkowitz<mailto:glyph@twistedmatrix.com>} | |
| 11 """ | |
| 12 | |
| 13 import os | |
| 14 import sys | |
| 15 | |
| 16 from zope.interface import Interface, providedBy | |
| 17 | |
| 18 def _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 | |
| 29 pickle = _determinePickleModule() | |
| 30 | |
| 31 from twisted.python.components import getAdapterFactory | |
| 32 from twisted.python.reflect import namedAny | |
| 33 from twisted.python import log | |
| 34 from twisted.python.modules import getModule | |
| 35 | |
| 36 | |
| 37 | |
| 38 class 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 | |
| 49 class 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 | |
| 78 class 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 | |
| 102 def _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 cachedPlugin = CachedPlugin(dropin, k, v.__doc__, list(providedBy(pl
ugin))) | |
| 109 return dropin | |
| 110 | |
| 111 try: | |
| 112 fromkeys = dict.fromkeys | |
| 113 except AttributeError: | |
| 114 def fromkeys(keys, value=None): | |
| 115 d = {} | |
| 116 for k in keys: | |
| 117 d[k] = value | |
| 118 return d | |
| 119 | |
| 120 def getCache(module): | |
| 121 """ | |
| 122 Compute all the possible loadable plugins, while loading as few as | |
| 123 possible and hitting the filesystem as little as possible. | |
| 124 | |
| 125 @param module: a Python module object. This represents a package to search | |
| 126 for plugins. | |
| 127 | |
| 128 @return: a dictionary mapping module names to CachedDropin instances. | |
| 129 """ | |
| 130 allCachesCombined = {} | |
| 131 mod = getModule(module.__name__) | |
| 132 # don't want to walk deep, only immediate children. | |
| 133 lastPath = None | |
| 134 buckets = {} | |
| 135 # Fill buckets with modules by related entry on the given package's | |
| 136 # __path__. There's an abstraction inversion going on here, because this | |
| 137 # information is already represented internally in twisted.python.modules, | |
| 138 # but it's simple enough that I'm willing to live with it. If anyone else | |
| 139 # wants to fix up this iteration so that it's one path segment at a time, | |
| 140 # be my guest. --glyph | |
| 141 for plugmod in mod.iterModules(): | |
| 142 fpp = plugmod.filePath.parent() | |
| 143 if fpp not in buckets: | |
| 144 buckets[fpp] = [] | |
| 145 bucket = buckets[fpp] | |
| 146 bucket.append(plugmod) | |
| 147 for pseudoPackagePath, bucket in buckets.iteritems(): | |
| 148 dropinPath = pseudoPackagePath.child('dropin.cache') | |
| 149 try: | |
| 150 lastCached = dropinPath.getModificationTime() | |
| 151 dropinDotCache = pickle.load(dropinPath.open('rb')) | |
| 152 except: | |
| 153 dropinDotCache = {} | |
| 154 lastCached = 0 | |
| 155 | |
| 156 needsWrite = False | |
| 157 existingKeys = {} | |
| 158 for pluginModule in bucket: | |
| 159 pluginKey = pluginModule.name.split('.')[-1] | |
| 160 existingKeys[pluginKey] = True | |
| 161 if ((pluginKey not in dropinDotCache) or | |
| 162 (pluginModule.filePath.getModificationTime() >= lastCached)): | |
| 163 needsWrite = True | |
| 164 try: | |
| 165 provider = pluginModule.load() | |
| 166 except: | |
| 167 # dropinDotCache.pop(pluginKey, None) | |
| 168 log.err() | |
| 169 else: | |
| 170 entry = _generateCacheEntry(provider) | |
| 171 dropinDotCache[pluginKey] = entry | |
| 172 # Make sure that the cache doesn't contain any stale plugins. | |
| 173 for pluginKey in dropinDotCache.keys(): | |
| 174 if pluginKey not in existingKeys: | |
| 175 del dropinDotCache[pluginKey] | |
| 176 needsWrite = True | |
| 177 if needsWrite: | |
| 178 try: | |
| 179 dropinPath.setContent(pickle.dumps(dropinDotCache)) | |
| 180 except: | |
| 181 log.err() | |
| 182 allCachesCombined.update(dropinDotCache) | |
| 183 return allCachesCombined | |
| 184 | |
| 185 | |
| 186 def getPlugins(interface, package=None): | |
| 187 """ | |
| 188 Retrieve all plugins implementing the given interface beneath the given modu
le. | |
| 189 | |
| 190 @param interface: An interface class. Only plugins which implement this | |
| 191 interface will be returned. | |
| 192 | |
| 193 @param package: A package beneath which plugins are installed. For | |
| 194 most uses, the default value is correct. | |
| 195 | |
| 196 @return: An iterator of plugins. | |
| 197 """ | |
| 198 if package is None: | |
| 199 import twisted.plugins as package | |
| 200 allDropins = getCache(package) | |
| 201 for dropin in allDropins.itervalues(): | |
| 202 for plugin in dropin.plugins: | |
| 203 try: | |
| 204 adapted = interface(plugin, None) | |
| 205 except: | |
| 206 log.err() | |
| 207 else: | |
| 208 if adapted is not None: | |
| 209 yield adapted | |
| 210 | |
| 211 | |
| 212 # Old, backwards compatible name. Don't use this. | |
| 213 getPlugIns = getPlugins | |
| 214 | |
| 215 | |
| 216 def pluginPackagePaths(name): | |
| 217 """ | |
| 218 Return a list of additional directories which should be searched for | |
| 219 modules to be included as part of the named plugin package. | |
| 220 | |
| 221 @type name: C{str} | |
| 222 @param name: The fully-qualified Python name of a plugin package, eg | |
| 223 C{'twisted.plugins'}. | |
| 224 | |
| 225 @rtype: C{list} of C{str} | |
| 226 @return: The absolute paths to other directories which may contain plugin | |
| 227 modules for the named plugin package. | |
| 228 """ | |
| 229 package = name.split('.') | |
| 230 # Note that this may include directories which do not exist. It may be | |
| 231 # preferable to remove such directories at this point, rather than allow | |
| 232 # them to be searched later on. | |
| 233 # | |
| 234 # Note as well that only '__init__.py' will be considered to make a | |
| 235 # directory a package (and thus exclude it from this list). This means | |
| 236 # that if you create a master plugin package which has some other kind of | |
| 237 # __init__ (eg, __init__.pyc) it will be incorrectly treated as a | |
| 238 # supplementary plugin directory. | |
| 239 return [ | |
| 240 os.path.abspath(os.path.join(x, *package)) | |
| 241 for x | |
| 242 in sys.path | |
| 243 if | |
| 244 not os.path.exists(os.path.join(x, *package + ['__init__.py']))] | |
| 245 | |
| 246 __all__ = ['getPlugins', 'pluginPackagePaths'] | |
| OLD | NEW |