jmri_logixng.tree
LogixNG tree node ABCs.
A LogixNG program is a tree of Node instances. There are three
flavors:
Action— does something (mutates layout state).Expression[T]— evaluates to a value of type T. The most common variant isExpression[bool](a "digital expression"); analog expressions return floats and string expressions return strings.Node— the common base. Both Actions and Expressions are Nodes.
Containers like ConditionalNG and LogixNG themselves
satisfy the Action ABC so they can be nested.
Every node:
- has a stable
idused for serialization and event correlation; - has an optional
user_namefor human-readable identification; - declares a class-level
type_tagused by the serializer.
1"""LogixNG tree node ABCs. 2 3A LogixNG program is a tree of :class:`Node` instances. There are three 4flavors: 5 6* :class:`Action` — does something (mutates layout state). 7* :class:`Expression[T]` — evaluates to a value of type *T*. The most common 8 variant is ``Expression[bool]`` (a "digital expression"); analog 9 expressions return floats and string expressions return strings. 10* :class:`Node` — the common base. Both Actions and Expressions are Nodes. 11 12Containers like :class:`ConditionalNG` and :class:`LogixNG` themselves 13satisfy the Action ABC so they can be nested. 14 15Every node: 16 * has a stable ``id`` used for serialization and event correlation; 17 * has an optional ``user_name`` for human-readable identification; 18 * declares a class-level ``type_tag`` used by the serializer. 19""" 20from __future__ import annotations 21 22import uuid 23from abc import ABC, abstractmethod 24from typing import TYPE_CHECKING, Any, ClassVar 25 26if TYPE_CHECKING: 27 from jmri_logixng.context import ExecutionContext 28 29 30class Node(ABC): # noqa: B024 — abstract by intent; the type_tag rule is enforced 31 """Common base for every LogixNG element.""" 32 33 type_tag: ClassVar[str] = "" # subclasses must set 34 35 def __init__(self, *, user_name: str = "", node_id: str | None = None) -> None: 36 if not self.type_tag: 37 raise TypeError(f"{type(self).__name__}.type_tag must be set on the class") 38 self.user_name = user_name 39 self.id = node_id or f"{type(self).__name__}-{uuid.uuid4().hex[:8]}" 40 41 def to_dict(self) -> dict[str, Any]: 42 """Serialize this node + its children to a dict suitable for YAML/JSON. 43 44 Default implementation captures ``type``, ``id``, ``user_name``. 45 Subclasses extend by overriding :meth:`extra_fields`. 46 """ 47 out: dict[str, Any] = {"type": self.type_tag, "id": self.id} 48 if self.user_name: 49 out["user_name"] = self.user_name 50 out.update(self.extra_fields()) 51 return out 52 53 def extra_fields(self) -> dict[str, Any]: 54 """Hook for subclasses to add their own fields to ``to_dict``.""" 55 return {} 56 57 def __repr__(self) -> str: 58 if self.user_name: 59 return f"<{type(self).__name__} {self.user_name!r}>" 60 return f"<{type(self).__name__} {self.id}>" 61 62 63class Action(Node): 64 """A node that *does* something when executed.""" 65 66 @abstractmethod 67 async def execute(self, ctx: ExecutionContext) -> None: ... 68 69 70class Expression[T](Node): 71 """A node that evaluates to a value of type *T*. 72 73 The most common variant is ``Expression[bool]``. Concrete subclasses 74 implement :meth:`evaluate`. 75 """ 76 77 @abstractmethod 78 async def evaluate(self, ctx: ExecutionContext) -> T: ...
class
Node(abc.ABC):
31class Node(ABC): # noqa: B024 — abstract by intent; the type_tag rule is enforced 32 """Common base for every LogixNG element.""" 33 34 type_tag: ClassVar[str] = "" # subclasses must set 35 36 def __init__(self, *, user_name: str = "", node_id: str | None = None) -> None: 37 if not self.type_tag: 38 raise TypeError(f"{type(self).__name__}.type_tag must be set on the class") 39 self.user_name = user_name 40 self.id = node_id or f"{type(self).__name__}-{uuid.uuid4().hex[:8]}" 41 42 def to_dict(self) -> dict[str, Any]: 43 """Serialize this node + its children to a dict suitable for YAML/JSON. 44 45 Default implementation captures ``type``, ``id``, ``user_name``. 46 Subclasses extend by overriding :meth:`extra_fields`. 47 """ 48 out: dict[str, Any] = {"type": self.type_tag, "id": self.id} 49 if self.user_name: 50 out["user_name"] = self.user_name 51 out.update(self.extra_fields()) 52 return out 53 54 def extra_fields(self) -> dict[str, Any]: 55 """Hook for subclasses to add their own fields to ``to_dict``.""" 56 return {} 57 58 def __repr__(self) -> str: 59 if self.user_name: 60 return f"<{type(self).__name__} {self.user_name!r}>" 61 return f"<{type(self).__name__} {self.id}>"
Common base for every LogixNG element.
def
to_dict(self) -> dict[str, typing.Any]:
42 def to_dict(self) -> dict[str, Any]: 43 """Serialize this node + its children to a dict suitable for YAML/JSON. 44 45 Default implementation captures ``type``, ``id``, ``user_name``. 46 Subclasses extend by overriding :meth:`extra_fields`. 47 """ 48 out: dict[str, Any] = {"type": self.type_tag, "id": self.id} 49 if self.user_name: 50 out["user_name"] = self.user_name 51 out.update(self.extra_fields()) 52 return out
Serialize this node + its children to a dict suitable for YAML/JSON.
Default implementation captures type, id, user_name.
Subclasses extend by overriding extra_fields().
64class Action(Node): 65 """A node that *does* something when executed.""" 66 67 @abstractmethod 68 async def execute(self, ctx: ExecutionContext) -> None: ...
A node that does something when executed.
71class Expression[T](Node): 72 """A node that evaluates to a value of type *T*. 73 74 The most common variant is ``Expression[bool]``. Concrete subclasses 75 implement :meth:`evaluate`. 76 """ 77 78 @abstractmethod 79 async def evaluate(self, ctx: ExecutionContext) -> T: ...
A node that evaluates to a value of type T.
The most common variant is Expression[bool]. Concrete subclasses
implement evaluate().