Ran update_copyright.py.
[hooke.git] / hooke / command_stack.py
1 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """The ``command_stack`` module provides tools for managing and
20 executing stacks of :class:`~hooke.engine.CommandMessage`\s.
21
22 In experiment analysis, the goal is to construct a
23 :class:`~hooke.command_stack.CommandStack` that starts with your raw
24 experiment data and ends with your analyzed results.  These
25 :class:`~hooke.command_stack.CommandStack`\s are stored in your
26 :class:`~hooke.playlist.FilePlaylist`, so they are saved to disk with
27 the analysis results.  This means you will always have a record of
28 exactly how you processed the raw data to produce your analysis
29 results, which makes it easy to audit your approach or go back and
30 reanalyze older data.
31 """
32
33 import os
34 import os.path
35
36 import yaml
37
38 from .engine import CommandMessage
39
40
41 class CommandStack (list):
42     """Store a stack of commands.
43
44     Examples
45     --------
46     >>> c = CommandStack([CommandMessage('CommandA', {'param':'A'})])
47     >>> c.append(CommandMessage('CommandB', {'param':'B'}))
48     >>> c.append(CommandMessage('CommandA', {'param':'C'}))
49     >>> c.append(CommandMessage('CommandB', {'param':'D'}))
50
51     Implement a dummy :meth:`execute_command` for testing.
52
53     >>> def execute_cmd(hooke, command_message, stack=None):
54     ...     cm = command_message
55     ...     print 'EXECUTE', cm.command, cm.arguments
56     >>> c.execute_command = execute_cmd
57
58     >>> c.execute(hooke=None)  # doctest: +ELLIPSIS
59     EXECUTE CommandA {'param': 'A'}
60     EXECUTE CommandB {'param': 'B'}
61     EXECUTE CommandA {'param': 'C'}
62     EXECUTE CommandB {'param': 'D'}
63
64     :meth:`filter` allows you to select which commands get executed.
65     If, for example, you are applying a set of commands to the current
66     :class:`~hooke.curve.Curve`, you may only want to execute
67     instances of :class:`~hooke.plugin.curve.CurveCommand`.  Here we
68     only execute commands named `CommandB`.
69     
70     >>> def filter(hooke, command_message):
71     ...     return command_message.command == 'CommandB'
72
73     Apply the stack to the current curve.
74
75     >>> c.execute(hooke=None, filter=filter)  # doctest: +ELLIPSIS
76     EXECUTE CommandB {'param': 'B'}
77     EXECUTE CommandB {'param': 'D'}
78
79     Execute a new command and add it to the stack.
80
81     >>> cm = CommandMessage('CommandC', {'param':'E'})
82     >>> c.execute_command(hooke=None, command_message=cm)
83     EXECUTE CommandC {'param': 'E'}
84     >>> c.append(cm)
85     >>> print [repr(cm) for cm in c]  # doctest: +NORMALIZE_WHITESPACE
86     ['<CommandMessage CommandA {param: A}>',
87      '<CommandMessage CommandB {param: B}>',
88      '<CommandMessage CommandA {param: C}>',
89      '<CommandMessage CommandB {param: D}>',
90      '<CommandMessage CommandC {param: E}>']
91
92     The data-type is also pickleable, which ensures we can move it
93     between processes with :class:`multiprocessing.Queue`\s and easily
94     save it to disk.  We must remove the unpickleable dummy executor
95     before testing though.
96
97     >>> c.execute_command  # doctest: +ELLIPSIS
98     <function execute_cmd at 0x...>
99     >>> del(c.__dict__['execute_command'])
100     >>> c.execute_command  # doctest: +ELLIPSIS
101     <bound method CommandStack.execute_command of ...>
102     
103     Lets also attach a child command message to demonstrate recursive
104     serialization (we can't append `c` itself because of
105     `Python issue 1062277`_).
106
107     .. _Python issue 1062277: http://bugs.python.org/issue1062277
108
109     >>> import copy
110     >>> c.append(CommandMessage('CommandD', {'param': copy.deepcopy(c)}))
111
112     Run the pickle (and YAML) tests.
113
114     >>> import pickle
115     >>> s = pickle.dumps(c)
116     >>> z = pickle.loads(s)
117     >>> print '\\n'.join([repr(cm) for cm in c]
118     ...     )  # doctest: +NORMALIZE_WHITESPACE,
119     <CommandMessage CommandA {param: A}>
120     <CommandMessage CommandB {param: B}>
121     <CommandMessage CommandA {param: C}>
122     <CommandMessage CommandB {param: D}>
123     <CommandMessage CommandC {param: E}>
124     <CommandMessage CommandD {param:
125       [<CommandMessage CommandA {param: A}>,
126        <CommandMessage CommandB {param: B}>,
127        <CommandMessage CommandA {param: C}>,
128        <CommandMessage CommandB {param: D}>,
129        <CommandMessage CommandC {param: E}>]}>
130     >>> import yaml
131     >>> print yaml.dump(c)  # doctest: +REPORT_UDIFF
132     !!python/object/new:hooke.command_stack.CommandStack
133     listitems:
134     - !!python/object:hooke.engine.CommandMessage
135       arguments: {param: A}
136       command: CommandA
137       explicit_user_call: true
138     - !!python/object:hooke.engine.CommandMessage
139       arguments: {param: B}
140       command: CommandB
141       explicit_user_call: true
142     - !!python/object:hooke.engine.CommandMessage
143       arguments: {param: C}
144       command: CommandA
145       explicit_user_call: true
146     - !!python/object:hooke.engine.CommandMessage
147       arguments: {param: D}
148       command: CommandB
149       explicit_user_call: true
150     - !!python/object:hooke.engine.CommandMessage
151       arguments: {param: E}
152       command: CommandC
153       explicit_user_call: true
154     - !!python/object:hooke.engine.CommandMessage
155       arguments:
156         param: !!python/object/new:hooke.command_stack.CommandStack
157           listitems:
158           - !!python/object:hooke.engine.CommandMessage
159             arguments: {param: A}
160             command: CommandA
161             explicit_user_call: true
162           - !!python/object:hooke.engine.CommandMessage
163             arguments: {param: B}
164             command: CommandB
165             explicit_user_call: true
166           - !!python/object:hooke.engine.CommandMessage
167             arguments: {param: C}
168             command: CommandA
169             explicit_user_call: true
170           - !!python/object:hooke.engine.CommandMessage
171             arguments: {param: D}
172             command: CommandB
173             explicit_user_call: true
174           - !!python/object:hooke.engine.CommandMessage
175             arguments: {param: E}
176             command: CommandC
177             explicit_user_call: true
178       command: CommandD
179       explicit_user_call: true
180     <BLANKLINE>
181
182     There is also a convenience function for clearing the stack.
183
184     >>> c.clear()
185     >>> print [repr(cm) for cm in c]
186     []
187
188     YAMLize a curve argument.
189
190     >>> from .curve import Curve
191     >>> c.append(CommandMessage('curve info', {'curve': Curve(path=None)}))
192     >>> print yaml.dump(c)  # doctest: +REPORT_UDIFF
193     !!python/object/new:hooke.command_stack.CommandStack
194     listitems:
195     - !!python/object:hooke.engine.CommandMessage
196       arguments:
197         curve: !!python/object:hooke.curve.Curve {}
198       command: curve info
199       explicit_user_call: true
200     <BLANKLINE>
201     """
202     def execute(self, hooke, filter=None, stack=False):
203         """Execute a stack of commands.
204
205         See Also
206         --------
207         execute_command, filter
208         """
209         if filter == None:
210             filter = self.filter
211         for command_message in self:
212             if filter(hooke, command_message) == True:
213                 self.execute_command(
214                     hooke=hooke, command_message=command_message, stack=stack)
215
216     def filter(self, hooke, command_message):
217         """Return `True` to execute `command_message`, `False` otherwise.
218
219         The default implementation always returns `True`.
220         """
221         return True
222
223     def execute_command(self, hooke, command_message, stack=False):
224         arguments = dict(command_message.arguments)
225         arguments['stack'] = stack
226         hooke.run_command(command=command_message.command,
227                           arguments=arguments)
228
229     def clear(self):
230         while len(self) > 0:
231             self.pop()
232
233
234 class FileCommandStack (CommandStack):
235     """A file-backed :class:`CommandStack`.
236     """
237     version = '0.1'
238
239     def __init__(self, *args, **kwargs):
240         super(FileCommandStack, self).__init__(*args, **kwargs)
241         self.name = self.path = None
242
243     def __setstate__(self, state):
244         self.name = self.path = None
245         for key,value in state.items():
246             setattr(self, key, value)
247         self.set_path(state.get('path', None))
248
249     def set_path(self, path):
250         """Set the path (and possibly the name) of the command  stack.
251
252         Examples
253         --------
254         >>> c = FileCommandStack([CommandMessage('CommandA', {'param':'A'})])
255
256         :attr:`name` is set only if it starts out equal to `None`.
257         >>> c.name == None
258         True
259         >>> c.set_path(os.path.join('path', 'to', 'my', 'command', 'stack'))
260         >>> c.path
261         'path/to/my/command/stack'
262         >>> c.name
263         'stack'
264         >>> c.set_path(os.path.join('another', 'path'))
265         >>> c.path
266         'another/path'
267         >>> c.name
268         'stack'
269         """
270         if path != None:
271             self.path = path
272             if self.name == None:
273                 self.name = os.path.basename(path)
274
275     def save(self, path=None, makedirs=True):
276         """Saves the command stack to `path`.
277         """
278         self.set_path(path)
279         dirname = os.path.dirname(self.path) or '.'
280         if makedirs == True and not os.path.isdir(dirname):
281             os.makedirs(dirname)
282         with open(self.path, 'w') as f:
283             f.write(self.flatten())
284
285     def load(self, path=None):
286         """Load a command stack from `path`.
287         """
288         self.set_path(path)
289         with open(self.path, 'r') as f:
290             text = f.read()
291         self.from_string(text)
292
293     def flatten(self):
294         """Create a string representation of the command stack.
295
296         A playlist is a YAML document with the following syntax::
297
298             - arguments: {param: A}
299               command: CommandA
300             - arguments: {param: B, ...}
301               command: CommandB
302             ...
303
304         Examples
305         --------
306         >>> c = FileCommandStack([CommandMessage('CommandA', {'param':'A'})])
307         >>> c.append(CommandMessage('CommandB', {'param':'B'}))
308         >>> c.append(CommandMessage('CommandA', {'param':'C'}))
309         >>> c.append(CommandMessage('CommandB', {'param':'D'}))
310         >>> print c.flatten()
311         - arguments: {param: A}
312           command: CommandA
313         - arguments: {param: B}
314           command: CommandB
315         - arguments: {param: C}
316           command: CommandA
317         - arguments: {param: D}
318           command: CommandB
319         <BLANKLINE>
320         """
321         return yaml.dump([{'command':cm.command,'arguments':cm.arguments}
322                           for cm in self])
323
324     def from_string(self, string):
325         """Load a playlist from a string.
326
327         .. warning:: This is *not safe* with untrusted input.
328
329         Examples
330         --------
331
332         >>> string = '''- arguments: {param: A}
333         ...   command: CommandA
334         ... - arguments: {param: B}
335         ...   command: CommandB
336         ... - arguments: {param: C}
337         ...   command: CommandA
338         ... - arguments: {param: D}
339         ...   command: CommandB
340         ... '''
341         >>> c = FileCommandStack()
342         >>> c.from_string(string)
343         >>> print [repr(cm) for cm in c]  # doctest: +NORMALIZE_WHITESPACE
344         ['<CommandMessage CommandA {param: A}>',
345          '<CommandMessage CommandB {param: B}>',
346          '<CommandMessage CommandA {param: C}>',
347          '<CommandMessage CommandB {param: D}>']
348         """
349         for x in yaml.load(string):
350             self.append(CommandMessage(command=x['command'],
351                                        arguments=x['arguments']))