Skip to content

Commit e2afe40

Browse files
authored
Merge pull request #99 from Summoner-Network/event-plus-plus
Event plus plus
2 parents 435eca8 + 82a846c commit e2afe40

9 files changed

Lines changed: 2006 additions & 147 deletions

File tree

summoner/client/client.py

Lines changed: 682 additions & 137 deletions
Large diffs are not rendered by default.

summoner/client/merger.py

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"""
5050

5151
from importlib import import_module
52-
from typing import Optional, Any
52+
from typing import Optional, Any, Callable
5353
from contextlib import suppress
5454
from pathlib import Path
5555
import 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+
169260
class 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

summoner/protocol/process.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,26 @@ def activated_nodes(
306306

307307
@dataclass(frozen=True, init=False)
308308
class Sender:
309-
__slots__ = ('fn', 'multi', 'actions', 'triggers', 'use_data')
309+
__slots__ = (
310+
'fn',
311+
'multi',
312+
'actions',
313+
'triggers',
314+
'use_data',
315+
'data_mode',
316+
'every',
317+
'run_while',
318+
'registration_id',
319+
)
310320
fn: Callable[..., Awaitable]
311321
multi: bool
312322
actions: Optional[set[Type]]
313323
triggers: Optional[set[Signal]]
314324
use_data: bool
325+
data_mode: Optional[str]
326+
every: Optional[float]
327+
run_while: Any
328+
registration_id: Optional[str]
315329

316330
def __init__(
317331
self,
@@ -320,12 +334,20 @@ def __init__(
320334
actions: Optional[set[Type]],
321335
triggers: Optional[set[Signal]],
322336
use_data: bool = False,
337+
data_mode: Optional[str] = None,
338+
every: Optional[float] = None,
339+
run_while: Any = None,
340+
registration_id: Optional[str] = None,
323341
):
324342
object.__setattr__(self, "fn", fn)
325343
object.__setattr__(self, "multi", multi)
326344
object.__setattr__(self, "actions", actions)
327345
object.__setattr__(self, "triggers", triggers)
328346
object.__setattr__(self, "use_data", use_data)
347+
object.__setattr__(self, "data_mode", data_mode)
348+
object.__setattr__(self, "every", every)
349+
object.__setattr__(self, "run_while", run_while)
350+
object.__setattr__(self, "registration_id", registration_id)
329351

330352
def responds_to(self, event: Any) -> bool:
331353
action_check = True

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

tests/test_client_runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from summoner.protocol.payload import wrap_with_types
88
from summoner.protocol.process import Direction, Node
99
from summoner.protocol.triggers import load_triggers
10-
from tests.helpers import DummyWriter
10+
from .helpers import DummyWriter
1111

1212

1313
def test_message_receiver_loop_applies_receive_hooks_updates_state_and_bridges_events():

0 commit comments

Comments
 (0)