jmri_core.bean

NamedBean — the universal interface every JMRI domain object satisfies.

A bean has:

  • an immutable system_name — globally unique within a manager, formed of a connection prefix (e.g. "L" for LocoNet) plus a type letter (e.g. "T" for turnout) plus the system-specific address ("12");
  • an optional user_name — a human-friendly label;
  • a comment — free-form;
  • a state_changed Signal that fires on any state mutation;
  • a generic properties dict for ad-hoc extensions.

The NamedBean Protocol is structural; any object with the right shape satisfies it. AbstractNamedBean is the canonical implementation domain beans inherit.

 1"""``NamedBean`` — the universal interface every JMRI domain object satisfies.
 2
 3A bean has:
 4  * an immutable ``system_name`` — globally unique within a manager, formed of
 5    a connection prefix (e.g. ``"L"`` for LocoNet) plus a type letter (e.g.
 6    ``"T"`` for turnout) plus the system-specific address (``"12"``);
 7  * an optional ``user_name`` — a human-friendly label;
 8  * a ``comment`` — free-form;
 9  * a ``state_changed`` :class:`Signal` that fires on any state mutation;
10  * a generic ``properties`` dict for ad-hoc extensions.
11
12The :class:`NamedBean` Protocol is structural; any object with the right shape
13satisfies it. :class:`AbstractNamedBean` is the canonical implementation
14domain beans inherit.
15"""
16from __future__ import annotations
17
18from typing import Any, Protocol, runtime_checkable
19
20from jmri_core.events import Signal
21
22
23@runtime_checkable
24class NamedBean(Protocol):
25    """Structural protocol for any JMRI domain object."""
26
27    system_name: str
28    user_name: str | None
29    comment: str | None
30    state_changed: Signal[Any]
31
32
33class AbstractNamedBean:
34    """Concrete mixin providing the standard ``NamedBean`` machinery.
35
36    Subclasses extend this and add their state attributes + transition logic.
37    """
38
39    __slots__ = ("comment", "properties", "state_changed", "system_name", "user_name")
40
41    def __init__(self, system_name: str, user_name: str | None = None) -> None:
42        if not system_name:
43            raise ValueError("system_name must be a non-empty string")
44        self.system_name = system_name
45        self.user_name = user_name
46        self.comment: str | None = None
47        self.state_changed: Signal[Any] = Signal()
48        self.properties: dict[str, Any] = {}
49
50    @property
51    def display_name(self) -> str:
52        """``user_name`` if set, else ``system_name``. Mirrors JMRI's getDisplayName()."""
53        return self.user_name if self.user_name else self.system_name
54
55    def __repr__(self) -> str:
56        return f"{type(self).__name__}({self.system_name!r})"
57
58    def __eq__(self, other: object) -> bool:
59        # Equality by (type, system_name) — JMRI manager invariant.
60        if type(self) is not type(other):
61            return False
62        return self.system_name == other.system_name
63
64    def __hash__(self) -> int:
65        return hash((type(self).__name__, self.system_name))
@runtime_checkable
class NamedBean(typing.Protocol):
24@runtime_checkable
25class NamedBean(Protocol):
26    """Structural protocol for any JMRI domain object."""
27
28    system_name: str
29    user_name: str | None
30    comment: str | None
31    state_changed: Signal[Any]

Structural protocol for any JMRI domain object.

NamedBean(*args, **kwargs)
1945def _no_init_or_replace_init(self, *args, **kwargs):
1946    cls = type(self)
1947
1948    if cls._is_protocol:
1949        raise TypeError('Protocols cannot be instantiated')
1950
1951    # Already using a custom `__init__`. No need to calculate correct
1952    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1953    if cls.__init__ is not _no_init_or_replace_init:
1954        return
1955
1956    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1957    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1958    # searches for a proper new `__init__` in the MRO. The new `__init__`
1959    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1960    # instantiation of the protocol subclass will thus use the new
1961    # `__init__` and no longer call `_no_init_or_replace_init`.
1962    for base in cls.__mro__:
1963        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1964        if init is not _no_init_or_replace_init:
1965            cls.__init__ = init
1966            break
1967    else:
1968        # should not happen
1969        cls.__init__ = object.__init__
1970
1971    cls.__init__(self, *args, **kwargs)
system_name: str
user_name: str | None
comment: str | None
state_changed: jmri_core.Signal[typing.Any]
class AbstractNamedBean:
34class AbstractNamedBean:
35    """Concrete mixin providing the standard ``NamedBean`` machinery.
36
37    Subclasses extend this and add their state attributes + transition logic.
38    """
39
40    __slots__ = ("comment", "properties", "state_changed", "system_name", "user_name")
41
42    def __init__(self, system_name: str, user_name: str | None = None) -> None:
43        if not system_name:
44            raise ValueError("system_name must be a non-empty string")
45        self.system_name = system_name
46        self.user_name = user_name
47        self.comment: str | None = None
48        self.state_changed: Signal[Any] = Signal()
49        self.properties: dict[str, Any] = {}
50
51    @property
52    def display_name(self) -> str:
53        """``user_name`` if set, else ``system_name``. Mirrors JMRI's getDisplayName()."""
54        return self.user_name if self.user_name else self.system_name
55
56    def __repr__(self) -> str:
57        return f"{type(self).__name__}({self.system_name!r})"
58
59    def __eq__(self, other: object) -> bool:
60        # Equality by (type, system_name) — JMRI manager invariant.
61        if type(self) is not type(other):
62            return False
63        return self.system_name == other.system_name
64
65    def __hash__(self) -> int:
66        return hash((type(self).__name__, self.system_name))

Concrete mixin providing the standard NamedBean machinery.

Subclasses extend this and add their state attributes + transition logic.

AbstractNamedBean(system_name: str, user_name: str | None = None)
42    def __init__(self, system_name: str, user_name: str | None = None) -> None:
43        if not system_name:
44            raise ValueError("system_name must be a non-empty string")
45        self.system_name = system_name
46        self.user_name = user_name
47        self.comment: str | None = None
48        self.state_changed: Signal[Any] = Signal()
49        self.properties: dict[str, Any] = {}
system_name
user_name
comment: str | None
state_changed: jmri_core.Signal[typing.Any]
properties: dict[str, typing.Any]
display_name: str
51    @property
52    def display_name(self) -> str:
53        """``user_name`` if set, else ``system_name``. Mirrors JMRI's getDisplayName()."""
54        return self.user_name if self.user_name else self.system_name

user_name if set, else system_name. Mirrors JMRI's getDisplayName().