我们如何衡量 PEP 符合性
Basilisk 由官方 python/typing 符合性套件评分——也就是类型社区用来为 pyright、mypy、pyrefly、ty 等打分的同一套测试与评分工具。我们在每次改动时,对真实的 basilisk 二进制文件原样运行该工具。
目前的结果是 40.4%——146 个测试文件中 59 个通过,捕获 955 个必需错误,仍有 285 处误报和 36 处遗漏的必需错误待清除。21 个类别中有 3 个达到 100%。目标是 100%,我们逐步逼近。
Python 类型规范 ↗ 符合性套件与 README ↗ 已发布结果 ↗ 我们的评分器 score.py ↗ 内置计算器 ↗
符合性套件是什么
Python 类型规范定义了类型系统应当如何运作——泛型、协议、dataclass、TypedDict、重载、字面量等。为了让规范不停留在纸面上,类型社区在 python/typing 仓库中与规范并行维护着一套符合性测试套件。
它的工作方式是:
- 每个规范章节对应一个或多个测试文件——普通的 Python 模块,用
# E注释标出每一行符合规范的类型检查器必须报告错误的位置(以及用# E[tag]组标出多个相关错误中报告其一即可的位置)。 - 一个小型评分工具对这些文件运行某个类型检查器,并将其输出与注释做差异比对。文件只有在差异为空时才通过:每个必需错误都被报告,且没有任何诊断落在套件未标记的行上。
- 维护者用它为每个检查器打分,并发布结果表——pyright 约 99%、pyrefly 约 86% 等数字便是这样得出的。
我们使用的正是这套套件,固定在提交 268d0c4e。因为同样的工具与文件为所有人打分,这个数字在各检查器之间可比,也不是我们能朝自己有利方向调整的。
一个文件如何评分
整个算法就是套件 main.py 中的两个函数——get_expected_errors(读取 # E 注释)与 diff_expected_errors(与检查器输出比对)。文件当且仅当该差异为空时通过:
- 套件的规则(
upstream_main.py:185):"Fail" if errors_diff.strip() else "Pass"
我们计入检查器发出的每一个诊断——错误和警告,不排除任何诊断代码。这是套件最严格的读法,也是参考检查器 pyright 的评分方式。一个多余的诊断(一处误报)就会让整个文件失败,这正是误报数与通过数同样重要的原因。
我们如何在不分叉的情况下运行它
套件的 main.py 是给 python/typing 维护者用的批处理工具:它一次性为所有已知检查器打分,引入 TOML 配置/报告依赖,并写出结果矩阵。它无法调用我们的二进制文件。因此,正如套件为每个检查器所做的那样(PyrightTypeChecker、MypyTypeChecker 等),我们加一个薄薄的适配器,复用套件自己的评分而非重新实现。我们的 score.py:
- 适配器——运行
basilisk check --output json,把结果整理成套件函数期望的{line: [errors]}字典(这是套件唯一无法替我们做的事)。 - 计算器——从一份字节级一致的套件
main.pycommitted 副本中导入get_expected_errors与diff_expected_errors并原样调用(score.py:287对应套件自己在upstream_main.py:175的调用)。它不含任何自己的评分逻辑。 - 门禁——将结果与
coverage-thresholds.json比较,任何回归都让 CI 失败。
为保证计算器可信,内置副本经 sha256 固定。score.py 在每次运行时重新哈希它,若有漂移则拒绝评分(score.py:99);本网站在构建时也会再次重新哈希:
保持官方文件不被改动正是要点所在:适配器与门禁住在另一个可审计的文件里,因此计算器逐字节就是套件自己的那一份。
我们做的一处更正
我们的得分过去由仓库内自己的一个脚本衡量,而它是错误的。该脚本将若干诊断代码排除在评分之外,且未计入误报,因此报出的数字一路爬到了 100%。这是一个诚实的失误,并非有意调高——但它仍然是错的。
我们用上面所述的官方计算器替换了它。在计入每个诊断、不排除任何代码之后,诚实的数字是 40.4%:
下面的图表在构建时直接读取 conformance/conformance_status.csv 的 git 历史:每个改动该文件的提交对应一个点,绘制该提交实际记录的得分。
在 Jun 21,仓库内脚本报告了 100%。官方计算器首次于 Jun 23 运行,报出 40.4%——这是更正,而非回归。
- 早期仓库内脚本(排除部分代码、未计入误报)
- 官方
python/typing计算器
每个点都是对 conformance/conformance_status.csv 的真实提交,每次构建重新计算。悬停某点可查看其日期、提交、得分与误报数。
各类别现状
构建时从 conformance/conformance_status.csv 实时读取:
| 类别 | 通过 | 得分 | |
|---|---|---|---|
| Aliases | 3 / 7 | 42.9% | |
| Annotations | 3 / 5 | 60% | |
| Callables | 1 / 4 | 25% | |
| Classes | 0 / 2 | 0% | |
| Constructors | 1 / 6 | 16.7% | |
| Dataclasses | 7 / 16 | 43.8% | |
| Directives | 7 / 11 | 63.6% | |
| Enums | 3 / 8 | 37.5% | |
| Exceptions | 0 / 1 | 0% | |
| Generics | 9 / 30 | 30% | |
| Historical | 1 / 1 | 100% | |
| Literals | 0 / 4 | 0% | |
| NamedTuples | 4 / 4 | 100% | |
| Narrowing | 0 / 2 | 0% | |
| Overloads | 0 / 4 | 0% | |
| Protocols | 6 / 13 | 46.2% | |
| Qualifiers | 2 / 5 | 40% | |
| Special types | 0 / 5 | 0% | |
| Tuples | 0 / 3 | 0% | |
| TypedDicts | 11 / 14 | 78.6% | |
| TypeForms | 1 / 1 | 100% |
自己复现
# 构建二进制、获取(被 git 忽略的)测试夹具、对其运行官方 python/typing
# 计算器、写出 conformance_status.csv,并强制执行 coverage-thresholds.json 中的棘轮门禁。
make conformance
这一切都在两个文件里:conformance/score.py(我们的适配器与门禁)和 conformance/upstream_main.py(套件的计算器,committed 且经 sha256 固定)。完整的注解规则见 python/typing 符合性 README。