Moved hooke.plugin.fit -> polymer_fit & updated to Plugin/Command architecture.
[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 #    ('macro', True),
41 #    ('massanalysis', True),
42 #    ('multidistance', True),
43 #    ('multifit', True),
44 #    ('pcluster', True),
45     ('polymer_fit', True),
46 #    ('procplots', True),
47 #    ('review', True),
48 #    ('showconvoluted', True),
49 #    ('superimpose', True),
50 #    ('tccd', True),
51     ('tutorial', True),
52     ('vclamp', True),
53     ]
54 """List of plugin modules and whether they should be included by
55 default.  TODO: autodiscovery
56 """
57
58 BUILTIN_MODULES = [
59     'config',
60     'curve',
61     'debug',
62     'license',
63     'note',
64     'playlist',
65     'playlists',
66     'system',
67     ]
68 """List of builtin modules.  TODO: autodiscovery
69 """
70
71 PLUGIN_SETTING_SECTION = 'plugins'
72 """Name of the config section which controls plugin selection.
73 """
74
75
76 # Plugins and settings
77
78 class Plugin (object):
79     """A pluggable collection of Hooke commands.
80
81     Fulfills the same role for Hooke that a software package does for
82     an operating system: contains a chunk of related commands and
83     routines.  Command configuration also happens on the `Plugin`
84     level, with per-plugin sections in the configuration file.
85     """
86     def __init__(self, name):
87         self.name = name
88         self.setting_section = '%s plugin' % self.name
89         self.config = {}
90         self._commands = []
91
92     def dependencies(self):
93         """Return a list of names of :class:`Plugin`\s we require."""
94         return []
95
96     def default_settings(self):
97         """Return a list of :class:`hooke.config.Setting`\s for any
98         configurable plugin settings.
99
100         The suggested section setting is::
101
102             Setting(section=self.setting_section, help=self.__doc__)
103         """
104         return []
105
106     def commands(self):
107         """Return a list of :class:`hooke.command.Command`\s provided.
108         """
109         return list(self._commands)
110
111
112 class Builtin (Plugin):
113     """A required collection of Hooke commands.
114
115     These "core" plugins provide essential administrative commands
116     (playlist handling, etc.).
117     """
118     pass
119
120
121 # Plugin utility functions
122
123 def argument_to_setting(section_name, argument):
124     """Convert an :class:`~hooke.command.Argument` to a
125     `~hooke.conf.Setting`.
126
127     This is useful if, for example, you want to define arguments with
128     configurable default values.
129
130     Conversion is lossy transition, because
131     :class:`~hooke.command.Argument`\s store more information than
132     `~hooke.conf.Setting`\s.
133     """
134     return Setting(section_name, option=argument.name, value=argument.default,
135                    type=argument.type, count=argument.count,
136                    help=argument._help)
137
138
139 # Construct plugin dependency graph and load plugin instances.
140
141 PLUGIN_GRAPH = construct_graph(
142     this_modname=__name__,
143     submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES,
144     class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin]))
145 """Topologically sorted list of all possible :class:`Plugin`\s and
146 :class:`Builtin`\s.
147 """
148
149 def default_settings():
150     settings = [Setting(PLUGIN_SETTING_SECTION,
151                         help='Enable/disable default plugins.')]
152     for pnode in PLUGIN_GRAPH:
153         if pnode.data.name in BUILTIN_MODULES:
154             continue # builtin inclusion is not optional
155         plugin = pnode.data
156         default_include = [di for mod_name,di in PLUGIN_MODULES
157                            if mod_name == plugin.name][0]
158         help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()])
159         settings.append(Setting(
160                 section=PLUGIN_SETTING_SECTION,
161                 option=plugin.name,
162                 value=str(default_include),
163                 help=help,
164                 ))
165     for pnode in PLUGIN_GRAPH:
166         plugin = pnode.data
167         settings.extend(plugin.default_settings())
168     return settings
169
170 def load_graph(graph, config, include_section):
171     enabled = {}
172     items = []
173     conditions = config.items('conditions')
174     for node in graph:
175         item = node.data
176         try:
177             include = config.getboolean(include_section, item.name)
178         except configparser.NoOptionError:
179             include = True # non-optional include (e.g. a Builtin)
180         enabled[item.name] = include
181         if include == True:
182             for dependency in node:
183                 if enabled.get(dependency.data.name, None) != True:
184                     log = logging.getLogger('hooke')
185                     log.warn(
186                      'could not setup plugin %s.  unsatisfied dependency on %s.'
187                      % (item.name, dependency.data.name))
188                     enabled[item.name] = False
189                     continue
190             try:
191                 item.config = dict(config.items(item.setting_section))
192             except configparser.NoSectionError:
193                 item.config = {}
194             for key,value in conditions:
195                 if key not in item.config:
196                     item.config[key] = value
197             items.append(item)
198     return items