Files
skillhub-062-pyrefly-type-c…/SKILL.md
T
2026-07-13 21:36:01 +08:00

8.9 KiB

name, description
name description
pyrefly-type-coverage 迁移文件以使用更严格的 Pyrefly 类型检查,要求所有函数、类和属性都带有注解。

Pyrefly 类型覆盖技能

前置条件

  • 文件必须位于包含 pyrefly.toml 的项目中。
  • pyreflylintrunner 以及项目的测试运行器必须位于 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-returnunannotated-parameterimplicit-any 错误——参见第 4 步的阶梯。这三个目标类别始终是可解决的;绝不使用 # pyrefly: ignore 来抑制它们。唯一的例外是 @compatibility(is_backward_compatible=True)(第 4 步)。

其他类别(bad-argument-typemissing-attribute 等)是真实的类型错误。根据 pyrefly 报告的位置来处理它们:

  • 在其他文件中报告(路径 != 目标):保持不动。不要扩大范围。如果该错误现在阻塞了目标文件,请在报告位置使用 # pyrefly: ignore[<category>] # TODO 进行抑制。
  • 在目标文件中报告,但消息引用的是其他地方定义的符号(例如,由于导入函数的注解错误导致的 bad-return):使用相同的 TODO 注释进行本地抑制。不要编造一个 cast() 来掩盖上游的缺口。
  • 在目标文件中报告,且源自本地:修复它。

仅作为最后手段使用 # pyrefly: ignore[...],且仅用于非目标类别。

第 4 步:添加注解

当从函数体无法判断正确的类型时,检查调用点。

注解约定

  • 使用 PEP 604 / PEP 585 语法(int | Nonelist[str])——假设 Python >= 3.10。
  • 对于抽象基类(CallableSequenceGenerator 等),优先使用 collections.abc 而非 typing
  • 对于泛型辅助工具,当项目的最低 Python 版本支持时从 typing 导入,仅在需要使用较新特性时才从 typing_extensions 导入(例如,如果支持低于 3.11/3.12 的版本,则使用 Selfoverride;或者 TypeVar / ParamSpec 的 PEP 696 default=)。不要无差别地从 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-returnunannotated-parameterimplicit-any 始终可以通过添加注解来解决;# pyrefly: ignore[<其中某一个>] 是不可接受的结果。唯一的例外是下面的向后兼容性特例。
  • 扩大类型,而非放弃。 当正确的类型难以推断时,请遵循以下阶梯,而不是使用 ignore:
    1. 从调用点和返回路径中可观察到的最具体的具体类型。
    2. 联合类型(X | Y)、Sequence[X] 风格的抽象类型,或者对于真正泛型函数(恒等传递、容器辅助函数)使用有界的 TypeVar
    3. object——仍然能通过类型检查的最严格的兜底类型。强制调用者在使用前进行窄化,例如 def serialize(value: object) -> str:。视觉上类似于 Any,但更严格——pyrefly 会在没有 isinstance 的情况下拒绝 value.foo()
    4. 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 放宽)。

  1. 向后兼容性检查。 仅在以下情况下运行: grep -l '@compatibility(is_backward_compatible=True)' <target> 返回该文件——该装饰器才是黄金文件的真正前置条件。更宽泛的「导入了 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]XSequencelist 在调用 .append 时等)。

备注

  • 类体中的前向引用在没有 from __future__ import annotations 时仍需要字符串引号:
    class MyClass:
        def __new__(cls) -> "MyClass": ...
    
  • 提交:除非用户明确要求,否则不要提交(根据仓库 CLAUDE.md)。当文件干净时,停下来展示差异以供审查。