首页
/ ty 类型检查器 `allowed-unresolved-imports` 配置完全指南:用模块 Glob 通配符精确控制未解析导入诊断

ty 类型检查器 `allowed-unresolved-imports` 配置完全指南:用模块 Glob 通配符精确控制未解析导入诊断

2026-09-09 19:36:49作者:翟江哲Frasier

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****foofoo.bar** 这类与其它文本混写的模式是非法的,会在解析阶段直接报 InvalidDoubleStarUsage 错误(见 module_glob.rs 及对应单元测试)。

4.1 结尾的 **:匹配以某前缀开头的整个子树

foo.** 匹配所有以 foo 开头的模块名,包括 foo 本身、foo.barfoo.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 结尾的模块名,包括 foobar.foobaz.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.foofoo 结尾可以放行,但 main.py 中这个导入的模块以 bar 结尾(foo 只是中间组件),依然报错:

main.py:

from foo.bar import foo  # error: [unresolved-import]

4.3 中间的 **:首尾固定的任意深度

foo.**.bar 匹配"首组件为 foo、末组件为 bar"的任意模块名,包括 foo.barfoo.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*.footest* 可以匹配 testtesting,因此上面的两个导入都被放行。而下面这个导入中,test.ing.foo 的第一组件是 test、第二组件是 ing——模式要求 test* 之后紧跟 .foo,中间隔了一个组件,* 无法跨过 .,于是报错:

import test.ing.foo  # error: [unresolved-import]

module_glob.rsglob_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* 要求前缀恰好是 awsawwsaww 开头);foo.aws 的首组件是 foocaws 的首组件不以 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

awsawsclicaws(含子串 aws)全部命中;而 awws(含 ww 而非 aws)和 foo.awsaws 不在首组件中)仍然报错。官方配置文档中"抑制任何首组件包含子串 test 的模块"建议写法 *test*.** 正是该模式的直接应用(见 configuration.md)。

七、否定模式:! 前缀与"最后匹配优先"

与 gitignore 类似,模式可以加 ! 前缀取反。当多个模式同时命中同一个导入时,列表中靠后的模式(无论正负)总是优先于靠前的模式。如果最后命中的是正向模式,模块被放行;如果最后命中的是否定模式,则照常报 unresolved-import。这一"last match wins"语义在 module_glob.rsModuleGlobSet::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.bartest.** 放行;而 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,便于排查复杂的模式组合:

  1. 解析与校验ModuleGlobSetBuilder::add):剥离 ! 前缀标记否定;拒绝空模式("""!")、首/尾点号(.foofoo.)、连续点号(foo..bar)、以及 ** 混写进普通组件(foo****bar)等非法输入(见 module_glob.rsModuleGlobError 枚举)。

  2. 转换为正则glob_to_regex):按 . 拆分组件后逐段翻译——** 开头译为 (?:[^.]+\.)*、中间或结尾译为 (?:\.[^.]+)*;独立 * 组件译为 [^.]+;混写在文本中的 * 译为 [^.]*;其余正则元字符被转义(见 module_glob.rs)。

  3. 编译与匹配:所有模式编译进一个 RegexSetmatches 时取命中集合中索引最大者裁决,negated 则返回 Exclude,否则返回 Include(见 module_glob.rs)。ModuleNameMatch 三态枚举 None / Include / Excludeis_include() 辅助方法则被导入推断层直接消费。

  4. 配置接线Options.allowed_unresolved_imports 声明在 options.rs,经 to_settings 转换为 AnalysisSettings.allowed_unresolved_importsModuleGlobSet 类型,默认空集),最终进入 ty_python_semantic 的导入推断。

ModuleGlobSet 自带完整的单元测试(见 module_glob.rs),覆盖精确匹配、test.**.testfoo.*.bartest.****.bartest.**.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 作为补充手段。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525