test: 补充 PEP 562 re-export 契约守护测试 (ce-code-review)
ce-code-review 9 个 reviewer 共识 finding:core/exceptions.py 的 PEP 562 __getattr__ re-export 契约无自动化守护。27 个调用方依赖 `from agentkit.core.exceptions import LLMProviderError`,但现有测试仅 顺带触发 happy path,未验证 is identity / isinstance 跨路径 / AttributeError / __dir__ / 缓存行为。误删 __getattr__ 仅靠现有测试可能延迟发现。 新增 tests/unit/test_core_exceptions_reexport.py (6 tests) 锁定 5 个不变量: 1. 三条导入路径返回同一类对象 (is identity) 2. 跨路径 isinstance/except 捕获正常 3. 未知属性抛 AttributeError (PEP 562 契约) 4. __dir__() 包含 re-export 名字 (通配导入可发现) 5. __getattr__ 调用后缓存到模块 __dict__ finding 1 of 2 (P2, gated_auto, requires_verification=true)
This commit is contained in:
parent
a4d3f227a6
commit
7449458710
|
|
@ -0,0 +1,81 @@
|
|||
"""PEP 562 re-export 契约守护测试。
|
||||
|
||||
core/exceptions.py 用模块级 __getattr__ 延迟 re-export LLM 异常类,
|
||||
打破 core.exceptions ↔ llm.exceptions 循环依赖。27 个调用方依赖
|
||||
`from agentkit.core.exceptions import LLMProviderError` 契约。
|
||||
|
||||
本测试锁定以下不变量,防止 __getattr__ 被误删或 re-export 断裂:
|
||||
1. 三条导入路径返回同一类对象(is identity)
|
||||
2. 跨路径 isinstance/except 捕获正常
|
||||
3. 未知属性抛 AttributeError
|
||||
4. __dir__() 使通配导入能发现 re-export
|
||||
5. __getattr__ 调用后缓存到模块 __dict__(后续访问不再触发 lazy import)
|
||||
"""
|
||||
|
||||
import agentkit.core.exceptions as core_exc
|
||||
import agentkit.llm.exceptions as llm_exc
|
||||
from agentkit.core.exceptions import (
|
||||
LLMError,
|
||||
LLMProviderError,
|
||||
ModelNotFoundError,
|
||||
)
|
||||
|
||||
_REEXPORT_NAMES = ("LLMError", "LLMProviderError", "ModelNotFoundError")
|
||||
|
||||
|
||||
def test_reexport_class_identity() -> None:
|
||||
"""core.exceptions 的 LLM 异常类与 llm.exceptions 中的是同一对象。"""
|
||||
assert core_exc.LLMError is llm_exc.LLMError
|
||||
assert core_exc.LLMProviderError is llm_exc.LLMProviderError
|
||||
assert core_exc.ModelNotFoundError is llm_exc.ModelNotFoundError
|
||||
|
||||
|
||||
def test_reexport_isinstance_cross_path() -> None:
|
||||
"""从 llm.exceptions raise 的异常能被 core.exceptions 的 except 捕获。"""
|
||||
for exc_cls in (llm_exc.LLMError, llm_exc.LLMProviderError, llm_exc.ModelNotFoundError):
|
||||
try:
|
||||
raise exc_cls("test")
|
||||
except LLMError:
|
||||
pass # 所有三个都是 LLMError 子类或自身
|
||||
else:
|
||||
raise AssertionError(f"{exc_cls.__name__} 未被 core.exceptions.LLMError 捕获")
|
||||
|
||||
|
||||
def test_getattr_raises_attributeerror_for_unknown() -> None:
|
||||
"""访问不存在的属性抛 AttributeError(PEP 562 契约)。"""
|
||||
try:
|
||||
core_exc.NonExistentException # noqa: B018
|
||||
except AttributeError as e:
|
||||
assert "NonExistentException" in str(e)
|
||||
else:
|
||||
raise AssertionError("预期 AttributeError 未抛出")
|
||||
|
||||
|
||||
def test_dir_includes_reexport_names() -> None:
|
||||
"""__dir__() 包含三个 re-export 名字,使 from ... import * 可发现。"""
|
||||
dir_names = set(dir(core_exc))
|
||||
for name in _REEXPORT_NAMES:
|
||||
assert name in dir_names, f"{name} 不在 dir(core.exceptions) 中"
|
||||
|
||||
|
||||
def test_getattr_caches_to_module_dict() -> None:
|
||||
"""__getattr__ 调用后,类被缓存到模块 __dict__,后续访问不再触发 lazy import。
|
||||
|
||||
白盒验证:直接调用 __getattr__ 后检查 __dict__。即使其他测试已触发缓存,
|
||||
本测试仍有效(幂等:再次调用 __getattr__ 会重新写入相同类对象)。
|
||||
"""
|
||||
result = core_exc.__getattr__("LLMError") # 显式触发 lazy import
|
||||
assert result is llm_exc.LLMError
|
||||
# 缓存后,三个名字都应存在于模块 __dict__(__getattr__ 一次性缓存全部)
|
||||
for name in _REEXPORT_NAMES:
|
||||
assert name in core_exc.__dict__, f"{name} 未被缓存到 core.exceptions.__dict__"
|
||||
assert core_exc.__dict__[name] is getattr(llm_exc, name)
|
||||
|
||||
|
||||
def test_llmprovider_error_inheritance_preserved() -> None:
|
||||
"""re-export 不影响继承链:LLMProviderError 仍是 LLMError 子类。"""
|
||||
assert issubclass(LLMProviderError, LLMError)
|
||||
assert issubclass(ModelNotFoundError, LLMError)
|
||||
err = LLMProviderError(provider="openai", reason="timeout")
|
||||
assert isinstance(err, LLMError)
|
||||
assert err.provider == "openai"
|
||||
Loading…
Reference in New Issue