Skip to content

Commit

Permalink
This fixes #544
Browse files Browse the repository at this point in the history
also adjusted some docstrings.
  • Loading branch information
aleneum committed Sep 2, 2021
1 parent bce3917 commit 3e61917
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 10 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.8.9

- Bugfix #544: `NestedEvent` now wraps the machine's scope into partials passed to `HierarchicalMachine._process`. This prevents queued transitions from losing their scope.
- Feature #533: `(A)Graph.draw` function (object returned by `GraphMachine.get_graph()`) can be passed a file/stream object as first parameter or `None`. The later will result in `draw` returning a binary string. (thanks @Blindfreddy).
- Feature #532: Use id(model) instead of model for machine-bound caches in `LockedMachine`, `AsyncMachine` and `GraphMachine`. This might influence pickling (thanks @thedrow).

Expand Down
10 changes: 10 additions & 0 deletions tests/test_nesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,16 @@ class CorrectStateClass(self.machine_cls):
m = WrongStateClass()
m = CorrectStateClass()

def test_queued_callbacks(self):
states = [
"initial",
{'name': 'A', 'children': [{'name': '1', 'on_enter': 'go'}, '2'],
'transitions': [['go', '1', '2']], 'initial': '1'}
]
machine = self.machine_cls(states=states, initial='initial', queued=True)
machine.to_A()
self.assertEqual("A{0}2".format(self.state_cls.separator), machine.state)


class TestSeparatorsBase(TestCase):

Expand Down
3 changes: 3 additions & 0 deletions tests/test_nesting_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ def test_get_nested_transitions(self):
def test_correct_subclassing(self):
pass # not supported by legacy machine

def test_queued_callbacks(self):
pass # not supported by legacy machine


class TestReuseLegacySeparatorDefault(TestReuseSeparatorBase):

Expand Down
17 changes: 14 additions & 3 deletions transitions/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,13 @@ def add_transition(self, transition):
self.transitions[transition.source].append(transition)

def trigger(self, model, *args, **kwargs):
""" Serially execute all transitions that match the current state,
halting as soon as one successfully completes.
""" Executes all transitions that match the current state,
halting as soon as one successfully completes. More precisely, it prepares a partial
of the internal ``_trigger`` function, passes this to ``Machine._process``.
It is up to the machine's configuration of the Event whether processing happens queued (sequentially) or
whether further Events are processed as they occur.
Args:
model (object): The currently processed model
args and kwargs: Optional positional or named arguments that will
be passed onto the EventData object, enabling arbitrary state
information to be passed on to downstream triggered functions.
Expand All @@ -398,7 +402,14 @@ def trigger(self, model, *args, **kwargs):

def _trigger(self, model, *args, **kwargs):
""" Internal trigger function called by the ``Machine`` instance. This should not
be called directly but via the public method ``Machine.trigger``.
be called directly but via the public method ``Machine.process``.
Args:
model (object): The currently processed model
args and kwargs: Optional positional or named arguments that will
be passed onto the EventData object, enabling arbitrary state
information to be passed on to downstream triggered functions.
Returns: boolean indicating whether or not a transition was
successfully executed (True if successful, False if not).
"""
state = self.machine.get_model_state(model)
if state.name not in self.transitions:
Expand Down
52 changes: 45 additions & 7 deletions transitions/extensions/nesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,27 +91,64 @@ class NestedEvent(Event):
"""

def trigger(self, _model, _machine, *args, **kwargs):
""" Serially execute all transitions that match the current state,
halting as soon as one successfully completes. NOTE: This should only
""" Executes all transitions that match the current state,
halting as soon as one successfully completes. More precisely, it prepares a partial
of the internal ``_trigger`` function, passes this to ``machine._process``.
It is up to the machine's configuration of the Event whether processing happens queued (sequentially) or
whether further Events are processed as they occur. NOTE: This should only
be called by HierarchicalMachine instances.
Args:
_model (object): model object to
_model (object): The currently processed model
_machine (HierarchicalMachine): Since NestedEvents can be used in multiple machine instances, this one
will be used to determine the current state separator.
will be used to determine the current state separator and the current scope.
args and kwargs: Optional positional or named arguments that will
be passed onto the EventData object, enabling arbitrary state
information to be passed on to downstream triggered functions.
Returns: boolean indicating whether or not a transition was
successfully executed (True if successful, False if not).
"""
func = partial(self._trigger, _model, _machine, *args, **kwargs)
# Save the current scope (_machine.scoped, _machine.states, _machine.events) in partial
# since queued transitions could otherwise loose their scope.
func = partial(self._trigger, _model, _machine, (_machine.scoped, _machine.states, _machine.events),
*args, **kwargs)
# pylint: disable=protected-access
# noinspection PyProtectedMember
# Machine._process should not be called somewhere else. That's why it should not be exposed
# to Machine users.
return _machine._process(func)

def _trigger(self, _model, _machine, *args, **kwargs):
def _trigger(self, _model, _machine, _scope, *args, **kwargs):
""" Internal trigger function called by the ``HierarchicalMachine`` instance. This should not
be called directly but via the public method ``HierarchicalMachine.process``. In contrast to
the inherited ``Event._trigger``, this requires a scope tuple to process triggers in the right context.
Args:
_model (object): The currently processed model
_machine (HierarchicalMachine): The machine that should be used to process the event
_scope (Tuple): A tuple containing information about the currently scoped object, states an transitions.
args and kwargs: Optional positional or named arguments that will
be passed onto the EventData object, enabling arbitrary state
information to be passed on to downstream triggered functions.
Returns: boolean indicating whether or not a transition was
successfully executed (True if successful, False if not).
"""
if _scope[0] != _machine.scoped:
with _machine(_scope):
return self._trigger_scoped(_model, _machine, *args, **kwargs)
else:
return self._trigger_scoped(_model, _machine, *args, **kwargs)

def _trigger_scoped(self, _model, _machine, *args, **kwargs):
""" Internal scope-adjusted trigger function called by the ``NestedEvent._trigger`` instance. This should not
be called directly.
Args:
_model (object): The currently processed model
_machine (HierarchicalMachine): The machine that should be used to process the event
args and kwargs: Optional positional or named arguments that will
be passed onto the EventData object, enabling arbitrary state
information to be passed on to downstream triggered functions.
Returns: boolean indicating whether or not a transition was
successfully executed (True if successful, False if not).
"""
state_tree = _machine._build_state_tree(getattr(_model, _machine.model_attribute), _machine.state_cls.separator)
state_tree = reduce(dict.get, _machine.get_global_name(join=False), state_tree)
ordered_states = _resolve_order(state_tree)
Expand Down Expand Up @@ -575,7 +612,8 @@ def add_transition(self, trigger, source, dest, conditions=None,
_super(HierarchicalMachine, self).add_transition(trigger, source, dest, conditions,
unless, before, after, prepare, **kwargs)

def get_global_name(self, state=None, join=True):
def get_global_name(self, state=None, join=True, scope=None):
scope = scope or self
local_stack = [s[0] for s in self._stack] + [self.scoped]
local_stack_start = len(local_stack) - local_stack[::-1].index(self)
domains = [s.name for s in local_stack[local_stack_start:]]
Expand Down

0 comments on commit 3e61917

Please sign in to comment.