Moved the pylit script sources into a src directory
[pylit.git] / src / simplestates.py
blobb183e54bba1cbdfe0520d520927b3d596ac7f821
1 #!/usr/bin/env python
2 # -*- coding: iso-8859-1 -*-
4 # .. simplestates.py: generic state machine class using iterators
5 #
6 # ===========================================
7 # Generic state machine class using iterators
8 # ===========================================
9 #
10 # :Version: 0.2
11 # :Date: 2006-12-01
12 # :Copyright: 2006 Guenter Milde.
13 # Released under the terms of the GNU General Public License
14 # (v. 2 or later)
16 # Detailled documentation of this class and the design rationales (including
17 # tested variants) is available in the file simplestates-test.py.txt
19 # ::
21 """Simple generic state machine class using iterators
23 Usage
24 =====
26 Example: A two-state machine sorting numbers in the categories
27 "< 3" and ">= 3".
29 Preparation
30 -----------
32 Import the basic class::
34 >>> from simplestates import SimpleStates
36 Subclass and add state handlers:
38 >>> class StateExample(SimpleStates):
39 ... def high_handler_generator(self):
40 ... result = []
41 ... for token in self.data_iterator:
42 ... if token <= 3:
43 ... self.state = "low"
44 ... yield result
45 ... result = []
46 ... else:
47 ... result.append(token)
48 ... yield result
49 ... def low_handler_generator(self):
50 ... result = []
51 ... for token in self.data_iterator:
52 ... if token > 3:
53 ... self.state = "high"
54 ... yield result
55 ... result = []
56 ... else:
57 ... result.append(token)
58 ... yield result
61 Set up an instance of the StateExample machine with some test data::
63 >>> testdata = [1, 2, 3, 4, 5, 4, 3, 2, 1]
64 >>> testmachine = StateExample(testdata, state="low")
66 >>> print [name for name in dir(testmachine) if name.endswith("generator")]
67 ['high_handler_generator', 'low_handler_generator']
70 Running
71 -------
73 Iterating over the state machine yields the results of state processing::
75 >>> for result in testmachine:
76 ... print result,
77 ...
78 [1, 2, 3] [5, 4] [2, 1]
80 For a correct working sort algorithm, we would expect::
82 [1, 2, 3] [4, 5, 4] [3, 2, 1]
84 However, to achieve this a backtracking algorithm is needed. See iterqueue.py
85 and simplestates-test.py for an example.
88 The `__call__` method returns a list of results. It is used if you call
89 an instance of the class::
91 >>> testmachine()
92 [[1, 2, 3], [5, 4], [2, 1]]
94 """
96 # Abstract State Machine Class
97 # ============================
99 # ::
101 class SimpleStates:
102 """generic state machine acting on iterable data
104 Class attributes:
106 state -- name of the current state (next state_handler method called)
107 state_handler_generator_suffix -- common suffix of generator functions
108 returning a state-handler iterator
110 state = 'start'
111 state_handler_generator_suffix = "_handler_generator"
113 # Initialisation
114 # --------------
116 # * sets the data object to the `data` argument.
118 # * remaining keyword arguments are stored as class attributes (or methods, if
119 # they are function objects) overwriting class defaults (a neat little trick
120 # I found somewhere on the net)
122 # ..note: This is the same as `self.__dict__.update(keyw)`. However,
123 # the "Tutorial" advises to confine the direct use of `__dict__`
124 # to post-mortem analysis or the like...
126 # ::
128 def __init__(self, data, **keyw):
129 """data -- iterable data object
130 (list, file, generator, string, ...)
131 **keyw -- all remaining keyword arguments are
132 stored as class attributes
134 self.data = data
135 for (key, value) in keyw.iteritems():
136 setattr(self, key, value)
142 # Iteration over class instances
143 # ------------------------------
145 # The special `__iter__` method returns an iterator. This allows to use
146 # a class instance directly in an iteration loop. We define it as is a
147 # generator method that sets the initial state and then iterates over the
148 # data calling the state methods::
150 def __iter__(self):
151 """Generate and return an iterator
153 * ensure `data` is an iterator
154 * convert the state generators into iterators
155 * (re) set the state attribute to the initial state
156 * pass control to the active states state_handler
157 which should call and process self.data_iterator.next()
159 self.data_iterator = iter(self.data)
160 self._initialize_state_generators()
161 # now start the iteration
162 while True:
163 yield getattr(self, self.state)()
165 # a helper function generates state handlers from generators. It is called by
166 # the `__iter__` method above::
168 def _initialize_state_generators(self):
169 """Generic function to initialize state handlers from generators
171 functions whose name matches `[^_]<state>_handler_generator` will
172 be converted to iterators and their `.next()` method stored as
173 `self.<state>`.
175 suffix = self.state_handler_generator_suffix
176 shg_names = [name for name in dir(self)
177 if name.endswith(suffix)
178 and not name.startswith("_")]
179 for name in shg_names:
180 shg = getattr(self, name)
181 setattr(self, name[:-len(suffix)], shg().next)
183 # Use instances like functions
184 # ----------------------------
186 # To allow use of class instances as callable objects, we add a `__call__`
187 # method::
189 def __call__(self):
190 """Iterate over state-machine and return results as a list"""
191 return [token for token in self]
193 # Command line usage
194 # ==================
196 # running this script directly should do a doctest::
198 if __name__ == "__main__":
199 import doctest
200 doctest.testmod()