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"""
7Plugin system for Twisted.
8
9@author: Jp Calderone
10@author: Glyph Lefkowitz
11"""
12
13import os
14import sys
15
16from zope.interface import Interface, providedBy
17
18def _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
29pickle = _determinePickleModule()
30
31from twisted.python.components import getAdapterFactory
32from twisted.python.reflect import namedAny
33from twisted.python import log
34from twisted.python.modules import getModule
35
36
37
38class 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
49class 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
78class 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
102def _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
112try:
113    fromkeys = dict.fromkeys
114except AttributeError:
115    def fromkeys(keys, value=None):
116        d = {}
117        for k in keys:
118            d[k] = value
119        return d
120
121
122
123def 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
195def 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.
222getPlugIns = getPlugins
223
224
225def 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']
256