ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ruff ty 类型检查器中的泛型内建类型:未绑定继承方法与可变关键字参数的类型推断

Ruff ty 类型检查器中的泛型内建类型:未绑定继承方法与可变关键字参数的类型推断 Ruff ty 类型检查器中的泛型内建类型未绑定继承方法与可变关键字参数的类型推断【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff泛型内建类型generic builtins是类型推断中既基础又容易出错的环节。本文以 Ruff 类型检查器 ty 的测试文档 crates/ty_python_semantic/resources/mdtest/generics/builtins.md 为主体讲解两类核心场景list、dict等泛型内建类型通过继承获得的未绑定方法如list.clear为何无需提供类型参数即可调用以及自定义 typeshed 中dict若不按泛型类声明会导致**kwargs被推断为dict[Unknown, Unknown, Unknown]的惊讶结果。读完本文你将掌握 mdtest 测试格式、reveal_type断言语法以及泛型内建类型声明对推断结果的直接影响。一、背景mdtest 与 Generic builtins 测试文档1.1 mdtest以 Markdown 为载体的类型推断测试在 Ruff 仓库中ty 类型检查器的类型推断与类型检查测试大量使用mdtest格式Markdown 文件中的代码块py、pyi、ipynb、toml被解析为可执行的测试用例行内注释如# revealed:、# error:作为断言与类型检查器输出的诊断结果逐行比对。仓库中 crates/ty_python_semantic/resources/README.md 明确说明Markdown files within themdtest/subdirectory are tests of type inference and type checking; executed by thetests/mdtest.rsintegration test.即crates/ty_python_semantic/resources/mdtest/目录下的所有 Markdown 文件都是类型推断与类型检查的测试由crates/ty_python_semantic/tests/mdtest.rs集成测试执行。这些测试会被打包进 crate 的resources/mdtest/目录随测试运行加载。从 crates/mdtest/src/parser.rs 的解析逻辑可以进一步确认测试格式的细节语言为toml且没有显式文件路径的代码块被视作该测试用例的配置块process_config_block内容以 TOML 反序列化后存入当前节Sectionpy/python/pyi/ipynb代码块作为可检查的嵌入式文件checkable embedded file文件名会自动生成如mdtest_snippet.py、mdtest_snippet.pyi代码块支持显式路径语法例如/typeshed/stdlib/builtins.pyi:前缀用于创建指定路径的测试文件。1.2 本测试文档的位置与主题本文要讲解的 builtins.md 位于generics/目录下与 scoping.md、set_theoretic.md 以及pep695/、legacy/两个子目录共同构成泛型generics主题的测试矩阵。文档标题为 Generic builtins聚焦两个具体场景下面逐一展开。二、未绑定的继承方法list.clear 与 dict.clear2.1 原文档示例原文档的第一节 Unbound inherited methods 给出了如下示例def clear_containers(items: list[int], mapping: dict[str, int]) - None: list.clear(items) dict.clear(mapping)该代码块没有任何断言注释因此这是一个应当通过类型检查而不产生任何诊断的用例。它验证的核心行为是在 typeshed 中list从MutableSequence继承clear方法dict从MutableMapping继承clear方法我们可以直接通过list和dict调用这些继承来的方法而无需提供类型参数即不必写成list[int].clear(items)之类。2.2 为什么未绑定是个值得专门测试的点这里的未绑定有两层含义方法未绑定到实例list.clear(items)是直接通过类而不是实例调用属于未绑定方法调用需要显式传入第一个参数即被操作的容器本身。items: list[int]作为实参传入后类型检查器需要把list的泛型参数与items的类型对齐。类型参数未显式指定MutableSequence本身是泛型通常带有Self或元素类型参数clear方法的签名中可能包含对Self的引用。调用list.clear(items)时类型检查器必须正确解析这些泛型方法签名中出现的未绑定类型变量unbound typevar在不提供任何类型参数的情况下完成推断。这一行为在实现层面并非理所当然。从 crates/ty_python_semantic/src/types/generics.rs 的源码结构可以看到ty 对类型变量的绑定与解析有一套专门的逻辑resolve_unbound_typevar约 generics.rs 第 74 行负责绑定一个未绑定的类型变量——当表达式解析到一个类型变量而它尚未被任何包围作用域绑定时会进入该路径处理is_visible_across_class_boundary约 generics.rs 第 148 行判断一个绑定在跨越嵌套类边界后是否仍然可见这与从基类继承来的泛型上下文密切相关with_inherited_generic_context约 generics.rs 第 731 行将函数签名与继承自基类的泛型上下文合并。可以推断list.clear(items)之所以能通过检查正是依赖上述继承泛型上下文的解析机制clear的签名来自基类MutableSequence其Self类型变量在调用点通过items: list[int]被正确绑定到具体的list[int]因此无需显式类型参数。2.3 与 PEP 695 / legacy 泛型声明的对照同类目录下的 generics/pep695/classes.md 与generics/legacy/classes.md覆盖了用户自定义泛型类的继承场景其中反复出现通过继承另一个泛型类并使泛型参数部分或全部特化的用例以及# revealed:断言。与本节内容合在一起看可以形成一条完整的脉络无论是内建类型的基类MutableSequence、MutableMapping还是用户自定义泛型类的基类类型检查器都必须维护好继承而来的泛型上下文才能在方法调用时不要求调用方重复提供类型参数。三、可变关键字参数与自定义 dict 的泛型声明3.1 问题背景原文档的第二节 Variadic keyword arguments with a customdict 讨论了一个容易被忽略的坑当你在自定义 typeshed 中定义dict时必须像真实 typeshed 一样把它定义为泛型类否则可变关键字参数**kwargs的类型推断结果会出人意料。3.2 测试环境配置该测试用例首先通过 TOML 配置块指定使用自定义 typeshed 作为环境[environment] typeshed /typeshed这意味着类型检查器在该测试中将读取位于/typeshed的自定义类型存根而不是内置的 vendored typeshed。这与真实项目中使用自定义 stubs 的场景例如为私有类型或自定义运行时编写存根一致。3.3 自定义 typeshed 中的泛型 dict测试随后通过显式路径语法声明了两个存根文件。/typeshed/stdlib/builtins.pyiclass object: ... class int: ... class tuple: ... class dict[K, V, Extra]: .../typeshed/stdlib/typing_extensions.pyidef reveal_type(obj, /): ...注意这里的dict被声明为三个类型参数的泛型类dict[K, V, Extra]即键类型K、值类型V以及一个额外的类型参数Extra。这正是与真实 typeshed 保持一致的关键——真实的dict在 typeshed 中同样是带多个类型参数的泛型类而**kwargs在类型检查器内部正是通过dict类型来表达的其类型参数会直接反映在推断结果中。从 crates/mdtest/src/parser.rs 的解析器实现看显式路径语法在代码块上方用路径:标记正是为了在测试中构造这种多文件、自定义路径的虚拟环境而reveal_type作为测试辅助函数在自定义的typing_extensions.pyi中声明说明 mdtest 允许测试自定义辅助 API。3.4 错误声明会带来什么后果原文档紧接着给出了对照性的断言如果dict没有按泛型类声明例如被定义为普通非泛型类那么推断**kwargs的类型时类型检查器无法获知键、值以及额外维度分别对应什么类型结果将是def f(**kwargs): reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown] def g(**kwargs: int): reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown]这里有三个值得注意的细节# revealed:是行内断言reveal_type(kwargs)的推断结果被以注释形式写在下一行格式为# revealed: 推断出的类型。mdtest 运行时会将其与类型检查器实际输出的reveal_type结果比对不一致即测试失败。这与本仓库中其他 mdtest 用例的断言风格完全一致参见 generics/pep695/classes.md 中的# revealed: ty_extensions._internal.GenericContext[...]等写法。即使标注了**kwargs: int也是Unknown第二个函数g显式声明了关键字参数的值类型为int但推断结果依然是dict[Unknown, Unknown, Unknown]。这说明当dict不是正确声明的泛型类时类型检查器对**kwargs的整体类型表达都退化为Unknown——声明值类型并不能补救dict本身缺少类型参数的问题。这正是原文档所说的surprising results。dict[K, V, Extra]与dict[Unknown, Unknown, Unknown]一一对应对比 3.3 节的自定义声明可以看到三个类型参数恰好对应推断结果中的三个槽位。可以推断ty 在推断**kwargs时会将收集到的字典表示为dict类型的实例并分别填充键类型、值类型与额外维度的类型参数只要dict不是泛型类这三个槽位就无法被实例化只能以Unknown兜底。3.5 从源码看 kwargs 的推断路径在 crates/ty_python_semantic/src/types/generics.rs 以及types/目录如 types.rs、types/instance.rs中存在大量围绕类型变量绑定、泛型实例化与reveal_type相关的实现且多处测试用例覆盖**kwargs场景在src/types/infer/、src/types/function.rs等文件中都能检索到相关断言。结合本节文档可以确认一条实现层面的原则**kwargs的类型推断依赖dict作为泛型类的正确声明类型参数的数量与含义直接决定推断结果的形状。这也解释了为什么文档要专门强调必须以与真实 typeshed 相同的方式将其定义为泛型类。四、如何在本地运行这些 mdtest 用例4.1 通过 cargo 运行该测试套件由ty_python_semanticcrate 的集成测试承载标准运行方式是cargo test -p ty_python_semantic --test mdtest当某个测试失败时crates/mdtest/src/lib.rs 中的run函数会在失败信息末尾给出精确重跑该用例的命令其格式为MDTEST_TEST_FILTER完整测试名 cargo test -p ty_python_semantic --test mdtest -- mdtest其中MDTEST_TEST_FILTER是 crates/mdtest/src/lib.rs 中定义的环境变量第 23 行测试名由 Markdown 标题层级拼接而成如Generic builtins - Unbound inherited methods。4.2 常用环境变量crates/mdtest/src/lib.rs 定义了三个与运行行为相关的环境变量环境变量作用MDTEST_TEST_FILTER只运行名称包含该过滤串的测试MDTEST_UPDATE_SNAPSHOTS设为非0时自动更新行内快照snapshot代码块内容MDTEST_GITHUB_ANNOTATIONS_FORMAT设置后以 GitHub Actions annotation 格式输出错误信息4.3 使用 mdtest.py 进入监听模式仓库还提供了 Python 脚本 crates/ty_python_semantic/mdtest.py它以 watch 模式持续监听 Rust 源码、vendored typeshed 与resources/mdtest/目录的变化改动后自动重新编译并运行相关测试。其命令行参数包括位置参数filters部分路径过滤例如generics/builtins.md或builtins.md脚本内部会自动去掉.md后缀匹配--enable-external/-e启用依赖外部资源的测试--no-lockfile-upgrades禁止在 Markdown 测试的依赖要求变化时自动升级 lockfile--no-snapshot-updates禁止自动更新过期的行内快照。默认情况下过期的snapshot代码块会被自动更新lockfile 也会在依赖变化时升级因此开发测试用例时通常无需手动维护这两类文件。五、延伸泛型测试矩阵与阅读建议generics/目录下除本文档外还按语法风格分为两个子目录构成完整的泛型能力测试矩阵generics/pep695/PEP 695 语法class C[T]、def f[T]等内含 classes.md、functions.md、typevartuple.md、paramspec.md 等generics/legacy/PEP 484 传统语法TypeVar、Generic[T]等同样覆盖 classes、functions、typevartuple、paramspec、variance 等主题。对泛型内建类型感兴趣的读者建议按以下顺序阅读本文档builtins.md先理解内建类型继承方法与dict泛型声明的语义pep695/classes.md理解用户自定义泛型类中继承未特化/部分特化的基类如何影响泛型上下文legacy/目录下对应文件对照传统Generic[T]语法下相同的语义是否保持一致。这三者共同回答了同一个问题当一个方法或类型源自泛型基类时调用方需要或不需要显式提供多少类型信息——而本文档正是从内建类型这个最常用的切面出发把答案固定成了可自动验证的测试用例。结语通过 builtins.md 这份测试文档我们看到了 ty 类型检查器在泛型内建类型上的两个明确契约其一list.clear、dict.clear这类继承自泛型基类MutableSequence、MutableMapping的方法调用时无需提供类型参数类型检查器会依据实参自动完成类型变量的绑定其二dict必须在 typeshed 中保持泛型类声明dict[K, V, Extra]否则**kwargs的推断结果会退化为dict[Unknown, Unknown, Unknown]。这两条契约都通过 mdtest 以可复现、可回归的方式固化在仓库中既服务了类型检查器自身的正确性也为使用者编写自定义 typeshed 时提供了直接的实践参照——定义内建类型存根时泛型参数的声明方式会真实地改变类型推断的输出。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进