Oops. Fix "'." -> ".'" in dependency warning message.
[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 #    ('fit', True),
38 #    ('flatfilts-rolf', True),
39     ('flatfilt', True),
40 #    ('jumpstat', True),
41 #    ('macro', True),
42 #    ('massanalysis', True),
43 #    ('multidistance', True),
44 #    ('multifit', True),
45 #    ('pcluster', 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 a 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                    help=argument._help)
133
134
135 # Construct plugin dependency graph and load plugin instances.
136
137 PLUGIN_GRAPH = construct_graph(
138     this_modname=__name__,
139     submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES,
140     class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin]))
141 """Topologically sorted list of all possible :class:`Plugin`\s and
142 :class:`Builtin`\s.
143 """
144
145 def default_settings():
146     settings = [Setting(PLUGIN_SETTING_SECTION,
147                         help='Enable/disable default plugins.')]
148     for pnode in PLUGIN_GRAPH:
149         if pnode.data.name in BUILTIN_MODULES:
150             continue # builtin inclusion is not optional
151         plugin = pnode.data
152         default_include = [di for mod_name,di in PLUGIN_MODULES
153                            if mod_name == plugin.name][0]
154         help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()])
155         settings.append(Setting(
156                 section=PLUGIN_SETTING_SECTION,
157                 option=plugin.name,
158                 value=str(default_include),
159                 help=help,
160                 ))
161     for pnode in PLUGIN_GRAPH:
162         plugin = pnode.data
163         settings.extend(plugin.default_settings())
164     return settings
165
166 def load_graph(graph, config, include_section):
167     enabled = {}
168     items = []
169     for node in graph:
170         item = node.data
171         try:
172             include = config.getboolean(include_section, item.name)
173         except configparser.NoOptionError:
174             include = True # non-optional include (e.g. a Builtin)
175         enabled[item.name] = include
176         print 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             try:
187                 item.config = dict(config.items(item.setting_section))
188             except configparser.NoSectionError:
189                 pass
190             items.append(item)
191     return items