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 |