1 .. #!/usr/bin/env python
6 Test the simplestates.py generic state machine
7 ==============================================
11 :Copyright: 2006 Guenter Milde.
12 Released under the terms of the GNU General Public License
15 .. default-role:: literal
17 .. contents:: :depth: 2
20 Abstract State Machine Class
21 ----------------------------
23 First version of an abstract state machine
27 """generic state machine acting on iterable data
30 init_state -- name of the first state_handler method
36 * sets the data object to the `data` argument.
37 * remaining keyword arguments are stored as class attributes (or methods, if
38 they are function objects) overwriting class defaults (a neat little trick
39 I found somewhere on the net)
40 * the `state_handler` attribute is set to the method named in `init_state`
44 def __init__(self, data, **keyw):
45 """data -- iterable data object
46 (list, file, generator, string, ...)
47 **keyw -- all remaining keyword arguments are
48 stored as class attributes
51 self.__dict__.update(keyw)
53 The special `__iter__` method returns an iterator_. This allows to use
54 a class instance directly in an iteration loop. We define it as is a
55 generator_ method that sets the initial state and then iterates over the
56 data calling the state methods::
59 self.state_handler = getattr(self, self.init_state)
60 for token in self.data:
61 yield self.state_handler(token)
63 To use class instances as callable objects, we add a `__call__` method::
66 """Run state-machine and return tokens as a list"""
67 return [token for token in self]
69 Example 1: A two-state machine sorting numbers
70 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
72 Our small example state machine subclasses the `SimpleStates1` class::
74 class Example1(SimpleStates1):
75 """A silly example two-state machine sorting numbers
76 in the categories "low" (< 3) and "high" (>= 3).
79 It will be completed by two state methods and a `__str__` method.
84 State methods are functions that are called to iterate over the data. They
87 * test the data token for a change of state indicator
88 * return the data token after some processing
90 In our example, the `low` method switches to `high` (and calls it with the
91 data token), if token is bigger than 3. If not, it returns "l(token)"::
94 # print "low(", token, ")",
96 self.state_handler = self.high
98 return self.state_handler(token)
101 The `high` method switches to `low`, if token is bigger than 3. If not, it
104 def high(self, token):
105 # print "high(", token, ")",
107 self.state_handler = self.low
109 return self.state_handler(token)
112 Conversion of the class instance to a string is done by joining the list
113 that is returned by a call to the instance with spaces::
116 return " ".join(self())
121 Testing is done with the nose_ test framework. This will collect and
122 execute all test functions and methods (basically everything that starts or
123 ends with "[Tt]est"). This is similar to the more known "py.test".
125 .. _nose: http://somethingaboutorange.com/mrl/projects/nose/
127 We set up some test data::
129 testdata = [1, 2, 3, 4, 5, 4, 3, 2, 1]
131 and define a test function::
134 statemachine = Example1(testdata, init_state='low')
135 for result in statemachine:
139 # Calling an instance should return a list of results
141 assert statemachine() == ['l(1)','l(2)','l(3)', # low numbers
142 'h(4)','h(5)','h(4)', # high numbers
143 'l(3)','l(2)','l(1)'] # low again
145 # Converting to a string should call the __str__ method::
146 print str(statemachine)
147 assert str(statemachine) == "l(1) l(2) l(3) h(4) h(5) h(4) l(3) l(2) l(1)"
152 The sorting works as expected. However, as the state handlers get the data
153 token by token, acting on subsequent tokens or tests that combine the
154 knowledge of several tokens are hard to achieve.
156 An example would be a state handler that sums up the data tokens and
157 returns the result if it exceeds a threshold.
161 Varied State Machine Class Template
162 -----------------------------------
164 The second version of an abstract state machine converts the test data to an
165 iterator which is shared by the state methods.
167 There is no need to pass this on via arguments, as class methods share the
168 class instances attributes (class variables).
170 We subclass our first version and modify to our needs::
172 class SimpleStates2(SimpleStates1):
173 """second version of the abstract state machine class
176 We add the initialisation of the data to the `__iter__` method. The data is
177 transformed into an iterator_ first. ::
180 self.data_iterator = iter(self.data)
181 self.state_handler = getattr(self, self.init_state)
182 # do not pass data tokens as argument
183 # (state methods should call self.data_iterator.next() instead)
185 yield self.state_handler()
187 Iterators "use up" the data, so the state methods will always get a "new"
188 token until the data is fully "used up" and `StopIteration` is raised
189 aborting the iteration.
191 Doing the conversion from iterable to iterator in `__iter__` and not in
192 `__init__` allows repeated iteration over the class instance (if the data is
193 a list or a file and not already a generator) as the "used up" generator is
194 replaced by a new one.
196 Example 2: Another two-state machine sorting numbers
197 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
199 Our small example state machine subclasses the `SimpleStates2` class
200 and adds 2 methods as state handlers. ::
202 class Example2(SimpleStates2):
203 """An example two-state machine sorting numbers
204 in the categories "low" (< 3) and "high" (>= 3).
210 This time, the state methods will get the data tokens not as argument but
211 take them from the `data_iterator`. Note that *backtracking* is impossible
212 with a standard iterator. See below for the problem this causes for our
213 sorting algorithm. ::
216 # print "low(", token, ")",
217 token = self.data_iterator.next()
219 self.state_handler = self.high
222 # print "high(", token, ")",
223 token = self.data_iterator.next()
225 self.state_handler = self.low
231 Define a second test function::
234 statemachine = Example2(testdata, init_state='low')
236 Calling an instance should return a list of results. However, as
237 we cannot backtrack on a normal iterator, the result is not as we expected:
238 There is a "hysteresis" the "switch triggering" token is always processed by
242 assert statemachine() == ['l(1)', 'l(2)', 'l(3)', # low numbers
243 'l(4)', 'h(5)', 'h(4)', # high numbers
244 'h(3)', 'l(2)', 'l(1)'] # low numbers
249 Missing backtracks break our number sorting machine. The remedy
250 is the use of an iterator with an appendleft() method (known from the
251 dqueue() standard class). We will come to this in `Example 4`__
253 __ `Example 4: A two-state machine with generators and backtracking`_
255 OTOH, as the state methods do the fetching of data tokens themself, a state
256 handler that sums up the data tokens and returns the result if it exceeds a
257 threshold would be easy to implement. We will do this in our next example
258 using state handler generators.
261 State Machine class using state_handler generators
262 --------------------------------------------------
264 The variations in `StateMachine2` complicate the StateMachine design. They
265 makes sense, however, if we use generated iterators to handle the states.
266 No changes are needed to the abstract base class, so that Example 3 can
267 build on `StateMachine2`::
269 class Example3(SimpleStates2):
271 Example 3: A two-state machine with state handler generators
272 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
277 State Generators generate and return an iterator that will handle the next
278 data token(s) if its .next() method is called. This is easily achieved with a
279 for loop over self.data and the `yield` keyword.
282 def high_handler_generator(self):
283 """Return an iterator, whose next() method
284 returns "h(token)" and switches to `low`, if token > 3
286 for token in self.data_iterator:
288 self.state_handler = self.low
291 def low_handler_generator(self):
292 """Return an iterator, whose next() method sums up data tokens.
293 If the sum exceeds 8, it is returned and the state
297 for token in self.data_iterator:
300 self.state_handler = self.high
302 sum = 0 # next iteration continues here
303 # no more tokens but sum not reached
304 yield "p=%d"%sum # partial sum
306 The iterator must instantiate the state-iterators before starting the
310 """Generate and return an iterator
312 * convert `data` to an iterator
313 * convert the state generators into iterators
314 * (re) set the state_handler attribute to the init-state
315 * pass control to the active states state_handler
316 which should call and process self.data_iterator.next()
318 self.data_iterator = iter(self.data)
319 self.high = self.high_handler_generator().next
320 self.low = self.low_handler_generator().next
322 self.state_handler = getattr(self, self.init_state)
323 # now start the iteration, aborts if data is empty
325 yield self.state_handler()
330 Again define a test function that gets an instance of the Example3 class ::
333 statemachine = Example3(testdata, init_state='low')
335 Calling statemachine() should iterate over the test data and return the
336 processed values as list::
339 assert statemachine() == ['s=10','h(5)','h(4)','h(3)', 'p=3']
344 the iterqueue module provides an "extensible" iterator with, e.g.,
345 an `appendleft` method to push back values::
347 from iterqueue import XIter
349 Thus we can prepend a non-processed data item
350 to the data iterator for use by the next state handler
352 Example 4: A two-state machine with generators and backtracking
353 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
355 Again we start from the `SimpleStates2` base class::
357 class Example4(SimpleStates2):
358 """two-state machine with generators and backtracking
361 Let the iterator wrap the data in an XIter instance with `appendleft`
365 """Generate and return an iterator
367 * convert `data` to an iterator queue
368 * convert the state generators into iterators
369 * (re) set the state_handler attribute to the init-state
370 * pass control to the active states state_handler
371 which should call and process self.data_iterator.next()
373 self.data_iterator = XIter(self.data) # queue with `appendleft` method
374 self.high = self.high_handler_generator().next
375 self.low = self.low_handler_generator().next
376 self.state_handler = getattr(self, self.init_state)
377 # now start the iteration
379 yield self.state_handler()
381 Add state method generators that use the "backtracking" feature::
383 def high_handler_generator(self):
384 """Return an iterator, whose next() method
385 returns "h(token)" and switches to `low`, if token > 3
387 for token in self.data_iterator:
388 # print "high got", token
390 # push back data token
391 self.data_iterator.appendleft(token)
393 self.state_handler = self.low
394 # return non-value indicating the state switch
399 def low_handler_generator(self):
400 """Return an iterator, whose next() method
401 returns "l(token)" and switches to `high`, if token <=3
403 for token in self.data_iterator:
404 # print "low got", token
406 self.data_iterator.appendleft(token) # push back
407 # set and run the new state
408 self.state_handler = self.high
409 # alternatively, return the token processed by the new
411 yield self.state_handler()
415 The `__str__` converter should ignore the switch-indicator::
418 tokens = [token for token in self() if token != None]
419 return " ".join(tokens)
424 Again define a test function. This time with an instance of the Example4
428 statemachine = Example4(testdata, init_state='low')
430 Calling statemachine() should iterate over the test data and return the
431 processed values as list. If the state of the machine changes, the special
432 "non-value" `None` is returned. ::
434 print statemachine() # only printed if something goes wrong
435 assert statemachine() == ['l(1)', 'l(2)', 'l(3)',
436 'h(4)', 'h(5)', 'h(4)', None, # switch indicator
437 'l(3)', 'l(2)', 'l(1)']
439 Converting to a string should skip the `None` values::
442 assert str(statemachine) == "l(1) l(2) l(3) h(4) h(5) h(4) l(3) l(2) l(1)"
447 The `XIter` class allows backtracking also in a state machine with state
448 handlers acting on a common iterator object. The "high" and "low" handlers
449 demonstrate two possible actions for the state-transition with backtrack:
450 Either call the new state handler from the current one
451 (like the `low_handler_generator`) or return a "non-value" that signifies
452 that processing the data token did not produce any output data.
454 Using generators made the state handlers shorter and (once the concept of a
455 generator is clear) easier. Further advantages of the generator concept are
457 * internal variables are easily saved over subsequent invocations
458 * no function-call overhead (not relevant in this example but maybe for a
459 state machine that has to process long data lists.
462 Converting all state method generators with a generic function
463 --------------------------------------------------------------
465 In `Example4`, we had to redefine the `__iter__` method to convert the
466 method state generators into iterators. It would be nice if this could be
467 done in the base class.
469 `SimpleStates3` adds a generic function for this task that relies on a
470 simple naming convention: functions whose name matches
471 `<state>_handler_generator` should be converted to iterators and their
472 `.next()` method stored as `<state>`.
475 class SimpleStates5(SimpleStates2):
476 """generic state machine acting on iterable data
478 def _initialize_state_generators(self):
479 """Generic function to initialise state handlers from generators
481 functions whose name matches `[^_]<state>_handler_generator` should
482 be converted to iterators and their `.next()` method stored as
485 suffix = "_handler_generator"
486 shg_names = [name for name in dir(self)
487 if name.endswith(suffix)
488 and not name.startswith("_")]
489 for name in shg_names:
490 shg = getattr(self, name)
492 setattr(self, name[:-len(suffix)], shg().next)
496 """Generate and return an iterator
498 * convert `data` to an iterator queue
499 * convert the state generators into iterators
500 * (re) set the state_handler attribute to the init-state
501 * pass control to the active states state_handler
502 which should call and process self.data_iterator.next()
504 self.data_iterator = XIter(self.data) # queue with `appendleft` method
505 self._initialize_state_generators()
506 self.state_handler = getattr(self, self.init_state)
507 # now start the iteration
509 yield self.state_handler()
514 The next example combines the state handlers from Example 4 and the new
517 class Example5(Example4, SimpleStates5):
518 """one more example"""
524 A function that has the generator-suffix but is prefixed with an underscore,
525 should be skipped by the `_initialize_state_generators` method::
527 class Test_SimpleStates5:
528 E5 = Example5(testdata)
529 E5._bogus_handler_generator = "low"
530 def test_initialize_state_generators(self):
531 self.E5._initialize_state_generators()
533 A test function. This time with an instance of the Example5 class ::
536 statemachine = Example5(testdata, init_state='low')
537 print statemachine.__dict__
539 Calling statemachine() should iterate over the test data and return the
540 processed values as list. If the state of the machine changes, the special
541 "non-value" `None` is returned. ::
543 print statemachine() # only printed if something goes wrong
544 assert statemachine() == ['l(1)', 'l(2)', 'l(3)',
545 'h(4)', 'h(5)', 'h(4)', None, # switch indicator
546 'l(3)', 'l(2)', 'l(1)']
548 Converting to a string should skip the `None` values::
551 assert str(statemachine) == "l(1) l(2) l(3) h(4) h(5) h(4) l(3) l(2) l(1)"
556 The file `simplestates.py` contains the full definition of the `SimpleStates5`
557 class in a self-contained version.
562 The final Example is used to test whether we have put it together well. It
563 subclasses SimpleStates and adds state method generators for "high" and
567 class Example6(simplestates.SimpleStates):
568 """two-state machine with generators and backtracking
570 def high_handler_generator(self):
571 """Return an iterator, whose next() method
572 returns "h(token)" and switches to `low`, if token > 3
574 for token in self.data_iterator:
575 # print "high got", token
577 # push back data token
578 self.data_iterator.appendleft(token)
580 self.state_handler = self.low
581 # return the token processed by the new state handler
582 yield self.state_handler()
586 def low_handler_generator(self):
587 """Return an iterator, whose next() method
588 returns "l(token)" and switches to `high`, if token <=3
590 for token in self.data_iterator:
591 # print "low got", token
593 self.data_iterator.appendleft(token) # push back
594 # set and run the new state
595 self.state_handler = self.high
596 # return the token processed by the new state handler
597 yield self.state_handler()
604 In order not to make it dependent on the iterqueue module, the final
605 `SimpleStates` does not wrap the data in an XIter instance. This step should
606 be done at the instantiation of a state machine. ::
609 statemachine = Example5(XIter(testdata), init_state='low')
610 print statemachine.__dict__
612 Calling statemachine() should iterate over the test data and return the
613 processed values as list::
615 print statemachine() # only printed if something goes wrong
616 # reset the data iterator as it is "used up" now
617 statemachine.data = XIter(testdata)
618 assert statemachine() == ['l(1)', 'l(2)', 'l(3)',
619 'h(4)', 'h(5)', 'h(4)', None,
620 'l(3)', 'l(2)', 'l(1)']
626 :_`generator`: A function with a `yield` keyword. Calling this function will
629 :_`iterator`: An object with a `next()` method. Calling `<iterator>.next()`
630 will (typically) return one data token (list element, line in
631 a file, ...). If there is no more data the `StopIteration`
637 running this script should explore it in the "nose" test framework::
639 if __name__ == "__main__":
641 # first run any doctests
643 # then run nose on this module
644 nose.runmodule() # requires nose 0.9.1