summaryrefslogtreecommitdiffstats
path: root/tools/cr/cr/plugin.py
blob: 1dbf9f5f8bd34c4d63aeb83c9e7c92451f083777 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# 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