You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

259 lines
8.2 KiB

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