1 """The plugin module provides optional submodules that add new Hooke
4 All of the science happens in here.
10 from ..config import Setting
11 from ..util.graph import Node, Graph
15 # ('curvetools', True),
18 # ('flatfilts-rolf', True),
19 # ('flatfilts', True),
20 # ('generalclamp', True),
21 # ('generaltccd', True),
22 # ('generalvclamp', True),
25 # ('massanalysis', True),
26 # ('multidistance', True),
29 # ('procplots', True),
31 # ('showconvoluted', True),
32 # ('superimpose', True),
36 """List of plugin modules and whether they should be included by
37 default. TODO: autodiscovery
40 # Plugins and settings
42 class Plugin (object):
43 """The pluggable collection of Hooke commands.
45 Fulfills the same role for Hooke that a software package does for
48 def __init__(self, name):
51 def dependencies(self):
52 """Return a list of :class:`Plugin`\s we require."""
55 def default_settings(self):
56 """Return a list of :class:`hooke.config.Setting`\s for any
57 configurable plugin settings.
59 The suggested section setting is::
61 Setting(section='%s plugin' % self.name, help=self.__doc__)
66 """Return a list of :class:`Commands` provided."""
71 # Commands and arguments
73 class CommandExit (Exception):
75 return self.__class__.__name__
77 class Success (CommandExit):
80 class Failure (CommandExit):
83 class Command (object):
84 """One-line command description here.
86 >>> c = Command(name='test', help='An example Command.')
87 >>> status = c.run(NullQueue(), PrintQueue(), help=True)
92 help HELP (bool) Print a help message.
98 def __init__(self, name, aliases=None, arguments=[], help=''):
102 self.aliases = aliases
104 Argument(name='help', type='bool', default=False, count=1,
105 callback=StoreValue(True), help='Print a help message.'),
109 def run(self, inqueue=None, outqueue=None, **kwargs):
110 """`Normalize inputs and handle <Argument help> before punting
114 inqueue = NullQueue()
116 outqueue = NullQueue()
118 params = self.handle_arguments(inqueue, outqueue, kwargs)
119 if params['help'] == True:
120 outqueue.put(self.help())
122 self._run(inqueue, outqueue, params)
123 except CommandExit, e:
124 if isinstance(e, Failure):
125 outqueue.put(e.message)
131 def _run(self, inqueue, outqueue, params):
132 """This is where the command-specific magic will happen.
136 def handle_arguments(self, inqueue, outqueue, params):
137 """Normalize and validate input parameters (:class:`Argument` values).
139 for argument in self.arguments:
140 names = [argument.name] + argument.aliases
141 settings = [(name,v) for name,v in params.items() if name in names]
142 if len(settings) == 0:
143 if argument.optional == True or argument.count == 0:
144 settings = [(argument.name, argument.default)]
146 raise Failure('Required argument %s not set.'
148 if len(settings) > 1:
149 raise Failure('Multiple settings for %s:\n %s'
151 '\n '.join(['%s: %s' % (name,value)
152 for name,value in sorted(settings)])))
153 name,value = settings[0]
154 if name != argument.name:
156 params[argument.name] = value
157 if argument.callback != None:
158 value = argument.callback(self, argument, value)
159 params[argument.name] = value
160 argument.validate(value)
163 def help(self, *args):
164 name_part = 'Command: %s' % self.name
165 if len(self.aliases) > 0:
166 name_part += ' (%s)' % ', '.join(self.aliases)
167 argument_part = ['Arguments:'] + [a.help() for a in self.arguments]
168 argument_part = '\n'.join(argument_part)
169 help_part = self._help
170 return '\n\n'.join([name_part, argument_part, help_part])
172 class Argument (object):
173 """Structured user input for :class:`Command`\s.
175 TODO: ranges for `count`?
177 def __init__(self, name, aliases=None, type='string', metavar=None,
178 default=None, optional=True, count=1,
179 completion_callback=None, callback=None, help=''):
183 self.aliases = aliases
186 metavar = type.upper()
187 self.metavar = metavar
188 self.default = default
189 self.optional = optional
191 self.completion_callback = completion_callback
192 self.callback = callback
196 return '<%s %s>' % (self.__class__.__name__, self.name)
199 return self.__str__()
202 parts = ['%s ' % self.name]
203 if self.metavar != None:
204 parts.append('%s ' % self.metavar)
205 parts.extend(['(%s) ' % self.type, self._help])
206 return ''.join(parts)
208 def validate(self, value):
209 """If `value` is not appropriate, raise `ValueError`.
211 pass # TODO: validation
213 # TODO: type conversion
215 # TODO: type extensions?
219 class StoreValue (object):
220 def __init__(self, value):
222 def __call__(self, command, argument, fragment=None):
225 class NullQueue (queue.Queue):
226 """The :class:`queue.Queue` equivalent of `/dev/null`.
228 This is a bottomless pit. Items go in, but never come out.
230 def get(self, block=True, timeout=None):
231 """Raise queue.Empty.
233 There's really no need to override the base Queue.get, but I
234 want to know if someone tries to read from a NullQueue. With
235 the default implementation they would just block silently
240 def put(self, item, block=True, timeout=None):
241 """Dump an item into the void.
243 Block and timeout are meaningless, because there is always a
244 free slot available in a bottomless pit.
248 class PrintQueue (NullQueue):
249 """Debugging :class:`NullQueue` that prints items before dropping
252 def put(self, item, block=True, timeout=None):
253 """Print `item` and then dump it into the void.
255 print 'ITEM:\n%s' % item
258 # Construct plugin dependency graph and load default plugins.
261 """(name,instance) :class:`dict` of all possible :class:`Plugin`\s.
264 for plugin_modname,default_include in PLUGIN_MODULES:
265 assert len([mod_name for mod_name,di in PLUGIN_MODULES]) == 1, \
266 'Multiple %s entries in PLUGIN_MODULES' % mod_name
267 this_mod = __import__(__name__, fromlist=[plugin_modname])
268 plugin_mod = getattr(this_mod, plugin_modname)
269 for objname in dir(plugin_mod):
270 obj = getattr(plugin_mod, objname)
272 subclass = issubclass(obj, Plugin)
275 if subclass == True and obj != Plugin:
277 if p.name != plugin_modname:
278 raise Exception('Plugin name %s does not match module name %s'
279 % (p.name, plugin_modname))
282 PLUGIN_GRAPH = Graph([Node([PLUGINS[name] for name in p.dependencies()],
284 for p in PLUGINS.values()])
285 PLUGIN_GRAPH.topological_sort()
288 def default_settings():
290 'plugins', help='Enable/disable default plugins.')]
291 for pnode in PLUGIN_GRAPH:
293 default_include = [di for mod_name,di in PLUGIN_MODULES
294 if mod_name == plugin.name][0]
295 help = 'Commands: ' + ', '.join([c.name for c in p.commands()])
296 settings.append(Setting(
299 value=str(default_include),
302 for pnode in PLUGIN_GRAPH:
304 settings.extend(plugin.default_settings())