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 --> +