Skip to content

Commit

Permalink
Allow multiple target states by calling back
Browse files Browse the repository at this point in the history
This should, however be used in very rare cases and transitions to single target
states be preferred.
  • Loading branch information
Matthias Vogelgesang committed Feb 7, 2014
1 parent 72e5ade commit 6d23ff3
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 4 deletions.
22 changes: 18 additions & 4 deletions concert/fsm.py
Expand Up @@ -5,7 +5,11 @@


class TransitionNotAllowed(Exception):
pass
def __init__(self, source, target):
self.value = "Invalid transition from {} to {}".format(source, target)

def __str__(self):
return repr(self.value)


class Error(Exception):
Expand Down Expand Up @@ -96,6 +100,7 @@ def __get__(self, instance, owner):


class Meta(object):

def __init__(self):
self.transitions = defaultdict()
self.state_name = None
Expand Down Expand Up @@ -127,9 +132,18 @@ def do_transition(self, instance, target):
except KeyError:
next_state = self.transitions.get('*')

if current is None or next_state != target:
msg = "Invalid transition from {} to {}".format(current, target)
raise TransitionNotAllowed(msg)
if isinstance(next_state, list):
if not hasattr(instance, 'get_real_state'):
raise TypeError("{} must implement `get_real_state'".format(instance))

# Override target with actual, current device state
target = instance.get_real_state()

if target not in next_state:
raise TransitionNotAllowed(device_state, next_state)
else:
if current is None or next_state != target:
raise TransitionNotAllowed(current, target)

attr = self.get_state_attribute(instance)
attr._set_value(target)
Expand Down
14 changes: 14 additions & 0 deletions concert/tests/unit/test_fsm.py
Expand Up @@ -18,6 +18,9 @@ def __init__(self):
super(SomeDevice, self).__init__()
self.velocity = STOP_VELOCITY

def get_real_state(self):
return 'standby' if self.velocity == STOP_VELOCITY else 'moving'

@transition(source='standby', target='moving')
def start_moving(self, velocity):
self.velocity = velocity
Expand All @@ -37,6 +40,10 @@ def move_some_time(self, velocity, duration):
def stop_no_matter_what(self):
self.velocity = STOP_VELOCITY

@transition(source='*', target=['standby', 'moving'])
def set_velocity(self, velocity):
self.velocity = velocity

@transition(source='*')
def cause_erroneous_behaviour(self, msg):
raise Error(msg, self._fix_problem)
Expand Down Expand Up @@ -84,6 +91,13 @@ def test_multiple_source_states(self):

self.assertTrue(self.device.state.is_currently('standby'))

def test_multiple_target_states(self):
self.device.set_velocity(MOVE_VELOCITY)
self.assertTrue(self.device.state.is_currently('moving'))

self.device.set_velocity(STOP_VELOCITY)
self.assertTrue(self.device.state.is_currently('standby'))

def test_error(self):
with self.assertRaises(RuntimeError):
self.device.cause_erroneous_behaviour("Oops")
Expand Down

0 comments on commit 6d23ff3

Please sign in to comment.