-"""The plugin module provides optional submodules that add new Hooke
+# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
+#
+# 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
+# <http://www.gnu.org/licenses/>.
+
+"""The `plugin` module provides optional submodules that add new Hooke
commands.
All of the science happens in here.
"""
-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),
+ ('convfilt', True),
('cut', True),
-# ('fit', True),
-# ('flatfilts-rolf', True),
-# ('flatfilts', True),
-# ('generalclamp', True),
-# ('generaltccd', True),
-# ('generalvclamp', True),
+# ('fclamp', True),
+ ('flatfilt', True),
# ('jumpstat', True),
-# ('macro', True),
# ('massanalysis', True),
# ('multidistance', True),
# ('multifit', True),
# ('pcluster', True),
-# ('procplots', True),
-# ('review', True),
+ ('polymer_fit', 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
"""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):
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.
"""
pass
-# Commands and arguments
-class CommandExit (Exception):
- def __str__(self):
- return self.__class__.__name__
+# Plugin utility functions
-class Success (CommandExit):
- pass
+def argument_to_setting(section_name, argument):
+ """Convert an :class:`~hooke.command.Argument` to a
+ `~hooke.conf.Setting`.
-class Failure (CommandExit):
- pass
-
-class Command (object):
- """One-line command description here.
+ This is useful if, for example, you want to define arguments with
+ configurable default values.
- >>> c = Command(name='test', help='An example Command.')
- >>> status = c.run(NullQueue(), PrintQueue(), help=True) # doctest: +REPORT_UDIFF
- ITEM:
- Command: test
- <BLANKLINE>
- Arguments:
- help BOOL (bool) Print a help message.
- <BLANKLINE>
- An example Command.
- ITEM:
- Success
+ Conversion is lossy transition, because
+ :class:`~hooke.command.Argument`\s store more information than
+ `~hooke.conf.Setting`\s.
"""
- 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 <Argument help> 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
-
- 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 Argument (object):
- """Structured user input for :class:`Command`\s.
-
- TODO: ranges for `count`?
- """
- 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,
"""
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.name in BUILTIN_MODULES:
+ 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 p.commands()])
+ 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,
plugin = pnode.data
settings.extend(plugin.default_settings())
return settings
+
+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