1 # Copyright (C) 2010-2011 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
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.
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.
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/>.
19 """The ``command_stack`` module provides tools for managing and
20 executing stacks of :class:`~hooke.engine.CommandMessage`\s.
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
38 from .engine import CommandMessage
41 class CommandStack (list):
42 """Store a stack of commands.
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'}))
51 Implement a dummy :meth:`execute_command` for testing.
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
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'}
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`.
70 >>> def filter(hooke, command_message):
71 ... return command_message.command == 'CommandB'
73 Apply the stack to the current curve.
75 >>> c.execute(hooke=None, filter=filter) # doctest: +ELLIPSIS
76 EXECUTE CommandB {'param': 'B'}
77 EXECUTE CommandB {'param': 'D'}
79 Execute a new command and add it to the stack.
81 >>> cm = CommandMessage('CommandC', {'param':'E'})
82 >>> c.execute_command(hooke=None, command_message=cm)
83 EXECUTE CommandC {'param': 'E'}
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}>']
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.
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 ...>
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`_).
107 .. _Python issue 1062277: http://bugs.python.org/issue1062277
110 >>> c.append(CommandMessage('CommandD', {'param': copy.deepcopy(c)}))
112 Run the pickle (and YAML) tests.
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}>]}>
131 >>> print yaml.dump(c) # doctest: +REPORT_UDIFF
132 !!python/object/new:hooke.command_stack.CommandStack
134 - !!python/object:hooke.engine.CommandMessage
135 arguments: {param: A}
137 explicit_user_call: true
138 - !!python/object:hooke.engine.CommandMessage
139 arguments: {param: B}
141 explicit_user_call: true
142 - !!python/object:hooke.engine.CommandMessage
143 arguments: {param: C}
145 explicit_user_call: true
146 - !!python/object:hooke.engine.CommandMessage
147 arguments: {param: D}
149 explicit_user_call: true
150 - !!python/object:hooke.engine.CommandMessage
151 arguments: {param: E}
153 explicit_user_call: true
154 - !!python/object:hooke.engine.CommandMessage
156 param: !!python/object/new:hooke.command_stack.CommandStack
158 - !!python/object:hooke.engine.CommandMessage
159 arguments: {param: A}
161 explicit_user_call: true
162 - !!python/object:hooke.engine.CommandMessage
163 arguments: {param: B}
165 explicit_user_call: true
166 - !!python/object:hooke.engine.CommandMessage
167 arguments: {param: C}
169 explicit_user_call: true
170 - !!python/object:hooke.engine.CommandMessage
171 arguments: {param: D}
173 explicit_user_call: true
174 - !!python/object:hooke.engine.CommandMessage
175 arguments: {param: E}
177 explicit_user_call: true
179 explicit_user_call: true
182 There is also a convenience function for clearing the stack.
185 >>> print [repr(cm) for cm in c]
188 YAMLize a curve argument.
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
195 - !!python/object:hooke.engine.CommandMessage
197 curve: !!python/object:hooke.curve.Curve {}
199 explicit_user_call: true
202 def execute(self, hooke, filter=None, stack=False):
203 """Execute a stack of commands.
207 execute_command, 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)
216 def filter(self, hooke, command_message):
217 """Return `True` to execute `command_message`, `False` otherwise.
219 The default implementation always returns `True`.
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,
234 class FileCommandStack (CommandStack):
235 """A file-backed :class:`CommandStack`.
239 def __init__(self, *args, **kwargs):
240 super(FileCommandStack, self).__init__(*args, **kwargs)
241 self.name = self.path = None
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))
249 def set_path(self, path):
250 """Set the path (and possibly the name) of the command stack.
254 >>> c = FileCommandStack([CommandMessage('CommandA', {'param':'A'})])
256 :attr:`name` is set only if it starts out equal to `None`.
259 >>> c.set_path(os.path.join('path', 'to', 'my', 'command', 'stack'))
261 'path/to/my/command/stack'
264 >>> c.set_path(os.path.join('another', 'path'))
272 if self.name == None:
273 self.name = os.path.basename(path)
275 def save(self, path=None, makedirs=True):
276 """Saves the command stack to `path`.
279 dirname = os.path.dirname(self.path) or '.'
280 if makedirs == True and not os.path.isdir(dirname):
282 with open(self.path, 'w') as f:
283 f.write(self.flatten())
285 def load(self, path=None):
286 """Load a command stack from `path`.
289 with open(self.path, 'r') as f:
291 self.from_string(text)
294 """Create a string representation of the command stack.
296 A playlist is a YAML document with the following syntax::
298 - arguments: {param: A}
300 - arguments: {param: B, ...}
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}
313 - arguments: {param: B}
315 - arguments: {param: C}
317 - arguments: {param: D}
321 return yaml.dump([{'command':cm.command,'arguments':cm.arguments}
324 def from_string(self, string):
325 """Load a playlist from a string.
327 .. warning:: This is *not safe* with untrusted input.
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
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}>']
349 for x in yaml.load(string):
350 self.append(CommandMessage(command=x['command'],
351 arguments=x['arguments']))