Moved hooke.playlist -> hooke.plugin.playlist and added hooke.plugin.Builtin.
authorW. Trevor King <wking@drexel.edu>
Sun, 9 May 2010 13:29:39 +0000 (09:29 -0400)
committerW. Trevor King <wking@drexel.edu>
Sun, 9 May 2010 13:29:39 +0000 (09:29 -0400)
Highlights:
  * Cleaner Playlist, with more consistent file format.
  * New "Builtin" class for required (core) commands.
  * Moved NotRecognized from hooke.driver -> hooke.curve to avoid
    import-order issues.  Now hooke.curve can be fully loaded before
    hooke.driver.
  * Moved hooke.curve.Data.notes -> hooke.curve.Data.info['note']
    for better consistency with Curve and Playlist.
  * Started cleaning up plugin loading into flexible functions.

hooke/curve.py
hooke/driver/__init__.py
hooke/hooke.py
hooke/playlist.py [deleted file]
hooke/plugin/__init__.py
hooke/plugin/playlist.py [new file with mode: 0644]
hooke/ui/gui/hookeplaylist.py

index 5aeaeaac887f3bc3cc26f0b8ead8a73098deb7ee..0be10bc3144ee673ad1861817d2f06ab2b8d4284 100644 (file)
@@ -5,8 +5,12 @@ force curves.
 import os.path
 import numpy
 
-from .driver import NotRecognized
 
+class NotRecognized (ValueError):
+    def __init__(self, curve):
+        msg = 'Not a recognizable curve format: %s' % curve.path
+        ValueError.__init__(self, msg)
+        self.curve = curve
 
 class Data (numpy.ndarray):
     """Stores a single, continuous data set.
@@ -61,14 +65,15 @@ class Curve (object):
       Hooke commands could like to know if they're looking at force
       clamp data, regardless of their origin.
     """
-    def __init__(self, path):
+    def __init__(self, path, info=None):
         #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
         self.path = path
         self.driver = None
         self.data = []
-        self.info = None
+        if info == None:
+            info = {}
+        self.info = info
         self.name = os.path.basename(path)
-        self.notes = ''
 
     def identify(self, drivers):
         """Identify the appropriate :class:`hooke.driver.Driver` for
@@ -78,7 +83,7 @@ class Curve (object):
             if driver.is_me(self.path):
                 self.driver = driver # remember the working driver
                 return
-        raise NotRecognized(self.path)
+        raise NotRecognized(self)
 
     def load(self):
         """Use the driver to read the curve into memory.
index ea4a9c9c253241299aae152cc92f3a6e3e419dc2..ff90910237c2854ddd6f9063fc69935f3d99eea1 100644 (file)
@@ -26,11 +26,6 @@ DRIVER_MODULES = [
 default.  TODO: autodiscovery
 """
 
-class NotRecognized (ValueError):
-    def __init__(self, path):
-        msg = 'Not a recognizable curve format: %s' % self.path
-        ValueError.__init__(self, msg)
-        self.path = path
 
 class Driver(object):
     """Base class for file format drivers.
index 7a3f692b25f704e6fd7671a91a45a5d3d4332d29..923f6cba0e756b12415ca098735a823f3d32ad77 100644 (file)
@@ -43,12 +43,24 @@ class Hooke (object):
                 default_settings=default_settings)
             config.read()
         self.config = config
+        self.load_builtins()
         self.load_plugins()
         self.load_drivers()
+        self.setup_commands()
+
+    def load_builtins(self):
+        self.builtins = []
+        for builtin in plugin_mod.BUILTINS.values():
+            builtin = plugin_mod.BUILTINS[builtin_name]
+            try:
+                builtin.config = dict(
+                    self.config.items(builtin.setting_section))
+            except configparser.NoSectionError:
+                pass
+            self.builtins.append(plugin_mod.BUILTINS[builtin_name])
 
     def load_plugins(self):
         self.plugins = []
-        self.commands = []
         for plugin_name,include in self.config.items('plugins'):
             if include == 'True':
                 plugin = plugin_mod.PLUGINS[plugin_name]
@@ -58,7 +70,6 @@ class Hooke (object):
                 except configparser.NoSectionError:
                     pass
                 self.plugins.append(plugin_mod.PLUGINS[plugin_name])
-                self.commands.extend(plugin.commands())
 
     def load_drivers(self):
         self.drivers = []
@@ -72,10 +83,22 @@ class Hooke (object):
                     pass
                 self.drivers.append(driver_mod.DRIVERS[driver_name])
 
+    def setup_commands(self):
+        self.commands = []
+        for plugin in self.builtins + self.plugins:
+            self.commands.extend(plugin.commands())
+
     def close(self):
         if self.config.changed:
             self.config.write() # Does not preserve original comments
 
+    def playlist_status(self, playlist):
+        if playlist.has_curves()
+            return '%s (%s/%s)' % (playlist.name, playlist._index + 1,
+                                   len(playlist))
+        return 'The playlist %s does not contain any valid force curve data.' \
+            % self.name
+
 #    def _GetActiveCurveIndex(self):
 #        playlist = self.GetActivePlaylist()
 #        #get the selected item from the tree
@@ -138,7 +161,7 @@ class Hooke (object):
 #        if files:
 #            playlist = Playlist.Playlist(self.drivers)
 #            for item in files:
-#                playlist.add_curve(item)
+#                playlist.append_curve_by_path(item)
 #        if playlist.count > 0:
 #            playlist.name = self._GetUniquePlaylistName(name)
 #            playlist.reset()
@@ -365,7 +388,7 @@ class Hooke (object):
 #    def GetActiveCurve(self):
 #        playlist = self.GetActivePlaylist()
 #        if playlist is not None:
-#            return playlist.get_active_curve()
+#            return playlist.active_curve()
 #        return None
 #
 #    def GetActivePlaylist(self):
@@ -569,7 +592,7 @@ class Hooke (object):
 #                self.plotNotebook.SetSelection(index)
 #            #if a curve was double-clicked
 #            item = self.panelPlaylists.PlaylistsTree.GetSelection()
-#            #TODO: fix with get_active_curve
+#            #TODO: fix with active_curve
 #            if not self.panelPlaylists.PlaylistsTree.ItemHasChildren(item):
 #                index = self._GetActiveCurveIndex()
 #            else:
@@ -592,7 +615,7 @@ class Hooke (object):
 #            #if a curve was clicked
 #            item = self.panelPlaylists.PlaylistsTree.GetSelection()
 #            if not self.panelPlaylists.PlaylistsTree.ItemHasChildren(item):
-#                #TODO: fix with get_active_curve
+#                #TODO: fix with active_curve
 #                index = self._GetActiveCurveIndex()
 #                if index >= 0:
 #                    #playlist = self.playlists[playlist_name][0]
@@ -847,7 +870,7 @@ class Hooke (object):
 #            #self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
 #            playlist = playlist.Playlist(self.drivers)
 #            for item in files:
-#                curve = playlist.add_curve(item)
+#                curve = playlist.append_curve_by_path(item)
 #                plot = copy.deepcopy(curve.plots[0])
 #                #add the 'raw' data
 #                curve.add_data('raw', plot.vectors[0][0], plot.vectors[0][1], color=plot.colors[0], style='plot')
@@ -944,7 +967,7 @@ class Hooke (object):
 #        playlist_name = self._GetActivePlaylistName()
 #        index = self._GetActiveCurveIndex()
 #        playlist = self.playlists[playlist_name][0]
-#        curve = playlist.get_active_curve()
+#        curve = playlist.active_curve()
 #        plot = playlist.get_active_plot()
 #        figure = self.playlists[playlist_name][1]
 #
diff --git a/hooke/playlist.py b/hooke/playlist.py
deleted file mode 100644 (file)
index 26ab8dc..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-import copy
-import os
-import os.path
-import xml.dom.minidom
-
-from . import hooke as hooke
-from . import curve as lhc
-from . import libhooke as lh
-
-class Playlist(object):
-    def __init__(self, drivers):
-        self._saved = False
-        self.count = 0
-        self.curves = []
-        self.drivers = drivers
-        self.path = ''
-        self.genericsDict = {}
-        self.hiddenAttributes = ['curve', 'driver', 'name', 'plots']
-        self.index = -1
-        self.name = 'Untitled'
-        self.plotPanel = None
-        self.plotTab = None
-        self.xml = None
-
-    def add_curve(self, path, attributes={}):
-        curve = lhc.HookeCurve(path)
-        for key,value in attribures.items():
-            setattr(curve, key, value)
-        curve.identify(self.drivers)
-        curve.plots = curve.driver.default_plots()
-        self.curves.append(curve)
-        self._saved = False
-        self.count = len(self.curves)
-        return curve
-
-    def close_curve(self, index):
-        if index >= 0 and index < self.count:
-            self.curves.remove(index)
-
-    def filter_curves(self, keeper_fn=labmda curve:True):
-        playlist = copy.deepcopy(self)
-        for curve in reversed(playlist.curves):
-            if not keeper_fn(curve):
-                playlist.curves.remove(curve)
-        try: # attempt to maintain the same active curve
-            playlist.index = playlist.curves.index(self.get_active_curve())
-        except ValueError:
-            playlist.index = 0
-        playlist._saved = False
-        playlist.count = len(playlist.curves)
-        return playlist
-
-    def get_active_curve(self):
-        return self.curves[self.index]
-
-    #TODO: do we need this?
-    def get_active_plot(self):
-        return self.curves[self.index].plots[0]
-
-    def get_status_string(self):
-        if self.has_curves()
-            return '%s (%s/%s)' % (self.name, self.index + 1, self.count)
-        return 'The file %s does not contain any valid force curve data.' \
-            % self.name
-
-    def has_curves(self):
-        if self.count > 0:
-            return True
-        return False
-
-    def is_saved(self):
-        return self._saved
-
-    def load(self, path):
-        '''
-        loads a playlist file
-        '''
-        self.path = path
-        self.name = os.path.basename(path)
-        playlist = lh.delete_empty_lines_from_xmlfile(path)
-        self.xml = xml.dom.minidom.parse(path)
-        # Strip blank spaces:
-        self._removeWhitespaceNodes()
-
-        generics_list = self.xml.getElementsByTagName('generics')
-        curve_list = self.xml.getElementsByTagName('curve')
-        self._loadGenerics(generics_list)
-        self._loadCurves(curve_list)
-        self._saved = True
-
-    def _removeWhitespaceNodes(self, root_node=None):
-        if root_node == None:
-            root_node = self.xml
-        for node in root_node.childNodes:
-            if node.nodeType == node.TEXT_NODE and node.data.strip() == '':
-                root_node.removeChild(node) # drop this whitespace node
-            else:
-                _removeWhitespaceNodes(root_node=node) # recurse down a level
-
-    def _loadGenerics(self, generics_list, clear=True):
-        if clear:
-            self.genericsDict = {}
-        #populate generics
-        generics_list = self.xml.getElementsByTagName('generics')
-        for generics in generics_list:
-            for attribute in generics.attributes.keys():
-                self.genericsDict[attribute] = generics_list[0].getAttribute(attribute)
-        if self.genericsDict.has_key('pointer'):
-            index = int(self.genericsDict['pointer'])
-            if index >= 0 and index < len(self.curves):
-                self.index = index
-            else:
-                index = 0
-
-    def _loadCurves(self, curve_list, clear=True):
-        if clear:
-            self.curves = []
-        #populate playlist with curves
-        for curve in curve_list:
-            #rebuild a data structure from the xml attributes
-            curve_path = lh.get_file_path(element.getAttribute('path'))
-            #extract attributes for the single curve
-            attributes = dict([(k,curve.getAttribute(k))
-                               for k in curve.attributes.keys()])
-            attributes.pop('path')
-            curve = self.add_curve(os.path.join(path, curve_path), attributes)
-            if curve is not None:
-                for plot in curve.plots:
-                    curve.add_data('raw', plot.vectors[0][0], plot.vectors[0][1], color=plot.colors[0], style='plot')
-                    curve.add_data('raw', plot.vectors[1][0], plot.vectors[1][1], color=plot.colors[1], style='plot')
-
-    def next(self):
-        self.index += 1
-        if self.index > self.count - 1:
-            self.index = 0
-
-    def previous(self):
-        self.index -= 1
-        if self.index < 0:
-            self.index = self.count - 1
-
-    def reset(self):
-        if self.has_curves():
-            self.index = 0
-        else:
-            self.index = None
-
-    def save(self, path):
-        '''
-        saves the playlist in a XML file.
-        '''
-        try:
-            output_file = file(path, 'w')
-        except IOError, e:
-            #TODO: send message
-            print 'Cannot save playlist: %s' % e
-            return
-        self.xml.writexml(output_file, indent='\n')
-        output_file.close()
-        self._saved = True
-
-    def set_XML(self):
-        '''
-        Creates an initial playlist from a list of files.
-        A playlist is an XML document with the following syntax:
-          <?xml version="1.0" encoding="utf-8"?>
-          <playlist>
-            <generics pointer="0"/>
-            <curve path="/my/file/path/"/ attribute="value" ...>
-            <curve path="...">
-          </playlist>
-        Relative paths are interpreted relative to the location of the
-        playlist file.
-        '''
-        #create the output playlist, a simple XML document
-        implementation = xml.dom.minidom.getDOMImplementation()
-        #create the document DOM object and the root element
-        self.xml = implementation.createDocument(None, 'playlist', None)
-        root = self.xml.documentElement
-
-        #save generics variables
-        playlist_generics = self.xml.createElement('generics')
-        root.appendChild(playlist_generics)
-        self.genericsDict['pointer'] = self.index
-        for key in self.genericsDict.keys():
-            self.xml.createAttribute(key)
-            playlist_generics.setAttribute(key, str(self.genericsDict[key]))
-            
-        #save curves and their attributes
-        for item in self.curves:
-            playlist_curve = self.xml.createElement('curve')
-            root.appendChild(playlist_curve)
-            for key in item.__dict__:
-                if not (key in self.hiddenAttributes):
-                    self.xml.createAttribute(key)
-                    playlist_curve.setAttribute(key, str(item.__dict__[key]))
-        self._saved = False
index 2bf05e840ccecff7f90741c3a5837d57ee8da315..f56644b000a9485a780b32016fd99a0e586af784 100644 (file)
@@ -36,10 +36,17 @@ PLUGIN_MODULES = [
 default.  TODO: autodiscovery
 """
 
+BUILTIN_MODULES = [
+    'playlist',
+    ]
+"""List of builtin modules.  TODO: autodiscovery
+"""
+
+
 # Plugins and settings
 
 class Plugin (object):
-    """The pluggable collection of Hooke commands.
+    """A pluggable collection of Hooke commands.
 
     Fulfills the same role for Hooke that a software package does for
     an operating system.
@@ -67,7 +74,13 @@ class Plugin (object):
         """Return a list of :class:`Commands` provided."""
         return []
 
+class Builtin (Plugin):
+    """A required collection of Hooke commands.
 
+    These "core" plugins provide essential administrative commands
+    (playlist handling, etc.).
+    """
+    pass
 
 # Commands and arguments
 
@@ -85,12 +98,12 @@ class Command (object):
     """One-line command description here.
 
     >>> c = Command(name='test', help='An example Command.')
-    >>> status = c.run(NullQueue(), PrintQueue(), help=True)
+    >>> status = c.run(NullQueue(), PrintQueue(), help=True) # doctest: +REPORT_UDIFF
     ITEM:
     Command: test
     <BLANKLINE>
     Arguments:
-    help HELP (bool) Print a help message.
+    help BOOL (bool) Print a help message.
     <BLANKLINE>
     An example Command.
     ITEM:
@@ -256,40 +269,94 @@ class PrintQueue (NullQueue):
         print 'ITEM:\n%s' % item
 
 
-# Construct plugin dependency graph and load default plugins.
+# Construct plugin dependency graph and load plugin instances.
 
-PLUGINS = {}
-"""(name,instance) :class:`dict` of all possible :class:`Plugin`\s.
-"""
+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`).
 
-for plugin_modname,default_include in PLUGIN_MODULES:
-    assert len([mod_name for mod_name,di in PLUGIN_MODULES]) == 1, \
-        'Multiple %s entries in PLUGIN_MODULES' % mod_name
-    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)
+    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(obj, Plugin)
+            subclass = issubclass(other, self.base_class)
         except TypeError:
-            continue
-        if subclass == True and obj != Plugin:
-            p = obj()
-            if p.name != plugin_modname:
-                raise Exception('Plugin name %s does not match module name %s'
-                                % (p.name, plugin_modname))
-            PLUGINS[p.name] = p
-
-PLUGIN_GRAPH = Graph([Node([PLUGINS[name] for name in p.dependencies()],
-                           data=p)
-                      for p in PLUGINS.values()])
-PLUGIN_GRAPH.topological_sort()
-
+            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(
             'plugins', help='Enable/disable default plugins.')]
     for pnode in PLUGIN_GRAPH:
+        if pnode.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]
diff --git a/hooke/plugin/playlist.py b/hooke/plugin/playlist.py
new file mode 100644 (file)
index 0000000..58832d3
--- /dev/null
@@ -0,0 +1,410 @@
+"""Defines :class:`PlaylistPlugin` several associated
+:class:`hooke.plugin.Command`\s.
+"""
+
+import copy
+import hashlib
+import os
+import os.path
+import xml.dom.minidom
+
+
+from .. import curve as curve
+from ..plugin import Plugin, Command, Argument
+
+class PlaylistPlugin (Plugin):
+    def __init__(self):
+        super(PlaylistPlugin, self).__init__(name='playlist')
+
+    def commands(self):
+        return [NextCommand(), PreviousCommand(), JumpCommand(),
+                SaveCommand(), LoadCommand(),
+                AddCommand(), RemoveCommand(), FilterCommand()]
+
+class Playlist (list):
+    """A list of :class:`hooke.curve.Curve`\s.
+
+    Keeps a list of :attr:`drivers` for loading curves, the
+    :attr:`index` (i.e. "bookmark") of the currently active curve, and
+    a :class:`dict` of additional informtion (:attr:`info`).
+    """
+    def __init__(self, drivers, name=None):
+        super(Playlist, self).__init__()
+        self.drivers = drivers
+        self.name = name
+        self.info = {}
+        self._index = 0
+
+    def append_curve_by_path(self, path, info=None, identify=True):
+        if self.path != None:
+            path = os.path.join(self.path, path)
+        path = os.path.normpath(path)
+        c = curve.Curve(path, info=info)
+        if identify == True:
+            c.identify(self.drivers)
+        self.append(c)
+        return c
+
+    def active_curve(self):
+        return self[self._index]
+
+    def has_curves(self):
+        return len(self) > 0
+
+    def jump(self, index):
+        if len(self) == 0:
+            self._index = 0
+        else:
+            self._index = index % len(self)
+
+    def next(self):
+        self.jump(self._index + 1)
+
+    def previous(self):
+        self.jump(self._index - 1)
+
+    def filter(self, keeper_fn=lambda curve:True):
+        playlist = copy.deepcopy(self)
+        for curve in reversed(playlist.curves):
+            if keeper_fn(curve) != True:
+                playlist.curves.remove(curve)
+        try: # attempt to maintain the same active curve
+            playlist._index = playlist.index(self.active_curve())
+        except ValueError:
+            playlist._index = 0
+        return playlist
+
+class FilePlaylist (Playlist):
+    version = '0.1'
+
+    def __init__(self, drivers, name=None, path=None):
+        if name == None and path != None:
+            name = os.path.basename(path)
+        super(FilePlaylist, self).__init__(drivers, name)
+        self.path = path
+        self._digest = None
+
+    def is_saved(self):
+        return self.digest() == self._digest
+
+    def digest(self):
+        r"""Compute the sha1 digest of the flattened playlist
+        representation.
+
+        Examples
+        --------
+
+        >>> root_path = os.path.sep + 'path'
+        >>> p = FilePlaylist(drivers=[],
+        ...                  path=os.path.join(root_path, 'to','playlist'))
+        >>> p.info['note'] = 'An example playlist'
+        >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
+        >>> c.info['note'] = 'The first curve'
+        >>> p.append(c)
+        >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
+        >>> c.info['note'] = 'The second curve'
+        >>> p.append(c)
+        >>> p.digest()
+        "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1"
+        """
+        string = self.flatten()
+        return hashlib.sha1(string).digest()
+
+    def flatten(self, absolute_paths=False):
+        """Create a string representation of the playlist.
+
+        A playlist is an XML document with the following syntax::
+
+            <?xml version="1.0" encoding="utf-8"?>
+            <playlist attribute="value">
+              <curve path="/my/file/path/"/ attribute="value" ...>
+              <curve path="...">
+            </playlist>
+
+        Relative paths are interpreted relative to the location of the
+        playlist file.
+        
+        Examples
+        --------
+
+        >>> root_path = os.path.sep + 'path'
+        >>> p = FilePlaylist(drivers=[],
+        ...                  path=os.path.join(root_path, 'to','playlist'))
+        >>> p.info['note'] = 'An example playlist'
+        >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
+        >>> c.info['note'] = 'The first curve'
+        >>> p.append(c)
+        >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
+        >>> c.info['note'] = 'The second curve'
+        >>> p.append(c)
+        >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
+        <?xml version="1.0" encoding="utf-8"?>
+        <playlist index="0" note="An example playlist" version="0.1">
+            <curve note="The first curve" path="../curve/one"/>
+            <curve note="The second curve" path="../curve/two"/>
+        </playlist>
+        <BLANKLINE>
+        >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
+        <?xml version="1.0" encoding="utf-8"?>
+        <playlist index="0" note="An example playlist" version="0.1">
+            <curve note="The first curve" path="/path/to/curve/one"/>
+            <curve note="The second curve" path="/path/to/curve/two"/>
+        </playlist>
+        <BLANKLINE>
+        """
+        implementation = xml.dom.minidom.getDOMImplementation()
+        # create the document DOM object and the root element
+        doc = implementation.createDocument(None, 'playlist', None)
+        root = doc.documentElement
+        root.setAttribute('version', self.version) # store playlist version
+        root.setAttribute('index', str(self._index))
+        for key,value in self.info.items(): # save info variables
+            root.setAttribute(key, str(value))
+        for curve in self: # save curves and their attributes
+            curve_element = doc.createElement('curve')
+            root.appendChild(curve_element)
+            path = os.path.abspath(os.path.expanduser(curve.path))
+            if absolute_paths == False:
+                path = os.path.relpath(
+                    path,
+                    os.path.abspath(os.path.expanduser(self.path)))
+            curve_element.setAttribute('path', path)
+            for key,value in curve.info.items():
+                curve_element.setAttribute(key, str(value))
+        string = doc.toprettyxml(encoding='utf-8')
+        root.unlink() # break circular references for garbage collection
+        return string
+
+    def _from_xml_doc(self, doc):
+        """Load a playlist from an :class:`xml.dom.minidom.Document`
+        instance.
+        """
+        root = doc.documentElement
+        for attribute,value in root.attributes.items():
+            if attribute == 'version':
+                assert value == self.version, \
+                    'Cannot read v%s playlist with a v%s reader' \
+                    % (value, self.version)
+            elif attribute == 'index':
+                self._index = int(value)
+            else:
+                self.info[attribute] = value
+        for curve_element in doc.getElementsByTagName('curve'):
+            path = curve_element.getAttribute('path')
+            info = dict(curve_element.attributes.items())
+            info.pop('path')
+            self.append_curve_by_path(path, info, identify=False)
+        self.jump(self._index) # ensure valid index
+
+    def from_string(self, string):
+        """Load a playlist from a string.
+
+        Examples
+        --------
+
+        >>> string = '''<?xml version="1.0" encoding="utf-8"?>
+        ... <playlist index="1" note="An example playlist" version="0.1">
+        ...     <curve note="The first curve" path="../curve/one"/>
+        ...     <curve note="The second curve" path="../curve/two"/>
+        ... </playlist>
+        ... '''
+        >>> p = FilePlaylist(drivers=[],
+        ...                  path=os.path.join('path', 'to','playlist'))
+        >>> p.from_string(string)
+        >>> p._index
+        1
+        >>> p.info
+        {u'note': u'An example playlist'}
+        >>> for curve in p:
+        ...     print curve.path
+        path/to/curve/one
+        path/to/curve/two
+        """
+        doc = xml.dom.minidom.parseString(string)
+        return self._from_xml_doc(doc)
+
+    def load(self, path=None):
+        """Load a playlist from a file.
+        """
+        if path != None:
+            self.path = path
+        if self.name == None:
+            self.name = os.path.basename(self.path)
+        doc = xml.dom.minidom.parse(path)
+        return self._from_xml_doc(doc)
+
+    def save(self, path):
+        """Saves the playlist in a XML file.
+        """
+        f = file(path, 'w')
+        f.write(self.flatten())
+        f.close()
+
+class NextCommand (Command):
+    """Move playlist to the next curve.
+    """
+    def __init__(self):
+        super(NextCommand, self).__init__(
+            name='next curve',
+            arguments=[
+                Argument(name='playlist', type='playlist', optional=False,
+                         help="""
+:class:``hooke.plugin.playlist.Playlist`` to act on.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+       params['playlist'].next()
+
+class PreviousCommand (Command):
+    """Move playlist to the previous curve.
+    """
+    def __init__(self):
+        super(PreviousCommand, self).__init__(
+            name='previous curve',
+            arguments=[
+                Argument(name='playlist', type='playlist', optional=False,
+                         help="""
+:class:``hooke.plugin.playlist.Playlist`` to act on.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+       params['playlist'].previous()
+
+class JumpCommand (Command):
+    """Move playlist to a given curve.
+    """
+    def __init__(self):
+        super(JumpCommand, self).__init__(
+            name='jump to curve',
+            arguments=[
+                Argument(name='playlist', type='playlist', optional=False,
+                         help="""
+:class:``hooke.plugin.playlist.Playlist`` to act on.
+""".strip()),
+                Argument(name='index', type='int', optional=False, help="""
+Index of target curve.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+       params['playlist'].jump(params['index'])
+
+class SaveCommand (Command):
+    """Save a playlist.
+    """
+    def __init__(self):
+        super(SaveCommand, self).__init__(
+            name='save playlist',
+            arguments=[
+                Argument(name='playlist', type='playlist', optional=False,
+                         help="""
+:class:``hooke.plugin.playlist.Playlist`` to act on.
+""".strip()),
+                Argument(name='output', type='file',
+                         help="""
+File name for the output playlist.  Defaults to overwring the input
+playlist.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+       params['playlist'].save(params['output'])
+
+class LoadCommand (Command):
+    """Load a playlist.
+    """
+    def __init__(self):
+        super(LoadCommand, self).__init__(
+            name='load playlist',
+            arguments=[
+                Argument(name='input', type='file', optional=False,
+                         help="""
+File name for the input playlist.
+""".strip()),
+                Argument(name='digests', type='digest', optional=False,
+                         count=-1,
+                         help="""
+Digests for loading curves.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+        p = FilePlaylist(drivers=params['drivers'], path=params['input'])
+        p.load()
+       outqueue.put(p)
+
+class AddCommand (Command):
+    """Add a curve to a playlist.
+    """
+    def __init__(self):
+        super(AddCommand, self).__init__(
+            name='add curve to playlist',
+            arguments=[
+                Argument(name='playlist', type='playlist', optional=False,
+                         help="""
+:class:``hooke.plugin.playlist.Playlist`` to act on.
+""".strip()),
+                Argument(name='input', type='file', optional=False,
+                         help="""
+File name for the input :class:`hooke.curve.Curve`.
+""".strip()),
+                Argument(name='info', type='dict', optional=True,
+                         help="""
+Additional information for the input :class:`hooke.curve.Curve`.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+        params['playlist'].append_curve_by_path(params['input'],
+                                                params['info'])
+
+class RemoveCommand (Command):
+    """Remove a curve from a playlist.
+    """
+    def __init__(self):
+        super(RemoveCommand, self).__init__(
+            name='remove curve from playlist',
+            arguments=[
+                Argument(name='playlist', type='playlist', optional=False,
+                         help="""
+:class:``hooke.plugin.playlist.Playlist`` to act on.
+""".strip()),
+                Argument(name='index', type='int', optional=False, help="""
+Index of target curve.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+        params['playlist'].pop(params['index'])
+        params['playlist'].jump(params._index)
+
+class FilterCommand (Command):
+    """Create a subset playlist via a selection function.
+    """
+    def __init__(self):
+        super(FilterCommand, self).__init__(
+            name='filter playlist',
+            arguments=[
+                Argument(name='playlist', type='playlist', optional=False,
+                         help="""
+:class:``hooke.plugin.playlist.Playlist`` to act on.
+""".strip()),
+                Argument(name='filter', type='function', optional=False,
+                         help="""
+Function returning `True` for "good" curves.
+""".strip()),
+                ],
+            help=self.__doc__)
+
+    def _run(inqueue, outqueue, params):
+        p = params['playlist'].filter(params['filter'])
+        outqueue.put(p)
index 5d731aed8c5e03fb531fe641c77624f6b1f94289..2646316c1dc348897ba1840218e5122098cf0b71 100644 (file)
@@ -30,7 +30,7 @@ class Playlists(wx.Panel):
         #if files:
             #playlist = hookeplaylist.Playlist(self.drivers)
             #for item in files:
-                #playlist.add_curve(item)
+                #playlist.append_curve_by_path(item)
         #if playlist.count > 0:
             #playlist_name = name
             #count = 1