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 is Expression[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 id used for serialization and event correlation;
  • has an optional user_name for human-readable identification;
  • declares a class-level type_tag used 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.

Node(*, user_name: str = '', node_id: str | None = None)
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]}"
type_tag: ClassVar[str] = ''
user_name
id
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().

def extra_fields(self) -> dict[str, typing.Any]:
54    def extra_fields(self) -> dict[str, Any]:
55        """Hook for subclasses to add their own fields to ``to_dict``."""
56        return {}

Hook for subclasses to add their own fields to to_dict.

class Action(Node):
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.

@abstractmethod
async def execute(self, ctx: jmri_logixng.ExecutionContext) -> None:
67    @abstractmethod
68    async def execute(self, ctx: ExecutionContext) -> None: ...
class Expression(Node, typing.Generic[T]):
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().

@abstractmethod
async def evaluate(self, ctx: jmri_logixng.ExecutionContext) -> 'T':
78    @abstractmethod
79    async def evaluate(self, ctx: ExecutionContext) -> T: ...