ty 类型检查器 `allowed-unresolved-imports` 配置完全指南:用模块 Glob 通配符精确控制未解析导入诊断
allowed-unresolved-imports 是 ruff 仓库中 ty 类型检查器(位于 crates/ty_python_semantic)提供的一项 analysis 配置项,用于按模块名模式抑制 unresolved-import 诊断。本指南以仓库内的可执行文档测试 allowed_unresolved_imports.md 为核心骨架,结合 ty_module_resolver 的底层 Glob 编译实现与导入推断源码,系统讲解字面量、*、**、否定模式(!)的完整匹配语义,帮助你精准豁免无法解析的第三方或动态生成模块,同时保留其余导入的严格检查。
读完本文,你将掌握:在 pyproject.toml / ty.toml 中正确配置该选项、理解"最后匹配优先"的 gitignore 式覆盖规则、区分 * 与 ** 的边界行为,并能够组合出"允许全部但排除特定模块"等进阶策略。
一、为什么需要 allowed-unresolved-imports
ty 在无法解析导入的模块时,会默认报告 unresolved-import 诊断(源码中对应 UNRESOLVED_IMPORT lint,见 imports.rs)。但在真实项目中,存在大量"明知解析不到却必须导入"的合法场景:
- 尚未安装的运行时依赖或可选的动态扩展模块;
- 依赖注入或插件体系下按约定生成的模块;
- 仅在特定平台/版本存在的条件导入;
- 与 C 扩展绑定或原生扩展对应的包。
逐个写 # type: ignore 既繁琐又难以维护。ty 为此提供了 analysis.allowed-unresolved-imports:只要模块名匹配配置中的某个 Glob 模式,ty 就不再对相关导入发出 unresolved-import 诊断。
从源码结构看,该选项最终被编译为一个 ModuleGlobSet,存放在 AnalysisSettings.allowed_unresolved_imports 字段中(默认空集,见 lib.rs),并在导入推断的三个关键路径上做放行判断:
- 顶层
import/from ... import语句(report_unresolved_import入口,见 imports.rs); - 相对导入解析失败时(见 imports.rs);
from pkg import member中子模块pkg.member无法解析时(见 imports.rs)。
三处统一使用 .matches(module_name).is_include() 判断:返回 Include 则直接 return,跳过诊断上报。这意味着该配置不仅豁免"整模块未解析",也会抑制"模块存在但成员不存在"的诊断(下文详述)。
二、在哪里配置
allowed-unresolved-imports 位于 [analysis] 配置组,类型为 list[str],默认值为 [](空列表,即不豁免任何模块)。既可以在 pyproject.toml 的 [tool.ty.analysis] 段配置,也可以使用独立的 ty.toml 文件配置(参考 configuration.md):
# pyproject.toml
[tool.ty.analysis]
allowed-unresolved-imports = ["foo"]
# ty.toml
[analysis]
allowed-unresolved-imports = ["foo"]
此外它同样支持通过 [tool.ty.overrides.analysis](ty.toml 中为 [overrides.analysis])按文件路径覆盖,这在处理子项目差异配置时非常有用(见 configuration.md)。
提示:
replace-imports-with-any是与它相邻的另一项配置。二者都会抑制导入诊断,但语义不同——replace-imports-with-any会把匹配模块的类型替换为typing.Any,即使模块实际上能解析也会替换;而allowed-unresolved-imports只负责放行未解析导入,模块类型保持Unknown。从源码可见,在 report_unresolved_import 中二者是"或"的关系,任一命中即不报错。
三、字面量匹配:只豁免精确模块名
最简单的用法是直接写模块名。配置 ["foo"] 后:
from foo import bar
import foo
reveal_type(foo) # revealed: Unknown
reveal_type(bar) # revealed: Unknown
字面量模式只精确匹配该模块名本身,模块被豁免后其类型仍是 Unknown(这一点与 replace-imports-with-any 不同:后者在可解析时也会把类型替换为 Any)。字面子串不会匹配到它的子模块——以下写法依然会触发 unresolved-import:
from foo.sub import bar # error: [unresolved-import]
import foo.sub.bar # error: [unresolved-import]
如果你希望连同子模块一起豁免,就需要使用 ** 通配符。从 module_glob.rs 的文档注释可知,test 模式只匹配模块 test 本身(不匹配 test.foo),这与文档测试的结论完全一致。
四、** 通配符:按模块组件匹配
** 匹配零个到任意多个模块组件(即用 . 分隔的段)。注意:** 必须作为一个完整的独立组件出现——foo**、**foo、foo.bar** 这类与其它文本混写的模式是非法的,会在解析阶段直接报 InvalidDoubleStarUsage 错误(见 module_glob.rs 及对应单元测试)。
4.1 结尾的 **:匹配以某前缀开头的整个子树
foo.** 匹配所有以 foo 开头的模块名,包括 foo 本身、foo.bar、foo.bar.baz 等任意深度:
[analysis]
allowed-unresolved-imports = ["foo.**"]
from foo import bar
from foo.sub import bar2
from foo.sub.baz import bar3
import foo.baz
但前提是模块的第一个组件必须是 foo。下面这个导入虽然含有名为 foo 的组件,但首组件是 bar,因此仍会报错:
from bar import foo # error: [unresolved-import]
4.2 开头的 **:匹配以某后缀结尾的任意模块
**.foo 匹配所有以 foo 结尾的模块名,包括 foo、bar.foo、baz.bar.foo:
[analysis]
allowed-unresolved-imports = ["**.foo"]
from foo import bar
from bar.foo import baz
from baz.bar.foo import qux
import bar.foo
同样地,它要求模块的最后一个组件必须是 foo。下面的 foo.bar.foo 以 foo 结尾可以放行,但 main.py 中这个导入的模块以 bar 结尾(foo 只是中间组件),依然报错:
main.py:
from foo.bar import foo # error: [unresolved-import]
4.3 中间的 **:首尾固定的任意深度
foo.**.bar 匹配"首组件为 foo、末组件为 bar"的任意模块名,包括 foo.bar、foo.bar.baz.bar 等:
[analysis]
allowed-unresolved-imports = ["foo.**.bar"]
from foo.bar import baz
from foo.bar.baz.bar import qux
import foo.bar.baz.bar
五、* 通配符:按字符匹配,但不跨 .
* 匹配零个或多个字符,但不能匹配 .。换言之,一个 * 只能在一个模块组件内部"消化"字符,无法跨越组件边界。
[analysis]
allowed-unresolved-imports = ["test*.foo"]
from test.foo import bar
from testing.foo import baz
test*.foo 中 test* 可以匹配 test 或 testing,因此上面的两个导入都被放行。而下面这个导入中,test.ing.foo 的第一组件是 test、第二组件是 ing——模式要求 test* 之后紧跟 .foo,中间隔了一个组件,* 无法跨过 .,于是报错:
import test.ing.foo # error: [unresolved-import]
从 module_glob.rs 的 glob_to_regex 实现可以看到,* 被翻译为正则 [^.]*,即"非点号字符重复零次或多次";而完整组件 *(独立成段)则翻译为 [^.]+,匹配恰好一个非空组件。这从实现层面解释了 * 不跨 . 的原因。
六、* 与 ** 组合:控制首组件的形态
将 * 与 ** 组合,可以精确约束"第一组件"的形态,同时放行任意深度的子模块。
6.1 首组件以 * 结尾:匹配前缀相似的一组包
模式 aws*.** 的含义是"任意首组件以 aws 开头的模块及其全部子树":
[analysis]
allowed-unresolved-imports = ["aws*.**"]
import aws
import awscli
import awscli.alias
import awscli.customizations.sagemaker
from awscli.customizations import sagemaker
import awws # error: [unresolved-import]
import foo.aws # error: [unresolved-import]
import caws # error: [unresolved-import]
reveal_type(aws) # revealed: Unknown
reveal_type(awscli) # revealed: Unknown
reveal_type(awscli.alias) # revealed: Unknown
reveal_type(awscli.customizations.sagemaker) # revealed: Unknown
reveal_type(sagemaker) # revealed: Unknown
reveal_type(awws) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
reveal_type(foo.aws) # revealed: Unknown
reveal_type(caws) # revealed: Unknown
注意三个反例:awws 虽然以 a 开头但不以 aws 开头(aws* 要求前缀恰好是 aws,awws 是 aww 开头);foo.aws 的首组件是 foo;caws 的首组件不以 aws 开头。它们全部继续报 unresolved-import。同时注意,被豁免的模块类型都是 Unknown,不会因为放行而获得任何类型推断。
6.2 首组件含多个 *:匹配"包含某子串"的一组包
模式 *aws*.** 的含义是"任意首组件包含字符串 aws 的模块及其全部子树":
[analysis]
allowed-unresolved-imports = ["*aws*.**"]
import aws
import awscli
import awscli.alias
import awscli.customizations.sagemaker
import caws
import caws.foo.bar
from awscli.customizations import sagemaker
import awws # error: [unresolved-import]
import foo.aws # error: [unresolved-import]
reveal_type(aws) # revealed: Unknown
reveal_type(awscli) # revealed: Unknown
reveal_type(awscli.alias) # revealed: Unknown
reveal_type(awscli.customizations.sagemaker) # revealed: Unknown
reveal_type(sagemaker) # revealed: Unknown
reveal_type(awws) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
reveal_type(foo.aws) # revealed: Unknown
reveal_type(caws) # revealed: Unknown
reveal_type(caws.foo.bar) # revealed: Unknown
aws、awscli、caws(含子串 aws)全部命中;而 awws(含 ww 而非 aws)和 foo.aws(aws 不在首组件中)仍然报错。官方配置文档中"抑制任何首组件包含子串 test 的模块"建议写法 *test*.** 正是该模式的直接应用(见 configuration.md)。
七、否定模式:! 前缀与"最后匹配优先"
与 gitignore 类似,模式可以加 ! 前缀取反。当多个模式同时命中同一个导入时,列表中靠后的模式(无论正负)总是优先于靠前的模式。如果最后命中的是正向模式,模块被放行;如果最后命中的是否定模式,则照常报 unresolved-import。这一"last match wins"语义在 module_glob.rs 的 ModuleGlobSet::matches 中有明确实现:它从 RegexSet 的命中集合中取索引最大(即列表中最靠后)的模式作为裁决依据。
7.1 否定模式跟在正向模式之后:白名单中剔除例外
下面的配置"放行所有首组件为 test 的模块,但 test.foo 除外":
[analysis]
allowed-unresolved-imports = ["test.**", "!test.foo"]
from test.bar import baz
from test.foo import bar # error: [unresolved-import]
test.bar 被 test.** 放行;而 test.foo 同时命中 test.**(正向)与 !test.foo(否定),由于否定模式在列表中更靠后、优先级更高,最终判定为排除,诊断照常发出。
官方配置文档在 configuration.md 中给出的默认示例正是这种写法。仓库文档测试还指出了它的典型应用场景:覆盖父级配置。例如项目主配置放行所有 test 模块的导入,而子项目可以通过 overrides 配置追加 !test.foo,对该模块重新启用严格的 unresolved-import 检查——这正是第二节提到的 overrides.analysis 配置组的用武之地。
7.2 正向模式跟在否定模式之后:精确白名单
下面的配置表明"只有 test.foo 一个导入允许未解析而不报错":
[analysis]
allowed-unresolved-imports = ["test.**", "!test.**", "test.foo"]
from test.bar import baz # error: [unresolved-import]
from test.foo import bar
test.bar 依次命中 test.**(正向)、!test.**(否定),最后命中的是否定模式,因此被排除、报错;test.foo 则最后命中 test.foo(正向),被放行。这一技巧适用于"父配置放行了整个子树,但只想保留极少数例外"的精确控制。
八、顺带抑制"模块缺少成员"诊断
allowed-unresolved-imports 的豁免范围比直觉上更大:当从"已存在但缺少该成员"的模块导入成员时,ty 通常会报 Module "X" has no member "Y";而该配置同样能抑制这类诊断。
[analysis]
allowed-unresolved-imports = ["pkg.nonexistent"]
pkg/__init__.py:
x = 1
pkg/a.py:
from pkg import nonexistent
pkg 模块可以正常解析,但 pkg 中不存在成员 nonexistent。由于配置了 allowed-unresolved-imports = ["pkg.nonexistent"],这一"缺少成员"诊断也被放行。结合 imports.rs 的源码可以确认:ty 把 from pkg import nonexistent 视作对子模块 pkg.nonexistent 的解析尝试,命中豁免模式后即跳过诊断,但导入的绑定类型仍以 Type::unknown() 写入符号表。
九、底层实现:从 Glob 到正则的编译管线
理解匹配语义后,再来看仓库中承担编译与匹配的核心模块 module_glob.rs,便于排查复杂的模式组合:
-
解析与校验(
ModuleGlobSetBuilder::add):剥离!前缀标记否定;拒绝空模式(""、"!")、首/尾点号(.foo、foo.)、连续点号(foo..bar)、以及**混写进普通组件(foo**、**bar)等非法输入(见 module_glob.rs 与ModuleGlobError枚举)。 -
转换为正则(
glob_to_regex):按.拆分组件后逐段翻译——**开头译为(?:[^.]+\.)*、中间或结尾译为(?:\.[^.]+)*;独立*组件译为[^.]+;混写在文本中的*译为[^.]*;其余正则元字符被转义(见 module_glob.rs)。 -
编译与匹配:所有模式编译进一个
RegexSet,matches时取命中集合中索引最大者裁决,negated则返回Exclude,否则返回Include(见 module_glob.rs)。ModuleNameMatch三态枚举None/Include/Exclude及is_include()辅助方法则被导入推断层直接消费。 -
配置接线:
Options.allowed_unresolved_imports声明在 options.rs,经to_settings转换为AnalysisSettings.allowed_unresolved_imports(ModuleGlobSet类型,默认空集),最终进入ty_python_semantic的导入推断。
ModuleGlobSet 自带完整的单元测试(见 module_glob.rs),覆盖精确匹配、test.*、*.test、foo.*.bar、test.**、**.bar、test.**.bar、**、否定模式及其覆盖顺序、非法模式报错等场景,可作为理解匹配语义的补充参考。
十、实战建议与总结
综合以上语义,配置 allowed-unresolved-imports 时的实用建议:
- 放行单一已知模块:直接写字面量
["foo"],范围最小、最精确; - 放行某包及其全部子模块:使用
["pkg.**"];注意["pkg"]不会覆盖pkg.sub; - 放行前缀不确定的一组包(如 SDK 变体):用
["aws*.**"]约束首组件前缀;["*aws*.**"]则放宽为"首组件包含子串"; - 全局放行 + 精准剔除:利用
["test.**", "!test.foo"]的"后匹配优先"规则,在父配置放行基础上由overrides追加例外; - 只保留极少数例外:使用
["test.**", "!test.**", "test.foo"]三段式,把白名单收敛到单个模块。
同时要意识到该配置的代价:被豁免的模块类型一律是 Unknown,后续对它做属性访问、方法调用时 ty 不会再给出类型级检查,因此豁免范围应当尽量收窄,避免用 ["**"] 之类的一揽子模式关闭整个项目的导入解析。合理的做法是优先修正环境与依赖声明(如补全 project.dependencies),仅在确认"解析不到是预期行为"时才使用豁免,并配合 reveal_type 确认豁免后的类型结果符合预期。如需把豁免模块的语义提升为 Any 以便继续做有限的类型交互,可评估同配置组的 replace-imports-with-any 作为补充手段。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00