资讯动态

ty 类型检查器中的 Annotated 处理全解析:元数据忽略、type[...] 交互与参数化校验

发布时间:2026/9/10 2:23:02 来源:尧图企业网站定制
ty 类型检查器中的 Annotated 处理全解析元数据忽略、type[...] 交互与参数化校验【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/rufftyping.Annotated是 Python 类型注解中附加元数据而不改变类型的利器。在 Ruff 仓库的 ty 类型检查器crates/ty_python_semantic中Annotated的语义通过一套 Markdown 驱动的测试套件mdtest被严格定义与验证。本文以 crates/ty_python_semantic/resources/mdtest/annotations/annotated.md 为骨架逐条剖析 ty 对Annotated的推导规则、错误诊断、type[...]内部行为与继承语义并结合源码实现解释其底层原理。读完本文你将掌握 ty 中Annotated的完整行为模型以及如何阅读与扩展 mdtest 用例来验证类型推导结果。基本语义Annotated[T, ...]等价于Tmdtest 的第一个断言直接点明核心语义Annotated[T, ...]与T完全等价——所有元数据参数metadata arguments都会被简单地忽略不参与类型推导。from typing_extensions import Annotated def _(x: Annotated[int, foo]): reveal_type(x) # revealed: int def _(x: Annotated[int, lambda: 0 1 * 2 // 3, _(4)]): reveal_type(x) # revealed: int def _(x: Annotated[int, arbitrary, metadata, elements, are, fine]): reveal_type(x) # revealed: int def _(x: Annotated[tuple[str, int], bytes]): reveal_type(x) # revealed: tuple[str, int]注意几点值得玩味的细节元数据可以是字符串字面量foo、lambda 表达式、任意调用_(4)乃至字节串bytes类型检查器都会逐个推导它们确保语法与名称解析正确但丢弃其值只取第一个参数作为实际注解类型第二个用例中的lambda: 0 1 * 2 // 3证明元数据表达式可以是任意 Python 表达式ty 会正常推导其类型但忽略结果第四个用例说明Annotated包裹泛型实例tuple[str, int]时同样等价于该实例本身。这一忽略元数据的行为在源码中有着清晰的实现crates/ty_python_semantic/src/types/infer/builder/subscript.rs中的parse_subscription_of_annotated_special_form下标解析实现要求下标切片必须是元组随后遍历arguments[1..]即所有元数据元素逐一调用infer_expression完成推导但丢弃返回结果最后只把第一个参数交给subscript_context.infer得到真正的类型。Annotated本身被建模为SpecialFormType::Annotated见 special_form.rs 中的定义对应typing.Annotated或typing_extensions.Annotated符号。字符串注解中的元数据解包字典与条件表达式当注解以字符串形式出现延迟求值 / 前向引用场景时ty 同样支持Annotated且元数据部分可以包含带解包字典的调用dict(**{...})。无论元数据写得多复杂都不会影响被注解的类型from typing_extensions import Annotated value: Annotated[int, dict(**{})] def convert(value: Annotated[str, dict(**{name: value})]) - Annotated[int, dict(**{})]: reveal_type(value) # revealed: str return 1这里dict(**{})是合法的元数据表达式ty 在解析字符串注解后照常推导但结果仍是str/int。条件表达式同样可以作为元数据且不改变注解类型def flag() - bool: return True conditional_value: Annotated[int, 1 if flag() else 2] 11 if flag() else 2的推导结果无关紧要conditional_value的类型依然是int。这背后对应源码中的parse_string_annotation见 annotation_expression.rs字符串注解会先被解析成 AST再走与普通注解相同的Annotated下标处理路径。Annotated在type[...]内部的行为Annotated可以包裹类或特化泛型类并出现在type[...]内且不会改变最终得到的类对象类型from typing_extensions import Annotated def _( simple: type[Annotated[int, metadata]], generic: type[Annotated[list[str], metadata]], ): reveal_type(simple) # revealed: type[int] reveal_type(generic) # revealed: type[list[str]]对类的联合与嵌套Annotated同样成立def _( union: type[Annotated[int | str, metadata]], nested: type[Annotated[Annotated[int, inner], outer]], ): reveal_type(union) # revealed: type[int | str] reveal_type(nested) # revealed: type[int]type[Annotated[Annotated[int, inner], outer]]被推导为type[int]说明嵌套的Annotated会逐层剥离元数据直到露出真正的类型。但反过来把一个非类类型包裹进Annotated并不能让它变成type[...]的合法参数from typing import Callable def _( # error: [invalid-type-form] The argument to type[] must be a class object type invalid: type[Annotated[Callable[[], int], metadata]], ): reveal_type(invalid) # revealed: type[Unknown]Callable不是类对象类型即使套上Annotatedty 依然报告invalid-type-form错误并把结果降级为type[Unknown]以便后续诊断继续。这说明Annotated的等价性是先剥离元数据、再按裸类型进行合法性检查的。参数化规则少于两个参数即报错Annotated至少需要两个参数一个类型 至少一个元数据元素ty 会对各种非法形式报出invalid-type-form诊断。mdtest 对这一规则覆盖得极其详尽from typing_extensions import Annotated # error: [invalid-type-form] typing.Annotated requires at least two arguments when used in a parameter annotation def _(x: Annotated): reveal_type(x) # revealed: Unknown def _(flag: bool): if flag: X Annotated else: X bool # error: [invalid-type-form] typing.Annotated requires at least two arguments when used in a parameter annotation def f(y: X): reveal_type(y) # revealed: Unknown | bool # error: [invalid-type-form] typing.Annotated requires at least two arguments when used in a parameter annotation def _(x: Annotated | bool): reveal_type(x) # revealed: Unknown | bool # error: [invalid-type-form] Special form typing.Annotated expected at least 2 arguments (one type and at least one metadata element) # error: [invalid-type-form] Special form typing.Annotated expected at least 2 arguments (one type and at least one metadata element) def _(x: Annotated[()], y: list[Annotated[()]]): reveal_type(x) # revealed: Unknown reveal_type(y) # revealed: list[Unknown] # error: [invalid-type-form] def _(x: Annotated[int]): # Annotated[T] is invalid and will raise an error at runtime, # but we treat it the same as T to provide better diagnostics later on. # The subscription itself is still reported, regardless. # Same for the (int,) form below. reveal_type(x) # revealed: int # error: [invalid-type-form] def _(x: Annotated[(int,)]): reveal_type(x) # revealed: int这条规则包含几个值得注意的行为分支裸Annotated未下标化时类型为Unknown并报告requires at least two arguments通过变量间接使用X Annotated条件分支中后再用于参数注解类型退化为Unknown | bool并同样报错——说明 ty 对运行时可能取到Annotated的特殊形式也能识别并报出invalid-type-form参与联合Annotated | bool中Annotated不再是合法的类型表达式整体退化为Unknown | bool空元组下标Annotated[()]与list[Annotated[()]]各自报出一条invalid-type-form消息为 Special formtyping.Annotatedexpected at least 2 arguments (one type and at least one metadata element)对应类型分别为Unknown与list[Unknown]只有类型、没有元数据Annotated[int]与Annotated[(int,)]在运行时都会抛错但 ty 采用宽容策略——订阅本身照常报invalid-type-form同时把类型当作T即int处理reveal_type仍显示int。mdtest 中的注释给出了设计意图we treat it the same asTto provide better diagnostics later on为避免级联错误而保留可用类型。上述消息文本与invalid-type-form规则的对应关系可以在 diagnostic.rs 的report_invalid_arguments_to_annotated实现 中找到该函数借助INVALID_TYPE_FORMlint 构造诊断消息为 Special formtyping.Annotatedexpected at least 2 arguments (one type and at least one metadata element)而至少两个参数的检查则在parse_subscription_of_annotated_special_form中通过arguments.len() 2完成。继承正确参数化等价于继承裸类型正确参数化继承Annotated[T, ...]等价于直接继承T这一点通过reveal_mro来自ty_extensions._internal验证from typing_extensions import Annotated, Any from ty_extensions._internal import reveal_mro class C(Annotated[int, foo]): ... # revealed: (class C, class int, class object) reveal_mro(C) class D(Annotated[list[str], foo]): ... # revealed: (class D, class list[str], class MutableSequence[str], class Sequence[str], class Reversible[str], class Collection[str], class Iterable[str], class Container[Any], typing.Protocol, typing.Generic, class object) reveal_mro(D) class E(Annotated[list[E], metadata]): ... # error: [revealed-type] Revealed MRO: (class E, class list[E], class MutableSequence[E], class Sequence[E], class Reversible[E], class Collection[E], class Iterable[E], class Container[Any], typing.Protocol, typing.Generic, class object) reveal_mro(E) class F(Annotated[Any, metadata]): ... # revealed: (class F, Any, class object) reveal_mro(F)C的 MRO 为(C, int, object)与直接继承int完全一致D继承Annotated[list[str], ...]MRO 展开为list[str]的完整协议链MutableSequence[str]→Sequence[str]→Reversible[str]→Collection[str]→Iterable[str]→Container[Any]最后是typing.Protocol与typing.Generic说明 ty 将list[str]的继承结构原样保留E在元数据无关、但被注解类型内部递归引用类自身list[E]时MRO 正确解析为list[E]由于该 MRO 与测试注释中的预期不一致注释中写的是list[E]形式mdtest 以# error: [revealed-type]断言报错恰好展示了reveal_mro输出被当作可断言内容的能力F继承Annotated[Any, ...]得到(F, Any, object)Any在 MRO 中作为特殊元素显示。未参数化而裸Annotated作为基类在运行时本身就是错误ty 报出invalid-base并给出Unknown兜底from typing_extensions import Annotated from ty_extensions._internal import reveal_mro # At runtime, this is an error. # error: [invalid-base] class C(Annotated): ... reveal_mro(C) # revealed: (class C, Unknown, class object)这一用例说明在继承位置ty 对特殊形式的校验更严格——Annotated未经参数化即出现在基类列表中会触发invalid-baseMRO 中的对应槽位以Unknown填充避免推导彻底中断。底层实现从下标解析到类型推导Annotated的处理集中在crates/ty_python_semantic/src/types/infer/builder/subscript.rs与annotation_expression.rs两个文件核心调用链如下识别特殊形式推导下标对象时若发现其类型是Type::SpecialForm(SpecialFormType::Annotated)special_form.rs 中定义的符号即进入Annotated专用分支校验切片结构parse_subscription_of_annotated_special_form要求切片必须是元组否则通过report_invalid_arguments_to_annotated报invalid-type-form参数个数少于 2 同样报错推导并丢弃元数据对arguments[1..]逐个infer_expression只做推导、不取结果推导真正的类型把第一个参数交给AnnotatedExprContext::infer。该上下文枚举subscript.rs 定义区分两种场景TypeExpression调用infer_type_expression推导外层包装为KnownInstanceType::Annotated(...)AnnotationExpression调用infer_annotation_expression_impl禁用 PEP 613 别名策略并保留内层类型的限定符qualifiers。这条链路解释了为什么元数据无论如何写都不影响结果它们只是被推导一遍确保没有未定义名称等错误随后即被丢弃。如何运行与扩展这些测试annotated.md是 ty 的mdtest测试套件的一部分——任何 Markdown 文件都可以作为测试套件其中以 py 围栏代码块嵌入的文件会被写入内存文件系统并执行类型检查再通过# revealed:与# error:注释断言与诊断结果逐一匹配。测试框架的完整说明见 crates/ty_test/README.md运行入口见 crates/ty_python_semantic/tests/mdtest.rs它把resources/mdtest目录下所有 Markdown 文件注册为测试套件。常用运行方式基于 mdtest 运行器的环境变量约定# 运行整个 ty_python_semantic 的 mdtest 套件 cargo test -p ty_python_semantic --test mdtest # 只运行名字包含 annotated 的测试 MDTEST_TEST_FILTERannotated cargo test -p ty_python_semantic --test mdtest # 更新内联快照inline snapshot MDTEST_UPDATE_SNAPSHOTS1 cargo test -p ty_python_semantic --test mdtest调试时还可以设置MDTEST_GITHUB_ANNOTATIONS_FORMAT让失败信息以 GitHub Actions 注解格式输出。新增行为验证时遵循 ty 的literate测试风格crates/ty/CONTRIBUTING.md 有明确要求用一段散文说明测试动机再用reveal_type/reveal_mro与# revealed:、# error:断言写下期望结果。小结ty 对Annotated的语义可以总结为一条主线无论元数据是什么、写在何处普通注解、字符串注解、type[...]内部、继承基类Annotated都被先剥壳成其第一个类型参数再按该裸类型完成后续所有检查。唯一的例外是参数化形式不合法时少于两个参数ty 会报出invalid-type-form诊断但为了诊断连续性仍尽力推导出可用类型如把Annotated[int]当作int。这套行为既有完整的 mdtest 用例背书也有parse_subscription_of_annotated_special_form、AnnotatedExprContext、report_invalid_arguments_to_annotated等源码实现直接对应读者可以沿着 annotated.md 及同目录下的any.md、literal.md、union.md等姊妹用例继续探索 ty 对其他特殊形式的处理。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价