ARTICLE DETAIL

建站实战干货

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

Ruff 类型检查器(ty)对 attrs 库的类型推断支持:mdtest 外部依赖测试全解析

2026/9/10 18:44:33 拓冰建站 浏览量
Ruff 类型检查器(ty)对 attrs 库的类型推断支持:mdtest 外部依赖测试全解析 Ruff 类型检查器ty对 attrs 库的类型推断支持mdtest 外部依赖测试全解析【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffRuff 仓库中的ty类型检查器通过 Markdown 驱动的测试框架mdtest来验证对第三方库的类型推断能力其中 attrs.md 正是针对attrs库的专项测试套件。本文将以此文档为骨架逐一拆解其中覆盖的attr.s/attrs.define两种声明式 API、field参数init、kw_only、converter、alias的类型行为以及当前尚未支持的default装饰器限制并深入对应源码验证实现原理最后给出完整的本地运行方法。读完本文你将理解 ty 类型检查器如何处理 attrs 声明式类定义以及如何基于 mdtest 复现与扩展这些类型检查测试。一、先读懂文件本质attrs.md 是一个可执行的测试套件位于 crates/ty_python_semantic/resources/mdtest/external/attrs.md 的这份文档并非普通的技术笔记而是遵循 mdtest 格式编写的类型推断与类型检查测试套件。根据 crates/ty_test/README.md 的说明任何 Markdown 文件都可以成为测试套件其中的py代码块会被写入内存文件系统交由类型检查器检查然后通过与断言注释匹配来验证诊断结果。mdtest 支持两种核心断言本文档中两者都用到了# revealed: 类型必须与reveal_type(...)揭示出的推断类型精确一致用于验证某个表达式被推断出的类型# error: [规则码]断言在该行会产生指定规则码的诊断例如invalid-assignment类型不兼容赋值、call-non-callable对不可调用对象发起调用、missing-argument缺少必填参数。该文件处于external/子目录意味着它依赖外部第三方包。external/README.md 明确说明该目录下的测试需要使用外部包并在运行时通过uv sync --locked安装依赖后把site-packages复制进测试的内存文件系统详见 crates/ty_test/README.md 的Testing with external dependencies一节。同目录下还存放着pydantic.lock、numpy.lock、sqlalchemy.lock等锁文件attrs.lock即对应本测试。二、测试环境固定 Python 3.13 与 attrs 25.4.0文档开头的 TOML 代码块声明了本次测试的运行环境这是外部依赖测试的标准配置格式[environment] python-version 3.13 python-platform linux [project] dependencies [attrs25.4.0][environment]指定python-version3.13与python-platformlinux。对于带外部依赖的测试这两项是必填的因为它们参与包的解析过程详见 crates/ty_test/README.md[project]通过dependencies声明外部依赖官方建议锁定精确版本以保证可复现。这里固定为attrs25.4.0。对应地仓库中存放了 attrs.lock其中记录了 attrs 25.4.0 的 sdist 与 wheel 哈希、requires-python 3.13.*以及mdtest-deps虚拟包信息。锁文件确保测试在任何环境、任何 CI 运行下结果一致。三、旧式 APIattr.sattr.ib的基础类第一个测试用例覆盖 attrs 的传统legacyAPI。import attr导入的是模块名attr.s是类装饰器attr.ib()用于声明字段import attr attr.s class User: id: int attr.ib() name: str attr.ib() user User(id1, nameJohn Doe) reveal_type(user.id) # revealed: int reveal_type(user.name) # revealed: str这个用例断言了三点attr.s修饰的类可以正常实例化User(id1, nameJohn Doe)不会产生参数相关诊断说明 ty 能识别 attrs 自动生成的构造器签名字段属性按声明注解推断类型user.id被揭示为intuser.name被揭示为strattr.ib()作为字段说明符其推断类型不影响字段本身的声明类型——字段类型始终以注解为准。四、新式 APIattrs.definefield含别名alias机制第二个用例改用 attrs 现代 API并引入field(alias...)参数from attrs import define, field define class User: id: int field() internal_name: str field(aliasname) user User(id1, nameJohn Doe) reveal_type(user.id) # revealed: int reveal_type(user.internal_name) # revealed: str关键点在于alias字段在类内部的属性名是internal_name但构造器接受的参数名是name。因此实例化时传入nameJohn Doe构造参数名走 alias访问属性时使用user.internal_name内部名类型揭示为str。这验证了 ty 能正确区分构造参数名与属性名两条通道并且对 alias 后的构造调用不报missing-argument/unknown-argument类诊断。五、field参数的精细控制init、kw_only、converter第三个用例是整份文档中信息量最大的一节它用同一个Product类验证了三个field参数在类型层面的完整语义from attrs import define, field def serialize_data(data: dict[str, int]) - bytes: raise NotImplementedError define class Product: id: int field(initFalse) name: str field() price_cent: int field(kw_onlyTrue) data: bytes field(converterserialize_data, kw_onlyTrue) reveal_type(Product.__init__) # revealed: (self: Product, name: str, *, price_cent: int, data: dict[str, int]) - None p Product(nameGadget, price_cent1999, data{a: 1}) p.data {b: 2} reveal_type(p.data) # revealed: bytes p.data not a dict # error: [invalid-assignment]逐一解读field(initFalse)id不参与构造器参数。揭示出的构造器签名中只有name、price_cent、dataid被排除因此Product(name..., price_cent..., data...)的调用是合法的field(kw_onlyTrue)price_cent与data变为仅限关键字参数。注意revealed的签名里name与self之间用, *分隔*之后即为关键字专用参数——这是对kw_only最直观的类型层面呈现field(converterserialize_data)data字段的属性类型是bytes由注解决定但构造器接受的入参类型是dict[str, int]即 converter 函数的入参类型。签名中data: dict[str, int]而非bytes证明 ty 在构造器签名推导中把 converter 函数的参数类型作为实参类型赋值时的类型检查由于属性类型是bytesp.data {b: 2}这类与属性类型不符的赋值会触发invalid-assignment诊断而reveal_type(p.data)依然揭示为bytes说明赋值行为不会污染属性的推断类型。该测试同时覆盖了构造参数类型与属性类型分离推断的完整链路这也是类型检查器对声明式数据类支持中最复杂的部分之一。六、已知限制default装饰器暂不支持文档最后一节以我们目前不支持这个特性We currently do not support this明确标注了当前实现的边界from attrs import define, field define class Person: id: int field() name: str field() # error: [call-non-callable] Object of type _MISSING_TYPE is not callable id.default def _default_id(self) - int: raise NotImplementedError # error: [missing-argument] No argument provided for required parameter id person Person(nameAlice) reveal_type(person.id) # revealed: int reveal_type(person.name) # revealed: str这里呈现了两个预期内的诊断id.default报call-non-callableattrs 的default装饰器允许为字段注册默认值工厂。由于 ty 目前没有对该模式做特殊建模id.default被当作普通属性访问其推断类型是缺省值哨兵_MISSING_TYPE而_MISSING_TYPE不是可调用对象于是触发Object of type_MISSING_TYPEis not callablePerson(nameAlice)报missing-argument因为没有识别出default装饰器注册的默认值构造器签名中id仍是必填参数未提供时触发 No argument provided for required parameterid。值得注意的是尽管存在上述两个诊断person.id与person.name的属性类型仍被正确揭示为int与str说明该限制仅影响默认值注册与构造器签名推导不影响字段本身的类型推断。这是一个典型的已知缺口 期望行为测试用例——它把当前实现的不足固化为可回归的断言一旦未来实现了default装饰器支持这些# error:断言就会驱动开发者更新测试。七、源码佐证字段说明符field specifier的实现机制上述测试行为在源码中有清晰的对应实现主要集中在类型推断与调用绑定两个环节。7.1 字段说明符的类型保留设计在 crates/ty_python_semantic/src/types/infer/builder.rs 中fn should_preserve_inferred_binding_type(ty: Type_) - bool { // Dataclass field specifiers carry metadata in the inferred RHS type; replacing it with the // declared field type would lose settings like initFalse. matches!(ty, Type::KnownInstance(KnownInstanceType::Field(_))) }attr.ib()、attrs.field()这类字段说明符的推断类型需要被保留在绑定中否则initFalse、kw_only等设置在后续步骤中会丢失。同一文件的注释还披露了内部存储策略attrs uses 2 specifiers, pydantic and strawberry use 3 specifiers. SQLAlchemy uses 7 field specifiers. We could probably store more inline if this turns out to be a performance problem. For now, we optimize for memory usage.即当前只为标准库 dataclass 预留 1 个内联字段说明符槽位attrs 需要 2 个pydantic/strawberry 需要 3 个SQLAlchemy 需要 7 个——这是出于内存占用考量而有意为之的优化取舍常量NUM_FIELD_SPECIFIERS_INLINE 1。7.2 字段说明符的返回类型约定在 crates/ty_python_semantic/src/types/call/bind.rs 中注释解释了处理dataclasses.field及pydantic、attrs、SQLAlchemy等库字段说明符函数的统一策略dataclasses.fieldand field-specifier functions of commonly used libraries likepydantic,attrs, andSQLAlchemyall return the default type for the field (orAny) instead of an actualFieldinstance, even if this is not what happens at runtime... We still make use of this fact and pretend that all field specifiers return the type of the default value.也就是说尽管运行时字段说明符返回的是Field实例类型检查器仍按约定把其返回类型视为字段默认值的类型无默认值时为Any。这解释了为什么field(converter...)场景下需要额外读取 converter 参数来推导构造器入参类型——字段说明符的返回类型约定并不直接给出 converter 的入参信息。八、本地运行如何复现这些断言方式一cargo test过滤 mdtest 套件所有 Markdown 测试由 crates/ty_python_semantic/tests/mdtest.rs 中的datatest_stable::harness!驱动自动发现resources/mdtest下所有.md文件。运行全部 mdtestcargo test -p ty_python_semantic -- mdtest方式二运行带外部依赖的测试由于 attrs.md 属于外部依赖测试默认情况下这些测试可能被跳过通过MDTEST_EXTERNAL环境变量控制见 mdtest.py 中的_run_mdtest。显式启用外部依赖运行MDTEST_EXTERNAL1 cargo test -p ty_python_semantic --test mdtest -- mdtest__external该过程要求本机安装uv并位于PATH中测试框架会创建临时pyproject.toml、复制attrs.lock、执行uv sync --locked安装依赖再把虚拟环境的site-packages挂载进测试的内存文件系统crates/ty_test/README.md 相关章节。方式三Python 运行器 watch 模式仓库提供了带监视模式的 Python 运行器uv run crates/ty_python_semantic/mdtest.py -e external/其中-e/--enable-external启用外部依赖测试external/是过滤参数。运行器会监视 Markdown 与 Rust 源码变化Markdown 修改后自动重跑对应测试Rust 代码变化时自动重新编译测试再运行mdtest.py 的 watch 实现适合在开发类型检查器时做快速回归。结语通过这份 attrs.md我们可以看到 ty 类型检查器对 attrs 库支持的全貌旧式attr.s/attr.ib与新式attrs.define/field均能正确推断字段类型并推导构造器签名alias、initFalse、kw_only、converter等参数在类型层面各有精确呈现而default装饰器则被明确标记为尚未支持的已知限制并以可回归的# error:断言形式固化在测试中。配合 infer/builder.rs 与 call/bind.rs 的实现细节这类 Markdown 测试不仅是最直观的行为规范文档也是驱动类型检查器持续演进的回归基线——对理解声明式库attrs、pydantic、dataclasses的类型支持机制具有直接的参考价值。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考