Applied idiomatic Python improvements and micro-optimizations across registries and API layer.
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Successful in 20s
CI / Tests (Python 3.13) (push) Successful in 19s
CI / Tests (Python 3.14) (push) Successful in 16s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 5s

This commit is contained in:
2026-03-26 12:46:50 -04:00
parent 3cf93da28d
commit 0ddffe5e09
18 changed files with 192 additions and 172 deletions
+21 -15
View File
@@ -27,12 +27,13 @@ from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, cast
from ..api.commands import CommandEvent, CommandHandler, CommandInfo, CommandMark
from ..api.context import CommandContext, EventContext
from ..api.event_types import ChatEvent
if TYPE_CHECKING:
from types import ModuleType
from ..api.context import EventContext, ModuleContext
from ..api.context import ModuleContext
from ..api.owncast_client import OwncastClient
type BuiltinCommandHandler = Callable[[ChatEvent, "OwncastClient"], Awaitable[None]]
@@ -61,7 +62,7 @@ class CommandRegistry:
self.prefix = prefix
logger.debug(f"CommandRegistry initialized with prefix '{prefix}'.")
logger.debug("CommandRegistry initialized with prefix '%s'.", prefix)
def register(
self,
@@ -114,9 +115,13 @@ class CommandRegistry:
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.commands")
module_logger.debug(
f"Registered command '{name_lower}' with aliases {sorted(alias_set)}, "
f"authenticated={requires_authenticated}, moderator={requires_moderator}, "
f"cooldown={cooldown}."
"Registered command '%s' with aliases %s, "
"authenticated=%s, moderator=%s, cooldown=%s.",
name_lower,
sorted(alias_set),
requires_authenticated,
requires_moderator,
cooldown,
)
def unregister(self, name: str) -> bool:
@@ -141,7 +146,7 @@ class CommandRegistry:
del self._commands[primary]
module_logger = logging.getLogger(f"owlbot.modules.{info.module_name}.commands")
module_logger.debug(f"Unregistered command '{primary}'.")
module_logger.debug("Unregistered command '%s'.", primary)
return True
def get(self, trigger: str) -> CommandInfo | None:
@@ -183,7 +188,7 @@ class CommandRegistry:
for name, info in self._commands.items()
if info.module_name == module_name
]
module_logger.debug(f"Unregistering all commands ({len(to_remove)} total).")
module_logger.debug("Unregistering all commands (%d total).", len(to_remove))
for name in to_remove:
self.unregister(name)
return len(to_remove)
@@ -230,7 +235,7 @@ class CommandRegistry:
command = parts[0].lower()
args = parts[1] if len(parts) > 1 else ""
logger.debug(f"Parsed command: {command!r} with args: {args!r}")
logger.debug("Parsed command: %r with args: %r", command, args)
return command, args
@@ -302,7 +307,7 @@ class CommandDispatcher:
module_name="__builtin__",
cooldown=60,
)
logger.debug(f"Registered built-in command '{name_lower}'.")
logger.debug("Registered built-in command '%s'.", name_lower)
def register(
self,
@@ -416,8 +421,6 @@ class CommandDispatcher:
:param event: The chat event to check for commands.
"""
from ..api.context import CommandContext, EventContext
parsed = self._command_registry.parse(event.body)
if parsed is None:
@@ -429,7 +432,7 @@ class CommandDispatcher:
if command_info is None:
# Log for debugging but don't spam the chat with "unknown command" errors.
logger.debug(f"Unknown command: {command_name}")
logger.debug("Unknown command: %s", command_name)
return
user = event.user
@@ -492,8 +495,9 @@ class CommandDispatcher:
)
elapsed = (time.perf_counter() - start) * 1000
logger.debug(
f"Built-in command '{command_info.name}' "
f"completed in {elapsed:.1f}ms."
"Built-in command '%s' completed in %.1fms.",
command_info.name,
elapsed,
)
except TimeoutError:
logger.warning(
@@ -535,7 +539,9 @@ class CommandDispatcher:
)
elapsed = (time.perf_counter() - start) * 1000
logger.debug(f"Command '{command_info.name}' completed in {elapsed:.1f}ms.")
logger.debug(
"Command '%s' completed in %.1fms.", command_info.name, elapsed
)
except TimeoutError:
logger.warning(
f"Command handler '{command_info.name}' "
+30 -33
View File
@@ -20,11 +20,13 @@ Internal infrastructure for managing event handler registration and dispatch.
from __future__ import annotations
import asyncio
import bisect
import logging
import time
from collections import defaultdict
from typing import TYPE_CHECKING, NamedTuple, cast
from ..api.context import PropagationState
from ..api.context import EventContext, PropagationState
from ..api.event_types import ChatEvent, Event, EventType, log_event
from ..api.events import EventHandler, EventMark, Priority
@@ -43,7 +45,7 @@ if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from types import ModuleType
from ..api.context import EventContext, ModuleContext
from ..api.context import ModuleContext
logger = logging.getLogger("owlbot.events")
@@ -59,7 +61,7 @@ class EventRegistry:
"""Initialize an empty handler registry."""
# Maps event type values (strings) to lists of
# (handler, module_name, priority) tuples.
self._handlers: EventHandlerMap = {}
self._handlers: EventHandlerMap = defaultdict(list)
def register(
self,
@@ -82,16 +84,19 @@ class EventRegistry:
for event_type in event_types:
key = event_type.value
if key not in self._handlers:
self._handlers[key] = []
# Add the handler to the list. Multiple handlers
# for the same event are allowed.
self._handlers[key].append(HandlerEntry(handler, module_name, priority))
# Insert in descending priority order so the dispatch loop
# can iterate without a separate sort step. The priority is
# negated because bisect works in ascending order. bisect.insort
# uses bisect_right, so same-priority handlers stay in
# registration (FIFO) order.
entry = HandlerEntry(handler, module_name, priority)
bisect.insort(self._handlers[key], entry, key=lambda e: -e.priority)
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.events")
module_logger.debug(
f"Registered handler '{handler.__name__}' for {event_type.value} "
f"(priority={priority})."
"Registered handler '%s' for %s (priority=%s).",
handler.__name__,
event_type.value,
priority,
)
def unregister(self, handler: EventHandler) -> bool:
@@ -116,8 +121,9 @@ class EventRegistry:
if module_name and removed_from:
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.events")
module_logger.debug(
f"Unregistered handler '{handler.__name__}' "
f"for {', '.join(removed_from)}."
"Unregistered handler '%s' for %s.",
handler.__name__,
", ".join(removed_from),
)
return True
return False
@@ -170,7 +176,7 @@ class EventRegistry:
self._handlers[key] = kept
if seen:
module_logger.debug(f"Unregistered all handlers ({len(seen)} total).")
module_logger.debug("Unregistered all handlers (%d total).", len(seen))
return len(seen)
def register_from_module(self, module: ModuleType, module_name: str) -> None:
@@ -322,27 +328,19 @@ class EventDispatcher:
# Shared state for propagation control (all handlers see the same instance).
propagation = PropagationState()
# Phase 1: Event handlers (sequential, sorted by priority descending).
# Phase 1: Event handlers (sequential, already sorted by priority descending).
if handler_entries:
# Sort handlers by priority (highest first).
# Python's sort is stable, so handlers at the same priority
# run in their original registration order.
sorted_handlers = sorted(
handler_entries,
key=lambda entry: entry.priority,
reverse=True,
)
logger.debug(
f"Dispatching {event_type} to {len(sorted_handlers)} handler(s)."
"Dispatching %s to %d handler(s).", event_type, len(handler_entries)
)
for handler, module_name, _priority in sorted_handlers:
for handler, module_name, _priority in handler_entries:
# Has propagation been stopped by a previous handler?
if propagation.stopped:
reason = propagation.reason
logger.debug(
f"Propagation stopped{': ' + reason if reason else '.'}"
"Propagation stopped%s",
": " + reason if reason else ".",
)
break
@@ -350,7 +348,7 @@ class EventDispatcher:
handler, event, event_type, module_name, propagation
)
else:
logger.debug(f"No handlers registered for event type: {event_type}")
logger.debug("No handlers registered for event type: %s", event_type)
# Phase 2: Command dispatch (CHAT events only, if not cancelled).
if event_type == EventType.CHAT and isinstance(event, ChatEvent):
@@ -358,7 +356,8 @@ class EventDispatcher:
if propagation.stopped:
reason = propagation.reason
logger.debug(
f"Command dispatch skipped{': ' + reason if reason else '.'}"
"Command dispatch skipped%s",
": " + reason if reason else ".",
)
else:
try:
@@ -382,10 +381,8 @@ class EventDispatcher:
:param module_name: The module that owns this handler.
:param propagation: Shared propagation state for this dispatch cycle.
"""
from ..api.context import EventContext
handler_name = handler.__name__
logger.debug(f"Calling handler: {handler_name} from module: {module_name}")
logger.debug("Calling handler: %s from module: %s", handler_name, module_name)
module_ctx = self._get_module_context(module_name)
@@ -400,7 +397,7 @@ class EventDispatcher:
await asyncio.wait_for(handler(ctx), timeout=self._handler_timeout)
elapsed = (time.perf_counter() - start) * 1000
logger.debug(f"Handler '{handler_name}' completed in {elapsed:.1f}ms.")
logger.debug("Handler '%s' completed in %.1fms.", handler_name, elapsed)
except TimeoutError:
logger.warning(
f"Handler '{handler_name}' from module '{module_name}' "
+20 -15
View File
@@ -22,12 +22,14 @@ from __future__ import annotations
import asyncio
import logging
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast, overload
from aiohttp import web
from aiohttp.web import DynamicResource
from ..api.context import RouteContext
from ..api.routes import RouteHandler, RouteInfo, RouteMark
if TYPE_CHECKING:
@@ -69,7 +71,7 @@ class RouteRegistry:
# Each group represents a unique path pattern with one or more handlers.
self._groups: list[_RouteGroup] = []
# Maps module_name -> list of full_paths (for cleanup).
self._module_routes: dict[str, list[str]] = {}
self._module_routes: dict[str, list[str]] = defaultdict(list)
logger.debug("RouteRegistry initialized.")
def _find_group(self, full_path: str) -> _RouteGroup | None:
@@ -139,14 +141,14 @@ class RouteRegistry:
else:
group.handlers.append(info)
if module_name not in self._module_routes:
self._module_routes[module_name] = []
if full_path not in self._module_routes[module_name]:
self._module_routes[module_name].append(full_path)
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.routes")
module_logger.debug(
f"Registered route '{full_path}' [{', '.join(sorted(info.methods))}]."
"Registered route '%s' [%s].",
full_path,
", ".join(sorted(info.methods)),
)
return info
@@ -179,7 +181,7 @@ class RouteRegistry:
module_names = {h.module_name for h in group.handlers}
for name in module_names:
module_logger = logging.getLogger(f"owlbot.modules.{name}.routes")
module_logger.debug(f"Unregistered route '{full_path}'.")
module_logger.debug("Unregistered route '%s'.", full_path)
return True
# Remove only the handler for the specified method.
@@ -206,7 +208,7 @@ class RouteRegistry:
f"owlbot.modules.{module_name}.routes"
)
module_logger.debug(
f"Unregistered route '{full_path}' [{method_upper}]."
"Unregistered route '%s' [%s].", full_path, method_upper
)
return True
@@ -325,7 +327,7 @@ class RouteRegistry:
self._groups.pop(i)
if count > 0:
module_logger.debug(f"Unregistered all routes ({count} handler(s) total).")
module_logger.debug("Unregistered all routes (%d handler(s) total).", count)
self._module_routes.pop(module_name, None)
return count
@@ -485,7 +487,7 @@ class RouteDispatcher:
try:
result = self._route_registry.match(full_path, request.method)
except LookupError:
mod_logger.debug(f"No route registered for '{full_path}'.")
mod_logger.debug("No route registered for '%s'.", full_path)
return web.Response(status=404)
if result[0] is None:
@@ -493,8 +495,10 @@ class RouteDispatcher:
_, allowed_methods, _ = result
allowed = ", ".join(sorted(allowed_methods))
mod_logger.debug(
f"Method {request.method} not allowed for '{full_path}' "
f"(allowed: {allowed})"
"Method %s not allowed for '%s' (allowed: %s)",
request.method,
full_path,
allowed,
)
return web.Response(status=405, headers={"Allow": allowed})
@@ -514,9 +518,6 @@ class RouteDispatcher:
:param match_info: Captured path parameters from pattern matching.
:return: HTTP response.
"""
# Import here to avoid circular imports at module load time.
from ..api.context import RouteContext
module_name = route_info.module_name
mod_logger = logging.getLogger(f"owlbot.modules.{module_name}.routes")
@@ -529,7 +530,9 @@ class RouteDispatcher:
)
logger.debug(
f"Calling route handler: {route_info.full_path} from module: {module_name}"
"Calling route handler: %s from module: %s",
route_info.full_path,
module_name,
)
try:
@@ -540,7 +543,9 @@ class RouteDispatcher:
elapsed = (time.perf_counter() - start) * 1000
mod_logger.debug(
f"Route handler '{route_info.full_path}' completed in {elapsed:.1f}ms."
"Route handler '%s' completed in %.1fms.",
route_info.full_path,
elapsed,
)
if result is None: