Add hooke.playlist.NoteIndexList and refactor exception handling in commandline.
authorW. Trevor King <wking@drexel.edu>
Mon, 10 May 2010 18:19:59 +0000 (14:19 -0400)
committerW. Trevor King <wking@drexel.edu>
Mon, 10 May 2010 18:19:59 +0000 (14:19 -0400)
Highlights:
  * hooke.playlist classes now subclass the new hooke.playlist.NoteIndexList
  * hooke.plugin.cut.CurveArgument replaces previous _run hacking with
    a callback to initialize the default curve.x
  * hooke.ui.commandline.command_parser -> CommandLineParser.  The new
    class is cleaner, and raises exceptions rather than exiting.  We
    don't want Hooke dying until the user has time to clean up.

hooke/command.py
hooke/hooke.py
hooke/playlist.py
hooke/plugin/cut.py
hooke/plugin/playlist.py
hooke/ui/__init__.py
hooke/ui/commandline.py

index c044c0682135271bda016d921e83138aa6bb0088..bc18ffdb903ceade6f4cd950757a4eaa1c96b427 100644 (file)
@@ -9,8 +9,7 @@ import traceback
 
 
 class CommandExit (Exception):
-    def __str__(self):
-        return self.__class__.__name__
+    pass
 
 class Success (CommandExit):
     pass
@@ -20,10 +19,10 @@ class Failure (CommandExit):
 
 class UncaughtException (Failure):
     def __init__(self, exception):
-        super(UncaughtException, self).__init__(exception)
         self.exception = exception
         self.exc_string = traceback.format_exc()
         sys.exc_clear()
+        super(UncaughtException, self).__init__(self.exc_string)
 
 class Command (object):
     """One-line command description here.
@@ -68,14 +67,15 @@ class Command (object):
             self._run(hooke, inqueue, outqueue, params)
         except CommandExit, e:
             if isinstance(e, Failure):
-                outqueue.put(str(e))
                 outqueue.put(e)
                 return 1
+            # other CommandExit subclasses fall through to the end
         except Exception, e:
             x = UncaughtException(e)
-            outqueue.put(x.exc_string)
             outqueue.put(x)
             return 1
+        else:
+            e = Success()
         outqueue.put(e)
         return 0
 
index 4142a5f2a4ff5385ea09e369805990a9b88ecf60..5fc357e5d789eeea0a14ccc6fe45e0d33e7b79dd 100644 (file)
@@ -35,7 +35,7 @@ class Hooke (object):
         self.load_ui()
         self.command = engine.CommandEngine()
 
-        self.playlists = playlist.PlaylistManager()
+        self.playlists = playlist.NoteIndexList()
 
     def load_plugins(self):
         self.plugins = plugin_mod.load_graph(
index 35bf54781f8c1d887190fc672728e92b9ef89d43..d629023f57dd6bf667a88c70c870f83a61b0ed2b 100644 (file)
@@ -10,36 +10,25 @@ import xml.dom.minidom
 
 from . import curve as curve
 
-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`).
+class NoteIndexList (list):
+    """A list that keeps track of a "current" item and additional notes.
+
+    :attr:`index` (i.e. "bookmark") is the index of the currently
+    current curve.  Also keep a :class:`dict` of additional information
+    (:attr:`info`).
     """
-    def __init__(self, drivers, name=None):
-        super(Playlist, self).__init__()
-        self.drivers = drivers
+    def __init__(self, name=None):
+        super(NoteIndexList, self).__init__()
         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):
+    def current(self):
+        if len(self) == 0:
+            return None
         return self[self._index]
 
-    def has_curves(self):
-        return len(self) > 0
-
     def jump(self, index):
         if len(self) == 0:
             self._index = 0
@@ -52,17 +41,35 @@ class Playlist (list):
     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())
+    def filter(self, keeper_fn=lambda item:True):
+        c = copy.deepcopy(self)
+        for item in reversed(c):
+            if keeper_fn(item) != True:
+                c.remove(item)
+        try: # attempt to maintain the same current item
+            c._index = c.index(self.current())
         except ValueError:
-            playlist._index = 0
-        return playlist
+            c._index = 0
+        return c
+
+class Playlist (NoteIndexList):
+    """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
+
+    Keeps a list of :attr:`drivers` for loading curves.
+    """
+    def __init__(self, drivers, name=None):
+        super(Playlist, self).__init__(name=name)
+        self.drivers = drivers
 
+    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
 
 class FilePlaylist (Playlist):
     version = '0.1'
index dc55d89161a719e27bb81d8076d2186c2a4420bd..68dd2a0223b2a1f55f4ef466adfaa4c331b11aa9 100644 (file)
@@ -1,7 +1,7 @@
 """Defines :class:`CutPlugin` and :class:`CutCommand`.
 """
 
-from ..command import Command, Argument
+from ..command import Command, Argument, Failure
 from ..plugin import Plugin
 
 
@@ -12,6 +12,27 @@ class CutPlugin (Plugin):
     def commands(self):
         return [CutCommand()]
 
+
+# Define common or complicated arguments
+
+def current_curve_callback(hooke, command, argument, value):
+    playlist = hooke.playlists.current()
+    if playlist == None:
+        raise Failure('No playlists loaded')
+    curve = playlist.current()
+    if curve == None:
+        raise Failure('No curves in playlist %s' % playlist.name)
+    return curve
+
+CurveArgument = Argument(
+    name='curve', type='curve', callback=current_curve_callback,
+    help="""
+:class:`hooke.curve.Curve` to cut from.  Defaults to the current curve.
+""".strip())
+
+
+# Define commands
+
 class CutCommand (Command):
     """Cut the selected signal between two points and write it to a file.
 
@@ -22,9 +43,7 @@ class CutCommand (Command):
         super(CutCommand, self).__init__(
             name='cut',
             arguments=[
-                Argument(name='curve', type='curve', help="""
-:class:`hooke.curve.Curve` to cut from.  Defaults to the current curve.
-""".strip()),
+                CurveArgument,
                 Argument(name='block', aliases=['set'], type='int', default=0,
                     help="""
 Data block to save.  For an approach/retract force curve, `0` selects
index 8d0559c91c613544c988ea55c0e1648d714dd125..060312c4006b79db918a11683bc2abebfb26953b 100644 (file)
@@ -5,7 +5,7 @@ classes.
 
 import glob
 
-from ..command import Command, Argument
+from ..command import Command, Argument, Failure
 from ..playlist import FilePlaylist
 from ..plugin import Builtin
 
@@ -20,17 +20,21 @@ class PlaylistPlugin (Builtin):
                 AddCommand(), AddGlobCommand(),
                 RemoveCommand(), FilterCommand()]
 
+
 # Define common or complicated arguments
 
 def current_playlist_callback(hooke, command, argument, value):
-    return hooke.playlists.current()
+    playlist = hooke.playlists.current()
+    if playlist == None:
+        raise Failure('No playlists loaded')
+    return playlist
 
 PlaylistArgument = Argument(
     name='playlist', type='playlist', callback=current_playlist_callback,
     help="""
 :class:`hooke.plugin.playlist.Playlist` to act on.  Defaults to the
 current playlist.
-""".strip()),
+""".strip())
 
 def playlist_name_callback(hooke, command, argument, value):
         return hooke.playlists.free_name()
index f46e8937e54d5d3c7cf2dd18b23ed1481cabce3a..3d0a65762d589efb7a1270644ef036da4ff5fda0 100644 (file)
@@ -65,7 +65,7 @@ COPYRIGHT
 """ % version()).strip()
 
     def _playlist_status(self, playlist):
-        if playlist.has_curves():
+        if len(playlist) > 0:
             return '%s (%s/%s)' % (playlist.name, playlist._index + 1,
                                    len(playlist))
         return 'The playlist %s does not contain any valid force curve data.' \
index 41fe990a436ff84dc3b50280534dac815912e82e..9d1806e3778b939af3e148da0ee4ccbd17088f28 100644 (file)
@@ -8,14 +8,46 @@ import readline # including readline makes cmd.Cmd.cmdloop() smarter
 import shlex
 
 from ..command import CommandExit, Command, Argument
+from ..compat.odict import odict
 from ..ui import UserInterface, CommandMessage
 
 
-# Define a few helper classes.  The .__call__ methods of these
-# functions will provide the do_*, help_*, and complete_* methods of
-# HookeCmd.
+# Define a few helper classes.
+
+class CommandLineParser (optparse.OptionParser):
+    """Implement a command line syntax for a
+    :class:`hooke.command.Command`.
+    """
+    def __init__(self, command):
+        optparse.OptionParser.__init__(self, prog=command.name)
+        self.command = command
+        self.command_opts = odict()
+        self.command_args = odict()
+        for a in command.arguments:
+            if a.name == 'help':
+                continue # 'help' is a default OptionParser option
+            name = a.name.replace('_', '-')
+            if a.optional == True:
+                self.add_option('--%s' % name, dest=a.name, default=a.default)
+                self.command_opts[name] = a
+            else:
+                self.command_args[name] = a
+
+    def exit(self, status=0, msg=None):
+        """Override :meth:`optparse.OptionParser.exit` which calls
+        :func:`sys.exit`.
+        """
+        if msg:
+            raise optparse.OptParseError(msg)
+        raise optparse.OptParseError('OptParse EXIT')
 
 class CommandMethod (object):
+    """Base class for method replacer.
+
+    The .__call__ methods of `CommandMethod` subclasses functions will
+    provide the `do_*`, `help_*`, and `complete_*` methods of
+    :class:`HookeCmd`.
+    """
     def __init__(self, cmd, command):
         self.cmd = cmd
         self.command = command
@@ -23,32 +55,24 @@ class CommandMethod (object):
     def __call__(self, *args, **kwargs):
         raise NotImplementedError
 
-def command_parser(command):
-    p = optparse.OptionParser()
-    opts = []
-    args = []
-    for a in command.arguments:
-        if a.name == 'help':
-            continue # 'help' is a default OptionParser option
-        name = a.name.replace('_', '-')
-        if a.optional == True:
-            p.add_option('--%s' % name, dest=a.name, default=a.default)
-            opts.append((name, a))
-        else:
-            args.append((name, a))
-    return (p, opts, args)
-
 class DoCommand (CommandMethod):
     def __init__(self, *args, **kwargs):
         super(DoCommand, self).__init__(*args, **kwargs)
-        self.parser,self.opts,self.args = command_parser(self.command)
+        self.parser = CommandLineParser(self.command)
 
     def __call__(self, args):
-        args = self._parse_args(self.command, args)
+        try:
+            args = self._parse_args(self.command, args)
+        except optparse.OptParseError, e:
+            self.cmd.stdout.write(str(e))
+            self.cmd.stdout.write('Failure\n')
+            return
         self.cmd.inqueue.put(CommandMessage(self.command, args))
         while True:
             msg = self.cmd.outqueue.get()
             if isinstance(msg, CommandExit):
+                self.cmd.stdout.write(msg.__class__.__name__+'\n')
+                self.cmd.stdout.write(str(msg).rstrip()+'\n')
                 break
             self.cmd.stdout.write(str(msg).rstrip()+'\n')
 
@@ -56,18 +80,16 @@ class DoCommand (CommandMethod):
         argv = shlex.split(args, comments=True, posix=True)
         options,args = self.parser.parse_args(argv)
         params = {}
-        for namearg in self.opts:
-            name,argument = namearg
+        for name,argument in self.parser.command_opts.items():
             params[name] = getattr(options, name)
-        for namearg,value in zip(self.args, args):
-            name,argument = namearg
+        for name,argument in self.parser.command_args.items():
             params[name] = value
         return params
 
 class HelpCommand (CommandMethod):
     def __init__(self, *args, **kwargs):
         super(HelpCommand, self).__init__(*args, **kwargs)
-        self.parser,self.opts,self.args = command_parser(self.command)
+        self.parser = CommandLineParser(self.command)
 
     def __call__(self):
         blocks = [self.command.help(),
@@ -80,11 +102,12 @@ class HelpCommand (CommandMethod):
         return self.command.help()
 
     def _usage_string(self):
-        if len(self.args) == len(self.command.arguments):
+        if len(self.parser.command_opts) == 0:
             options_string = ''
         else:
             options_string = '[options]'
-        arg_string = ' '.join([name for name,arg in self.args])
+        arg_string = ' '.join([name for name,arg
+                               in self.parser.command_args.items()])
         return ' '.join([x for x in [self.command.name,
                                      options_string,
                                      arg_string]
@@ -155,8 +178,8 @@ class CommandLine (UserInterface):
     def __init__(self):
         super(CommandLine, self).__init__(name='command line')
 
-    def run(self, hooke, ui_to_command_queue, command_to_ui_queue):
-        cmd = HookeCmd(hooke,
+    def run(self, commands, ui_to_command_queue, command_to_ui_queue):
+        cmd = HookeCmd(commands,
                        inqueue=ui_to_command_queue,
                        outqueue=command_to_ui_queue)
         cmd.cmdloop(self._splash_text())