| Viewing file:  plugin.py (8.21 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
# -*- test-case-name: twisted.test.test_plugin -*-# Copyright (c) 2005 Divmod, Inc.
 # Copyright (c) Twisted Matrix Laboratories.
 # See LICENSE for details.
 
 """
 Plugin system for Twisted.
 
 @author: Jp Calderone
 @author: Glyph Lefkowitz
 """
 
 from __future__ import absolute_import, division
 
 import os
 import sys
 
 from zope.interface import Interface, providedBy
 
 def _determinePickleModule():
 """
 Determine which 'pickle' API module to use.
 """
 try:
 import cPickle
 return cPickle
 except ImportError:
 import pickle
 return pickle
 
 pickle = _determinePickleModule()
 
 from twisted.python.components import getAdapterFactory
 from twisted.python.reflect import namedAny
 from twisted.python import log
 from twisted.python.modules import getModule
 from twisted.python.compat import iteritems
 
 
 
 class IPlugin(Interface):
 """
 Interface that must be implemented by all plugins.
 
 Only objects which implement this interface will be considered for return
 by C{getPlugins}.  To be useful, plugins should also implement some other
 application-specific interface.
 """
 
 
 
 class CachedPlugin(object):
 def __init__(self, dropin, name, description, provided):
 self.dropin = dropin
 self.name = name
 self.description = description
 self.provided = provided
 self.dropin.plugins.append(self)
 
 def __repr__(self):
 return '<CachedPlugin %r/%r (provides %r)>' % (
 self.name, self.dropin.moduleName,
 ', '.join([i.__name__ for i in self.provided]))
 
 def load(self):
 return namedAny(self.dropin.moduleName + '.' + self.name)
 
 def __conform__(self, interface, registry=None, default=None):
 for providedInterface in self.provided:
 if providedInterface.isOrExtends(interface):
 return self.load()
 if getAdapterFactory(providedInterface, interface, None) is not None:
 return interface(self.load(), default)
 return default
 
 # backwards compat HOORJ
 getComponent = __conform__
 
 
 
 class CachedDropin(object):
 """
 A collection of L{CachedPlugin} instances from a particular module in a
 plugin package.
 
 @type moduleName: C{str}
 @ivar moduleName: The fully qualified name of the plugin module this
 represents.
 
 @type description: C{str} or C{NoneType}
 @ivar description: A brief explanation of this collection of plugins
 (probably the plugin module's docstring).
 
 @type plugins: C{list}
 @ivar plugins: The L{CachedPlugin} instances which were loaded from this
 dropin.
 """
 def __init__(self, moduleName, description):
 self.moduleName = moduleName
 self.description = description
 self.plugins = []
 
 
 
 def _generateCacheEntry(provider):
 dropin = CachedDropin(provider.__name__,
 provider.__doc__)
 for k, v in iteritems(provider.__dict__):
 plugin = IPlugin(v, None)
 if plugin is not None:
 # Instantiated for its side-effects.
 CachedPlugin(dropin, k, v.__doc__, list(providedBy(plugin)))
 return dropin
 
 try:
 fromkeys = dict.fromkeys
 except AttributeError:
 def fromkeys(keys, value=None):
 d = {}
 for k in keys:
 d[k] = value
 return d
 
 
 
 def getCache(module):
 """
 Compute all the possible loadable plugins, while loading as few as
 possible and hitting the filesystem as little as possible.
 
 @param module: a Python module object.  This represents a package to search
 for plugins.
 
 @return: a dictionary mapping module names to L{CachedDropin} instances.
 """
 allCachesCombined = {}
 mod = getModule(module.__name__)
 # don't want to walk deep, only immediate children.
 buckets = {}
 # Fill buckets with modules by related entry on the given package's
 # __path__.  There's an abstraction inversion going on here, because this
 # information is already represented internally in twisted.python.modules,
 # but it's simple enough that I'm willing to live with it.  If anyone else
 # wants to fix up this iteration so that it's one path segment at a time,
 # be my guest.  --glyph
 for plugmod in mod.iterModules():
 fpp = plugmod.filePath.parent()
 if fpp not in buckets:
 buckets[fpp] = []
 bucket = buckets[fpp]
 bucket.append(plugmod)
 for pseudoPackagePath, bucket in iteritems(buckets):
 dropinPath = pseudoPackagePath.child('dropin.cache')
 try:
 lastCached = dropinPath.getModificationTime()
 with dropinPath.open('r') as f:
 dropinDotCache = pickle.load(f)
 except:
 dropinDotCache = {}
 lastCached = 0
 
 needsWrite = False
 existingKeys = {}
 for pluginModule in bucket:
 pluginKey = pluginModule.name.split('.')[-1]
 existingKeys[pluginKey] = True
 if ((pluginKey not in dropinDotCache) or
 (pluginModule.filePath.getModificationTime() >= lastCached)):
 needsWrite = True
 try:
 provider = pluginModule.load()
 except:
 # dropinDotCache.pop(pluginKey, None)
 log.err()
 else:
 entry = _generateCacheEntry(provider)
 dropinDotCache[pluginKey] = entry
 # Make sure that the cache doesn't contain any stale plugins.
 for pluginKey in list(dropinDotCache.keys()):
 if pluginKey not in existingKeys:
 del dropinDotCache[pluginKey]
 needsWrite = True
 if needsWrite:
 try:
 dropinPath.setContent(pickle.dumps(dropinDotCache))
 except OSError as e:
 log.msg(
 format=(
 "Unable to write to plugin cache %(path)s: error "
 "number %(errno)d"),
 path=dropinPath.path, errno=e.errno)
 except:
 log.err(None, "Unexpected error while writing cache file")
 allCachesCombined.update(dropinDotCache)
 return allCachesCombined
 
 
 
 def getPlugins(interface, package=None):
 """
 Retrieve all plugins implementing the given interface beneath the given module.
 
 @param interface: An interface class.  Only plugins which implement this
 interface will be returned.
 
 @param package: A package beneath which plugins are installed.  For
 most uses, the default value is correct.
 
 @return: An iterator of plugins.
 """
 if package is None:
 import twisted.plugins as package
 allDropins = getCache(package)
 for key, dropin in iteritems(allDropins):
 for plugin in dropin.plugins:
 try:
 adapted = interface(plugin, None)
 except:
 log.err()
 else:
 if adapted is not None:
 yield adapted
 
 
 # Old, backwards compatible name.  Don't use this.
 getPlugIns = getPlugins
 
 
 def pluginPackagePaths(name):
 """
 Return a list of additional directories which should be searched for
 modules to be included as part of the named plugin package.
 
 @type name: C{str}
 @param name: The fully-qualified Python name of a plugin package, eg
 C{'twisted.plugins'}.
 
 @rtype: C{list} of C{str}
 @return: The absolute paths to other directories which may contain plugin
 modules for the named plugin package.
 """
 package = name.split('.')
 # Note that this may include directories which do not exist.  It may be
 # preferable to remove such directories at this point, rather than allow
 # them to be searched later on.
 #
 # Note as well that only '__init__.py' will be considered to make a
 # directory a package (and thus exclude it from this list).  This means
 # that if you create a master plugin package which has some other kind of
 # __init__ (eg, __init__.pyc) it will be incorrectly treated as a
 # supplementary plugin directory.
 return [
 os.path.abspath(os.path.join(x, *package))
 for x
 in sys.path
 if
 not os.path.exists(os.path.join(x, *package + ['__init__.py']))]
 
 __all__ = ['getPlugins', 'pluginPackagePaths']
 
 |