8.9 KiB
name, description
| name | description |
|---|---|
| pyrefly-type-coverage | 迁移文件以使用更严格的 Pyrefly 类型检查,要求所有函数、类和属性都带有注解。 |
Pyrefly 类型覆盖技能
前置条件
- 文件必须位于包含
pyrefly.toml的项目中。 pyrefly、lintrunner以及项目的测试运行器必须位于 PATH 中。如果其中任何一个缺失,请停下来询问是否需要激活 conda 环境——不要安装或替换(根据仓库 CLAUDE.md)。
第 1 步:移除文件级别的类型检查抑制
从文件顶部删除以下任何内容(Pyrefly 会为了兼容 mypy 而识别 # 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 等)是真实的类型错误。根据 pyrefly 报告的位置来处理它们:
- 在其他文件中报告(路径 != 目标):保持不动。不要扩大范围。如果该错误现在阻塞了目标文件,请在报告位置使用
# pyrefly: ignore[<category>] # TODO进行抑制。 - 在目标文件中报告,但消息引用的是其他地方定义的符号(例如,由于导入函数的注解错误导致的
bad-return):使用相同的 TODO 注释进行本地抑制。不要编造一个cast()来掩盖上游的缺口。 - 在目标文件中报告,且源自本地:修复它。
仅作为最后手段使用 # pyrefly: ignore[...],且仅用于非目标类别。
第 4 步:添加注解
当从函数体无法判断正确的类型时,检查调用点。
注解约定
- 使用 PEP 604 / PEP 585 语法(
int | None、list[str])——假设 Python >= 3.10。 - 对于抽象基类(
Callable、Sequence、Generator等),优先使用collections.abc而非typing。 - 对于泛型辅助工具,当项目的最低 Python 版本支持时从
typing导入,仅在需要使用较新特性时才从typing_extensions导入(例如,如果支持低于 3.11/3.12 的版本,则使用Self和override;或者TypeVar/ParamSpec的 PEP 696default=)。不要无差别地从typing_extensions导入。 - 始终对
Callable进行参数化——当签名确实未知时使用Callable[..., Any],绝不使用裸的Callable。(关于保持签名的包装器情况,请参见下面的 ParamSpec。) - 在
__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,但更严格——pyrefly 会在没有isinstance的情况下拒绝value.foo()。Any——最后一阶。始终优于在目标类别上使用# pyrefly: ignore,但仅在前 1–3 阶失败之后使用。必须能够说明为什么每个更早的阶梯都不适用(例如,「联合类型超过 8 种」、「没有可观察到的公共边界」、「调用者确实从不窄化」)。
- 在决定某个参数必须是
Any之前,至少阅读三个调用点——不要在第一次尝试时就模式匹配「看起来是动态的」。 - 小范围的
# pyrefly: ignore[...](用于非目标类别)保留给 pyrefly 确实错误 的情况——动态元编程、第三方存根缺口:# pyrefly: ignore[attr-defined] result = getattr(obj, dynamic_name)()
向后兼容性(从不抑制规则的唯一例外)
关键:使用 @compatibility(is_backward_compatible=True) 装饰的函数绝不能更改其签名。向后兼容性测试(test_function_back_compat)会将字符串化的 inspect.signature 与黄金文件进行比较——添加注解(即使是 -> 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] 会丢失签名。如果包装器确实接受任意可调用对象,则跳过 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 错误,表明函数实际返回了不兼容的类型——修复这些错误。重复直到没有错误。
第 6 步:代码检查
在移交之前必须执行——注解经常改变导入顺序和行长度:
lintrunner -a <files...>
手动解决 lintrunner 无法自动修复的问题。
第 7 步:测试
当某些测试失败时的优先级:测试通过 > pyrefly 无错误 > 注解严格度。如果新添加的注解导致测试失败,在回退文件之前,按严格度阶梯下降一级(例如,从具体类型降到 object,或者移除导致下游 isinstance 检查失败的 Any 放宽)。
-
向后兼容性检查。 仅在以下情况下运行:
grep -l '@compatibility(is_backward_compatible=True)' <target>返回该文件——该装饰器才是黄金文件的真正前置条件。更宽泛的「导入了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、Sequence与list在调用.append时等)。
备注
- 类体中的前向引用在没有
from __future__ import annotations时仍需要字符串引号:class MyClass: def __new__(cls) -> "MyClass": ... - 提交:除非用户明确要求,否则不要提交(根据仓库 CLAUDE.md)。当文件干净时,停下来展示差异以供审查。