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.
7 Plugin system for Twisted.
10 @author: Glyph Lefkowitz
16 from zope.interface import Interface, providedBy
18 def _determinePickleModule():
20 Determine which 'pickle' API module to use.
29 pickle = _determinePickleModule()
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
38 class IPlugin(Interface):
40 Interface that must be implemented by all plugins.
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.
49 class CachedPlugin(object):
50 def __init__(self, dropin, name, description, provided):
53 self.description = description
54 self.provided = provided
55 self.dropin.plugins.append(self)
58 return '<CachedPlugin %r/%r (provides %r)>' % (
59 self.name, self.dropin.moduleName,
60 ', '.join([i.__name__ for i in self.provided]))
63 return namedAny(self.dropin.moduleName + '.' + self.name)
65 def __conform__(self, interface, registry=None, default=None):
66 for providedInterface in self.provided:
67 if providedInterface.isOrExtends(interface):
69 if getAdapterFactory(providedInterface, interface, None) is not None:
70 return interface(self.load(), default)
73 # backwards compat HOORJ
74 getComponent = __conform__
78 class CachedDropin(object):
80 A collection of L{CachedPlugin} instances from a particular module in a
83 @type moduleName: C{str}
84 @ivar moduleName: The fully qualified name of the plugin module this
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).
91 @type plugins: C{list}
92 @ivar plugins: The L{CachedPlugin} instances which were loaded from this
95 def __init__(self, moduleName, description):
96 self.moduleName = moduleName
97 self.description = description
102 def _generateCacheEntry(provider):
103 dropin = CachedDropin(provider.__name__,
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)))
113 fromkeys = dict.fromkeys
114 except AttributeError:
115 def fromkeys(keys, value=None):
123 def getCache(module):
125 Compute all the possible loadable plugins, while loading as few as
126 possible and hitting the filesystem as little as possible.
128 @param module: a Python module object. This represents a package to search
131 @return: a dictionary mapping module names to L{CachedDropin} instances.
133 allCachesCombined = {}
134 mod = getModule(module.__name__)
135 # don't want to walk deep, only immediate children.
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:
147 bucket = buckets[fpp]
148 bucket.append(plugmod)
149 for pseudoPackagePath, bucket in buckets.iteritems():
150 dropinPath = pseudoPackagePath.child('dropin.cache')
152 lastCached = dropinPath.getModificationTime()
153 dropinDotCache = pickle.load(dropinPath.open('r'))
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)):
167 provider = pluginModule.load()
169 # dropinDotCache.pop(pluginKey, None)
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]
181 dropinPath.setContent(pickle.dumps(dropinDotCache))
185 "Unable to write to plugin cache %(path)s: error "
187 path=dropinPath.path, errno=e.errno)
189 log.err(None, "Unexpected error while writing cache file")
190 allCachesCombined.update(dropinDotCache)
191 return allCachesCombined
195 def getPlugins(interface, package=None):
197 Retrieve all plugins implementing the given interface beneath the given module.
199 @param interface: An interface class. Only plugins which implement this
200 interface will be returned.
202 @param package: A package beneath which plugins are installed. For
203 most uses, the default value is correct.
205 @return: An iterator of plugins.
208 import twisted.plugins as package
209 allDropins = getCache(package)
210 for dropin in allDropins.itervalues():
211 for plugin in dropin.plugins:
213 adapted = interface(plugin, None)
217 if adapted is not None:
221 # Old, backwards compatible name. Don't use this.
222 getPlugIns = getPlugins
225 def pluginPackagePaths(name):
227 Return a list of additional directories which should be searched for
228 modules to be included as part of the named plugin package.
231 @param name: The fully-qualified Python name of a plugin package, eg
232 C{'twisted.plugins'}.
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.
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.
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.
249 os.path.abspath(os.path.join(x, *package))
253 not os.path.exists(os.path.join(x, *package + ['__init__.py']))]
255 __all__ = ['getPlugins', 'pluginPackagePaths']