41873825c93e8d37ea0c82f9a17d3422886c8e1c
[hooke.git] / hooke / plugin / command_stack.py
1 # Copyright (C) 2008-2012 Alberto Gomez-Casado <a.gomezcasado@tnw.utwente.nl>
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 under the
8 # terms of the GNU Lesser General Public License as published by the Free
9 # Software Foundation, either version 3 of the License, or (at your option) any
10 # later version.
11 #
12 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
15 # details.
16 #
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
19
20 """The ``command_stack`` module provides :class:`CommandStackPlugin`
21 and several associated :class:`~hooke.command.Command`\s exposing
22 :mod`hooke.command_stack`'s functionality.
23 """
24
25 import logging
26 import os.path
27 from Queue import Queue
28
29 from ..command import Command, Argument, Success, Failure
30 from ..command_stack import FileCommandStack
31 from ..config import Setting
32 from ..engine import CloseEngine, CommandMessage
33 from . import Builtin
34
35
36 # Define useful command subclasses
37
38 class CommandStackCommand (Command):
39     """Subclass to avoid pushing control commands to the stack.
40     """
41     def __init__(self, *args, **kwargs):
42         super(CommandStackCommand, self).__init__(*args, **kwargs)
43         stack = [a for a in self.arguments if a.name == 'stack'][0]
44         stack.default = False
45
46     def _set_state(self, state):
47         try:
48             self.plugin.set_state(state)
49         except ValueError, e:
50             self.plugin.log('raising error: %s' % e)
51             raise Failure('invalid state change: %s' % e.state_change)
52
53
54 class CaptureCommand (CommandStackCommand):
55     """Run a mock-engine and save the incoming commands.
56
57     Notes
58     -----
59     Due to limitations in the --script and --command option
60     implementations in ./bin/hooke, capture sessions will die at the
61     end of the script and command execution before entering
62     --persist's interactive session.
63     """
64     def __init__(self, name, plugin):
65         super(CaptureCommand, self).__init__(
66             name=name, help=self.__doc__, plugin=plugin)
67
68     def _run(self, hooke, inqueue, outqueue, params):
69         # TODO: possibly merge code with CommandEngine.run()
70
71         # Fake successful completion so UI continues sending commands.
72         outqueue.put(Success())
73
74         while True:
75             msg = inqueue.get()
76             if isinstance(msg, CloseEngine):
77                 outqueue.put('CloseEngine')
78                 inqueue.put(msg)  # Put CloseEngine back for CommandEngine.
79                 self._set_state('inactive')
80                 return
81             assert isinstance(msg, CommandMessage), type(msg)
82             cmd = hooke.command_by_name[msg.command]
83             if (msg.explicit_user_call == False
84                 or 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                 os.path.expanduser(self.plugin.config['path']),
239                 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                 os.path.expanduser(self.plugin.config['path']),
269                 params['input'])
270         return params
271
272
273 class ExecuteCommand (Command):
274     """Execute a :class:`~hooke.command_stack.CommandStack`.
275     """
276     def __init__(self, plugin):
277         super(ExecuteCommand, self).__init__(
278             name='execute command stack',
279             arguments=[
280                 Argument(name='commands', type='command stack',
281                          help="""
282 Command stack to apply to each curve.  Defaults to the plugin's
283 current stack.
284 """.strip()),
285                 ],
286             help=self.__doc__, plugin=plugin)
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