资讯动态

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

发布时间:2026/9/10 20:32:01 来源:尧图企业网站定制
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.pyistub仅声明x: inty: intImport 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.pyix: 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: intreveal_type是 ty 的测试内省原语其“返回值”出现在注释里由 mdtest 断言框架与检查器实际推导结果比对。此例验证的核心语义是Stub 声明即便无运行时实现足以支撑完整的类型推导。用例二从带声明与定义的普通模块导入第二组测试代码from b import x y x reveal_type(y) # revealed: int配套的实现文件b.pyx: int 1解读注解声明与初始值同时存在b.py中x: int 1同时携带两件事声明declarationx: int标注类型为int定义definitionx 1给出初始值。对类型检查器而言x的最终类型以注解为准int初始值1也必须与注解兼容。因此y x之后y的类型同样是int。这一用例与用例一形成对照覆盖了“声明与定义齐全的运行时模块”这一导入来源确保类型系统不会只在 Stub 场景下工作。两个用例背后的共性ty 的模块解析架构虽然关联文档只有两个用例但它们背后是一整套模块解析基础设施集中实现在 ty_module_resolver crate 中。理解这一层才能真正读懂“为什么b会解析到b.pyi而不是b.py”。模块表示Module 枚举module.rs 定义了模块的抽象pub enum Moduledb { File(FileModuledb), Namespace(NamespacePackagedb), }File(FileModule)对应磁盘上真实文件foo.py或foo.pyiFileModule内部记录了模块名、ModuleKind、搜索路径、文件句柄与解析环境Namespace(NamespacePackage)命名空间包横跨多个搜索路径、没有单一代码文件module.rs 的注释专门解释了这一点。ModuleKind则区分两种形态module.rspub 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 Runtimeresolve.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默认的检查模式优先选择 stub。from b import x若同时存在b.pyi与b.py解析结果将是b.pyi关联文档用例一只有b.pyi自然解析到 stub。Runtime即“goto definition”模式忽略 stub 直接找真实实现在查询搜索路径时还会用真实 stdlib 替换 typeshed。stub_file_to_real_moduleresolve.rs正是用Runtime模式从 stub 反查其运行时模块用于实现“跳转到定义”。解析流程的顶层入口是resolve_moduleresolve.rs它在常规搜索路径失败后会退到desperately_resolve_module“绝望解析”以导入文件所在目录的祖先目录作为临时搜索路径模拟 Python 运行时把脚本目录临时加入sys.path的行为。由于这一退化路径很少触发ty 把它独立成查询以便缓存命中的常规路径。搜索路径与导入解析顺序SearchPaths::from_settingsresolve.rs实现了 typing 规范中的导入解析顺序import resolution ordering其分层为extra paths额外路径用户手动配置、优先级最高、完全由用户控制的搜索层例如extra-paths [/stubs]src roots一/三方源码根项目自身的一/三方源码目录stdlib / typeshed标准库 stubty 内置于ruff_db的 vendored typeshed位于 ruff_db/src/vendored.rs支持通过配置替换为自定义 typeshedsite-packages安装的第三方包目录其中还包含对.pth文件中 editable 安装路径的探测site_packages_editables。同时SearchPaths维护了stdlib_pathtyping 模式使用的 typeshed与real_stdlib_pathRuntime 模式使用的真实 stdlib两份标准库路径二者在ModuleResolveMode不同时切换resolve.rs。Stub 包的专项支持除单文件 stub 外ty 还完整支持 PEP 561 的 stub-only 包foo-stubs/目录与 partial stub 包相关实现证据集中在StubPackageIndex与stub_package_indexresolve.rs索引可能包含-stubs顶层目录的搜索路径并保留它们相对于 stdlib 的解析顺序用于 stub 包的 overlay 解析search_path_may_contain_stub_packageresolve.rs扫描目录中是否存在以-stubs结尾的条目Module::is_type_check_onlymodule.rs判断模块是否为仅用于类型检查的捆绑 stub_typeshed、typing_extensions、ty_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的大致流程是切换到内存文件系统db.use_in_memory_system()以/src为项目根解析 Markdown 中所有内嵌代码块支持的语言为py、pyi、python、ipynb、toml及被跳过的ignore把它们写入内存文件系统——这正是stubs.md里b.py/b.pyi代码块会被真实落盘的原因以测试头部可选的[environment]/[configuration]TOML 块构建配置对每个测试文件运行类型检查attempt_test收集诊断由 crates/mdtest/src/matcher.rs 的匹配器将reveal_type/revealed:/error:等内联断言与实际诊断结果比对不一致即测试失败快照诊断输出到 snapshots 目录。stubs.md没有携带[environment]配置块意味着测试在默认环境下运行main文件位于项目根b.pyi/b.py与被检查文件同目录属于默认的一/三方搜索路径覆盖范围。借助import/stub_packages.md、import/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.rs中assert_matches!显式断言支持py/pyi/python/ipynb/tomllib.rs。stubs.md用到的pyi标签是 stub 文件的声明入口mdtest 解析器会据此将代码块按SourceType::Python与is_stub标记处理lib.rs。实战如何读懂与扩展这类用例阅读方法先看被检查文件通常命名为main.py或以文件名命名的代码块reveal_type(...)是你需要关注的目标# revealed: T是期望结果再看被导入模块的文件块与文件名——.pyi表示 stub 声明、.py表示真实实现若有# error: [code]注释则表示该行必须产生对应诊断例如 import/stub_packages.md 中的# error: [unresolved-import]最后看文件头部的[environment]/[configuration]块确认搜索路径、Python 版本等前提。扩展方法新增用例只需在resources/mdtest对应主题目录下新建/追加 Markdown 块用py/pyi/toml等标签声明文件用reveal_typerevealed:描述期望类型再运行 mdtest 套件即可自动验证与更新快照。新增文件的落盘根目录固定为/src因此测试内引用绝对路径如/packages、/.venv时可任意规划目录结构。小结Ruff 内置类型检查器 ty 的 stub 支持遵循“声明优先于实现”的类型检查原则关联文档 import/stubs.md 用两个对照用例锁定了两条基本事实从.pyistub 声明导入可推导出完整类型从带注解声明与定义的.py导入同样可推导出完整类型底层 ty_module_resolver 通过ModuleResolveMode::Typing优先选择 stub、按 typing 规范的导入解析顺序组织搜索路径并以resolve_module→desperately_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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价