news 2026/9/12 3:28:48

Ruff Ty 类型检查器 Sentinels 支持解析:从 `typing_extensions.Sentinel` 到 `builtins.sentinel`

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ruff Ty 类型检查器 Sentinels 支持解析:从 `typing_extensions.Sentinel` 到 `builtins.sentinel`

Ruff Ty 类型检查器 Sentinels 支持解析:从typing_extensions.Sentinelbuiltins.sentinel

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

导读

本文以 crates/ty_python_semantic/resources/mdtest/sentinels.md 为核心骨架,系统讲解 Ruff 新一代类型检查器 Ty 对 Python "哨兵(sentinel)" 对象的完整类型建模:包括typing_extensions.Sentinel的构造规则、在类型表达式与默认参数中的用法、类作用域声明、is收窄(narrowing)与联合类型推断,以及 Python 3.15 引入builtins.sentinel后的版本分派逻辑。读完本文,你将掌握如何在 Ty 中正确声明与使用哨兵类型,理解其底层的源码实现路径,并能用 mdtest 测试框架验证这些行为。

Sentinels 是什么:类型系统中的"唯一标记"模式

哨兵对象是 Python 中一种经典的设计模式:用一个独一无二的对象实例来代表"某个参数没有被显式提供",从而区别于NoneFalse0或空字符串等"合法但可能被误传"的值。典型应用包括inspect.Parameter.emptyargparse.SUPPRESS等。

传统做法需要手动编写一个类并覆写__repr__,非常繁琐。而typing_extensions.Sentinel提供了一行式构造方式,让哨兵既能作为运行时值,又能作为类型注解直接使用。Ty 类型检查器对它有专门支持,相关行为全部由 mdtest 文档驱动测试覆盖(mdtest 框架实现在 crates/mdtest/src/lib.rs,负责把 Markdown 中的 Python 代码块作为可执行断言运行)。

基础用法:用Sentinel(...)构造类型级哨兵

环境前提

Sentinel 支持不依赖特定的 Python 版本即可用于类型表达式,文档中给出的基准环境为 Python 3.10:

[environment] python-version = "3.10"

从源码看,Ty 在 Python 3.15 之前将Sentinel映射到typing_extensions模块,3.15 起才切换到builtins(见 crates/ty_python_semantic/src/types/class/known.rs):

Self::Sentinel => { if python_version >= PythonVersion::PY315 { KnownModule::Builtins } else { KnownModule::TypingExtensions } }

构造语法与 reveal_type 结果

Sentinel接受一个字符串字面量名称,并可选的第二个位置参数或repr=关键字参数来定制其repr展示:

from typing_extensions import Sentinel, assert_type MISSING = Sentinel("MISSING") OTHER = Sentinel("OTHER") WITH_REPR = Sentinel("WITH_REPR", "<with repr>") WITH_REPR_KEYWORD = Sentinel("WITH_REPR_KEYWORD", repr="<with repr keyword>") reveal_type(MISSING) # revealed: MISSING reveal_type(OTHER) # revealed: OTHER reveal_type(WITH_REPR) # revealed: WITH_REPR reveal_type(WITH_REPR_KEYWORD) # revealed: WITH_REPR_KEYWORD

注意reveal_type的结果是哨兵自身的名称MISSINGOTHER等),而不是Sentinel类本身。这来自 Ty 将每个哨兵建模为独立"已知实例类型"(KnownInstance)的设计:KnownInstanceType::Sentinel(SentinelInstance),其中SentinelInstance以 salsa 内部化结构保存namedefinition(声明位置),见 crates/ty_python_semantic/src/types/known_instance.rs。

哨兵类型的显示名称也直接使用声明时的名字,见 crates/ty_python_semantic/src/types/display.rs:

KnownInstanceType::Sentinel(sentinel) => { f.with_type(ty).write_str(sentinel.name(db).as_str()) }

构造调用的底层识别路径

Ty 并不是把Sentinel(...)当作普通函数调用处理。在 crates/ty_python_semantic/src/types/infer/builder.rs 中可以看到分派逻辑:

Some(KnownClass::Sentinel) => self .infer_sentinel_expression(target, call_expr, definition) .unwrap_or_else(|| { self.infer_call_expression_impl(call_expr, callable_type, tcx) }),

即:当被调用的可调用对象被识别为内置已知类KnownClass::Sentinel时,会先尝试走专用路径infer_sentinel_expression;只有该路径返回None(无法识别为合法哨兵声明)时,才回退到普通调用推断。

infer_sentinel_expression(见 crates/ty_python_semantic/src/types/infer/builder.rs)内部的具体约束包括:

  • 赋值目标必须是一个Name表达式(简单变量名);
  • 参数列表中不能出现*args星号展开;
  • 位置参数只能是 1 个(name)或 2 个(name, repr);
  • 关键字参数只允许repr,且不能与位置形式的 repr 同时出现;
  • name参数必须是字符串字面量;
  • repr参数必须是字符串字面量或None

一旦满足条件,就构造SentinelInstance并把该哨兵作为类型返回。

哨兵在函数签名中的使用:类型表达式与默认值

参数注解中的唯一类型

每个哨兵都是一个独一无二的类型,因此可以被直接用作参数注解,并且互相不兼容:

def accepts_missing(x: MISSING) -> None: ... def accepts_other(x: OTHER) -> None: ... accepts_missing(MISSING) accepts_missing(OTHER) # error: [invalid-argument-type] accepts_other(OTHER) accepts_other(MISSING) # error: [invalid-argument-type]

传入"错误"的哨兵会触发invalid-argument-type错误,说明 Ty 依据is_same_sentinel判断同一性——两个哨兵只有在同一文件、同一文件作用域、同一位置(即同一个声明语句)时才视为同一个类型,见 crates/ty_python_semantic/src/types/known_instance.rs。

默认值的合法性校验

哨兵不能作为不兼容类型的默认值。普通int参数如果默认值是哨兵,会报invalid-parameter-default

def bad_default(x: int = MISSING) -> None: # error: [invalid-parameter-default] pass

正确姿势是把哨兵纳入参数类型的联合中,让默认值成为联合的成员之一:

def good_default(x: int | MISSING | OTHER = MISSING) -> None: if x is MISSING: assert_type(x, MISSING) reveal_type(x) # revealed: MISSING else: assert_type(x, int | OTHER) reveal_type(x) # revealed: int | OTHER good_default(1) good_default(MISSING) good_default(OTHER)

这里的assert_type(x, ...)reveal_type(x)断言验证了核心能力:is比较可以对哨兵联合类型进行收窄(narrowing)——在x is MISSING为真的分支中x被收窄为精确的MISSING,在else分支中则收窄为int | OTHER

四种is收窄方向:正反向与嵌套分支

Ty 对哨兵的收窄支持四种写法,且收窄方向全部正确(对应的收窄实现参与逻辑位于 crates/ty_python_semantic/src/types/infer/comparisons.rs,其中Sentinel被列为参与比较的类型之一):

def reverse_check(x: int | MISSING | OTHER) -> None: if MISSING is x: # 反写 is:哨兵在左 assert_type(x, MISSING) reveal_type(x) # revealed: MISSING else: assert_type(x, int | OTHER) reveal_type(x) # revealed: int | OTHER def negative_check(x: int | MISSING | OTHER) -> None: if x is not MISSING: # 否定形式 is not assert_type(x, int | OTHER) reveal_type(x) # revealed: int | OTHER else: assert_type(x, MISSING) reveal_type(x) # revealed: MISSING def reverse_negative_check(x: int | MISSING | OTHER) -> None: if MISSING is not x: # 反写 + 否定 assert_type(x, int | OTHER) reveal_type(x) # revealed: int | OTHER else: assert_type(x, MISSING) reveal_type(x) # revealed: MISSING

这四种组合(is/is not× 哨兵在左/在右)覆盖了实际代码中常见的哨兵判断写法,保证if x is MISSING:if MISSING is x:在类型层面行为一致。

哨兵对象的运行时属性:真值、元数据与禁止继承

哨兵对象在运行时遵循以下约定,Ty 均给出了对应的类型断言:

MISSING = Sentinel("MISSING") reveal_type(bool(MISSING)) # revealed: Literal[True] reveal_type(MISSING.__module__) # revealed: str class MissingSubclass(MISSING): # error: [invalid-base] pass
  • 总是真值bool(MISSING)的类型被推断为Literal[True],绝不会是False
  • 标准元数据属性__module__等标准哨兵属性可正常访问,类型为str
  • 禁止作为基类:试图class MissingSubclass(MISSING):会报invalid-base。从源码看,crates/ty_python_semantic/src/types/class_base.rs 参与了基类校验,KnownClass::Sentinel也在 class 相关检查中被特别处理(见 crates/ty_python_semantic/src/types/class/known.rs 中多处Sentinel枚举分支)。

类作用域中的哨兵:C.MARKER形态

哨兵不仅可以在模块顶层声明,也可以声明在类体内,并通过C.MARKER的形式引用:

class C: MARKER = Sentinel("C.MARKER") def accepts_marker(x: C.MARKER) -> None: ... accepts_marker(C.MARKER) def class_default(x: int | C.MARKER = C.MARKER) -> None: if x is C.MARKER: assert_type(x, C.MARKER) reveal_type(x) # revealed: MARKER else: assert_type(x, int) reveal_type(x) # revealed: int def class_reverse_negative(x: int | C.MARKER) -> None: if C.MARKER is not x: assert_type(x, int) reveal_type(x) # revealed: int else: assert_type(x, C.MARKER) reveal_type(x) # revealed: MARKER

注意这里reveal_type显示的名称是MARKER而非C.MARKER——类型展示使用哨兵声明时的name,而注解写法仍是限定名C.MARKER。类作用域哨兵同样支持默认值联合与is/is not收窄。

底层作用域限制

为什么只支持模块与类作用域?这与infer_sentinel_expression前置调用的sentinel_definition_scope_is_supported检查直接相关(crates/ty_python_semantic/src/types/infer/builder.rs):

fn sentinel_definition_scope_is_supported(&self) -> bool { let db = self.db(); let mut scope_id = self.scope.file_scope_id(db); loop { let scope = self.index.scope(scope_id); match scope.node().scope_kind() { ScopeKind::Module => return true, ScopeKind::Class => {} ScopeKind::Function | ScopeKind::Lambda | ScopeKind::Comprehension | ScopeKind::TypeAlias | ScopeKind::TypeParams => return false, } let Some(parent) = scope.parent() else { return false; }; scope_id = parent; } }

从源码结构看,这是对声明位置的白名单式校验:从当前作用域向上遍历,只要遇到函数、Lambda、推导式、类型别名或类型参数作用域就拒绝,只有模块与类作用域(含嵌套类)允许。因此:

def outer(): LOCAL = Sentinel("LOCAL") def inner(x: LOCAL) -> None: ... # error: [invalid-type-form]

在函数内部声明的哨兵不会被识别为哨兵类型,注解x: LOCALinvalid-type-form

哨兵不是泛型:禁止下标特化

哨兵类型不能被下标特化:

MISSING = Sentinel("MISSING") def f(x: MISSING[int]) -> None: ... # error: [invalid-type-form]

这源于类型表达式推断中对KnownInstanceType::Sentinel的专门分支处理(crates/ty_python_semantic/src/types/infer/builder/type_expression.rs):

KnownInstanceType::Sentinel(sentinel) => { if !self.in_string_annotation() { self.infer_expression(&subscript.slice, TypeContext::default()); } if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "`{}` is a sentinel and cannot be specialized", sentinel.name(self.db()) )); } Type::unknown() }

在字符串注解(如"MISSING[int]")中,Ty 仍会尝试推断下标切片内容,但同样会报告invalid-type-form并把该类型当作unknown处理。

非法构造回退:非字面量参数走普通调用路径

Sentinel(...)的识别要求 name 与 repr 都是字符串字面量。一旦参数不是字面量,构造表达式就"降级"为普通函数调用,此时不会产生哨兵类型,而是暴露出常规的调用检查错误:

NAME = "NAME" NON_LITERAL_NAME = Sentinel(NAME) UNKNOWN_NAME = Sentinel(UNKNOWN) # error: [unresolved-reference] NON_LITERAL_REPR = Sentinel("NON_LITERAL_REPR", repr=NAME) UNKNOWN_REPR = Sentinel("UNKNOWN_REPR", repr=UNKNOWN) # error: [unresolved-reference] UNKNOWN_KEYWORD = Sentinel("UNKNOWN_KEYWORD", unknown=NAME) # error: [unknown-argument]
  • Sentinel(NAME)NAME不是字符串字面量,回退普通调用,不报错但也不产生哨兵类型;
  • Sentinel(UNKNOWN)UNKNOWN未解析,报unresolved-reference
  • repr=NAME:repr 非字面量,回退普通调用;
  • repr=UNKNOWN:未解析引用,报unresolved-reference
  • unknown=NAME:非法关键字参数,报unknown-argument

这与源码中"专用路径返回None则回退infer_call_expression_impl"的分派设计完全对应。

Python 3.15:builtins.sentinel与版本分派

新环境下的等价行为

从 Python 3.15 起,标准库新增了builtins.sentineltyping_extensions.Sentinel变为它的再导出。在 mdtest 中通过环境切换验证:

[environment] python-version = "3.15"
from typing import assert_type MISSING = sentinel("MISSING") OTHER = sentinel("OTHER") WITH_REPR = sentinel("WITH_REPR", "<with repr>") WITH_REPR_KEYWORD = sentinel("WITH_REPR_KEYWORD", repr="<with repr keyword>") reveal_type(MISSING) # revealed: MISSING reveal_type(OTHER) # revealed: OTHER reveal_type(WITH_REPR) # revealed: WITH_REPR reveal_type(WITH_REPR_KEYWORD) # revealed: WITH_REPR_KEYWORD

builtins.sentinel的构造语法与typing_extensions.Sentinel完全一致:位置参数name、可选的位置reprrepr=关键字,reveal_type同样显示哨兵名称。其余行为(参数注解唯一性、默认值校验、四种is收窄、类作用域、真值/元数据/禁止继承、非泛型、非法构造回退)在 3.15 下与 3.10 下逐条一致,mdtest 文档对其完整复述了一遍,确保两个版本的行为不产生回归。

底层模块映射

Ty 对Sentinel的已知类定义在 Python 3.15 前后指向不同的模块,相关映射见 crates/ty_python_semantic/src/types/class/known.rs 与 crates/ty_python_semantic/src/types/class/known.rs:

Self::Sentinel => python_version >= PythonVersion::PY315,

在已知类列表中,Sentinel被标记为"仅在 Python 3.15 及以上才属于 builtins";模块归属同样按版本切换:3.15 之前归TypingExtensions,3.15 起归Builtins。这保证了import builtins; sentinel(...)import typing_extensions; Sentinel(...)在各自版本下都能被正确识别为同一概念。

3.15 下typing_extensions.Sentinel依旧可用

即便在 Python 3.15 下,typing_extensions.Sentinel作为再导出依然可以正常使用:

import typing_extensions EXTENSIONS_MISSING = typing_extensions.Sentinel("EXTENSIONS_MISSING") def f(x: int | EXTENSIONS_MISSING): ... f(42) f(EXTENSIONS_MISSING) f(None) # error: [invalid-argument-type]

x: int | EXTENSIONS_MISSING的联合类型正常工作:42与哨兵本身可传参,None则报invalid-argument-type。这验证了版本迁移的向后兼容性——升级到 3.15 后既可用新语法sentinel(...),也不必立刻改掉已有的typing_extensions.Sentinel代码。

设计要点总结

围绕上述文档与源码,可以把 Ty 的哨兵类型支持归纳为以下设计要点:

设计维度行为证据位置
类型建模每个哨兵是独立KnownInstanceType::Sentinel,携带namedefinitionknown_instance.rs
同一性判定同文件、同文件作用域、同位置的声明才视为同一哨兵known_instance.rs
构造识别专用路径infer_sentinel_expression,失败回退普通调用builder.rs
声明作用域仅模块与类作用域,函数内不识别builder.rs
联合收窄is/is not四种写法均正确收窄comparisons.rs
禁止特化MISSING[int]invalid-type-formtype_expression.rs
版本分派3.15 前归typing_extensions,3.15 起归builtinsknown.rs

如何在 Ty 中运行本文的全部示例

本文所有代码示例均来自 crates/ty_python_semantic/resources/mdtest/sentinels.md,它们不是普通文档,而是可执行的类型检查测试。mdtest 是 Ty 生态自带的 Markdown 测试框架(crates/mdtest/src/lib.rs),会把 Markdown 中的 Python 代码块解析为断言——reveal_type/assert_type断言期望的类型,# error: [code]断言期望的诊断错误码(如invalid-argument-typeinvalid-parameter-defaultinvalid-baseinvalid-type-formunresolved-referenceunknown-argument)。通过[environment] python-version配置块还可以切换 Python 版本以覆盖 3.10 与 3.15 两条行为分支。

若要在本地复现这些类型检查结果,可以借助仓库中的相关测试基础设施运行 mdtest 测试套件;也可以在支持 Ty 的编辑器环境中直接尝试这些代码片段,观察reveal_type与错误诊断的实时输出。文档中标注# revealed:# error:的行即为权威预期结果,可作为校验实现是否正确的基准。

【免费下载链接】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/12 3:28:06

Java泛型原理深度解析:擦除机制与通配符本质

1. 为什么“从零开始学Java之泛型基本使用”这个标题背后藏着一个被严重低估的认知断层&#xff1f;我带过三届校招新人&#xff0c;也给二十多家中小企业的开发团队做过Java基础复训。每次讲到泛型&#xff0c;总有人在笔记本上抄下List<String>、Map<K, V>的写法…

作者头像 李华
网站建设 2026/9/12 3:27:01

Arduino IDE全平台安装避坑指南:驱动、权限与信任链详解

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

作者头像 李华
网站建设 2026/9/12 3:26:57

即梦AI替代工具实测:4款商用级图像生成平台迁移指南

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

作者头像 李华
网站建设 2026/9/12 3:26:41

Viggle AI小鸡消失模板:从动作包到零门槛AI梗图生成指南

1. 小鸡消失梗图模板到底是什么1.1 一条十几秒视频引发的跟风最近刷短视频&#xff0c;频繁刷到同一类内容&#xff1a;画面里一只小鸡正在啄米、走动&#xff0c;下一秒整个身体刷地一下消失&#xff0c;只剩背景和一个浅浅的残影&#xff0c;配合慢半拍的字幕“这下真没了”&…

作者头像 李华
网站建设 2026/9/12 3:25:21

Java并发编程中的锁机制优化与工程实践

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

作者头像 李华