From: W. Trevor King Date: Fri, 13 Aug 2010 01:38:04 +0000 (-0400) Subject: Added hooke.plugin.playlist.ApplyCommandStack and related changes. X-Git-Url: http://git.tremily.us/?p=hooke.git;a=commitdiff_plain;h=7f628b00df5065d72e89cc86be596756eb9f1ac0 Added hooke.plugin.playlist.ApplyCommandStack and related changes. Highlights: * Added test/apply_command_stack.py testing the new command. * Added hooke.plugin.playlist.PlaylistCommand mirroring CurveCommand. * Added hooke.plugin.playlist.AddingPlaylistCommand mirroring ColumnAddingCommand. * Renamed CommandStack._execute -> .execute_command since it may be used externally. * Changed assorted log messages to use CommandMessage string-ification. * Set Hooke.engine to None in the UI process. * Set ._index during NoteIndexList.items() iteration. --- diff --git a/hooke/command_stack.py b/hooke/command_stack.py index 1e6c27c..dcf2ce2 100644 --- a/hooke/command_stack.py +++ b/hooke/command_stack.py @@ -32,12 +32,12 @@ class CommandStack (list): >>> c.append(CommandMessage('CommandA', {'param':'C'})) >>> c.append(CommandMessage('CommandB', {'param':'D'})) - Implement a dummy :meth:`_execute` for testing. + Implement a dummy :meth:`execute_command` for testing. - >>> def execute(hooke, command_message): + >>> def execute_cmd(hooke, command_message): ... cm = command_message ... print 'EXECUTE', cm.command, cm.arguments - >>> c._execute = execute + >>> c.execute_command = execute_cmd >>> c.execute(hooke=None) # doctest: +ELLIPSIS EXECUTE CommandA {'param': 'A'} @@ -55,10 +55,24 @@ class CommandStack (list): ... return command_message.command == 'CommandB' >>> c.filter = filter - Apply the stack to the current curve + Apply the stack to the current curve. + >>> c.execute(hooke=None) # doctest: +ELLIPSIS EXECUTE CommandB {'param': 'B'} EXECUTE CommandB {'param': 'D'} + + Execute a new command and add it to the stack. + + >>> cm = CommandMessage('CommandC', {'param':'E'}) + >>> c.execute_command(hooke=None, command_message=cm) + EXECUTE CommandC {'param': 'E'} + >>> c.append(cm) + >>> print [repr(cm) for cm in c] # doctest: +NORMALIZE_WHITESPACE + ["", + "", + "", + "", + ""] """ def execute(self, hooke): """Execute a stack of commands. @@ -69,7 +83,8 @@ class CommandStack (list): """ for command_message in self: if self.filter(hooke, command_message) == True: - self._execute(hooke, command_message) + self.execute_command( + hooke=hooke, command_message=command_message) def filter(self, hooke, command_message): """Return `True` to execute `command_message`, `False` otherwise. @@ -78,5 +93,6 @@ class CommandStack (list): """ return True - def _execute(self, hooke, command_message): - hooke.run_command(command_message.command, command_message.arguments) + def execute_command(self, hooke, command_message): + hooke.run_command(command=command_message.command, + arguments=command_message.arguments) diff --git a/hooke/engine.py b/hooke/engine.py index c7832ab..e024c73 100644 --- a/hooke/engine.py +++ b/hooke/engine.py @@ -106,8 +106,7 @@ class CommandEngine (object): 'engine closing, placed hooke instance in return queue') break assert isinstance(msg, CommandMessage), type(msg) - log.debug('engine running %s with %s' - % (msg.command, msg.arguments)) + log.debug('engine running %s' % msg) cmd = hooke.command_by_name[msg.command] cmd.run(hooke, ui_to_command_queue, command_to_ui_queue, **msg.arguments) @@ -124,8 +123,8 @@ class CommandEngine (object): interaction. """ log = logging.getLogger('hooke') - log.debug('engine running internal %s with %s' - % (command, arguments)) + log.debug('engine running internal %s' + % CommandMessage(command, arguments)) outqueue = Queue() cmd = hooke.command_by_name[command] cmd.run(hooke, NullQueue(), outqueue, **arguments) diff --git a/hooke/hooke.py b/hooke/hooke.py index 70ca59a..c475d3a 100644 --- a/hooke/hooke.py +++ b/hooke/hooke.py @@ -123,7 +123,7 @@ class Hooke (object): self.config.write() # Does not preserve original comments def run_command(self, command, arguments): - """Run `command` with `arguments` using + """Run the command named `command` with `arguments` using :meth:`~hooke.engine.CommandEngine.run_command`. Allows for running commands without spawning another process @@ -169,6 +169,7 @@ class HookeRunner (object): command = multiprocessing.Process(name='command engine', target=hooke.engine.run, args=(hooke, ui_to_command, command_to_ui)) command.start() + hooke.engine = None # no more need for the UI-side version. return (ui_to_command, command_to_ui, command) def _cleanup_run(self, ui_to_command, command_to_ui, command): diff --git a/hooke/playlist.py b/hooke/playlist.py index 9eb7e8b..dfec6d7 100644 --- a/hooke/playlist.py +++ b/hooke/playlist.py @@ -88,13 +88,27 @@ class NoteIndexList (list): def items(self, reverse=False): """Iterate through `self` calling `_setup_item` on each item before yielding. + + Notes + ----- + Updates :attr:`_index` during the iteration so + :func:`~hooke.plugin.curve.current_curve_callback` works as + expected in :class:`~hooke.command.Command`\s called from + :class:`~hooke.plugin.playlist.ApplyCommandStack`. After the + iteration completes, :attr:`_index` is restored to its + original value. """ + index = self._index items = self if reverse == True: - items = reversed(self) - for item in items: + items = reversed(enumerate(self)) + else: + items = enumerate(self) + for i,item in items: + self._index = i self._setup_item(item) yield item + self._index = index def filter(self, keeper_fn=lambda item:True, *args, **kwargs): c = copy.deepcopy(self) diff --git a/hooke/plugin/curve.py b/hooke/plugin/curve.py index 75f28ee..e6d5ce3 100644 --- a/hooke/plugin/curve.py +++ b/hooke/plugin/curve.py @@ -91,7 +91,7 @@ class CurveCommand (Command): Notes ----- `hooke` is intended to attach the selected curve to the local - playlist, and the returned curve should not be effected by the + playlist; the returned curve should not be effected by the state of `hooke`. This is important for reliable :class:`~hooke.command_stack.CommandStack`\s. """ diff --git a/hooke/plugin/playlist.py b/hooke/plugin/playlist.py index 1ab04fd..4a5efd4 100644 --- a/hooke/plugin/playlist.py +++ b/hooke/plugin/playlist.py @@ -37,7 +37,9 @@ class PlaylistPlugin (Builtin): GetCommand(self), IndexCommand(self), CurveListCommand(self), SaveCommand(self), LoadCommand(self), AddCommand(self), AddGlobCommand(self), - RemoveCommand(self), FilterCommand(self), NoteFilterCommand(self)] + RemoveCommand(self), ApplyCommandStack(self), + FilterCommand(self), NoteFilterCommand(self), + ] # Define common or complicated arguments @@ -69,7 +71,8 @@ def playlist_name_callback(hooke, command, argument, value): i += 1 PlaylistNameArgument = Argument( - name='name', type='string', optional=True, callback=playlist_name_callback, + name='output playlist', type='string', optional=True, + callback=playlist_name_callback, help=""" Name of the new playlist (defaults to an auto-generated name). """.strip()) @@ -78,40 +81,85 @@ def all_drivers_callback(hooke, command, argument, value): return hooke.drivers +# Define useful command subclasses + +class PlaylistCommand (Command): + """A :class:`~hooke.command.Command` operating on a + :class:`~hooke.playlist.Playlist`. + """ + def __init__(self, **kwargs): + if 'arguments' in kwargs: + kwargs['arguments'].insert(0, PlaylistArgument) + else: + kwargs['arguments'] = [PlaylistArgument] + super(PlaylistCommand, self).__init__(**kwargs) + + def _playlist(self, hooke, params): + """Get the selected playlist. + + Notes + ----- + `hooke` is intended to attach the selected playlist to the + local hooke instance; the returned playlist should not be + effected by the state of `hooke`. + """ + # HACK? rely on params['playlist'] being bound to the local + # hooke (i.e. not a copy, as you would get by passing a + # playlist through the queue). Ugh. Stupid queues. As an + # alternative, we could pass lookup information through the + # queue... + return params['playlist'] + + +class PlaylistAddingCommand (Command): + """A :class:`~hooke.command.Command` adding a + :class:`~hooke.playlist.Playlist`. + """ + def __init__(self, **kwargs): + if 'arguments' in kwargs: + kwargs['arguments'].insert(0, PlaylistNameArgument) + else: + kwargs['arguments'] = [PlaylistNameArgument] + super(PlaylistAddingCommand, self).__init__(**kwargs) + + def _set_playlist(self, hooke, params, playlist): + """Attach a new playlist. + """ + playlist.name = params['output playlist'] + hooke.playlists.append(playlist) + + # Define commands -class NextCommand (Command): +class NextCommand (PlaylistCommand): """Move playlist to the next curve. """ def __init__(self, plugin): super(NextCommand, self).__init__( - name='next curve', - arguments=[PlaylistArgument], - help=self.__doc__, plugin=plugin) + name='next curve', help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - params['playlist'].next() + self._playlist(hooke, params).next() + -class PreviousCommand (Command): +class PreviousCommand (PlaylistCommand): """Move playlist to the previous curve. """ def __init__(self, plugin): super(PreviousCommand, self).__init__( - name='previous curve', - arguments=[PlaylistArgument], - help=self.__doc__, plugin=plugin) + name='previous curve', help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - params['playlist'].previous() + self._playlist(hooke, params).previous() + -class JumpCommand (Command): +class JumpCommand (PlaylistCommand): """Move playlist to a given curve. """ def __init__(self, plugin): super(JumpCommand, self).__init__( name='jump to curve', arguments=[ - PlaylistArgument, Argument(name='index', type='int', optional=False, help=""" Index of target curve. """.strip()), @@ -119,49 +167,45 @@ Index of target curve. help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - params['playlist'].jump(params['index']) + self._playlist(hooke, params).jump(params['index']) -class IndexCommand (Command): + +class IndexCommand (PlaylistCommand): """Print the index of the current curve. The first curve has index 0. """ def __init__(self, plugin): super(IndexCommand, self).__init__( - name='curve index', - arguments=[ - PlaylistArgument, - ], - help=self.__doc__, plugin=plugin) + name='curve index', help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - outqueue.put(params['playlist'].index()) + outqueue.put(self._playlist(hooke, params).index()) + -class GetCommand (Command): +class GetCommand (PlaylistCommand): """Return a :class:`hooke.playlist.Playlist`. """ def __init__(self, plugin): super(GetCommand, self).__init__( - name='get playlist', - arguments=[PlaylistArgument], - help=self.__doc__, plugin=plugin) + name='get playlist', help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - outqueue.put(params['playlist']) + outqueue.put(self._playlist(hooke, params)) -class CurveListCommand (Command): + +class CurveListCommand (PlaylistCommand): """Get the curves in a playlist. """ def __init__(self, plugin): super(CurveListCommand, self).__init__( - name='playlist curves', - arguments=[PlaylistArgument], - help=self.__doc__, plugin=plugin) + name='playlist curves', help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - outqueue.put(list(params['playlist'])) + outqueue.put(list(self._playlist(hooke, params))) + -class SaveCommand (Command): +class SaveCommand (PlaylistCommand): """Save a playlist. """ def __init__(self, plugin): @@ -179,9 +223,10 @@ created from scratch with 'new playlist'), this option is required. help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - params['playlist'].save(params['output']) + self._playlist(hooke, params).save(params['output']) -class LoadCommand (Command): + +class LoadCommand (PlaylistAddingCommand): """Load a playlist. """ def __init__(self, plugin): @@ -203,17 +248,17 @@ Drivers for loading curves. def _run(self, hooke, inqueue, outqueue, params): p = FilePlaylist(drivers=params['drivers'], path=params['input']) p.load(hooke=hooke) - hooke.playlists.append(p) + self._set_playlist(hooke, params, p) outqueue.put(p) -class AddCommand (Command): + +class AddCommand (PlaylistCommand): """Add a curve to a playlist. """ def __init__(self, plugin): super(AddCommand, self).__init__( name='add curve to playlist', arguments=[ - PlaylistArgument, Argument(name='input', type='file', optional=False, help=""" File name for the input :class:`hooke.curve.Curve`. @@ -226,10 +271,11 @@ Additional information for the input :class:`hooke.curve.Curve`. help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - params['playlist'].append_curve_by_path( + self._playlist(hooke, params).append_curve_by_path( params['input'], params['info'], hooke=hooke) -class AddGlobCommand (Command): + +class AddGlobCommand (PlaylistCommand): """Add curves to a playlist with file globbing. Adding lots of files one at a time can be tedious. With this @@ -240,7 +286,6 @@ class AddGlobCommand (Command): super(AddGlobCommand, self).__init__( name='glob curves to playlist', arguments=[ - PlaylistArgument, Argument(name='input', type='string', optional=False, help=""" File name glob for the input :class:`hooke.curve.Curve`. @@ -254,17 +299,17 @@ Additional information for the input :class:`hooke.curve.Curve`. def _run(self, hooke, inqueue, outqueue, params): for path in sorted(glob.glob(params['input'])): - params['playlist'].append_curve_by_path( + self._playlist(hooke, params).append_curve_by_path( path, params['info'], hooke=hooke) -class RemoveCommand (Command): + +class RemoveCommand (PlaylistCommand): """Remove a curve from a playlist. """ def __init__(self, plugin): super(RemoveCommand, self).__init__( name='remove curve from playlist', arguments=[ - PlaylistArgument, Argument(name='index', type='int', optional=False, help=""" Index of target curve. """.strip()), @@ -272,10 +317,47 @@ Index of target curve. help=self.__doc__, plugin=plugin) def _run(self, hooke, inqueue, outqueue, params): - params['playlist'].pop(params['index']) - params['playlist'].jump(params.index()) + self._playlist(hooke, params).pop(params['index']) + self._playlist(hooke, params).jump(params.index()) -class FilterCommand (Command): + +class ApplyCommandStack (PlaylistCommand): + """Apply a :class:`~hooke.command_stack.CommandStack` to each + curve in a playlist. + + TODO: discuss `evaluate`. + """ + def __init__(self, plugin): + super(ApplyCommandStack, self).__init__( + name='apply command stack', + arguments=[ + Argument(name='commands', type='command stack', optional=False, + help=""" +Command stack to apply to each curve. +""".strip()), + Argument(name='evaluate', type='bool', default=False, + help=""" +Evaluate the applied command stack immediately. +""".strip()), + ], + help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + if len(params['commands']) == 0: + return + p = self._playlist(hooke, params) + if params['evaluate'] == True: + for curve in p.items(): + for command in params['commands']: + curve.command_stack.execute_command(hooke, command) + curve.command_stack.append(command) + else: + for curve in p: + curve.command_stack.extend(params['commands']) + curve.unload() # force command stack execution on next access. + + +class FilterCommand (PlaylistAddingCommand, PlaylistCommand): """Create a subset playlist via a selection function. Removing lots of curves one at a time can be tedious. With this @@ -294,12 +376,7 @@ class FilterCommand (Command): """ def __init__(self, plugin, name='filter playlist'): super(FilterCommand, self).__init__( - name=name, - arguments=[ - PlaylistArgument, - PlaylistNameArgument, - ], - help=self.__doc__, plugin=plugin) + name=name, help=self.__doc__, plugin=plugin) if not hasattr(self, 'filter'): self.arguments.append( Argument(name='filter', type='function', optional=False, @@ -313,14 +390,15 @@ Function returning `True` for "good" curves. filter_fn = params['filter'] else: filter_fn = self.filter - p = params['playlist'].filter(filter_fn, + p = self._playlist(hooke, params).filter(filter_fn, hooke=hooke, inqueue=inqueue, outqueue=outqueue, params=params) p.name = params['name'] if hasattr(p, 'path') and p.path != None: p.set_path(os.path.join(os.path.dirname(p.path), p.name)) - hooke.playlists.append(p) + self._set_playlist(hooke, params, p) outqueue.put(p) + class NoteFilterCommand (FilterCommand): """Create a subset playlist of curves with `.info['note'] != None`. """ diff --git a/hooke/ui/commandline.py b/hooke/ui/commandline.py index 5f04c36..192918e 100644 --- a/hooke/ui/commandline.py +++ b/hooke/ui/commandline.py @@ -129,8 +129,9 @@ class DoCommand (CommandMethod): self.cmd.stdout.write(str(e).lstrip()+'\n') self.cmd.stdout.write('Failure\n') return - self.log.debug('executing %s with %s' % (self.command.name, args)) - self.cmd.inqueue.put(CommandMessage(self.command.name, args)) + cm = CommandMessage(self.command.name, args) + self.log.debug('executing %s' % cm) + self.cmd.inqueue.put(cm) while True: msg = self.cmd.outqueue.get() if isinstance(msg, Exit): diff --git a/hooke/ui/gui/__init__.py b/hooke/ui/gui/__init__.py index f885177..38ba927 100644 --- a/hooke/ui/gui/__init__.py +++ b/hooke/ui/gui/__init__.py @@ -335,8 +335,9 @@ class HookeFrame (wx.Frame): args[arg.name].pop() if len(args[arg.name]) == 0: args[arg.name] = arg.default - self.log.debug('executing %s with %s' % (command.name, args)) - self.inqueue.put(CommandMessage(command.name, args)) + cm = CommandMessage(self.command.name, args) + self.log.debug('executing %s' % cm) + self.inqueue.put(cm) results = [] while True: msg = self.outqueue.get() diff --git a/test/apply_command_stack.py b/test/apply_command_stack.py new file mode 100644 index 0000000..e6a659e --- /dev/null +++ b/test/apply_command_stack.py @@ -0,0 +1,81 @@ +# Copyright (C) 2010 W. Trevor King +# +# This file is part of Hooke. +# +# Hooke is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Hooke is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General +# Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Hooke. If not, see +# . + +""" +>>> import logging +>>> import sys +>>> from hooke.command_stack import CommandStack +>>> from hooke.engine import CommandMessage +>>> from hooke.hooke import Hooke +>>> h = Hooke() + +Setup logging so we can check command output in the doctest. + +>>> log = logging.getLogger('hooke') +>>> stdout_handler = logging.StreamHandler(sys.stdout) +>>> log.addHandler(stdout_handler) + +Setup a playlist to act on. + +>>> h.run_command('load playlist', +... {'input': 'test/data/vclamp_picoforce/playlist'}) # doctest: +ELLIPSIS +engine running internal +engine message from load playlist (): +engine message from load playlist (): +>>> stack = CommandStack([ +... CommandMessage('get curve'), +... CommandMessage('zero surface contact point'), +... ]) + +Test `apply command stack`. + +>>> h.run_command('apply command stack', +... {'commands': stack, 'evaluate': True}) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF +engine running internal , + ], + evaluate: True}> +loading curve 20071120a_i27_t33.100 with driver ... +engine running internal +engine message from get curve (): +engine message from get curve (): +engine running internal +engine message from zero surface contact point (): {...} +engine message from zero surface contact point (): +loading curve 20071120a_i27_t33.101 with driver ... +engine running internal +engine message from get curve (): +engine message from get curve (): +engine running internal +engine message from zero surface contact point (): {...} +engine message from zero surface contact point (): +loading curve 20071120a_i27_t33.102 with driver ... +... +loading curve 20071120a_i27_t33.199 with driver ... +engine running internal +engine message from get curve (): +engine message from get curve (): +engine running internal +engine message from zero surface contact point (): {...} +engine message from zero surface contact point (): +loading curve 0x06130001 with driver ... +unloading curve 20071120a_i27_t33.100 +engine running internal +... +engine message from apply command stack (): +"""