text: # -*- 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']
  