From: W. Trevor King Date: Fri, 13 Aug 2010 16:34:25 +0000 (-0400) Subject: Fleshed out hooke.plugin.command_stack except for save/load X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=a078c0a31187b85ce426eba9e1745d179ad8ed02;p=hooke.git Fleshed out hooke.plugin.command_stack except for save/load --- diff --git a/hooke/plugin/__init__.py b/hooke/plugin/__init__.py index 63f17a3..95c56e2 100644 --- a/hooke/plugin/__init__.py +++ b/hooke/plugin/__init__.py @@ -55,6 +55,7 @@ default. TODO: autodiscovery """ BUILTIN_MODULES = [ + 'command_stack', 'config', 'curve', 'debug', diff --git a/hooke/plugin/command_stack.py b/hooke/plugin/command_stack.py index 5552c7e..abb03b7 100644 --- a/hooke/plugin/command_stack.py +++ b/hooke/plugin/command_stack.py @@ -23,220 +23,193 @@ and several associated :class:`~hooke.command.Command`\s exposing :mod`hooke.command_stack`'s functionality. """ -from ..command import Command, Argument, Failure +import logging +from Queue import Queue + +from ..command import Command, Argument, Success, Failure +from ..command_stack import CommandStack +from ..engine import CloseEngine, CommandMessage from . import Builtin -from .curve import CurveCommand - -class macroCommands(object): - - currentmacro=[] - pause=0 - auxprompt=[] - macrodir=None - - - def _plug_init(self): - self.currentmacro=[] - self.auxprompt=self.prompt - self.macrodir=self.config['workdir'] - if not os.path.exists(os.path.join(self.macrodir,'macros')): - try: - os.mkdir('macros') - except: - print 'Warning: cannot create macros folder.' - print 'Probably you do not have permissions in your Hooke folder, use macro at your own risk.' - self.macrodir=os.path.join(self.macrodir,'macros') - - def collect(self): - - print 'Enter STOP / PAUSE to go back to normal mode\nUNDO to remove last command' - line=[] - while not(line=='STOP' or line=='PAUSE'): - line=raw_input('hooke (macroREC): ') - if line=='PAUSE': - self.pause=1 - self.prompt='hooke (macroPAUSE): ' - break - if line=='STOP': - self.prompt=self.auxprompt - self.do_recordmacro('stop') - break - if line=='UNDO': - self.currentmacro.pop() - continue - param=line.split() - - #FIXME check if accessing param[2] when it doesnt exist breaks something - if param[0] =='export': - exportline=param[0]+' __curve__ ' - if len(param)==3: - exportline=exportline+param[2] - self.currentmacro.append(exportline) - self.onecmd(line) - continue - - if param[0] =='txt': - exportline=param[0] - if len(param)==3: - exportline=exportline+' '+param[2] - exportline=exportline+'__curve__' - self.currentmacro.append(exportline) - self.onecmd(line) - continue - - self.onecmd(line) - - self.currentmacro.append(line) - - - def do_recordmacro(self, args): - '''RECORDMACRO - Stores input commands to create script files - ------- - Syntax: recordmacro [start / stop] - If a macro is currently paused start resumes recording - ''' - - - if len(args)==0: - args='start' - - if args=='stop': - self.pause=0 - self.prompt=self.auxprompt - if len(self.currentmacro) != 0: - answer=linput.safeinput('Do you want to save this macro? ',['y']) - if answer[0].lower() == 'y': - self.do_savemacro('') - else: - print 'Macro discarded' - self.currentmacro=[] - else: - print 'Macro was empty' - - if args=='start': - - if self.pause==1: - self.pause=0 - self.collect() - else: - if len(self.currentmacro) != 0: - answer=linput.safeinput('Another macro is already beign recorded\nDo you want to save it?',['y']) - if answer[0].lower() == 'y': - self.do_savemacro('') - else: - print 'Old macro discarded, you can start recording the new one' - - self.currentmacro=[] - self.collect() - - - def do_savemacro(self, macroname): - - '''SAVEMACRO - Saves previously recorded macro into a script file for future use - ------- - Syntax: savemacro [macroname] - If no macroname is supplied one will be interactively asked - ''' - - saved_ok=0 - if self.currentmacro==None: - print 'No macro is being recorded!' - return 0 - if len(macroname)==0: - macroname=linput.safeinput('Enter new macro name: ') - if len(macroname) == 0: - print 'Invalid name' - - macroname=os.path.join(self.macrodir,macroname+'.hkm') - if os.path.exists(macroname): - overwrite=linput.safeinput('That name is in use, overwrite?',['n']) - if overwrite[0].lower()!='y': - print 'Cancelled save' - return 0 - txtfile=open(macroname,'w+') - self.currentmacro='\n'.join(self.currentmacro) - txtfile.write(self.currentmacro) - txtfile.close() - print 'Saved on '+macroname - self.currentmacro=[] - - def do_execmacro (self, args): - - '''EXECMACRO - Loads a macro and executes it over current curve / playlist - ----- - Syntax: execmacro macroname [playlist] [v] - - macroname.hkm should be present at [hooke]/macros directory - By default the macro will be executed over current curve - passing 'playlist' word as second argument executes macroname - over all curves - By default curve(s) will be processed silently, passing 'v' - as second/third argument will print each command that is - executed - - Note that macros applied to playlists should end by export - commands so the processed curves are not lost - ''' - verbose=0 - cycle=0 - curve=None - - if len(self.currentmacro) != 0: - print 'Warning!: you are calling a macro while recording other' - if len(args) == 0: - print 'You must provide a macro name' - return 0 - args=args.split() - - #print 'args ' + ' '.join(args) - - if len(args)>1: - if args[1] == 'playlist': - cycle=1 - print 'Remember! macros applied over playlists should include export orders' - if len(args)>2 and args[2] == 'v': - verbose=1 - else: - if args[1] == 'v': - verbose=1 - #print cycle - #print verbose - - macropath=os.path.join(self.macrodir,args[0]+'.hkm') - if not os.path.exists(macropath): - print 'Could not find a macro named '+macropath - return 0 - txtfile=open(macropath) - if cycle ==1: - #print self.current_list - for item in self.current_list: - self.current=item - self.do_plot(0) - - for command in txtfile: - - if verbose==1: - print 'Executing command '+command - testcmd=command.split() - w=0 - for word in testcmd: - if word=='__curve__': - testcmd[w]=os.path.splitext(os.path.basename(item.path))[0] - w=w+1 - self.onecmd(' '.join(testcmd)) - self.current.curve.close_all() - txtfile.seek(0) - else: - for command in txtfile: - testcmd=command.split() - w=0 - for word in testcmd: - if word=='__curve__': - w=w+1 - testcmd[w]=os.path.splitext(os.path.basename(self.current.path))[0]+'-'+string.lstrip(os.path.splitext(os.path.basename(self.current.path))[1],'.') - if verbose==1: - print 'Executing command '+' '.join(testcmd) - self.onecmd(' '.join(testcmd)) + + +# Define useful command subclasses + +class CommandStackCommand (Command): + """Subclass to avoid pushing control commands to the stack. + """ + def _set_state(self, state): + try: + self.plugin.set_state(state) + except ValueError, e: + self.plugin.log('raising error: %s' % e) + raise Failure('invalid state change: %s' % e.state_change) + + +class CaptureCommand (CommandStackCommand): + """Run a mock-engine and save the incoming commands. + + Notes + ----- + Due to limitations in the --script and --command option + implementations in ./bin/hooke, capture sessions will die at the + end of the script and command execution before entering + --persist's interactive session. + """ + def __init__(self, name, plugin): + super(CaptureCommand, self).__init__( + name=name, help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + # TODO: possibly merge code with CommandEngine.run() + + # Fake successful completion so UI continues sending commands. + outqueue.put(Success()) + + while True: + msg = inqueue.get() + if isinstance(msg, CloseEngine): + outqueue.put('CloseEngine') + inqueue.put(msg) # Put CloseEngine back for CommandEngine. + self._set_state('inactive') + return + assert isinstance(msg, CommandMessage), type(msg) + cmd = hooke.command_by_name[msg.command] + if isinstance(cmd, CommandStackCommand): + if isinstance(cmd, StopCaptureCommand): + outqueue = Queue() # Grab StopCaptureCommand's completion. + cmd.run(hooke, inqueue, outqueue, **msg.arguments) + if isinstance(cmd, StopCaptureCommand): + assert self.plugin.state == 'inactive', self.plugin.state + # Return the stolen completion as our own. + raise outqueue.get(block=False) + else: + self.plugin.log('appending %s' % msg) + self.plugin.command_stack.append(msg) + # Fake successful completion so UI continues sending commands. + outqueue.put(Success()) + + +# The plugin itself + +class CommandStackPlugin (Builtin): + def __init__(self): + super(CommandStackPlugin, self).__init__(name='command_stack') + self._commands = [ + StartCaptureCommand(self), StopCaptureCommand(self), + ReStartCaptureCommand(self), + PopCommand(self), GetCommand(self), GetStateCommand(self), + SaveCommand(self), LoadCommand(self), + ] + self.command_stack = CommandStack() + self.path = None + self.state = 'inactive' + # inactive <-> active. + self._valid_transitions = { + 'inactive': ['active'], + 'active': ['inactive'], + } + + def log(self, msg): + log = logging.getLogger('hooke') + log.debug('%s %s' % (self.name, msg)) + + def set_state(self, state): + state_change = '%s -> %s' % (self.state, state) + self.log('changing state: %s' % state_change) + if state not in self._valid_transitions[self.state]: + e = ValueError(state) + e.state_change = state_change + raise e + self.state = state + + +# Define commands + +class StartCaptureCommand (CaptureCommand): + """Clear any previous stack and run the mock-engine. + """ + def __init__(self, plugin): + super(StartCaptureCommand, self).__init__( + name='start command capture', plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + self._set_state('active') + self.plugin.command_stack = CommandStack() # clear command stack + super(StartCaptureCommand, self)._run(hooke, inqueue, outqueue, params) + + +class ReStartCaptureCommand (CaptureCommand): + """Run the mock-engine. + """ + def __init__(self, plugin): + super(ReStartCaptureCommand, self).__init__( + name='restart command capture', plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + self._set_state('active') + super(ReStartCaptureCommand, self)._run(hooke, inqueue, outqueue, params) + + +class StopCaptureCommand (CommandStackCommand): + """Stop the mock-engine. + """ + def __init__(self, plugin): + super(StopCaptureCommand, self).__init__( + name='stop command capture', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + self._set_state('inactive') + + +class PopCommand (CommandStackCommand): + """Pop the top command off the stack. + """ + def __init__(self, plugin): + super(PopCommand, self).__init__( + name='pop command from stack', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + outqueue.put(self.plugin.command_stack.pop()) + + +class GetCommand (CommandStackCommand): + """Return the command stack. + """ + def __init__(self, plugin): + super(GetCommand, self).__init__( + name='get command stack', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + outqueue.put(self.plugin.command_stack) + +class GetStateCommand (CommandStackCommand): + """Return the mock-engine state. + """ + def __init__(self, plugin): + super(GetStateCommand, self).__init__( + name='get command capture state', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + outqueue.put(self.plugin.state) + + +class SaveCommand (CommandStackCommand): + """Save the command stack. + """ + def __init__(self, plugin): + super(SaveCommand, self).__init__( + name='save command stack', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + pass + + +class LoadCommand (CommandStackCommand): + """Load the command stack. + """ + def __init__(self, plugin): + super(LoadCommand, self).__init__( + name='load command stack', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + pass diff --git a/test/command_stack.py b/test/command_stack.py new file mode 100644 index 0000000..48ef4d1 --- /dev/null +++ b/test/command_stack.py @@ -0,0 +1,161 @@ +# 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 +# . + +""" +>>> from hooke.hooke import Hooke, HookeRunner +>>> h = Hooke() +>>> r = HookeRunner() + +The command stack starts off empty. + +>>> h = r.run_lines(h, ['get_command_stack']) +[] +Success + + +And inactive, so you can't stop it. + +>>> h = r.run_lines(h, ['get_command_capture_state']) +inactive +Success + +>>> h = r.run_lines(h, ['stop_command_capture']) +Failure +invalid state change: inactive -> inactive + +Because :meth:`hooke.hooke.HookeRunner.run_lines` spawns and closes +its own engine subprocess, we need to run the whole capture session in +a single call. The command stack, on the other hand, will be +preserved between calls. + +You can't restart recording. + +>>> h = r.run_lines(h, ['start_command_capture', +... 'get_command_capture_state', +... 'start_command_capture', +... 'restart_command_capture']) # doctest: +REPORT_UDIFF +Success + +active +Success + +Failure +invalid state change: active -> active +Failure +invalid state change: active -> active + +But you can stop and restart. + +>>> h = r.run_lines(h, ['start_command_capture', +... 'stop_command_capture', +... 'restart_command_capture']) # doctest: +REPORT_UDIFF +Success + +Success + +Success + + +Lets add some commands to the stack. + +>>> h = r.run_lines(h, ['start_command_capture', +... 'load_playlist test/data/test', +... 'get_curve', +... 'stop_command_capture']) # doctest: +REPORT_UDIFF +Success + +Success + +Success + +Success + +>>> h = r.run_lines(h, ['get_command_stack']) # doctest: +NORMALIZE_WHITESPACE +[, + ] +Success + + +When capture is stopped, command execution is normal. + +>>> h = r.run_lines(h, ['restart_command_capture', +... 'curve_info', +... 'stop_command_capture', +... 'version', +... 'restart_command_capture', +... 'previous_curve', +... 'stop_command_capture'] +... ) # doctest: +ELLIPSIS, +REPORT_UDIFF +Success + +Success + +Success + +Hooke 1.0.0.alpha (Ninken) +... +Success + +Success + +Success + + +You can pop commands regardless of the recording state. + +>>> h = r.run_lines(h, ['pop_command_from_stack']) + +Success + +>>> h = r.run_lines(h, ['get_command_stack']) # doctest: +NORMALIZE_WHITESPACE +[, + , + ] +Success + + +>>> h = r.run_lines(h, ['restart_command_capture', +... 'pop_command_from_stack', +... 'get_command_stack', +... 'stop_command_capture'] +... ) # doctest: +NORMALIZE_WHITESPACE, +REPORT_UDIFF +Success + + +Success + +[, + ] +Success + +Success + + +If you start up again (using `start` not `restart`), the stack is cleared. + +>>> h = r.run_lines(h, ['start_command_capture', +... 'stop_command_capture']) +Success + +Success + +>>> h = r.run_lines(h, ['get_command_stack']) +[] +Success + +"""