将指定文件迁移至更严格的 Pyrefly 类型检查模式,要求所有函数、类以及属性必须补全类型标注。
Pyrefly Type Coverage Skill
前置条件
- 目标文件必须位于包含
pyrefly.toml的项目目录中。 - 系统 PATH 中必须能找到
pyrefly、lintrunner以及该项目的测试运行工具。如果缺少任何一个,请立即停止并询问是否需要激活 conda 环境 —— 切勿擅自安装或寻找替代工具(遵循仓库的 CLAUDE.md 规范)。
步骤 1:清理文件开头的类型检查忽略注释
删除文件顶部所有的此类注释(为了兼容 mypy,pyrefly 也会识别 # mypy: ignore-errors,因此这个也必须删掉):
# pyre-ignore-all-errors
# pyre-ignore-all-errors[16,21,53,56]
# @lint-ignore-every PYRELINT
# mypy: ignore-errors
步骤 2:在 pyrefly.toml 中添加子配置项
[[sub-config]]
matches = "path/to/directory/**"
[sub-config.errors]
implicit-import = false
implicit-any = true
bad-param-name-override = false
unannotated-return = true
unannotated-parameter = true
重要提示:在 [sub-config.errors] 中配置任何错误类型标识,只会覆盖父级配置中的该项规则;但开启 unannotated-return / unannotated-parameter / implicit-any 会让之前全文件屏蔽的错误重新暴露出来。如果你发现不相关的错误(例如 bad-param-name-override)大量刷屏,可以在该子配置中显式复制父级配置对该项的设定以将其静音。
步骤 3:运行 pyrefly 校验
pyrefly check <FILENAME>
核心目标: 通过补充类型标注,彻底解决所有 unannotated-return、unannotated-parameter 和 implicit-any 报错 —— 详见步骤 4 的推导梯级。这三类目标报错始终是可以被修复的;绝不要使用 # pyrefly: ignore 去硬压屏蔽它们。唯一的特例是带 @compatibility(is_backward_compatible=True) 装饰器的代码(详见步骤 4)。
至于其他类别的报错(如 bad-argument-type、missing-attribute 等),它们属于真实的类型 Bug。请根据 pyrefly 报错的位置分别处理:
- 报错出在其他文件(路径 != 目标文件):保持原状,不要扩大修改范围。如果该报错阻塞了当前目标文件的测试,请直接在报错位置加上
# pyrefly: ignore[<category>] # TODO进行屏蔽。 - 报错出在目标文件内部,但报错信息涉及的符号定义在外部(例如因导入函数的类型标注有误,导致当前文件报
bad-return):同样在本地使用上述带 TODO 的注释进行压制。不要擅自使用cast()试图掩盖上游缺陷。 - 报错在目标文件内部,且源头也在本地:直接修复问题。
原则上,只有作为保底手段且仅针对非目标类别的报错时,才允许使用 # pyrefly: ignore[...]。
步骤 4:添加类型标注
当无法仅凭函数体直接推断出准确类型时,请深入检查其调用处。
标注规范约定
- 使用 PEP 604 / PEP 585 语法(如
int | None、list[str])—— 假设环境为 Python >= 3.10。 - 对于抽象基类(ABCs,如
Callable、Sequence、Generator等),优先从collections.abc导入,而非typing。 - 对于泛型辅助工具,若项目支持的最低 Python 版本已内置,则从
typing导入;仅当确实需要较新的语言特性时(例如在兼容 < 3.11/3.12 版本时使用Self或override,或使用 PEP 696 中TypeVar/ParamSpec的default=语法),才使用typing_extensions。切勿一概从typing_extensions盲目导入。 Callable必须带参数类型标注(严禁使用裸Callable)。推荐优先使用Callable[..., object];只有当调用方确实需要消费其动态返回值时,才选用Callable[..., Any]—— 如果该返回值只是透传(甚至该可调用对象根本不会被触发调用),object更加严格且同样正确。(若为保持函数签名的包装器场景,请参阅后文的 ParamSpec 说明。)- 凡是你新引入的模块局部全局变量,名称一律加上前缀下划线 —— 无论是
TypeVar/ParamSpec(需与字符串参数保持一致:_T = TypeVar("_T")、_P = ParamSpec("_P")、_R = TypeVar("_R")),还是TypeAlias、辅助常量或 Sentinel。这是 PyTorch 源码中非公开名称的标准约定(在整个代码库中_P与P的使用比例约为 6:1)。例外情况(无需下划线):被其他模块显式导入的名称、在__all__中列出的名称,或用作运行时标记的名称(例如注解字符串分发标记)。本规则仅适用于你新添加的名称 —— 切勿重命名代码库中原有的全局变量,那属于超出本 Skill 范围的无关重构。 - 对于返回布尔值的断言函数(命名为
is_*/has_*,通常接收较宽泛的类型如object并返回bool),往往需要声明为TypeGuard[X](或TypeIs[X],后者还能同时对否定分支进行类型收窄)。TypeGuard属于typing(Python >= 3.10,直接从此处导入);TypeIs在 Python 3.13 才加入typing,因此需从typing_extensions(>= 4.10)导入以保持 3.10 兼容。对于接收klass: type[_T]的类似issubclass的辅助工具,返回值应为TypeGuard[type[_T]]。在issubclass()附近,相比使用try/except TypeError捕获异常,更推荐显式使用isinstance(x, type)防护 —— 这样可读性更好,且便于检查器自动收窄类型。 - 当返回值类型推导自入参时 —— 比如透传/恒等函数、“返回入参之一”的辅助工具、装饰器、以类型为键的注册表 —— 应该使用
TypeVar(或者针对函数签名透传的 callable 参数,使用带有ParamSpec/TypeVar的Callable[_P, _R]),而不是放宽到object/Any。“输出类型 == 某个输入类型”正是TypeVar表达的核心逻辑;直接设为object输入 /object输出会丢失这个约束关系。注意:如果函数会转换传入的值,导致输出类型与输入不同(例如将数组转成 int),那么使用单个TypeVar就是错的 —— 此时应写出具体的实际类型。 - 在
__init__中赋值的类属性,应该在类层级显式声明类型标注,以便 pyrefly 识别。 - 使用
if TYPE_CHECKING:解耦循环导入 —— 仅用于类型标注的导入放入此条件块中,并配合from __future__ import annotations(或字符串形式的前向引用),从而保持运行时导入的延迟加载特性:from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from torch.fx import GraphModule def transform(gm: GraphModule) -> GraphModule: ... - 绝对不要屏蔽这三类目标报错。
unannotated-return、unannotated-parameter和implicit-any始终可以通过补充类型标注来解决;直接使用# pyrefly: ignore[<其中之一>]是完全不可接受的结果。唯一的例外是下文说明的向后兼容特例。 - 宁可放宽类型,也不要放弃检查。 当难以推断出最准确的类型时,请顺着以下阶梯依次退守,而不是直接使用 ignore:
- 从调用处和返回路径中可观察到的最具体的具体类型。
- 联合类型(
X | Y)、Sequence[X]风格的抽象类型,或者针对纯泛型函数(透传、容器辅助函数)的有界TypeVar。 object—— 能通过类型检查的最严格退守类型。它会强制调用方在使用前先显式收窄类型,例如def serialize(value: object) -> str:。视觉上与Any类似,但约束严格得多 —— 如果没有使用isinstance收窄,pyrefly 会直接拒绝value.foo()。Any—— 最底层的阶梯。在目标报错类别上,它的优先级始终高于# pyrefly: ignore,但前提是 1–3 级全都无法满足。请务必说明为什么前几级都不适用(例如“联合类型包含了超过 8 种类型”、“找不到明确的共性基类边界”、“调用方确实完全不需要做类型收窄”)。
- 对返回值位置上的
object/Any要保持高度警惕 —— 函数本身对它产出的数据结构的了解,通常远多于调用方。宽泛的返回值类型只在真正的边界场景下才合理(比如原封不动返回输入值,或者返回值完全由 Handler/调用方控制);如果函数体内部构造了固定结构,请务必写出清晰的类型(定义领域别名或联合类型,效果都远好于object)。 - 在判定某个参数必须使用
Any之前,请至少翻阅 3 处调用位置 —— 不要一看到有动态调用的苗头就立马打上Any。 - 窄作用域的
# pyrefly: ignore[...](针对非目标类别)仅限用于 pyrefly 对局部具体报错确实判断有误的场景 —— 比如动态元编程、第三方缺少 stub 补丁包:# pyrefly: ignore[attr-defined] result = getattr(obj, dynamic_name)() - 当单行的行内
# pyrefly: ignore[...]会导致超出行长限制时,请直接将其放在报错行的上一行,不要引入# fmt: skip来维持同行 —— pyrefly 完全支持在上一行进行 ignore 压制。(例外:下文的向后兼容场景必须写在def同行。)
向后兼容性(绝对不压制的唯一例外)
重中之重:带有 @compatibility(is_backward_compatible=True) 装饰器的函数,绝对不能修改其函数签名。向后兼容测试(test_function_back_compat)会将字符串化的 inspect.signature 与 Golden baseline 文件进行对比 —— 只要增加了类型标注(哪怕是 -> None),导出的签名字符串就会改变,导致测试跑失败。这种情况下必须改用 pyrefly ignore 注释:
@compatibility(is_backward_compatible=True)
def my_function( # pyrefly: ignore[unannotated-return]
self,
arg1, # 这里也不能加类型标注
):
...
# pyrefly: ignore 注释必须紧跟在 def 这一行(即 pyrefly 抛出报错的位置),而不能放在闭合括号 ) 处。
保持签名的包装器专用 ParamSpec(用于装饰器、类似 functools.wraps 的辅助函数)。使用 Callable[P, R] 可以将被包装函数的签名原封不动传递给调用方 —— 换成 Callable[..., Any] 会丢失完整的类型签名信息。只有当包装器确实接受任意 callable 时才跳过 ParamSpec。如果包装器会追加或前置参数,请配合 Concatenate[X, P] 使用。
from collections.abc import Callable
from typing import ParamSpec, TypeVar
_P = ParamSpec("_P")
_R = TypeVar("_R")
def log_calls(fn: Callable[_P, _R]) -> Callable[_P, _R]:
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
return fn(*args, **kwargs)
return wrapper
步骤 5:迭代与修复
重新运行 pyrefly check。新的类型标注通常会暴露原本隐蔽的 bad-return 错误(即函数实际返回了不兼容的类型)—— 修复这些问题。重复此过程,直到检查完全通过。
如果你收紧了公共辅助函数的类型定义(例如添加了 TypeGuard 或更精准的返回值),可能导致调用方原本存在的 # pyrefly: ignore 注释失效。请再次校验并清理这些已失效的压制注释及过期说明注释 —— 避免残留垃圾代码。
步骤 6:代码格式与 Lint 校验
提交交付前必须执行 —— 添加类型标注经常会导致导入顺序改变或行长超限:
lintrunner -a <files...>
对于 lintrunner 无法自动修复的问题,进行手动修复。
步骤 7:测试验证
测试失败时的优先级原则:测试通过 > pyrefly 检查干净 > 类型标注严格度。如果新添加的类型标注破坏了测试,请在恢复文件前,先顺着标注阶梯往下退守一格(例如:具体类型 → object,或者移除破坏了下游 isinstance 校验的 Any 放宽)。
-
向后兼容性检查。 当且仅当
grep -l '@compatibility(is_backward_compatible=True)' <target>能查出目标文件时才执行此项 —— 该装饰器才是 Golden baseline 文件真正的触发前提。泛泛地查找“是否导入了torch.fx”这套启发规则会误伤torch/目录下一半的代码。python -m pytest test/test_fx.py::TestFXAPIBackwardCompatibility -x -v -
受改动模块的单测。 在下结论说“没有单测覆盖”前,请按以下两种方式做双向排查:
# torch/foo/bar.py 通常由 test/test_foo.py 或 test/test_bar.py 覆盖 ls test/ | grep -i <module-name> # 或者按导入语句进行搜索 grep -rl "from torch.foo.bar import\|import torch.foo.bar" test/如果两种方式都没搜出测试文件,请明确告知用户 —— 切勿默默跳过。类型的变更可能会引发真实的运行时回归问题(如
Optional[X]与X的差异、或调用.append时Sequence与list的行为差异等)。
注意事项
- 类体内部的前向引用:在没有配置
from __future__ import annotations时,依然需要加字符串引号:class MyClass: def __new__(cls) -> "MyClass": ... - 提交代码(Commit):除非用户明确要求(遵循仓库 CLAUDE.md),否则切勿自行执行 git commit。文件修复完毕并通过检查后,停下来并将 diff 展示给用户进行 Review。






