fix: 插件重载窗口期装饰器包装类方法误判直调导致 missing event TypeError

事件绑定解析在 owner_class 为 None 时仅靠限定名区分自由函数与类方法声明,
装饰器包装的方法限定名含 <locals> 被误判为自由函数 unbound 直调,把 event
吞进 self 触发 TypeError。局部作用域限定名无法区分包装方法与局部自由函数,
按调用约定兜底:签名首参为 self/cls 才视为类方法声明跳过执行。
This commit is contained in:
jxxghp
2026-08-20 08:51:37 +08:00
parent 307d54bf68
commit 27ae1b5290
2 changed files with 54 additions and 2 deletions
+20 -2
View File
@@ -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]]:
+34
View File
@@ -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(