X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;f=hooke%2Fplugin%2F__init__.py;h=8e77f2fd90f834c1d49fec30aaf7e471acbd5139;hb=eee325d14069576c405bc5e12157ff76838cd1ec;hp=544200ae250ecc9ed250d019332dee7815f300fb;hpb=0667cd57b42e1fb0b1dbcf0c961623dbdd14739a;p=hooke.git diff --git a/hooke/plugin/__init__.py b/hooke/plugin/__init__.py index 544200a..8e77f2f 100644 --- a/hooke/plugin/__init__.py +++ b/hooke/plugin/__init__.py @@ -1,25 +1,42 @@ -"""The plugin module provides optional submodules that add new Hooke +# Copyright (C) 2010 W. Trevor King +# +# This file is part of Hooke. +# +# Hooke is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Hooke is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General +# Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Hooke. If not, see +# . + +"""The `plugin` module provides optional submodules that add new Hooke commands. All of the science happens in here. """ import ConfigParser as configparser -import Queue as queue +import logging from ..config import Setting -from ..util.graph import Node, Graph +from ..util.pluggable import IsSubclass, construct_graph + PLUGIN_MODULES = [ # ('autopeak', True), -# ('curvetools', True), + ('convfilt', True), ('cut', True), +# ('fclamp', True), # ('fit', True), # ('flatfilts-rolf', True), -# ('flatfilts', True), -# ('generalclamp', True), -# ('generaltccd', True), -# ('generalvclamp', True), + ('flatfilt', True), # ('jumpstat', True), # ('macro', True), # ('massanalysis', True), @@ -30,19 +47,31 @@ PLUGIN_MODULES = [ # ('review', True), # ('showconvoluted', True), # ('superimpose', True), -# ('tutorial', True), -# ('viewer', True), +# ('tccd', True), + ('tutorial', True), + ('vclamp', True), ] """List of plugin modules and whether they should be included by default. TODO: autodiscovery """ BUILTIN_MODULES = [ + 'config', + 'curve', + 'debug', + 'license', + 'note', 'playlist', + 'playlists', + 'system', ] """List of builtin modules. TODO: autodiscovery """ +PLUGIN_SETTING_SECTION = 'plugins' +"""Name of the config section which controls plugin selection. +""" + # Plugins and settings @@ -50,15 +79,18 @@ class Plugin (object): """A pluggable collection of Hooke commands. Fulfills the same role for Hooke that a software package does for - an operating system. + an operating system: contains a chunk of related commands and + routines. Command configuration also happens on the `Plugin` + level, with per-plugin sections in the configuration file. """ def __init__(self, name): self.name = name self.setting_section = '%s plugin' % self.name self.config = {} + self._commands = [] def dependencies(self): - """Return a list of :class:`Plugin`\s we require.""" + """Return a list of names of :class:`Plugin`\s we require.""" return [] def default_settings(self): @@ -72,8 +104,10 @@ class Plugin (object): return [] def commands(self): - """Return a list of :class:`Commands` provided.""" - return [] + """Return a list of :class:`hooke.command.Command`\s provided. + """ + return list(self._commands) + class Builtin (Plugin): """A required collection of Hooke commands. @@ -83,267 +117,27 @@ class Builtin (Plugin): """ pass -# Commands and arguments - -class CommandExit (Exception): - def __str__(self): - return self.__class__.__name__ - -class Success (CommandExit): - pass - -class Failure (CommandExit): - pass - -class Command (object): - """One-line command description here. - >>> c = Command(name='test', help='An example Command.') - >>> status = c.run(NullQueue(), PrintQueue(), help=True) # doctest: +REPORT_UDIFF - ITEM: - Command: test - - Arguments: - help BOOL (bool) Print a help message. - - An example Command. - ITEM: - Success - """ - def __init__(self, name, aliases=None, arguments=[], help=''): - self.name = name - if aliases == None: - aliases = [] - self.aliases = aliases - self.arguments = [ - Argument(name='help', type='bool', default=False, count=1, - callback=StoreValue(True), help='Print a help message.'), - ] + arguments - self._help = help - - def run(self, inqueue=None, outqueue=None, **kwargs): - """`Normalize inputs and handle before punting - to :meth:`_run`. - """ - if inqueue == None: - inqueue = NullQueue() - if outqueue == None: - outqueue = NullQueue() - try: - params = self.handle_arguments(inqueue, outqueue, kwargs) - if params['help'] == True: - outqueue.put(self.help()) - raise(Success()) - self._run(inqueue, outqueue, params) - except CommandExit, e: - if isinstance(e, Failure): - outqueue.put(e.message) - outqueue.put(e) - return 1 - outqueue.put(e) - return 0 - - def _run(self, inqueue, outqueue, params): - """This is where the command-specific magic will happen. - """ - pass +# Plugin utility functions - def handle_arguments(self, inqueue, outqueue, params): - """Normalize and validate input parameters (:class:`Argument` values). - """ - for argument in self.arguments: - names = [argument.name] + argument.aliases - settings = [(name,v) for name,v in params.items() if name in names] - if len(settings) == 0: - if argument.optional == True or argument.count == 0: - settings = [(argument.name, argument.default)] - else: - raise Failure('Required argument %s not set.' - % argument.name) - if len(settings) > 1: - raise Failure('Multiple settings for %s:\n %s' - % (argument.name, - '\n '.join(['%s: %s' % (name,value) - for name,value in sorted(settings)]))) - name,value = settings[0] - if name != argument.name: - params.remove(name) - params[argument.name] = value - if argument.callback != None: - value = argument.callback(self, argument, value) - params[argument.name] = value - argument.validate(value) - return params +def argument_to_setting(section_name, argument): + """Convert an :class:`~hooke.command.Argument` to a + `~hooke.conf.Setting`. - def help(self, *args): - name_part = 'Command: %s' % self.name - if len(self.aliases) > 0: - name_part += ' (%s)' % ', '.join(self.aliases) - argument_part = ['Arguments:'] + [a.help() for a in self.arguments] - argument_part = '\n'.join(argument_part) - help_part = self._help - return '\n\n'.join([name_part, argument_part, help_part]) + This is useful if, for example, you want to define arguments with + configurable default values. -class Argument (object): - """Structured user input for :class:`Command`\s. - - TODO: ranges for `count`? + Conversion is lossy transition, because + :class:`~hooke.command.Argument`\s store more information than + `~hooke.conf.Setting`\s. """ - def __init__(self, name, aliases=None, type='string', metavar=None, - default=None, optional=True, count=1, - completion_callback=None, callback=None, help=''): - self.name = name - if aliases == None: - aliases = [] - self.aliases = aliases - self.type = type - if metavar == None: - metavar = type.upper() - self.metavar = metavar - self.default = default - self.optional = optional - self.count = count - self.completion_callback = completion_callback - self.callback = callback - self._help = help - - def __str__(self): - return '<%s %s>' % (self.__class__.__name__, self.name) - - def __repr__(self): - return self.__str__() - - def help(self): - parts = ['%s ' % self.name] - if self.metavar != None: - parts.append('%s ' % self.metavar) - parts.extend(['(%s) ' % self.type, self._help]) - return ''.join(parts) - - def validate(self, value): - """If `value` is not appropriate, raise `ValueError`. - """ - pass # TODO: validation - - # TODO: type conversion - -# TODO: type extensions? - -# Useful callbacks - -class StoreValue (object): - def __init__(self, value): - self.value = value - def __call__(self, command, argument, fragment=None): - return self.value - -class NullQueue (queue.Queue): - """The :class:`queue.Queue` equivalent of `/dev/null`. - - This is a bottomless pit. Items go in, but never come out. - """ - def get(self, block=True, timeout=None): - """Raise queue.Empty. - - There's really no need to override the base Queue.get, but I - want to know if someone tries to read from a NullQueue. With - the default implementation they would just block silently - forever :(. - """ - raise queue.Empty - - def put(self, item, block=True, timeout=None): - """Dump an item into the void. - - Block and timeout are meaningless, because there is always a - free slot available in a bottomless pit. - """ - pass - -class PrintQueue (NullQueue): - """Debugging :class:`NullQueue` that prints items before dropping - them. - """ - def put(self, item, block=True, timeout=None): - """Print `item` and then dump it into the void. - """ - print 'ITEM:\n%s' % item + return Setting(section_name, option=argument.name, value=argument.default, + type=argument.type, count=argument.count, + help=argument._help) # Construct plugin dependency graph and load plugin instances. -def construct_graph(this_modname, submodnames, class_selector, - assert_name_match=True): - """Search the submodules `submodnames` of a module `this_modname` - for class objects for which `class_selector(class)` returns - `True`. These classes are instantiated, and the `instance.name` - is compared to the `submodname` (if `assert_name_match` is - `True`). - - The instances are further arranged into a dependency - :class:`hooke.util.graph.Graph` according to their - `instance.dependencies()` values. The topologically sorted graph - is returned. - """ - instances = {} - for submodname in submodnames: - count = len([s for s in submodnames if s == submodname]) - assert count > 0, 'No %s entries: %s' % (submodname, submodnames) - assert count == 1, 'Multiple (%d) %s entries: %s' \ - % (count, submodname, submodnames) - this_mod = __import__(this_modname, fromlist=[submodname]) - submod = getattr(this_mod, submodname) - for objname in dir(submod): - obj = getattr(submod, objname) - if class_selector(obj): - instance = obj() - if assert_name_match == True and instance.name != submodname: - raise Exception( - 'Instance name %s does not match module name %s' - % (instance.name, submodname)) - instances[instance.name] = instance - graph = Graph([Node([instances[name] for name in i.dependencies()], - data=i) - for i in instances.values()]) - graph.topological_sort() - return graph - -class IsSubclass (object): - """A safe subclass comparator. - - Examples - -------- - - >>> class A (object): - ... pass - >>> class B (A): - ... pass - >>> C = 5 - >>> is_subclass = IsSubclass(A) - >>> is_subclass(A) - True - >>> is_subclass = IsSubclass(A, blacklist=[A]) - >>> is_subclass(A) - False - >>> is_subclass(B) - True - >>> is_subclass(C) - False - """ - def __init__(self, base_class, blacklist=None): - self.base_class = base_class - if blacklist == None: - blacklist = [] - self.blacklist = blacklist - def __call__(self, other): - try: - subclass = issubclass(other, self.base_class) - except TypeError: - return False - if other in self.blacklist: - return False - return subclass - PLUGIN_GRAPH = construct_graph( this_modname=__name__, submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES, @@ -353,8 +147,8 @@ PLUGIN_GRAPH = construct_graph( """ def default_settings(): - settings = [Setting( - 'plugins', help='Enable/disable default plugins.')] + settings = [Setting(PLUGIN_SETTING_SECTION, + help='Enable/disable default plugins.')] for pnode in PLUGIN_GRAPH: if pnode.data.name in BUILTIN_MODULES: continue # builtin inclusion is not optional @@ -363,7 +157,7 @@ def default_settings(): if mod_name == plugin.name][0] help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()]) settings.append(Setting( - section='plugins', + section=PLUGIN_SETTING_SECTION, option=plugin.name, value=str(default_include), help=help, @@ -374,6 +168,7 @@ def default_settings(): return settings def load_graph(graph, config, include_section): + enabled = {} items = [] for node in graph: item = node.data @@ -381,10 +176,18 @@ def load_graph(graph, config, include_section): include = config.getboolean(include_section, item.name) except configparser.NoOptionError: include = True # non-optional include (e.g. a Builtin) + enabled[item.name] = include if include == True: + for dependency in node: + if enabled.get(dependency.data.name, None) != True: + log = logging.getLogger('hooke') + log.warn( + 'could not setup plugin %s. unsatisfied dependency on %s.' + % (item.name, dependency.data.name)) + enabled[item.name] = False + continue try: - item.config = dict( - config.items(item.setting_section)) + item.config = dict(config.items(item.setting_section)) except configparser.NoSectionError: pass items.append(item)