├── LICENSE ├── README.md ├── setup.py └── src └── fsm.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Slawek Ligus All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | * Neither the name of the author nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-fsm 2 | Python Finite State Machine implementation with a pygraphviz hook. 3 | ## TCP/IP state transitions 4 | ![TCP/IP](http://js-hosting.appspot.com/images/tcpip.png) 5 | ``` 6 | #!/usr/bin/python 7 | 8 | from fsm import FiniteStateMachine, get_graph, State 9 | 10 | STATES = ['LISTEN', 'SYN RCVD', 'ESTABLISHED', 'SYN SENT', 11 | 'FIN WAIT 1', 'FIN WAIT 2', 'TIME WAIT', 'CLOSING', 'CLOSE WAIT', 12 | 'LAST ACK'] 13 | 14 | tcpip = FiniteStateMachine('TCP IP') 15 | 16 | closed = State('CLOSED', initial=True) 17 | listen, synrcvd, established, synsent, finwait1, finwait2, timewait, \ 18 | closing, closewait, lastack = [State(s) for s in STATES] 19 | 20 | timewait['(wait)'] = closed 21 | closed.update({r'passive\nopen': listen, 22 | 'send SYN': synsent}) 23 | 24 | synsent.update({r'close /\ntimeout': closed, 25 | r'recv SYN,\nsend\nSYN+ACK': synrcvd, 26 | r'recv SYN+ACK,\nsend ACK': established}) 27 | 28 | listen.update({r'recv SYN,\nsend\nSYN+ACK': synrcvd, 29 | 'send SYN': synsent}) 30 | 31 | synrcvd.update({'recv ACK': established, 32 | 'send FIN': finwait1, 33 | 'recv RST': listen}) 34 | 35 | established.update({'send FIN': finwait1, 36 | r'recv FIN,\nsend ACK': closewait}) 37 | 38 | closewait['send FIN'] = lastack 39 | 40 | lastack['recv ACK'] = closed 41 | 42 | finwait1.update({'send ACK': closing, 43 | 'recv ACK': finwait2, 44 | r'recv FIN, ACK\n send ACK': timewait}) 45 | 46 | finwait2[r'recv FIN,\nsend ACK'] = timewait 47 | 48 | closing[r'recv\nACK'] = timewait 49 | 50 | graph = get_graph(tcpip) 51 | graph.draw('tcp.png', prog='dot') 52 | ``` 53 | 54 | ## Dublin City Council Parking Meter (2011) 55 | ![Irish Park-o-meter](http://js-hosting.appspot.com/images/parking.png) 56 | ``` 57 | """A Moore Machine modeled on Dublin's City parking meters.""" 58 | from fsm import * 59 | 60 | parking_meter = MooreMachine('Parking Meter') 61 | 62 | ready = State('Ready', initial=True) 63 | verify = State('Verify') 64 | await_action = State(r'Await\naction') 65 | print_tkt = State('Print ticket') 66 | return_money = State(r'Return\nmoney') 67 | reject = State('Reject coin') 68 | ready[r'coin inserted'] = verify 69 | 70 | verify.update({'valid': State(r'add value\rto ticket'), 71 | 'invalid': reject}) 72 | 73 | for coin_value in verify: 74 | verify[coin_value][''] = await_action 75 | 76 | await_action.update({'print': print_tkt, 77 | 'coin': verify, 78 | 'abort': return_money, 79 | 'timeout': return_money}) 80 | return_money[''] = print_tkt[''] = ready 81 | get_graph(parking_meter).draw('parking.png', prog='dot') 82 | ``` 83 | ## Binary Adder 84 | ![Binary Adder](http://js-hosting.appspot.com/images/adder.png) 85 | ``` 86 | adder = MealyMachine('Binary addition') 87 | 88 | carry = State('carry') 89 | nocarry = State('no carry', initial=True) 90 | 91 | nocarry[(1, 0), 1] = nocarry 92 | nocarry[(0, 1), 1] = nocarry 93 | nocarry[(0, 0), 0] = nocarry 94 | nocarry[(1, 1), 0] = carry 95 | 96 | carry[(1, 1), 1] = carry 97 | carry[(0, 1), 0] = carry 98 | carry[(1, 0), 0] = carry 99 | carry[(0, 0), 1] = nocarry 100 | 101 | number1 = list(int (i) for i in '0001010') 102 | number2 = list(int (i) for i in '0001111') 103 | 104 | inputs = zip(number1, number2) 105 | 106 | print list(adder.process(inputs[::-1]))[::-1] 107 | ``` 108 | the code above will print [0, 0, 1, 1, 0, 0, 1] 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | print __file__ 3 | """An implementation of the Finite State Machine. 4 | 5 | This module can be used to build and describe finite-state automata. 6 | 7 | The list of implemented FSM types: 8 | 9 | - FiniteStateMachine -- a semiautomaton base for all following classes. 10 | This class implements the process() method which takes an iterator 11 | as input and processes it. 12 | http://en.wikipedia.org/wiki/Semiautomaton 13 | 14 | - Acceptor -- an acceptor returning either True or False from the process() 15 | method depending on whether its final state is classified as accepting 16 | or not. 17 | http://en.wikipedia.org/wiki/Finite_state_machine#Acceptors_and_recognizers 18 | 19 | - Transducer -- a transducer class extends FiniteStateMachine by implementing 20 | an output() method which takes an input value passed to a the current 21 | state and returns current state's name. 22 | http://en.wikipedia.org/wiki/Finite-state_machine#Transducers 23 | 24 | - MooreMachine -- a specialized transducer. Its output() method returns 25 | an output value stored in the current state. 26 | http://en.wikipedia.org/wiki/Moore_machine 27 | 28 | - MealyMachine -- another specialized transducer. Its output() method returns 29 | a value assigned to the transition that the input value caused. 30 | http://en.wikipedia.org/wiki/Mealy_machine 31 | """ 32 | 33 | import sys 34 | import os 35 | from distutils.core import setup 36 | 37 | src_path = os.path.dirname(os.path.realpath(__file__)) + '/src' 38 | sys.path.insert(0, src_path) 39 | from fsm import __version__ 40 | 41 | setup(name='python-fsm', 42 | version=__version__, 43 | license='BSD', 44 | description='Finite State Machines', 45 | long_description=__doc__, 46 | author='Slawek Ligus', 47 | author_email='root@ooz.ie', 48 | url='https://github.com/oozie/python-fsm', 49 | download_url='http://python-fsm.googlecode.com/files/python-fsm-%s.tar.gz' % __version__, 50 | package_dir={'': 'src'}, 51 | py_modules=['fsm'], 52 | ) 53 | 54 | -------------------------------------------------------------------------------- /src/fsm.py: -------------------------------------------------------------------------------- 1 | """An implementation of the Finite State Machine. 2 | 3 | This module can be used to build and describe finite-state automata. 4 | 5 | Author: Slawek Ligus 6 | 7 | Overview of classes: 8 | 9 | State -- a class representing a state which can be used in a finite state 10 | machine of any type. 11 | 12 | FiniteStateMachine -- a semiautomaton base for all following classes. 13 | This class implements the process() method which takes an iterator 14 | as input and processes it. 15 | http://en.wikipedia.org/wiki/Semiautomaton 16 | 17 | Acceptor -- an acceptor returning either True or False from the process() 18 | method depending on whether its final state is classified as accepting 19 | or not. 20 | http://en.wikipedia.org/wiki/Finite_state_machine#Acceptors_and_recognizers 21 | 22 | Transducer -- a transducer class extends FiniteStateMachine by implementing 23 | an output() method which takes an input value passed to a the current 24 | state and returns current state's name. 25 | http://en.wikipedia.org/wiki/Finite-state_machine#Transducers 26 | 27 | MooreMachine -- a specialized transducer. Its output() method returns 28 | an output value stored in the current state. 29 | http://en.wikipedia.org/wiki/Moore_machine 30 | 31 | MealyMachine -- another specialized transducer. Its output() method returns 32 | a value assigned to the transition cause by the input value. 33 | http://en.wikipedia.org/wiki/Mealy_machine 34 | """ 35 | 36 | __version__ = '0.01' 37 | 38 | MACHINES = dict() 39 | 40 | NOOP = lambda: None 41 | NOOP_ARG = lambda arg: None 42 | 43 | 44 | class FSMError(Exception): 45 | """Base FSM exception.""" 46 | pass 47 | 48 | class TransitionError(FSMError): 49 | """Transition exception.""" 50 | pass 51 | 52 | class StateError(FSMError): 53 | """State manipulation error.""" 54 | 55 | 56 | class FiniteStateMachine(object): 57 | 58 | """Generic Finite State Machine.""" 59 | 60 | DOT_ATTRS = { 61 | 'directed': True, 62 | 'strict': False, 63 | 'rankdir': 'LR', 64 | 'ratio': '0.3' 65 | } 66 | 67 | def __init__(self, name, default=True): 68 | """Construct a FSM.""" 69 | self.name = name 70 | FiniteStateMachine._setup(self) 71 | self._setup() 72 | self.current_state = None 73 | MACHINES[name] = self 74 | if default: 75 | MACHINES['default'] = MACHINES[name] 76 | 77 | def _setup(self): 78 | """Setup a FSM.""" 79 | # All finite state machines share the following attributes. 80 | self.inputs = list() 81 | self.states = list() 82 | self.init_state = None 83 | 84 | @property 85 | def all_transitions(self): 86 | """Get transitions from states. 87 | 88 | Returns: 89 | List of three element tuples each consisting of 90 | (source state, input, destination state) 91 | """ 92 | transitions = list() 93 | for src_state in self.states: 94 | for input_value, dst_state in src_state.items(): 95 | transitions.append((src_state, input_value, dst_state)) 96 | return transitions 97 | 98 | def transition(self, input_value): 99 | """Transition to the next state.""" 100 | current = self.current_state 101 | if current is None: 102 | raise TransitionError('Current state not set.') 103 | 104 | destination_state = current.get(input_value, current.default_transition) 105 | if destination_state is None: 106 | raise TransitionError('Cannot transition from state %r' 107 | ' on input %r.' % (current.name, input_value)) 108 | else: 109 | self.current_state = destination_state 110 | 111 | def reset(self): 112 | """Enter the Finite State Machine.""" 113 | self.current_state = self.init_state 114 | 115 | def process(self, input_data): 116 | """Process input data.""" 117 | self.reset() 118 | for item in input_data: 119 | self.transition(item) 120 | 121 | 122 | class Acceptor(FiniteStateMachine): 123 | 124 | """Acceptor machine.""" 125 | 126 | def _setup(self): 127 | """Setup an acceptor.""" 128 | self.accepting_states = list() 129 | 130 | def process(self, input_data): 131 | """Process input data.""" 132 | self.reset() 133 | for item in input_data: 134 | self.transition(item) 135 | return id(self.current_state) in [id(s) for s in self.accepting_states] 136 | 137 | 138 | class Transducer(FiniteStateMachine): 139 | 140 | """A semiautomaton transducer.""" 141 | 142 | def _setup(self): 143 | """Setup a transducer.""" 144 | self.outputs = list() 145 | 146 | def output(self, input_value): 147 | """Return state's name as output.""" 148 | return self.current_state.name 149 | 150 | def process(self, input_data, yield_none=True): 151 | """Process input data.""" 152 | self.reset() 153 | for item in input_data: 154 | if yield_none: 155 | yield self.output(item) 156 | elif self.output(item) is not None: 157 | yield self.output(item) 158 | self.transition(item) 159 | 160 | 161 | class MooreMachine(Transducer): 162 | 163 | """Moore Machine.""" 164 | 165 | def output(self, input_value): 166 | """Return output value assigned to the current state.""" 167 | return self.current_state.output_values[0][1] 168 | 169 | 170 | class MealyMachine(Transducer): 171 | 172 | """Mealy Machine.""" 173 | 174 | def output(self, input_value): 175 | """Return output for a given state transition.""" 176 | return dict(self.current_state.output_values).get(input_value) 177 | 178 | 179 | class State(dict): 180 | 181 | """State class.""" 182 | 183 | DOT_ATTRS = { 184 | 'shape': 'circle', 185 | 'height': '1.2', 186 | } 187 | DOT_ACCEPTING = 'doublecircle' 188 | 189 | def __init__(self, name, initial=False, accepting=False, output=None, 190 | on_entry=NOOP, on_exit=NOOP, on_input=NOOP_ARG, 191 | on_transition=NOOP_ARG, machine=None, default=None): 192 | """Construct a state.""" 193 | dict.__init__(self) 194 | self.name = name 195 | self.entry_action = on_entry 196 | self.exit_action = on_exit 197 | self.input_action = on_input 198 | self.transition_action = on_transition 199 | self.output_values = [(None, output)] 200 | self.default_transition = default 201 | if machine is None: 202 | try: 203 | machine = MACHINES['default'] 204 | except KeyError: 205 | pass 206 | 207 | if machine: 208 | machine.states.append(self) 209 | if accepting: 210 | try: 211 | machine.accepting_states.append(self) 212 | except AttributeError: 213 | raise StateError('The %r %s does not support accepting ' 214 | 'states.' % (machine.name, 215 | machine.__class__.__name__)) 216 | if initial: 217 | machine.init_state = self 218 | 219 | def __getitem__(self, input_value): 220 | """Make a transition to the next state.""" 221 | next_state = dict.__getitem__(self, input_value) 222 | self.input_action(input_value) 223 | self.exit_action() 224 | self.transition_action(next_state) 225 | next_state.entry_action() 226 | return next_state 227 | 228 | def __setitem__(self, input_value, next_state): 229 | """Set a transition to a new state.""" 230 | if not isinstance(next_state, State): 231 | raise StateError('A state must transition to another state,' 232 | ' got %r instead.' % next_state) 233 | if isinstance(input_value, tuple): 234 | input_value, output_value = input_value 235 | self.output_values.append((input_value, output_value)) 236 | dict.__setitem__(self, input_value, next_state) 237 | 238 | def __repr__(self): 239 | """Represent the object in a string.""" 240 | return '<%r %s @ 0x%x>' % (self.name, self.__class__.__name__, id(self)) 241 | 242 | 243 | def get_graph(fsm, title=None): 244 | """Generate a DOT graph with pygraphviz.""" 245 | try: 246 | import pygraphviz as pgv 247 | except ImportError: 248 | pgv = None 249 | 250 | if title is None: 251 | title = fsm.name 252 | elif title is False: 253 | title = '' 254 | 255 | fsm_graph = pgv.AGraph(title=title, **fsm.DOT_ATTRS) 256 | fsm_graph.node_attr.update(State.DOT_ATTRS) 257 | 258 | for state in [fsm.init_state] + fsm.states: 259 | shape = State.DOT_ATTRS['shape'] 260 | if hasattr(fsm, 'accepting_states'): 261 | if id(state) in [id(s) for s in fsm.accepting_states]: 262 | shape = state.DOT_ACCEPTING 263 | fsm_graph.add_node(n=state.name, shape=shape) 264 | 265 | fsm_graph.add_node('null', shape='plaintext', label=' ') 266 | fsm_graph.add_edge('null', fsm.init_state.name) 267 | 268 | for src, input_value, dst in fsm.all_transitions: 269 | label = str(input_value) 270 | if isinstance(fsm, MealyMachine): 271 | label += ' / %s' % dict(src.output_values).get(input_value) 272 | fsm_graph.add_edge(src.name, dst.name, label=label) 273 | for state in fsm.states: 274 | if state.default_transition is not None: 275 | fsm_graph.add_edge(state.name, state.default_transition.name, 276 | label='else') 277 | return fsm_graph 278 | --------------------------------------------------------------------------------