mcpField
P
pyrefly-type-coverage

pyrefly-type-coverage

github.com/pytorch开发105.2k3.2k 次安装今天更新
代码仓库

关于 pyrefly-type-coverage

pyrefly-type-coverage 是用于提升 PyTorch Python 文件 Pyrefly 类型覆盖率的技能,提供配置、注解、Lint 和测试流程,并说明如何保留向后兼容的签名。

主要功能

  1. 01Pyrefly 配置指南
  2. 02移除文件级别的抑制项
  3. 03注解规范与类型收窄
  4. 04Lint 和测试流程
  5. 05处理向后兼容的函数签名

适用场景

  1. 01为 Python 模块添加类型注解
  2. 02解决 Pyrefly 的隐式 Any 和缺少注解错误
  3. 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:
    1. 从调用点和返回路径能观察到的最具体类型。
    2. 联合类型(X | Y)、Sequence[X] 风格的抽象类型,或适用于真正泛型函数(恒等直通、容器辅助函数)的有界 TypeVar。
    3. object——仍能通过类型检查的最严格回退方案。它会强制调用方在使用前先缩窄类型,例如 def serialize(value: object) -> str:。视觉上与 Any 相似,但限制更严格——未使用 isinstance 时,pyrefly 会拒绝 value.foo()。
    4. 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 扩宽),而不是还原文件。

  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 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)。文件检查通过后,请停止操作并提供差异供审查。

评论

支持 Markdown