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