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