尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Ruff 回归测试剖析:用 `RegularCallableTypeOf` 与 `into_regular_callable` 验证可调用类型赋值性

Ruff 回归测试剖析:用 `RegularCallableTypeOf` 与 `into_regular_callable` 验证可调用类型赋值性 Ruff 回归测试剖析用RegularCallableTypeOf与into_regular_callable验证可调用类型赋值性【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读本文以 Ruff 仓库中 ty 类型检查器的回归测试文档 3020_callable_type_of.md 为主线深入讲解ty_extensions内部提供的TypeOf、RegularCallableTypeOf特殊形式以及into_regular_callable转换函数的用途与实现原理。读完本文你将掌握为什么typing.Callable[...]与函数对象签名之间的类型关系需要专门提取如何通过static_assertis_assignable_to/is_subtype_of在类型检查期断言该关系以及 Ruff 源码中CallableTypeKind如何区分“函数式可调用”与“普通可调用”。背景issue #3020 与回归测试的由来该回归测试文档标题为 Regression test for #3020对应 ty 项目Ruff 仓库内的类型检查器组件issue #3020。文档的定位非常明确它不是一个功能教程而是一段可被 mdtest 框架执行的 Python 测试代码用于防止某个已修复的类型推断 bug 在未来回归。在 Ruff 仓库中这类回归测试被放在 crates/ty_python_semantic/resources/mdtest/regression/ 目录下由 mdtest 机制运行文档中的 Python 代码片段会被真正解析、推断类型并与期望结果比对。文档开头没有冗长的背景描述直接给出可执行代码——这正是回归测试文档的典型形态用最小可复现用例锁定一个曾经出错的行为。该测试的核心诉求是验证当普通函数f的签名与typing.Callable[[int, str], None]完全一致时类型系统能否正确判定二者存在子类型/赋值关系。看似简单但其中涉及 ty 内部对“函数式可调用function-like callable”与“普通可调用regular callable”的区分这正是 #3020 所暴露问题的根源。测试用例逐行拆解回归测试文档正文是一个完整的 Python 文件我们先看导入部分from typing import Callable from ty_extensions import static_assert from ty_extensions._internal import ( RegularCallableTypeOf, TypeOf, is_assignable_to, is_subtype_of, into_regular_callable, )涉及四类能力导入项来源模块作用Callabletyping标准库构造Callable[[int, str], None]这样的可调用类型注解static_assertty_extensions公开 API在类型检查期对布尔表达式做静态断言表达式为False时直接报类型错误TypeOf/RegularCallableTypeOfty_extensions._internal特殊形式用于从表达式提取其推断出的类型is_assignable_to/is_subtype_ofty_extensions._internal类型谓词返回Literal[True]/Literal[False]into_regular_callablety_extensions._internal运行时转换函数把可调用对象规整为普通可调用类型被测试的目标函数def f(a: int, b: str, /) - None: ...注意这里的/a、b都是仅限位置参数positional-only。函数体为空...返回类型为None。随后是四条静态断言static_assert(is_assignable_to(Callable[[int, str], None], RegularCallableTypeOf[f])) static_assert(is_subtype_of(Callable[[int, str], None], RegularCallableTypeOf[f])) static_assert(is_assignable_to(Callable[[int, str], None], TypeOf[into_regular_callable(f)])) static_assert(is_subtype_of(Callable[[int, str], None], TypeOf[into_regular_callable(f)]))四条断言两两成对前两条针对RegularCallableTypeOf[f]后两条针对TypeOf[into_regular_callable(f)]。它们分别从类型提取与运行时转换两条路径验证同一结论Callable[[int, str], None]可以赋值给 / 是f的规范化后可调用类型。为什么要区分CallableTypeOf与RegularCallableTypeOf仅看这段回归测试容易误以为RegularCallableTypeOf[f]与CallableTypeOf[f]等价。事实上二者在 ty 的类型系统中语义不同。仓库文档 crates/ty_python_semantic/resources/mdtest/ty_extensions.md 的 CallableTypeOf 与 RegularCallableTypeOf 两节给出了权威解释CallableTypeOf提取可调用对象“所承载的可调用类型”外部可见签名保留函数式行为——方法、描述符、可调用实例等仍与普通函数在类型理论上保持区别RegularCallableTypeOf同样提取可调用类型但会把结果规整为普通typing.Callable风格的类型保留签名丢弃函数式/描述符语义。该文档中还给出了一个与回归测试互为镜像的对照用例def f(x: int, /) - None: ... static_assert(not is_assignable_to(Callable[[int], None], CallableTypeOf[f])) static_assert(is_assignable_to(Callable[[int], None], RegularCallableTypeOf[f]))同样是签名完全一致的函数fCallableTypeOf[f]由于保留了函数式行为不被判定为可赋值给普通Callable[[int], None]而RegularCallableTypeOf[f]规整后则可以。回归测试 #3020 选择后者的原因正在于此它要验证的正是“普通Callable[...]与规整后函数签名”这一层干净的关系。从源码看RegularCallableTypeOf的实现在 ty 语义层TypeOf、CallableTypeOf、RegularCallableTypeOf被建模为特殊形式Special Form定义位于 crates/ty_python_semantic/src/types/special_form.rs如第 93-98 行所示三者并列声明且仅允许来自ty_extensions._internal模块。类型表达式求值时crates/ty_python_semantic/src/types/infer/builder/type_expression.rs 的第 2446-2511 行统一处理CallableTypeOf与RegularCallableTypeOf要求恰好一个类型参数否则报invalid-type-formSpecial form ... expected exactly 1 type argument, got N对参数表达式做普通表达式推断而非类型表达式推断——因为它期望的是“一个可调用对象”这样的运行时值通过try_upcast_to_callable_with_recursive_fallback把对象类型提升为可调用类型集合支持递归定义避免自引用RegularCallableTypeOf[f]之类的循环引用导致栈溢出关键分叉若当前特殊形式是RegularCallableTypeOf则对每个可调用类型调用into_regular(db)CallableTypeOf则原样保留。其中的into_regular定义在 crates/ty_python_semantic/src/types/callable.rs 第 775-777 行pub(crate) fn into_regular(self, db: db dyn Db) - CallableTypedb { self.with_kind(db, CallableTypeKind::Regular) }即保留参数签名与返回类型仅把CallableTypeKind改写为Regular。而CallableTypeKind的FunctionLike/StaticMethodLike/ClassMethodLike等“函数式”变体见同文件is_method_like等方法的匹配逻辑正是让CallableTypeOf[f]与普通Callable[...]无法直接兼容的根源。into_regular正是那个“抹平函数式语义”的动作。into_regular_callable运行时转换的底层实现回归测试的后两条断言没有直接使用RegularCallableTypeOf[f]而是先执行into_regular_callable(f)再用TypeOf[...]提取其结果类型static_assert(is_assignable_to(Callable[[int, str], None], TypeOf[into_regular_callable(f)])) static_assert(is_subtype_of(Callable[[int, str], None], TypeOf[into_regular_callable(f)]))into_regular_callable是ty_extensions._internal提供的运行时函数。在 ty 内部它被注册为“已知函数”KnownFunction枚举定义位于 crates/ty_python_semantic/src/types/function.rs 第 2337-2338 行IntoRegularCallable并且check_module要求其必须来自ty_extensions._internal模块同文件第 2441-2455 行的匹配分支。调用求值逻辑在 crates/ty_python_semantic/src/types/call/bind.rs 第 2387-2406 行当识别到IntoCallable/IntoRegularCallable时取出唯一的参数类型调用try_upcast_to_callable提升为可调用类型若是IntoRegularCallable同样对每个可调用调用into_regular(db)然后把结果设为返回类型。可见into_regular_callable(f)的类型 把f提升为可调用类型并规整为Regular的结果RegularCallableTypeOf[f]的结果 把表达式f的类型提升为可调用并规整为Regular的结果。二者殊途同归——这正是回归测试同时覆盖两条路径的原因既要验证“特殊形式提取”路径也要验证“运行时转换 TypeOf提取”路径任何一条在类型推断上回归四条断言都会失败。TypeOf本身的语义在 crates/ty_python_semantic/src/types/infer/builder/type_expression.rs 第 2384-2413 行有体现它使用infer_expression表达式推断而非infer_type_expression类型表达式推断从而拿到的是表达式在运行时求值后的类型而不是表达式文本的“类型解释”。这也是为什么TypeOf[into_regular_callable(f)]能得到函数调用的返回类型而非把into_regular_callable(f)当作类型构造器去解析。谓词与断言机制is_assignable_to、is_subtype_of与static_assert回归测试的可执行性依赖两套机制1. 类型谓词Type Predicatesis_assignable_to(A, B)与is_subtype_of(A, B)都是ty_extensions._internal中“以函数形式实现、返回Literal[True]或Literal[False]”的谓词详见 crates/ty_python_semantic/resources/mdtest/ty_extensions.md Type predicates 一节。二者的差别在于子类型关系考察的是“值的集合”之间的包含关系例如bool是int的子类型而赋值性还额外考虑双向放宽、Any通配等运行时赋值场景例如Any可以赋给任何类型。回归测试对同一结论同时使用两个谓词说明 #3020 的修复要求这两种关系判定都要正确。2. 静态断言static_assertstatic_assert(expr)是ty_extensions的公开 API接受任意表达式在类型检查期验证其是否为静态已知的真值若求值为False直接产生static-assert-error诊断诊断快照见 crates/ty_python_semantic/resources/mdtest/ty_extensions.md Diagnostic snapshots 一节例如Static assertion error: argument evaluates to \False。在回归测试中四条static_assert全部通过意味着类型检查器必须能在编译期算出Callable[[int, str], None]对RegularCallableTypeOf[f] 的赋值性/子类型关系为真。测试的价值与可复现方式这段回归测试虽然只有 22 行却在守护 ty 类型系统中一个精细而关键的角落锁定 #3020 的行为契约签名一致的普通函数必须能规整为普通Callable[...]类型且RegularCallableTypeOf与into_regular_callable两条提取路径结果一致约束函数式语义边界CallableTypeOf与RegularCallableTypeOf的差异在 crates/ty_python_semantic/resources/mdtest/ty_extensions.md 的对照断言中被进一步固化双重路径覆盖特殊形式提取与运行时转换缺一不可防止任何一侧在重构中悄悄偏离。仓库内同类回归与特性测试还散落在 crates/ty_python_semantic/resources/mdtest/ 的多个子目录type_properties/、annotations/、generics/等并辅以 crates/ty_python_semantic/resources/corpus/ty_extensions.py 这类语法语料做支撑。读者若想在本地复现该测试可进入 Ruff 仓库根目录通过 ty 组件现有的 mdtest 测试入口crates/ty_python_semantic与crates/ruff_mdtest的相关测试运行回归目录下的文档用例观察四条静态断言是否全部通过也可以把RegularCallableTypeOf[f]换成CallableTypeOf[f]自行对照体会函数式语义带来的差异——这正是理解 ty 可调用类型建模的最佳实验。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表