Skip to content

Commit deb84b2

Browse files
committed
fix: Fix some edge cases of initial state configuration
1 parent d2a002a commit deb84b2

11 files changed

Lines changed: 36 additions & 181 deletions

File tree

statemachine/engines/base.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -602,10 +602,7 @@ def add_descendant_states_to_enter(
602602
)
603603
elif state and state.is_compound:
604604
states_for_default_entry.add(info)
605-
initial_state = next(s for s in state.states if s.initial)
606-
transition = next(
607-
t for t in state.transitions if t.initial and t.target == initial_state
608-
)
605+
transition = next(t for t in state.transitions if t.initial)
609606
info_initial = StateTransition(
610607
transition=transition,
611608
target=transition.target,

statemachine/factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ def _initials_by_document_order(
9595
for s in states:
9696
s.document_order = order
9797
order += 1
98-
cls._initials_by_document_order(s.states, s, order)
98+
if s.states:
99+
cls._initials_by_document_order(s.states, s, order)
99100
if s.initial:
100101
initial = s
101102
if not initial and states:

statemachine/io/scxml/actions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from .schema import ScriptAction
2828

2929
logger = logging.getLogger(__name__)
30-
protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name"}
30+
protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name", "_event"}
3131

3232

3333
class ParseTime:

statemachine/io/scxml/parser.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import xml.etree.ElementTree as ET
3+
from typing import Iterable
34
from typing import Set
45

56
from .schema import Action
@@ -33,13 +34,20 @@ def strip_namespaces(tree: ET.Element):
3334
attrib[new_name] = attrib.pop(name)
3435

3536

37+
def visit_states(states: Iterable[State], parents: list[State]):
38+
for state in states:
39+
yield state, parents
40+
if state.states:
41+
yield from visit_states(state.states.values(), parents + [state])
42+
43+
3644
def _parse_initial(initial_content: "str | None") -> Set[str]:
3745
if initial_content is None:
3846
return set()
3947
return set(initial_content.split())
4048

4149

42-
def parse_scxml(scxml_content: str) -> StateMachineDefinition:
50+
def parse_scxml(scxml_content: str) -> StateMachineDefinition: # noqa: C901
4351
root = ET.fromstring(scxml_content)
4452
strip_namespaces(root)
4553

@@ -75,6 +83,21 @@ def parse_scxml(scxml_content: str) -> StateMachineDefinition:
7583
for s in definition.initial_states:
7684
definition.states[s].initial = True
7785

86+
# If the initial states definition does not contain any first level state,
87+
# we find the first level states that are ancestor of the initial states
88+
# and also set them as the initial states.
89+
if not set(definition.states.keys()) & definition.initial_states:
90+
not_found = set(definition.initial_states)
91+
for state, parents in visit_states(definition.states.values(), []):
92+
if state.id in definition.initial_states:
93+
not_found.remove(state.id)
94+
if parents:
95+
topmost_state = parents[0]
96+
topmost_state.initial = True
97+
definition.initial_states.add(topmost_state.id)
98+
if not not_found:
99+
break
100+
78101
return definition
79102

80103

statemachine/io/scxml/processor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,16 @@ def process_definition(self, definition, location: str):
6565
if definition.datamodel:
6666
datamodel = create_datamodel_action_callable(definition.datamodel)
6767
if datamodel:
68-
initial_state = next(s for s in iter(states_dict.values()) if s["initial"])
68+
try:
69+
initial_state = next(s for s in iter(states_dict.values()) if s.get("initial"))
70+
except StopIteration:
71+
# If there's no explicit initial state, use the first one
72+
initial_state = next(iter(states_dict.values()))
73+
6974
if "enter" not in initial_state:
7075
initial_state["enter"] = []
7176
if isinstance(initial_state["enter"], list):
7277
initial_state["enter"].insert(0, datamodel)
73-
7478
self._add(location, {"states": states_dict, "prepare_event": self._prepare_event})
7579

7680
def _prepare_event(self, *args, **kwargs):

tests/scxml/w3c/mandatory/test346.fail.md

Lines changed: 0 additions & 38 deletions
This file was deleted.

tests/scxml/w3c/mandatory/test355.fail.md

Lines changed: 0 additions & 59 deletions
This file was deleted.

tests/scxml/w3c/mandatory/test355.scxml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ we enter s0 first we succeed, if s1, failure. -->
66

77
<state id="s0">
88
<transition target="pass" />
9+
<transition target="s1" cond="1 &lt; 0" /><!-- fgm: added to have a single component in graph -->
910
</state>
1011

1112
<state id="s1">

tests/scxml/w3c/mandatory/test372.fail.md

Lines changed: 0 additions & 38 deletions
This file was deleted.

tests/scxml/w3c/mandatory/test413.fail.md

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)