More cleanups to Setting/Argument type handling, mostly for count != 1.
[hooke.git] / hooke / plugin / __init__.py
index 544200ae250ecc9ed250d019332dee7815f300fb..8e77f2fd90f834c1d49fec30aaf7e471acbd5139 100644 (file)
@@ -1,25 +1,42 @@
-"""The plugin module provides optional submodules that add new Hooke
+# Copyright (C) 2010 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 ConfigParser as configparser
 commands.
 
 All of the science happens in here.
 """
 
 import ConfigParser as configparser
-import Queue as queue
+import logging
 
 from ..config import Setting
 
 from ..config import Setting
-from ..util.graph import Node, Graph
+from ..util.pluggable import IsSubclass, construct_graph
+
 
 PLUGIN_MODULES = [
 #    ('autopeak', True),
 
 PLUGIN_MODULES = [
 #    ('autopeak', True),
-#    ('curvetools', True),
+    ('convfilt', True),
     ('cut', True),
     ('cut', True),
+#    ('fclamp', True),
 #    ('fit', True),
 #    ('flatfilts-rolf', True),
 #    ('fit', True),
 #    ('flatfilts-rolf', True),
-#    ('flatfilts', True),
-#    ('generalclamp', True),
-#    ('generaltccd', True),
-#    ('generalvclamp', True),
+    ('flatfilt', True),
 #    ('jumpstat', True),
 #    ('macro', True),
 #    ('massanalysis', True),
 #    ('jumpstat', True),
 #    ('macro', True),
 #    ('massanalysis', True),
@@ -30,19 +47,31 @@ PLUGIN_MODULES = [
 #    ('review', True),
 #    ('showconvoluted', True),
 #    ('superimpose', 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 = [
     ]
 """List of plugin modules and whether they should be included by
 default.  TODO: autodiscovery
 """
 
 BUILTIN_MODULES = [
+    'config',
+    'curve',
+    'debug',
+    'license',
+    'note',
     'playlist',
     'playlist',
+    'playlists',
+    'system',
     ]
 """List of builtin modules.  TODO: autodiscovery
 """
 
     ]
 """List of builtin modules.  TODO: autodiscovery
 """
 
+PLUGIN_SETTING_SECTION = 'plugins'
+"""Name of the config section which controls plugin selection.
+"""
+
 
 # Plugins and settings
 
 
 # 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
     """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 = {}
     """
     def __init__(self, name):
         self.name = name
         self.setting_section = '%s plugin' % self.name
         self.config = {}
+        self._commands = []
 
     def dependencies(self):
 
     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 default_settings(self):
@@ -72,8 +104,10 @@ class Plugin (object):
         return []
 
     def commands(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.
 
 class Builtin (Plugin):
     """A required collection of Hooke commands.
@@ -83,267 +117,27 @@ class Builtin (Plugin):
     """
     pass
 
     """
     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
-    <BLANKLINE>
-    Arguments:
-    help BOOL (bool) Print a help message.
-    <BLANKLINE>
-    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 <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
+# 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.
 
 
 
 # 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,
 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():
 """
 
 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
     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(
                            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,
                 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):
     return settings
 
 def load_graph(graph, config, include_section):
+    enabled = {}
     items = []
     for node in graph:
         item = node.data
     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)
             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:
         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:
             try:
-                item.config = dict(
-                    config.items(item.setting_section))
+                item.config = dict(config.items(item.setting_section))
             except configparser.NoSectionError:
                 pass
             items.append(item)
             except configparser.NoSectionError:
                 pass
             items.append(item)