├── .gitignore ├── README.md └── fysom.py /.gitignore: -------------------------------------------------------------------------------- 1 | test.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Better Versions 2 | 3 | Unfortunately I don't get time to add unit tests or improve on this 4 | anymore (as I don't use it much if at all) but there are other forks of 5 | fysom that you should definitely check out: 6 | 7 | * [Maximilien Riehl's](https://github.com/mriehl/fysom) smoothened fork 8 | with 100% test coverage and install scripts. He's also made it 9 | available through PyPi (easily installable using 10 | `pip install fysom`). 11 | 12 | * [Bartosz Ptaszynski's](https://github.com/foobarto/fysom) beefed up 13 | version with per event+state callbacks and multiple destination event 14 | choices. 15 | 16 | ## Other ports 17 | 18 | Below is the list of other ports of this library: 19 | 20 | * [Jake Gordon's](https://github.com/jakesgordon/javascript-state-machine) 21 | original version which this is a port of. 22 | 23 | * [Max Persson's](https://github.com/looplab/fsm) port to 24 | [Go](http://golang.org/). 25 | 26 | ## Usage 27 | 28 | See the pydoc in fysom.py 29 | -------------------------------------------------------------------------------- /fysom.py: -------------------------------------------------------------------------------- 1 | # 2 | # fysom.py - pYthOn Finite State Machine - this is a port of Jake 3 | # Gordon's javascript-state-machine to python 4 | # https://github.com/jakesgordon/javascript-state-machine 5 | # 6 | # Copyright (C) 2011 Mansour Behabadi , Jake Gordon 7 | # and other contributors 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | 29 | """ 30 | USAGE 31 | 32 | from fysom import Fysom 33 | 34 | fsm = Fysom({ 35 | 'initial': 'green', 36 | 'events': [ 37 | {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, 38 | {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, 39 | {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, 40 | {'name': 'clear', 'src': 'yellow', 'dst': 'green'} 41 | ] 42 | }) 43 | 44 | ... will create an object with a method for each event: 45 | 46 | - fsm.warn() - transition from 'green' to 'yellow' 47 | - fsm.panic() - transition from 'yellow' to 'red' 48 | - fsm.calm() - transition from 'red' to 'yellow' 49 | - fsm.clear() - transition from 'yellow' to 'green' 50 | 51 | along with the following members: 52 | 53 | - fsm.current - contains the current state 54 | - fsm.isstate(s) - return True if state s is the current state 55 | - fsm.can(e) - return True if event e can be fired in the current 56 | state 57 | - fsm.cannot(e) - return True if event s cannot be fired in the 58 | current state 59 | 60 | MULTIPLE SRC AND TO STATES FOR A SINGLE EVENT 61 | 62 | fsm = Fysom({ 63 | 'initial': 'hungry', 64 | 'events': [ 65 | {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, 66 | {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, 67 | {'name': 'eat', 'src': 'full', 'dst': 'sick'}, 68 | {'name': 'rest', 'src': ['hungry', 'satisfied', 'full', 'sick'], 69 | 'dst': 'hungry'} 70 | ] 71 | }) 72 | 73 | This example will create an object with 2 event methods: 74 | 75 | - fsm.eat() 76 | - fsm.rest() 77 | 78 | The rest event will always transition to the hungry state, while the eat 79 | event will transition to a state that is dependent on the current state. 80 | 81 | NOTE the rest event in the above example can also be specified as 82 | multiple events with the same name if you prefer the verbose approach. 83 | 84 | CALLBACKS 85 | 86 | 4 callbacks are available if your state machine has methods using the 87 | following naming conventions: 88 | 89 | - onbefore_event_ - fired before the _event_ 90 | - onleave_state_ - fired when leaving the old _state_ 91 | - onenter_state_ - fired when entering the new _state_ 92 | - onafter_event_ - fired after the _event_ 93 | 94 | You can affect the event in 2 ways: 95 | 96 | - return False from an onbefore_event_ handler to cancel the event. 97 | - return False from an onleave_state_ handler to perform an 98 | asynchronous state transition (see next section) 99 | 100 | For convenience, the 2 most useful callbacks can be shortened: 101 | 102 | - on_event_ - convenience shorthand for onafter_event_ 103 | - on_state_ - convenience shorthand for onenter_state_ 104 | 105 | In addition, a generic onchangestate() calback can be used to call a 106 | single function for all state changes. 107 | 108 | All callbacks will be passed one argument 'e' which is an object with 109 | following attributes: 110 | 111 | - fsm Fysom object calling the callback 112 | - event Event name 113 | - src Source state 114 | - dst Destination state 115 | - (any other keyword arguments you passed into the original event 116 | method) 117 | 118 | Note that when you call an event, only one instance of 'e' argument is 119 | created and passed to all 4 callbacks. This allows you to preserve data 120 | across a state transition by storing it in 'e'. It also allows you to 121 | shoot yourself in the foot if you're not careful. 122 | 123 | Callbacks can be specified when the state machine is first created: 124 | 125 | def onpanic(e): print 'panic! ' + e.msg 126 | def oncalm(e): print 'thanks to ' + e.msg 127 | def ongreen(e): print 'green' 128 | def onyellow(e): print 'yellow' 129 | def onred(e): print 'red' 130 | 131 | fsm = Fysom({ 132 | 'initial': 'green', 133 | 'events': [ 134 | {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, 135 | {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, 136 | {'name': 'panic', 'src': 'green', 'dst': 'red'}, 137 | {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, 138 | {'name': 'clear', 'src': 'yellow', 'dst': 'green'} 139 | ], 140 | 'callbacks': { 141 | 'onpanic': onpanic, 142 | 'oncalm': oncalm, 143 | 'ongreen': ongreen, 144 | 'onyellow': onyellow, 145 | 'onred': onred 146 | } 147 | }) 148 | 149 | fsm.panic(msg='killer bees') 150 | fsm.calm(msg='sedatives in the honey pots') 151 | 152 | Additionally, they can be added and removed from the state machine at 153 | any time: 154 | 155 | def printstatechange(e): 156 | print 'event: %s, src: %s, dst: %s' % (e.event, e.src, e.dst) 157 | 158 | del fsm.ongreen 159 | del fsm.onyellow 160 | del fsm.onred 161 | fsm.onchangestate = printstatechange 162 | 163 | ASYNCHRONOUS STATE TRANSITIONS 164 | 165 | Sometimes, you need to execute some asynchronous code during a state 166 | transition and ensure the new state is not entered until you code has 167 | completed. 168 | 169 | A good example of this is when you run a background thread to download 170 | something as result of an event. You only want to transition into the 171 | new state after the download is complete. 172 | 173 | You can return False from your onleave_state_ handler and the state 174 | machine will be put on hold until you are ready to trigger the 175 | transition using transition() method. 176 | 177 | Example: TODO 178 | 179 | INITIALIZATION OPTIONS 180 | 181 | How the state machine should initialize can depend on your application 182 | requirements, so the library provides a number of simple options. 183 | 184 | By default, if you don't specify any initial state, the state machine 185 | will be in the 'none' state and you would need to provide an event to 186 | take it out of this state: 187 | 188 | fsm = Fysom({ 189 | 'events': [ 190 | {'name': 'startup', 'src': 'none', 'dst': 'green'}, 191 | {'name': 'panic', 'src': 'green', 'dst': 'red'}, 192 | {'name': 'calm', 'src': 'red', 'dst': 'green'}, 193 | ] 194 | }) 195 | print fsm.current # "none" 196 | fsm.startup() 197 | print fsm.current # "green" 198 | 199 | If you specifiy the name of you initial event (as in all the earlier 200 | examples), then an implicit 'startup' event will be created for you and 201 | fired when the state machine is constructed: 202 | 203 | fsm = Fysom({ 204 | 'initial': 'green', 205 | 'events': [ 206 | {'name': 'panic', 'src': 'green', 'dst': 'red'}, 207 | {'name': 'calm', 'src': 'red', 'dst': 'green'}, 208 | ] 209 | }) 210 | print fsm.current # "green" 211 | 212 | If your object already has a startup method, you can use a different 213 | name for the initial event: 214 | 215 | fsm = Fysom({ 216 | 'initial': {'state': 'green', 'event': 'init'}, 217 | 'events': [ 218 | {'name': 'panic', 'src': 'green', 'dst': 'red'}, 219 | {'name': 'calm', 'src': 'red', 'dst': 'green'}, 220 | ] 221 | }) 222 | print fsm.current # "green" 223 | 224 | Finally, if you want to wait to call the initiall state transition 225 | event until a later date, you can defer it: 226 | 227 | fsm = Fysom({ 228 | 'initial': {'state': 'green', 'event': 'init', 'defer': True}, 229 | 'events': [ 230 | {'name': 'panic', 'src': 'green', 'dst': 'red'}, 231 | {'name': 'calm', 'src': 'red', 'dst': 'green'}, 232 | ] 233 | }) 234 | print fsm.current # "none" 235 | fsm.init() 236 | print fsm.current # "green" 237 | 238 | Of course, we have now come full circle, this last example pretty much 239 | functions the same as the first example in this section where you simply 240 | define your own startup event. 241 | 242 | So you have a number of choices available to you when initializing your 243 | state machine. 244 | 245 | """ 246 | 247 | __author__ = 'Mansour Behabadi' 248 | __copyright__ = 'Copyright 2011, Mansour Behabadi and Jake Gordon' 249 | __credits__ = ['Mansour Behabadi', 'Jake Gordon'] 250 | __license__ = 'MIT' 251 | __version__ = '1.0' 252 | __maintainer__ = 'Mansour Behabadi' 253 | __email__ = 'mansour@oxplot.com' 254 | 255 | try: 256 | unicode = unicode 257 | except NameError: 258 | unicode = str 259 | basestring = (str, bytes) 260 | 261 | class FysomError(Exception): 262 | pass 263 | 264 | class Fysom(object): 265 | 266 | def __init__(self, cfg): 267 | self._apply(cfg) 268 | 269 | def isstate(self, state): 270 | return self.current == state 271 | 272 | def can(self, event): 273 | return event in self._map and self.current in self._map[event] \ 274 | and not hasattr(self, 'transition') 275 | 276 | def cannot(self, event): 277 | return not self.can(event) 278 | 279 | def _apply(self, cfg): 280 | init = cfg['initial'] if 'initial' in cfg else None 281 | if isinstance(init, basestring): 282 | init = {'state': init} 283 | events = cfg['events'] if 'events' in cfg else [] 284 | callbacks = cfg['callbacks'] if 'callbacks' in cfg else {} 285 | tmap = {} 286 | self._map = tmap 287 | 288 | def add(e): 289 | src = [e['src']] if isinstance(e['src'], basestring) else e['src'] 290 | if e['name'] not in tmap: 291 | tmap[e['name']] = {} 292 | for s in src: 293 | tmap[e['name']][s] = e['dst'] 294 | 295 | if init: 296 | if 'event' not in init: 297 | init['event'] = 'startup' 298 | add({'name': init['event'], 'src': 'none', 'dst': init['state']}) 299 | 300 | for e in events: 301 | add(e) 302 | 303 | for name in tmap: 304 | setattr(self, name, self._build_event(name)) 305 | 306 | for name in callbacks: 307 | setattr(self, name, callbacks[name]) 308 | 309 | self.current = 'none' 310 | 311 | if init and 'defer' not in init: 312 | getattr(self, init['event'])() 313 | 314 | def _build_event(self, event): 315 | 316 | def fn(**kwargs): 317 | 318 | if hasattr(self, 'transition'): 319 | raise FysomError("event %s inappropriate because previous" 320 | " transition did not complete" % event) 321 | if not self.can(event): 322 | raise FysomError("event %s inappropriate in current state" 323 | " %s" % (event, self.current)) 324 | 325 | src = self.current 326 | dst = self._map[event][src] 327 | 328 | class _e_obj(object): 329 | pass 330 | e = _e_obj() 331 | e.fsm, e.event, e.src, e.dst = self, event, src, dst 332 | for k in kwargs: 333 | setattr(e, k, kwargs[k]) 334 | 335 | if self.current != dst: 336 | if self._before_event(e) == False: 337 | return 338 | def _tran(): 339 | delattr(self, 'transition') 340 | self.current = dst 341 | self._enter_state(e) 342 | self._change_state(e) 343 | self._after_event(e) 344 | self.transition = _tran 345 | 346 | if self._leave_state(e) != False: 347 | if hasattr(self, 'transition'): 348 | self.transition() 349 | 350 | return fn 351 | 352 | def _before_event(self, e): 353 | fnname = 'onbefore' + e.event 354 | if hasattr(self, fnname): 355 | return getattr(self, fnname)(e) 356 | 357 | def _after_event(self, e): 358 | for fnname in ['onafter' + e.event, 'on' + e.event]: 359 | if hasattr(self, fnname): 360 | return getattr(self, fnname)(e) 361 | 362 | def _leave_state(self, e): 363 | fnname = 'onleave' + e.src 364 | if hasattr(self, fnname): 365 | return getattr(self, fnname)(e) 366 | 367 | def _enter_state(self, e): 368 | for fnname in ['onenter' + e.dst, 'on' + e.dst]: 369 | if hasattr(self, fnname): 370 | return getattr(self, fnname)(e) 371 | 372 | def _change_state(self, e): 373 | fnname = 'onchangestate' 374 | if hasattr(self, fnname): 375 | return getattr(self, fnname)(e) 376 | 377 | if __name__ == '__main__': 378 | pass 379 | --------------------------------------------------------------------------------