This page documents the Python SDK interface for working with Summoner signals and trigger trees.
The module provides:
- A signal-tree loader that parses an indented "TRIGGERS" definition into a nested tree.
- A dynamically generated
Triggerclass whose attributes areSignalinstances. - Lightweight event wrappers (
Move,Stay,Test) and anActioncontainer. - Utilities for extracting
Signalfrom either aSignalor anEvent.
def is_valid_varname(name: str) -> boolChecks whether name is a valid Python identifier suitable for use as a generated Trigger.<name> attribute.
- Type:
str - Meaning: Candidate signal name.
- Type:
bool - Meaning:
Trueif the name matches identifier rules, otherwiseFalse.
from summoner.protocol.triggers import is_valid_varname
assert is_valid_varname("OK") is True
assert is_valid_varname("all_good") is True
assert is_valid_varname("not valid") is False
assert is_valid_varname("123bad") is Falsedef preprocess_line(raw: str, lineno: int, tabsize: int) -> Optional[tuple[int, int, str]]Normalizes a raw line from a triggers file:
- Strips the trailing newline.
- Expands tabs into spaces (using
tabsize). - Measures indentation from leading spaces.
- Removes inline comments starting with
#. - Skips blank or comment-only lines.
- Type:
str - Meaning: Raw line from the triggers definition.
- Type:
int - Meaning: 1-based line number used for error messages.
- Type:
int - Meaning: Tab expansion width.
-
Type:
Optional[tuple[int, int, str]] -
Meaning:
- Returns
(lineno, indent, name)if the line defines a signal. - Returns
Noneif the line should be skipped.
- Returns
from summoner.protocol.triggers import preprocess_line
assert preprocess_line("OK\n", 1, 8) == (1, 0, "OK")
assert preprocess_line(" acceptable # comment\n", 2, 8) == (2, 4, "acceptable")
assert preprocess_line("# comment-only\n", 3, 8) is None
assert preprocess_line(" \n", 4, 8) is Nonedef update_hierarchy(indent: int, indent_levels: list[int]) -> intMaintains indentation state while parsing a triggers file and returns the computed tree depth for the current line.
- If
indentmatches the current level, depth stays the same. - If
indentincreases, a new depth level is created. - If
indentdecreases, it must match a previously seen indentation level; otherwise parsing fails.
- Type:
int - Meaning: The indentation (in spaces) of the current line.
- Type:
list[int] - Meaning: The current stack of indentation levels. Mutated in place.
- Type:
int - Meaning: The computed depth for the current line.
from summoner.protocol.triggers import update_hierarchy
levels = [0]
assert update_hierarchy(0, levels) == 0
assert update_hierarchy(4, levels) == 1
assert levels == [0, 4]
assert update_hierarchy(4, levels) == 1
assert update_hierarchy(0, levels) == 0def simplify_leaves(tree: dict[str, Any]) -> NonePost-processes a nested dict tree in place so that empty dictionaries are replaced with None to mark leaves.
- Type:
dict[str, Any] - Meaning: The parsed nested tree.
Returns None (operates in place).
from summoner.protocol.triggers import simplify_leaves
tree = {"OK": {"acceptable": {}}, "error": {}}
simplify_leaves(tree)
assert tree == {"OK": {"acceptable": None}, "error": None}def parse_signal_tree_lines(lines: list[str], tabsize: int = 8) -> dict[str, Any]Parses a list of trigger-definition lines into a nested dict tree.
Rules:
- Each non-empty, non-comment line defines a signal name.
- Indentation defines parent-child relationships.
- Names must be valid Python identifiers.
- Duplicate names at the same indentation scope are rejected.
- Inconsistent indentation decreases (to an unseen indent level) raises an error.
- Type:
list[str] - Meaning: Raw lines that define the trigger hierarchy.
- Type:
int - Meaning: Tab expansion width for parsing.
- Default:
8
- Type:
dict[str, Any] - Meaning: Nested dict describing the hierarchy, with leaves represented as
None.
from summoner.protocol.triggers import parse_signal_tree_lines
text = """
OK
acceptable
all_good
error
minor
major
"""
tree = parse_signal_tree_lines(text.splitlines())
assert tree["OK"]["acceptable"] is None
assert tree["error"]["major"] is Nonedef parse_signal_tree(filepath: str, tabsize: int = 8) -> dict[str, Any]Reads a triggers file from disk and parses it into a nested dict tree using parse_signal_tree_lines.
- Type:
str - Meaning: Path to the triggers file.
- Type:
int - Meaning: Tab expansion width.
- Default:
8
- Type:
dict[str, Any] - Meaning: Nested dict describing the hierarchy.
from summoner.protocol.triggers import parse_signal_tree
tree = parse_signal_tree("TRIGGERS")class SignalRepresents a named signal with a stable hierarchical path:
path: a tuple of integer indices describing the position in the trigger tree.name: the signal's name as written in the triggers definition.
Signals support ordering comparisons based on ancestry:
a > bis true whenais a strict ancestor ofb(prefix match on paths).a >= bincludes equality.- Hashing and equality are based on
path.
- Type:
tuple[int, ...] - Meaning: Hierarchical path.
- Type:
str - Meaning: Signal name.
A Signal instance.
from summoner.protocol.triggers import Signal
root = Signal((0,), "OK")
child = Signal((0, 0), "acceptable")
assert root > child
assert child < root
assert root.name == "OK"
assert child.path == (0, 0)def build_triggers(tree: dict[str, Any]) -> typeBuilds and returns a dynamically-generated Trigger class.
The returned class:
- Exposes each signal name as a class attribute:
Trigger.OK,Trigger.acceptable, etc. - Assigns each attribute a
Signalinstance with a stablepath. - Provides
Trigger.name_of(*path)to resolve a name from a path tuple.
Names that conflict with Python reserved keywords or common object attributes are rejected.
- Type:
dict[str, Any] - Meaning: Nested trigger tree (as returned by
parse_signal_tree_lines/parse_signal_tree).
- Type:
type - Meaning: A dynamically generated
Triggerclass.
from summoner.protocol.triggers import build_triggers
tree = {"OK": {"acceptable": None}, "error": {"major": None}}
Trigger = build_triggers(tree)
assert Trigger.OK.name == "OK"
assert Trigger.acceptable.path == (0, 0)
assert Trigger.name_of(1, 0) == "major"class EventWraps a Signal as a typed protocol event. The module defines three event subclasses:
Move(signal, data=None)Stay(signal, data=None)Test(signal, data=None)
Test is marked with __test__ = False to avoid certain test discovery behaviors.
An event can also carry arbitrary data. This is the payload consumed by reactive senders that register with use_data=True; sender-side data_mode decides whether that payload is observed live or as a snapshot, and when_data can filter it before the sender runs.
- Type:
Signal - Meaning: The underlying signal carried by the event.
- Type:
Any - Meaning: Optional event payload forwarded to data-aware senders and available as
event.data. - Default:
None
An Event (or subclass) instance.
from summoner.protocol.triggers import Signal, Move, Stay, Test
s = Signal((0,), "OK")
e1 = Move(s, data={"origin": "receiver"})
e2 = Stay(s)
e3 = Test(s, data=123)
assert e1.signal is s
assert e1.data == {"origin": "receiver"}class ActionA simple container mapping action names to event classes:
Action.MOVEisMoveAction.STAYisStayAction.TESTisTest
This is commonly used as a stable reference set when validating or resolving actions by name.
None.
Not instanced (used as a namespace container).
from summoner.protocol.triggers import Action, Signal
s = Signal((0,), "OK")
e = Action.MOVE(s)
assert type(e).__name__ == "Move"def extract_signal(trigger: Any) -> Optional[Signal]Normalizes an input into a Signal:
- If input is an
Event, returnsevent.signal. - If input is a
Signal, returns it unchanged. - If input is
None, returnsNone. - Otherwise raises
TypeError.
- Type:
Any - Meaning: A
Signal, anEvent, orNone.
- Type:
Optional[Signal] - Meaning: Extracted signal (or
None).
from summoner.protocol.triggers import Signal, Move, extract_signal
s = Signal((0,), "OK")
e = Move(s)
assert extract_signal(s) is s
assert extract_signal(e) is s
assert extract_signal(None) is Nonedef load_triggers(
triggers_file: Optional[str] = "TRIGGERS",
text: Optional[str] = None,
json_dict: Optional[dict[str, Any]] = None,
) -> typeBuilds and returns a Trigger class from one of three inputs:
Priority:
- If
textis provided, parse it as trigger definitions. - Else if
json_dictis provided, use it as the tree directly (shallow-copied). - Else, read and parse
triggers_filerelative to the process working directory used by the module.
If the triggers file cannot be found, raises FileNotFoundError with a clearer message.
- Type:
Optional[str] - Meaning: Filename/path to the triggers file used when
textandjson_dictare not provided. - Default:
"TRIGGERS"
- Type:
Optional[str] - Meaning: Inline trigger definitions. If provided, it is used instead of reading a file.
- Type:
Optional[dict[str, Any]] - Meaning: Nested tree structure matching the output schema of
parse_signal_tree_lines.
- Type:
type - Meaning: A dynamically generated
Triggerclass whose attributes areSignalinstances.
from summoner.protocol.triggers import load_triggers
Trigger = load_triggers()
print(Trigger.OK)from summoner.protocol.triggers import load_triggers
text = """
OK
acceptable
error
major
"""
Trigger = load_triggers(text=text)
assert Trigger.acceptable.path == (0, 0)
assert Trigger.name_of(1, 0) == "major"from summoner.protocol.triggers import load_triggers
tree = {"OK": {"acceptable": None}, "error": {"major": None}}
Trigger = load_triggers(json_dict=tree)
assert Trigger.OK.name == "OK"
assert Trigger.major.name == "major"
« Previous: Summoner.protocol | Next: Summoner.protocol.process »