c3b25b7d3bdfea4f0fc21fde0087834c28f7a7b0
[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 os.path
8 import Queue as queue
9
10 from ..config import Setting
11 from ..util.graph import Node, Graph
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 #    ('procplots', True),
30 #    ('review', True),
31 #    ('showconvoluted', True),
32 #    ('superimpose', True),
33 #    ('tutorial', True),
34 #    ('viewer', True),
35     ]
36 """List of plugin modules and whether they should be included by
37 default.  TODO: autodiscovery
38 """
39
40 # Plugins and settings
41
42 def Plugin (object):
43     """The pluggable collection of Hooke commands.
44
45     Fulfills the same role for Hooke that a software package does for
46     an operating system.
47     """
48     name = "base plugin"
49
50     def dependencies(self):
51         """Return a list of Plugins we require."""
52         return []
53
54     def default_settings(self):
55         """Return a list of hooke.config.Settings() for any
56         configurable module settings."""
57         return []
58
59     def commands(self):
60         """Return a list of Commands provided."""
61         return []
62
63 PLUGINS = {}
64 """(name,instance) :class:`dict` of all possible :class:`Plugin`\s.
65 """
66
67 print __name__
68 for plugin_modname,value in PLUGIN_MODULES:
69     this_mod = __import__(__name__, fromlist=[plugin_modname])
70     plugin_mod = getattr(this_mod, plugin_modname)
71     for objname in dir(plugin_mod):
72         obj = getattr(plugin_mod, objname)
73         if type(obj) == Plugin:
74             obj.module_name = plugin_modname
75             PLUGINS[p.name] = p
76
77 PLUGIN_GRAPH = Graph([Node([PLUGINS[name] for name in p.dependencies()])
78                       for p in PLUGINS.values()])
79 PLUGIN_GRAPH.topological_sort()
80
81
82 def default_settings(self):
83     settings = [Setting(
84             'plugins', help='Enable/disable default plugins.')]
85     for pnode in PLUGIN_GRAPH:
86         settings.append(Setting(p.name, str(PLUGIN_MODULES[p.module_name][1])))      
87     for pnode in PLUGIN_GRAPH:
88         plugin = pnode.data
89         settings.extend(plugin.default_settings())
90     return settings
91
92
93 # Commands and arguments
94
95 class CommandExit (Exception):
96     def __str__(self):
97         return self.__class__.__name__
98
99 class Success (CommandExit):
100     pass
101
102 class Failure (CommandExit):
103     pass
104
105 class Command (object):
106     """One-line command description here.
107
108     >>> c = Command(name='test', help='An example Command.')
109     >>> status = c.run(NullQueue(), PrintQueue(), help=True)
110     ITEM:
111     Command: test
112     <BLANKLINE>
113     Arguments:
114     help HELP (bool) Print a help message.
115     <BLANKLINE>
116     An example Command.
117     ITEM:
118     Success
119     """
120     def __init__(self, name, arguments=[], aliases=None, help=''):
121         self.name = name
122         self.arguments = [
123             Argument(name='help', type='bool', default=False, count=1,
124                      callback=StoreValue(True), help='Print a help message.'),
125             ] + arguments
126         if aliases == None:
127             aliases = []
128         self.aliases = aliases
129         self._help = help
130
131     def run(self, inqueue=None, outqueue=None, **kwargs):
132         """`Normalize inputs and handle <Argument help> before punting
133         to :meth:`_run`.
134         """
135         if inqueue == None:
136             inqueue = NullQueue()
137         if outqueue == None:
138             outqueue = NullQueue()
139         try:
140             params = self.handle_arguments(inqueue, outqueue, kwargs)
141             if params['help'] == True:
142                 outqueue.put(self.help())
143                 raise(Success())
144             self._run(inqueue, outqueue, params)
145         except CommandExit, e:
146             if isinstance(e, Failure):
147                 outqueue.put(e.message)
148                 outqueue.put(e)
149                 return 1
150         outqueue.put(e)
151         return 0
152
153     def _run(self, inqueue, outqueue, params):
154         """This is where the command-specific magic will happen.
155         """
156         pass
157
158     def handle_arguments(self, inqueue, outqueue, params):
159         """Normalize and validate input parameters (:class:`Argument` values).
160         """
161         for argument in self.arguments:
162             names = [argument.name] + argument.aliases
163             settings = [(name,v) for name,v in params.items() if name in names]
164             if len(settings) == 0:
165                 if argument.optional == True or argument.count == 0:
166                     settings = [(argument.name, argument.default)]
167                 else:
168                     raise Failure('Required argument %s not set.'
169                                   % argument.name)
170             if len(settings) > 1:
171                 raise Failure('Multiple settings for %s:\n  %s'
172                     % (argument.name,
173                        '\n  '.join(['%s: %s' % (name,value)
174                                     for name,value in sorted(settings)])))
175             name,value = settings[0]
176             if name != argument.name:
177                 params.remove(name)
178                 params[argument.name] = value
179             if argument.callback != None:
180                 value = argument.callback(self, argument, value)
181                 params[argument.name] = value
182             argument.validate(value)
183         return params
184
185     def help(self, *args):
186         name_part = 'Command: %s' % self.name
187         if len(self.aliases) > 0:
188             name_part += ' (%s)' % ', '.join(self.aliases)
189         argument_part = ['Arguments:'] + [a.help() for a in self.arguments]
190         argument_part = '\n'.join(argument_part)
191         help_part = self._help
192         return '\n\n'.join([name_part, argument_part, help_part])
193
194 class Argument (object):
195     """Structured user input for :class:`Command`\s.
196     
197     TODO: ranges for `count`?
198     """
199     def __init__(self, name, type='string', metavar=None, default=None, 
200                  optional=True, count=1, completion_callback=None,
201                  callback=None, aliases=None, help=''):
202         self.name = name
203         self.type = type
204         if metavar == None:
205             metavar = name.upper()
206         self.metavar = metavar
207         self.default = default
208         self.optional = optional
209         self.count = count
210         self.completion_callback = completion_callback
211         self.callback = callback
212         if aliases == None:
213             aliases = []
214         self.aliases = aliases
215         self._help = help
216
217     def __str__(self):
218         return '<%s %s>' % (self.__class__.__name__, self.name)
219
220     def __repr__(self):
221         return self.__str__()
222
223     def help(self):
224         parts = ['%s ' % self.name]
225         if self.metavar != None:
226             parts.append('%s ' % self.metavar)
227         parts.extend(['(%s) ' % self.type, self._help])
228         return ''.join(parts)
229
230     def validate(self, value):
231         """If `value` is not appropriate, raise `ValueError`.
232         """
233         pass # TODO: validation
234
235     # TODO: type conversion
236
237 # TODO: type extensions?
238
239 # Useful callbacks
240
241 class StoreValue (object):
242     def __init__(self, value):
243         self.value = value
244     def __call__(self, command, argument, fragment=None):
245         return self.value
246
247 class NullQueue (queue.Queue):
248     """The :class:`queue.Queue` equivalent of `/dev/null`.
249
250     This is a bottomless pit.  Items go in, but never come out.
251     """
252     def get(self, block=True, timeout=None):
253         """Raise queue.Empty.
254         
255         There's really no need to override the base Queue.get, but I
256         want to know if someone tries to read from a NullQueue.  With
257         the default implementation they would just block silently
258         forever :(.
259         """
260         raise queue.Empty
261
262     def put(self, item, block=True, timeout=None):
263         """Dump an item into the void.
264
265         Block and timeout are meaningless, because there is always a
266         free slot available in a bottomless pit.
267         """
268         pass
269
270 class PrintQueue (NullQueue):
271     """Debugging :class:`NullQueue` that prints items before dropping
272     them.
273     """
274     def put(self, item, block=True, timeout=None):
275         """Print `item` and then dump it into the void.
276         """
277         print 'ITEM:\n%s' % item