jmri_scripting.api

Public script-authoring API — the shim as an importable module.

The runtime shim's JmriShim.globals() injects sensors, turnouts, powermanager, etc. into every script's namespace, so scripts can use them without any import. That's the JMRI-Jython convention and it's covered.

This module is the authored alternative: scripts (or small helper modules a user writes) can from jmri_scripting.api import turnouts, powermanager and reference the same shim managers explicitly. The extra clarity helps when:

  • one script factors reusable code into a helper module and the helper can't rely on the injected globals (module top-level code runs at import time, not exec time);
  • an IDE / type-checker resolves imports and needs a real symbol;
  • a user prefers explicit-over-implicit imports.

Semantics: each name is a module-level attribute proxy that resolves via jython_shim._active_shim on every access. So the same import works across scripts on the same worker thread — each script binds its shim before exec() and unbinds on teardown.

  1"""Public script-authoring API — the shim as an importable module.
  2
  3The runtime shim's :meth:`JmriShim.globals` injects ``sensors``,
  4``turnouts``, ``powermanager``, etc. into every script's namespace, so
  5scripts can use them without any import. That's the JMRI-Jython
  6convention and it's covered.
  7
  8This module is the *authored* alternative: scripts (or small helper
  9modules a user writes) can ``from jmri_scripting.api import turnouts,
 10powermanager`` and reference the same shim managers explicitly. The
 11extra clarity helps when:
 12
 13* one script factors reusable code into a helper module and the helper
 14  can't rely on the injected globals (module top-level code runs at
 15  import time, not exec time);
 16* an IDE / type-checker resolves imports and needs a real symbol;
 17* a user prefers explicit-over-implicit imports.
 18
 19Semantics: each name is a **module-level attribute proxy** that
 20resolves via :data:`jython_shim._active_shim` on every access. So the
 21same import works across scripts on the same worker thread — each
 22script binds its shim before ``exec()`` and unbinds on teardown.
 23"""
 24from __future__ import annotations
 25
 26from typing import Any
 27
 28from jmri_scripting import jython_shim
 29
 30
 31class _ShimProxy:
 32    """Attribute proxy that reads *attr* off the active :class:`JmriShim`.
 33
 34    A single proxy instance is installed at module import time as one
 35    of ``sensors``, ``turnouts``, etc. Every attribute access on the
 36    proxy is forwarded to the active shim's matching attribute — so a
 37    helper module can capture ``turnouts`` at import time and every
 38    call still runs against the currently-executing script's shim.
 39
 40    Trying to use a proxy outside a running script raises the same
 41    clear error as :meth:`jython_shim._get_active_shim`.
 42    """
 43
 44    __slots__ = ("_attr",)
 45
 46    def __init__(self, attr: str) -> None:
 47        self._attr = attr
 48
 49    def _target(self) -> Any:
 50        return getattr(jython_shim._get_active_shim(), self._attr)
 51
 52    def __getattr__(self, name: str) -> Any:
 53        return getattr(self._target(), name)
 54
 55    def __iter__(self) -> Any:
 56        return iter(self._target())
 57
 58    def __len__(self) -> int:
 59        return len(self._target())
 60
 61    def __repr__(self) -> str:
 62        try:
 63            return f"<api.{self._attr} → {self._target()!r}>"
 64        except RuntimeError:
 65            return f"<api.{self._attr} (no active script)>"
 66
 67
 68sensors = _ShimProxy("sensors")
 69turnouts = _ShimProxy("turnouts")
 70lights = _ShimProxy("lights")
 71memories = _ShimProxy("memories")
 72reporters = _ShimProxy("reporters")
 73blocks = _ShimProxy("blocks")
 74routes = _ShimProxy("routes")
 75signals = _ShimProxy("signals")
 76masts = _ShimProxy("masts")
 77powermanager = _ShimProxy("powermanager")
 78consists = _ShimProxy("consists")
 79programmers = _ShimProxy("programmers")
 80
 81
 82# The state-constant enums are just re-exports — they aren't
 83# per-script.
 84from jmri_core.state import (  # noqa: E402
 85    LightState,
 86    PowerState,
 87    SensorState,
 88    ThrottleDirection,
 89    TurnoutState,
 90)
 91
 92
 93# Bare state constants, matching the injected globals namespace.
 94# JMRI Jython scripts commonly do ``t.setCommandedState(THROWN)``
 95# with no explicit import; exposing the same names here means a
 96# script that opts into ``from jmri_scripting.api import ...`` can
 97# still write the same code.
 98ACTIVE = SensorState.ACTIVE
 99INACTIVE = SensorState.INACTIVE
100CLOSED = TurnoutState.CLOSED
101THROWN = TurnoutState.THROWN
102ON = LightState.ON
103OFF = LightState.OFF
104FORWARD = ThrottleDirection.FORWARD
105REVERSE = ThrottleDirection.REVERSE
106
107
108__all__ = [
109    "ACTIVE",
110    "CLOSED",
111    "FORWARD",
112    "INACTIVE",
113    "LightState",
114    "OFF",
115    "ON",
116    "PowerState",
117    "REVERSE",
118    "SensorState",
119    "THROWN",
120    "ThrottleDirection",
121    "TurnoutState",
122    "blocks",
123    "consists",
124    "lights",
125    "masts",
126    "memories",
127    "powermanager",
128    "programmers",
129    "reporters",
130    "routes",
131    "sensors",
132    "signals",
133    "turnouts",
134]
ACTIVE = <SensorState.ACTIVE: 2>
CLOSED = <TurnoutState.CLOSED: 2>
FORWARD = <ThrottleDirection.FORWARD: 1>
INACTIVE = <SensorState.INACTIVE: 4>
class LightState(enum.IntEnum):
75class LightState(IntEnum):
76    """Matches ``jmri.Light.OFF/ON/INTERMEDIATE``."""
77
78    UNKNOWN      = 1
79    OFF          = 2
80    ON           = 4
81    INTERMEDIATE = 8

Matches jmri.Light.OFF/ON/INTERMEDIATE.

UNKNOWN = <LightState.UNKNOWN: 1>
OFF = <LightState.OFF: 2>
ON = <LightState.ON: 4>
INTERMEDIATE = <LightState.INTERMEDIATE: 8>
OFF = <LightState.OFF: 2>
ON = <LightState.ON: 4>
class PowerState(enum.IntEnum):
84class PowerState(IntEnum):
85    """Matches ``jmri.PowerManager.UNKNOWN/ON/OFF/IDLE``."""
86
87    UNKNOWN = 0
88    ON      = 2
89    OFF     = 4
90    IDLE    = 8

Matches jmri.PowerManager.UNKNOWN/ON/OFF/IDLE.

UNKNOWN = <PowerState.UNKNOWN: 0>
ON = <PowerState.ON: 2>
OFF = <PowerState.OFF: 4>
IDLE = <PowerState.IDLE: 8>
REVERSE = <ThrottleDirection.REVERSE: 0>
class SensorState(enum.IntEnum):
39class SensorState(IntEnum):
40    """Matches ``jmri.Sensor.UNKNOWN/ACTIVE/INACTIVE/INCONSISTENT``."""
41
42    UNKNOWN      = 1
43    ACTIVE       = 2
44    INACTIVE     = 4
45    INCONSISTENT = 8

Matches jmri.Sensor.UNKNOWN/ACTIVE/INACTIVE/INCONSISTENT.

UNKNOWN = <SensorState.UNKNOWN: 1>
ACTIVE = <SensorState.ACTIVE: 2>
INACTIVE = <SensorState.INACTIVE: 4>
INCONSISTENT = <SensorState.INCONSISTENT: 8>
THROWN = <TurnoutState.THROWN: 4>
class ThrottleDirection(enum.IntEnum):
102class ThrottleDirection(IntEnum):
103    """Locomotive direction. Matches NMRA convention."""
104
105    REVERSE = 0
106    FORWARD = 1

Locomotive direction. Matches NMRA convention.

REVERSE = <ThrottleDirection.REVERSE: 0>
FORWARD = <ThrottleDirection.FORWARD: 1>
class TurnoutState(enum.IntEnum):
16class TurnoutState(IntEnum):
17    """Matches ``jmri.Turnout.UNKNOWN/CLOSED/THROWN/INCONSISTENT`` Java constants."""
18
19    UNKNOWN      = 1
20    CLOSED       = 2
21    THROWN       = 4
22    INCONSISTENT = 8

Matches jmri.Turnout.UNKNOWN/CLOSED/THROWN/INCONSISTENT Java constants.

UNKNOWN = <TurnoutState.UNKNOWN: 1>
CLOSED = <TurnoutState.CLOSED: 2>
THROWN = <TurnoutState.THROWN: 4>
INCONSISTENT = <TurnoutState.INCONSISTENT: 8>
blocks = <api.blocks (no active script)>
consists = <api.consists (no active script)>
lights = <api.lights (no active script)>
masts = <api.masts (no active script)>
memories = <api.memories (no active script)>
powermanager = <api.powermanager (no active script)>
programmers = <api.programmers (no active script)>
reporters = <api.reporters (no active script)>
routes = <api.routes (no active script)>
sensors = <api.sensors (no active script)>
signals = <api.signals (no active script)>
turnouts = <api.turnouts (no active script)>