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