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 7This holds the Plugin class and supporting code, that controls how plugins are 8found and used. 9The module registers a scan hook with the cr.loader system to enable it to 10discover plugins as they are loaded. 11""" 12from operator import attrgetter 13 14import cr 15import cr.loader 16 17 18def _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') 26CONFIG_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 39cr.config.GLOBALS.extend(CONFIG_TYPES) 40_plugins = {} 41 42 43# Actually a decorator, so pylint: disable=invalid-name 44class 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 58class 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 80def _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 90class 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(*args, **kwargs): 108 active = owner.GetActivePlugin() 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(*args, **kwargs) 117 118 def bound(*args, **kwargs): 119 return self.method(instance, *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): 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 ClassInit(cls): 186 pass 187 188 @classmethod 189 def GetInstance(cls): 190 """Gets an instance of this plugin. 191 192 This looks in the plugin registry, and if an instance is not found a new 193 one is built and registered. 194 195 Returns: 196 The registered plugin instance. 197 """ 198 plugin = _plugins.get(cls, None) 199 if plugin is None: 200 # Run delayed class initialization 201 cls.ClassInit() 202 # Build a new instance of cls, and register it as the main instance. 203 plugin = cls() 204 _plugins[cls] = plugin 205 # Wire up the hierarchy for Config objects. 206 for name, value in cls.__dict__.items(): 207 if isinstance(value, cr.Config): 208 for base in cls.__bases__: 209 child = getattr(base, name, None) 210 if child is not None: 211 value.AddChild(child) 212 plugin.Init() 213 return plugin 214 215 @classmethod 216 def AllPlugins(cls): 217 # Don't yield abstract roots, just children. We detect roots as direct 218 # sub classes of Plugin.Type 219 if Plugin.Type not in cls.__bases__: 220 yield cls.GetInstance() 221 for child in cls.__subclasses__(): 222 for p in child.AllPlugins(): 223 yield p 224 225 @classmethod 226 def UnorderedPlugins(cls): 227 """Returns all enabled plugins of type cls, in undefined order.""" 228 plugin = cls.GetInstance() 229 if plugin.enabled: 230 yield plugin 231 for child in cls.__subclasses__(): 232 for p in child.UnorderedPlugins(): 233 yield p 234 235 @classmethod 236 def Plugins(cls): 237 """Return all enabled plugins of type cls in priority order.""" 238 return sorted(cls.UnorderedPlugins(), 239 key=attrgetter('priority'), reverse=True) 240 241 @classmethod 242 def Choices(cls): 243 return DynamicChoices(cls) 244 245 @classmethod 246 def FindPlugin(cls, name, only_active=True): 247 if only_active: 248 plugins = cls.UnorderedPlugins() 249 else: 250 plugins = cls.AllPlugins() 251 for plugin in plugins: 252 if plugin.name == name or plugin.__class__.__name__ == name: 253 return plugin 254 return None 255 256 @classmethod 257 def GetPlugin(cls, name): 258 result = cls.FindPlugin(name) 259 if result is None: 260 raise KeyError(name) 261 return result 262 263 @classmethod 264 def GetAllActive(cls): 265 return [plugin for plugin in cls.UnorderedPlugins() if plugin.is_active] 266 267 @classmethod 268 def GetActivePlugin(cls): 269 """Gets the active plugin of type cls. 270 271 This method will select a plugin to be the active one, and will activate 272 the plugin if needed. 273 Returns: 274 the plugin that is currently active. 275 """ 276 plugin, _ = _GetActivePlugin(cls) 277 return plugin 278 279 @classproperty 280 def default(cls): 281 """Returns the plugin that should be used if the user did not choose one.""" 282 result = None 283 for plugin in cls.UnorderedPlugins(): 284 if not result or plugin.priority > result.priority: 285 result = plugin 286 return result 287 288 @classmethod 289 def Select(cls): 290 """Called to determine which plugin should be the active one.""" 291 plugin = cls.default 292 selector = getattr(cls, 'SELECTOR', None) 293 if selector: 294 if plugin is not None: 295 _selectors[selector] = plugin.name 296 name = cr.context.Find(selector) 297 if name is not None: 298 plugin = cls.FindPlugin(name) 299 return plugin 300 301 302def ChainModuleConfigs(module): 303 """Detects and connects the default Config objects from a module.""" 304 for config_root in CONFIG_TYPES: 305 if hasattr(module, config_root.name): 306 config = getattr(module, config_root.name) 307 config.name = module.__name__ 308 config_root.AddChild(config) 309 310 311cr.loader.scan_hooks.append(ChainModuleConfigs) 312 313 314def _GetActivePlugin(cls): 315 activated = False 316 actives = cls.GetAllActive() 317 plugin = cls.Select() 318 for active in actives: 319 if active != plugin: 320 active.Deactivate() 321 if plugin and not plugin.is_active: 322 activated = True 323 plugin.Activate() 324 return plugin, activated 325 326 327def Activate(): 328 """Activates a plugin for all known plugin types.""" 329 types = Plugin.Type.__subclasses__() 330 modified = True 331 while modified: 332 modified = False 333 for child in types: 334 _, activated = _GetActivePlugin(child) 335 if activated: 336 modified = True 337