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