Files
MoviePilot/app/runtime/event/binding.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

208 lines
7.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
事件处理器声明到运行实例的显式绑定解析。
"""
from __future__ import annotations
import inspect
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Optional, Type
from app.runtime.event.registry import EventRegistry
from app.runtime.log import logger
@dataclass(frozen=True, slots=True)
class EventHandlerBinding:
"""
描述上层运行时为某个事件处理器提供的实例绑定。
:param instance: 接收处理器调用的绑定实例;无需绑定时为 None
:param owner_name: 处理器的责任域名称,用于生命周期管理
:param run_sync_in_threadpool: 是否将同步处理器交给受控线程池执行
"""
instance: Optional[Any]
owner_name: str
run_sync_in_threadpool: bool = False
HandlerInstanceResolver = Callable[
[Type[Any]], Optional[EventHandlerBinding]
]
class EventBindingResolver:
"""
只通过已登记 resolver 把类处理器绑定到托管运行实例。
"""
def __init__(
self,
*,
lock: Any,
resolvers: Callable[[], dict[str, HandlerInstanceResolver]],
) -> None:
"""
绑定 resolver 存储,并记录未命中的处理器用于启动诊断。
"""
self._lock = lock
self._resolvers = resolvers
self._unresolved: set[str] = set()
def register(self, name: str, resolver: HandlerInstanceResolver) -> None:
"""
注册或替换命名实例解析器。
"""
with self._lock:
self._resolvers()[name] = resolver
def unregister(self, name: str) -> None:
"""
移除命名实例解析器;名称不存在时保持幂等。
"""
with self._lock:
self._resolvers().pop(name, None)
def unresolved_handlers(self) -> tuple[str, ...]:
"""
返回本进程中未被显式 resolver 接管的类处理器。
"""
with self._lock:
return tuple(sorted(self._unresolved))
@staticmethod
def parse_handler_names(handler: Callable[..., object]) -> tuple[str, str]:
"""
解析处理器限定名中的类名和方法名。
"""
names = handler.__qualname__.split(".")
if len(names) < 2:
return "", names[0]
return names[0], names[1]
@staticmethod
def is_class_method_declaration(handler: Callable[..., object]) -> bool:
"""
判断处理器是否声明在类体内(限定名含类前缀或签名首参为 self/cls)。
模块级顶层自由函数的限定名不含 ``.``;类方法、装饰器包装和嵌套函数
的限定名含 ``.``。局部作用域自由函数(如测试内联 handler)限定名形如
``func.<locals>.handler``,与装饰器包装方法 ``SiteStatistic.<locals>.
wrapper`` 无法靠限定名区分,需按调用约定兜底:签名首参为 self/cls
才视为类方法声明。模块卸载后残留的类方法一旦被 unbound 直调,会把
事件对象被当作 self 接收后会触发缺少 event 参数的 TypeError,因此先跳过并等待重载恢复。
"""
parts = handler.__qualname__.split(".")
if len(parts) < 2:
return False
if "<locals>" not in parts:
return True
try:
parameters = list(inspect.signature(handler).parameters.values())
except (TypeError, ValueError):
return True
if not parameters:
return True
return parameters[0].name in ("self", "cls")
@staticmethod
def owner_class(handler: Callable[..., object]) -> Optional[Type[Any]]:
"""
从处理器对象本身解析声明类,不按字符串动态导入模块。
"""
if inspect.ismethod(handler):
bound_owner = handler.__self__
return bound_owner if isinstance(bound_owner, type) else type(bound_owner)
module = inspect.getmodule(handler)
if not module:
return None
owner: Any = module
for part in handler.__qualname__.split(".")[:-1]:
if part == "<locals>":
return None
owner = getattr(owner, part, None)
if owner is None:
return None
return owner if isinstance(owner, type) else None
def _record_unresolved(self, identifier: str, reason: str) -> None:
"""
首次未命中时记录告警,避免重载窗口内重复刷屏。
"""
with self._lock:
first_miss = identifier not in self._unresolved
self._unresolved.add(identifier)
if first_miss:
logger.warning(reason, identifier)
def resolve(
self,
handler: Callable[..., object],
) -> Optional[tuple[Callable[..., object], EventHandlerBinding, str, str]]:
"""
通过显式 resolver 解析当前实例方法;自由函数直接返回。
"""
binding: Optional[EventHandlerBinding]
owner_class = self.owner_class(handler)
method_name = getattr(
handler,
"__name__",
self.parse_handler_names(handler)[1],
)
if owner_class is None:
# 插件重载会先清除模块缓存,窗口期内残留的旧类方法声明无法定位声明类;
# 此时直接调用原始函数会绕过实例绑定(旧签名可能与事件调用约定不一致),
# 因此按未绑定处理跳过,等待重载完成后按新 handler 注册自愈。
if self.is_class_method_declaration(handler):
self._record_unresolved(
EventRegistry.handler_identifier(handler),
"事件处理器所属模块已卸载或声明类不可解析,跳过执行:%s",
)
return None
binding = EventHandlerBinding(
instance=None,
owner_name=EventRegistry.handler_identifier(handler),
run_sync_in_threadpool=True,
)
return handler, binding, "", method_name
with self._lock:
resolvers = tuple(self._resolvers().items())
binding = None
resolver_name = ""
for name, resolver in resolvers:
candidate = resolver(owner_class)
if candidate is not None:
binding = candidate
resolver_name = name
break
if binding is None:
self._record_unresolved(
EventRegistry.handler_identifier(handler),
"事件处理器未绑定显式 resolver,已跳过:%s",
)
return None
logger.debug(
"事件处理器绑定:%s -> %s",
EventRegistry.handler_identifier(handler),
resolver_name,
)
if binding.instance is None:
return None
method = getattr(binding.instance, method_name, None)
if not callable(method):
fallback_name = self.parse_handler_names(handler)[1]
method = getattr(binding.instance, fallback_name, None)
if fallback_name == method_name or not callable(method):
logger.warning(
"事件处理器 %s 无法解析为实例方法 %s.%s,跳过执行",
EventRegistry.handler_identifier(handler),
owner_class.__name__,
method_name,
)
return None
method_name = fallback_name
return method, binding, owner_class.__name__, method_name