1 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
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
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
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/>.
18 """The ``command_stack`` module provides tools for managing and
19 executing stacks of :class:`~hooke.engine.CommandMessage`\s.
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
37 from .engine import CommandMessage
40 class CommandStack (list):
41 """Store a stack of commands.
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'}))
50 Implement a dummy :meth:`execute_command` for testing.
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
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'}
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`.
69 >>> def filter(hooke, command_message):
70 ... return command_message.command == 'CommandB'
72 Apply the stack to the current curve.
74 >>> c.execute(hooke=None, filter=filter) # doctest: +ELLIPSIS
75 EXECUTE CommandB {'param': 'B'}
76 EXECUTE CommandB {'param': 'D'}
78 Execute a new command and add it to the stack.
80 >>> cm = CommandMessage('CommandC', {'param':'E'})
81 >>> c.execute_command(hooke=None, command_message=cm)
82 EXECUTE CommandC {'param': 'E'}
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}>']
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.
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 ...>
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`_).
106 .. _Python issue 1062277: http://bugs.python.org/issue1062277
109 >>> c.append(CommandMessage('CommandD', {'param': copy.deepcopy(c)}))
111 Run the pickle (and YAML) tests.
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}>]}>
130 >>> print yaml.dump(c) # doctest: +REPORT_UDIFF
131 !!python/object/new:hooke.command_stack.CommandStack
133 - !!python/object:hooke.engine.CommandMessage
134 arguments: {param: A}
136 explicit_user_call: true
137 - !!python/object:hooke.engine.CommandMessage
138 arguments: {param: B}
140 explicit_user_call: true
141 - !!python/object:hooke.engine.CommandMessage
142 arguments: {param: C}
144 explicit_user_call: true
145 - !!python/object:hooke.engine.CommandMessage
146 arguments: {param: D}
148 explicit_user_call: true
149 - !!python/object:hooke.engine.CommandMessage
150 arguments: {param: E}
152 explicit_user_call: true
153 - !!python/object:hooke.engine.CommandMessage
155 param: !!python/object/new:hooke.command_stack.CommandStack
157 - !!python/object:hooke.engine.CommandMessage
158 arguments: {param: A}
160 explicit_user_call: true
161 - !!python/object:hooke.engine.CommandMessage
162 arguments: {param: B}
164 explicit_user_call: true
165 - !!python/object:hooke.engine.CommandMessage
166 arguments: {param: C}
168 explicit_user_call: true
169 - !!python/object:hooke.engine.CommandMessage
170 arguments: {param: D}
172 explicit_user_call: true
173 - !!python/object:hooke.engine.CommandMessage
174 arguments: {param: E}
176 explicit_user_call: true
178 explicit_user_call: true
181 There is also a convenience function for clearing the stack.
184 >>> print [repr(cm) for cm in c]
187 YAMLize a curve argument.
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
194 - !!python/object:hooke.engine.CommandMessage
196 curve: !!python/object:hooke.curve.Curve {}
198 explicit_user_call: true
201 def execute(self, hooke, filter=None, stack=False):
202 """Execute a stack of commands.
206 execute_command, 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)
215 def filter(self, hooke, command_message):
216 """Return `True` to execute `command_message`, `False` otherwise.
218 The default implementation always returns `True`.
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,
233 class FileCommandStack (CommandStack):
234 """A file-backed :class:`CommandStack`.
238 def __init__(self, *args, **kwargs):
239 super(FileCommandStack, self).__init__(*args, **kwargs)
240 self.name = self.path = None
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))
248 def set_path(self, path):
249 """Set the path (and possibly the name) of the command stack.
253 >>> c = FileCommandStack([CommandMessage('CommandA', {'param':'A'})])
255 :attr:`name` is set only if it starts out equal to `None`.
258 >>> c.set_path(os.path.join('path', 'to', 'my', 'command', 'stack'))
260 'path/to/my/command/stack'
263 >>> c.set_path(os.path.join('another', 'path'))
271 if self.name == None:
272 self.name = os.path.basename(path)
274 def save(self, path=None, makedirs=True):
275 """Saves the command stack to `path`.
278 dirname = os.path.dirname(self.path) or '.'
279 if makedirs == True and not os.path.isdir(dirname):
281 with open(self.path, 'w') as f:
282 f.write(self.flatten())
284 def load(self, path=None):
285 """Load a command stack from `path`.
288 with open(self.path, 'r') as f:
290 self.from_string(text)
293 """Create a string representation of the command stack.
295 A playlist is a YAML document with the following syntax::
297 - arguments: {param: A}
299 - arguments: {param: B, ...}
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}
312 - arguments: {param: B}
314 - arguments: {param: C}
316 - arguments: {param: D}
320 return yaml.dump([{'command':cm.command,'arguments':cm.arguments}
323 def from_string(self, string):
324 """Load a playlist from a string.
326 .. warning:: This is *not safe* with untrusted input.
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
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}>']
348 for x in yaml.load(string):
349 self.append(CommandMessage(command=x['command'],
350 arguments=x['arguments']))