Moved note handling commands to hooke.plugin.note.
[hooke.git] / hooke / plugin / __init__.py
index c3b25b7d3bdfea4f0fc21fde0087834c28f7a7b0..440c3bfa785f2f5fedbcb8508aa0111f8fb0ef9f 100644 (file)
@@ -1,19 +1,19 @@
-"""The plugin module provides optional submodules that add new Hooke
+"""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
 
 from ..config import Setting
 from ..util.graph import Node, Graph
 
+
 PLUGIN_MODULES = [
 #    ('autopeak', True),
 #    ('curvetools', True),
-#    ('cut', True),
+    ('cut', True),
 #    ('fit', True),
 #    ('flatfilts-rolf', True),
 #    ('flatfilts', True),
@@ -26,252 +26,189 @@ PLUGIN_MODULES = [
 #    ('multidistance', True),
 #    ('multifit', True),
 #    ('pcluster', True),
+#    ('peakspot', True),
 #    ('procplots', True),
 #    ('review', True),
 #    ('showconvoluted', True),
 #    ('superimpose', True),
 #    ('tutorial', True),
-#    ('viewer', True),
     ]
 """List of plugin modules and whether they should be included by
 default.  TODO: autodiscovery
 """
 
+BUILTIN_MODULES = [
+    'debug',
+    'note',
+    'playlist',
+    '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.
     """
-    name = "base plugin"
+    def __init__(self, name):
+        self.name = name
+        self.setting_section = '%s plugin' % self.name
+        self.config = {}
 
     def dependencies(self):
-        """Return a list of Plugins we require."""
+        """Return a list of :class:`Plugin`\s we require."""
         return []
 
     def default_settings(self):
-        """Return a list of hooke.config.Settings() for any
-        configurable module settings."""
+        """Return a list of :class:`hooke.config.Setting`\s for any
+        configurable plugin settings.
+
+        The suggested section setting is::
+
+            Setting(section=self.setting_section, help=self.__doc__)
+        """
         return []
 
     def commands(self):
-        """Return a list of Commands provided."""
+        """Return a list of :class:`hooke.command.Command`\s provided.
+        """
         return []
 
-PLUGINS = {}
-"""(name,instance) :class:`dict` of all possible :class:`Plugin`\s.
-"""
+class Builtin (Plugin):
+    """A required collection of Hooke commands.
 
-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
+    These "core" plugins provide essential administrative commands
+    (playlist handling, etc.).
+    """
+    pass
 
-PLUGIN_GRAPH = Graph([Node([PLUGINS[name] for name in p.dependencies()])
-                      for p in PLUGINS.values()])
-PLUGIN_GRAPH.topological_sort()
+# 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`).
 
-def default_settings(self):
-    settings = [Setting(
-            'plugins', help='Enable/disable default plugins.')]
+    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
+    nodes = {}
+    for i in instances.values():     # make nodes for each instance
+        nodes[i.name] = Node(data=i)
+    for n in nodes.values():         # fill in dependencies for each node
+        n.extend([nodes[name] for name in n.data.dependencies()])
+    graph = Graph(nodes.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,
+    class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin]))
+"""Topologically sorted list of all possible :class:`Plugin`\s and
+:class:`Builtin`\s.
+"""
+
+def default_settings():
+    settings = [Setting(PLUGIN_SETTING_SECTION,
+                        help='Enable/disable default plugins.')]
     for pnode in PLUGIN_GRAPH:
-        settings.append(Setting(p.name, str(PLUGIN_MODULES[p.module_name][1])))      
+        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
 
-
-# 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)
-    ITEM:
-    Command: test
-    <BLANKLINE>
-    Arguments:
-    help HELP (bool) Print a help message.
-    <BLANKLINE>
-    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 <Argument help> before punting
-        to :meth:`_run`.
-        """
-        if inqueue == None:
-            inqueue = NullQueue()
-        if outqueue == None:
-            outqueue = NullQueue()
+def load_graph(graph, config, include_section):
+    items = []
+    for node in graph:
+        item = node.data
         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, 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)
-
-    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
+            include = config.getboolean(include_section, item.name)
+        except configparser.NoOptionError:
+            include = True # non-optional include (e.g. a Builtin)
+        if include == True:
+            try:
+                item.config = dict(config.items(item.setting_section))
+            except configparser.NoSectionError:
+                pass
+            items.append(item)
+    return items