Home | History | Annotate | Download | only in cr
      1 # Copyright 2013 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """The plugin management system for the cr tool.
      6 
      7 This holds the Plugin class and supporting code, that controls how plugins are
      8 found and used.
      9 The module registers a scan hook with the cr.loader system to enable it to
     10 discover plugins as they are loaded.
     11 """
     12 from operator import attrgetter
     13 
     14 import cr
     15 import cr.loader
     16 
     17 
     18 def _PluginConfig(name, only_enabled=False, only_active=False):
     19   config = cr.Config(name)
     20   config.only_active = only_active
     21   config.only_enabled = only_enabled or config.only_active
     22   config.property_name = name.lower() + '_config'
     23   return config
     24 
     25 _selectors = cr.Config('PRIORITY')
     26 CONFIG_TYPES = [
     27     # Lowest priority, always there default values.
     28     _PluginConfig('DEFAULT').AddChild(_selectors),
     29     # Only turned on if the plugin is enabled.
     30     _PluginConfig('ENABLED', only_enabled=True),
     31     # Only turned on while the plugin is the active one.
     32     _PluginConfig('ACTIVE', only_active=True),
     33     # Holds detected values for active plugins.
     34     _PluginConfig('DETECTED', only_active=True),
     35     # Holds overrides, used in custom setup plugins.
     36     _PluginConfig('OVERRIDES'),
     37 ]
     38 
     39 cr.config.GLOBALS.extend(CONFIG_TYPES)
     40 _plugins = {}
     41 
     42 
     43 # Actually a decorator, so pylint: disable=invalid-name
     44 class classproperty(object):
     45   """This adds a property to a class.
     46 
     47   This is like a simple form of @property except it is for the class, rather
     48   than instances of the class. Only supports readonly properties.
     49   """
     50 
     51   def __init__(self, getter):
     52     self.getter = getter
     53 
     54   def __get__(self, instance, owner):
     55     return self.getter(owner)
     56 
     57 
     58 class DynamicChoices(object):
     59   """Manages the list of active plugins for command line options.
     60 
     61   Looks like a simple iterable, but it can change as the underlying plugins
     62   arrive and enable/disable themselves. This allows it to be used as the
     63   set of valid choices for the argparse command line options.
     64   """
     65 
     66   # If this is True, all DynamicChoices only return active plugins.
     67   # If false, all plugins are included.
     68   only_active = True
     69 
     70   def __init__(self, cls):
     71     self.cls = cls
     72 
     73   def __contains__(self, name):
     74     return self.cls.FindPlugin(name, self.only_active) is not None
     75 
     76   def __iter__(self):
     77     return [p.name for p in self.cls.Plugins()].__iter__()
     78 
     79 
     80 def _FindRoot(cls):
     81   if Plugin.Type in cls.__bases__:
     82     return cls
     83   for base in cls.__bases__:
     84     result = _FindRoot(base)
     85     if result is not None:
     86       return result
     87   return None
     88 
     89 
     90 class Plugin(cr.loader.AutoExport):
     91   """Base class for managing registered plugin types."""
     92 
     93   class Type(object):
     94     """Base class that tags a class as an abstract plugin type."""
     95 
     96   class activemethod(object):
     97     """A decorator that delegates a static method to the active plugin.
     98 
     99     Makes a static method that delegates to the equivalent method on the
    100     active instance of the plugin type.
    101     """
    102 
    103     def __init__(self, method):
    104       self.method = method
    105 
    106     def __get__(self, instance, owner):
    107       def unbound(context, *args, **kwargs):
    108         active = owner.GetActivePlugin(context)
    109         if not active:
    110           print 'No active', owner.__name__
    111           exit(1)
    112         method = getattr(active, self.method.__name__, None)
    113         if not method:
    114           print owner.__name__, 'does not support', self.method.__name__
    115           exit(1)
    116         return method(context, *args, **kwargs)
    117 
    118       def bound(context, *args, **kwargs):
    119         return self.method(instance, context, *args, **kwargs)
    120 
    121       if instance is None:
    122         return unbound
    123       return bound
    124 
    125   def __init__(self):
    126     # Default the name to the lowercased class name.
    127     self._name = self.__class__.__name__.lower()
    128     # Strip the common suffix if present.
    129     self._root = _FindRoot(self.__class__)
    130     rootname = self._root.__name__.lower()
    131     if self._name.endswith(rootname) and self.__class__ != self._root:
    132       self._name = self._name[:-len(rootname)]
    133     for config_root in CONFIG_TYPES:
    134       config = cr.Config()
    135       setattr(self, config_root.property_name, config)
    136     self._is_active = False
    137 
    138   def Init(self):
    139     """Post plugin registration initialisation method."""
    140     for config_root in CONFIG_TYPES:
    141       config = getattr(self, config_root.property_name)
    142       config.name = self.name
    143       if config_root.only_active and not self.is_active:
    144         config.enabled = False
    145       if config_root.only_enabled and not self.enabled:
    146         config.enabled = False
    147       child = getattr(self.__class__, config_root.name, None)
    148       if child is not None:
    149         child.name = self.__class__.__name__
    150         config.AddChild(child)
    151       config_root.AddChild(config)
    152 
    153   @property
    154   def name(self):
    155     return self._name
    156 
    157   @property
    158   def priority(self):
    159     return 0
    160 
    161   @property
    162   def enabled(self):
    163     # By default all non type classes are enabled.
    164     return Plugin.Type not in self.__class__.__bases__
    165 
    166   @property
    167   def is_active(self):
    168     return self._is_active
    169 
    170   def Activate(self, unused_context):
    171     assert not self._is_active
    172     self._is_active = True
    173     for config_root in CONFIG_TYPES:
    174       if config_root.only_active:
    175         getattr(self, config_root.property_name).enabled = True
    176 
    177   def Deactivate(self):
    178     assert self._is_active
    179     self._is_active = False
    180     for config_root in CONFIG_TYPES:
    181       if config_root.only_active:
    182         getattr(self, config_root.property_name).enabled = False
    183 
    184   @classmethod
    185   def GetInstance(cls):
    186     """Gets an instance of this plugin.
    187 
    188     This looks in the plugin registry, and if an instance is not found a new
    189     one is built and registered.
    190 
    191     Returns:
    192         The registered plugin instance.
    193     """
    194     plugin = _plugins.get(cls, None)
    195     if plugin is None:
    196       # Build a new instance of cls, and register it as the main instance.
    197       plugin = cls()
    198       _plugins[cls] = plugin
    199       # Wire up the hierarchy for Config objects.
    200       for name, value in cls.__dict__.items():
    201         if isinstance(value, cr.Config):
    202           for base in cls.__bases__:
    203             child = getattr(base, name, None)
    204             if child is not None:
    205               value.AddChild(child)
    206       plugin.Init()
    207     return plugin
    208 
    209   @classmethod
    210   def AllPlugins(cls):
    211     # Don't yield abstract roots, just children. We detect roots as direct
    212     # sub classes of Plugin.Type
    213     if Plugin.Type not in cls.__bases__:
    214       yield cls.GetInstance()
    215     for child in cls.__subclasses__():
    216       for p in child.AllPlugins():
    217         yield p
    218 
    219   @classmethod
    220   def UnorderedPlugins(cls):
    221     """Returns all enabled plugins of type cls, in undefined order."""
    222     plugin = cls.GetInstance()
    223     if plugin.enabled:
    224       yield plugin
    225     for child in cls.__subclasses__():
    226       for p in child.UnorderedPlugins():
    227         yield p
    228 
    229   @classmethod
    230   def Plugins(cls):
    231     """Return all enabled plugins of type cls in priority order."""
    232     return sorted(cls.UnorderedPlugins(),
    233                   key=attrgetter('priority'), reverse=True)
    234 
    235   @classmethod
    236   def Choices(cls):
    237     return DynamicChoices(cls)
    238 
    239   @classmethod
    240   def FindPlugin(cls, name, only_active=True):
    241     if only_active:
    242       plugins = cls.UnorderedPlugins()
    243     else:
    244       plugins = cls.AllPlugins()
    245     for plugin in plugins:
    246       if plugin.name == name or plugin.__class__.__name__ == name:
    247         return plugin
    248     return None
    249 
    250   @classmethod
    251   def GetPlugin(cls, name):
    252     result = cls.FindPlugin(name)
    253     if result is None:
    254       raise KeyError(name)
    255     return result
    256 
    257   @classmethod
    258   def GetAllActive(cls):
    259     return [plugin for plugin in cls.UnorderedPlugins() if plugin.is_active]
    260 
    261   @classmethod
    262   def GetActivePlugin(cls, context):
    263     """Gets the active plugin of type cls.
    264 
    265     This method will select a plugin to be the active one, and will activate
    266     the plugin if needed.
    267     Args:
    268       context: The context to select the active plugin for.
    269     Returns:
    270       the plugin that is currently active.
    271     """
    272     actives = cls.GetAllActive()
    273     plugin = cls.Select(context)
    274     for active in actives:
    275       if active != plugin:
    276         active.Deactivate()
    277     if plugin and not plugin.is_active:
    278       plugin.Activate(context)
    279     return plugin
    280 
    281   @classproperty
    282   def default(cls):
    283     """Returns the plugin that should be used if the user did not choose one."""
    284     result = None
    285     for plugin in cls.UnorderedPlugins():
    286       if not result or plugin.priority > result.priority:
    287         result = plugin
    288     return result
    289 
    290   @classmethod
    291   def Select(cls, context):
    292     """Called to determine which plugin should be the active one."""
    293     plugin = cls.default
    294     selector = getattr(cls, 'SELECTOR', None)
    295     if selector:
    296       if plugin is not None:
    297         _selectors[selector] = plugin.name
    298       name = context.Find(selector)
    299       if name is not None:
    300         plugin = cls.FindPlugin(name)
    301     return plugin
    302 
    303 
    304 def ChainModuleConfigs(module):
    305   """Detects and connects the default Config objects from a module."""
    306   for config_root in CONFIG_TYPES:
    307     if hasattr(module, config_root.name):
    308       config = getattr(module, config_root.name)
    309       config.name = module.__name__
    310       config_root.AddChild(config)
    311 
    312 
    313 cr.loader.scan_hooks.append(ChainModuleConfigs)
    314 
    315 
    316 def Activate(context):
    317   """Activates a plugin for all known plugin types."""
    318   types = Plugin.Type.__subclasses__()
    319   for child in types:
    320     child.GetActivePlugin(context)
    321