Added pydocstyle (D) rules to Ruff and fixed all violations.
CI / Formatting (push) Successful in 12s
CI / Linting (push) Successful in 13s
CI / Tests (Python 3.12) (push) Successful in 26s
CI / Tests (Python 3.13) (push) Successful in 25s
CI / Tests (Python 3.14) (push) Successful in 25s
CI / Type Checking (push) Successful in 26s

This commit is contained in:
2026-02-19 11:47:47 -05:00
parent 33dd49e20a
commit ca4adbcebf
30 changed files with 309 additions and 591 deletions
+27 -54
View File
@@ -41,16 +41,14 @@ logger = logging.getLogger("owlbot.commands")
class CommandRegistry:
"""
Holds all registered commands for a bot instance.
"""Holds all registered commands for a bot instance.
Instance-scoped to enable proper dependency injection and allow multiple
bot instances to coexist without sharing state.
"""
def __init__(self, prefix: str = "!") -> None:
"""
Initialize the command registry.
"""Initialize the command registry.
:param prefix: Command prefix character (e.g., "!" for "!ping").
"""
@@ -76,8 +74,7 @@ class CommandRegistry:
cooldown: int | float = 0,
module_name: str,
) -> None:
"""
Register a command handler.
"""Register a command handler.
:param name: Primary command name (case-insensitive).
:param handler: Async function to handle the command.
@@ -123,8 +120,7 @@ class CommandRegistry:
)
def unregister(self, name: str) -> bool:
"""
Unregister a command and all its aliases.
"""Unregister a command and all its aliases.
:param name: The primary command name or any alias.
:return: True if command was found and removed, False otherwise.
@@ -149,8 +145,7 @@ class CommandRegistry:
return True
def get(self, trigger: str) -> CommandInfo | None:
"""
Look up a command by name or alias.
"""Look up a command by name or alias.
:param trigger: Command name or alias (case-insensitive).
:return: CommandInfo if found, None otherwise.
@@ -162,8 +157,7 @@ class CommandRegistry:
return self._commands.get(primary)
def exists(self, trigger: str) -> bool:
"""
Check if a command is registered.
"""Check if a command is registered.
:param trigger: Command name or alias (case-insensitive).
:return: True if the command exists, False otherwise.
@@ -171,16 +165,14 @@ class CommandRegistry:
return trigger.lower() in self._aliases
def get_all(self) -> dict[str, CommandInfo]:
"""
Get all registered commands.
"""Get all registered commands.
:return: Dict mapping primary command names to CommandInfo.
"""
return self._commands.copy()
def unregister_by_module(self, module_name: str) -> int:
"""
Remove all commands registered by a specific module.
"""Remove all commands registered by a specific module.
:param module_name: The module whose commands should be removed.
:return: Number of commands removed.
@@ -197,8 +189,7 @@ class CommandRegistry:
return len(to_remove)
def register_from_module(self, module: ModuleType, module_name: str) -> None:
"""
Scan a Python module for @on_command-decorated functions and register them.
"""Scan a Python module for @on_command-decorated functions and register them.
Looks for functions with the ``_owlbot_command`` attribute set by
the ``@on_command`` decorator and registers each one.
@@ -222,8 +213,7 @@ class CommandRegistry:
)
def parse(self, message: str) -> tuple[str, str] | None:
"""
Parse a message to extract command and arguments.
"""Parse a message to extract command and arguments.
:param message: The chat message body.
:return: Tuple of (command_name, args_string), or None if not a command.
@@ -245,8 +235,7 @@ class CommandRegistry:
class CommandDispatcher:
"""
Dispatches chat events to registered command handlers.
"""Dispatches chat events to registered command handlers.
Parses messages, checks authentication/moderator requirements,
and calls the appropriate command handler.
@@ -260,8 +249,7 @@ class CommandDispatcher:
loaded_modules: set[str],
command_prefix: str = "!",
) -> None:
"""
Initialize the command dispatcher.
"""Initialize the command dispatcher.
Creates and owns a :class:`CommandRegistry` internally.
@@ -296,8 +284,7 @@ class CommandDispatcher:
*,
aliases: list[str] | tuple[str, ...] | None = None,
) -> None:
"""
Register a built-in command handler.
"""Register a built-in command handler.
Built-in commands use a simpler handler signature (event, owncast_client)
and don't require module infrastructure.
@@ -328,8 +315,7 @@ class CommandDispatcher:
cooldown: int | float = 0,
module_name: str,
) -> None:
"""
Register a command handler.
"""Register a command handler.
Delegates to the internal CommandRegistry.
@@ -355,8 +341,7 @@ class CommandDispatcher:
self._cooldown_tracker.pop(name.lower(), None)
def unregister(self, name: str) -> bool:
"""
Unregister a command and all its aliases.
"""Unregister a command and all its aliases.
Delegates to the internal CommandRegistry.
@@ -371,8 +356,7 @@ class CommandDispatcher:
return result
def get(self, trigger: str) -> CommandInfo | None:
"""
Look up a command by name or alias.
"""Look up a command by name or alias.
Delegates to the internal CommandRegistry.
@@ -382,8 +366,7 @@ class CommandDispatcher:
return self._command_registry.get(trigger)
def exists(self, trigger: str) -> bool:
"""
Check if a command is registered.
"""Check if a command is registered.
Delegates to the internal CommandRegistry.
@@ -393,8 +376,7 @@ class CommandDispatcher:
return self._command_registry.exists(trigger)
def get_by_module(self, module_name: str) -> dict[str, CommandInfo]:
"""
Get all commands registered by a specific module.
"""Get all commands registered by a specific module.
:param module_name: The module whose commands to return.
:return: Dict mapping primary command names to CommandInfo for that module.
@@ -406,8 +388,7 @@ class CommandDispatcher:
}
def register_from_module(self, module: ModuleType, module_name: str) -> None:
"""
Scan a Python module for @on_command-decorated functions and register them.
"""Scan a Python module for @on_command-decorated functions and register them.
Delegates to the internal CommandRegistry.
@@ -417,8 +398,7 @@ class CommandDispatcher:
self._command_registry.register_from_module(module, module_name)
def unregister_by_module(self, module_name: str) -> int:
"""
Remove all commands registered by a specific module.
"""Remove all commands registered by a specific module.
Delegates to the internal CommandRegistry.
@@ -432,8 +412,7 @@ class CommandDispatcher:
return self._command_registry.unregister_by_module(module_name)
async def dispatch(self, event: ChatEvent) -> None:
"""
Dispatch a chat event to the appropriate command handler if it's a command.
"""Dispatch a chat event to the appropriate command handler if it's a command.
:param event: The chat event to check for commands.
"""
@@ -605,8 +584,7 @@ class CommandDispatcher:
class ModuleCommands:
"""
Module-scoped wrapper around CommandDispatcher.
"""Module-scoped wrapper around CommandDispatcher.
This wrapper auto-supplies the module name for registration operations,
so modules don't need to pass their own name back into the API.
@@ -614,8 +592,7 @@ class ModuleCommands:
"""
def __init__(self, dispatcher: CommandDispatcher, module_name: str) -> None:
"""
Initialize the module-scoped command wrapper.
"""Initialize the module-scoped command wrapper.
:param dispatcher: The CommandDispatcher that owns the command registry.
:param module_name: The name of the module using this wrapper.
@@ -643,8 +620,7 @@ class ModuleCommands:
requires_moderator: bool = False,
cooldown: int | float = 0,
) -> None:
"""
Register a command handler for this module.
"""Register a command handler for this module.
The module name is automatically supplied.
@@ -667,8 +643,7 @@ class ModuleCommands:
)
def unregister(self, name: str) -> bool:
"""
Unregister a command and all its aliases.
"""Unregister a command and all its aliases.
Only commands registered by this module can be unregistered.
@@ -682,8 +657,7 @@ class ModuleCommands:
return self._dispatcher.unregister(name)
def get(self, trigger: str) -> CommandInfo | None:
"""
Look up a command by name or alias within this module's registrations.
"""Look up a command by name or alias within this module's registrations.
:param trigger: Command name or alias (case-insensitive).
:return: CommandInfo if found and owned by this module, None otherwise.
@@ -694,8 +668,7 @@ class ModuleCommands:
return info
def exists(self, trigger: str) -> bool:
"""
Check if a command is registered across all modules.
"""Check if a command is registered across all modules.
:param trigger: Command name or alias (case-insensitive).
:return: True if the command exists, False otherwise.
+23 -46
View File
@@ -49,8 +49,7 @@ logger = logging.getLogger("owlbot.events")
class EventRegistry:
"""
Holds all registered event handlers for a bot instance.
"""Holds all registered event handlers for a bot instance.
Instance-scoped to enable proper dependency injection and allow multiple
bot instances to coexist without sharing state.
@@ -69,8 +68,7 @@ class EventRegistry:
module_name: str,
priority: int = Priority.NORMAL,
) -> None:
"""
Register a handler for the given event types.
"""Register a handler for the given event types.
Called by the module loader after scanning for decorated functions.
@@ -97,8 +95,7 @@ class EventRegistry:
)
def unregister(self, handler: EventHandler) -> bool:
"""
Unregister a handler from all event types it is registered for.
"""Unregister a handler from all event types it is registered for.
:param handler: The handler function to unregister.
:return: True if handler was found and removed, False otherwise.
@@ -126,8 +123,7 @@ class EventRegistry:
return False
def get(self, event_type: EventType) -> list[HandlerEntry]:
"""
Get all handlers registered for a specific event type.
"""Get all handlers registered for a specific event type.
:param event_type: The event type to look up.
:return: List of (handler, module_name, priority) tuples.
@@ -135,8 +131,7 @@ class EventRegistry:
return self._handlers.get(event_type.value, [])
def get_all(self) -> EventHandlerMap:
"""
Get a copy of the entire handler registry.
"""Get a copy of the entire handler registry.
:return: Dict mapping event type values to handler lists.
"""
@@ -145,8 +140,7 @@ class EventRegistry:
}
def get_handler_module(self, handler: EventHandler) -> str | None:
"""
Look up which module registered a given handler.
"""Look up which module registered a given handler.
:param handler: The handler function to look up.
:return: The module name if found, None otherwise.
@@ -158,8 +152,7 @@ class EventRegistry:
return None
def unregister_by_module(self, module_name: str) -> int:
"""
Remove all handlers registered by a specific module.
"""Remove all handlers registered by a specific module.
:param module_name: The module whose handlers should be removed.
:return: Number of handlers removed.
@@ -181,8 +174,7 @@ class EventRegistry:
return len(seen)
def register_from_module(self, module: ModuleType, module_name: str) -> None:
"""
Scan a Python module for @on_event-decorated functions and register them.
"""Scan a Python module for @on_event-decorated functions and register them.
Looks for functions with the ``_owlbot_event`` attribute set by
the ``@on_event`` decorator and registers each one.
@@ -201,8 +193,7 @@ class EventRegistry:
class EventDispatcher:
"""
Dispatches events to registered handlers sequentially by priority.
"""Dispatches events to registered handlers sequentially by priority.
After all event handlers complete, command dispatch is triggered for
CHAT events (via the injected command_dispatch callback).
@@ -214,8 +205,7 @@ class EventDispatcher:
get_module_context: Callable[[str], ModuleContext],
handler_timeout: float,
) -> None:
"""
Initialize the event dispatcher.
"""Initialize the event dispatcher.
Creates and owns a :class:`EventRegistry` internally.
@@ -237,8 +227,7 @@ class EventDispatcher:
module_name: str,
priority: int = Priority.NORMAL,
) -> None:
"""
Register a handler for the given event types.
"""Register a handler for the given event types.
A single handler can respond to multiple event types; the registry
stores a separate entry per event type. This is the shared entry
@@ -255,8 +244,7 @@ class EventDispatcher:
self._handler_registry.register(handler, event_types, module_name, priority)
def unregister(self, handler: EventHandler) -> bool:
"""
Unregister a handler from all event types it is registered for.
"""Unregister a handler from all event types it is registered for.
Unlike commands (looked up by name string), event handlers are
identified by object identity. A handler registered for multiple
@@ -268,8 +256,7 @@ class EventDispatcher:
return self._handler_registry.unregister(handler)
def get_by_module(self, module_name: str) -> EventHandlerMap:
"""
Get all handlers registered by a specific module, grouped by event type.
"""Get all handlers registered by a specific module, grouped by event type.
The registry stores handlers grouped by event type, not by module,
so this filters across all event types to collect a single module's
@@ -288,8 +275,7 @@ class EventDispatcher:
return result
def get_handler_module(self, handler: EventHandler) -> str | None:
"""
Reverse-lookup which module registered a given handler.
"""Reverse-lookup which module registered a given handler.
Scans all event types since handlers are stored by event type,
not by module.
@@ -300,8 +286,7 @@ class EventDispatcher:
return self._handler_registry.get_handler_module(handler)
def register_from_module(self, module: ModuleType, module_name: str) -> None:
"""
Scan a Python module for ``@on_event``-decorated functions and register them.
"""Scan a Python module for ``@on_event``-decorated functions and register them.
This is the import-phase entry point: the module loader calls it once
per module. Decorator attributes are read here and passed as explicit
@@ -314,8 +299,7 @@ class EventDispatcher:
self._handler_registry.register_from_module(module, module_name)
def unregister_by_module(self, module_name: str) -> int:
"""
Remove all handlers registered by a specific module.
"""Remove all handlers registered by a specific module.
Used during module teardown to clean up all of a module's handlers
in one call, regardless of which event types they were registered for.
@@ -326,8 +310,7 @@ class EventDispatcher:
return self._handler_registry.unregister_by_module(module_name)
async def dispatch(self, event_type: EventType, event: Event) -> None:
"""
Dispatch an event to handlers sequentially by priority, then to commands.
"""Dispatch an event to handlers sequentially by priority, then to commands.
:param event_type: The type of event to dispatch.
:param event: The parsed event instance.
@@ -391,8 +374,7 @@ class EventDispatcher:
module_name: str,
propagation: PropagationState,
) -> None:
"""
Call a single handler with timeout enforcement and transaction management.
"""Call a single handler with timeout enforcement and transaction management.
:param handler: The handler function to call.
:param event: The event to pass to the handler.
@@ -444,8 +426,7 @@ class EventDispatcher:
class ModuleEvents:
"""
Module-scoped wrapper around EventDispatcher.
"""Module-scoped wrapper around EventDispatcher.
This wrapper auto-supplies the module name for registration operations,
so modules don't need to pass their own name back into the API.
@@ -453,8 +434,7 @@ class ModuleEvents:
"""
def __init__(self, dispatcher: EventDispatcher, module_name: str) -> None:
"""
Initialize the module-scoped handler wrapper.
"""Initialize the module-scoped handler wrapper.
:param dispatcher: The EventDispatcher that owns the handler registry.
:param module_name: The name of the module using this wrapper.
@@ -473,8 +453,7 @@ class ModuleEvents:
event_types: tuple[EventType, ...],
priority: int = Priority.NORMAL,
) -> None:
"""
Register an event handler for this module.
"""Register an event handler for this module.
The module name is automatically supplied.
@@ -491,8 +470,7 @@ class ModuleEvents:
)
def unregister(self, handler: EventHandler) -> bool:
"""
Unregister a handler from all event types it is registered for.
"""Unregister a handler from all event types it is registered for.
Only handlers registered by this module can be unregistered.
@@ -505,8 +483,7 @@ class ModuleEvents:
return self._dispatcher.unregister(handler)
def get(self, event_type: EventType) -> list[HandlerEntry]:
"""
Get handlers registered by this module for a specific event type.
"""Get handlers registered by this module for a specific event type.
:param event_type: The event type to look up.
:return: List of (handler, module_name, priority) tuples for this module only.
+27 -54
View File
@@ -39,8 +39,7 @@ logger = logging.getLogger("owlbot.web")
class RouteRegistry:
"""
Holds all registered routes for a bot instance.
"""Holds all registered routes for a bot instance.
Routes are namespaced by module to prevent conflicts. Supports path
patterns using aiohttp's ``{name}`` and ``{name:regex}`` syntax via
@@ -64,8 +63,7 @@ class RouteRegistry:
methods: list[str] | None = None,
module_name: str,
) -> RouteInfo:
"""
Register a route handler.
"""Register a route handler.
:param path: URL path relative to module namespace. Supports
``{name}`` and ``{name:regex}`` patterns.
@@ -108,8 +106,7 @@ class RouteRegistry:
return info
def unregister(self, full_path: str) -> bool:
"""
Unregister a route by its full path.
"""Unregister a route by its full path.
:param full_path: The full route path including namespace.
:return: True if route was found and removed, False otherwise.
@@ -134,8 +131,7 @@ class RouteRegistry:
return False
def get(self, full_path: str) -> RouteInfo | None:
"""
Look up a route by its full path (exact match on registered pattern).
"""Look up a route by its full path (exact match on registered pattern).
:param full_path: The full route path including namespace.
:return: RouteInfo if found, None otherwise.
@@ -146,8 +142,7 @@ class RouteRegistry:
return None
def match(self, full_path: str) -> tuple[RouteInfo, dict[str, str]] | None:
"""
Match a request path against registered routes.
"""Match a request path against registered routes.
Scans routes in registration order (first match wins). Uses
``DynamicResource._match()`` for both plain and parameterized paths.
@@ -162,16 +157,14 @@ class RouteRegistry:
return None
def get_all(self) -> dict[str, RouteInfo]:
"""
Get all registered routes.
"""Get all registered routes.
:return: Dict mapping full paths to RouteInfo.
"""
return {info.full_path: info for _, info in self._routes}
def get_by_module(self, module_name: str) -> list[RouteInfo]:
"""
Get all routes registered by a specific module.
"""Get all routes registered by a specific module.
:param module_name: The module name.
:return: List of RouteInfo for that module.
@@ -180,8 +173,7 @@ class RouteRegistry:
return [info for _, info in self._routes if info.full_path in paths]
def unregister_by_module(self, module_name: str) -> int:
"""
Remove all routes registered by a specific module.
"""Remove all routes registered by a specific module.
:param module_name: The module whose routes should be removed.
:return: Number of routes removed.
@@ -202,8 +194,7 @@ class RouteRegistry:
return count
def register_from_module(self, module: ModuleType, module_name: str) -> None:
"""
Scan a Python module for @on_route-decorated functions and register them.
"""Scan a Python module for @on_route-decorated functions and register them.
Looks for functions with the ``_owlbot_route`` attribute set by
the ``@on_route`` decorator and registers each one.
@@ -225,8 +216,7 @@ class RouteRegistry:
class RouteDispatcher:
"""
Dispatches HTTP requests to registered module route handlers.
"""Dispatches HTTP requests to registered module route handlers.
Looks up routes in the RouteRegistry, validates methods, creates
RouteContext, and calls the handler with timeout and transaction management.
@@ -237,8 +227,7 @@ class RouteDispatcher:
get_module_context: Callable[[str], ModuleContext],
handler_timeout: float,
) -> None:
"""
Initialize the route dispatcher.
"""Initialize the route dispatcher.
Creates and owns a :class:`RouteRegistry` internally.
@@ -258,8 +247,7 @@ class RouteDispatcher:
methods: list[str] | None = None,
module_name: str,
) -> RouteInfo:
"""
Register a route handler.
"""Register a route handler.
Delegates to the internal RouteRegistry.
@@ -278,8 +266,7 @@ class RouteDispatcher:
)
def unregister(self, full_path: str) -> bool:
"""
Unregister a route by its full path.
"""Unregister a route by its full path.
Delegates to the internal RouteRegistry.
@@ -289,8 +276,7 @@ class RouteDispatcher:
return self._route_registry.unregister(full_path)
def get(self, full_path: str) -> RouteInfo | None:
"""
Look up a route by its full path.
"""Look up a route by its full path.
Delegates to the internal RouteRegistry.
@@ -300,8 +286,7 @@ class RouteDispatcher:
return self._route_registry.get(full_path)
def get_by_module(self, module_name: str) -> list[RouteInfo]:
"""
Get all routes registered by a specific module.
"""Get all routes registered by a specific module.
Delegates to the internal RouteRegistry.
@@ -311,8 +296,7 @@ class RouteDispatcher:
return self._route_registry.get_by_module(module_name)
def register_from_module(self, module: ModuleType, module_name: str) -> None:
"""
Scan a Python module for @on_route-decorated functions and register them.
"""Scan a Python module for @on_route-decorated functions and register them.
Delegates to the internal RouteRegistry.
@@ -322,8 +306,7 @@ class RouteDispatcher:
self._route_registry.register_from_module(module, module_name)
def unregister_by_module(self, module_name: str) -> int:
"""
Remove all routes registered by a specific module.
"""Remove all routes registered by a specific module.
Delegates to the internal RouteRegistry.
@@ -333,8 +316,7 @@ class RouteDispatcher:
return self._route_registry.unregister_by_module(module_name)
async def dispatch(self, request: web.Request) -> web.StreamResponse:
"""
Dispatch an HTTP request to the appropriate module route handler.
"""Dispatch an HTTP request to the appropriate module route handler.
Extracts module_name and path from the URL, matches it against
registered routes (supporting path patterns), validates the HTTP
@@ -374,8 +356,7 @@ class RouteDispatcher:
route_info: RouteInfo,
match_info: dict[str, str] | None = None,
) -> web.StreamResponse:
"""
Handle an HTTP request to a module-registered route.
"""Handle an HTTP request to a module-registered route.
:param request: The aiohttp request object.
:param route_info: Information about the registered route.
@@ -447,8 +428,7 @@ class RouteDispatcher:
class ModuleRoutes:
"""
Module-scoped wrapper around RouteDispatcher.
"""Module-scoped wrapper around RouteDispatcher.
This wrapper auto-supplies the module name for route operations,
so modules don't need to know the internal routing namespace.
@@ -458,8 +438,7 @@ class ModuleRoutes:
def __init__(
self, dispatcher: RouteDispatcher, module_name: str, public_base_url: str
) -> None:
"""
Initialize the module-scoped routes wrapper.
"""Initialize the module-scoped routes wrapper.
:param dispatcher: The RouteDispatcher that owns the route registry.
:param module_name: The name of the module using this wrapper.
@@ -475,8 +454,7 @@ class ModuleRoutes:
return self._dispatcher.get_by_module(self._module_name)
def url_for(self, path: str) -> str:
"""
Build a public URL for a route registered by this module.
"""Build a public URL for a route registered by this module.
:param path: The route path (e.g., "/list").
:return: Full public URL (e.g., "http://host/owlbot/quotes/list").
@@ -490,8 +468,7 @@ class ModuleRoutes:
*,
methods: list[str] | None = None,
) -> RouteInfo:
"""
Register a route handler for this module.
"""Register a route handler for this module.
The module name is automatically supplied.
@@ -509,8 +486,7 @@ class ModuleRoutes:
)
def unregister(self, path: str) -> bool:
"""
Unregister a route by its relative path.
"""Unregister a route by its relative path.
:param path: Relative route path (e.g., "/stats").
:return: True if route was found and removed, False otherwise.
@@ -518,8 +494,7 @@ class ModuleRoutes:
return self._dispatcher.unregister(self._full_path(path))
def get(self, path: str) -> RouteInfo | None:
"""
Look up a route by its relative path.
"""Look up a route by its relative path.
:param path: Relative route path (e.g., "/stats").
:return: RouteInfo if found, None otherwise.
@@ -527,8 +502,7 @@ class ModuleRoutes:
return self._dispatcher.get(self._full_path(path))
def exists(self, path: str) -> bool:
"""
Check if a route is registered at the given relative path.
"""Check if a route is registered at the given relative path.
:param path: Relative route path (e.g., "/stats").
:return: True if the route exists, False otherwise.
@@ -536,8 +510,7 @@ class ModuleRoutes:
return self.get(path) is not None
def _full_path(self, path: str) -> str:
"""
Normalize a relative path into the full namespaced path.
"""Normalize a relative path into the full namespaced path.
:param path: Relative route path (e.g., "/list" or "list").
:return: Full path (e.g., "/owlbot/quotes/list").