3 # # -*- coding: iso-8859-1 -*-
5 # Test the simplestates.py generic state machine
6 # ==============================================
10 # :Copyright: 2006 Guenter Milde.
11 # Released under the terms of the GNU General Public License
14 # .. 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
102 # returns "h(token)"::
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 initialization of the data to the `__iter__` method. The data is
177 # transformed inta 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 instanciate 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 "extendable" 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 # methode 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 initialize 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)"
553 # Putting it together
554 # ===================
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` doesnot wrap the data in an XIter instance. This step should
606 # be done at the instanciation 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
627 # return an iterator_
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`
632 # exception is raised.
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