news 2026/9/10 20:31:58

Ruff 的 ty 类型检查器如何解析 `.pyi` Stub 文件:模块解析、PEP 561 与 mdtest 验证全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ruff 的 ty 类型检查器如何解析 `.pyi` Stub 文件:模块解析、PEP 561 与 mdtest 验证全解

Ruff 的 ty 类型检查器如何解析.pyiStub 文件:模块解析、PEP 561 与 mdtest 验证全解

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

本指南以 Ruff 仓库内置类型检查器 ty 的测试夹具 import/stubs.md 为核心,系统讲解类型检查器在“声明文件(Stub)优先”原则下如何从.pyi.py文件导入符号、推断类型,并揭示背后的模块解析架构(ty_module_resolver)与测试驱动(mdtest)机制。读完本文,你将掌握 stub 解析的完整链路、reveal_type断言的含义,以及如何读懂并扩展 ty 的类型检查测试用例。

关联文档说明:一份“最小可运行”的解析规范夹具

Ruff 仓库(基于 Rust 实现,包含 lint 工具 ruff 与类型检查器 ty)中,类型检查器的行为由大量 Markdown 测试夹具约束。import/stubs.md 是其中一份高度凝练的文档:它只包含两个测试用例,每个用例由一个「待检查的 Python 文件 + 一个被导入模块的源码」构成,并以内联注释revealed:声明类型检查器必须推导出的类型。这份文档与其说是“说明文档”,不如说是 ty 的可执行行为规范——凡是通过 mdtest 跑出的reveal_type结果与文档注释不一致,测试即失败。

两个用例的骨架如下:

用例待检查文件被导入模块期望推导结果
Import from stub declarationmain(隐式)b.pyi(stub,仅声明x: inty: int
Import from non-stub with declaration and definitionmain(隐式)b.py(实现文件,x: int = 1y: int

两个用例的最终推导类型都是int,但被导入模块的存在形式截然不同:前者只有声明(declaration),后者同时具有声明与定义(definition)。这正是类型检查器处理“声明与定义分离”的核心场景。

用例一:从 Stub 声明中导入

文档给出了第一组测试代码:

from b import x y = x reveal_type(y) # revealed: int

配套的 stub 文件b.pyi

x: int

解读:什么是 Stub 文件

.pyi后缀文件即 PEP 484 定义的 Stub(类型存根)文件。它只携带类型信息,不包含任何可执行实现,是类型检查器优先使用的“声明层”。本例中b.pyi仅声明x: int,没有给x赋值——这正是 Stub 的典型形态:x: int是纯粹的注解声明(declaration),运行时不产生任何真实对象。

ty 在类型检查(Typing)模式下会把.pyi置于普通.py之前优先解析(详见 resolve.rs 中ModuleResolveMode::Typing的注释:“type checkers are in fact supposed topreferstubs over the actual implementations”)。因此from b import x中的b会解析到b.pyix的类型即注解声明的int

声明与定义的分离

Stub 中的x: int只有声明没有定义,但类型检查不要求“值必须存在”——检查器关心的是类型形状。y = x只是把int绑定到新名字y,所以:

reveal_type(y) # revealed: int

reveal_type是 ty 的测试内省原语,其“返回值”出现在注释里,由 mdtest 断言框架与检查器实际推导结果比对。此例验证的核心语义是:Stub 声明(即便无运行时实现)足以支撑完整的类型推导

用例二:从带声明与定义的普通模块导入

第二组测试代码:

from b import x y = x reveal_type(y) # revealed: int

配套的实现文件b.py

x: int = 1

解读:注解声明与初始值同时存在

b.pyx: int = 1同时携带两件事:

  • 声明(declaration):x: int,标注类型为int
  • 定义(definition):x = 1,给出初始值。

对类型检查器而言,x的最终类型以注解为准(int),初始值1也必须与注解兼容。因此y = x之后y的类型同样是int。这一用例与用例一形成对照,覆盖了“声明与定义齐全的运行时模块”这一导入来源,确保类型系统不会只在 Stub 场景下工作。

两个用例背后的共性:ty 的模块解析架构

虽然关联文档只有两个用例,但它们背后是一整套模块解析基础设施,集中实现在 ty_module_resolver crate 中。理解这一层,才能真正读懂“为什么b会解析到b.pyi而不是b.py”。

模块表示:Module 枚举

module.rs 定义了模块的抽象:

pub enum Module<'db> { File(FileModule<'db>), Namespace(NamespacePackage<'db>), }
  • File(FileModule):对应磁盘上真实文件(foo.pyfoo.pyi),FileModule内部记录了模块名、ModuleKind、搜索路径、文件句柄与解析环境;
  • Namespace(NamespacePackage):命名空间包,横跨多个搜索路径、没有单一代码文件(module.rs 的注释专门解释了这一点)。

ModuleKind则区分两种形态(module.rs):

pub enum ModuleKind { /// A single-file module (e.g. `foo.py` or `foo.pyi`) Module, /// A python package (`foo/__init__.py` or `foo/__init__.pyi`) Package, }

从代码结构可以推断:b.pyi这种单文件 stub 对应ModuleKind::Module;若 stub 以包形式存在(b/__init__.pyi),则对应ModuleKind::Package。二者在类型检查中的行为一致,区别仅在于解析路径的形态。

解析模式:Typing vs Runtime

resolve.rs 中的ModuleResolveMode是理解“stub 优先”的关键:

pub enum ModuleResolveMode { /// Resolve modules for type checking, preferring stubs over runtime implementations. Typing, /// Resolve modules to their runtime implementations without considering stubs. Runtime, /// Like Runtime, but permits some modules to be shadowed. RuntimeSomeShadowingAllowed, }
  • Typing:默认的检查模式,优先选择 stubfrom b import x若同时存在b.pyib.py,解析结果将是b.pyi;关联文档用例一只有b.pyi,自然解析到 stub。
  • Runtime:即“goto definition”模式,忽略 stub 直接找真实实现,在查询搜索路径时还会用真实 stdlib 替换 typeshed。stub_file_to_real_module(resolve.rs)正是用Runtime模式从 stub 反查其运行时模块,用于实现“跳转到定义”。

解析流程的顶层入口是resolve_module(resolve.rs),它在常规搜索路径失败后会退到desperately_resolve_module(“绝望解析”):以导入文件所在目录的祖先目录作为临时搜索路径,模拟 Python 运行时把脚本目录临时加入sys.path的行为。由于这一退化路径很少触发,ty 把它独立成查询以便缓存命中的常规路径。

搜索路径与导入解析顺序

SearchPaths::from_settings(resolve.rs)实现了 typing 规范中的导入解析顺序(import resolution ordering),其分层为:

  1. extra paths(额外路径):用户手动配置、优先级最高、完全由用户控制的搜索层,例如extra-paths = ["/stubs"]
  2. src roots(一/三方源码根):项目自身的一/三方源码目录;
  3. stdlib / typeshed:标准库 stub(ty 内置于ruff_db的 vendored typeshed,位于 ruff_db/src/vendored.rs),支持通过配置替换为自定义 typeshed;
  4. site-packages:安装的第三方包目录,其中还包含对.pth文件中 editable 安装路径的探测(site_packages_editables)。

同时SearchPaths维护了stdlib_path(typing 模式使用的 typeshed)与real_stdlib_path(Runtime 模式使用的真实 stdlib)两份标准库路径,二者在ModuleResolveMode不同时切换(resolve.rs)。

Stub 包的专项支持

除单文件 stub 外,ty 还完整支持 PEP 561 的 stub-only 包(foo-stubs/目录)与 partial stub 包,相关实现证据集中在:

  • StubPackageIndexstub_package_index(resolve.rs):索引可能包含-stubs顶层目录的搜索路径,并保留它们相对于 stdlib 的解析顺序,用于 stub 包的 overlay 解析;
  • search_path_may_contain_stub_package(resolve.rs):扫描目录中是否存在以-stubs结尾的条目;
  • Module::is_type_check_only(module.rs):判断模块是否为仅用于类型检查的捆绑 stub(_typeshedtyping_extensionsty_extensions)。

ty 在 user-controlled 的 extra-path 层内给予foo-stubsstub 包对普通foo包的优先权(无论搜索路径先后),且 namespace stub 包总是被视为 partial,普通 stub 包仅当py.typed内含partial标记时视为 partial——这些行为细节可见于 import/stub_packages.md 与 import/partial_stub_packages.md 这两份同目录测试夹具(后者也是关联文档stubs.md的姊妹篇,解释了 partial stub 的 fall-through 合并语义:stub 缺失的模块会回落到底层实现包继续查找)。

这些用例如何被验证:mdtest 测试框架

关联文档中的reveal_type# revealed: int注释并非普通注释,而是 mdtest 框架的断言语法。

mdtest 的目录组织

mdtest 测试夹具位于 crates/ty_python_semantic/resources/mdtest,按主题分子目录(import/class/narrow/generics/等),每个.md文件即一组相关用例;mdtest/snapshots 目录存放相应运行快照。

运行机制

执行器位于 crates/ruff_mdtest/src/lib.rs,其run_test函数(lib.rs)的大致流程是:

  1. 切换到内存文件系统(db.use_in_memory_system()),以/src为项目根;
  2. 解析 Markdown 中所有内嵌代码块(支持的语言为pypyipythonipynbtoml及被跳过的ignore),把它们写入内存文件系统——这正是stubs.mdb.py/b.pyi代码块会被真实落盘的原因;
  3. 以测试头部可选的[environment]/[configuration]TOML 块构建配置;
  4. 对每个测试文件运行类型检查(attempt_test),收集诊断;
  5. 由 crates/mdtest/src/matcher.rs 的匹配器将reveal_type/revealed:/error:等内联断言与实际诊断结果比对,不一致即测试失败;
  6. 快照诊断输出到 snapshots 目录。

stubs.md没有携带[environment]配置块,意味着测试在默认环境下运行:main文件位于项目根,b.pyi/b.py与被检查文件同目录,属于默认的一/三方搜索路径覆盖范围。借助import/stub_packages.mdimport/partial_stub_packages.md中的写法可以看到,一旦涉及自定义路径,用例会通过如下 TOML 声明环境:

[environment] extra-paths = ["/packages"]

这也解释了关联文档为何如此精简——它刻意剥离了环境配置,聚焦于“stub 声明”与“实现模块”两种导入来源的类型推导等价性。

测试语言支持与 fixture 解析

同目录其他测试文件展示了 mdtest 支持的全部语言标签,例如partial_stub_packages.md中的py.typed文件使用text块、stub_packages.md中 editable 安装使用pth块,而lib.rsassert_matches!显式断言支持py/pyi/python/ipynb/toml(lib.rs)。stubs.md用到的pyi标签是 stub 文件的声明入口,mdtest 解析器会据此将代码块按SourceType::Pythonis_stub标记处理(lib.rs)。

实战:如何读懂与扩展这类用例

阅读方法

  1. 先看被检查文件(通常命名为main.py或以文件名命名的代码块),reveal_type(...)是你需要关注的目标,# revealed: T是期望结果;
  2. 再看被导入模块的文件块与文件名——.pyi表示 stub 声明、.py表示真实实现;
  3. 若有# error: [code]注释,则表示该行必须产生对应诊断(例如 import/stub_packages.md 中的# error: [unresolved-import]);
  4. 最后看文件头部的[environment]/[configuration]块,确认搜索路径、Python 版本等前提。

扩展方法

新增用例只需在resources/mdtest对应主题目录下新建/追加 Markdown 块,用py/pyi/toml等标签声明文件,用reveal_type+revealed:描述期望类型,再运行 mdtest 套件即可自动验证与更新快照。新增文件的落盘根目录固定为/src,因此测试内引用绝对路径(如/packages/.venv)时可任意规划目录结构。

小结

Ruff 内置类型检查器 ty 的 stub 支持遵循“声明优先于实现”的类型检查原则:

  • 关联文档 import/stubs.md 用两个对照用例锁定了两条基本事实:.pyistub 声明导入可推导出完整类型;从带注解声明与定义的.py导入同样可推导出完整类型
  • 底层 ty_module_resolver 通过ModuleResolveMode::Typing优先选择 stub、按 typing 规范的导入解析顺序组织搜索路径,并以resolve_moduledesperately_resolve_module的二级解析兜底;
  • 这些行为通过 ruff_mdtest 以 Markdown 内联断言的形式固化,保证类型检查器对 stub、实现、stub-only 包、partial stub 包等各种形态的导入语义始终如一。

如果你想进一步深入,可以依次阅读 crates/ty_module_resolver/src/resolve.rs、crates/ty_module_resolver/src/module.rs,再对照 import/stub_packages.md、import/partial_stub_packages.md 与 crates/ruff_mdtest/src/lib.rs 逐步验证。

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 20:31:43

2026年10款硬核降AIGC工具推荐:AIGC检测轻松拿捏

随着知网、维普、万方等主流学术平台对AIGC检测标准不断收紧&#xff0c;论文通过率面临严峻挑战。如何有效降低AI痕迹与查重率&#xff0c;成为众多学者和学生的共同难题。本文将实测对比10款主流降AI工具&#xff0c;助你精准选择最适合的解决方案。为什么需要降 AI 率工具&a…

作者头像 李华
网站建设 2026/9/10 20:30:21

堆场布局调整的毫秒级迭代:动态增量重建如何让数字港口弹性适配 技术白皮书

1 概述1.1 技术背景智慧港口、自动化码头的核心竞争力&#xff0c;源于堆场空间调度、设备协同、箱位排布的动态适配能力。随着集装箱吞吐量持续攀升、船舶大型化迭代、内外贸航线高频切换、江海联运业务密集叠加&#xff0c;港口堆场呈现箱态动态杂乱、设备密集交织、任务瞬时…

作者头像 李华
网站建设 2026/9/10 20:28:52

三星M393A DDR4服务器内存实战指南:原理、选型与避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 20:27:30

MemTest86内存检测工具使用全指南

1. 为什么我们需要专业的内存测试工具刚装好的新电脑频繁蓝屏&#xff1f;游戏打到一半突然卡死&#xff1f;这些看似随机的系统不稳定现象&#xff0c;很可能就是内存条在作祟。作为计算机系统中负责临时数据存储的关键部件&#xff0c;内存的健康状况直接影响着整机稳定性。不…

作者头像 李华
网站建设 2026/9/10 20:26:58

<Skill title>

【免费下载链接】oh-my-codex OmX - Oh My codeX: Your codex is not alone. Add hooks, agent teams, HUDs, and so much more. 项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-codex Purpose What durable, codebase-specific outcome does this skill pro…

作者头像 李华