Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #647: Process all states in order in may_trigger #675

Merged
merged 1 commit into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 0.9.2 ()

- Bug #610: Decorate models appropriately when `HierarchicalMachine` is passed to `add_state` (thanks @e0lithic)
- Bug #647: Let `may_<trigger>` check all parallel states in processing order (thanks @spearsear)
- Bug: `HSM.is_state` works with parallel states now

## 0.9.1 (May 2024)

Expand Down
47 changes: 47 additions & 0 deletions tests/test_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,53 @@ def test_model_state_conversion(self):
self.assertEqual(tree, m.build_state_tree(states, sep))
self.assertEqual(states, _build_state_list(tree, sep))

def test_may_transition_with_parallel(self):
states = ['A',
{'name': 'P',
'parallel': [
{'name': '1', 'states': ["a", "b"], "initial": 'a'},
{'name': '2', 'states': ["a", "b"], "initial": 'a',
'transitions': [['valid', 'a', 'b']]}
]}]
m = self.machine_cls(states=states, initial="A")
assert not m.may_valid()
assert m.to_P()
assert m.is_P(allow_substates=True)
assert m.is_P_1_a()
assert m.is_P_2_a()
assert m.may_valid()
assert m.valid()
assert m.is_P_1_a()
assert not m.is_P_2_a()
assert m.is_P_2_b()

def test_is_state_parallel(self):
states = ['A',
{'name': 'P',
'parallel': [
'1',
{'name': '2', 'parallel': [
{'name': 'a'},
{'name': 'b', 'parallel': [
{'name': 'x', 'parallel': ['1', '2']}, 'y'
]}
]},
]}]
m = self.machine_cls(states=states, initial="A")
assert m.is_A()
assert not m.is_P_2()
assert not m.is_P_2_a()
assert not m.is_P_2_b()
assert not m.is_P_2_b_x()
assert not m.is_P(allow_substates=True)
m.to_P()
assert m.is_P_1()
assert m.is_P_2_a()
assert not m.is_P_2()
assert m.is_P(allow_substates=True)
assert m.is_P_2(allow_substates=True)
assert not m.is_A(allow_substates=True)


@skipIf(pgv is None, "pygraphviz is not available")
class TestParallelWithPyGraphviz(TestParallel):
Expand Down
25 changes: 14 additions & 11 deletions transitions/extensions/nesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -759,9 +759,11 @@ def remove_transition(self, trigger, source="*", dest="*"):
def _can_trigger(self, model, trigger, *args, **kwargs):
state_tree = self.build_state_tree(getattr(model, self.model_attribute), self.state_cls.separator)
ordered_states = resolve_order(state_tree)
for state_path in ordered_states:
with self():
return self._can_trigger_nested(model, trigger, state_path, *args, **kwargs)
with self():
return any(
self._can_trigger_nested(model, trigger, state_path, *args, **kwargs)
for state_path in ordered_states
)

def _can_trigger_nested(self, model, trigger, path, *args, **kwargs):
if trigger in self.events:
Expand Down Expand Up @@ -822,14 +824,15 @@ def has_trigger(self, trigger, state=None):
return trigger in state.events or any(self.has_trigger(trigger, sta) for sta in state.states.values())

def is_state(self, state, model, allow_substates=False):
if allow_substates:
current = getattr(model, self.model_attribute)
current_name = self.state_cls.separator.join(self._get_enum_path(current))\
if isinstance(current, Enum) else current
state_name = self.state_cls.separator.join(self._get_enum_path(state))\
if isinstance(state, Enum) else state
return current_name.startswith(state_name)
return getattr(model, self.model_attribute) == state
tree = self.build_state_tree(listify(getattr(model, self.model_attribute)),
self.state_cls.separator)

path = self._get_enum_path(state) if isinstance(state, Enum) else state.split(self.state_cls.separator)
for elem in path:
if elem not in tree:
return False
tree = tree[elem]
return len(tree) == 0 or allow_substates

def on_enter(self, state_name, callback):
"""Helper function to add callbacks to states in case a custom state separator is used.
Expand Down