1 """The plugin module provides optional submodules that add new Hooke
4 All of the science happens in here.
9 from ..config import Setting
10 from ..util.graph import Node, Graph
14 # ('curvetools', True),
17 # ('flatfilts-rolf', True),
18 # ('flatfilts', True),
19 # ('generalclamp', True),
20 # ('generaltccd', True),
21 # ('generalvclamp', True),
24 # ('massanalysis', True),
25 # ('multidistance', True),
28 # ('procplots', True),
30 # ('showconvoluted', True),
31 # ('superimpose', True),
35 """List of plugin modules and whether they should be included by
36 default. TODO: autodiscovery
42 """List of builtin modules. TODO: autodiscovery
46 # Plugins and settings
48 class Plugin (object):
49 """A pluggable collection of Hooke commands.
51 Fulfills the same role for Hooke that a software package does for
54 def __init__(self, name):
56 self.setting_section = '%s plugin' % self.name
59 def dependencies(self):
60 """Return a list of :class:`Plugin`\s we require."""
63 def default_settings(self):
64 """Return a list of :class:`hooke.config.Setting`\s for any
65 configurable plugin settings.
67 The suggested section setting is::
69 Setting(section=self.setting_section, help=self.__doc__)
74 """Return a list of :class:`Commands` provided."""
77 class Builtin (Plugin):
78 """A required collection of Hooke commands.
80 These "core" plugins provide essential administrative commands
81 (playlist handling, etc.).
85 # Commands and arguments
87 class CommandExit (Exception):
89 return self.__class__.__name__
91 class Success (CommandExit):
94 class Failure (CommandExit):
97 class Command (object):
98 """One-line command description here.
100 >>> c = Command(name='test', help='An example Command.')
101 >>> status = c.run(NullQueue(), PrintQueue(), help=True) # doctest: +REPORT_UDIFF
106 help BOOL (bool) Print a help message.
112 def __init__(self, name, aliases=None, arguments=[], help=''):
116 self.aliases = aliases
118 Argument(name='help', type='bool', default=False, count=1,
119 callback=StoreValue(True), help='Print a help message.'),
123 def run(self, inqueue=None, outqueue=None, **kwargs):
124 """`Normalize inputs and handle <Argument help> before punting
128 inqueue = NullQueue()
130 outqueue = NullQueue()
132 params = self.handle_arguments(inqueue, outqueue, kwargs)
133 if params['help'] == True:
134 outqueue.put(self.help())
136 self._run(inqueue, outqueue, params)
137 except CommandExit, e:
138 if isinstance(e, Failure):
139 outqueue.put(e.message)
145 def _run(self, inqueue, outqueue, params):
146 """This is where the command-specific magic will happen.
150 def handle_arguments(self, inqueue, outqueue, params):
151 """Normalize and validate input parameters (:class:`Argument` values).
153 for argument in self.arguments:
154 names = [argument.name] + argument.aliases
155 settings = [(name,v) for name,v in params.items() if name in names]
156 if len(settings) == 0:
157 if argument.optional == True or argument.count == 0:
158 settings = [(argument.name, argument.default)]
160 raise Failure('Required argument %s not set.'
162 if len(settings) > 1:
163 raise Failure('Multiple settings for %s:\n %s'
165 '\n '.join(['%s: %s' % (name,value)
166 for name,value in sorted(settings)])))
167 name,value = settings[0]
168 if name != argument.name:
170 params[argument.name] = value
171 if argument.callback != None:
172 value = argument.callback(self, argument, value)
173 params[argument.name] = value
174 argument.validate(value)
177 def help(self, *args):
178 name_part = 'Command: %s' % self.name
179 if len(self.aliases) > 0:
180 name_part += ' (%s)' % ', '.join(self.aliases)
181 argument_part = ['Arguments:'] + [a.help() for a in self.arguments]
182 argument_part = '\n'.join(argument_part)
183 help_part = self._help
184 return '\n\n'.join([name_part, argument_part, help_part])
186 class Argument (object):
187 """Structured user input for :class:`Command`\s.
189 TODO: ranges for `count`?
191 def __init__(self, name, aliases=None, type='string', metavar=None,
192 default=None, optional=True, count=1,
193 completion_callback=None, callback=None, help=''):
197 self.aliases = aliases
200 metavar = type.upper()
201 self.metavar = metavar
202 self.default = default
203 self.optional = optional
205 self.completion_callback = completion_callback
206 self.callback = callback
210 return '<%s %s>' % (self.__class__.__name__, self.name)
213 return self.__str__()
216 parts = ['%s ' % self.name]
217 if self.metavar != None:
218 parts.append('%s ' % self.metavar)
219 parts.extend(['(%s) ' % self.type, self._help])
220 return ''.join(parts)
222 def validate(self, value):
223 """If `value` is not appropriate, raise `ValueError`.
225 pass # TODO: validation
227 # TODO: type conversion
229 # TODO: type extensions?
233 class StoreValue (object):
234 def __init__(self, value):
236 def __call__(self, command, argument, fragment=None):
239 class NullQueue (queue.Queue):
240 """The :class:`queue.Queue` equivalent of `/dev/null`.
242 This is a bottomless pit. Items go in, but never come out.
244 def get(self, block=True, timeout=None):
245 """Raise queue.Empty.
247 There's really no need to override the base Queue.get, but I
248 want to know if someone tries to read from a NullQueue. With
249 the default implementation they would just block silently
254 def put(self, item, block=True, timeout=None):
255 """Dump an item into the void.
257 Block and timeout are meaningless, because there is always a
258 free slot available in a bottomless pit.
262 class PrintQueue (NullQueue):
263 """Debugging :class:`NullQueue` that prints items before dropping
266 def put(self, item, block=True, timeout=None):
267 """Print `item` and then dump it into the void.
269 print 'ITEM:\n%s' % item
272 # Construct plugin dependency graph and load plugin instances.
274 def construct_graph(this_modname, submodnames, class_selector,
275 assert_name_match=True):
276 """Search the submodules `submodnames` of a module `this_modname`
277 for class objects for which `class_selector(class)` returns
278 `True`. These classes are instantiated, and the `instance.name`
279 is compared to the `submodname` (if `assert_name_match` is
282 The instances are further arranged into a dependency
283 :class:`hooke.util.graph.Graph` according to their
284 `instance.dependencies()` values. The topologically sorted graph
288 for submodname in submodnames:
289 count = len([s for s in submodnames if s == submodname])
290 assert count > 0, 'No %s entries: %s' % (submodname, submodnames)
291 assert count == 1, 'Multiple (%d) %s entries: %s' \
292 % (count, submodname, submodnames)
293 this_mod = __import__(this_modname, fromlist=[submodname])
294 submod = getattr(this_mod, submodname)
295 for objname in dir(submod):
296 obj = getattr(submod, objname)
297 if class_selector(obj):
299 if assert_name_match == True and instance.name != submodname:
301 'Instance name %s does not match module name %s'
302 % (instance.name, submodname))
303 instances[instance.name] = instance
304 graph = Graph([Node([instances[name] for name in i.dependencies()],
306 for i in instances.values()])
307 graph.topological_sort()
310 class IsSubclass (object):
311 """A safe subclass comparator.
316 >>> class A (object):
321 >>> is_subclass = IsSubclass(A)
324 >>> is_subclass = IsSubclass(A, blacklist=[A])
332 def __init__(self, base_class, blacklist=None):
333 self.base_class = base_class
334 if blacklist == None:
336 self.blacklist = blacklist
337 def __call__(self, other):
339 subclass = issubclass(other, self.base_class)
342 if other in self.blacklist:
346 PLUGIN_GRAPH = construct_graph(
347 this_modname=__name__,
348 submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES,
349 class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin]))
350 """Topologically sorted list of all possible :class:`Plugin`\s and
354 def default_settings():
356 'plugins', help='Enable/disable default plugins.')]
357 for pnode in PLUGIN_GRAPH:
358 if pnode.name in BUILTIN_MODULES:
359 continue # builtin inclusion is not optional
361 default_include = [di for mod_name,di in PLUGIN_MODULES
362 if mod_name == plugin.name][0]
363 help = 'Commands: ' + ', '.join([c.name for c in p.commands()])
364 settings.append(Setting(
367 value=str(default_include),
370 for pnode in PLUGIN_GRAPH:
372 settings.extend(plugin.default_settings())