Fleshed out hooke.plugin.command_stack except for save/load
authorW. Trevor King <wking@drexel.edu>
Fri, 13 Aug 2010 16:34:25 +0000 (12:34 -0400)
committerW. Trevor King <wking@drexel.edu>
Fri, 13 Aug 2010 16:34:25 +0000 (12:34 -0400)
hooke/plugin/__init__.py
hooke/plugin/command_stack.py
test/command_stack.py [new file with mode: 0644]

index 63f17a3..95c56e2 100644 (file)
@@ -55,6 +55,7 @@ default.  TODO: autodiscovery
 """
 
 BUILTIN_MODULES = [
+    'command_stack',
     'config',
     'curve',
     'debug',
index 5552c7e..abb03b7 100644 (file)
@@ -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 (file)
index 0000000..48ef4d1
--- /dev/null
@@ -0,0 +1,161 @@
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# 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
+# <http://www.gnu.org/licenses/>.
+
+"""
+>>> 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
+<BLANKLINE>
+
+And inactive, so you can't stop it.
+
+>>> h = r.run_lines(h, ['get_command_capture_state'])
+inactive
+Success
+<BLANKLINE>
+>>> 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
+<BLANKLINE>
+active
+Success
+<BLANKLINE>
+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
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+
+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
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+>>> h = r.run_lines(h, ['get_command_stack'])  # doctest: +NORMALIZE_WHITESPACE
+[<CommandMessage load playlist {input: test/data/test}>,
+ <CommandMessage get curve>]
+Success
+<BLANKLINE>
+
+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
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Hooke 1.0.0.alpha (Ninken)
+...
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+
+You can pop commands regardless of the recording state.
+
+>>> h = r.run_lines(h, ['pop_command_from_stack'])
+<CommandMessage previous curve>
+Success
+<BLANKLINE>
+>>> h = r.run_lines(h, ['get_command_stack'])  # doctest: +NORMALIZE_WHITESPACE
+[<CommandMessage load playlist {input: test/data/test}>,
+ <CommandMessage get curve>,
+ <CommandMessage curve info>]
+Success
+<BLANKLINE>
+
+>>> h = r.run_lines(h, ['restart_command_capture',
+...                     'pop_command_from_stack',
+...                     'get_command_stack',
+...                     'stop_command_capture']
+...     )  # doctest: +NORMALIZE_WHITESPACE, +REPORT_UDIFF
+Success
+<BLANKLINE>
+<CommandMessage curve info>
+Success
+<BLANKLINE>
+[<CommandMessage load playlist {input: test/data/test}>,
+ <CommandMessage get curve>]
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+
+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
+<BLANKLINE>
+Success
+<BLANKLINE>
+>>> h = r.run_lines(h, ['get_command_stack'])
+[]
+Success
+<BLANKLINE>
+"""