X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fplugin%2F__init__.py;h=d04f3740f3797fae36e731a0d01de3a43d56622f;hp=c3b25b7d3bdfea4f0fc21fde0087834c28f7a7b0;hb=b90995fb4b6d8151df862d40edc8c369d7052cfa;hpb=1c41ec100b0754eb5e598ec48b33887ea405af88 diff --git a/hooke/plugin/__init__.py b/hooke/plugin/__init__.py index c3b25b7..d04f374 100644 --- a/hooke/plugin/__init__.py +++ b/hooke/plugin/__init__.py @@ -1,277 +1,191 @@ -"""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 os.path -import Queue as queue +import ConfigParser as configparser +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), -# ('cut', True), -# ('fit', True), + ('convfilt', True), + ('cut', True), +# ('fclamp', True), # ('flatfilts-rolf', True), -# ('flatfilts', True), -# ('generalclamp', True), -# ('generaltccd', True), -# ('generalvclamp', True), + ('flatfilt', True), # ('jumpstat', True), -# ('macro', True), # ('massanalysis', True), # ('multidistance', True), # ('multifit', True), # ('pcluster', True), + ('polymer_fit', True), # ('procplots', True), # ('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 = [ + 'command_stack', + 'config', + 'curve', + 'debug', + 'engine', + '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 -def Plugin (object): - """The pluggable collection of Hooke commands. +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. """ - name = "base plugin" + 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 Plugins we require.""" + """Return a list of names of :class:`Plugin`\s we require.""" return [] def default_settings(self): - """Return a list of hooke.config.Settings() for any - configurable module settings.""" - return [] - - def commands(self): - """Return a list of Commands provided.""" - return [] - -PLUGINS = {} -"""(name,instance) :class:`dict` of all possible :class:`Plugin`\s. -""" - -print __name__ -for plugin_modname,value in PLUGIN_MODULES: - this_mod = __import__(__name__, fromlist=[plugin_modname]) - plugin_mod = getattr(this_mod, plugin_modname) - for objname in dir(plugin_mod): - obj = getattr(plugin_mod, objname) - if type(obj) == Plugin: - obj.module_name = plugin_modname - PLUGINS[p.name] = p + """Return a list of :class:`hooke.config.Setting`\s for any + configurable plugin settings. -PLUGIN_GRAPH = Graph([Node([PLUGINS[name] for name in p.dependencies()]) - for p in PLUGINS.values()]) -PLUGIN_GRAPH.topological_sort() - - -def default_settings(self): - settings = [Setting( - 'plugins', help='Enable/disable default plugins.')] - for pnode in PLUGIN_GRAPH: - settings.append(Setting(p.name, str(PLUGIN_MODULES[p.module_name][1]))) - for pnode in PLUGIN_GRAPH: - plugin = pnode.data - settings.extend(plugin.default_settings()) - return settings - - -# Commands and arguments - -class CommandExit (Exception): - def __str__(self): - return self.__class__.__name__ - -class Success (CommandExit): - pass - -class Failure (CommandExit): - pass + The suggested section setting is:: -class Command (object): - """One-line command description here. - - >>> c = Command(name='test', help='An example Command.') - >>> status = c.run(NullQueue(), PrintQueue(), help=True) - ITEM: - Command: test - - Arguments: - help HELP (bool) Print a help message. - - An example Command. - ITEM: - Success - """ - def __init__(self, name, arguments=[], aliases=None, help=''): - self.name = name - self.arguments = [ - Argument(name='help', type='bool', default=False, count=1, - callback=StoreValue(True), help='Print a help message.'), - ] + arguments - if aliases == None: - aliases = [] - self.aliases = aliases - self._help = help - - def run(self, inqueue=None, outqueue=None, **kwargs): - """`Normalize inputs and handle before punting - to :meth:`_run`. + Setting(section=self.setting_section, help=self.__doc__) """ - 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 + return [] - def _run(self, inqueue, outqueue, params): - """This is where the command-specific magic will happen. + def commands(self): + """Return a list of :class:`hooke.command.Command`\s provided. """ - pass + return list(self._commands) - 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 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]) +class Builtin (Plugin): + """A required collection of Hooke commands. -class Argument (object): - """Structured user input for :class:`Command`\s. - - TODO: ranges for `count`? + These "core" plugins provide essential administrative commands + (playlist handling, etc.). """ - def __init__(self, name, type='string', metavar=None, default=None, - optional=True, count=1, completion_callback=None, - callback=None, aliases=None, help=''): - self.name = name - self.type = type - if metavar == None: - metavar = name.upper() - self.metavar = metavar - self.default = default - self.optional = optional - self.count = count - self.completion_callback = completion_callback - self.callback = callback - if aliases == None: - aliases = [] - self.aliases = aliases - 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) + pass - def validate(self, value): - """If `value` is not appropriate, raise `ValueError`. - """ - pass # TODO: validation - # TODO: type conversion +# Plugin utility functions -# TODO: type extensions? +def argument_to_setting(section_name, argument): + """Convert an :class:`~hooke.command.Argument` to a + `~hooke.conf.Setting`. -# Useful callbacks + This is useful if, for example, you want to define arguments with + configurable default values. -class StoreValue (object): - def __init__(self, value): - self.value = value - def __call__(self, command, argument, fragment=None): - return self.value + Conversion is lossy transition, because + :class:`~hooke.command.Argument`\s store more information than + `~hooke.conf.Setting`\s. + """ + return Setting(section_name, option=argument.name, value=argument.default, + type=argument.type, count=argument.count, + help=argument._help) -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 +# Construct plugin dependency graph and load plugin instances. - def put(self, item, block=True, timeout=None): - """Dump an item into the void. +PLUGIN_GRAPH = construct_graph( + this_modname=__name__, + submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES, + class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin])) +"""Topologically sorted list of all possible :class:`Plugin`\s and +:class:`Builtin`\s. +""" - Block and timeout are meaningless, because there is always a - free slot available in a bottomless pit. - """ - pass +def default_settings(): + 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 + plugin = pnode.data + default_include = [di for mod_name,di in PLUGIN_MODULES + if mod_name == plugin.name][0] + help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()]) + settings.append(Setting( + section=PLUGIN_SETTING_SECTION, + option=plugin.name, + value=str(default_include), + help=help, + )) + for pnode in PLUGIN_GRAPH: + plugin = pnode.data + settings.extend(plugin.default_settings()) + return settings -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 +def load_graph(graph, config, include_section): + enabled = {} + items = [] + for node in graph: + item = node.data + try: + 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 + items.append(item) + return items