1212from statemachine .statemachine import StateMachine
1313
1414
15- def send_event (machine : StateMachine , event_to_send : str ) -> None :
16- machine .send (event_to_send )
17-
18-
1915def parse_onentry (element ):
2016 """Parses the <onentry> XML into a callable."""
2117 actions = [parse_element (child ) for child in element ]
2218
2319 def execute_block (* args , ** kwargs ):
24- for action in actions :
25- action (* args , ** kwargs )
20+ machine = kwargs ["machine" ]
21+ try :
22+ for action in actions :
23+ action (* args , ** kwargs )
24+ except Exception :
25+ machine .send ("error.execution" )
2626
2727 return execute_block
2828
@@ -34,6 +34,8 @@ def parse_element(element):
3434 return parse_raise (element )
3535 elif tag == "assign" :
3636 return parse_assign (element )
37+ elif tag == "foreach" :
38+ return parse_foreach (element )
3739 elif tag == "log" :
3840 return parse_log (element )
3941 elif tag == "if" :
@@ -80,6 +82,58 @@ def assign_action(*args, **kwargs):
8082 return assign_action
8183
8284
85+ def parse_foreach (element ): # noqa: C901
86+ """
87+ Parses the <foreach> element into a callable.
88+
89+ - `array`: The iterable collection (required).
90+ - `item`: The variable name for the current item (required).
91+ - `index`: The variable name for the current index (optional).
92+ - Child elements are executed for each iteration.
93+ """
94+ array_expr = element .attrib .get ("array" )
95+ if not array_expr :
96+ raise ValueError ("<foreach> must have an 'array' attribute" )
97+
98+ item_var = element .attrib .get ("item" )
99+ if not item_var :
100+ raise ValueError ("<foreach> must have an 'item' attribute" )
101+
102+ index_var = element .attrib .get ("index" )
103+ child_actions = [parse_element (child ) for child in element ]
104+
105+ def foreach_action (* args , ** kwargs ): # noqa: C901
106+ machine = kwargs ["machine" ]
107+ context = {** machine .model .__dict__ } # Shallow copy of the model's attributes
108+
109+ try :
110+ # Evaluate the array expression to get the iterable
111+ array = eval (array_expr , {}, context )
112+ if not hasattr (array , "__iter__" ):
113+ raise ValueError (
114+ f"<foreach> 'array' must evaluate to an iterable, got: { type (array ).__name__ } "
115+ )
116+ except Exception as e :
117+ raise ValueError (f"Error evaluating <foreach> 'array' expression: { e } " ) from e
118+
119+ if not item_var .isidentifier ():
120+ raise ValueError (
121+ f"<foreach> 'item' must be a valid Python attribute name, got: { item_var } "
122+ )
123+ # Iterate over the array
124+ for index , item in enumerate (array ):
125+ # Assign the item and optionally the index
126+ setattr (machine .model , item_var , item )
127+ if index_var :
128+ setattr (machine .model , index_var , index )
129+
130+ # Execute child actions
131+ for action in child_actions :
132+ action (* args , ** kwargs )
133+
134+ return foreach_action
135+
136+
83137def _normalize_cond (cond : "str | None" ) -> "str | None" :
84138 """
85139 Normalizes a JavaScript-like condition string to be compatible with Python's eval.
@@ -108,6 +162,19 @@ def _normalize_cond(cond: "str | None") -> "str | None":
108162 return pattern .sub (lambda match : replacements [match .group (0 )], cond )
109163
110164
165+ def parse_cond (cond ):
166+ """Parses the <cond> element into a callable."""
167+ cond = _normalize_cond (cond )
168+ if cond is None :
169+ return None
170+
171+ def cond_action (* args , ** kwargs ):
172+ machine = kwargs ["machine" ]
173+ return eval (cond , {}, {"machine" : machine , ** machine .model .__dict__ })
174+
175+ return cond_action
176+
177+
111178def parse_if (element ): # noqa: C901
112179 """Parses the <if> element into a callable."""
113180 branches = []
@@ -198,6 +265,8 @@ def parse_data(element):
198265 """
199266 data_id = element .attrib ["id" ]
200267 expr = element .attrib .get ("expr" )
268+ if not expr :
269+ expr = element .text and element .text .strip ()
201270
202271 def data_initializer (model ):
203272 # Evaluate the expression if provided, or set to None
@@ -230,19 +299,8 @@ def parse_scxml(scxml_content: str) -> Dict[str, Any]: # noqa: C901
230299 Parse SCXML content and return a dictionary definition compatible with
231300 create_machine_class_from_definition.
232301
233- The returned dictionary has the format:
234- {
235- "states": {
236- "state_id": {"initial": True},
237- ...
238- },
239- "events": {
240- "event_name": [
241- {"from": "source_state", "to": "target_state"},
242- ...
243- ]
244- }
245- }
302+ The returned dictionary has the format compatible with
303+ :ref:`create_machine_class_from_definition`.
246304 """
247305 # Parse XML content
248306 root = ET .fromstring (scxml_content )
@@ -258,7 +316,6 @@ def parse_scxml(scxml_content: str) -> Dict[str, Any]: # noqa: C901
258316
259317 # Build states dictionary
260318 states = {}
261- events : Dict [str , List [Dict [str , str ]]] = {}
262319
263320 def _parse_state (state_elem , final = False ): # noqa: C901
264321 state_id = state_elem .get ("id" )
@@ -272,21 +329,21 @@ def _parse_state(state_elem, final=False): # noqa: C901
272329 for trans_elem in state_elem .findall ("transition" ):
273330 event = trans_elem .get ("event" ) or None
274331 target = trans_elem .get ("target" )
332+ cond = parse_cond (trans_elem .get ("cond" ))
275333
276334 if target :
277- if event not in events :
278- events [event ] = []
335+ state = states [state_id ]
336+ if "on" not in state :
337+ state ["on" ] = {}
338+
339+ if event not in state ["on" ]:
340+ state ["on" ][event ] = {"target" : target }
341+ if cond :
342+ state ["on" ][event ]["cond" ] = cond
279343
280344 if target not in states :
281345 states [target ] = {}
282346
283- events [event ].append (
284- {
285- "from" : state_id ,
286- "to" : target ,
287- }
288- )
289-
290347 for onentry_elem in state_elem .findall ("onentry" ):
291348 entry_action = parse_onentry (onentry_elem )
292349 state = states [state_id ]
@@ -316,4 +373,4 @@ def _parse_state(state_elem, final=False): # noqa: C901
316373 first_state = next (iter (states ))
317374 states [first_state ]["initial" ] = True
318375
319- return {"states" : states , "events" : events , ** extra_data }
376+ return {"states" : states , ** extra_data }
0 commit comments