Skip to content

Commit d43d5d7

Browse files
committed
dotbot/controller: check the debug level under any structlog logger
structlog's stdlib BoundLogger only gained is_enabled_for in 26.1, so on the declared floor every received frame raised and was dropped; its native filtering logger has no isEnabledFor, and before 25.1 no check at all. debug_enabled accepts all three. AI-assisted: Claude Opus 5.5
1 parent 135ae8f commit d43d5d7

3 files changed

Lines changed: 61 additions & 3 deletions

File tree

‎dotbot/controller.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import asyncio
1111
import dataclasses
12-
import logging
1312
import math
1413
import os
1514
import random
@@ -58,7 +57,7 @@
5857
CSVLog,
5958
camera_log_path,
6059
)
61-
from dotbot.logger import LOGGER
60+
from dotbot.logger import LOGGER, debug_enabled
6261
from dotbot.models import (
6362
DotBotBodyModel,
6463
DotBotCalibrationSessionModel,
@@ -689,7 +688,7 @@ def handle_received_frame(
689688
)
690689
return
691690
payload = frame.packet.payload
692-
debug = self.logger.is_enabled_for(logging.DEBUG)
691+
debug = debug_enabled(self.logger)
693692
dotbot = self.dotbots.get(source)
694693
if dotbot is None and payload_type not in ADVERTISEMENT_PAYLOAD_TYPES:
695694
self.logger.debug("Ignoring non advertised dotbot", source=source)

‎dotbot/logger.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,17 @@ def setup_logging(filename, level, handlers):
9595
logging.config.dictConfig(stdlib_config)
9696

9797

98+
def debug_enabled(logger) -> bool:
99+
"""Whether `logger` emits DEBUG events.
100+
101+
structlog's stdlib logger spells the check `isEnabledFor`, its native
102+
filtering logger `is_enabled_for` (from 25.1), and before that the native
103+
logger has neither, in which case this answers True.
104+
"""
105+
is_enabled = getattr(logger, "isEnabledFor", None) or getattr(
106+
logger, "is_enabled_for", None
107+
)
108+
return is_enabled is None or is_enabled(logging.DEBUG)
109+
110+
98111
LOGGER = structlog.get_logger("pydotbot")

‎dotbot/tests/test_controller.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,52 @@ async def test_an_advertisement_without_a_heading_clears_the_last_one(controller
922922
assert dotbot.pose.heading_source == "none"
923923

924924

925+
class _StdlibLogger:
926+
"""structlog's stdlib BoundLogger before 26.1: `isEnabledFor` only."""
927+
928+
def __init__(self):
929+
self.events = []
930+
931+
def isEnabledFor(self, level): # pylint:disable=invalid-name
932+
return True
933+
934+
def debug(self, event, **kw):
935+
self.events.append(event)
936+
937+
info = warning = debug
938+
939+
940+
class _NativeLogger(_StdlibLogger):
941+
"""structlog's native filtering logger from 25.1: `is_enabled_for` only."""
942+
943+
isEnabledFor = None
944+
945+
def is_enabled_for(self, level):
946+
return True
947+
948+
949+
class _OldNativeLogger(_StdlibLogger):
950+
"""structlog's native filtering logger before 25.1: no level check."""
951+
952+
isEnabledFor = None
953+
954+
955+
@pytest.mark.parametrize(
956+
"logger_class", [_StdlibLogger, _NativeLogger, _OldNativeLogger]
957+
)
958+
@pytest.mark.asyncio
959+
async def test_a_frame_is_handled_whatever_the_structlog_logger(
960+
controller, logger_class
961+
):
962+
"""Guards the structlog floor: the level check exists under every logger."""
963+
controller.logger = logger_class()
964+
controller.handle_received_frame(
965+
_advertised(BOT, direction=90, pos_x=1000, pos_y=2000)
966+
)
967+
assert addr_to_hex(BOT) in controller.dotbots
968+
assert "Advertisement Data" in controller.logger.events
969+
970+
925971
@pytest.mark.asyncio
926972
async def test_the_advertisement_debug_log_reports_y(controller):
927973
with capture_logs() as logs:

0 commit comments

Comments
 (0)