blob: 1dbf9f5f8bd34c4d63aeb83c9e7c92451f083777 [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""The plugin management system for the cr tool.
This holds the Plugin class and supporting code, that controls how plugins are
found and used.
The module registers a scan hook with the cr.loader system to enable it to
discover plugins as they are loaded.
"""
from operator import attrgetter
import cr
import cr.loader
def _PluginConfig(name, only_enabled=False, only_active=False):
config = cr.Config(name)
config.only_active = only_active
config.only_enabled = only_enabled or config.only_active
config.property_name = name.lower() + '_config'
return config
_selectors = cr.Config('PRIORITY')
CONFIG_TYPES = [
# Lowest priority, always there default values.
_PluginConfig('DEFAULT').AddChild(_selectors),
# Only turned on if the plugin is enabled.
_PluginConfig('ENABLED', only_enabled=True),
# Only turned on while the plugin is the active one.
_PluginConfig('ACTIVE', only_active=True),
# Holds detected values for active plugins.
_PluginConfig('DETECTED', only_active=True),
# Holds overrides, used in custom setup plugins.
_PluginConfig('OVERRIDES'),
]
cr.config.GLOBALS.extend(CONFIG_TYPES)
_plugins = {}
# Actually a decorator, so pylint: disable=invalid-name
class classproperty(object):
"""This adds a property to a class.
This is like a simple form of @property except it is for the class, rather
than instances of the class. Only supports readonly properties.
"""
def __init__(self, getter):
self.getter = getter
def __get__(self, instance, owner):
return self.getter(owner)
class DynamicChoices(object):
"""Manages the list of active plugins for command line options.
Looks like a simple iterable, but it can change as the underlying plugins
arrive and enable/disable themselves. This allows it to be used as the
set of valid choices for the argparse command line options.
"""
# If this is True, all DynamicChoices only return active plugins.
# If false, all plugins are included.
only_active = True
def __init__(self, cls):
self.cls = cls
def __contains__(self, name):
return self.cls.FindPlugin(name, self.only_active) is not None
def __iter__(self):
return [p.name for p in self.cls.Plugins()].__iter__()
def _FindRoot(cls):
if Plugin.Type in cls.__bases__:
return cls
for base in cls.__bases__:
result = _FindRoot(base)
if result is not None:
return result
return None
class Plugin(cr.loader.AutoExport):
"""Base class for managing registered plugin types."""
class Type(object):
"""Base class that tags a class as an abstract plugin type."""
class activemethod(object):
"""A decorator that delegates a static method to the active plugin.
Makes a static method that delegates to the equivalent method on the
active instance of the plugin type.
"""
def __init__(self, method):
self.method = method
def __get__(self, instance, owner):
def unbound(*args, **kwargs):
active = owner.GetActivePlugin()
if not active:
print 'No active', owner.__name__
exit(1)
method = getattr(active, self.method.__name__, None)
if not method:
print owner.__name__, 'does not support', self.method.__name__
exit(1)
return method(*args, **kwargs)
def bound(*args, **kwargs):
return self.method(instance, *args, **kwargs)
if instance is None:
return unbound
return bound
def __init__(self):
# Default the name to the lowercased class name.
self._name = self.__class__.__name__.lower()
# Strip the common suffix if present.
self._root = _FindRoot(self.__class__)
rootname = self._root.__name__.lower()
if self._name.endswith(rootname) and self.__class__ != self._root:
self._name = self._name[:-len(rootname)]
for config_root in CONFIG_TYPES:
config = cr.Config()
setattr(self, config_root.property_name, config)
self._is_active = False
def Init(self):
"""Post plugin registration initialisation method."""
for config_root in CONFIG_TYPES:
config = getattr(self, config_root.property_name)
config.name = self.name
if config_root.only_active and not self.is_active:
config.enabled = False
if config_root.only_enabled and not self.enabled:
config.enabled = False
child = getattr(self.__class__, config_root.name, None)
if child is not None:
child.name = self.__class__.__name__
config.AddChild(child)
config_root.AddChild(config)
@property
def name(self):
return self._name
@property
def priority(self):
return 0
@property
def enabled(self):
# By default all non type classes are enabled.
return Plugin.Type not in self.__class__.__bases__
@property
def is_active(self):
return self._is_active
def Activate(self):
assert not self._is_active
self._is_active = True
for config_root in CONFIG_TYPES:
if config_root.only_active:
getattr(self, config_root.property_name).enabled = True
def Deactivate(self):
assert self._is_active
self._is_active = False
for config_root in CONFIG_TYPES:
if config_root.only_active:
getattr(self, config_root.property_name).enabled = False
@classmethod
def ClassInit(cls):
pass
@classmethod
def GetInstance(cls):
"""Gets an instance of this plugin.
This looks in the plugin registry, and if an instance is not found a new
one is built and registered.
Returns:
The registered plugin instance.
"""
plugin = _plugins.get(cls, None)
if plugin is None:
# Run delayed class initialization
cls.ClassInit()
# Build a new instance of cls, and register it as the main instance.
plugin = cls()
_plugins[cls] = plugin
# Wire up the hierarchy for Config objects.
for name, value in cls.__dict__.items():
if isinstance(value, cr.Config):
for base in cls.__bases__:
child = getattr(base, name, None)
if child is not None:
value.AddChild(child)
plugin.Init()
return plugin
@classmethod
def AllPlugins(cls):
# Don't yield abstract roots, just children. We detect roots as direct
# sub classes of Plugin.Type
if Plugin.Type not in cls.__bases__:
yield cls.GetInstance()
for child in cls.__subclasses__():
for p in child.AllPlugins():
yield p
@classmethod
def UnorderedPlugins(cls):
"""Returns all enabled plugins of type cls, in undefined order."""
plugin = cls.GetInstance()
if plugin.enabled:
yield plugin
for child in cls.__subclasses__():
for p in child.UnorderedPlugins():
yield p
@classmethod
def Plugins(cls):
"""Return all enabled plugins of type cls in priority order."""
return sorted(cls.UnorderedPlugins(),
key=attrgetter('priority'), reverse=True)
@classmethod
def Choices(cls):
return DynamicChoices(cls)
@classmethod
def FindPlugin(cls, name, only_active=True):
if only_active:
plugins = cls.UnorderedPlugins()
else:
plugins = cls.AllPlugins()
for plugin in plugins:
if plugin.name == name or plugin.__class__.__name__ == name:
return plugin
return None
@classmethod
def GetPlugin(cls, name):
result = cls.FindPlugin(name)
if result is None:
raise KeyError(name)
return result
@classmethod
def GetAllActive(cls):
return [plugin for plugin in cls.UnorderedPlugins() if plugin.is_active]
@classmethod
def GetActivePlugin(cls):
"""Gets the active plugin of type cls.
This method will select a plugin to be the active one, and will activate
the plugin if needed.
Returns:
the plugin that is currently active.
"""
plugin, _ = _GetActivePlugin(cls)
return plugin
@classproperty
def default(cls):
"""Returns the plugin that should be used if the user did not choose one."""
result = None
for plugin in cls.UnorderedPlugins():
if not result or plugin.priority > result.priority:
result = plugin
return result
@classmethod
def Select(cls):
"""Called to determine which plugin should be the active one."""
plugin = cls.default
selector = getattr(cls, 'SELECTOR', None)
if selector:
if plugin is not None:
_selectors[selector] = plugin.name
name = cr.context.Find(selector)
if name is not None:
plugin = cls.FindPlugin(name)
return plugin
def ChainModuleConfigs(module):
"""Detects and connects the default Config objects from a module."""
for config_root in CONFIG_TYPES:
if hasattr(module, config_root.name):
config = getattr(module, config_root.name)
config.name = module.__name__
config_root.AddChild(config)
cr.loader.scan_hooks.append(ChainModuleConfigs)
def _GetActivePlugin(cls):
activated = False
actives = cls.GetAllActive()
plugin = cls.Select()
for active in actives:
if active != plugin:
active.Deactivate()
if plugin and not plugin.is_active:
activated = True
plugin.Activate()
return plugin, activated
def Activate():
"""Activates a plugin for all known plugin types."""
types = Plugin.Type.__subclasses__()
modified = True
while modified:
modified = False
for child in types:
_, activated = _GetActivePlugin(child)
if activated:
modified = True