Moved note handling commands to hooke.plugin.note.
[hooke.git] / hooke / plugin / __init__.py
index a9f883737a8560d1da4a3144cfb0a62027784d38..440c3bfa785f2f5fedbcb8508aa0111f8fb0ef9f 100644 (file)
-#!/usr/bin/env python
-'''
-Commands and settings panel for Hooke
-
-Displays commands and settings for Hooke in a tree control
-(c) Dr. Rolf Schmidt, 2009
-'''
-
-from configobj import ConfigObj
-import os.path
-from validate import Validator
-import wx
-
-import libhooke as lh
-
-class Commands(wx.Panel):
-
-    def __init__(self, parent):
-        # Use the WANTS_CHARS style so the panel doesn't eat the Return key.
-        wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS|wx.NO_BORDER, size=(160, 200))
-
-        self.CommandsTree = wx.TreeCtrl(self, -1, wx.Point(0, 0), wx.Size(160, 250), wx.TR_DEFAULT_STYLE|wx.NO_BORDER|wx.TR_HIDE_ROOT)
-        imglist = wx.ImageList(16, 16, True, 2)
-        imglist.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_OTHER, wx.Size(16, 16)))
-        imglist.Add(wx.ArtProvider.GetBitmap(wx.ART_EXECUTABLE_FILE, wx.ART_OTHER, wx.Size(16, 16)))
-        self.CommandsTree.AssignImageList(imglist)
-        self.CommandsTree.AddRoot('Commands and Settings', 0)
-
-        self.ExecuteButton = wx.Button(self, -1, 'Execute')
-
-        sizer = wx.BoxSizer(wx.VERTICAL)
-        sizer.Add(self.CommandsTree, 1, wx.EXPAND)
-        sizer.Add(self.ExecuteButton, 0, wx.EXPAND)
-
-        self.SetSizer(sizer)
-        sizer.Fit(self)
-
-    def Initialize(self, plugins):
-        tree_root = self.CommandsTree.GetRootItem()
-        for plugin in plugins:
-            filename = ''.join([plugin, '.ini'])
-            path = lh.get_file_path(filename, ['plugins'])
-            config = ConfigObj()
-            if os.path.isfile(path):
-                config.filename = path
-                config.reload()
-                #append the ini file to the plugin
-                plugin_root = self.CommandsTree.AppendItem(tree_root, plugin, 0, data=wx.TreeItemData(config))
-            else:
-                plugin_root = self.CommandsTree.AppendItem(tree_root, plugin, 0)
-
-            #add all commands to the tree
-            for command in plugins[plugin]:
-                command_label = command.replace('do_', '')
-                #do not add the ini file to the command (we'll access the ini file of the plugin (ie parent) instead, see above)
-                self.CommandsTree.AppendItem(plugin_root, command_label, 1)
-            self.CommandsTree.Expand(plugin_root)
+"""The `plugin` module provides optional submodules that add new Hooke
+commands.
+
+All of the science happens in here.
+"""
+
+import ConfigParser as configparser
+
+from ..config import Setting
+from ..util.graph import Node, Graph
+
+
+PLUGIN_MODULES = [
+#    ('autopeak', True),
+#    ('curvetools', True),
+    ('cut', True),
+#    ('fit', True),
+#    ('flatfilts-rolf', True),
+#    ('flatfilts', True),
+#    ('generalclamp', True),
+#    ('generaltccd', True),
+#    ('generalvclamp', True),
+#    ('jumpstat', True),
+#    ('macro', True),
+#    ('massanalysis', True),
+#    ('multidistance', True),
+#    ('multifit', True),
+#    ('pcluster', True),
+#    ('peakspot', True),
+#    ('procplots', True),
+#    ('review', True),
+#    ('showconvoluted', True),
+#    ('superimpose', True),
+#    ('tutorial', 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
+
+class Plugin (object):
+    """A pluggable collection of Hooke commands.
+
+    Fulfills the same role for Hooke that a software package does for
+    an operating system.
+    """
+    def __init__(self, name):
+        self.name = name
+        self.setting_section = '%s plugin' % self.name
+        self.config = {}
+
+    def dependencies(self):
+        """Return a list of :class:`Plugin`\s we require."""
+        return []
+
+    def default_settings(self):
+        """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 :class:`hooke.command.Command`\s provided.
+        """
+        return []
+
+class Builtin (Plugin):
+    """A required collection of Hooke commands.
+
+    These "core" plugins provide essential administrative commands
+    (playlist handling, etc.).
+    """
+    pass
+
+# 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
+    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:
+        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
+
+def load_graph(graph, config, include_section):
+    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)
+        if include == True:
+            try:
+                item.config = dict(config.items(item.setting_section))
+            except configparser.NoSectionError:
+                pass
+            items.append(item)
+    return items