为 YACS 配置开启类型检查和自动补全
通过type stub为yacs配置提供IDE类型信息和代码补全
Motivation
作为从强类型语言入门的开发者,笔者在编写 Python 代码时总是尽可能添加类型标注,并习惯于随时获得类型提示(实质上是自动补全)。在 IDE 无法提供类型信息的代码区域,笔者不得不反复确认类型的正确性。
笔者曾吐槽过 yacs:一个两年未更新的项目,为何有如此多的深度学习项目仍在采用。不更新或许可以接受,但一个在 2022 年仍不支持类型标注与自动补全的配置系统,是否确实已经落后?
如果由笔者来编写深度学习项目的配置系统,绝不会选择 yacs。本科毕设时使用的是 omegaconf,实际体验也相差无几。如今笔者更倾向于使用 pydantic,但尚缺乏机会。在学术研究层面,深度学习项目通常参考已有实现,鲜有人从零开始重新编写配置系统。在此背景下,也就不难理解为何一个功能平凡且体验欠佳的库能够流行至今。
上午通过一个小技巧解决了上述问题。代码已开源并打包上传至 PyPI,本文介绍其思路,分享给有类似需求的读者。
思路
yacs 的"嵌套节点-赋值"语法决定了它无法被 type checker 解析。参考 pydantic,层级式配置应当按"嵌套类-属性"的方式管理。由此可以建立一套转换关系:配置节点对应类,配置项对应类的属性。利用 yacs 特有的默认值逻辑,可以从默认值推导出属性的类型标注。
_C.LOGDIR = 'results'
# 可以转换为
class RootClass:
LOGDIR: str # type('results') is str
_C: RootClass
因此可以遍历给定配置,通过上述转换生成一个 stub file,放置在默认配置的旁边。由于 stub file 在类型解析中优先级高于原文件,IDE 会完全采纳生成的类型标注,而不会感知到其背后的模拟逻辑。
此时导入 cfg 即可获得类型提示与自动补全。但仍有两件事尚未解决:
- import RootClass
- isinstance(cfg, RootClass)
RootClass 是一个模拟出来的类,配置文件中并不存在该对象。直接导入会引发导入错误,isinstance 也无法使用。解决方案是在原文件中添加一个类型别名:
RootClass = CfgNode # 在原文件中增加一个类型别名
至此达成一致:解析器可以正常导入 RootClass,且它是 CfgNode 的别名。IDE 可能知道 RootClass 实际指向 CfgNode,但会优先遵循 stub file 中的类型定义,从而为 yacs 的各个节点提供类型提示与自动补全。
后记
梳理思路比实际实现更为重要。笔者原以为需要借助 ast 分析、转换和导出才能解决此问题,但实际上通过遍历 CfgNode 替代了 ast 分析,并使用 yaml 编写 stub 文件,完全未涉及 ast。项目的核心代码非常精简,各个部分的实现颇具技巧性。
最后放出仓库,用法请参考 README。近期可能还会更新数次,之后便无需频繁更新。
Hook this up to your favourite commenting platform — Giscus, Disqus, or your own.
