pyrefly-type-coverage

pyrefly-type-coverage

热门

将指定文件迁移至更严格的 Pyrefly 类型检查模式,要求所有函数、类以及属性必须补全类型标注。

10万Star
2.9万Fork
更新于 2026/8/4
SKILL.md
只读
名称
pyrefly-type-coverage
描述

将指定文件迁移至更严格的 Pyrefly 类型检查模式,要求所有函数、类以及属性必须补全类型标注。

Pyrefly Type Coverage Skill

前置条件

  • 目标文件必须位于包含 pyrefly.toml 的项目目录中。
  • 系统 PATH 中必须能找到 pyreflylintrunner 以及该项目的测试运行工具。如果缺少任何一个,请立即停止并询问是否需要激活 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-returnunannotated-parameterimplicit-any 报错 —— 详见步骤 4 的推导梯级。这三类目标报错始终是可以被修复的;绝不要使用 # pyrefly: ignore 去硬压屏蔽它们。唯一的特例是带 @compatibility(is_backward_compatible=True) 装饰器的代码(详见步骤 4)。

至于其他类别的报错(如 bad-argument-typemissing-attribute 等),它们属于真实的类型 Bug。请根据 pyrefly 报错的位置分别处理:

  • 报错出在其他文件(路径 != 目标文件):保持原状,不要扩大修改范围。如果该报错阻塞了当前目标文件的测试,请直接在报错位置加上 # pyrefly: ignore[<category>] # TODO 进行屏蔽。
  • 报错出在目标文件内部,但报错信息涉及的符号定义在外部(例如因导入函数的类型标注有误,导致当前文件报 bad-return):同样在本地使用上述带 TODO 的注释进行压制。不要擅自使用 cast() 试图掩盖上游缺陷。
  • 报错在目标文件内部,且源头也在本地:直接修复问题。

原则上,只有作为保底手段且仅针对非目标类别的报错时,才允许使用 # pyrefly: ignore[...]

步骤 4:添加类型标注

当无法仅凭函数体直接推断出准确类型时,请深入检查其调用处。

标注规范约定
  • 使用 PEP 604 / PEP 585 语法(如 int | Nonelist[str])—— 假设环境为 Python >= 3.10。
  • 对于抽象基类(ABCs,如 CallableSequenceGenerator 等),优先从 collections.abc 导入,而非 typing
  • 对于泛型辅助工具,若项目支持的最低 Python 版本已内置,则从 typing 导入;仅当确实需要较新的语言特性时(例如在兼容 < 3.11/3.12 版本时使用 Selfoverride,或使用 PEP 696 中 TypeVar / ParamSpecdefault= 语法),才使用 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 源码中非公开名称的标准约定(在整个代码库中 _PP 的使用比例约为 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/TypeVarCallable[_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-returnunannotated-parameterimplicit-any 始终可以通过补充类型标注来解决;直接使用 # pyrefly: ignore[<其中之一>] 是完全不可接受的结果。唯一的例外是下文说明的向后兼容特例。
  • 宁可放宽类型,也不要放弃检查。 当难以推断出最准确的类型时,请顺着以下阶梯依次退守,而不是直接使用 ignore:
    1. 从调用处和返回路径中可观察到的最具体的具体类型。
    2. 联合类型(X | Y)、Sequence[X] 风格的抽象类型,或者针对纯泛型函数(透传、容器辅助函数)的有界 TypeVar
    3. object —— 能通过类型检查的最严格退守类型。它会强制调用方在使用前先显式收窄类型,例如 def serialize(value: object) -> str:。视觉上与 Any 类似,但约束严格得多 —— 如果没有使用 isinstance 收窄,pyrefly 会直接拒绝 value.foo()
    4. 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 放宽)。

  1. 向后兼容性检查。 当且仅当
    grep -l '@compatibility(is_backward_compatible=True)' <target> 能查出目标文件时才执行此项 —— 该装饰器才是 Golden baseline 文件真正的触发前提。泛泛地查找“是否导入了 torch.fx”这套启发规则会误伤 torch/ 目录下一半的代码。

    python -m pytest test/test_fx.py::TestFXAPIBackwardCompatibility -x -v
    
  2. 受改动模块的单测。 在下结论说“没有单测覆盖”前,请按以下两种方式做双向排查:

    # 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 的差异、或调用 .appendSequencelist 的行为差异等)。

注意事项

  • 类体内部的前向引用:在没有配置 from __future__ import annotations 时,依然需要加字符串引号:
    class MyClass:
        def __new__(cls) -> "MyClass": ...
    
  • 提交代码(Commit):除非用户明确要求(遵循仓库 CLAUDE.md),否则切勿自行执行 git commit。文件修复完毕并通过检查后,停下来并将 diff 展示给用户进行 Review。