From 1b99f4d42cd2440ea3a6cd71fa9e6d06d46b8522 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 10 May 2010 14:19:59 -0400 Subject: [PATCH] Add hooke.playlist.NoteIndexList and refactor exception handling in commandline. 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 | 10 ++--- hooke/hooke.py | 2 +- hooke/playlist.py | 69 +++++++++++++++++++--------------- hooke/plugin/cut.py | 27 ++++++++++++-- hooke/plugin/playlist.py | 10 +++-- hooke/ui/__init__.py | 2 +- hooke/ui/commandline.py | 81 ++++++++++++++++++++++++++-------------- 7 files changed, 127 insertions(+), 74 deletions(-) diff --git a/hooke/command.py b/hooke/command.py index c044c06..bc18ffd 100644 --- a/hooke/command.py +++ b/hooke/command.py @@ -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 diff --git a/hooke/hooke.py b/hooke/hooke.py index 4142a5f..5fc357e 100644 --- a/hooke/hooke.py +++ b/hooke/hooke.py @@ -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( diff --git a/hooke/playlist.py b/hooke/playlist.py index 35bf547..d629023 100644 --- a/hooke/playlist.py +++ b/hooke/playlist.py @@ -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' diff --git a/hooke/plugin/cut.py b/hooke/plugin/cut.py index dc55d89..68dd2a0 100644 --- a/hooke/plugin/cut.py +++ b/hooke/plugin/cut.py @@ -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 diff --git a/hooke/plugin/playlist.py b/hooke/plugin/playlist.py index 8d0559c..060312c 100644 --- a/hooke/plugin/playlist.py +++ b/hooke/plugin/playlist.py @@ -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() diff --git a/hooke/ui/__init__.py b/hooke/ui/__init__.py index f46e893..3d0a657 100644 --- a/hooke/ui/__init__.py +++ b/hooke/ui/__init__.py @@ -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.' \ diff --git a/hooke/ui/commandline.py b/hooke/ui/commandline.py index 41fe990..9d1806e 100644 --- a/hooke/ui/commandline.py +++ b/hooke/ui/commandline.py @@ -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()) -- 2.26.2