Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 54 additions & 114 deletions ovos_utils/fakebus.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from threading import Event

from ovos_utils.log import LOG, log_deprecation
from ovos_spec_tools import NamespaceTranslator
from pyee import EventEmitter


Expand All @@ -17,16 +16,11 @@ def dig_for_message():
return None


# sentinel: lets us tell "kwarg not passed" apart from "kwarg passed True/False"
_UNSET = object()


def _bus_flag(env_var, config_key, default=True):
def _bus_flag(env_var, config_key, default=False):
"""Resolve a boolean bus flag the way ``MessageBusClient._bus_flag`` does.

Precedence: env var (when set) > ``websocket.<config_key>`` in ovos_config
> ``default``. The env var wins when set to a truthy/falsy string; ovos_config
is optional, so any failure to read it falls back to ``default``.
> ``default``.

Kept layering-clean: mirrors ``ovos_bus_client.client.client._bus_flag``
without importing from bus-client (bus-client depends on utils, not vice-versa).
Expand All @@ -41,20 +35,53 @@ def _bus_flag(env_var, config_key, default=True):
return default


def _resolve_bus_flags(kwargs):
"""Build the namespace ``NamespaceTranslator`` for a fake bus instance.

An explicitly-passed ``modernize``/``emit_legacy`` kwarg wins (back-compat for
callers passing ``emit_legacy=True/False``); otherwise the flag is resolved via
env var -> ``websocket.*`` config -> default ``True``, matching the real client.
# --- the legacy wire bridge is GONE -----------------------------------------
#
# The fake bus used to mirror ``MessageBusClient``'s two migration bridges: the
# OVOS-MSG-1 namespace bridge (a spec topic also reached listeners on the
# legacy topic it replaced, and the reverse) and the OVOS-INTENT-4 intent-topic
# bridge (a canonical ``<skill_id>:<intent>`` dispatch also reached the old
# ``<skill_id>:<intent>.intent`` spelling). Both were removed from the real
# client, so both are removed here — a test double that kept them would let a
# harness pass against behaviour the fleet no longer has.

#: Bus flags that used to steer the bridge.
_REMOVED_BRIDGE_FLAGS = (
("OVOS_BUS_EMIT_LEGACY", "emit_legacy"),
("OVOS_BUS_MODERNIZE", "modernize"),
("OVOS_BUS_INTENT_REEMIT_BLANKET", "intent_reemit_blanket"),
)


def _reject_removed_bridge_flags(kwargs):
"""Handle a caller that still asks for the removed bridge.

The two spellings get different treatment on purpose.

An **env var or config entry** is an operator asking a live deployment to
keep the legacy topics on the wire. That belief is now wrong, and silence
would hand them a fleet that drops messages, so it raises — the same error
the real ``MessageBusClient`` raises.

A **constructor kwarg** is a test harness, not a deployment. Harnesses
across the ecosystem pass ``emit_legacy=True`` unconditionally (ovoscope's
``MiniCroft`` is one), and raising there would break every one of them at
construction without telling anybody anything useful. The kwarg is
accepted, ignored, and warned about instead.
"""
modernize = kwargs.get("modernize", _UNSET)
if modernize is _UNSET:
modernize = _bus_flag("OVOS_BUS_MODERNIZE", "modernize", default=True)
emit_legacy = kwargs.get("emit_legacy", _UNSET)
if emit_legacy is _UNSET:
emit_legacy = _bus_flag("OVOS_BUS_EMIT_LEGACY", "emit_legacy", default=True)
return NamespaceTranslator(modernize=modernize, emit_legacy=emit_legacy)
for env_var, config_key in _REMOVED_BRIDGE_FLAGS:
if _bus_flag(env_var, config_key, default=False):
raise RuntimeError(
f"'{config_key}' (env {env_var}) is enabled, but the legacy "
"wire bridge was removed from the fake bus, matching "
"ovos-bus-client. Legacy bus topics are no longer emitted or "
"mirrored. Migrate the producers and consumers to the "
f"OVOS-MSG-1 spec topics, then unset '{config_key}'.")
if kwargs.get(config_key):
log_deprecation(
f"the '{config_key}' kwarg no longer does anything: the "
"legacy wire bridge was removed from the fake bus. Drop it "
"from the call.", "1.0.0")


class FakeBus:
Expand All @@ -63,13 +90,9 @@ def __init__(self, *args, **kwargs):
self.session_id = "default"
self.ee = kwargs.get("emitter") or EventEmitter()
self.ee.on("error", self.on_error)
# mirror MessageBusClient's namespace migration so the test/satellite
# double bridges legacy<->ovos.* topics identically. Flags resolve the
# same way the real client does: explicit modernize=/emit_legacy= kwarg
# wins, else env var -> websocket.* config -> default on.
self._translator = _resolve_bus_flags(kwargs)
self._handler_guards = {} # handler -> shared mirror-guard
self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...]
# the migration window is over: no namespace bridge, no intent-topic
# twin, no mirror-guard — matching MessageBusClient.
_reject_removed_bridge_flags(kwargs)
self.on_open()
try:
self.session_id = kwargs["session"].session_id
Expand All @@ -80,22 +103,6 @@ def __init__(self, *args, **kwargs):
self.on_default_session_update)

def on(self, msg_type, handler):
# wrap handlers on migrated topics so a handler subscribed to both the
# legacy and ovos.* topic fires once (the mirror is dropped)
if self._translator.is_migrated(msg_type):
guard = self._handler_guards.get(handler)
if guard is None:
guard = self._translator.new_mirror_guard()
self._handler_guards[handler] = guard

def wrapped(message=None):
if guard(message):
return
return handler(message)

self.ee.on(msg_type, wrapped)
self._dedup_registrations.setdefault(handler, []).append((msg_type, wrapped))
return
self.ee.on(msg_type, handler)

def once(self, msg_type, handler):
Expand Down Expand Up @@ -126,20 +133,6 @@ def emit(self, message):
self.ee.emit(message.msg_type, message)
except Exception as e:
LOG.exception(f"Error in event handler for '{message.msg_type}': {e}")
# namespace migration: also dispatch the counterpart topic(s) so a
# listener on either namespace receives the event (consumers dedupe).
# the mirrored payload is reshaped into the counterpart topic's shape
# (identity for payload-compatible renames, a per-topic transform for
# shape-changing ones) so a listener on it receives the payload in *its*
# shape -- matching MessageBusClient's bridge.
for topic in self._translator.counterpart_topics(message.msg_type):
try:
translated = self._translator.translate_payload(
from_topic=message.msg_type, to_topic=topic,
data=message.data)
self.ee.emit(topic, message.forward(topic, translated))
except Exception as e:
LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}")

def on_message(self, *args):
"""
Expand Down Expand Up @@ -228,18 +221,6 @@ def rcv(m):
return msg

def remove(self, msg_type, handler):
regs = self._dedup_registrations.get(handler)
if regs:
for ev, wrapped in [r for r in regs if r[0] == msg_type]:
try:
self.ee.remove_listener(ev, wrapped)
except Exception:
pass
regs.remove((ev, wrapped))
if not regs:
self._dedup_registrations.pop(handler, None)
self._handler_guards.pop(handler, None)
return
try:
self.ee.remove_listener(msg_type, handler)
except Exception:
Expand Down Expand Up @@ -357,7 +338,7 @@ def __new__(cls, *args, **kwargs):
return FakeMessage(*args, **kwargs)


class AsyncFakeBus:
class AsyncFakeBus():
"""In-process stand-in for ``AsyncMessageBusClient``.

Mirrors the same surface as the real async bus client: ``connect`` /
Expand All @@ -381,10 +362,8 @@ def __init__(self, *args, **kwargs):
self.session_id = "default"
self.ee = kwargs.get("emitter") or EventEmitter()
self.ee.on("error", self.on_error)
# mirror MessageBusClient's namespace migration (see FakeBus.__init__).
self._translator = _resolve_bus_flags(kwargs)
self._handler_guards = {} # handler -> shared mirror-guard
self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...]
# no legacy wire bridge (see FakeBus.__init__).
_reject_removed_bridge_flags(kwargs)
self.connected_event = asyncio.Event()
self.connected_event.set()
self.on_open()
Expand All @@ -401,41 +380,12 @@ def __init__(self, *args, **kwargs):
# ------------------------------------------------------------------

def on(self, msg_type, handler):
# wrap handlers on migrated topics so a handler subscribed to both the
# legacy and ovos.* topic fires once (the mirror is dropped) -- same as
# FakeBus.on / MessageBusClient.on.
if self._translator.is_migrated(msg_type):
guard = self._handler_guards.get(handler)
if guard is None:
guard = self._translator.new_mirror_guard()
self._handler_guards[handler] = guard

def wrapped(message=None):
if guard(message):
return
return handler(message)

self.ee.on(msg_type, wrapped)
self._dedup_registrations.setdefault(handler, []).append((msg_type, wrapped))
return
self.ee.on(msg_type, handler)

def once(self, msg_type, handler):
self.ee.once(msg_type, handler)

def remove(self, msg_type, handler):
regs = self._dedup_registrations.get(handler)
if regs:
for ev, wrapped in [r for r in regs if r[0] == msg_type]:
try:
self.ee.remove_listener(ev, wrapped)
except Exception:
pass
regs.remove((ev, wrapped))
if not regs:
self._dedup_registrations.pop(handler, None)
self._handler_guards.pop(handler, None)
return
try:
self.ee.remove_listener(msg_type, handler)
except Exception:
Expand Down Expand Up @@ -483,16 +433,6 @@ async def emit(self, message):
self.ee.emit(message.msg_type, message)
except Exception as e:
LOG.exception(f"Error in event handler for '{message.msg_type}': {e}")
# namespace migration: also dispatch the counterpart topic(s) with the
# payload reshaped into each counterpart's shape -- same as FakeBus.emit.
for topic in self._translator.counterpart_topics(message.msg_type):
try:
translated = self._translator.translate_payload(
from_topic=message.msg_type, to_topic=topic,
data=message.data)
self.ee.emit(topic, message.forward(topic, translated))
except Exception as e:
LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}")

# ------------------------------------------------------------------
# Sync helpers used internally — same as FakeBus
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies = [
"rich-click~=1.7",
"rich~=13.7",
"python-dateutil",
"ovos-spec-tools>=0.16.1a2",
"ovos-spec-tools>=1.6.0a2", # intent_topics helpers for the FakeBus legacy re-emit
]

[project.urls]
Expand Down
Loading
Loading