我们如何衡量 PEP 符合性

Basilisk 由官方 python/typing 符合性套件评分——也就是类型社区用来为 pyright、mypy、pyrefly、ty 等打分的同一套测试与评分工具。我们在每次改动时,对真实的 basilisk 二进制文件原样运行该工具。

目前的结果是 40.4%——146 个测试文件中 59 个通过,捕获 955 个必需错误,仍有 285 处误报36 处遗漏的必需错误待清除。21 个类别中有 3 个达到 100%。目标是 100%,我们逐步逼近。

符合性套件是什么

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 配置/报告依赖,并写出结果矩阵。它无法调用我们的二进制文件。因此,正如套件为每个检查器所做的那样(PyrightTypeCheckerMypyTypeChecker 等),我们加一个薄薄的适配器,复用套件自己的评分而非重新实现。我们的 score.py

  1. 适配器——运行 basilisk check --output json,把结果整理成套件函数期望的 {line: [errors]} 字典(这是套件唯一无法替我们做的事)。
  2. 计算器——从一份字节级一致的套件 main.py committed 副本中导入 get_expected_errorsdiff_expected_errors 并原样调用(score.py:287 对应套件自己在 upstream_main.py:175 的调用)。它不含任何自己的评分逻辑。
  3. 门禁——将结果与 coverage-thresholds.json 比较,任何回归都让 CI 失败。

为保证计算器可信,内置副本经 sha256 固定score.py 在每次运行时重新哈希它,若有漂移则拒绝评分(score.py:99);本网站在构建时也会再次重新哈希:

保持官方文件不被改动正是要点所在:适配器与门禁住在另一个可审计的文件里,因此计算器逐字节就是套件自己的那一份。

我们做的一处更正

我们的得分过去由仓库内自己的一个脚本衡量,而它是错误的。该脚本将若干诊断代码排除在评分之外,且未计入误报,因此报出的数字一路爬到了 100%。这是一个诚实的失误,并非有意调高——但它仍然是错的。

我们用上面所述的官方计算器替换了它。在计入每个诊断、不排除任何代码之后,诚实的数字是 40.4%

100% 40.4% 检查器没有变差——是衡量变正确了。100% 是我们正在努力达成的目标,而非对当下的宣称。

下面的图表在构建时直接读取 conformance/conformance_status.csv 的 git 历史:每个改动该文件的提交对应一个点,绘制该提交实际记录的得分。

符合性得分随时间变化从早期仓库内数字到官方计算器
0% 25% 50% 75% 100% Apr 27 (69038b92): 83.4% — 121/145, 434 false positives · earlier in-repo harness Apr 27 Apr 27 (f4d6e27c): 91.1% — 133/146, 258 false positives · earlier in-repo harness Apr 27 (d690f130): 91.8% — 134/146, 177 false positives · earlier in-repo harness Apr 27 (c4ecadf2): 91.8% — 134/146, 174 false positives · earlier in-repo harness Apr 27 (bc8ac5e1): 92.5% — 135/146, 174 false positives · earlier in-repo harness May 30 (0c790c93): 92.5% — 135/146, 173 false positives · earlier in-repo harness May 30 May 30 (a2341e76): 92.5% — 135/146, 170 false positives · earlier in-repo harness Jun 3 (bf832a07): 93.2% — 136/146, 170 false positives · earlier in-repo harness Jun 3 Jun 3 (75aa31c9): 93.2% — 136/146, 126 false positives · earlier in-repo harness Jun 6 (19a0ad54): 93.8% — 137/146, 120 false positives · earlier in-repo harness Jun 6 Jun 12 (a273d83d): 98.6% — 144/146, 54 false positives · earlier in-repo harness Jun 12 Jun 19 (f9e14551): 98.6% — 144/146, 0 false positives · earlier in-repo harness Jun 19 Jun 21 (7bca6179): 100% — 146/146, 0 false positives · earlier in-repo harness Jun 21 Jun 23 (214e9812): 40.4% — 59/146, 285 false positives · official calculator Jun 23 100% 40.4%

Jun 21,仓库内脚本报告了 100%。官方计算器首次于 Jun 23 运行,报出 40.4%——这是更正,而非回归。

  • 早期仓库内脚本(排除部分代码、未计入误报)
  • 官方 python/typing 计算器

每个点都是对 conformance/conformance_status.csv 的真实提交,每次构建重新计算。悬停某点可查看其日期、提交、得分与误报数。

各类别现状

构建时从 conformance/conformance_status.csv 实时读取:

类别通过得分
Aliases3 / 742.9%
Annotations3 / 560%
Callables1 / 425%
Classes0 / 20%
Constructors1 / 616.7%
Dataclasses7 / 1643.8%
Directives7 / 1163.6%
Enums3 / 837.5%
Exceptions0 / 10%
Generics9 / 3030%
Historical1 / 1100%
Literals0 / 40%
NamedTuples4 / 4100%
Narrowing0 / 20%
Overloads0 / 40%
Protocols6 / 1346.2%
Qualifiers2 / 540%
Special types0 / 50%
Tuples0 / 30%
TypedDicts11 / 1478.6%
TypeForms1 / 1100%

自己复现

# 构建二进制、获取(被 git 忽略的)测试夹具、对其运行官方 python/typing
# 计算器、写出 conformance_status.csv,并强制执行 coverage-thresholds.json 中的棘轮门禁。
make conformance

这一切都在两个文件里:conformance/score.py(我们的适配器与门禁)和 conformance/upstream_main.py(套件的计算器,committed 且经 sha256 固定)。完整的注解规则见 python/typing 符合性 README