plugin.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. Plugin system for Twisted.
  7. @author: Jp Calderone
  8. @author: Glyph Lefkowitz
  9. """
  10. import os
  11. import sys
  12. from zope.interface import Interface, providedBy
  13. def _determinePickleModule():
  14. """
  15. Determine which 'pickle' API module to use.
  16. """
  17. try:
  18. import cPickle
  19. return cPickle
  20. except ImportError:
  21. import pickle
  22. return pickle
  23. pickle = _determinePickleModule()
  24. from twisted.python.components import getAdapterFactory
  25. from twisted.python.reflect import namedAny
  26. from twisted.python import log
  27. from twisted.python.modules import getModule
  28. class IPlugin(Interface):
  29. """
  30. Interface that must be implemented by all plugins.
  31. Only objects which implement this interface will be considered for return
  32. by C{getPlugins}. To be useful, plugins should also implement some other
  33. application-specific interface.
  34. """
  35. class CachedPlugin(object):
  36. def __init__(self, dropin, name, description, provided):
  37. self.dropin = dropin
  38. self.name = name
  39. self.description = description
  40. self.provided = provided
  41. self.dropin.plugins.append(self)
  42. def __repr__(self):
  43. return '<CachedPlugin %r/%r (provides %r)>' % (
  44. self.name, self.dropin.moduleName,
  45. ', '.join([i.__name__ for i in self.provided]))
  46. def load(self):
  47. return namedAny(self.dropin.moduleName + '.' + self.name)
  48. def __conform__(self, interface, registry=None, default=None):
  49. for providedInterface in self.provided:
  50. if providedInterface.isOrExtends(interface):
  51. return self.load()
  52. if getAdapterFactory(providedInterface, interface, None) is not None:
  53. return interface(self.load(), default)
  54. return default
  55. # backwards compat HOORJ
  56. getComponent = __conform__
  57. class CachedDropin(object):
  58. """
  59. A collection of L{CachedPlugin} instances from a particular module in a
  60. plugin package.
  61. @type moduleName: C{str}
  62. @ivar moduleName: The fully qualified name of the plugin module this
  63. represents.
  64. @type description: C{str} or C{NoneType}
  65. @ivar description: A brief explanation of this collection of plugins
  66. (probably the plugin module's docstring).
  67. @type plugins: C{list}
  68. @ivar plugins: The L{CachedPlugin} instances which were loaded from this
  69. dropin.
  70. """
  71. def __init__(self, moduleName, description):
  72. self.moduleName = moduleName
  73. self.description = description
  74. self.plugins = []
  75. def _generateCacheEntry(provider):
  76. dropin = CachedDropin(provider.__name__,
  77. provider.__doc__)
  78. for k, v in provider.__dict__.iteritems():
  79. plugin = IPlugin(v, None)
  80. if plugin is not None:
  81. cachedPlugin = CachedPlugin(dropin, k, v.__doc__, list(providedBy(plugin)))
  82. return dropin
  83. try:
  84. fromkeys = dict.fromkeys
  85. except AttributeError:
  86. def fromkeys(keys, value=None):
  87. d = {}
  88. for k in keys:
  89. d[k] = value
  90. return d
  91. def getCache(module):
  92. """
  93. Compute all the possible loadable plugins, while loading as few as
  94. possible and hitting the filesystem as little as possible.
  95. @param module: a Python module object. This represents a package to search
  96. for plugins.
  97. @return: a dictionary mapping module names to CachedDropin instances.
  98. """
  99. allCachesCombined = {}
  100. mod = getModule(module.__name__)
  101. # don't want to walk deep, only immediate children.
  102. lastPath = None
  103. buckets = {}
  104. # Fill buckets with modules by related entry on the given package's
  105. # __path__. There's an abstraction inversion going on here, because this
  106. # information is already represented internally in twisted.python.modules,
  107. # but it's simple enough that I'm willing to live with it. If anyone else
  108. # wants to fix up this iteration so that it's one path segment at a time,
  109. # be my guest. --glyph
  110. for plugmod in mod.iterModules():
  111. fpp = plugmod.filePath.parent()
  112. if fpp not in buckets:
  113. buckets[fpp] = []
  114. bucket = buckets[fpp]
  115. bucket.append(plugmod)
  116. for pseudoPackagePath, bucket in buckets.iteritems():
  117. dropinPath = pseudoPackagePath.child('dropin.cache')
  118. try:
  119. lastCached = dropinPath.getModificationTime()
  120. dropinDotCache = pickle.load(dropinPath.open('rb'))
  121. except:
  122. dropinDotCache = {}
  123. lastCached = 0
  124. needsWrite = False
  125. existingKeys = {}
  126. for pluginModule in bucket:
  127. pluginKey = pluginModule.name.split('.')[-1]
  128. existingKeys[pluginKey] = True
  129. if ((pluginKey not in dropinDotCache) or
  130. (pluginModule.filePath.getModificationTime() >= lastCached)):
  131. needsWrite = True
  132. try:
  133. provider = pluginModule.load()
  134. except:
  135. # dropinDotCache.pop(pluginKey, None)
  136. log.err()
  137. else:
  138. entry = _generateCacheEntry(provider)
  139. dropinDotCache[pluginKey] = entry
  140. # Make sure that the cache doesn't contain any stale plugins.
  141. for pluginKey in dropinDotCache.keys():
  142. if pluginKey not in existingKeys:
  143. del dropinDotCache[pluginKey]
  144. needsWrite = True
  145. if needsWrite:
  146. try:
  147. dropinPath.setContent(pickle.dumps(dropinDotCache))
  148. except:
  149. log.err()
  150. allCachesCombined.update(dropinDotCache)
  151. return allCachesCombined
  152. def getPlugins(interface, package=None):
  153. """
  154. Retrieve all plugins implementing the given interface beneath the given module.
  155. @param interface: An interface class. Only plugins which implement this
  156. interface will be returned.
  157. @param package: A package beneath which plugins are installed. For
  158. most uses, the default value is correct.
  159. @return: An iterator of plugins.
  160. """
  161. if package is None:
  162. import twisted.plugins as package
  163. allDropins = getCache(package)
  164. for dropin in allDropins.itervalues():
  165. for plugin in dropin.plugins:
  166. try:
  167. adapted = interface(plugin, None)
  168. except:
  169. log.err()
  170. else:
  171. if adapted is not None:
  172. yield adapted
  173. # Old, backwards compatible name. Don't use this.
  174. getPlugIns = getPlugins
  175. def pluginPackagePaths(name):
  176. """
  177. Return a list of additional directories which should be searched for
  178. modules to be included as part of the named plugin package.
  179. @type name: C{str}
  180. @param name: The fully-qualified Python name of a plugin package, eg
  181. C{'twisted.plugins'}.
  182. @rtype: C{list} of C{str}
  183. @return: The absolute paths to other directories which may contain plugin
  184. modules for the named plugin package.
  185. """
  186. package = name.split('.')
  187. # Note that this may include directories which do not exist. It may be
  188. # preferable to remove such directories at this point, rather than allow
  189. # them to be searched later on.
  190. #
  191. # Note as well that only '__init__.py' will be considered to make a
  192. # directory a package (and thus exclude it from this list). This means
  193. # that if you create a master plugin package which has some other kind of
  194. # __init__ (eg, __init__.pyc) it will be incorrectly treated as a
  195. # supplementary plugin directory.
  196. return [
  197. os.path.abspath(os.path.join(x, *package))
  198. for x
  199. in sys.path
  200. if
  201. not os.path.exists(os.path.join(x, *package + ['__init__.py']))]
  202. __all__ = ['getPlugins', 'pluginPackagePaths']