././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631283387.511028 fysom-2.1.6/0000755000175000017500000000000000000000000012200 5ustar00mriehlmriehl././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/CHANGELOG0000644000175000017500000000466000000000000013420 0ustar00mriehlmriehlChangelog for fysom -------------------- * v2.1.6 Import ABC from collections.abc instead of collections for Python 3 compatibility [Pull request](https://github.com/mriehl/fysom/pull/42) by [tirkarthi](https://github.com/tirkarthi), thanks! * v2.1.5 Global machine and some improvements. [Pull request](https://github.com/mriehl/fysom/pull/36) by [jxskiss](https://github.com/jxskiss) * v2.1.2 Add special symbol for dst state equals to src state. Pull request by [irpab](https://github.com/irpab) * v2.1.0 In cases where a class has a state machine instance as a member and uses methods for callbacks, the dependencies between the parent class and the child state machine created cycles in the garbage collector. In order to break these cycles, we modified the state machine to store weak references to the method. For non-method functions, we can store these safely since the function itself should not hold a reference to the state machine. Pull request by [sjlongland](https://github.com/sjlongland) * v2.0.1 State re-entry or 'reflexive' callbacks (onreenter_state) called when the start and end state of a transition are the same. This is different to the change in v1.1.0 as the new callback type is for a particular state and not a particular event. This allows individual callbacks for reflexive transitions rather than event callbacks with conditional branches to handle both reflexive and non-reflexive transitions. Pull request by [@mattjml](https://github.com/mattjml) * v2.0.0 BREAKING CHANGE - Canceling an event by returning `False` from the onbefore callback will now raise `fysom.Canceled` instead of just ignoring the event. * v1.1.2 Extend Fysom constructor to allow for terser FSM specifications. Pull request by [@astanin](https://github.com/astanin). * v1.1.1 Resolved problems with installation on windows. * v1.1.0 Event callbacks (onbefore_event_|onafter_event_|on_event_) will now trigger when the state does not change. Previously those callbacks would only fire in the case of a state change, so events keeping the FSM in the same state would not fire callbacks at all. * v1.0.19 The `trigger` method now accepts any positional arguments and keyword arguments, and passes them to the underlying event method. Pull request by [@poundifdef](https://github.com/poundifdef). * v1.0.18 From now on, a changelog will be included. Furthermore, all PyPI releases will be GPG signed. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/MANIFEST.in0000644000175000017500000000004100000000000013731 0ustar00mriehlmriehlinclude README include CHANGELOG ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631283387.511028 fysom-2.1.6/PKG-INFO0000644000175000017500000000127600000000000013303 0ustar00mriehlmriehlMetadata-Version: 2.1 Name: fysom Version: 2.1.6 Summary: pYthOn Finite State Machine Home-page: https://github.com/mriehl/fysom Author: Mansour Behabadi, Jake Gordon, Maximilien Riehl, Stefano Author-email: mansour@oxplot.com, jake@codeincomplete.com, maximilien.riehl@gmail.com, unknown@domain.invalid Maintainer: Maintainer-email: License: MIT Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Scientific/Engineering UNKNOWN ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/README0000644000175000017500000003504600000000000013070 0ustar00mriehlmriehl.. image:: https://travis-ci.org/mriehl/fysom.png?branch=master :alt: Travis build status image :align: left :target: https://travis-ci.org/mriehl/fysom .. image:: https://coveralls.io/repos/mriehl/fysom/badge.png?branch=master :target: https://coveralls.io/r/mriehl/fysom?branch=master :alt: Coverage status .. image:: https://badge.fury.io/py/fysom.png :target: https://badge.fury.io/py/fysom :alt: Latest PyPI version License ======= MIT licensed. All credits go to Jake Gordon for the `original javascript implementation `_ and to Mansour Behabadi for the `python port `_. Synopsis ======== This is basically Mansours' implementation with unit tests and a build process added. It's also on PyPi (``pip install fysom``). Fysom is built and tested on python 2.6 to 3.5 and PyPy. Installation ============ From your friendly neighbourhood cheeseshop ------------------------------------------- :: pip install fysom Developer setup --------------- This module uses `PyBuilder `_. :: sudo pip install pyb_init pyb-init github mriehl : fysom Running the tests ----------------- :: pyb verify Generating and using a setup.py ------------------------------- :: pyb cd target/dist/fysom-$VERSION ./setup.py bdist_rpm #build RPM Looking at the coverage ----------------------- :: pyb cat target/reports/coverage USAGE ===== Basics ------ :: from fysom import Fysom fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) ... will create an object with a method for each event: - fsm.warn() - transition from 'green' to 'yellow' - fsm.panic() - transition from 'yellow' to 'red' - fsm.calm() - transition from 'red' to 'yellow' - fsm.clear() - transition from 'yellow' to 'green' along with the following members: - fsm.current - contains the current state - fsm.isstate(s) - return True if state s is the current state - fsm.can(e) - return True if event e can be fired in the current state - fsm.cannot(e) - return True if event s cannot be fired in the current state Shorter Syntax -------------- It's possible to define event transitions as 3-tuples (event name, source state, destination state) rather than dictionaries. ``Fysom`` constructor accepts also keyword arguments ``initial``, ``events``, ``callbacks``, and ``final``. This is a shorter version of the previous example:: fsm = Fysom(initial='green', events=[('warn', 'green', 'yellow'), ('panic', 'yellow', 'red'), ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')]) Initialization -------------- How the state machine should initialize can depend on your application requirements, so the library provides a number of simple options. By default, if you don't specify any initial state, the state machine will be in the 'none' state and you would need to provide an event to take it out of this state: :: fsm = Fysom({'events': [ {'name': 'startup', 'src': 'none', 'dst': 'green'}, {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}]}) print fsm.current # "none" fsm.startup() print fsm.current # "green" If you specify the name of your initial event (as in all the earlier examples), then an implicit 'startup' event will be created for you and fired when the state machine is constructed: :: fsm = Fysom({'initial': 'green', 'events': [ {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}]}) print fsm.current # "green" If your object already has a startup method, you can use a different name for the initial event: :: fsm = Fysom({'initial': {'state': 'green', 'event': 'init'}, 'events': [ {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}]}) print fsm.current # "green" Finally, if you want to wait to call the initial state transition event until a later date, you can defer it: :: fsm = Fysom({'initial': {'state': 'green', 'event': 'init', 'defer': True}, 'events': [ {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}]}) print fsm.current # "none" fsm.init() print fsm.current # "green" Of course, we have now come full circle, this last example pretty much functions the same as the first example in this section where you simply define your own startup event. So you have a number of choices available to you when initializing your state machine. You can also indicate which state should be considered final. This has no effect on the state machine, but lets you use a shorthand method is_finished() that returns true if the state machine is in this 'final' state: :: fsm = Fysom({'initial': 'green', 'final': 'red', 'events': [ {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}]}) print fsm.current # "green" fsm.is_finished() # False fsm.panic() fsm.is_finished() # True Dynamically generated event names --------------------------------- Sometimes you have to compute the name of an event you want to trigger on the fly. Instead of relying on `getattr` you can use the `trigger` method, which takes a string (the event name) as a parameter, followed by any arguments/keyword arguments you want to pass to the event method. This is also arguably better if you're not sure if the event exists at all (FysomError vs. AttributeError, see below). :: from fysom import Fysom fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) fsm.trigger('warn', msg="danger") # equivalent to fsm.warn(msg="danger") fsm.trigger('unknown') # FysomError, event does not exist fsm.unknown() # AttributeError, event does not exist Multiple source and destination states for a single event --------------------------------------------------------- :: fsm = Fysom({'initial': 'hungry', 'events': [ {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, {'name': 'eat', 'src': 'full', 'dst': 'sick'}, {'name': 'rest', 'src': ['hungry', 'satisfied', 'full', 'sick'], 'dst': 'hungry'}]}) This example will create an object with 2 event methods: - fsm.eat() - fsm.rest() The rest event will always transition to the hungry state, while the eat event will transition to a state that is dependent on the current state. NOTE the rest event in the above example can also be specified as multiple events with the same name if you prefer the verbose approach. NOTE if an event can be triggered from any state, you can specify it using the '*' wildcard, or even by omitting the src attribute from its definition: :: fsm = Fysom({'initial': 'hungry', 'events': [ {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, {'name': 'eat', 'src': 'full', 'dst': 'sick'}, {'name': 'eat_a_lot', 'src': '*', 'dst': 'sick'}, {'name': 'rest', 'dst': 'hungry'}]}) NOTE if an event will not change the current state, you can specify destination using the '=' symbol. It's useful when using wildcard source or multiply sources: :: fsm = Fysom({'initial': 'hungry', 'events': [ {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, {'name': 'eat', 'src': 'full', 'dst': 'sick'}, {'name': 'eat_a_little', 'src': '*', 'dst': '='}, {'name': 'eat_a_little', 'src': ['full', 'satisfied'], 'dst': '='}, {'name': 'eat_a_little', 'src': 'hungry', 'dst': '='}, {'name': 'rest', 'dst': 'hungry'}]}) Callbacks --------- 5 callbacks are available if your state machine has methods using the following naming conventions: - onbefore\_event\_ - fired before the *event* - onleave\_state\_ - fired when leaving the old *state* - onenter\_state\_ - fired when entering the new *state* - onreenter\_state\_ - fired when reentering the old *state* (a reflexive transition i.e. src == dst) - onafter\_event\_ - fired after the *event* You can affect the event in 2 ways: - return False from an onbefore\_event\_ handler to cancel the event. This will raise a fysom.Canceled exception. - return False from an onleave\_state\_ handler to perform an asynchronous state transition (see next section) For convenience, the 2 most useful callbacks can be shortened: - on\_event\_ - convenience shorthand for onafter\_event\_ - on\_state\_ - convenience shorthand for onenter\_state\_ In addition, a generic onchangestate() callback can be used to call a single function for all state changes. All callbacks will be passed one argument 'e' which is an object with following attributes: - fsm Fysom object calling the callback - event Event name - src Source state - dst Destination state - (any other keyword arguments you passed into the original event method) - (any positional argument you passed in the original event method, in the 'args' attribute of the event) Note that when you call an event, only one instance of 'e' argument is created and passed to all 4 callbacks. This allows you to preserve data across a state transition by storing it in 'e'. It also allows you to shoot yourself in the foot if you're not careful. Callbacks can be specified when the state machine is first created: :: def onpanic(e): print 'panic! ' + e.msg def oncalm(e): print 'thanks to ' + e.msg + ' done by ' + e.args[0] def ongreen(e): print 'green' def onyellow(e): print 'yellow' def onred(e): print 'red' fsm = Fysom({'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'}], 'callbacks': { 'onpanic': onpanic, 'oncalm': oncalm, 'ongreen': ongreen, 'onyellow': onyellow, 'onred': onred }}) fsm.panic(msg='killer bees') fsm.calm('bob', msg='sedatives in the honey pots') Additionally, they can be added and removed from the state machine at any time: :: def printstatechange(e): print 'event: %s, src: %s, dst: %s' % (e.event, e.src, e.dst) del fsm.ongreen del fsm.onyellow del fsm.onred fsm.onchangestate = printstatechange Asynchronous state transitions ------------------------------ Sometimes, you need to execute some asynchronous code during a state transition and ensure the new state is not entered until you code has completed. A good example of this is when you run a background thread to download something as result of an event. You only want to transition into the new state after the download is complete. You can return False from your onleave\_state\_ handler and the state machine will be put on hold until you are ready to trigger the transition using the transition() method. Use as global machine --------------------- To manipulating lots of objects with a small memory footprint, there is a FysomGlobal class. Also a useful FysomGlobalMixin class to give convenience access for the state machine methods. A use case is using with Django, which has a cache mechanism holds lots of model objects (database records) in memory, using global machine can save a lot of memory, `here is a compare `_. The basic usage is same with Fysom, with slit difference and enhancement: - Initial state will only be automatically triggered for class derived from FysomGlobalMixin. Or you need to trigger manually. - The snake_case python naming conversion is supported. - Conditions and conditional transitions are implemented. - When an event/transition is canceled, the event object will be attached to the raised fysom.Canceled exception. By doing this, additional information can be passed through the exception. Usage example: :: class Model(FysomGlobalMixin, object): GSM = FysomGlobal( events=[('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" {True: 'is_very_angry', 'else': 'yellow'} ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial='green', final='red', state_field='state' ) def __init__(self): self.state = None super(Model, self).__init__() def is_angry(self, event): return True def is_very_angry(self, event): return False obj = Model() obj.current # 'green' obj.warn() obj.is_state('yellow') # True # conditions and conditional transition obj.panic() obj.current # 'yellow' obj.is_finished() # False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631283387.511028 fysom-2.1.6/fysom/0000755000175000017500000000000000000000000013335 5ustar00mriehlmriehl././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/fysom/__init__.py0000644000175000017500000006145200000000000015456 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import functools import weakref import types import sys try: from collections.abc import Mapping except ImportError: from collections import Mapping __author__ = 'Mansour Behabadi' __copyright__ = 'Copyright 2011, Mansour Behabadi and Jake Gordon' __credits__ = ['Mansour Behabadi', 'Jake Gordon'] __license__ = 'MIT' __version__ = '2.1.6' __maintainer__ = 'Mansour Behabadi' __email__ = 'mansour@oxplot.com' WILDCARD = '*' SAME_DST = '=' class FysomError(Exception): ''' Raised whenever an unexpected event gets triggered. Optionally the event object can be attached to the exception in case of sharing event data. ''' def __init__(self, msg, event=None): super(FysomError, self).__init__(msg) self.event = event class Canceled(FysomError): ''' Raised when an event is canceled due to the onbeforeevent handler returning False ''' def _weak_callback(func): ''' Store a weak reference to a callback or method. ''' if isinstance(func, types.MethodType): # Don't hold a reference to the object, otherwise we might create # a cycle. # Reference: http://stackoverflow.com/a/6975682 # Tell coveralls to not cover this if block, as the Python 2.x case # doesn't test the 3.x code and vice versa. if sys.version_info[0] < 3: # pragma: no cover # Python 2.x case obj_ref = weakref.ref(func.im_self) func_ref = weakref.ref(func.im_func) else: # pragma: no cover # Python 3.x case obj_ref = weakref.ref(func.__self__) func_ref = weakref.ref(func.__func__) func = None def _callback(*args, **kwargs): obj = obj_ref() func = func_ref() if (obj is None) or (func is None): return return func(obj, *args, **kwargs) return _callback else: # We should be safe enough holding callback functions ourselves. return func class Fysom(object): ''' Wraps the complete finite state machine operations. ''' def __init__(self, cfg={}, initial=None, events=None, callbacks=None, final=None, **kwargs): ''' Construct a Finite State Machine. Arguments: cfg finite state machine specification, a dictionary with keys 'initial', 'events', 'callbacks', 'final' initial initial state events a list of dictionaries (keys: 'name', 'src', 'dst') or a list tuples (event name, source state or states, destination state or states) callbacks a dictionary mapping callback names to functions final a state of the FSM where its is_finished() method returns True Named arguments override configuration dictionary. Example: >>> fsm = Fysom(events=[('tic', 'a', 'b'), ('toc', 'b', 'a')], initial='a') >>> fsm.current 'a' >>> fsm.tic() >>> fsm.current 'b' >>> fsm.toc() >>> fsm.current 'a' ''' if (sys.version_info[0] >= 3): super().__init__(**kwargs) cfg = dict(cfg) # override cfg with named arguments if "events" not in cfg: cfg["events"] = [] if "callbacks" not in cfg: cfg["callbacks"] = {} if initial: cfg["initial"] = initial if final: cfg["final"] = final if events: cfg["events"].extend(list(events)) if callbacks: cfg["callbacks"].update(dict(callbacks)) # convert 3-tuples in the event specification to dicts events_dicts = [] for e in cfg["events"]: if isinstance(e, Mapping): events_dicts.append(e) elif hasattr(e, "__iter__"): name, src, dst = list(e)[:3] events_dicts.append({"name": name, "src": src, "dst": dst}) cfg["events"] = events_dicts self._apply(cfg) def isstate(self, state): ''' Returns if the given state is the current state. ''' return self.current == state is_state = isstate def can(self, event): ''' Returns if the given event be fired in the current machine state. ''' return ( event in self._map and ((self.current in self._map[event]) or WILDCARD in self._map[event]) and not hasattr(self, 'transition')) def cannot(self, event): ''' Returns if the given event cannot be fired in the current state. ''' return not self.can(event) def is_finished(self): ''' Returns if the state machine is in its final state. ''' return self._final and (self.current == self._final) def _apply(self, cfg): ''' Does the heavy lifting of machine construction. More notably: >> Sets up the initial and finals states. >> Sets the event methods and callbacks into the same object namespace. >> Prepares the event to state transitions map. ''' init = cfg['initial'] if 'initial' in cfg else None if self._is_base_string(init): init = {'state': init} self._final = cfg['final'] if 'final' in cfg else None events = cfg['events'] if 'events' in cfg else [] callbacks = cfg['callbacks'] if 'callbacks' in cfg else {} tmap = {} self._map = tmap def add(e): ''' Adds the event into the machine map. ''' if 'src' in e: src = [e['src']] if self._is_base_string( e['src']) else e['src'] else: src = [WILDCARD] if e['name'] not in tmap: tmap[e['name']] = {} for s in src: tmap[e['name']][s] = e['dst'] # Consider initial state as any other state that can have transition from none to # initial value on occurance of startup / init event ( if specified). if init: if 'event' not in init: init['event'] = 'startup' add({'name': init['event'], 'src': 'none', 'dst': init['state']}) for e in events: add(e) # For all the events as present in machine map, construct the event # handler. for name in tmap: setattr(self, name, self._build_event(name)) # For all the callbacks, register them into the current object # namespace. for name in callbacks: setattr(self, name, _weak_callback(callbacks[name])) self.current = 'none' # If initialization need not be deferred, trigger the event for # transition to initial state. if init and 'defer' not in init: getattr(self, init['event'])() def _build_event(self, event): ''' For every event in the state machine, prepares the event handler and registers the same into current object namespace. ''' def fn(*args, **kwargs): if hasattr(self, 'transition'): raise FysomError( "event %s inappropriate because previous transition did not complete" % event) # Check if this event can be triggered in the current state. if not self.can(event): raise FysomError( "event %s inappropriate in current state %s" % (event, self.current)) # On event occurence, source will always be the current state. src = self.current # Finds the destination state, after this event is completed. dst = ((src in self._map[event] and self._map[event][src]) or WILDCARD in self._map[event] and self._map[event][WILDCARD]) if dst == SAME_DST: dst = src # Prepares the object with all the meta data to be passed to # callbacks. class _e_obj(object): pass e = _e_obj() e.fsm, e.event, e.src, e.dst = self, event, src, dst for k in kwargs: setattr(e, k, kwargs[k]) setattr(e, 'args', args) # Try to trigger the before event, unless it gets canceled. if self._before_event(e) is False: raise Canceled( "Cannot trigger event {0} because the onbefore{0} handler returns False".format(e.event)) # Wraps the activities that must constitute a single successful # transaction. if self.current != dst: def _tran(): delattr(self, 'transition') self.current = dst self._enter_state(e) self._change_state(e) self._after_event(e) self.transition = _tran # Hook to perform asynchronous transition. if self._leave_state(e) is not False: self.transition() else: self._reenter_state(e) self._after_event(e) fn.__name__ = str(event) fn.__doc__ = ("Event handler for an {event} event. This event can be " + "fired if the machine is in {states} states.".format( event=event, states=self._map[event].keys())) return fn def _before_event(self, e): ''' Checks to see if the callback is registered before this event can be triggered. ''' for fnname in ['onbefore' + e.event, 'on_before_' + e.event]: if hasattr(self, fnname): return getattr(self, fnname)(e) def _after_event(self, e): ''' Checks to see if the callback is registered for, after this event is completed. ''' for fnname in ['onafter' + e.event, 'on' + e.event, 'on_after_' + e.event, 'on_' + e.event]: if hasattr(self, fnname): return getattr(self, fnname)(e) def _leave_state(self, e): ''' Checks to see if the machine can leave the current state and perform the transition. This is helpful if the asynchronous job needs to be completed before the machine can leave the current state. ''' for fnname in ['onleave' + e.src, 'on_leave_' + e.src]: if hasattr(self, fnname): return getattr(self, fnname)(e) def _enter_state(self, e): ''' Executes the callback for onenter_state_ or on_state_. ''' for fnname in ['onenter' + e.dst, 'on' + e.dst, 'on_enter_' + e.dst, 'on_' + e.dst]: if hasattr(self, fnname): return getattr(self, fnname)(e) def _reenter_state(self, e): ''' Executes the callback for onreenter_state_. This allows callbacks following reflexive transitions (i.e. where src == dst) ''' for fnname in ['onreenter' + e.dst, 'on_reenter_' + e.dst]: if hasattr(self, fnname): return getattr(self, fnname)(e) def _change_state(self, e): ''' A general change state callback. This gets triggered at the time of state transition. ''' for fnname in ['onchangestate', 'on_change_state']: if hasattr(self, fnname): return getattr(self, fnname)(e) def _is_base_string(self, object): # pragma: no cover ''' Returns if the object is an instance of basestring. ''' try: return isinstance(object, basestring) except NameError: return isinstance(object, str) def trigger(self, event, *args, **kwargs): ''' Triggers the given event. The event can be triggered by calling the event handler directly, for ex: fsm.eat() but this method will come in handy if the event is determined dynamically and you have the event name to trigger as a string. ''' if not hasattr(self, event): raise FysomError( "There isn't any event registered as %s" % event) return getattr(self, event)(*args, **kwargs) class FysomGlobalMixin(object): GSM = None # global state machine instance, override this def __init__(self, *args, **kwargs): super(FysomGlobalMixin, self).__init__(*args, **kwargs) if self.is_state('none'): _initial = self.GSM._initial if _initial and not _initial.get('defer'): self.trigger(_initial['event']) def __getattribute__(self, attr): ''' Proxy public event methods to global machine if available. ''' try: return super(FysomGlobalMixin, self).__getattribute__(attr) except AttributeError as err: if not attr.startswith('_'): gsm_attr = getattr(self.GSM, attr) if callable(gsm_attr): return functools.partial(gsm_attr, self) raise # pragma: no cover @property def current(self): ''' Simulate the behavior of Fysom's "current" attribute. ''' return self.GSM.current(self) @current.setter def current(self, state): setattr(self, self.GSM.state_field, state) class FysomGlobal(object): ''' Target to be used as global machine. ''' def __init__(self, cfg={}, initial=None, events=None, callbacks=None, final=None, state_field=None, **kwargs): ''' Construct a Global Finite State Machine. Takes same arguments as Fysom and an additional state_field to specify which field holds the state to be processed. Difference with Fysom: 1. Initial state will only be automatically triggered for class derived with FysomGlobalMixin. 2. Conditions and conditional transition are implemented. 3. When an event/transition is canceled, the event object will be attached to the raised Canceled exception. By doing this, additional information can be passed through the exception. Example: class Model(FysomGlobalMixin, object): GSM = FysomGlobal( events=[('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" {True: 'is_very_angry', 'else': 'yellow'} ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial='green', final='red', state_field='state' ) def __init__(self): self.state = None super(Model, self).__init__() def is_angry(self, event): return True def is_very_angry(self, event): return False >>> obj = Model() >>> obj.current 'green' >>> obj.warn() >>> obj.is_state('yellow') True >>> obj.panic() >>> obj.current 'yellow' >>> obj.is_finished() False ''' if sys.version_info[0] >= 3: super().__init__(**kwargs) cfg = dict(cfg) # state_field is required for global machine if not state_field: raise FysomError('state_field required for global machine') self.state_field = state_field if "events" not in cfg: cfg["events"] = [] if "callbacks" not in cfg: cfg["callbacks"] = {} if initial: cfg['initial'] = initial if events: cfg["events"].extend(list(events)) if callbacks: cfg["callbacks"].update(dict(callbacks)) if final: cfg["final"] = final # convert 3-tuples in the event specification to dicts events_dicts = [] for e in cfg["events"]: if isinstance(e, Mapping): events_dicts.append(e) elif hasattr(e, "__iter__"): name, src, dst = list(e)[:3] events_dicts.append({"name": name, "src": src, "dst": dst}) cfg["events"] = events_dicts self._map = {} # different with Fysom's _map attribute self._callbacks = {} self._initial = None self._final = None self._apply(cfg) def _apply(self, cfg): def add(e): if 'src' in e: src = [e['src']] if self._is_base_string( e['src']) else e['src'] else: src = [WILDCARD] _e = {'src': set(src), 'dst': e['dst']} conditions = e.get('cond') if conditions: _e['cond'] = _c = [] if self._is_base_string(conditions) or callable(conditions): _c.append({True: conditions}) else: for cond in conditions: if self._is_base_string(cond): _c.append({True: cond}) else: _c.append(cond) self._map[e['name']] = _e initial = cfg['initial'] if 'initial' in cfg else None if self._is_base_string(initial): initial = {'state': initial} if initial: if 'event' not in initial: initial['event'] = 'startup' self._initial = initial add({'name': initial['event'], 'src': 'none', 'dst': initial['state']}) if 'final' in cfg: self._final = cfg['final'] for e in cfg['events']: add(e) for event in self._map: setattr(self, event, self._build_event(event)) for name, callback in cfg['callbacks'].items(): self._callbacks[name] = _weak_callback(callback) def _build_event(self, event): def fn(obj, *args, **kwargs): if not self.can(obj, event): raise FysomError( 'event %s inappropriate in current state %s' % (event, self.current(obj))) # Prepare the event object with all the meta data to pas through. # On event occurrence, source will always be the current state. e = self._e_obj() e.fsm, e.obj, e.event, e.src, e.dst = ( self, obj, event, self.current(obj), self._map[event]['dst']) setattr(e, 'args', args) setattr(e, 'kwargs', kwargs) for k, v in kwargs.items(): setattr(e, k, v) # check conditions first, event dst may change during # checking conditions for c in self._map[event].get('cond', ()): target = True in c cond = c[target] _c_r = self._check_condition(obj, cond, target, e) if not _c_r: if 'else' in c: e.dst = c['else'] break else: raise Canceled( 'Cannot trigger event {0} because the {1} ' 'condition not returns {2}'.format( event, cond, target), e ) # try to trigger the before event, unless it gets cancelled. if self._before_event(obj, e) is False: raise Canceled( 'Cannot trigger event {0} because the onbefore{0} ' 'handler returns False'.format(event), e) # wraps the activities that must constitute a single transaction if self.current(obj) != e.dst: def _trans(): delattr(obj, 'transition') setattr(obj, self.state_field, e.dst) self._enter_state(obj, e) self._change_state(obj, e) self._after_event(obj, e) obj.transition = _trans # Hook to perform asynchronous transition if self._leave_state(obj, e) is not False: obj.transition() else: self._reenter_state(obj, e) self._after_event(obj, e) fn.__name__ = str(event) fn.__doc__ = ( "Event handler for an {event} event. This event can be " "fired if the machine is in {states} states.".format( event=event, states=self._map[event]['src'])) return fn class _e_obj(object): pass @staticmethod def _is_base_string(object): # pragma: no cover try: return isinstance(object, basestring) # noqa except NameError: return isinstance(object, str) # noqa def _do_callbacks(self, obj, callbacks, *args, **kwargs): for cb in callbacks: if cb in self._callbacks: return self._callbacks[cb](*args, **kwargs) if hasattr(obj, cb): return getattr(obj, cb)(*args, **kwargs) def _check_condition(self, obj, func, target, e): if callable(func): return func(e) is target return self._do_callbacks(obj, [func], e) is target def _before_event(self, obj, e): callbacks = ['onbefore' + e.event, 'on_before_' + e.event] return self._do_callbacks(obj, callbacks, e) def _after_event(self, obj, e): callbacks = ['onafter' + e.event, 'on' + e.event, 'on_after_' + e.event, 'on_' + e.event] return self._do_callbacks(obj, callbacks, e) def _leave_state(self, obj, e): callbacks = ['onleave' + e.src, 'on_leave_' + e.src] return self._do_callbacks(obj, callbacks, e) def _enter_state(self, obj, e): callbacks = ['onenter' + e.dst, 'on' + e.dst, 'on_enter_' + e.dst, 'on_' + e.dst] return self._do_callbacks(obj, callbacks, e) def _reenter_state(self, obj, e): callbacks = ['onreenter' + e.dst, 'on_reenter_' + e.dst] return self._do_callbacks(obj, callbacks, e) def _change_state(self, obj, e): callbacks = ['onchangestate', 'on_change_state'] return self._do_callbacks(obj, callbacks, e) def current(self, obj): return getattr(obj, self.state_field) or 'none' def isstate(self, obj, state): return self.current(obj) == state is_state = isstate def can(self, obj, event): if event not in self._map or hasattr(obj, 'transition'): return False src = self._map[event]['src'] return self.current(obj) in src or WILDCARD in src def cannot(self, obj, event): return not self.can(obj, event) def is_finished(self, obj): return self._final and (self.current(obj) == self._final) def trigger(self, obj, event, *args, **kwargs): if not hasattr(self, event): raise FysomError( "There isn't any event registered as %s" % event) return getattr(self, event)(obj, *args, **kwargs) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631283387.511028 fysom-2.1.6/fysom.egg-info/0000755000175000017500000000000000000000000015027 5ustar00mriehlmriehl././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631283387.0 fysom-2.1.6/fysom.egg-info/PKG-INFO0000644000175000017500000000127600000000000016132 0ustar00mriehlmriehlMetadata-Version: 2.1 Name: fysom Version: 2.1.6 Summary: pYthOn Finite State Machine Home-page: https://github.com/mriehl/fysom Author: Mansour Behabadi, Jake Gordon, Maximilien Riehl, Stefano Author-email: mansour@oxplot.com, jake@codeincomplete.com, maximilien.riehl@gmail.com, unknown@domain.invalid Maintainer: Maintainer-email: License: MIT Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Scientific/Engineering UNKNOWN ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631283387.0 fysom-2.1.6/fysom.egg-info/SOURCES.txt0000644000175000017500000000103100000000000016706 0ustar00mriehlmriehlCHANGELOG MANIFEST.in README setup.cfg setup.py fysom/__init__.py fysom.egg-info/PKG-INFO fysom.egg-info/SOURCES.txt fysom.egg-info/dependency_links.txt fysom.egg-info/namespace_packages.txt fysom.egg-info/top_level.txt fysom.egg-info/zip-safe test/test_async_state_transition.py test/test_callbacks.py test/test_finished_state.py test/test_garbage_collection.py test/test_global_machine.py test/test_initialization.py test/test_many_to_many.py test/test_same_dst.py test/test_state.py test/test_unicode_literals.py test/test_wildcard.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631283387.0 fysom-2.1.6/fysom.egg-info/dependency_links.txt0000644000175000017500000000000100000000000021075 0ustar00mriehlmriehl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631283387.0 fysom-2.1.6/fysom.egg-info/namespace_packages.txt0000644000175000017500000000000100000000000021351 0ustar00mriehlmriehl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631283387.0 fysom-2.1.6/fysom.egg-info/top_level.txt0000644000175000017500000000000600000000000017555 0ustar00mriehlmriehlfysom ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282883.0 fysom-2.1.6/fysom.egg-info/zip-safe0000644000175000017500000000000100000000000016457 0ustar00mriehlmriehl ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631283387.511028 fysom-2.1.6/setup.cfg0000644000175000017500000000022100000000000014014 0ustar00mriehlmriehl[bdist_rpm] packager = Maximilien Riehl requires = python >= 2.6 release = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/setup.py0000755000175000017500000000331100000000000013713 0ustar00mriehlmriehl#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup from setuptools.command.install import install as _install class install(_install): def pre_install_script(self): pass def post_install_script(self): pass def run(self): self.pre_install_script() _install.run(self) self.post_install_script() if __name__ == '__main__': setup( name = 'fysom', version = '2.1.6', description = 'pYthOn Finite State Machine', long_description = '', long_description_content_type = None, classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Natural Language :: English', 'Operating System :: OS Independent', 'Topic :: Scientific/Engineering' ], keywords = '', author = 'Mansour Behabadi, Jake Gordon, Maximilien Riehl, Stefano', author_email = 'mansour@oxplot.com, jake@codeincomplete.com, maximilien.riehl@gmail.com, unknown@domain.invalid', maintainer = '', maintainer_email = '', license = 'MIT', url = 'https://github.com/mriehl/fysom', project_urls = {}, scripts = [], packages = ['fysom'], namespace_packages = [], py_modules = [], entry_points = {}, data_files = [], package_data = {}, install_requires = [], dependency_links = [], zip_safe = True, cmdclass = {'install': install}, python_requires = '', obsoletes = [], ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631283387.511028 fysom-2.1.6/test/0000755000175000017500000000000000000000000013157 5ustar00mriehlmriehl././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_async_state_transition.py0000644000175000017500000000571600000000000021370 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest from fysom import Fysom, FysomError class FysomAsynchronousStateTransitionTests(unittest.TestCase): def on_leave_foo(self, e): self.leave_foo_event = e return False def on_enter_bar(self, e): self.on_enter_bar_fired = True self.on_enter_bar_event = e def setUp(self): self.on_enter_bar_fired = False self.fsm = Fysom({ 'initial': 'foo', 'events': [ {'name': 'footobar', 'src': 'foo', 'dst': 'bar'}, {'name': 'bartobar', 'src': 'bar', 'dst': 'bar'}, ], 'callbacks': { 'onleavefoo': self.on_leave_foo, 'onenterbar': self.on_enter_bar, } }) def test_fsm_should_be_put_on_hold_when_onleave_state_returns_false(self): self.fsm.footobar(id=123) self.assertEqual(self.fsm.current, 'foo') self.assertTrue(hasattr(self, 'leave_foo_event'), 'Callback onleavefoo did not fire.') self.assertTrue(self.leave_foo_event is not None) self.assertEqual(self.leave_foo_event.id, 123) self.fsm.transition() self.assertEqual(self.fsm.current, 'bar') def test_onenter_state_should_not_fire_when_fsm_is_put_on_hold(self): self.fsm.footobar(id=123) self.assertFalse(self.on_enter_bar_fired) self.fsm.transition() self.assertTrue(self.on_enter_bar_fired) def test_should_raise_exception_upon_further_transitions_when_fsm_is_on_hold(self): self.fsm.footobar(id=123) self.assertRaises(FysomError, self.fsm.bartobar) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_callbacks.py0000644000175000017500000002402500000000000016512 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest from fysom import Fysom, Canceled class FysomRepeatedBeforeEventCallbackTests(unittest.TestCase): def setUp(self): self.fired = [] def record_event(event): self.fired.append(event.msg) return 42 self.fsm = Fysom({ 'initial': 'notcalled', 'events': [ {'name': 'multiple', 'src': 'notcalled', 'dst': 'called'}, {'name': 'multiple', 'src': 'called', 'dst': 'called'}, ], 'callbacks': { 'onbeforemultiple': record_event } }) def test_should_fire_onbefore_event_repeatedly(self): self.fsm.multiple(msg="first") self.fsm.multiple(msg="second") self.assertEqual(self.fired, ["first", "second"]) class FysomRepeatedAfterEventCallbackTests(unittest.TestCase): def setUp(self): self.fired = [] def record_event(event): self.fired.append(event.msg) return 42 self.fsm = Fysom({ 'initial': 'notcalled', 'events': [ {'name': 'multiple', 'src': 'notcalled', 'dst': 'called'}, {'name': 'multiple', 'src': 'called', 'dst': 'called'}, ], 'callbacks': { 'onaftermultiple': record_event } }) def test_should_fire_onafter_event_repeatedly(self): self.fsm.multiple(msg="first") self.fsm.multiple(msg="second") self.assertEqual(self.fired, ["first", "second"]) class FysomCallbackTests(unittest.TestCase): def before_foo(self, e): self.before_foo_event = e self.fired_callbacks.append('before_foo') def before_bar(self, e): self.before_bar_event = e self.fired_callbacks.append('before_bar') def before_wait(self, e): self.before_wait_event = e self.fired_callbacks.append('before_wait') return False def on_foo(self, e): self.foo_event = e self.fired_callbacks.append('after_foo') def on_bar(self, e): self.bar_event = e self.fired_callbacks.append('after_bar') def on_baz(self, e): raise ValueError('Baz-inga!') def on_enter_fooed(self, e): self.enter_fooed_event = e def on_enter_bared(self, e): self.enter_bared_event = e def on_reenter_fooed(self, e): self.reenter_fooed_event = e def on_reenter_bared(self, e): self.reenter_bared_event = e def on_leave_sleeping(self, e): self.leave_sleeping_event = e def on_leave_fooed(self, e): self.leave_fooed_event = e def setUp(self): self.fired_callbacks = [] self.fsm = Fysom({ 'initial': 'sleeping', 'events': [ {'name': 'foo', 'src': 'sleeping', 'dst': 'fooed'}, {'name': 'foo', 'src': 'fooed', 'dst': 'fooed'}, {'name': 'bar', 'src': 'fooed', 'dst': 'bared'}, {'name': 'bar', 'src': 'bared', 'dst': 'bared'}, {'name': 'baz', 'src': 'bared', 'dst': 'bazed'}, {'name': 'wait', 'src': 'sleeping', 'dst': 'waiting'} ], 'callbacks': { 'onfoo': self.on_foo, 'onbar': self.on_bar, 'onbaz': self.on_baz, 'onbeforefoo': self.before_foo, 'onbeforebar': self.before_bar, 'onbeforewait': self.before_wait, 'onenterfooed': self.on_enter_fooed, 'onenterbared': self.on_enter_bared, 'onreenterfooed': self.on_reenter_fooed, 'onreenterbared': self.on_reenter_bared, 'onleavesleeping': self.on_leave_sleeping, 'onleavefooed': self.on_leave_fooed } }) def test_onafter_event_callbacks_should_fire_with_keyword_arguments_when_events_occur(self): self.fsm.foo(attribute='test') self.assertTrue( hasattr(self, 'foo_event'), 'Callback on_foo did not fire.') self.assertTrue(self.foo_event is not None) self.assertEqual(self.foo_event.attribute, 'test') self.fsm.bar(id=123) self.assertTrue( hasattr(self, 'bar_event'), 'Callback on_bar did not fire.') self.assertTrue(self.bar_event is not None) self.assertEqual(self.bar_event.id, 123) def test_onafter_event_callbacks_raising_exceptions_should_not_be_eaten(self): self.fsm.foo() self.fsm.bar() self.assertRaises(ValueError, self.fsm.baz) def test_onbefore_event_callbacks_should_fire_before_onafter_callbacks_with_keyword_arguments_when_events_occur( self): self.fsm.foo(attribute='test') self.assertTrue( hasattr(self, 'before_foo_event'), 'Callback onbeforefoo did not fire.') self.assertTrue(self.before_foo_event is not None) self.assertEqual(self.before_foo_event.attribute, 'test') self.fsm.bar(id=123) self.assertTrue( hasattr(self, 'before_bar_event'), 'Callback onbeforebar did not fire.') self.assertTrue(self.before_bar_event is not None) self.assertEqual(self.before_bar_event.id, 123) self.assertEqual( ['before_foo', 'after_foo', 'before_bar', 'after_bar'], self.fired_callbacks) def test_fsm_cancel_transition_when_onbefore_event_callbacks_return_false(self): self.assertRaises(Canceled, self.fsm.wait) self.assertEqual(self.fsm.current, 'sleeping') def test_onenter_state_callbacks_should_fire_with_keyword_arguments_when_state_transitions_occur(self): self.fsm.foo(attribute='test') self.assertTrue( hasattr(self, 'enter_fooed_event'), 'Callback onenterfooed did not fire.') self.assertTrue(self.enter_fooed_event is not None) self.assertEqual(self.enter_fooed_event.attribute, 'test') self.fsm.bar(id=123) self.assertTrue( hasattr(self, 'enter_bared_event'), 'Callback onenterbared did not fire.') self.assertTrue(self.enter_bared_event is not None) self.assertEqual(self.enter_bared_event.id, 123) def test_onreenter_state_callbacks_should_fire_with_keyword_arguments_when_state_transitions_occur(self): self.fsm.foo(attribute='testfail') self.fsm.foo(attribute='testpass') self.assertTrue( hasattr(self, 'reenter_fooed_event'), 'Callback onreenterfooed did not fire.') self.assertTrue(self.reenter_fooed_event is not None) self.assertEqual(self.reenter_fooed_event.attribute, 'testpass') self.fsm.bar(id=1234) self.fsm.bar(id=4321) self.assertTrue( hasattr(self, 'reenter_bared_event'), 'Callback onreenterbared did not fire.') self.assertTrue(self.reenter_bared_event is not None) self.assertEqual(self.reenter_bared_event.id, 4321) def test_onleave_state_callbacks_should_fire_with_keyword_arguments_when_state_transitions_occur(self): self.fsm.foo(attribute='test') self.assertTrue(hasattr(self, 'leave_sleeping_event'), 'Callback onleavesleeping did not fire.') self.assertTrue(self.leave_sleeping_event is not None) self.assertEqual(self.leave_sleeping_event.attribute, 'test') self.fsm.bar(id=123) self.assertTrue( hasattr(self, 'leave_fooed_event'), 'Callback onleavefooed did not fire.') self.assertTrue(self.leave_fooed_event is not None) self.assertEqual(self.leave_fooed_event.id, 123) def test_onchangestate_should_fire_for_all_state_changes(self): def on_change_state(e): self.current_event = e fsm = Fysom({ 'initial': 'foo', 'events': [ {'name': 'footobar', 'src': 'foo', 'dst': 'bar'}, {'name': 'bartobaz', 'src': 'bar', 'dst': 'baz'}, ], 'callbacks': { 'onchangestate': on_change_state } }) fsm.footobar(id=123) self.assertEqual(self.current_event.event, 'footobar') self.assertEqual(self.current_event.src, 'foo') self.assertEqual(self.current_event.dst, 'bar') self.assertEqual(self.current_event.id, 123) self.assertTrue(self.current_event.fsm is fsm) fsm.bartobaz('positional', named_attribute='test') self.assertEqual(self.current_event.event, 'bartobaz') self.assertEqual(self.current_event.src, 'bar') self.assertEqual(self.current_event.dst, 'baz') self.assertEqual(self.current_event.named_attribute, 'test') self.assertEqual(self.current_event.args[0], 'positional') self.assertTrue(self.current_event.fsm is fsm) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_finished_state.py0000644000175000017500000000524600000000000017570 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest from fysom import Fysom class FysomFinishedStateTests(unittest.TestCase): def test_it_should_indicate_whether_fsm_in_finished_state(self): fsm = Fysom({ 'initial': 'green', 'final': 'red', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) self.assertFalse(fsm.is_finished()) fsm.warn() self.assertFalse(fsm.is_finished()) fsm.panic() self.assertTrue(fsm.is_finished()) def test_never_finished_if_final_is_unspecified(self): fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) self.assertFalse(fsm.is_finished()) fsm.warn() self.assertFalse(fsm.is_finished()) fsm.panic() self.assertFalse(fsm.is_finished()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_garbage_collection.py0000644000175000017500000001056500000000000020402 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest import gc from fysom import Fysom class FysomGarbageCollectionTests(unittest.TestCase): def test_should_not_create_circular_ref(self): class MyTestObject(object): def __init__(self): self._states = [] self._fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ], 'callbacks': { 'ongreen': self._on_green, 'onyellow': self._on_yellow, 'onred': self._on_red } }) def warn(self): self._fsm.warn() def panic(self): self._fsm.panic() def calm(self): self._fsm.calm() def clear(self): self._fsm.clear() def _on_green(self, *args, **kwargs): self._states.append('green') def _on_yellow(self, *args, **kwargs): self._states.append('yellow') def _on_red(self, *args, **kwargs): self._states.append('red') obj = MyTestObject() obj.warn() obj.clear() del obj self.assertEqual(list(filter(lambda o: isinstance(o, MyTestObject), gc.get_objects())), []) def test_gc_should_not_break_callback(self): class MyTestObject(object): def __init__(self): self._states = [] self._fsm = None def warn(self): self._fsm.warn() def panic(self): self._fsm.panic() def calm(self): self._fsm.calm() def clear(self): self._fsm.clear() def _on_green(self, *args, **kwargs): self._states.append('green') def _on_yellow(self, *args, **kwargs): self._states.append('yellow') def _on_red(self, *args, **kwargs): self._states.append('red') obj = MyTestObject() fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ], 'callbacks': { 'ongreen': obj._on_green, 'onyellow': obj._on_yellow, 'onred': obj._on_red } }) obj._fsm = fsm obj.warn() obj.clear() del obj fsm.warn() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_global_machine.py0000644000175000017500000003207700000000000017525 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest from fysom import FysomError, Canceled, FysomGlobal, FysomGlobalMixin class FysomGlobalTests(unittest.TestCase): def setUp(self): self.GSM = FysomGlobal( events=[('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" {True: 'is_very_angry', 'else': 'yellow'} ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial='green', final='red', state_field='state' ) class BaseModel(object): def __init__(self): self.state = None self.can_angry = True self.can_very_angry = False self.logs = [] def is_angry(self, event): return self.can_angry def is_very_angry(self, event): return self.can_very_angry def on_change_state(self, event): self.logs.append('on_change_state') def check_true(self, event): return True def check_false(self, event): return False for _state in ('green', 'yellow', 'red'): for _at in ('enter', 'reenter', 'leave'): attr_name = 'on_%s_%s' % (_at, _state) def f(obj, event, attr_name=attr_name): obj.logs.append(attr_name) setattr(BaseModel, attr_name, f) for _event in ('warn', 'panic', 'calm', 'clear'): for _at in ('before', 'after'): attr_name = 'on_%s_%s' % (_at, _event) def f(obj, event, attr_name=attr_name): obj.logs.append(attr_name) setattr(BaseModel, attr_name, f) self.BaseModel = BaseModel class MixinModel(FysomGlobalMixin, BaseModel): GSM = self.GSM self.MixinModel = MixinModel def test_no_mixin_initial_state(self): obj = self.BaseModel() self.assertTrue(self.GSM.isstate(obj, 'none')) self.assertTrue(self.GSM.is_state(obj, 'none')) def test_mixin_initial_state(self): obj = self.MixinModel() self.assertTrue(obj.isstate('green')) self.assertTrue(obj.is_state('green')) def test_mixin_initial_event(self): obj = self.MixinModel() self.assertEqual(obj.logs, ['on_enter_green', 'on_change_state']) def test_mixin_current_property(self): obj = self.MixinModel() self.assertEqual(obj.current, 'green') obj.current = 'yellow' self.assertEqual(obj.current, 'yellow') def test_is_finished(self): obj = self.MixinModel() obj.can_very_angry = True obj.panic() self.assertTrue(obj.is_finished()) def test_valid_transition_is_allowed(self): obj = self.MixinModel() self.assertTrue(obj.is_state('green')) obj.warn() self.assertTrue(obj.is_state('yellow')) def test_invalid_transition_is_not_allowed(self): obj = self.MixinModel() self.assertTrue(obj.is_state('green')) obj.can_very_angry = True obj.warn() self.assertTrue(obj.is_state('yellow')) obj.panic() self.assertTrue(obj.is_state('red')) self.assertRaises(FysomError, obj.clear) def tests_callbacks_order(self): obj = self.MixinModel() obj.logs = [] obj.warn() self.assertEqual(obj.logs, ['on_before_warn', 'on_leave_green', 'on_enter_yellow', 'on_change_state', 'on_after_warn']) obj.can_angry = False obj.logs = [] self.assertRaises(Canceled, obj.panic) self.assertEqual(obj.logs, []) self.assertTrue(obj.is_state('yellow')) obj.can_angry = True obj.can_very_angry = False obj.panic() self.assertEqual(obj.logs, ['on_before_panic', 'on_reenter_yellow', 'on_after_panic']) def test_ok_condition_passed(self): obj = self.MixinModel() obj.can_very_angry = True obj.panic() self.assertTrue(obj.is_state('red')) def test_not_ok_condition_rejected(self): obj = self.MixinModel() obj.can_angry = False self.assertRaises(Canceled, obj.panic) self.assertTrue(obj.is_state('green')) def test_conditional_transition_passed(self): obj = self.MixinModel() self.assertTrue(obj.is_state('green')) obj.panic() self.assertFalse(obj.is_state('red')) self.assertTrue(obj.is_state('yellow')) def test_canceled_exception_with_event(self): obj = self.MixinModel() obj.can_angry = False self.assertRaises(Canceled, obj.panic) try: obj.panic() except Canceled as err: self.assertTrue(hasattr(err, 'event')) exc_event = err.event self.assertTrue(hasattr(exc_event, 'fsm')) self.assertTrue(hasattr(exc_event, 'obj')) self.assertTrue(hasattr(exc_event, 'src')) self.assertEqual(exc_event.src, 'green') self.assertTrue(hasattr(exc_event, 'dst')) self.assertEqual(exc_event.dst, 'red') self.assertTrue(hasattr(exc_event, 'args')) self.assertTrue(hasattr(exc_event, 'kwargs')) def test_trigger_works(self): obj = self.MixinModel() obj.trigger('warn') self.assertEqual(obj.current, 'yellow') self.assertRaises(FysomError, obj.trigger, 'unknown_event') self.assertEqual(obj.current, 'yellow') def test_no_state_field_specified(self): def _t(): gsm = FysomGlobal(events=[]) self.assertRaises(FysomError, _t) def test_cfg_parameter(self): gsm = FysomGlobal( cfg={ 'events': [ ('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" {True: 'is_very_angry', 'else': 'yellow'} ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')]}, initial='green', final='red', state_field='state' ) self.assertTrue(hasattr(gsm, 'warn')) self.assertTrue(hasattr(gsm, 'panic')) self.assertTrue(hasattr(gsm, 'calm')) self.assertTrue(hasattr(gsm, 'clear')) def test_manual_startup(self): gsm = FysomGlobal( events=[ ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial='red', state_field='state' ) obj = self.BaseModel() self.assertTrue(gsm.is_state(obj, 'none')) self.assertTrue(hasattr(gsm, 'startup')) gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) def test_manual_startup_event_name(self): gsm = FysomGlobal( events=[ ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial={'state': 'red', 'event': 'my_startup'}, state_field='state' ) obj = self.BaseModel() self.assertTrue(gsm.is_state(obj, 'none')) self.assertTrue(hasattr(gsm, 'my_startup')) gsm.my_startup(obj) self.assertTrue(gsm.current(obj) == 'red') def test_function_callback(self): def _func(event): event.obj.logs.append('function_callback') gsm = FysomGlobal( events=[ ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_after_clear': _func}, initial='red', state_field='state' ) obj = self.BaseModel() gsm.startup(obj) gsm.calm(obj) gsm.clear(obj) self.assertTrue('function_callback' in obj.logs) def test_asynchronous_transition(self): def _func(event): event.obj.logs.append('function_callback') def on_leave_red(event): return False gsm = FysomGlobal( events=[ ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_leave_red': on_leave_red, 'on_after_calm': _func}, initial='red', state_field='state' ) obj = self.BaseModel() gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'red')) self.assertTrue(hasattr(obj, 'transition')) self.assertFalse('functioon_callback' in obj.logs) obj.transition() self.assertTrue(gsm.is_state(obj, 'yellow')) self.assertFalse(hasattr(obj, 'transition')) self.assertTrue('function_callback' in obj.logs) def test_transition_with_args_kwargs(self): def _func(event): self.assertTrue(hasattr(event, 'args')) self.assertTrue(hasattr(event, 'kwargs')) self.assertTrue(hasattr(event, 'msg')) event.obj.logs.append('function_callback') gsm = FysomGlobal( events=[ ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_after_startup': _func}, initial='red', state_field='state' ) obj = self.BaseModel() gsm.startup(obj, msg='msg') self.assertTrue('function_callback' in obj.logs) def test_canceled_before_event_false(self): gsm = FysomGlobal( events=[ ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_before_calm': lambda e: False}, initial='red', state_field='state' ) obj = self.BaseModel() gsm.startup(obj) self.assertRaises(Canceled, gsm.calm, obj) def test_callable_or_basestring_condition(self): gsm = FysomGlobal( events=[ { 'name': 'calm', 'src': 'red', 'dst': 'yellow', 'cond': lambda e: True, }, { 'name': 'clear', 'src': 'yellow', 'dst': 'green', 'cond': 'check_false' }], initial='red', state_field='state' ) obj = self.BaseModel() gsm.startup(obj) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'yellow')) self.assertRaises(Canceled, gsm.clear, obj) self.assertTrue(gsm.is_state(obj, 'yellow')) def test_unknown_event(self): obj = self.MixinModel() self.assertFalse(obj.can('unknown_event')) self.assertTrue(self.GSM.cannot(obj, 'unknown_event')) def test_wildcard_src(self): gsm = FysomGlobal( events=[{'name': 'calm', 'dst': 'yellow'}], initial='red', state_field='state' ) obj = self.BaseModel() gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'yellow')) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_initialization.py0000644000175000017500000001247200000000000017625 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest from fysom import Fysom class FysomInitializationTests(unittest.TestCase): def test_should_have_no_state_when_no_initial_state_is_given(self): fsm = Fysom({ 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) self.assertEqual(fsm.current, 'none') def test_initial_state_should_be_green_when_configured(self): fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) self.assertEqual(fsm.current, 'green') def test_initial_state_should_work_with_different_event_name(self): fsm = Fysom({ 'initial': {'state': 'green', 'event': 'init'}, 'events': [ {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}, ] }) self.assertEquals(fsm.current, 'green') def test_deferred_initial_state_should_be_none_then_state(self): fsm = Fysom({ 'initial': {'state': 'green', 'event': 'init', 'defer': True}, 'events': [ {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}, ] }) self.assertEqual(fsm.current, 'none') fsm.init() self.assertEqual(fsm.current, 'green') def test_tuples_as_trasition_spec(self): fsm = Fysom({ 'initial': 'green', 'events': [ # freely mix dicts and tuples {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, ('panic', 'yellow', 'red'), ('calm', 'red', 'yellow'), {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) fsm.warn() fsm.panic() self.assertEqual(fsm.current, 'red') fsm.calm() fsm.clear() self.assertEqual(fsm.current, 'green') def test_kwargs_override_cfg(self): fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'panic', 'src': 'green', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'green'}, ]}, # override initial state and calm event initial='red', events=[('calm', 'red', 'black')]) self.assertEqual(fsm.current, "red") fsm.calm() self.assertEqual(fsm.current, "black") def test_init_kwargs_only(self): fsm = Fysom(initial='green', events=[('panic', 'green', 'red'), ('calm', 'red', 'green')]) self.assertEqual(fsm.current, "green") fsm.panic() self.assertEqual(fsm.current, "red") fsm.calm() self.assertEqual(fsm.current, "green") def test_final_kwarg(self): fsm = Fysom(initial='eternity', final='eternity') self.assertEqual(fsm.current, 'eternity') self.assertEqual(fsm.is_finished(), True) def test_callbacks_kwarg(self): history = [] def ontic(e): history.append('tic') def ontoc(e): history.append('toc') fsm = Fysom(initial='left', events=[('tic', 'left', 'right'), ('toc', 'right', 'left')], callbacks={'ontic': ontic, 'ontoc': ontoc}) fsm.tic() fsm.toc() fsm.tic() fsm.toc() self.assertEqual(history, ['tic', 'toc', 'tic', 'toc']) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_many_to_many.py0000644000175000017500000000605100000000000017264 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest import copy from fysom import Fysom class FysomManyToManyTransitionTests(unittest.TestCase): fsm_descr = { 'initial': 'hungry', 'events': [ {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, {'name': 'eat', 'src': 'full', 'dst': 'sick'}, {'name': 'rest', 'src': ['hungry', 'satisfied', 'full', 'sick'], 'dst': 'hungry'} ] } def _get_descr(self, initial=None): mycopy = copy.deepcopy(self.fsm_descr) if initial: mycopy['initial'] = initial return mycopy def test_rest_should_always_transition_to_hungry_state(self): fsm = Fysom(self.fsm_descr) fsm.rest() self.assertEquals(fsm.current, 'hungry') fsm = Fysom(self._get_descr('satisfied')) fsm.rest() self.assertEquals(fsm.current, 'hungry') fsm = Fysom(self._get_descr('full')) fsm.rest() self.assertEquals(fsm.current, 'hungry') fsm = Fysom(self._get_descr('sick')) fsm.rest() self.assertEquals(fsm.current, 'hungry') def test_eat_should_transition_to_satisfied_when_hungry(self): fsm = Fysom(self._get_descr('hungry')) fsm.eat() self.assertEqual(fsm.current, 'satisfied') def test_eat_should_transition_to_full_when_satisfied(self): fsm = Fysom(self._get_descr('satisfied')) fsm.eat() self.assertEqual(fsm.current, 'full') def test_eat_should_transition_to_sick_when_full(self): fsm = Fysom(self._get_descr('full')) fsm.eat() self.assertEqual(fsm.current, 'sick') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_same_dst.py0000644000175000017500000000615400000000000016375 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest from fysom import Fysom class FysomSameDstTransitionTests(unittest.TestCase): def test_if_src_not_specified_then_is_wildcard(self): fsm = Fysom({ 'initial': 'hungry', 'events': [ {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, {'name': 'eat', 'src': 'full', 'dst': 'sick'}, {'name': 'eat', 'src': 'sick', 'dst': '='}, {'name': 'rest', 'src': '*', 'dst': 'hungry'}, {'name': 'walk', 'src': '*', 'dst': '='}, {'name': 'run', 'src': ['hungry', 'sick'], 'dst': '='}, {'name': 'run', 'src': 'satisfied', 'dst': 'hungry'}, {'name': 'run', 'src': 'full', 'dst': 'satisfied'} ] }) fsm.walk() self.assertEqual(fsm.current, 'hungry') fsm.run() self.assertEqual(fsm.current, 'hungry') fsm.eat() self.assertEqual(fsm.current, 'satisfied') fsm.walk() self.assertEqual(fsm.current, 'satisfied') fsm.run() self.assertEqual(fsm.current, 'hungry') fsm.eat() self.assertEqual(fsm.current, 'satisfied') fsm.eat() self.assertEqual(fsm.current, 'full') fsm.walk() self.assertEqual(fsm.current, 'full') fsm.eat() self.assertEqual(fsm.current, 'sick') fsm.walk() self.assertEqual(fsm.current, 'sick') fsm.run() self.assertEqual(fsm.current, 'sick') fsm.rest() self.assertEqual(fsm.current, 'hungry') fsm.eat() self.assertEqual(fsm.current, 'satisfied') fsm.run() self.assertEqual(fsm.current, 'hungry') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_state.py0000644000175000017500000001025000000000000015706 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import unittest from fysom import Fysom, FysomError class FysomStateTests(unittest.TestCase): def setUp(self): self.fsm = Fysom({ 'initial': 'green', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'}, {'name': 'warm', 'src': 'green', 'dst': 'blue'} ] }) def test_is_state_should_succeed_for_initial_state(self): self.assertTrue(self.fsm.isstate('green')) def test_identity_transition_should_not_be_allowed_by_default(self): self.assertFalse(self.fsm.can('clear')) self.assertTrue(self.fsm.cannot('clear')) def test_configured_transition_should_work(self): self.assertTrue(self.fsm.can('warn')) def test_transition_should_change_state(self): self.fsm.warn() self.assertTrue(self.fsm.isstate('yellow')) def test_should_raise_exception_when_state_transition_is_not_allowed(self): self.assertRaises(FysomError, self.fsm.panic) self.assertRaises(FysomError, self.fsm.calm) self.assertRaises(FysomError, self.fsm.clear) def test_event_handler_has_name_and_docstring(self): self.assertEqual(self.fsm.warm.__name__, "warm", "Event handlers do not have appropriate name.") self.assertNotEqual(self.fsm.warm.__name__, None, "Docstring for event handler is None!") def test_trigger_should_trigger_the_event_handler(self): self.assertEqual(self.fsm.current, "green", "The initial state isn't the expected state.") self.fsm.trigger("warm") self.assertRaises(FysomError, self.fsm.trigger, "unknown_event") self.assertEqual(self.fsm.current, "blue", "The initial state isn't the expected state.") def test_trigger_should_trigger_the_event_handler_with_args(self): self.assertEqual(self.fsm.current, "green", "The initial state isn't the expected state.") def onblue(event): self.assertEqual(event.args, ("any-positional-argument",)) self.fsm.onblue = onblue self.fsm.trigger("warm", "any-positional-argument") self.assertEqual(self.fsm.current, "blue", "The initial state isn't the expected state.") def test_trigger_should_trigger_the_event_handler_with_kwargs(self): self.assertEqual(self.fsm.current, "green", "The initial state isn't the expected state.") def onblue(event): self.assertEqual(event.keyword_argument, "any-value") self.fsm.onblue = onblue self.fsm.trigger("warm", keyword_argument="any-value") self.assertEqual(self.fsm.current, "blue", "The initial state isn't the expected state.") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_unicode_literals.py0000644000175000017500000000412400000000000020116 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # from __future__ import unicode_literals import unittest from fysom import Fysom class FysomUnicodeLiterals(unittest.TestCase): def test_it_doesnt_break_with_unicode_literals(self): try: fsm = Fysom({ 'initial': 'green', 'final': 'red', 'events': [ {'name': 'warn', 'src': 'green', 'dst': 'yellow'}, {'name': 'panic', 'src': 'yellow', 'dst': 'red'}, {'name': 'calm', 'src': 'red', 'dst': 'yellow'}, {'name': 'clear', 'src': 'yellow', 'dst': 'green'} ] }) self.assertTrue(True) except TypeError: self.assertTrue(False) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631282855.0 fysom-2.1.6/test/test_wildcard.py0000644000175000017500000000503000000000000016357 0ustar00mriehlmriehl# coding=utf-8 # # fysom - pYthOn Finite State Machine - this is a port of Jake # Gordon's javascript-state-machine to python # https://github.com/jakesgordon/javascript-state-machine # # Copyright (C) 2011 Mansour Behabadi , Jake Gordon # and other contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # from fysom import Fysom from test_many_to_many import FysomManyToManyTransitionTests class FysomWildcardTransitionTests(FysomManyToManyTransitionTests): fsm_descr = { 'initial': 'hungry', 'events': [ {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, {'name': 'eat', 'src': 'full', 'dst': 'sick'}, {'name': 'rest', 'src': '*', 'dst': 'hungry'} ] } def test_if_src_not_specified_then_is_wildcard(self): fsm = Fysom({ 'initial': 'hungry', 'events': [ {'name': 'eat', 'src': 'hungry', 'dst': 'satisfied'}, {'name': 'eat', 'src': 'satisfied', 'dst': 'full'}, {'name': 'eat', 'src': 'full', 'dst': 'sick'}, {'name': 'rest', 'dst': 'hungry'} ] }) fsm.eat() self.assertEqual(fsm.current, 'satisfied') fsm.rest() self.assertEqual(fsm.current, 'hungry') fsm.eat() fsm.eat() self.assertEqual(fsm.current, 'full') fsm.rest() self.assertEqual(fsm.current, 'hungry')