diff --git a/statemachine/engines/base.py b/statemachine/engines/base.py
index d5d8eff..f96be39 100644
--- a/statemachine/engines/base.py
+++ b/statemachine/engines/base.py
@@ -602,10 +602,7 @@ def add_descendant_states_to_enter(
)
elif state and state.is_compound:
states_for_default_entry.add(info)
- initial_state = next(s for s in state.states if s.initial)
- transition = next(
- t for t in state.transitions if t.initial and t.target == initial_state
- )
+ transition = next(t for t in state.transitions if t.initial)
info_initial = StateTransition(
transition=transition,
target=transition.target,
diff --git a/statemachine/factory.py b/statemachine/factory.py
index dd7a85e..b0e0547 100644
--- a/statemachine/factory.py
+++ b/statemachine/factory.py
@@ -95,7 +95,8 @@ def _initials_by_document_order(
for s in states:
s.document_order = order
order += 1
- cls._initials_by_document_order(s.states, s, order)
+ if s.states:
+ cls._initials_by_document_order(s.states, s, order)
if s.initial:
initial = s
if not initial and states:
diff --git a/statemachine/io/scxml/actions.py b/statemachine/io/scxml/actions.py
index dbc0c06..d356b98 100644
--- a/statemachine/io/scxml/actions.py
+++ b/statemachine/io/scxml/actions.py
@@ -27,7 +27,7 @@
from .schema import ScriptAction
logger = logging.getLogger(__name__)
-protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name"}
+protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name", "_event"}
class ParseTime:
diff --git a/statemachine/io/scxml/parser.py b/statemachine/io/scxml/parser.py
index d3d735a..f2b0b4a 100644
--- a/statemachine/io/scxml/parser.py
+++ b/statemachine/io/scxml/parser.py
@@ -1,5 +1,6 @@
import re
import xml.etree.ElementTree as ET
+from typing import Iterable
from typing import Set
from .schema import Action
@@ -33,13 +34,20 @@ def strip_namespaces(tree: ET.Element):
attrib[new_name] = attrib.pop(name)
+def visit_states(states: Iterable[State], parents: list[State]):
+ for state in states:
+ yield state, parents
+ if state.states:
+ yield from visit_states(state.states.values(), parents + [state])
+
+
def _parse_initial(initial_content: "str | None") -> Set[str]:
if initial_content is None:
return set()
return set(initial_content.split())
-def parse_scxml(scxml_content: str) -> StateMachineDefinition:
+def parse_scxml(scxml_content: str) -> StateMachineDefinition: # noqa: C901
root = ET.fromstring(scxml_content)
strip_namespaces(root)
@@ -75,6 +83,21 @@ def parse_scxml(scxml_content: str) -> StateMachineDefinition:
for s in definition.initial_states:
definition.states[s].initial = True
+ # If the initial states definition does not contain any first level state,
+ # we find the first level states that are ancestor of the initial states
+ # and also set them as the initial states.
+ if not set(definition.states.keys()) & definition.initial_states:
+ not_found = set(definition.initial_states)
+ for state, parents in visit_states(definition.states.values(), []):
+ if state.id in definition.initial_states:
+ not_found.remove(state.id)
+ if parents:
+ topmost_state = parents[0]
+ topmost_state.initial = True
+ definition.initial_states.add(topmost_state.id)
+ if not not_found:
+ break
+
return definition
diff --git a/statemachine/io/scxml/processor.py b/statemachine/io/scxml/processor.py
index cf410f7..e5252e2 100644
--- a/statemachine/io/scxml/processor.py
+++ b/statemachine/io/scxml/processor.py
@@ -65,12 +65,16 @@ def process_definition(self, definition, location: str):
if definition.datamodel:
datamodel = create_datamodel_action_callable(definition.datamodel)
if datamodel:
- initial_state = next(s for s in iter(states_dict.values()) if s["initial"])
+ try:
+ initial_state = next(s for s in iter(states_dict.values()) if s.get("initial"))
+ except StopIteration:
+ # If there's no explicit initial state, use the first one
+ initial_state = next(iter(states_dict.values()))
+
if "enter" not in initial_state:
initial_state["enter"] = []
if isinstance(initial_state["enter"], list):
initial_state["enter"].insert(0, datamodel)
-
self._add(location, {"states": states_dict, "prepare_event": self._prepare_event})
def _prepare_event(self, *args, **kwargs):
diff --git a/tests/scxml/w3c/mandatory/test346.fail.md b/tests/scxml/w3c/mandatory/test346.fail.md
deleted file mode 100644
index 04aa448..0000000
--- a/tests/scxml/w3c/mandatory/test346.fail.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Testcase: test346
-
-ValueError: Inappropriate argument value (of correct type).
-
-Final configuration: `No configuration`
-
----
-
-## Logs
-```py
-No logs
-```
-
-## "On transition" events
-```py
-No events
-```
-
-## Traceback
-```py
-Traceback (most recent call last):
- File "/home/macedo/projects/python-statemachine/tests/scxml/test_scxml_cases.py", line 137, in test_scxml_usecase
- processor.parse_scxml_file(testcase_path)
- ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 31, in parse_scxml_file
- return self.parse_scxml(path.stem, scxml_content)
- ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 34, in parse_scxml
- definition = parse_scxml(scxml_content)
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/parser.py", line 62, in parse_scxml
- state = parse_state(state_elem, definition.initial_states)
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/parser.py", line 129, in parse_state
- transition = parse_transition(trans_elem)
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/parser.py", line 157, in parse_transition
- raise ValueError("Transition must have a 'target' attribute")
-ValueError: Transition must have a 'target' attribute
-
-```
diff --git a/tests/scxml/w3c/mandatory/test355.fail.md b/tests/scxml/w3c/mandatory/test355.fail.md
deleted file mode 100644
index 43bdccc..0000000
--- a/tests/scxml/w3c/mandatory/test355.fail.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Testcase: test355
-
-InvalidDefinition: The state machine has a definition error
-
-Final configuration: `No configuration`
-
----
-
-## Logs
-```py
-No logs
-```
-
-## "On transition" events
-```py
-No events
-```
-
-## Traceback
-```py
-Traceback (most recent call last):
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 114, in _add
- sc_class = create_machine_class_from_definition(location, **definition)
- File "/home/macedo/projects/python-statemachine/statemachine/io/__init__.py", line 140, in create_machine_class_from_definition
- return StateMachineMetaclass(name, (StateMachine,), attrs_mapper) # type: ignore[return-value]
- File "/home/macedo/projects/python-statemachine/statemachine/factory.py", line 76, in __init__
- cls._check()
- ~~~~~~~~~~^^
- File "/home/macedo/projects/python-statemachine/statemachine/factory.py", line 122, in _check
- cls._check_disconnected_state()
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
- File "/home/macedo/projects/python-statemachine/statemachine/factory.py", line 191, in _check_disconnected_state
- raise InvalidDefinition(
- ...<5 lines>...
- )
-statemachine.exceptions.InvalidDefinition: There are unreachable states. The statemachine graph should have a single component. Disconnected states: ['s1', 'fail']
-
-The above exception was the direct cause of the following exception:
-
-Traceback (most recent call last):
- File "/home/macedo/projects/python-statemachine/tests/scxml/test_scxml_cases.py", line 114, in test_scxml_usecase
- processor.parse_scxml_file(testcase_path)
- ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 30, in parse_scxml_file
- return self.parse_scxml(path.stem, scxml_content)
- ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 34, in parse_scxml
- self.process_definition(definition, location=sm_name)
- ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 49, in process_definition
- self._add(location, {"states": states_dict})
- ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 118, in _add
- raise InvalidDefinition(
- f"Failed to create state machine class: {e} from definition: {definition}"
- ) from e
-statemachine.exceptions.InvalidDefinition: Failed to create state machine class: There are unreachable states. The statemachine graph should have a single component. Disconnected states: ['s1', 'fail'] from definition: {'states': {'s0': {'initial': True}, 's1': {}, 'pass': {'final': True, 'enter': [ExecuteBlock(ExecutableContent(actions=[LogAction(label='Outcome', expr="'pass'")]))]}, 'fail': {'final': True, 'enter': [ExecuteBlock(ExecutableContent(actions=[LogAction(label='Outcome', expr="'fail'")]))]}}}
-
-```
diff --git a/tests/scxml/w3c/mandatory/test355.scxml b/tests/scxml/w3c/mandatory/test355.scxml
index 1601eeb..3107c9e 100644
--- a/tests/scxml/w3c/mandatory/test355.scxml
+++ b/tests/scxml/w3c/mandatory/test355.scxml
@@ -6,6 +6,7 @@ we enter s0 first we succeed, if s1, failure. -->
+
diff --git a/tests/scxml/w3c/mandatory/test372.fail.md b/tests/scxml/w3c/mandatory/test372.fail.md
deleted file mode 100644
index 613a2e7..0000000
--- a/tests/scxml/w3c/mandatory/test372.fail.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Testcase: test372
-
-KeyError: Mapping key not found.
-
-Final configuration: `No configuration`
-
----
-
-## Logs
-```py
-No logs
-```
-
-## "On transition" events
-```py
-No events
-```
-
-## Traceback
-```py
-Traceback (most recent call last):
- File "/home/macedo/projects/python-statemachine/tests/scxml/test_scxml_cases.py", line 114, in test_scxml_usecase
- processor.parse_scxml_file(testcase_path)
- ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 30, in parse_scxml_file
- return self.parse_scxml(path.stem, scxml_content)
- ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 34, in parse_scxml
- self.process_definition(definition, location=sm_name)
- ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 43, in process_definition
- initial_state = next(s for s in iter(states_dict.values()) if s["initial"])
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 43, in
- initial_state = next(s for s in iter(states_dict.values()) if s["initial"])
- ~^^^^^^^^^^^
-KeyError: 'initial'
-
-```
diff --git a/tests/scxml/w3c/mandatory/test413.fail.md b/tests/scxml/w3c/mandatory/test413.fail.md
deleted file mode 100644
index 07677cf..0000000
--- a/tests/scxml/w3c/mandatory/test413.fail.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Testcase: test413
-
-UnboundLocalError: Local name referenced but not bound to a value.
-
-Final configuration: `No configuration`
-
----
-
-## Logs
-```py
-No logs
-```
-
-## "On transition" events
-```py
-No events
-```
-
-## Traceback
-```py
-Traceback (most recent call last):
- File "/home/macedo/projects/python-statemachine/tests/scxml/test_scxml_cases.py", line 114, in test_scxml_usecase
- processor.parse_scxml_file(testcase_path)
- ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 30, in parse_scxml_file
- return self.parse_scxml(path.stem, scxml_content)
- ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/processor.py", line 33, in parse_scxml
- definition = parse_scxml(scxml_content)
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/parser.py", line 62, in parse_scxml
- state = parse_state(state_elem, definition.initial_states)
- File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/parser.py", line 146, in parse_state
- state.states[child_state.id] = child_state
- ^^^^^^^^^^^
-UnboundLocalError: cannot access local variable 'child_state' where it is not associated with a value
-
-```
diff --git a/tests/scxml/w3c/mandatory/test413.scxml b/tests/scxml/w3c/mandatory/test413.scxml
index 6b0f1db..3b3f11c 100644
--- a/tests/scxml/w3c/mandatory/test413.scxml
+++ b/tests/scxml/w3c/mandatory/test413.scxml
@@ -16,6 +16,7 @@ states we should not enter all have immediate transitions to failure in them -->
we're in
either s2p112 or s2p122, but not both of them -->
+