Run update-copyright.py.
[hooke.git] / hooke / plugin / __init__.py
1 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
8 # later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
17
18 """The `plugin` module provides optional submodules that add new Hooke
19 commands.
20
21 All of the science happens in here.
22 """
23
24 import ConfigParser as configparser
25 import logging
26
27 from ..config import Setting
28 from ..util.pluggable import IsSubclass, construct_graph
29
30
31 PLUGIN_MODULES = [
32 #    ('autopeak', True),
33     ('convfilt', True),
34     ('cut', True),
35 #    ('fclamp', True),
36     ('flatfilt', True),
37 #    ('jumpstat', True),
38 #    ('massanalysis', True),
39 #    ('multidistance', True),
40 #    ('multifit', True),
41 #    ('pcluster', True),
42     ('polymer_fit', True),
43 #    ('showconvoluted', True),
44 #    ('superimpose', True),
45 #    ('tccd', True),
46     ('tutorial', True),
47     ('vclamp', True),
48     ]
49 """List of plugin modules and whether they should be included by
50 default.  TODO: autodiscovery
51 """
52
53 BUILTIN_MODULES = [
54     'command_stack',
55     'config',
56     'curve',
57     'debug',
58     'engine',
59     'license',
60     'note',
61     'playlist',
62     'playlists',
63     'system',
64     ]
65 """List of builtin modules.  TODO: autodiscovery
66 """
67
68 PLUGIN_SETTING_SECTION = 'plugins'
69 """Name of the config section which controls plugin selection.
70 """
71
72
73 # Plugins and settings
74
75 class Plugin (object):
76     """A pluggable collection of Hooke commands.
77
78     Fulfills the same role for Hooke that a software package does for
79     an operating system: contains a chunk of related commands and
80     routines.  Command configuration also happens on the `Plugin`
81     level, with per-plugin sections in the configuration file.
82     """
83     def __init__(self, name):
84         self.name = name
85         self.setting_section = '%s plugin' % self.name
86         self.config = {}
87         self._commands = []
88
89     def dependencies(self):
90         """Return a list of names of :class:`Plugin`\s we require."""
91         return []
92
93     def default_settings(self):
94         """Return a list of :class:`hooke.config.Setting`\s for any
95         configurable plugin settings.
96
97         The suggested section setting is::
98
99             Setting(section=self.setting_section, help=self.__doc__)
100         """
101         return []
102
103     def commands(self):
104         """Return a list of :class:`hooke.command.Command`\s provided.
105         """
106         return list(self._commands)
107
108
109 class Builtin (Plugin):
110     """A required collection of Hooke commands.
111
112     These "core" plugins provide essential administrative commands
113     (playlist handling, etc.).
114     """
115     pass
116
117
118 # Plugin utility functions
119
120 def argument_to_setting(section_name, argument):
121     """Convert an :class:`~hooke.command.Argument` to a
122     `~hooke.conf.Setting`.
123
124     This is useful if, for example, you want to define arguments with
125     configurable default values.
126
127     Conversion is lossy transition, because
128     :class:`~hooke.command.Argument`\s store more information than
129     `~hooke.conf.Setting`\s.
130     """
131     return Setting(section_name, option=argument.name, value=argument.default,
132                    type=argument.type, count=argument.count,
133                    help=argument._help)
134
135
136 # Construct plugin dependency graph and load plugin instances.
137
138 PLUGIN_GRAPH = construct_graph(
139     this_modname=__name__,
140     submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES,
141     class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin]))
142 """Topologically sorted list of all possible :class:`Plugin`\s and
143 :class:`Builtin`\s.
144 """
145
146 def default_settings():
147     settings = [Setting(PLUGIN_SETTING_SECTION,
148                         help='Enable/disable default plugins.')]
149     for pnode in PLUGIN_GRAPH:
150         if pnode.data.name in BUILTIN_MODULES:
151             continue # builtin inclusion is not optional
152         plugin = pnode.data
153         default_include = [di for mod_name,di in PLUGIN_MODULES
154                            if mod_name == plugin.name][0]
155         help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()])
156         settings.append(Setting(
157                 section=PLUGIN_SETTING_SECTION,
158                 option=plugin.name,
159                 value=str(default_include),
160                 help=help,
161                 ))
162     for pnode in PLUGIN_GRAPH:
163         plugin = pnode.data
164         settings.extend(plugin.default_settings())
165     return settings
166
167 def load_graph(graph, config, include_section):
168     enabled = {}
169     items = []
170     for node in graph:
171         item = node.data
172         try:
173             include = config.getboolean(include_section, item.name)
174         except configparser.NoOptionError:
175             include = True # non-optional include (e.g. a Builtin)
176         enabled[item.name] = include
177         if include == True:
178             for dependency in node:
179                 if enabled.get(dependency.data.name, None) != True:
180                     log = logging.getLogger('hooke')
181                     log.warn(
182                      'could not setup plugin %s.  unsatisfied dependency on %s.'
183                      % (item.name, dependency.data.name))
184                     enabled[item.name] = False
185                     continue
186             items.append(item)
187     return items