Oops, the CommandMessage.explicit_user_call, not Command.explicit_user_call.
[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 (msg.explicit_user_call == False
85                 or isinstance(cmd, CommandStackCommand)):
86                 if isinstance(cmd, StopCaptureCommand):
87                     outqueue = Queue()  # Grab StopCaptureCommand's completion.
88                 cmd.run(hooke, inqueue, outqueue, **msg.arguments)
89                 if isinstance(cmd, StopCaptureCommand):
90                     assert self.plugin.state == 'inactive', self.plugin.state
91                     # Return the stolen completion as our own.
92                     raise outqueue.get(block=False)
93             else:
94                 self.plugin.log('appending %s' % msg)
95                 self.plugin.command_stack.append(msg)
96                 # Fake successful completion so UI continues sending commands.
97                 outqueue.put(Success())
98
99
100 # The plugin itself
101
102 class CommandStackPlugin (Builtin):
103     """Commands for managing a command stack (similar to macros).
104     """
105     def __init__(self):
106         super(CommandStackPlugin, self).__init__(name='command_stack')
107         self._commands = [
108             StartCaptureCommand(self), StopCaptureCommand(self),
109             ReStartCaptureCommand(self),
110             PopCommand(self), GetCommand(self), GetStateCommand(self),
111             SaveCommand(self), LoadCommand(self), ExecuteCommand(self),
112             ]
113         self._settings = [
114             Setting(section=self.setting_section, help=self.__doc__),
115             Setting(section=self.setting_section, option='path',
116                     value=os.path.join('resources', 'command_stack'),
117                     help='Directory containing command stack files.'), # TODO: allow colon separated list, like $PATH.
118             ]
119         self.command_stack = FileCommandStack()
120         self.state = 'inactive'
121         # inactive <-> active.
122         self._valid_transitions = {
123             'inactive': ['active'],
124             'active': ['inactive'],
125             }
126
127     def default_settings(self):
128         return self._settings
129
130     def log(self, msg):
131         log = logging.getLogger('hooke')
132         log.debug('%s %s' % (self.name, msg))
133
134     def set_state(self, state):
135         state_change = '%s -> %s' % (self.state, state)
136         self.log('changing state: %s' % state_change)
137         if state not in self._valid_transitions[self.state]:
138             e = ValueError(state)
139             e.state_change = state_change
140             raise e
141         self.state = state
142
143
144 # Define commands
145
146 class StartCaptureCommand (CaptureCommand):
147     """Clear any previous stack and run the mock-engine.
148     """
149     def __init__(self, plugin):
150         super(StartCaptureCommand, self).__init__(
151             name='start command capture', plugin=plugin)
152
153     def _run(self, hooke, inqueue, outqueue, params):
154         self._set_state('active')
155         self.plugin.command_stack = FileCommandStack()  # clear command stack
156         super(StartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
157
158
159 class ReStartCaptureCommand (CaptureCommand):
160     """Run the mock-engine.
161     """
162     def __init__(self, plugin):
163         super(ReStartCaptureCommand, self).__init__(
164             name='restart command capture', plugin=plugin)
165
166     def _run(self, hooke, inqueue, outqueue, params):
167         self._set_state('active')
168         super(ReStartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
169
170
171 class StopCaptureCommand (CommandStackCommand):
172     """Stop the mock-engine.
173     """
174     def __init__(self, plugin):
175         super(StopCaptureCommand, self).__init__(
176             name='stop command capture', help=self.__doc__, plugin=plugin)
177
178     def _run(self, hooke, inqueue, outqueue, params):
179         self._set_state('inactive')
180
181
182 class PopCommand (CommandStackCommand):
183     """Pop the top command off the stack.
184     """
185     def __init__(self, plugin):
186         super(PopCommand, self).__init__(
187             name='pop command from stack', help=self.__doc__, plugin=plugin)
188
189     def _run(self, hooke, inqueue, outqueue, params):
190         outqueue.put(self.plugin.command_stack.pop())
191
192
193 class GetCommand (CommandStackCommand):
194     """Return the command stack.
195     """
196     def __init__(self, plugin):
197         super(GetCommand, self).__init__(
198             name='get command stack', help=self.__doc__, plugin=plugin)
199
200     def _run(self, hooke, inqueue, outqueue, params):
201         outqueue.put(self.plugin.command_stack)
202
203 class GetStateCommand (CommandStackCommand):
204     """Return the mock-engine state.
205     """
206     def __init__(self, plugin):
207         super(GetStateCommand, self).__init__(
208             name='get command capture state', help=self.__doc__, plugin=plugin)
209
210     def _run(self, hooke, inqueue, outqueue, params):
211         outqueue.put(self.plugin.state)
212
213
214 class SaveCommand (CommandStackCommand):
215     """Save the command stack.
216     """
217     def __init__(self, plugin):
218         super(SaveCommand, self).__init__(
219             name='save command stack',
220             arguments=[
221                 Argument(name='output', type='file',
222                          help="""
223 File name for the output command stack.  Defaults to overwriting the
224 input command stack.  If the command stack does not have an input file
225 (e.g. it is the default) then the file name defaults to `default`.
226 """.strip()),
227                 ],
228             help=self.__doc__, plugin=plugin)
229
230     def _run(self, hooke, inqueue, outqueue, params):
231         params = self.__setup_params(hooke, params)
232         self.plugin.command_stack.save(params['output'])
233
234     def __setup_params(self, hooke, params):
235         if params['output'] == None and self.plugin.command_stack.path == None:
236             params['output'] = 'default'
237         if params['output'] != None:
238             params['output'] = os.path.join(
239                 self.plugin.config['path'], params['output'])
240         return params
241
242 class LoadCommand (CommandStackCommand):
243     """Load the command stack.
244
245     .. warning:: This is *not safe* with untrusted input.
246     """
247     def __init__(self, plugin):
248         super(LoadCommand, self).__init__(
249             name='load command stack',
250             arguments=[
251                 Argument(name='input', type='file',
252                          help="""
253 File name for the input command stack.
254 """.strip()),
255                 ],
256             help=self.__doc__, plugin=plugin)
257
258     def _run(self, hooke, inqueue, outqueue, params):
259         params = self.__setup_params(hooke, params)
260         self.plugin.command_stack.clear()
261         self.plugin.command_stack.load(params['input'])
262
263     def __setup_params(self, hooke, params):
264         if params['input'] == None and self.plugin.command_stack.path == None:
265             params['input'] = 'default'
266         if params['input'] != None:
267             params['input'] = os.path.join(
268                 self.plugin.config['path'], params['input'])
269         return params
270
271
272 class ExecuteCommand (Command):
273     """Execute a :class:`~hooke.command_stack.CommandStack`.
274     """
275     def __init__(self, plugin):
276         super(ExecuteCommand, self).__init__(
277             name='execute command stack',
278             arguments=[
279                 Argument(name='commands', type='command stack',
280                          help="""
281 Command stack to apply to each curve.  Defaults to the plugin's
282 current stack.
283 """.strip()),
284                 ],
285             help=self.__doc__, plugin=plugin)
286
287     def _run(self, hooke, inqueue, outqueue, params):
288         params = self.__setup_params(hooke=hooke, params=params)
289         if len(params['commands']) == 0:
290             return
291         params['commands'].execute(hooke=hooke, stack=params['stack'])
292
293     def __setup_params(self, hooke, params):
294         if params['commands'] == None:
295             params['commands'] = self.plugin.command_stack
296         return params