Moved hooke.libpeakspot -> hooke.plugin.peakspot
[hooke.git] / hooke / plugin / __init__.py
1 """The `plugin` module provides optional submodules that add new Hooke
2 commands.
3
4 All of the science happens in here.
5 """
6
7 import ConfigParser as configparser
8
9 from ..config import Setting
10 from ..util.graph import Node, Graph
11
12
13 PLUGIN_MODULES = [
14 #    ('autopeak', True),
15 #    ('curvetools', True),
16     ('cut', True),
17     ('debug', True),
18 #    ('fit', True),
19 #    ('flatfilts-rolf', True),
20 #    ('flatfilts', True),
21 #    ('generalclamp', True),
22 #    ('generaltccd', True),
23 #    ('generalvclamp', True),
24 #    ('jumpstat', True),
25 #    ('macro', True),
26 #    ('massanalysis', True),
27 #    ('multidistance', True),
28 #    ('multifit', True),
29 #    ('pcluster', True),
30 #    ('peakspot', True),
31 #    ('procplots', True),
32 #    ('review', True),
33 #    ('showconvoluted', True),
34 #    ('superimpose', True),
35     ('system', True),
36 #    ('tutorial', True),
37     ]
38 """List of plugin modules and whether they should be included by
39 default.  TODO: autodiscovery
40 """
41
42 BUILTIN_MODULES = [
43     'playlist',
44     ]
45 """List of builtin modules.  TODO: autodiscovery
46 """
47
48 PLUGIN_SETTING_SECTION = 'plugins'
49 """Name of the config section which controls plugin selection.
50 """
51
52
53 # Plugins and settings
54
55 class Plugin (object):
56     """A pluggable collection of Hooke commands.
57
58     Fulfills the same role for Hooke that a software package does for
59     an operating system.
60     """
61     def __init__(self, name):
62         self.name = name
63         self.setting_section = '%s plugin' % self.name
64         self.config = {}
65
66     def dependencies(self):
67         """Return a list of :class:`Plugin`\s we require."""
68         return []
69
70     def default_settings(self):
71         """Return a list of :class:`hooke.config.Setting`\s for any
72         configurable plugin settings.
73
74         The suggested section setting is::
75
76             Setting(section=self.setting_section, help=self.__doc__)
77         """
78         return []
79
80     def commands(self):
81         """Return a list of :class:`hooke.command.Command`\s provided.
82         """
83         return []
84
85 class Builtin (Plugin):
86     """A required collection of Hooke commands.
87
88     These "core" plugins provide essential administrative commands
89     (playlist handling, etc.).
90     """
91     pass
92
93 # Construct plugin dependency graph and load plugin instances.
94
95 def construct_graph(this_modname, submodnames, class_selector,
96                     assert_name_match=True):
97     """Search the submodules `submodnames` of a module `this_modname`
98     for class objects for which `class_selector(class)` returns
99     `True`.  These classes are instantiated, and the `instance.name`
100     is compared to the `submodname` (if `assert_name_match` is
101     `True`).
102
103     The instances are further arranged into a dependency
104     :class:`hooke.util.graph.Graph` according to their
105     `instance.dependencies()` values.  The topologically sorted graph
106     is returned.
107     """
108     instances = {}
109     for submodname in submodnames:
110         count = len([s for s in submodnames if s == submodname])
111         assert count > 0, 'No %s entries: %s' % (submodname, submodnames)
112         assert count == 1, 'Multiple (%d) %s entries: %s' \
113             % (count, submodname, submodnames)
114         this_mod = __import__(this_modname, fromlist=[submodname])
115         submod = getattr(this_mod, submodname)
116         for objname in dir(submod):
117             obj = getattr(submod, objname)
118             if class_selector(obj):
119                 instance = obj()
120                 if assert_name_match == True and instance.name != submodname:
121                     raise Exception(
122                         'Instance name %s does not match module name %s'
123                         % (instance.name, submodname))
124                 instances[instance.name] = instance
125     graph = Graph([Node([instances[name] for name in i.dependencies()],
126                         data=i)
127                    for i in instances.values()])
128     graph.topological_sort()
129     return graph
130
131 class IsSubclass (object):
132     """A safe subclass comparator.
133     
134     Examples
135     --------
136
137     >>> class A (object):
138     ...     pass
139     >>> class B (A):
140     ...     pass
141     >>> C = 5
142     >>> is_subclass = IsSubclass(A)
143     >>> is_subclass(A)
144     True
145     >>> is_subclass = IsSubclass(A, blacklist=[A])
146     >>> is_subclass(A)
147     False
148     >>> is_subclass(B)
149     True
150     >>> is_subclass(C)
151     False
152     """
153     def __init__(self, base_class, blacklist=None):
154         self.base_class = base_class
155         if blacklist == None:
156             blacklist = []
157         self.blacklist = blacklist
158     def __call__(self, other):
159         try:
160             subclass = issubclass(other, self.base_class)
161         except TypeError:
162             return False
163         if other in self.blacklist:
164             return False
165         return subclass
166
167 PLUGIN_GRAPH = construct_graph(
168     this_modname=__name__,
169     submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES,
170     class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin]))
171 """Topologically sorted list of all possible :class:`Plugin`\s and
172 :class:`Builtin`\s.
173 """
174
175 def default_settings():
176     settings = [Setting(PLUGIN_SETTING_SECTION,
177                         help='Enable/disable default plugins.')]
178     for pnode in PLUGIN_GRAPH:
179         if pnode.data.name in BUILTIN_MODULES:
180             continue # builtin inclusion is not optional
181         plugin = pnode.data
182         default_include = [di for mod_name,di in PLUGIN_MODULES
183                            if mod_name == plugin.name][0]
184         help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()])
185         settings.append(Setting(
186                 section=PLUGIN_SETTING_SECTION,
187                 option=plugin.name,
188                 value=str(default_include),
189                 help=help,
190                 ))
191     for pnode in PLUGIN_GRAPH:
192         plugin = pnode.data
193         settings.extend(plugin.default_settings())
194     return settings
195
196 def load_graph(graph, config, include_section):
197     items = []
198     for node in graph:
199         item = node.data
200         try:
201             include = config.getboolean(include_section, item.name)
202         except configparser.NoOptionError:
203             include = True # non-optional include (e.g. a Builtin)
204         if include == True:
205             try:
206                 item.config = dict(config.items(item.setting_section))
207             except configparser.NoSectionError:
208                 pass
209             items.append(item)
210     return items