ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ruff ty 类型检查器解析 `__import__` 与 `importlib.import_module` 动态导入的源码级原理

Ruff ty 类型检查器解析 `__import__` 与 `importlib.import_module` 动态导入的源码级原理 Ruff ty 类型检查器解析__import__与importlib.import_module动态导入的源码级原理【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读本文围绕 Ruff 仓库中 ty 类型检查器的 Markdown 测试文档 crates/ty_python_semantic/resources/mdtest/call/dunder_import.md系统讲解类型检查器如何识别__import__()与importlib.import_module()这两类动态导入调用并将它们解析为具体模块字面量类型module literal的完整规则与边界。读完本文你将掌握 ty 对动态导入的调用模式识别策略、兜底类型ModuleType的触发条件、联合类型Union与嵌套模块场景下的行为差异以及该能力在KnownFunction枚举中的底层实现位置与测试组织方式。背景为什么类型检查器需要特殊处理__import____import__()是 Python 内置的全局函数也是import语句在底层实际执行的入口其核心用途是实现动态导入——即在运行时根据字符串变量决定导入哪个模块。在静态类型检查层面动态导入的难点在于调用参数通常是普通字符串甚至运行时变量检查器无法像处理import sys这样的静态导入那样直接从 AST 中读取模块名。ty 类型检查器为此采取的策略是识别少数固定的调用模式把其中的字符串字面量参数当作模块名去模块解析器module resolver中查找并解析为字面量模块类型对于无法识别的调用模式或无法解析的模块名则统一兜底为通用的ModuleType。这一点正是 dunder_import.md 开头所概括的行为A few of its call patterns are recognized and resolved to literal module types instead of the generalModuleType, which is used as the fallback for unrecognized call patterns and unresolvable names.基本调用模式字面量模块名的解析位置参数与关键字参数ty 对__import__的第一种识别方式是参数必须是字符串字面量且支持位置参数与关键字参数两种写法二者等价reveal_type(__import__(sys)) # revealed: module sys reveal_type(__import__(nameshutil)) # revealed: module shutil在源码层面调用参数通过call_argument_node定义于 crates/ty_python_semantic/src/types/function.rs按名称与位置同时查找对__import__这类内置函数其第一个参数名是name因此find_argument(name, 0)既能命中位置参数sys也能命中关键字参数nameshutil之后通过as_string_literal()提取字符串字面量值见 function.rs。解析失败时的兜底ModuleType并非所有模块名都能解析成功下面这些情况会回落到通用的ModuleTypereveal_type(__import__(nonexistent)) # revealed: ModuleType reveal_type(__import__(collections.abc)) # revealed: ModuleType reveal_type(__import__(fnmatch, globals())) # revealed: ModuleType reveal_type(__import__(shelve, fromlist[])) # revealed: ModuleType逐一解读其背后的原因nonexistent模块解析器resolve_module见 crates/ty_python_semantic/src/semantic_model.rs在标准库与当前环境中找不到该模块返回None随即兜底collections.abc文档与源码注释明确指出——__import__(collections.abc)的返回值其实是顶层模块collections这是 CPython 中__import__与import_module的关键差异而 ty 目前还没有办法表示“返回顶层模块”这种类型因此在 function.rs 中对包含.的模块名直接提前返回落入ModuleType传入了globals()或fromlist[]等额外参数源码在提取第一个参数后检查rest.iter().any(Option::is_some)一旦发现除第一个参数外还有其他实参就放弃解析同样兜底为ModuleType。联合类型只有字面量能被解析文档特别强调被指定的模块名必须是字符串字面量如果传入的是运行时变量即使变量的联合类型收窄到了仅含两个可能的字符串值ty 也不会展开解析而是直接返回ModuleTypedef _(flag: bool): if flag: name sys else: name os reveal_type(name) # revealed: Literal[sys, os] reveal_type(__import__(name)) # revealed: ModuleType与之形成对比的是如果联合体现在调用表达式层面——即每次分支分别调用__import__并赋给同一个变量——那么结果会是两个模块字面量的联合if flag: module __import__(heapq) else: module __import__(curses) reveal_type(module) # revealed: module heapq | module curses这条规则体现了实现的关键约束as_string_literal()只接受单一的字面量字符串类型因此解析路径无法处理Literal[sys, os]这类联合参数而两个独立调用各自都能解析出模块字面量最终通过普通的联合类型归并机制自然得到module heapq | module curses。嵌套模块当前已知的局限对于点分模块名ty 明确标注了两个 TODO表示这是当前版本尚未完整实现的部分。文档以多文件结构展示了该场景main.py# TODO: Should be module a a reveal_type(__import__(a.b.c)) # revealed: ModuleType # TODO: Should be int, str, bytes reveal_type(a.a) # revealed: Any reveal_type(a.b.b) # revealed: Any reveal_type(a.b.c.c) # revealed: Anya/__init__.pya: int 1a/b/__init__.pyb: str a/b/c.pyc: bytes b可以看到两点现状由于module_name.contains(.)的检查function.rs__import__(a.b.c)当前返回ModuleType而期望的行为是返回顶层模块module a与 CPython 语义一致只是 ty 尚未实现对应的返回类型表示因此从返回值上取属性a.a、a.b.b、a.b.c.c都只能得到Any而不是各文件里标注的int、str、bytes。需要强调的是从代码结构看参考 function.rs 中的注释与 TODOimportlib.import_module在点分模块名场景下的行为与__import__不同它会返回子模块例如import_module(os.path)返回module os.path见下文。importlib.import_module()语义相似的兄弟函数importlib.import_module()与__import__行为相似但关键差异是返回被导入的子模块本身对点分名不截断到顶层模块import importlib reveal_type(importlib.import_module(bisect)) # revealed: module bisect reveal_type(importlib.import_module(os.path)) # revealed: module os.path reveal_type(importlib.import_module(nametempfile)) # revealed: module tempfile reveal_type(importlib.import_module(nonexistent)) # revealed: ModuleType reveal_type(importlib.import_module(config, logging)) # revealed: ModuleType在实现上import_module与__import__共享同一条识别路径二者在KnownFunction枚举中分别注册为DunderImport与ImportModulefunction.rs并在 function.rs 中进入同一个分支。区别体现在两处归属模块不同DunderImport要求函数来自内置模块builtinsmodule.is_builtins()见 function.rs而ImportModule要求来自importlibmodule.is_importlib()见 function.rs点分名处理不同__import__(collections.abc)应返回顶层collections模块ty 因无法表示而兜底ModuleTypeimportlib.import_module(os.path)返回的正是os.path子模块因此os.path能被正常解析为module os.path。importlib.import_module(config, logging)之所以返回ModuleType同样是因为除第一个参数外还传入了第二个位置参数触发了“多余参数”检查而放弃解析。底层实现位置与测试组织KnownFunction枚举与调用识别__import__和importlib.import_module的识别定义于KnownFunction枚举crates/ty_python_semantic/src/types/function.rs这是 ty 类型检查器中已知具有特殊行为的非穷尽函数列表Non-exhaustive enumeration of known functions与之并列的还有reveal_type、isinstance、namedtuple等DunderImport串行化为__import__归属于KnownModule::BuiltinsImportModule归属于KnownModule::ImportLib。实际的解析逻辑位于KnownFunction::infer_call分支中function.rs核心流程为提取第一个参数类型要求能as_string_literal()得到字符串字面量否则直接返回不设置返回类型保持默认行为检查其余参数是否全部为None任一存在则返回DunderImport且模块名含.时返回__import__语义下返回顶层模块ty 尚不支持用ModuleName::new(module_name)构造模块名并以ImportingFile::File(当前文件, 解析器环境)为上下文调用resolve_module解析解析成功则overload.set_return_type(Type::module_literal(db, ...))设置为模块字面量类型失败则保持兜底ModuleType。模块解析的底层入口resolve_module定义在 crates/ty_python_semantic/src/semantic_model.rs它负责把字符串模块名映射为Moduledb实体是动态导入解析与静态导入解析共用的解析通道。测试即文档mdtest 体系dunder_import.md 本身不是普通的说明文档而是 ty 的Markdown 测试mdtest用例文件中的reveal_type(...) # revealed: ...注释即断言由 crates/ty_python_semantic/mdtest.py 中的运行器执行逐条校验类型检查器的输出是否与注释一致。这种测试即文档的组织方式意味着文档中的每一行revealed都对应一个真实可运行的测试断言任何一条规则的行为变更都会导致对应断言失败文中的 TODO 注释如# TODO: Should be module a同时承担已知行为缺陷登记的功能后续修复时只需更新实现与注释即可闭环。同目录下还包含abstract_method.md、open.md、overloads.md、union.md等 其他调用模式测试共同覆盖KnownFunction枚举中各成员的特化行为。小结场景输入结果类型依据字面量模块名位置/关键字参数__import__(sys)、__import__(nameshutil)module sys、module shutilfunction.rs模块不存在__import__(nonexistent)ModuleType解析失败兜底点分名__import____import__(collections.abc)、__import__(a.b.c)ModuleType需返回顶层模块ty 暂不支持含 TODO额外参数__import__(fnmatch, globals())、import_module(config, logging)ModuleType非首个参数存在即放弃解析变量参数含字面量联合__import__(name)name:Literal[sys,os]ModuleTypeas_string_literal()不接受联合分支内的独立调用__import__(heapq)/__import__(curses)module heapq \| module curses各自解析后归并为联合类型子模块import_moduleimport_module(os.path)module os.pathImportModule返回子模块总体而言ty 对动态导入的处理遵循精确识别、兜底从宽的设计哲学只要调用模式清晰且模块名是字面量就尽力给出比ModuleType精确得多的模块字面量类型而在模式不清晰、参数不纯或模块无法解析时则安全地回落到ModuleType不做过度的推断。理解这套规则有助于你在动态导入场景下预判类型检查结果也能为阅读 dunder_import.md 及同目录其他 mdtest 用例提供清晰的背景框架。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表