"""`research-wiki/design/0009-keyword-only-public-types.md`:数据类一律只收关键字参数。 守的是一条对下游的承诺——**字段顺序不受任何保护**,所以往中间插字段永远是安全的。没有这条 限制,只要有人能按位置构造,字段顺序就自动成了承诺的一部分,而插字段会静默改掉后面每一个 参数的含义:不报错,只是每个值都进错了字段。 实验室里已经有一个现成的教训:另一个库的一个 18 字段类型,前 11 个的顺序被三个下游的测试 替身按位置构造锁死,它从此再也不能往中间插东西,还得专门写一条测试守着。那个约束不是它选 的,是它继承的。本库还没有任何下游装上,所以它可以不长出来。 **这条测试断言的是承诺不是实现**(`CLAUDE.md` §1.8):构造方式是对外承诺过的东西。 """ import dataclasses import importlib import inspect import pkgutil import pytest import polyloop pytestmark = pytest.mark.unit def _all_dataclasses() -> list[type]: """遍历包内每一个模块,收集其中定义的数据类。 按 `__module__` 过滤,免得把一个模块 import 进来的别处的数据类重复算一遍——重复本身无害, 但它会让下面那条 fail-closed 守卫的计数虚高,于是守卫失去意义。 """ found: dict[str, type] = {} modules = [polyloop] for info in pkgutil.walk_packages(polyloop.__path__, prefix="polyloop."): modules.append(importlib.import_module(info.name)) for module in modules: for name in dir(module): candidate = getattr(module, name) if not isinstance(candidate, type) or not dataclasses.is_dataclass(candidate): continue if candidate.__module__ != module.__name__: continue found[f"{candidate.__module__}.{candidate.__qualname__}"] = candidate return list(found.values()) def test_the_scan_finds_something() -> None: """守卫自身的 fail-closed 检查。 没有这一条,包被改名或搬走之后下面那条断言会遍历一个空列表然后安静地绿,而绿的含义从 「全都合规」变成了「什么都没检查」,两者在输出上分不出来。 """ found = _all_dataclasses() assert len(found) >= 20, f"只扫到 {len(found)} 个数据类,扫描范围多半错了" assert {cls.__module__ for cls in found} >= {"polyloop.types", "polyloop.ports"} def test_every_dataclass_is_keyword_only() -> None: """包内每一个数据类都只收关键字参数,内部模块也一样。 统一规则不留判断余地,也才写得成这条检查。按类型大小分档的话,每加一个类型都要判一次, 而判错那次不会当场报错。 **查的是构造签名,不是那个装饰器参数。** 要守的承诺是「按位置构造不了」,而 `kw_only=True` 只是达成它的一种写法——逐字段标 `field(kw_only=True)` 是另一种,将来 Python 再多一种也一样。查签名的话这条检查不会因为写法换了就漏掉。 """ positional: list[str] = [] for cls in _all_dataclasses(): offenders = [ name for name, parameter in inspect.signature(cls).parameters.items() if parameter.kind is not inspect.Parameter.KEYWORD_ONLY ] if offenders: positional.append(f"{cls.__module__}.{cls.__qualname__}: {offenders}") assert positional == [], f"这些数据类还能按位置构造:{positional}"