关于 pyrefly-type-coverage
pyrefly-type-coverage 是用于提升 PyTorch Python 文件 Pyrefly 类型覆盖率的技能,提供配置、注解、Lint 和测试流程,并说明如何保留向后兼容的签名。
主要功能
- 01Pyrefly 配置指南
- 02移除文件级别的抑制项
- 03注解规范与类型收窄
- 04Lint 和测试流程
- 05处理向后兼容的函数签名
适用场景
- 01为 Python 模块添加类型注解
- 02解决 Pyrefly 的隐式 Any 和缺少注解错误
- 03提升类型覆盖率且不改变兼容签名
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pytorch/ pytorch --skill pyrefly-type-coverage安装后在 Claude Code 中输入/pyrefly-type-coverage确认已加载。
详细介绍
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。 - 对于 ABC,优先使用
collections.abc而非typing(Callable、Sequence、Generator等)。 - 对于泛型辅助工具,如果项目最低 Python 版本已提供相关功能,则从
typing导入;只有在需要较新功能时才从typing_extensions导入(例如,支持 < 3.11/3.12 时使用Self和override,或为TypeVar/ParamSpec使用 PEP 696 的default=)。不要一概从typing_extensions导入。 - 始终为
Callable指定参数类型(绝不要使用不带参数的Callable)。优先使用Callable[..., object];只有在调用方确实会使用动态返回值时才使用Callable[..., Any]——如果结果只是传递下去(或根本不会调用该可调用对象),object更严格且同样正确。(签名保留包装器的情况见下文 ParamSpec。) - 为你新增的任何模块级全局名称添加前导下划线——
TypeVar/ParamSpec(与字符串参数匹配:_T = TypeVar("_T")、_P = ParamSpec("_P")、_R = TypeVar("_R"))、TypeAlias、辅助常量和哨兵均如此。这是 torch 中非公开名称的惯例(代码库中_P的数量约为P的 6 倍)。例外(不加下划线):被其他模块导入的名称、列在__all__中的名称,或用作运行时标记的名称(例如注解字符串分发标记)。仅适用于你新增的名称——不要重命名已有全局名称;那是与本技能无关的重构。 - 布尔谓词——名称形如
is_*/has_*、接受宽泛类型(通常为object)、返回bool——通常应使用TypeGuard[X](或TypeIs[X],它也会缩窄否定分支)。TypeGuard位于typing(>= 3.10,因此从那里导入);TypeIs直到 3.13 才加入typing,因此应从typing_extensions(>= 4.10)导入,以保持与 3.10 兼容。采用issubclass风格且接受klass: type[_T]的辅助函数应返回TypeGuard[type[_T]]。优先使用显式的isinstance(x, type)守卫,而不是在issubclass()周围使用try/except TypeError——这样更清晰,也能让检查器缩窄类型。 - 当返回类型由参数派生而来时——直通 / 恒等函数、“从这些参数中返回一个”的辅助函数、装饰器、以类型为键的注册表——应使用
TypeVar(或者,对于签名会传递下去的可调用参数,使用Callable[_P, _R]搭配ParamSpec/TypeVar),而不是扩大为object/Any。“输出类型 == 某个输入类型”正是TypeVar所表达的;输入为object、输出为object会丢失这种信息。注意:如果函数会转换值,使输出类型不同于输入类型(例如,将数组转换为整数),单个TypeVar就不合适——应明确写出实际的领域类型。 - 在
__init__中赋值的类属性应添加类级注解,以便 pyrefly 能识别: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——最后一级。对于目标错误类别,始终优先使用Any而非# pyrefly: ignore,但只有在第 1–3 级均不适用后才使用。应能说明前面每一级为何不适用(例如,“联合类型超过 8 种”“不存在可观察到的公共上界”“调用方确实从不缩窄类型”)。
- 对返回位置的
object/Any尤其要谨慎——函数通常比调用方更清楚自己会生成什么。只有在真正的边界处,宽泛的返回类型才合适(函数原样返回输入,或值由处理程序 / 调用方定义);如果函数体构造了已知形状的数据,应明确写出类型(领域别名或联合类型优于object)。 - 在认定参数必须使用
Any前,至少查看三个调用点——不要第一次尝试就凭“看起来是动态的”来判断。 - 对于 pyrefly 在某个特定局部错误上确实判断有误的情况(动态元编程、第三方存根缺失),才允许在非目标类别上使用范围狭窄的
# pyrefly: ignore[...]:# 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 与黄金文件进行比较——添加注解(即使是 -> None)也会更改该字符串,导致测试失败。
请改用 pyrefly 忽略注释:
@compatibility(is_backward_compatible=True)
def my_function( # pyrefly: ignore[unannotated-return]
self,
arg1, # can't add type here either
):
...
# 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 错误,表明函数实际返回了不兼容的类型——请修复这些问题。重复此过程,直到检查通过。
收紧共享辅助函数的类型(例如添加 TypeGuard 或精确的返回类型),可能会导致其调用方中原本就存在的 # pyrefly: ignore 注释变得多余。请重新检查并删除现在已失效的抑制注释及任何过时的说明注释——不要将它们留在代码中。
第 6 步:Lint
提交前必须完成——注解经常会改变导入顺序和行长度:
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 is usually covered by test/test_foo.py or test/test_bar.py ls test/ | grep -i <module-name> # or by import 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": ... - 提交:除非用户明确要求,否则不要提交(遵循仓库的 CLAUDE.md)。文件检查通过后,请停止操作并提供差异供审查。