| 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 | """ |
|---|
| 7 | Plugin system for Twisted. |
|---|
| 8 | |
|---|
| 9 | @author: Jp Calderone |
|---|
| 10 | @author: Glyph Lefkowitz |
|---|
| 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 | # Instantiated for its side-effects. |
|---|
| 109 | CachedPlugin(dropin, k, v.__doc__, list(providedBy(plugin))) |
|---|
| 110 | return dropin |
|---|
| 111 | |
|---|
| 112 | try: |
|---|
| 113 | fromkeys = dict.fromkeys |
|---|
| 114 | except AttributeError: |
|---|
| 115 | def fromkeys(keys, value=None): |
|---|
| 116 | d = {} |
|---|
| 117 | for k in keys: |
|---|
| 118 | d[k] = value |
|---|
| 119 | return d |
|---|
| 120 | |
|---|
| 121 | |
|---|
| 122 | |
|---|
| 123 | def 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 | |
|---|
| 195 | def 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. |
|---|
| 222 | getPlugIns = getPlugins |
|---|
| 223 | |
|---|
| 224 | |
|---|
| 225 | def 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'] |
|---|