mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 12:06:51 +08:00
fix: 插件重载窗口期装饰器包装类方法误判直调导致 missing event TypeError
事件绑定解析在 owner_class 为 None 时仅靠限定名区分自由函数与类方法声明, 装饰器包装的方法限定名含 <locals> 被误判为自由函数 unbound 直调,把 event 吞进 self 触发 TypeError。局部作用域限定名无法区分包装方法与局部自由函数, 按调用约定兜底:签名首参为 self/cls 才视为类方法声明跳过执行。
This commit is contained in:
@@ -59,9 +59,27 @@ class EventBindingResolver:
|
||||
|
||||
@staticmethod
|
||||
def is_class_method_declaration(handler: Callable) -> bool:
|
||||
"""判断处理器是否声明在类体内(限定名含类前缀且非局部闭包)。"""
|
||||
"""判断处理器是否声明在类体内(限定名含类前缀或签名首参为 self/cls)。
|
||||
|
||||
模块级顶层自由函数的限定名不含 ``.``;类方法、装饰器包装和嵌套函数
|
||||
的限定名含 ``.``。局部作用域自由函数(如测试内联 handler)限定名形如
|
||||
``func.<locals>.handler``,与装饰器包装方法 ``SiteStatistic.<locals>.
|
||||
wrapper`` 无法靠限定名区分,需按调用约定兜底:签名首参为 self/cls
|
||||
才视为类方法声明。模块卸载后残留的类方法一旦被 unbound 直调,会把
|
||||
event 吞进 self 触发 missing event TypeError,因此必须跳过等待重载自愈。
|
||||
"""
|
||||
parts = handler.__qualname__.split(".")
|
||||
return len(parts) >= 2 and "<locals>" not in parts
|
||||
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) -> Optional[Type[Any]]:
|
||||
|
||||
@@ -78,6 +78,40 @@ def test_unloaded_module_class_handler_is_skipped() -> None:
|
||||
sys.modules.pop(fake_name, None)
|
||||
|
||||
|
||||
def test_unloaded_module_decorator_wrapped_method_is_skipped() -> None:
|
||||
"""装饰器包装的类方法限定名含 <locals>,模块卸载后也必须跳过而非直调。"""
|
||||
fake_name = "tests._fake_unloaded_decorated_plugin"
|
||||
fake_module = types.ModuleType(fake_name)
|
||||
sys.modules[fake_name] = fake_module
|
||||
try:
|
||||
exec(
|
||||
"def _deco(f):\n"
|
||||
" def wrapper(self, event):\n"
|
||||
" return f(self, event)\n"
|
||||
" return wrapper\n"
|
||||
"class _DecoratedPlugin:\n"
|
||||
" @_deco\n"
|
||||
" def send_msg(self, event):\n"
|
||||
" raise AssertionError('residual handler must not run')\n",
|
||||
fake_module.__dict__,
|
||||
)
|
||||
residual_handler = fake_module._DecoratedPlugin.send_msg
|
||||
# 装饰器包装后限定名含 <locals>,不能因此被误判为自由函数
|
||||
assert "<locals>" in residual_handler.__qualname__
|
||||
del sys.modules[fake_name]
|
||||
|
||||
binding = EventBindingResolver(
|
||||
lock=threading.Lock(),
|
||||
resolvers=lambda: {},
|
||||
)
|
||||
assert binding.resolve(residual_handler) is None
|
||||
assert binding.unresolved_handlers() == (
|
||||
"unknown_module._deco.<locals>.wrapper",
|
||||
)
|
||||
finally:
|
||||
sys.modules.pop(fake_name, None)
|
||||
|
||||
|
||||
def test_free_function_handler_still_invoked_directly() -> None:
|
||||
"""自由函数处理器不属于类声明,保持直调路径不被新跳过逻辑影响。"""
|
||||
binding = EventBindingResolver(
|
||||
|
||||
Reference in New Issue
Block a user