jmri_scripting.jython_shim

Synthetic jmri namespace so real JMRI Jython scripts import unchanged.

JMRI's Jython environment exposes classes and utilities under the jmri.* package tree — jmri.InstanceManager, jmri.Path, jmri.jmrit.automat.AbstractAutomaton, and so on. Our runtime's injected globals cover the most common access pattern (sensors, turnouts, ACTIVE, CLOSED), but scripts that reach in via the JMRI namespace fail with ImportError: No module named 'jmri' before they even start.

This module builds a synthetic module tree and registers it in sys.modules on first install() call. Attribute coverage targets the top-200 paths from stage-0/contracts/script-api.md — the tail below rank 200 is a long list of one-off references not worth the maintenance cost.

Design:

  • Each synthetic module has __all__ for the attributes we actually populate. Attribute misses fall through _missing() which raises a clear AttributeError naming the shim's coverage rank + a hint.
  • Bean classes (jmri.Turnout, jmri.Sensor, …) are re-exports of jmri_core.beans.* so isinstance checks work.
  • jmri.InstanceManager is a static-method class whose getDefault(cls) maps a JMRI Java class object (or one of our shim classes) to the corresponding shim manager on the running JmriShim instance. The shim is picked up from a thread-local set by the runner just before exec().
  • Modules under jmri.jmrix.* and the deep jmri.jmrit.* sub-trees are stubs — scripts that touch them get a helpful error instead of a bare ImportError.
  1"""Synthetic ``jmri`` namespace so real JMRI Jython scripts import unchanged.
  2
  3JMRI's Jython environment exposes classes and utilities under the
  4``jmri.*`` package tree — ``jmri.InstanceManager``, ``jmri.Path``,
  5``jmri.jmrit.automat.AbstractAutomaton``, and so on. Our runtime's
  6injected globals cover the *most* common access pattern (``sensors``,
  7``turnouts``, ``ACTIVE``, ``CLOSED``), but scripts that reach in via the
  8JMRI namespace fail with ``ImportError: No module named 'jmri'`` before
  9they even start.
 10
 11This module builds a synthetic module tree and registers it in
 12``sys.modules`` on first :func:`install` call. Attribute coverage
 13targets the top-200 paths from ``stage-0/contracts/script-api.md`` —
 14the tail below rank 200 is a long list of one-off references not worth
 15the maintenance cost.
 16
 17Design:
 18
 19* Each synthetic module has ``__all__`` for the attributes we actually
 20  populate. Attribute misses fall through :func:`_missing` which raises
 21  a clear ``AttributeError`` naming the shim's coverage rank + a hint.
 22* Bean classes (:class:`jmri.Turnout`, :class:`jmri.Sensor`, …) are
 23  re-exports of :mod:`jmri_core.beans.*` so ``isinstance`` checks work.
 24* :class:`jmri.InstanceManager` is a static-method class whose
 25  ``getDefault(cls)`` maps a JMRI Java class object (or one of our
 26  shim classes) to the corresponding shim manager on the running
 27  :class:`JmriShim` instance. The shim is picked up from a
 28  thread-local set by the runner just before ``exec()``.
 29* Modules under ``jmri.jmrix.*`` and the deep ``jmri.jmrit.*`` sub-trees
 30  are stubs — scripts that touch them get a helpful error instead of a
 31  bare ``ImportError``.
 32"""
 33from __future__ import annotations
 34
 35import types
 36from typing import TYPE_CHECKING, Any
 37
 38from jmri_core.beans.block import Block
 39from jmri_core.beans.light import Light
 40from jmri_core.beans.memory import Memory
 41from jmri_core.beans.reporter import Reporter
 42from jmri_core.beans.route import Route
 43from jmri_core.beans.sensor import Sensor
 44from jmri_core.beans.signal_head import SignalHead
 45from jmri_core.beans.signal_mast import SignalMast
 46from jmri_core.beans.throttle import Throttle
 47from jmri_core.beans.turnout import Turnout
 48from jmri_core.state import LightState, PowerState, SensorState, TurnoutState
 49
 50from jmri_scripting.automaton import AbstractAutomaton
 51
 52if TYPE_CHECKING:
 53    from jmri_scripting.shim import JmriShim
 54
 55
 56# Thread-local slot the runner fills right before exec() so
 57# ``InstanceManager.getDefault(...)`` can find the active shim.
 58import threading  # noqa: E402
 59_active_shim: threading.local = threading.local()
 60
 61
 62def _get_active_shim() -> JmriShim:
 63    shim = getattr(_active_shim, "value", None)
 64    if shim is None:
 65        raise RuntimeError(
 66            "jmri.InstanceManager accessed outside a running script; the "
 67            "runtime shim is only available on script worker threads.",
 68        )
 69    return shim
 70
 71
 72def _bind_shim(shim: JmriShim) -> None:
 73    _active_shim.value = shim
 74
 75
 76def _unbind_shim() -> None:
 77    if hasattr(_active_shim, "value"):
 78        del _active_shim.value
 79
 80
 81# ----- jmri.Path -----------------------------------------------------------
 82
 83
 84class Path:
 85    """Layout Path constants and helpers.
 86
 87    JMRI's ``jmri.Path`` is a compass-direction bit field used by block
 88    tracking and route logic. We expose the constants and
 89    :meth:`decodeDirection`; anything that needs the full path/turnout
 90    linkage falls back to a clear AttributeError.
 91    """
 92
 93    NONE = 0
 94    NORTH = 1
 95    SOUTH = 2
 96    EAST = 4
 97    WEST = 8
 98    CW = 16      # clockwise
 99    CCW = 32     # counter-clockwise
100    LEFT = 64
101    RIGHT = 128
102    UP = 256
103    DOWN = 512
104
105    _NAMES = {
106        NONE: "None", NORTH: "North", SOUTH: "South", EAST: "East",
107        WEST: "West", CW: "CW", CCW: "CCW", LEFT: "Left",
108        RIGHT: "Right", UP: "Up", DOWN: "Down",
109    }
110
111    @staticmethod
112    def decodeDirection(direction: int) -> str:  # noqa: N802 — JMRI casing
113        """Return the human-readable name for a Path direction bit."""
114        return Path._NAMES.get(direction, f"Unknown({direction})")
115
116
117# ----- jmri.DccLocoAddress -------------------------------------------------
118
119
120class DccLocoAddress:
121    """Address container matching JMRI's ``jmri.DccLocoAddress`` shape.
122
123    Constructor + ``getNumber`` + ``isLongAddress`` cover every use in
124    the top-200 scripts. Equality follows JMRI's semantics: two
125    addresses are equal iff both number and long-address flag match.
126    """
127
128    __slots__ = ("_num", "_long")
129
130    def __init__(self, number: int, long_address: bool) -> None:
131        self._num = int(number)
132        self._long = bool(long_address)
133
134    def getNumber(self) -> int:  # noqa: N802
135        return self._num
136
137    def isLongAddress(self) -> bool:  # noqa: N802
138        return self._long
139
140    def __eq__(self, other: object) -> bool:
141        if not isinstance(other, DccLocoAddress):
142            return NotImplemented
143        return self._num == other._num and self._long == other._long
144
145    def __hash__(self) -> int:
146        return hash((self._num, self._long))
147
148    def __repr__(self) -> str:
149        return f"DccLocoAddress({self._num}, long={self._long})"
150
151
152# ----- jmri.PowerManager ---------------------------------------------------
153
154
155class PowerManager:
156    """Static holder for JMRI PowerManager constants + instance methods.
157
158    The class itself doubles as the type key for
159    ``InstanceManager.getDefault(PowerManager)``; instances of it (rare
160    in scripts) fall through to the active shim's PowerShim.
161    """
162
163    ON = PowerState.ON
164    OFF = PowerState.OFF
165    IDLE = PowerState.IDLE
166    UNKNOWN = PowerState.UNKNOWN
167
168
169# ---- InstanceManager.getDefault type keys --------------------------------
170
171# These are placeholder classes used as class keys — scripts do
172# ``InstanceManager.getDefault(ConsistManager)`` and we route by
173# ``is`` identity to the shim's manager. Kept as bare classes (not
174# instances) to match how JMRI Java Jython scripts pass the class
175# object itself as the lookup key.
176
177
178class ConsistManager:
179    """Type key for :meth:`_InstanceManager.getDefault` — resolves to
180    the shim's :class:`~jmri_scripting.shim._ConsistShim`. See the
181    active shim's ``consists`` attribute for the runtime object."""
182
183
184class ProgrammerManager:
185    """Type key for :meth:`_InstanceManager.getDefault` — resolves to
186    the shim's :class:`~jmri_scripting.shim._ProgrammerShim`."""
187
188
189# ----- jmri.InstanceManager -----------------------------------------------
190
191
192class _InstanceManager:
193    """Service locator matching JMRI's ``jmri.InstanceManager``.
194
195    ``getDefault(cls)`` returns the shim manager that services *cls* on
196    the currently running script's :class:`JmriShim`. Recognised
197    classes:
198
199    * :class:`Sensor`, :class:`Turnout`, :class:`Light`, :class:`Memory`,
200      :class:`Reporter`, :class:`Block`, :class:`Route`,
201      :class:`SignalHead`, :class:`SignalMast` — return the matching
202      manager.
203    * :class:`PowerManager` — return the shim's power manager.
204
205    Unknown types raise a clear AttributeError so a script author can
206    tell it's a shim-coverage gap, not a JMRI runtime bug.
207    """
208
209    @staticmethod
210    def getDefault(cls: Any) -> Any:  # noqa: N802 — JMRI casing
211        shim = _get_active_shim()
212        # Map class → shim manager. Uses identity comparison because
213        # scripts import the same class objects we exported.
214        if cls is Sensor:
215            return shim.sensors
216        if cls is Turnout:
217            return shim.turnouts
218        if cls is Light:
219            return shim.lights
220        if cls is Memory:
221            return shim.memories
222        if cls is Reporter:
223            return shim.reporters
224        if cls is Block:
225            return shim.blocks
226        if cls is Route:
227            return shim.routes
228        if cls is SignalHead:
229            return shim.signals
230        if cls is SignalMast:
231            return shim.masts
232        if cls is PowerManager:
233            return shim.powermanager
234        if cls is ConsistManager:
235            return shim.consists
236        if cls is ProgrammerManager:
237            return shim.programmers
238        raise AttributeError(
239            f"InstanceManager.getDefault({cls!r}) is not shimmed. Common "
240            "shimmed types: Sensor, Turnout, Light, Memory, Reporter, "
241            "Block, Route, SignalHead, SignalMast, PowerManager, "
242            "ConsistManager, ProgrammerManager.",
243        )
244
245    @staticmethod
246    def getList(cls: Any) -> list[Any]:  # noqa: N802
247        """Return all instances of the manager's beans as a plain list.
248
249        JMRI's version returns a Java List; here we hand back a Python
250        list of shim-wrapped beans. Scripts that call ``len(...)`` or
251        iterate over the return value work either way.
252        """
253        mgr = _InstanceManager.getDefault(cls)
254        return list(mgr)
255
256
257# ----- jmri.util.FileUtil --------------------------------------------------
258
259
260class _FileUtil:
261    """Subset of ``jmri.util.FileUtil`` used by the top-200 scripts.
262
263    JMRI's version implements a portable ``preference:``, ``profile:``,
264    ``resource:`` URL scheme. Our lightweight version maps the same
265    prefixes onto the current profile's directories where possible,
266    and returns raw paths otherwise so most script uses ``open(...)``
267    calls still work.
268    """
269
270    @staticmethod
271    def getExternalFilename(name: str) -> str:  # noqa: N802
272        """Resolve a JMRI-style filename to a native path.
273
274        Supported prefixes:
275
276        * ``profile:X`` → ``<profile>/X`` when a profile is bound.
277        * ``preference:X`` → same as profile (JMRI conflates them at
278          this layer).
279        * Anything else → passthrough (assumed already-native).
280        """
281        for prefix in ("profile:", "preference:"):
282            if name.startswith(prefix):
283                try:
284                    shim = _get_active_shim()
285                    profile_dir = getattr(shim, "profile_dir", None)
286                    if profile_dir is not None:
287                        return str(profile_dir / name[len(prefix):])
288                except RuntimeError:
289                    pass
290                return name[len(prefix):]
291        return name
292
293
294# ----- module tree construction --------------------------------------------
295
296
297def _missing(module_name: str, attr: str) -> "AttributeError":
298    """Build a helpful AttributeError for an un-shimmed attribute."""
299    return AttributeError(
300        f"module {module_name!r} has no attribute {attr!r}. "
301        "The Jython namespace shim covers the top-200 paths from "
302        "stage-0/contracts/script-api.md; this one falls outside. "
303        "Either use the shim's injected globals (sensors, turnouts, "
304        "…) or file an issue if this path is important.",
305    )
306
307
308def _make_module(name: str, **attrs: Any) -> types.ModuleType:
309    """Create a synthetic ModuleType with attribute-level fallthrough."""
310    mod = types.ModuleType(name)
311    mod.__all__ = list(attrs.keys())
312    for k, v in attrs.items():
313        setattr(mod, k, v)
314
315    def _module_getattr(item: str) -> Any:
316        raise _missing(name, item)
317
318    mod.__getattr__ = _module_getattr  # type: ignore[method-assign]
319    return mod
320
321
322def _build_tree() -> dict[str, types.ModuleType]:
323    """Assemble the ``jmri.*`` module tree without registering yet."""
324    # jmri.jmrit.automat — the AbstractAutomaton lives here in JMRI Java.
325    jmri_jmrit_automat = _make_module(
326        "jmri.jmrit.automat", AbstractAutomaton=AbstractAutomaton,
327    )
328    # Populated stubs for the sub-namespaces most scripts probe by
329    # existence check; each raises via _missing on unknown attribute
330    # access.
331    jmri_jmrit_roster = _make_module("jmri.jmrit.roster")
332    jmri_jmrit_operations = _make_module("jmri.jmrit.operations")
333    jmri_jmrit_operations_trains = _make_module(
334        "jmri.jmrit.operations.trains",
335    )
336    jmri_jmrit_operations_rollingstock = _make_module(
337        "jmri.jmrit.operations.rollingstock",
338    )
339    jmri_jmrit_display = _make_module("jmri.jmrit.display")
340    jmri_jmrit_catalog = _make_module("jmri.jmrit.catalog")
341    jmri_jmrit_tracker = _make_module("jmri.jmrit.tracker")
342
343    jmri_jmrit = _make_module(
344        "jmri.jmrit",
345        automat=jmri_jmrit_automat,
346        roster=jmri_jmrit_roster,
347        operations=jmri_jmrit_operations,
348        display=jmri_jmrit_display,
349        catalog=jmri_jmrit_catalog,
350        tracker=jmri_jmrit_tracker,
351    )
352
353    jmri_jmrix = _make_module("jmri.jmrix")
354    jmri_jmrix_loconet = _make_module("jmri.jmrix.loconet")
355    jmri_jmrix_mqtt = _make_module("jmri.jmrix.mqtt")
356
357    jmri_util = _make_module(
358        "jmri.util",
359        FileUtil=_FileUtil,
360    )
361    jmri_implementation = _make_module("jmri.implementation")
362
363    jmri = _make_module(
364        "jmri",
365        # Service locator + constants used by the vast majority of scripts.
366        InstanceManager=_InstanceManager,
367        Path=Path,
368        PowerManager=PowerManager,
369        ConsistManager=ConsistManager,
370        ProgrammerManager=ProgrammerManager,
371        DccLocoAddress=DccLocoAddress,
372        # Bean class re-exports — used for isinstance and as
373        # InstanceManager.getDefault keys.
374        Sensor=Sensor,
375        Turnout=Turnout,
376        Light=Light,
377        Memory=Memory,
378        Reporter=Reporter,
379        Block=Block,
380        Route=Route,
381        SignalHead=SignalHead,
382        SignalMast=SignalMast,
383        Throttle=Throttle,
384        # Wire-value enums, so a script that does
385        # ``jmri.Turnout.CLOSED`` still works via the class attribute
386        # even though our Turnout doesn't define a nested constant.
387        # We expose them as sibling attributes so both forms resolve.
388        SensorState=SensorState,
389        TurnoutState=TurnoutState,
390        LightState=LightState,
391        PowerState=PowerState,
392        # Submodules — must be settable so ``from jmri import jmrit`` works.
393        jmrit=jmri_jmrit,
394        jmrix=jmri_jmrix,
395        util=jmri_util,
396        implementation=jmri_implementation,
397    )
398
399    return {
400        "jmri": jmri,
401        "jmri.jmrit": jmri_jmrit,
402        "jmri.jmrit.automat": jmri_jmrit_automat,
403        "jmri.jmrit.roster": jmri_jmrit_roster,
404        "jmri.jmrit.operations": jmri_jmrit_operations,
405        "jmri.jmrit.operations.trains": jmri_jmrit_operations_trains,
406        "jmri.jmrit.operations.rollingstock": jmri_jmrit_operations_rollingstock,
407        "jmri.jmrit.display": jmri_jmrit_display,
408        "jmri.jmrit.catalog": jmri_jmrit_catalog,
409        "jmri.jmrit.tracker": jmri_jmrit_tracker,
410        "jmri.jmrix": jmri_jmrix,
411        "jmri.jmrix.loconet": jmri_jmrix_loconet,
412        "jmri.jmrix.mqtt": jmri_jmrix_mqtt,
413        "jmri.util": jmri_util,
414        "jmri.implementation": jmri_implementation,
415    }
416
417
418_INSTALLED = False
419
420
421def install() -> None:
422    """Register the synthetic ``jmri.*`` modules in :data:`sys.modules`.
423
424    Idempotent — invoked at module import time (see the bottom of this
425    file) so ``import jmri`` works immediately, and again from
426    :meth:`ScriptRunner.__init__` as a defensive belt. After this runs,
427    scripts can do ``from jmri import InstanceManager`` and
428    ``from jmri.jmrit.automat import AbstractAutomaton`` without
429    triggering the real Python import machinery.
430
431    ``sys.modules.setdefault`` is used so that if someone has a
432    legitimate ``jmri`` package pip-installed (rare but possible), we
433    don't clobber it — the real package wins.
434    """
435    global _INSTALLED
436    if _INSTALLED:
437        return
438    import sys  # noqa: PLC0415
439    for name, mod in _build_tree().items():
440        sys.modules.setdefault(name, mod)
441    _INSTALLED = True
442
443
444# Install eagerly. Importing this module means the caller wants the
445# ``jmri.*`` names available; running the registration at load time
446# keeps ``import jmri`` working from any code path that touches
447# jmri_scripting, including tests that inspect the shim without
448# constructing a ScriptRunner.
449install()
class Path:
 85class Path:
 86    """Layout Path constants and helpers.
 87
 88    JMRI's ``jmri.Path`` is a compass-direction bit field used by block
 89    tracking and route logic. We expose the constants and
 90    :meth:`decodeDirection`; anything that needs the full path/turnout
 91    linkage falls back to a clear AttributeError.
 92    """
 93
 94    NONE = 0
 95    NORTH = 1
 96    SOUTH = 2
 97    EAST = 4
 98    WEST = 8
 99    CW = 16      # clockwise
100    CCW = 32     # counter-clockwise
101    LEFT = 64
102    RIGHT = 128
103    UP = 256
104    DOWN = 512
105
106    _NAMES = {
107        NONE: "None", NORTH: "North", SOUTH: "South", EAST: "East",
108        WEST: "West", CW: "CW", CCW: "CCW", LEFT: "Left",
109        RIGHT: "Right", UP: "Up", DOWN: "Down",
110    }
111
112    @staticmethod
113    def decodeDirection(direction: int) -> str:  # noqa: N802 — JMRI casing
114        """Return the human-readable name for a Path direction bit."""
115        return Path._NAMES.get(direction, f"Unknown({direction})")

Layout Path constants and helpers.

JMRI's jmri.Path is a compass-direction bit field used by block tracking and route logic. We expose the constants and decodeDirection(); anything that needs the full path/turnout linkage falls back to a clear AttributeError.

NONE = 0
NORTH = 1
SOUTH = 2
EAST = 4
WEST = 8
CW = 16
CCW = 32
LEFT = 64
RIGHT = 128
UP = 256
DOWN = 512
@staticmethod
def decodeDirection(direction: int) -> str:
112    @staticmethod
113    def decodeDirection(direction: int) -> str:  # noqa: N802 — JMRI casing
114        """Return the human-readable name for a Path direction bit."""
115        return Path._NAMES.get(direction, f"Unknown({direction})")

Return the human-readable name for a Path direction bit.

class DccLocoAddress:
121class DccLocoAddress:
122    """Address container matching JMRI's ``jmri.DccLocoAddress`` shape.
123
124    Constructor + ``getNumber`` + ``isLongAddress`` cover every use in
125    the top-200 scripts. Equality follows JMRI's semantics: two
126    addresses are equal iff both number and long-address flag match.
127    """
128
129    __slots__ = ("_num", "_long")
130
131    def __init__(self, number: int, long_address: bool) -> None:
132        self._num = int(number)
133        self._long = bool(long_address)
134
135    def getNumber(self) -> int:  # noqa: N802
136        return self._num
137
138    def isLongAddress(self) -> bool:  # noqa: N802
139        return self._long
140
141    def __eq__(self, other: object) -> bool:
142        if not isinstance(other, DccLocoAddress):
143            return NotImplemented
144        return self._num == other._num and self._long == other._long
145
146    def __hash__(self) -> int:
147        return hash((self._num, self._long))
148
149    def __repr__(self) -> str:
150        return f"DccLocoAddress({self._num}, long={self._long})"

Address container matching JMRI's jmri.DccLocoAddress shape.

Constructor + getNumber + isLongAddress cover every use in the top-200 scripts. Equality follows JMRI's semantics: two addresses are equal iff both number and long-address flag match.

DccLocoAddress(number: int, long_address: bool)
131    def __init__(self, number: int, long_address: bool) -> None:
132        self._num = int(number)
133        self._long = bool(long_address)
def getNumber(self) -> int:
135    def getNumber(self) -> int:  # noqa: N802
136        return self._num
def isLongAddress(self) -> bool:
138    def isLongAddress(self) -> bool:  # noqa: N802
139        return self._long
class PowerManager:
156class PowerManager:
157    """Static holder for JMRI PowerManager constants + instance methods.
158
159    The class itself doubles as the type key for
160    ``InstanceManager.getDefault(PowerManager)``; instances of it (rare
161    in scripts) fall through to the active shim's PowerShim.
162    """
163
164    ON = PowerState.ON
165    OFF = PowerState.OFF
166    IDLE = PowerState.IDLE
167    UNKNOWN = PowerState.UNKNOWN

Static holder for JMRI PowerManager constants + instance methods.

The class itself doubles as the type key for InstanceManager.getDefault(PowerManager); instances of it (rare in scripts) fall through to the active shim's PowerShim.

ON = <PowerState.ON: 2>
OFF = <PowerState.OFF: 4>
IDLE = <PowerState.IDLE: 8>
UNKNOWN = <PowerState.UNKNOWN: 0>
class ConsistManager:
179class ConsistManager:
180    """Type key for :meth:`_InstanceManager.getDefault` — resolves to
181    the shim's :class:`~jmri_scripting.shim._ConsistShim`. See the
182    active shim's ``consists`` attribute for the runtime object."""

Type key for _InstanceManager.getDefault() — resolves to the shim's ~jmri_scripting.shim._ConsistShim. See the active shim's consists attribute for the runtime object.

class ProgrammerManager:
185class ProgrammerManager:
186    """Type key for :meth:`_InstanceManager.getDefault` — resolves to
187    the shim's :class:`~jmri_scripting.shim._ProgrammerShim`."""

Type key for _InstanceManager.getDefault() — resolves to the shim's ~jmri_scripting.shim._ProgrammerShim.

def install() -> None:
422def install() -> None:
423    """Register the synthetic ``jmri.*`` modules in :data:`sys.modules`.
424
425    Idempotent — invoked at module import time (see the bottom of this
426    file) so ``import jmri`` works immediately, and again from
427    :meth:`ScriptRunner.__init__` as a defensive belt. After this runs,
428    scripts can do ``from jmri import InstanceManager`` and
429    ``from jmri.jmrit.automat import AbstractAutomaton`` without
430    triggering the real Python import machinery.
431
432    ``sys.modules.setdefault`` is used so that if someone has a
433    legitimate ``jmri`` package pip-installed (rare but possible), we
434    don't clobber it — the real package wins.
435    """
436    global _INSTALLED
437    if _INSTALLED:
438        return
439    import sys  # noqa: PLC0415
440    for name, mod in _build_tree().items():
441        sys.modules.setdefault(name, mod)
442    _INSTALLED = True

Register the synthetic jmri.* modules in sys.modules.

Idempotent — invoked at module import time (see the bottom of this file) so import jmri works immediately, and again from ScriptRunner.__init__() as a defensive belt. After this runs, scripts can do from jmri import InstanceManager and from jmri.jmrit.automat import AbstractAutomaton without triggering the real Python import machinery.

sys.modules.setdefault is used so that if someone has a legitimate jmri package pip-installed (rare but possible), we don't clobber it — the real package wins.