ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Regression test for 3720

2026/9/10 22:04:35 拓冰建站 浏览量
Regression test for 3720 Regression test for #3720【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffRegression test for this issue.[environment] python-version 3.12from abc import ABC from typing import NamedTuple # error: [invalid-argument-type] Invalid argument to parameter 2 (bases) of type() # error: [invalid-named-tuple] is not a valid identifier T type(T, NamedTuple(T, [(, T)]), {}) T() T ABC从文档自述可以确认三件事 1. 这是 **astral-sh/ty 仓库 issue #3720 的回归测试**测试名 3720_dynamic_class_codegen_cycle 直接点明其要防御的故障模式动态类创建dynamic_class触发的**代码生成循环codegen cycle**。 2. 它使用 ty 的 **mdtest** 测试格式——Markdown 文档内嵌 toml 环境块与 py 代码块用 # error: 注释断言诊断。 3. 它运行在 **Python 3.12** 环境下因为代码用到了 type() 三参数形式、NamedTuple 的函数式构造与 ABC 重绑定。 ## 二、拆解测试代码三行代码为什么值得一条回归测试 测试代码虽然只有七行却把类型检查器置于一个极端位置。我们逐行拆解。 ### 2.1 函数式构造 NamedTuple产生一个类对象而非基类元组 py from abc import ABC from typing import NamedTuple T type(T, NamedTuple(T, [(, T)]), {}) 外层是 type(name, bases, dict) 的三参数动态类创建。Python 中 type(T, bases, namespace) 等价于一条 class T(bases): ... 语句其中 - 第一个参数 T 是类名字符串 - 第二个参数 bases 必须是**基类的可迭代对象**如 (Base,) 这样的 tuple - 第三个参数 {} 是类命名空间。 问题出在第二个参数NamedTuple(T, [(, T)])。 typing.NamedTuple 的函数式形式 NamedTuple(typename, fields) 返回的是**一个动态创建的类对象**——一个 tuple 子类字段由 fields 列表描述。也就是说这一表达式求值得到的类型是类对象而不是包含基类的元组。把它直接塞进 type() 的 bases 参数位置就构成了类型层面的错配。 type() 的调用语义在 ty 中有专门的行为测试见 [call/type.md](https://link.gitcode.com/i/6cfabfc43713f97acfc3d448428842ae)其中覆盖了单参数形式 type(1) 返回 int 的类对象以及多参数动态类创建的推断规则与之同族的 types.new_class() 动态类创建在 [call/new_class.md](https://link.gitcode.com/i/80bb9b17e87c21167396af4d9ace763b) 中有更详尽的无效调用矩阵例如 py # error: [invalid-argument-type] Invalid argument to parameter 2 (bases) of types.new_class(): Expected Iterable[object], found class Base types.new_class(Foo, Base)可以看到ty 对动态类创建中bases参数的期望类型是Iterable[object]一类的可迭代类型传入单个类对象会触发invalid-argument-type。2.2 字段名为空字符串非法标识符内层NamedTuple(T, [(, T)])的字段描述是(, T)字段名是空字符串字段类型注解是字符串T字符串形式的前向注解指向名为T的类。Python 中 NamedTuple 的字段名必须是合法标识符[A-Za-z_][A-Za-z0-9_]*空字符串显然不满足——它既无法作为属性名访问也无法被collections.namedtuple之类的底层机制接受。ty 为此专门设立了invalid-named-tuple这条 lint文档中对该 lint 的描述是 detects invalidNamedTupleclass definitions检测非法的NamedTuple类定义其声明见 types/diagnostic.rs默认级别为Error自 0.0.1-alpha.19 起稳定。2.3 名字重绑定循环的引爆点T() T ABC动态创建出的类被绑定为T之后紧接着被实例化T()然后T又被重新绑定为ABC。这三行的组合构成了测试名中codegen_cycle所指的病理条件动态类T的字段类型是字符串前向注解Tty 在代码生成codegen阶段需要把该注解解析回当前作用域中名为T的类型而T自身又是正在被 codegen 的动态类之后还被T ABC重绑定——解析目标与生成源相互依赖形成名字解析层面的循环加之bases参数类型错误、字段名非法引擎必须在错误恢复路径上处理这一循环而不能挂死、不能栈溢出、不能产生重复诊断。从测试文件命名dynamic_class_codegen_cycle与代码结构可以推断这条回归测试锁定的正是动态类 codegen 与作用域内同名重绑定相互纠缠时类型检查器必须以确定、终止的方式完成错误恢复。ty 的同类循环防御在regression/目录中还有多例如3593_function_known_decorators_cycle.md已知装饰器循环、3804_bound_typevar_default_cycle.md受约束类型变量默认值循环、3812_cyclic_generic_alias_base.md循环泛型别名基类、derived_constraint_cycles.md派生约束循环共同构成一张循环类病态输入的防御网。三、两条诊断断言的语义# error:注释如何锁定行为测试代码块中紧贴动态类创建语句的上方是两条# error:断言注释# error: [invalid-argument-type] Invalid argument to parameter 2 (bases) of type() # error: [invalid-named-tuple] is not a valid identifier T type(T, NamedTuple(T, [(, T)]), {})mdtest 的# error:语法含义是该语句必须产生对应 lint 代码、且诊断消息必须与引号内子串匹配的诊断。两条断言分别验证3.1 invalid-argument-typebases参数类型错配第一条断言要求type()调用产生invalid-argument-type诊断消息包含 Invalid argument to parameter 2 (bases) oftype()。这条 lint 的职责是 detects call arguments whose type is not assignable to the corresponding typed parameter检测实参类型不可赋值给对应形参的调用声明见 types/diagnostic.rs默认级别Error。在本例中type()的第二个形参bases期望一个可迭代的基类序列而实参NamedTuple(T, ...)是类对象tuple子类的类对象不可赋值给该形参于是触发诊断。消息模板中参数序号的措辞parameter 2 (\bases)与types.new_class 家族的消息风格完全一致说明二者共用同一套动态类创建校验逻辑。3.2 invalid-named-tuple空字符串不是合法标识符第二条断言要求同一语句产生invalid-named-tuple诊断消息包含 is not a valid identifier。这条 lint 在 types/diagnostic.rs 中声明用于检测非法的 NamedTuple 类定义。NamedTuple(T, [(, T)])的字段名不是合法标识符ty 在校验函数式 NamedTuple 定义时发现了这一点于是报告该错误。有意思的是invalid-named-tuple的诊断位置落在同一行上——因为整个NamedTuple(T, [(, T)])只是type()实参表达式的一部分ty 需要在嵌套表达式的错误恢复中同时维持两条诊断且互不干扰。四、回归测试为何必须存在类型检查器最怕挂死而不是报错对类型检查器而言病态输入的最大风险不是诊断过多而是无法终止。codegen_cycle一词指向的正是这种风险ty 在推断动态类时需要为类体生成内部表示codegen而类体中的字段类型T是字符串前向注解需要回查当前作用域作用域中T的名字正在被这条语句定义之后又被T ABC重绑定形成先使用后定义的环若引擎在 codegen 与名字解析之间缺少终止保护如深度上限、已访问标记、错误恢复截断就可能无限递归或进入指数级展开。回归测试的意义就在于把这条病态输入必须恰好产出两条诊断、且检查过程必须正常终止固化为可重复验证的行为契约。只要未来某个重构让引擎在这条输入上挂死、栈溢出或诊断数量/消息漂移mdtest 的断言与快照比对就会立刻失败把回归拦在 CI 阶段。ty 对type()调用本身同样有系统化的行为测试。call/type.md 中就有大量针对递归别名 类推断的终止性测试例如type Growing[T] T | Growing[list[T]] # error: [cyclic-type-alias-definition] def growing(value: Growing[int]): reveal_type(type(value)) # revealed: type 【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考