Ruff ty 类型检查器中的类型变量作用域规则:从 typing 规范到 mdtest 的完整实现解析
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
导读
本文聚焦于 ruff 仓库中ty类型检查器对泛型类型变量(type variable)作用域规则的实现与测试验证。以 crates/ty_python_semantic/resources/mdtest/generics/scoping.md 这份测试文档为骨架,完整覆盖了从“未绑定类型变量(unbound-type-variable)”到“类型变量遮蔽(shadowed-type-variable)”、“类型参数默认值越界(invalid-type-variable-default)”等二十余种场景,并结合ty_python_semanticcrate 的源码与 lint 定义,说明这些规则在 Rust 实现层面是如何落地、如何被诊断(diagnostic)系统报告的。读完本文,你将掌握ty对 Python typing 规范中泛型作用域的全部行为约定,并能直接阅读、运行和扩展对应的 mdtest 用例。
背景:这份文档在仓库中的角色
scoping.md位于 crates/ty_python_semantic/resources/mdtest/generics/ 目录下,属于ty_python_semanticcrate 的mdtest(Markdown 驱动测试)体系。这类文档中的每一个 Python 代码块都会被打包成独立测试用例,配合reveal_type的期望输出、# error: [diagnostic-name]注释以及内联 snapshot,直接驱动类型检查器执行并校验诊断结果。
运行这些测试的入口是 crates/ty_python_semantic/mdtest.py,它会先通过cargo test --package ty_python_semantic --test=mdtest编译测试,再调用编译产物逐个执行 Markdown 中提取的用例,并支持--enable-external、--no-snapshot-updates等参数控制外部依赖测试、lockfile 升级与 snapshot 更新。整个文档头部声明的[environment] python-version = "3.12"(在类型参数默认值章节切换为"3.13")正是为每个用例设置的解释器版本环境。
注意:文中大量测试用例来源于 PEP 695 泛型语法 与 typing 规范的 Scoping rules for type variables 章节,仓库作者在此基础上补充了更一般的规则假设(见“嵌套正式类型变量必须互异”一节)。
类型变量只能在泛型上下文使用:unbound-type-variable
基本规则
typing 规范规定:类型变量只能出现在泛型函数或泛型类的定义中。scoping.md开篇即验证了三种“越界”用法都会触发unbound-type-variable诊断:
from typing import TypeVar T = TypeVar("T") # error: [unbound-type-variable] x: T class C: # error: [unbound-type-variable] x: T def f() -> None: # error: [unbound-type-variable] x: T无论模块顶层、类体还是普通函数体内,只要类型变量没有被任何泛型作用域绑定,就属于非法使用。
源码层面的 lint 定义
该诊断在 crates/ty_python_semantic/src/types/diagnostic.rs 中声明:
declare_lint! { #[doc = include_str!("../../resources/lint_docs/unbound-type-variable.md")] pub(crate) static UNBOUND_TYPE_VARIABLE = { summary: "detects type variables used outside of their bound scope", status: LintStatus::stable("0.0.20"), default_level: Level::Error, } }其语义文档位于 crates/ty_python_semantic/resources/lint_docs/unbound-type-variable.md:检查“在未被任何泛型上下文绑定的作用域中使用类型变量”的情况,因为这种用法没有明确定义的含义。
构造函数调用必须使用已绑定的类型变量
类型检查器在解析list[T]()这类构造调用时,要求其中的类型实参必须在某个包围的泛型作用域中已绑定。特别地:
- 赋值不引入泛型上下文:
items = list[T]()同样是错误; - 嵌套类型实参遵循同一规则:
list[list[T]]()中内层T同样越界; - 自定义泛型类同样受限:
Box[T]()(Box(Generic[T]))也会报unbound-type-variable。
from typing import Generic, TypeVar T = TypeVar("T") # error: [unbound-type-variable] list[T]() # error: [unbound-type-variable] items = list[T]() # error: [unbound-type-variable] list[list[T]]() class Box(Generic[T]): ... # error: [unbound-type-variable] Box[T]()作为对照,泛型函数和泛型类可以使用自己的类型变量来调用构造函数,两种语法(legacyTypeVar与 PEP 695[T])行为一致:
def make(value: T) -> list[T]: result = listT reveal_type(result) # revealed: list[T@make] return result class Factory(Generic[T]): def make(self, value: T) -> list[T]: return listT def modernT -> list[T]: return listT值得注意的是reveal_type(result) # revealed: list[T@make]:这里T@make表示该类型变量绑定于make函数的泛型上下文,是ty检查器用来区分不同绑定实例的命名约定。
例外:类型别名赋值与类基列表可以引入泛型上下文
Alias = list[T] reveal_type(Alias[int]()) # revealed: list[int] Alias() class Derived(list[T]): ...Alias = list[T]这条类型别名赋值会引入一个携带T的泛型上下文,因此Alias[int]()与不带显式类型实参的Alias()都合法;class Derived(list[T])同理。这与“赋值给变量不引入泛型上下文”形成对比——区别在于别名赋值产生的是类型级绑定,而不是值级绑定。
存根(.pyi)中的一致行为
scoping.md用独立的pyi代码块验证:存根文件里的构造调用遵循与.py源码完全相同的规则,list[T]()和items = list[T]()在.pyi中同样报unbound-type-variable。
类型变量的推断实例化语义
Legacy 语法:同一类型变量可被多次推断为不同类型
A type variable used in a generic function could be inferred to represent different types in the same code block.
legacyTypeVar语法下,同一个模块级T可以被不同函数分别绑定,互不影响:
from typing import TypeVar T = TypeVar("T") def f1(x: T) -> T: return x def f2(x: T) -> T: return x f1(1) f2("a")这里不产生任何诊断:f1的T与f2的T在推断时是两个独立的实例化。
同一函数多次调用:每次调用独立实例化
这一规则也适用于同一个泛型函数被多次调用的情况——每次调用都把类型变量实例化为不同的具体类型:
def fT -> T: return x reveal_type(f(1)) # revealed: Literal[1] reveal_type(f("a")) # revealed: Literal["a"]PEP 695 语法下,f(1)推断出Literal[1]、f("a")推断出Literal["a"],证明类型变量在每次调用时被独立地求解。
类方法中的类型变量绑定语义
方法可以提及类的类型变量
A type variable used in a method of a generic class that coincides with one of the variables that parameterize this class is always bound to that variable.
类C[T]的方法中出现的T恒等于类的类型变量,由接收者(receiver)决定:
class C[T]: def m1(self, x: T) -> T: return x def m2(self, x: T) -> T: return x c: C[int] = C[int]() c.m1(1) c.m2(1) # error: [invalid-argument-type] "Argument to bound method `C.m2` is incorrect: Expected `int`, found `Literal["string"]`" c.m2("string")当c被注解为C[int]后,c.m2("string")被精确诊断为“期望int,实际为Literal["string"]”,说明T已随类特化被固定为int。
把已绑定的类类型变量传给更宽泛的参数
类型检查器必须遵守一条关键规则:类类型变量由接收者固定,不得从参数注解反向推断出新的特化。以带 bound 的类型变量为例:
class G[T: int]: def takes_object(self, value: object) -> None: ... def echo(self, value: T) -> T: return value def caller(self, value: T, other: "G[int]") -> None: self.takes_object(value) other.takes_object(value) reveal_type(self.echo(value)) # revealed: T@G # error: [invalid-argument-type] "Expected `int`" other.echo("bad") def explicit_receiver(self: "G[T]", value: T) -> None: self.takes_object(value)把T值传给object参数是合法的(类型变量值当然可以赋值给object),但这不意味着从takes_object的参数注解推导出什么新信息;self.echo(value)的结果仍保持T@G的符号形式。同时other: "G[int]"已被特化为int,因此other.echo("bad")报错。explicit_receiver(self: "G[T]", value: T)则展示了通过显式Self注解绑定T的写法同样成立。
相同的规则适用于classmethod与__contains__触发的成员测试:
class Container[T: int]: @classmethod def takes_object(cls, value: object) -> None: ... def __contains__(self, value: object) -> bool: return False def caller(self, value: T) -> None: self.takes_object(value) self.__contains__(value) reveal_type(value in self) # revealed: bool扩展到 bound 的父类、约束类型变量与 legacy 语法
scoping.md进一步验证了三个推广场景:
(1)参数不必是object,bound 的任意父类都接受其值:
class Base: ... class Child(Base): ... class G[T: Child]: def takes_base(self, value: Base) -> None: ... def caller(self, value: T) -> None: self.takes_base(value)(2)约束(constrained)类型变量:每个允许的特化都可赋值给object,而无需在方法调用处重新挑选某个约束:
class G[T: (int, str)]: def takes_object(self, value: object) -> None: ... def echo(self, value: T) -> T: return value def caller(self, value: T) -> None: self.takes_object(value) reveal_type(self.echo(value)) # revealed: T@G(3)legacy 类类型变量遵循同样的规则:
from typing import Generic, TypeVar T = TypeVar("T", bound=int) class G(Generic[T]): def takes_object(self, value: object) -> None: ... def caller(self, value: T) -> None: self.takes_object(value)泛型类上的函数是描述符:特化贯穿描述符协议
这一节在 crates/ty_python_semantic/resources/mdtest/call/methods.md 的“函数即描述符”测试基础上,重复到泛型类上,以确认特化信息能完整地穿过描述符协议(self参数正是通过它绑定到实例方法的):
from inspect import getattr_static class C[T]: def f(self, x: T) -> str: return "a" reveal_type(getattr_static(C[int], "f")) # revealed: def f(self, x: int) -> str reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper '__get__' of function 'f'> reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: def f(self, x: int) -> str # revealed: bound method C[int].f(x: int) -> str reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int])) reveal_type(C[int].f) # revealed: def f(self, x: int) -> str reveal_type(C[int]().f) # revealed: bound method C[int].f(x: int) -> str bound_method = C[int]().f reveal_type(bound_method.__self__) # revealed: C[int] reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str reveal_type(C[int]().f(1)) # revealed: str reveal_type(bound_method(1)) # revealed: str # error: [invalid-argument-type] "Argument to function `C.f` is incorrect: Argument type `Literal[1]` does not satisfy upper bound `C[int]` of type variable `Self`" C[int].f(1) # error: [missing-argument] reveal_type(C[int].f(C[int](), 1)) # revealed: str class DU: pass reveal_type(D[int]().f) # revealed: bound method D[int].f(x: int) -> str这里有几个值得注意的实现细节:
C[int].f是未绑定方法(def f(self, x: int) -> str),C[int]().f是绑定方法(bound method C[int].f(x: int) -> str);getattr_static系列调用验证了在不触发描述符协议的情况下特化信息依然保留;C[int].f(1)同时触发两个诊断:missing-argument(缺self实参)与invalid-argument-type(Literal[1]不满足Self的上界C[int]),这与Self类型变量有关;- 子类
DU特化为D[int]后,继承的方法f正确显示为bound method D[int].f(x: int) -> str。
方法可以提及其他类型变量:方法的泛型作用域
A type variable used in a method that does not match any of the variables that parameterize the class makes this method a generic function in that variable.
legacy 语法下,方法签名里不属于类的类型变量会使该方法在该变量上成为泛型函数:
from typing import TypeVar, Generic T = TypeVar("T") S = TypeVar("S") class Legacy(Generic[T]): def m(self, x: T, y: S) -> S: return y legacy: Legacy[int] = Legacy[int]() reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]关键点是:方法签名中的类类型变量T不会绑定新的实例——它在类被特化时(legacy: Legacy[int])已经被求解。仓库通过ty_extensions._internal.generic_context暴露了“泛型上下文”的内部表示来验证这一点:
from ty_extensions._internal import generic_context legacy.m("string", None) # error: [invalid-argument-type] reveal_type(legacy.m) # revealed: bound method Legacy[int].mS -> S # revealed: ty_extensions._internal.GenericContext[T@Legacy] reveal_type(generic_context(Legacy)) # revealed: ty_extensions._internal.GenericContext[Self@m, S@m] reveal_type(generic_context(legacy.m))generic_context(Legacy)显示类Legacy的泛型上下文只包含T@Legacy;generic_context(legacy.m)则显示该绑定方法的上下文为Self@m, S@m——即只有方法自身引入的Self与S,类类型变量T已通过特化被替换掉。
PEP 695 语法下这一点更清晰——方法使用独立的类型变量:
class C[T]: def mS -> S: return y c: C[int] = C() reveal_type(c.m(1, "string")) # revealed: Literal["string"]未绑定类型变量不应出现在函数体与类体中
Unbound type variables should not appear in the bodies of generic functions, or in the class bodies apart from method definitions.
legacy 语法下:
from typing import TypeVar, Generic T = TypeVar("T") S = TypeVar("S") def f(x: T) -> None: x: list[T] = [] # error: [unbound-type-variable] y: list[S] = [] class C(Generic[T]): # error: [unbound-type-variable] x: list[S] = [] # This is not an error, as shown in the previous test def m(self, x: S) -> S: return xf体内的x: list[T]合法(T已绑定),但y: list[S]中的S不属于f的泛型上下文,报错;类体属性x: list[S]同理,但方法签名中的S合法——因为方法在S上是独立的泛型函数。
PEP 695 语法行为一致,只是定义“未绑定类型变量”时仍需借助 legacy 语法(PEP 695 的类型变量不能出现在非泛型作用域中):
from typing import TypeVar S = TypeVar("S") def fT -> None: x: list[T] = [] # error: [unbound-type-variable] y: list[S] = [] class C[T]: # error: [unbound-type-variable] x: list[S] = [] def m1(self, x: S) -> S: return x def m2S -> S: return x注意m1(legacy 隐式泛型方法)与m2[S](显式泛型方法)都被允许:前者是“方法可提及其他类型变量”规则的体现,后者则用显式语法声明了独立的泛型上下文。
未决问题:Callable注解是否创建隐式泛型上下文
typing 规范尚未解决、各类型检查器之间也存在分歧的一个场景是:Callable[[T], T]这样的注解是否自动创建隐式泛型上下文?ty的当前实现不对以下片段报错,但作者明确注释“未来可能改变”:
from typing import TypeVar, Callable from ty_extensions._internal import generic_context T = TypeVar("T") x: Callable[[T], T] = lambda obj: obj # TODO: if we decide that `Callable` annotations always create an implicit generic context, # all of these revealed types and `invalid-argument-type` diagnostics are incorrect. # If we decide that they do not, we should emit `unbound-type-variable` on both the # declaration of `x` in the global scope and the parameter annotation of `y`. # # NOTE: all the `reveal_type`s are inside a function here so that we test the behaviour # of the declared type (from the annotation) rather than the local inferred type def test(y: Callable[[T], T]): # revealed: None reveal_type(generic_context(x)) # revealed: (TypeVar, /) -> TypeVar reveal_type(x) # error: [invalid-argument-type] # revealed: TypeVar reveal_type(x(42)) # revealed: None reveal_type(generic_context(y)) # revealed: (T@test, /) -> T@test reveal_type(y) # error: [invalid-argument-type] # revealed: T@test reveal_type(y(42))代码块内的TODO注释给出了两种可能走向:如果Callable注解总是创建隐式泛型上下文,那么上面的revealed类型与invalid-argument-type诊断全都是错误的;反之则应同时为全局作用域x的声明和y的参数注解报unbound-type-variable。测试特意把reveal_type放进函数体,是为了验证“注解声明的类型”而非局部推断类型。目前generic_context(x)与generic_context(y)都揭示为None,说明实现暂不把Callable[[T], T]当作泛型上下文。
嵌套正式类型变量必须互异:shadowed-type-variable
泛型函数与泛型类可以互相嵌套,但同一个类型变量不能用于嵌套的泛型定义中。typing 规范只明确提到了两种具体形态:
A generic class definition that appears inside a generic function should not use type variables that parameterize the generic function.
A generic class nested in another generic class cannot use the same type variables.
仓库作者注明:我们假设更一般的形式成立,即嵌套泛型定义(无论是函数还是类)都不得使用外层绑定的类型变量。触发该规则的 lint 是SHADOWED_TYPE_VARIABLE,在 crates/ty_python_semantic/src/types/diagnostic.rs 中声明(stable0.0.20,默认Level::Error),语义文档见 crates/ty_python_semantic/resources/lint_docs/shadowed-type-variable.md。
泛型函数嵌套泛型函数
def fT -> None: def okS -> None: ... # error: [shadowed-type-variable] def badT -> None: ...内层使用新类型变量S合法,复用外层T则报错。
泛型 TypeVarTuple 嵌套
同样规则适用于可变长类型参数元组*Ts:
def outer*Ts -> None: def ok*Us -> None: ... # snapshot: shadowed-type-variable def bad*Ts -> None: ...文档内嵌的 snapshot 展示了实际诊断输出:
error[shadowed-type-variable]: Generic function `bad` uses TypeVarTuple `Ts` already bound by an enclosing scope --> src/mdtest_snippet.py:5:9 | 5 | def bad*Ts -> None: ... | ^^^ `Ts` used in function definition here | ::: src/mdtest_snippet.py:1:5 | 1 | def outer*Ts -> None: | ------------------------------ TypeVarTuple `Ts` is bound in this enclosing scope可以看到ty的诊断不仅指出违规位置,还会用:::次级标注指出外层绑定该类型变量的作用域。
泛型方法嵌套泛型类
class C[T]: def okS -> None: ... # error: [shadowed-type-variable] def badT -> None: ...泛型类嵌套泛型函数
from typing import Iterable def fT -> None: class Ok[S]: ... # error: [shadowed-type-variable] class Bad1[T]: ... # error: [shadowed-type-variable] class Bad2(Iterable[T]): ...注意Bad2(Iterable[T])的基类列表里出现外层T同样是错误。
泛型类嵌套泛型类
from typing import Iterable class C[T]: class Ok1[S]: ... # error: [shadowed-type-variable] class Bad1[T]: ... # error: [shadowed-type-variable] class Bad2(Iterable[T]): ...例外:基类恰好与外层类型参数同名
一个微妙的边界情况:嵌套泛型类继承的泛型基类恰好带有与外层作用域类型参数同名的类型参数时,只要嵌套类只使用自己的类型参数,就不应报错:
class Base[T]: pass class Outer[T]: class InnerU: pass这里Base[U]使用的是Inner自己的U,Base的T与外层Outer的T只是恰好同名,互不相关。但直接引用外层类型变量依然非法:
class Outer[T]: # error: [shadowed-type-variable] class Bad(list[T]): ...类基列表在类型参数作用域内求值
类基列表的求值发生在该类的类型参数作用域之内。这意味着基类位置引用的名称可能被解析为类型变量(在声明之后),而不会引用到尚未定义的类名:
class C_T: ... # `D` in `list[D]` is resolved to be a type variable of class `D`. class DD: ... # error: [unresolved-reference] "Name `E` used when not defined" if E: class E_T: ... # error: [unresolved-reference] "Name `F` used when not defined" F # error: [unresolved-reference] "Name `F` used when not defined" class F_T: ... def foo(): class G_T: ... # error: [unresolved-reference] "Name `H` used when not defined" if H: class H_T: ...要点:
class C_T中基类C是在类定义完成前被引用的,因此解析为未定义名称,报unresolved-reference;class DD是合法的:基类列表中的D被解析为类D自己的类型变量(它此时已经在类型参数作用域内绑定),list[D]即list[D@D];- 函数体内定义的嵌套类遵循相同规则(
G、H在定义完成前引用自身都报未定义名称)。
类作用域不覆盖内部作用域
与普通符号一样,泛型类的类型变量只在该类的作用域内可用,不会泄漏到嵌套作用域:
class C[T]: ok1: list[T] = [] class Bad: # error: [unbound-type-variable] bad: list[T] = [] class Inner[S]: ... ok2: Inner[T]类体属性ok1: list[T]与ok2: Inner[T]合法,但嵌套类Bad的属性使用外层T报unbound-type-variable。
类型参数默认值不得引用外层作用域类型参数:invalid-type-variable-default
按 typing 规范,类型参数的默认值不得引用外层作用域的类型参数。本节环境切换到python-version = "3.13"(PEP 695 默认值语法在 3.13 才可用)。类类型参数上越界的默认值已由invalid-generic-class诊断覆盖,本节只覆盖PEP 695 函数/类型别名作用域以及legacyTypeVar用于函数/方法签名的剩余场景。对应 lint 在 crates/ty_python_semantic/src/types/diagnostic.rs 中声明(stable0.0.16,默认Level::Error)。
嵌套函数
def outer[T](): # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" def inner[U = T](): ... def ok[U = int](): ... # OKinner[U = T]中U的默认值引用了外层outer的T,报错;U = int合法。
类中嵌套函数
class C[T]: # error: [invalid-type-variable-default] def fU = T: ... def gU = int: ... # OK类中嵌套类型别名
class C[T]: # error: [invalid-type-variable-default] type Alias[U = T] = list[U] type Ok[U = int] = list[U] # OKlegacy TypeVar 用于方法、外层有类类型变量
from typing import TypeVar, Generic T1 = TypeVar("T1") T2 = TypeVar("T2", default=T1) class Foo(Generic[T1]): # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `T1`" def method(self, x: T2) -> T2: return xlegacy 类型变量T2的默认值引用了类Foo的类型变量T1,同样被拒绝。
legacy TypeVar 用于嵌套函数
from typing import TypeVar, Generic T = TypeVar("T") U = TypeVar("U", default=T) def outer(x: T) -> T: # error: [invalid-type-variable-default] def inner(y: U) -> U: return y return x默认值引用后声明的类型变量
legacy 类型变量的默认值只能引用先声明的类型变量:
from typing import TypeVar, Generic T = TypeVar("T", default=int) U = TypeVar("U", default=T) # error: [invalid-type-variable-default] def bad(y: U, z: T) -> tuple[U, T]: return y, z # OK, because the typevar with the default comes after the one without def fine(y: T, z: U) -> tuple[U, T]: return z, ybad中U的默认值T出现在T的声明之前(签名顺序y: U, z: T),报错;fine中T先声明、U后声明,合法。
legacy 类型变量顺序:带默认值的不得排在不带默认值的前面
from typing import TypeVar T1 = TypeVar("T1", default=int) T2 = TypeVar("T2") T3 = TypeVar("T3") DefaultStrT = TypeVar("DefaultStrT", default=str) # error: [invalid-type-variable-default] def f(x: T1, y: T2) -> tuple[T1, T2]: return x, y # error: [invalid-type-variable-default] def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: return x, y, z # error: [invalid-type-variable-default] def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: return x, y, z, w def ok(x: T2, y: T1) -> tuple[T2, T1]: return x, y def ok2(x: T1, y: DefaultStrT) -> tuple[T1, DefaultStrT]: return x, y规则是:带默认值的类型变量必须排在无默认值类型变量的后面。ok(T2无默认值在前、T1有默认值在后)与ok2(T1、DefaultStrT均有默认值)合法;f、g、h均因出现“默认值变量排在无默认值变量之前”而报错——这也与 Python 函数参数中默认参数的排序约束一致。
混合作用域类型参数
方法可以同时拥有方法自身作用域的类型参数与外层类的类型参数,两者在签名中共存。ty_extensions._internal.into_regular_callable用于把绑定方法转换成普通可调用对象以便揭示其完整签名:
from typing import Generic, TypeVar from ty_extensions._internal import into_regular_callable T = TypeVar("T") S = TypeVar("S") class Foo(Generic[T]): def bar(self, x: T, y: S) -> tuple[T, S]: raise NotImplementedError def f(x: type[Foo[T]]) -> T: # revealed: S -> tuple[T@f, S] reveal_type(into_regular_callable(x.bar)) raise NotImplementedError揭示结果显示:bar的T被绑定为函数f泛型上下文中的T@f(因为x: type[Foo[T]]特化了Foo),而S保持为bar方法自身的类型参数,用[S]前缀标注——这正是一个“混合作用域”类型参数的完整形态。
如何运行这些测试
如果你希望亲手运行scoping.md中的用例,可以借助 crates/ty_python_semantic/mdtest.py 提供的测试运行器。它支持按路径过滤,例如:
# 在仓库根目录执行,运行 generics 目录下的 scoping 测试 uv run crates/ty_python_semantic/mdtest.py generics/scoping.md该脚本的核心流程(见MDTestRunner类)为:
- 以
cargo test --package ty_python_semantic --test=mdtest编译测试(先以human消息格式检查编译错误,再以json格式定位 mdtest 可执行文件路径); - 执行编译产物,测试名形如
mdtest::generics/scoping.md,支持--exact精确匹配; - 通过环境变量控制行为:
MDTEST_EXTERNAL(是否启用外部依赖测试,对应 CLI--enable-external)、MDTEST_UPGRADE_LOCKFILES(对应--no-lockfile-upgrades)、MDTEST_UPDATE_SNAPSHOTS(对应--no-snapshot-updates); - 无参数运行时进入
watch模式:监听ty_python_semantic、ty_vendored、ty_test/src、mdtest/src等目录的变更,Rust 源码或 vendored typeshed 变更后自动重编译并重跑测试,.md变更则只重跑受影响的用例;删除被拒绝的.snap.new快照文件也会触发对应用例重跑。
scoping.md中同时使用了几种测试标注语法:# error: [diagnostic-name]内联断言、# revealed: ...的reveal_type期望输出、<!-- snapshot-diagnostics -->触发的诊断快照,以及内嵌snapshot代码块(如 TypeVarTuple 遮蔽一节所示)。每段代码块对应一个独立测试,因此这份 Markdown 文档本身就是ty泛型作用域行为的可执行规范。
总结:从规范到实现的作用域规则全景
回顾全文,ty对类型变量作用域的处理可以归纳为以下几条核心原则:
- 绑定即合法:类型变量只能在绑定它的泛型函数/类(或其方法、基类列表、类型别名赋值)中使用,否则报
unbound-type-variable; - 特化即固定:类类型变量由接收者特化后不再重新推断,向
object、bound 父类或约束的任意特化赋值都不改变其绑定(T@Class符号保持不变),诊断等级默认Error(见 diagnostic.rs); - 嵌套即隔离:嵌套泛型定义必须使用新的类型变量,复用外层变量报
shadowed-type-variable,但基类“恰好同名”的参数不受牵连(见 shadowed-type-variable.md); - 默认值受序约束:类型参数默认值不得引用外层作用域或后声明的类型变量,带默认值的变量不得排在不带默认值的变量之前,违者报
invalid-type-variable-default(见 invalid-type-variable-default.md); - 场景求值有序:类基列表在类型参数作用域内求值、类作用域不覆盖内部作用域、
Callable注解的隐式泛型上下文问题仍为未决事项。
这些规则在ty_python_semanticcrate 的类型推断与诊断系统中(src/types/、src/types/generics.rs)有完整的实现支撑,而 scoping.md 则以可执行测试的形式把它们固化下来——既是一份规范文档,也是ty泛型检查行为最权威的参考。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考