4949"""
5050
5151from importlib import import_module
52- from typing import Optional , Any
52+ from typing import Optional , Any , Callable
5353from contextlib import suppress
5454from pathlib import Path
5555import inspect
@@ -166,6 +166,97 @@ def _read_required_mapping_field(entry: dict[str, Any], field: str):
166166 return entry [field ]
167167
168168
169+ def _resolve_callable_reference (globals_dict : dict [str , Any ], ref : Optional [str ]) -> Optional [Callable [..., Any ]]:
170+ if not isinstance (ref , str ) or not ref :
171+ return None
172+
173+ if ":" in ref :
174+ module_name , qualname = ref .split (":" , 1 )
175+ try :
176+ obj = import_module (module_name )
177+ except Exception :
178+ obj = None
179+
180+ if obj is not None :
181+ try :
182+ for part in qualname .split ("." ):
183+ if part == "<locals>" :
184+ return None
185+ obj = getattr (obj , part )
186+ if callable (obj ):
187+ return obj
188+ except Exception :
189+ pass
190+
191+ fallback_name = qualname .split ("." )[- 1 ]
192+ fallback = globals_dict .get (fallback_name )
193+ if callable (fallback ):
194+ return fallback
195+ return None
196+
197+ candidate = globals_dict .get (ref )
198+ if callable (candidate ):
199+ return candidate
200+ return None
201+
202+
203+ def _resolve_callable_reference_from_source (
204+ globals_dict : dict [str , Any ],
205+ ref : Optional [str ],
206+ source : Optional [str ],
207+ ) -> Optional [Callable [..., Any ]]:
208+ if not isinstance (source , str ) or not source .strip ():
209+ return None
210+
211+ expected_name = None
212+ if isinstance (ref , str ) and ref :
213+ if ":" in ref :
214+ _ , qualname = ref .split (":" , 1 )
215+ if "<locals>" not in qualname :
216+ expected_name = qualname .split ("." )[- 1 ]
217+ else :
218+ expected_name = ref
219+
220+ if not isinstance (expected_name , str ) or not expected_name or expected_name == "<lambda>" :
221+ return None
222+
223+ try :
224+ if "__builtins__" not in globals_dict :
225+ globals_dict ["__builtins__" ] = __builtins__
226+ exec (compile (textwrap .dedent (source ), filename = "<summoner_run_while>" , mode = "exec" ), globals_dict )
227+ except Exception :
228+ return None
229+
230+ candidate = globals_dict .get (expected_name )
231+ if callable (candidate ):
232+ return candidate
233+ return None
234+
235+
236+ def _resolve_run_while_spec (
237+ globals_dict : dict [str , Any ],
238+ kind : str ,
239+ value : Any ,
240+ name : Optional [str ],
241+ source : Optional [str ] = None ,
242+ ) -> Any :
243+ if kind == "none" :
244+ return None
245+ if kind == "bool" :
246+ return bool (value )
247+ if kind == "callable" :
248+ resolved = _resolve_callable_reference (globals_dict , name )
249+ if not callable (resolved ):
250+ resolved = _resolve_callable_reference_from_source (globals_dict , name , source )
251+ if callable (resolved ):
252+ return resolved
253+ raise ValueError (
254+ "Could not resolve serialized run_while callable "
255+ f"{ name !r} from available replay context"
256+ )
257+ raise ValueError (f"Unknown run_while kind { kind !r} " )
258+
259+
169260class ClientMerger (SummonerClient ):
170261 """
171262 Merge multiple sources into one client.
@@ -792,19 +883,34 @@ def initiate_senders(self):
792883 if not self ._flow .in_use :
793884 for src in self .sources :
794885 if src ["kind" ] == "client" :
795- if any (dna .get ("use_data" , False ) for dna in src ["client" ]._dna_senders ):
886+ if any (
887+ dna .get ("use_data" , False ) or (
888+ dna .get ("every" ) is not None and (
889+ (dna .get ("on_triggers" ) or []) or
890+ (dna .get ("on_actions" ) or [])
891+ )
892+ )
893+ for dna in src ["client" ]._dna_senders
894+ ):
796895 raise RuntimeError (
797896 "ClientMerger.flow().activate() must be called before initiate_senders() "
798- "when replaying senders with use_data=True"
897+ "when replaying reactive timed senders or senders with use_data=True"
799898 )
800899 else :
801900 if any (
802- entry .get ("type" ) == "send" and entry .get ("use_data" , False )
901+ entry .get ("type" ) == "send" and (
902+ entry .get ("use_data" , False ) or (
903+ entry .get ("every" ) is not None and (
904+ (entry .get ("on_triggers" ) or []) or
905+ (entry .get ("on_actions" ) or [])
906+ )
907+ )
908+ )
803909 for entry in src ["dna_entries" ]
804910 ):
805911 raise RuntimeError (
806912 "ClientMerger.flow().activate() must be called before initiate_senders() "
807- "when replaying senders with use_data=True"
913+ "when replaying reactive timed senders or senders with use_data=True"
808914 )
809915
810916 for src in self .sources :
@@ -815,12 +921,24 @@ def initiate_senders(self):
815921 fn_clone = self ._clone_handler (dna ["fn" ], var_name )
816922 try :
817923 route = _read_required_mapping_field (dna , "route" )
924+ run_while = dna .get ("run_while" )
925+ if run_while is None :
926+ run_while = _resolve_run_while_spec (
927+ fn_clone .__globals__ ,
928+ dna .get ("run_while_kind" , "none" ),
929+ dna .get ("run_while_value" , None ),
930+ dna .get ("run_while_name" , None ),
931+ dna .get ("run_while_source" , None ),
932+ )
818933 self .send (
819934 route ,
820935 multi = dna .get ("multi" , False ),
821936 on_triggers = dna .get ("on_triggers" ),
822937 on_actions = dna .get ("on_actions" ),
823938 use_data = dna .get ("use_data" , False ),
939+ data_mode = dna .get ("data_mode" , None ),
940+ every = dna .get ("every" , None ),
941+ run_while = run_while ,
824942 )(fn_clone )
825943 except Exception as e :
826944 self .logger .warning (
@@ -846,12 +964,22 @@ def initiate_senders(self):
846964 g ["Trigger" ] = TriggerCls
847965 on_triggers = {_resolve_trigger (TriggerCls , t ) for t in trigger_names } or None
848966 on_actions = {_resolve_action (Action , a ) for a in entry .get ("on_actions" , [])} or None
967+ run_while = _resolve_run_while_spec (
968+ g ,
969+ entry .get ("run_while_kind" , "none" ),
970+ entry .get ("run_while_value" , None ),
971+ entry .get ("run_while_name" , None ),
972+ entry .get ("run_while_source" , None ),
973+ )
849974 dec = self .send (
850975 route ,
851976 multi = entry .get ("multi" , False ),
852977 on_triggers = on_triggers ,
853978 on_actions = on_actions ,
854979 use_data = entry .get ("use_data" , False ),
980+ data_mode = entry .get ("data_mode" , None ),
981+ every = entry .get ("every" , None ),
982+ run_while = run_while ,
855983 )
856984 self ._apply_with_source_patch (dec , fn , entry ["source" ])
857985
@@ -1171,12 +1299,19 @@ def initiate_senders(self):
11711299 - Action is resolved from the Action container by name
11721300 """
11731301 if (not self ._flow .in_use ) and any (
1174- entry .get ("type" ) == "send" and entry .get ("use_data" , False )
1302+ entry .get ("type" ) == "send" and (
1303+ entry .get ("use_data" , False ) or (
1304+ entry .get ("every" ) is not None and (
1305+ (entry .get ("on_triggers" ) or []) or
1306+ (entry .get ("on_actions" ) or [])
1307+ )
1308+ )
1309+ )
11751310 for entry in self ._dna_list
11761311 ):
11771312 raise RuntimeError (
11781313 "ClientTranslation.flow().activate() must be called before initiate_senders() "
1179- "when replaying senders with use_data=True"
1314+ "when replaying reactive timed senders or senders with use_data=True"
11801315 )
11811316
11821317 g = self ._sandbox_globals
@@ -1199,12 +1334,22 @@ def initiate_senders(self):
11991334 g ["Trigger" ] = TriggerCls
12001335 on_triggers = {_resolve_trigger (TriggerCls , t ) for t in trigger_names } or None
12011336 on_actions = {_resolve_action (Action , a ) for a in entry .get ("on_actions" , [])} or None
1337+ run_while = _resolve_run_while_spec (
1338+ g ,
1339+ entry .get ("run_while_kind" , "none" ),
1340+ entry .get ("run_while_value" , None ),
1341+ entry .get ("run_while_name" , None ),
1342+ entry .get ("run_while_source" , None ),
1343+ )
12021344 dec = self .send (
12031345 route ,
12041346 multi = entry .get ("multi" , False ),
12051347 on_triggers = on_triggers ,
12061348 on_actions = on_actions ,
12071349 use_data = entry .get ("use_data" , False ),
1350+ data_mode = entry .get ("data_mode" , None ),
1351+ every = entry .get ("every" , None ),
1352+ run_while = run_while ,
12081353 )
12091354 self ._apply_with_source_patch (dec , fn , entry ["source" ])
12101355
0 commit comments