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.
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
cachedPlugin = CachedPlugin(dropin, k, v.__doc__, list(providedBy(plugin)))
112
fromkeys = dict.fromkeys
113
except AttributeError:
114
def fromkeys(keys, value=None):
120
def getCache(module):
122
Compute all the possible loadable plugins, while loading as few as
123
possible and hitting the filesystem as little as possible.
125
@param module: a Python module object. This represents a package to search
128
@return: a dictionary mapping module names to CachedDropin instances.
130
allCachesCombined = {}
131
mod = getModule(module.__name__)
132
# don't want to walk deep, only immediate children.
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:
145
bucket = buckets[fpp]
146
bucket.append(plugmod)
147
for pseudoPackagePath, bucket in buckets.iteritems():
148
dropinPath = pseudoPackagePath.child('dropin.cache')
150
lastCached = dropinPath.getModificationTime()
151
dropinDotCache = pickle.load(dropinPath.open('rb'))
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)):
165
provider = pluginModule.load()
167
# dropinDotCache.pop(pluginKey, None)
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]
179
dropinPath.setContent(pickle.dumps(dropinDotCache))
182
allCachesCombined.update(dropinDotCache)
183
return allCachesCombined
186
def getPlugins(interface, package=None):
188
Retrieve all plugins implementing the given interface beneath the given module.
190
@param interface: An interface class. Only plugins which implement this
191
interface will be returned.
193
@param package: A package beneath which plugins are installed. For
194
most uses, the default value is correct.
196
@return: An iterator of plugins.
199
import twisted.plugins as package
200
allDropins = getCache(package)
201
for dropin in allDropins.itervalues():
202
for plugin in dropin.plugins:
204
adapted = interface(plugin, None)
208
if adapted is not None:
212
# Old, backwards compatible name. Don't use this.
213
getPlugIns = getPlugins
216
def pluginPackagePaths(name):
218
Return a list of additional directories which should be searched for
219
modules to be included as part of the named plugin package.
222
@param name: The fully-qualified Python name of a plugin package, eg
223
C{'twisted.plugins'}.
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.
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.
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.
240
os.path.abspath(os.path.join(x, *package))
244
not os.path.exists(os.path.join(x, *package + ['__init__.py']))]
246
__all__ = ['getPlugins', 'pluginPackagePaths']