Flesh out command stack execution.
[hooke.git] / hooke / plugin / command_stack.py
1 # Copyright (C) 2008-2010 Alberto Gomez-Casado
2 #                         Massimo Sandal <devicerandom@gmail.com>
3 #                         W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of Hooke.
6 #
7 # Hooke is free software: you can redistribute it and/or modify it
8 # under the terms of the GNU Lesser General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
11 #
12 # Hooke is distributed in the hope that it will be useful, but WITHOUT
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
15 # Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with Hooke.  If not, see
19 # <http://www.gnu.org/licenses/>.
20
21 """The ``command_stack`` module provides :class:`CommandStackPlugin`
22 and several associated :class:`~hooke.command.Command`\s exposing
23 :mod`hooke.command_stack`'s functionality.
24 """
25
26 import logging
27 import os.path
28 from Queue import Queue
29
30 from ..command import Command, Argument, Success, Failure
31 from ..command_stack import FileCommandStack
32 from ..config import Setting
33 from ..engine import CloseEngine, CommandMessage
34 from . import Builtin
35
36
37 # Define useful command subclasses
38
39 class CommandStackCommand (Command):
40     """Subclass to avoid pushing control commands to the stack.
41     """
42     def __init__(self, *args, **kwargs):
43         super(CommandStackCommand, self).__init__(*args, **kwargs)
44         stack = [a for a in self.arguments if a.name == 'stack'][0]
45         stack.default = False
46
47     def _set_state(self, state):
48         try:
49             self.plugin.set_state(state)
50         except ValueError, e:
51             self.plugin.log('raising error: %s' % e)
52             raise Failure('invalid state change: %s' % e.state_change)
53
54
55 class CaptureCommand (CommandStackCommand):
56     """Run a mock-engine and save the incoming commands.
57
58     Notes
59     -----
60     Due to limitations in the --script and --command option
61     implementations in ./bin/hooke, capture sessions will die at the
62     end of the script and command execution before entering
63     --persist's interactive session.
64     """
65     def __init__(self, name, plugin):
66         super(CaptureCommand, self).__init__(
67             name=name, help=self.__doc__, plugin=plugin)
68
69     def _run(self, hooke, inqueue, outqueue, params):
70         # TODO: possibly merge code with CommandEngine.run()
71
72         # Fake successful completion so UI continues sending commands.
73         outqueue.put(Success())
74
75         while True:
76             msg = inqueue.get()
77             if isinstance(msg, CloseEngine):
78                 outqueue.put('CloseEngine')
79                 inqueue.put(msg)  # Put CloseEngine back for CommandEngine.
80                 self._set_state('inactive')
81                 return
82             assert isinstance(msg, CommandMessage), type(msg)
83             cmd = hooke.command_by_name[msg.command]
84             if isinstance(cmd, CommandStackCommand):
85                 if isinstance(cmd, StopCaptureCommand):
86                     outqueue = Queue()  # Grab StopCaptureCommand's completion.
87                 cmd.run(hooke, inqueue, outqueue, **msg.arguments)
88                 if isinstance(cmd, StopCaptureCommand):
89                     assert self.plugin.state == 'inactive', self.plugin.state
90                     # Return the stolen completion as our own.
91                     raise outqueue.get(block=False)
92             else:
93                 self.plugin.log('appending %s' % msg)
94                 self.plugin.command_stack.append(msg)
95                 # Fake successful completion so UI continues sending commands.
96                 outqueue.put(Success())
97
98
99 # The plugin itself
100
101 class CommandStackPlugin (Builtin):
102     """Commands for managing a command stack (similar to macros).
103     """
104     def __init__(self):
105         super(CommandStackPlugin, self).__init__(name='command_stack')
106         self._commands = [
107             StartCaptureCommand(self), StopCaptureCommand(self),
108             ReStartCaptureCommand(self),
109             PopCommand(self), GetCommand(self), GetStateCommand(self),
110             SaveCommand(self), LoadCommand(self), ExecuteCommand(self),
111             ]
112         self._settings = [
113             Setting(section=self.setting_section, help=self.__doc__),
114             Setting(section=self.setting_section, option='path',
115                     value=os.path.join('resources', 'command_stack'),
116                     help='Directory containing command stack files.'), # TODO: allow colon separated list, like $PATH.
117             ]
118         self.command_stack = FileCommandStack()
119         self.state = 'inactive'
120         # inactive <-> active.
121         self._valid_transitions = {
122             'inactive': ['active'],
123             'active': ['inactive'],
124             }
125
126     def default_settings(self):
127         return self._settings
128
129     def log(self, msg):
130         log = logging.getLogger('hooke')
131         log.debug('%s %s' % (self.name, msg))
132
133     def set_state(self, state):
134         state_change = '%s -> %s' % (self.state, state)
135         self.log('changing state: %s' % state_change)
136         if state not in self._valid_transitions[self.state]:
137             e = ValueError(state)
138             e.state_change = state_change
139             raise e
140         self.state = state
141
142
143 # Define commands
144
145 class StartCaptureCommand (CaptureCommand):
146     """Clear any previous stack and run the mock-engine.
147     """
148     def __init__(self, plugin):
149         super(StartCaptureCommand, self).__init__(
150             name='start command capture', plugin=plugin)
151
152     def _run(self, hooke, inqueue, outqueue, params):
153         self._set_state('active')
154         self.plugin.command_stack = FileCommandStack()  # clear command stack
155         super(StartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
156
157
158 class ReStartCaptureCommand (CaptureCommand):
159     """Run the mock-engine.
160     """
161     def __init__(self, plugin):
162         super(ReStartCaptureCommand, self).__init__(
163             name='restart command capture', plugin=plugin)
164
165     def _run(self, hooke, inqueue, outqueue, params):
166         self._set_state('active')
167         super(ReStartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
168
169
170 class StopCaptureCommand (CommandStackCommand):
171     """Stop the mock-engine.
172     """
173     def __init__(self, plugin):
174         super(StopCaptureCommand, self).__init__(
175             name='stop command capture', help=self.__doc__, plugin=plugin)
176
177     def _run(self, hooke, inqueue, outqueue, params):
178         self._set_state('inactive')
179
180
181 class PopCommand (CommandStackCommand):
182     """Pop the top command off the stack.
183     """
184     def __init__(self, plugin):
185         super(PopCommand, self).__init__(
186             name='pop command from stack', help=self.__doc__, plugin=plugin)
187
188     def _run(self, hooke, inqueue, outqueue, params):
189         outqueue.put(self.plugin.command_stack.pop())
190
191
192 class GetCommand (CommandStackCommand):
193     """Return the command stack.
194     """
195     def __init__(self, plugin):
196         super(GetCommand, self).__init__(
197             name='get command stack', help=self.__doc__, plugin=plugin)
198
199     def _run(self, hooke, inqueue, outqueue, params):
200         outqueue.put(self.plugin.command_stack)
201
202 class GetStateCommand (CommandStackCommand):
203     """Return the mock-engine state.
204     """
205     def __init__(self, plugin):
206         super(GetStateCommand, self).__init__(
207             name='get command capture state', help=self.__doc__, plugin=plugin)
208
209     def _run(self, hooke, inqueue, outqueue, params):
210         outqueue.put(self.plugin.state)
211
212
213 class SaveCommand (CommandStackCommand):
214     """Save the command stack.
215     """
216     def __init__(self, plugin):
217         super(SaveCommand, self).__init__(
218             name='save command stack',
219             arguments=[
220                 Argument(name='output', type='file',
221                          help="""
222 File name for the output command stack.  Defaults to overwriting the
223 input command stack.  If the command stack does not have an input file
224 (e.g. it is the default) then the file name defaults to `default`.
225 """.strip()),
226                 ],
227             help=self.__doc__, plugin=plugin)
228
229     def _run(self, hooke, inqueue, outqueue, params):
230         params = self.__setup_params(hooke, params)
231         self.plugin.command_stack.save(params['output'])
232
233     def __setup_params(self, hooke, params):
234         if params['output'] == None and self.plugin.command_stack.path == None:
235             params['output'] = 'default'
236         if params['output'] != None:
237             params['output'] = os.path.join(
238                 self.plugin.config['path'], params['output'])
239         return params
240
241 class LoadCommand (CommandStackCommand):
242     """Load the command stack.
243
244     .. warning:: This is *not safe* with untrusted input.
245     """
246     def __init__(self, plugin):
247         super(LoadCommand, self).__init__(
248             name='load command stack',
249             arguments=[
250                 Argument(name='input', type='file',
251                          help="""
252 File name for the input command stack.
253 """.strip()),
254                 ],
255             help=self.__doc__, plugin=plugin)
256
257     def _run(self, hooke, inqueue, outqueue, params):
258         params = self.__setup_params(hooke, params)
259         self.plugin.command_stack.clear()
260         self.plugin.command_stack.load(params['input'])
261
262     def __setup_params(self, hooke, params):
263         if params['input'] == None and self.plugin.command_stack.path == None:
264             params['input'] = 'default'
265         if params['input'] != None:
266             params['input'] = os.path.join(
267                 self.plugin.config['path'], params['input'])
268         return params
269
270
271 class ExecuteCommand (Command):
272     """Execute a :class:`~hooke.command_stack.CommandStack`.
273     """
274     def __init__(self, plugin):
275         super(ExecuteCommand, self).__init__(
276             name='execute command stack',
277             arguments=[
278                 Argument(name='commands', type='command stack',
279                          help="""
280 Command stack to apply to each curve.  Defaults to the plugin's
281 current stack.
282 """.strip()),
283                 ],
284             help=self.__doc__, plugin=plugin)
285         stack = [a for a in self.arguments if a.name == 'stack'][0]
286         stack.default = False
287
288     def _run(self, hooke, inqueue, outqueue, params):
289         params = self.__setup_params(hooke=hooke, params=params)
290         if len(params['commands']) == 0:
291             return
292         params['commands'].execute(hooke=hooke, stack=params['stack'])
293
294     def __setup_params(self, hooke, params):
295         if params['commands'] == None:
296             params['commands'] = self.plugin.command_stack
297         return params