ARTICLE DETAIL

资讯详情

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

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

Ruff Ty 类型检查器 Sentinels 支持解析:从 `typing_extensions.Sentinel` 到 `builtins.sentinel` Ruff Ty 类型检查器 Sentinels 支持解析从typing_extensions.Sentinel到builtins.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 中一种经典的设计模式用一个独一无二的对象实例来代表某个参数没有被显式提供从而区别于None、False、0或空字符串等合法但可能被误传的值。典型应用包括inspect.Parameter.empty、argparse.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.rsSelf::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, reprwith 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的结果是哨兵自身的名称MISSING、OTHER等而不是Sentinel类本身。这来自 Ty 将每个哨兵建模为独立已知实例类型KnownInstance的设计KnownInstanceType::Sentinel(SentinelInstance)其中SentinelInstance以 salsa 内部化结构保存name和definition声明位置见 crates/ty_python_semantic/src/types/known_instance.rs。哨兵类型的显示名称也直接使用声明时的名字见 crates/ty_python_semantic/src/types/display.rsKnownInstanceType::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-defaultdef 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.rsfn 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: LOCAL报invalid-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.rsKnownInstanceType::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, reprNAME) UNKNOWN_REPR Sentinel(UNKNOWN_REPR, reprUNKNOWN) # error: [unresolved-reference] UNKNOWN_KEYWORD Sentinel(UNKNOWN_KEYWORD, unknownNAME) # error: [unknown-argument]Sentinel(NAME)NAME不是字符串字面量回退普通调用不报错但也不产生哨兵类型Sentinel(UNKNOWN)UNKNOWN未解析报unresolved-referencereprNAMErepr 非字面量回退普通调用reprUNKNOWN未解析引用报unresolved-referenceunknownNAME非法关键字参数报unknown-argument。这与源码中专用路径返回None则回退infer_call_expression_impl的分派设计完全对应。Python 3.15builtins.sentinel与版本分派新环境下的等价行为从 Python 3.15 起标准库新增了builtins.sentineltyping_extensions.Sentinel变为它的再导出。在 mdtest 中通过环境切换验证[environment] python-version 3.15from typing import assert_type MISSING sentinel(MISSING) OTHER sentinel(OTHER) WITH_REPR sentinel(WITH_REPR, with repr) WITH_REPR_KEYWORD sentinel(WITH_REPR_KEYWORD, reprwith 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_KEYWORDbuiltins.sentinel的构造语法与typing_extensions.Sentinel完全一致位置参数name、可选的位置repr或repr关键字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.rsSelf::Sentinel python_version PythonVersion::PY315,在已知类列表中Sentinel被标记为仅在 Python 3.15 及以上才属于 builtins模块归属同样按版本切换3.15 之前归TypingExtensions3.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携带name与definitionknown_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_extensions3.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-type、invalid-parameter-default、invalid-base、invalid-type-form、unresolved-reference、unknown-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),仅供参考
返回列表