build(repo): 落成工具链、依赖契约与十个模块的空骨架
第 ③ 阶段剩下的那半:架构文档之外,import-linter 契约也落地了。 pyproject.toml 把九条依赖规则里的七条写成五条 import-linter 契约。 分层用一条 layers 契约表达规则 1、2、3、8,`|` 表示同层互不 import; 另外四条 forbidden 分别管下游反向 import、polygateway 的唯一入口、 以及三个纯逻辑模块不碰 asyncio 与 pathlib。 契约不是平凡的绿:故意注入两处违规验证过,都被点名到行号。 先建十个模块的空包,是为了避开 PolyGateway bootstrap 期那段 Makefile 门控—— 它当时没有包,lint-imports 报 module not found 而红,只好加一段跳过逻辑。 空包让契约从第一天就真的在跑。 剩下两条规则落不进契约,写成了 tests/unit 下的测试: 规则 6「types 与 ports 不许 import 任何第三方」判据要反过来写(只许标准库和自己), 规则 9「import polyloop 之后 sys.modules 里没有 polygateway」是运行时事实。 另加硬约束 §1.1 零业务假设的黑名单扫描——它第一次跑就抓到 _assembly 的 docstring 里写了 dissect 的业务词,已改。三个扫描类测试都带 fail-closed 守卫, 防止目录搬走之后扫到空列表安静地绿。 工程约定取自实验室已有项目:setuptools + src layout、ruff 十一项 select、 line-length 100 来自 PolyGateway;dev 工具链版本钉死、--strict-markers 与 --import-mode=importlib 来自 CHSAnalyzer 与 dissect 踩过的坑,各自的理由写在配置注释里。 e2e 默认不跑,它打真实网关要花钱。 CLAUDE.md §0 那句「契约还没写,要等 src/ 落地」已过期,改掉; README 勾掉第 ②③ 阶段,补上本地检查命令与 GovDoc-Editor 那一行。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""包基线:版本号双写一致,以及 import 本库不会把网关拉起来。
|
||||
|
||||
**这个文件读源码文本和 `pyproject.toml`,不是只跑被测代码。** 它仍然属于 unit,因为
|
||||
分层判据是「依赖什么」(`CLAUDE.md` §1.9),而它不连任何外部服务。
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import polyloop
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def test_version_matches_pyproject() -> None:
|
||||
"""`__version__` 与 `pyproject.toml` 的 version 必须一致。
|
||||
|
||||
两处双写是刻意的(见 `polyloop/__init__.py`),代价就是会漂移,所以用这条断言钉住。
|
||||
漂移的表现是下游报 bug 时说的版本号和实际装的不是一个,而那种错查起来要绕很远。
|
||||
"""
|
||||
declared = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
assert polyloop.__version__ == declared["project"]["version"]
|
||||
|
||||
|
||||
def test_importing_polyloop_does_not_import_polygateway() -> None:
|
||||
"""依赖规则 9:`import polyloop` 之后 `sys.modules` 里不许出现 `polygateway`。
|
||||
|
||||
这条写不成 import-linter 契约——它是运行时事实,不是静态图上的边。适配器模块允许
|
||||
import 网关(规则 5),所以静态图上那条边合法;这里要禁的是**顶层 import 时就把它
|
||||
拉起来**。一个顺手提供的默认模型客户端会让每个进程在 import 本库时,把网关连同它的
|
||||
provider 目录一起加载。
|
||||
|
||||
在子进程里跑,因为本进程早就 import 过别的东西了,`sys.modules` 不干净。
|
||||
"""
|
||||
code = "import polyloop, sys; print('polygateway' in sys.modules)"
|
||||
result = subprocess.run( # noqa: S603
|
||||
[sys.executable, "-c", code],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=REPO_ROOT,
|
||||
)
|
||||
assert result.stdout.strip() == "False", result.stdout
|
||||
|
||||
|
||||
def test_py_typed_marker_ships_with_the_package() -> None:
|
||||
"""`py.typed` 必须在包根里。
|
||||
|
||||
少了它,下游的类型检查器把整个包当成无注解的黑盒——而这个库对下游的承诺大半写在
|
||||
签名里。这个文件是空的,很容易在某次移动目录时丢掉且不报错。
|
||||
"""
|
||||
assert (REPO_ROOT / "src" / "polyloop" / "py.typed").is_file()
|
||||
Reference in New Issue
Block a user