咨询MyPy插件中get_type_analyze_hook返回值及子类规则复现方法
我参与开发了科学工作流引擎Pydra,它利用Python类型注解在工作流构建阶段做动态类型检查,同时通过注解识别文件/目录输入,将内容纳入内部缓存的哈希计算中。
为解决带独立头文件的文件格式处理问题,我编写了FileFormats包,用于验证和处理不同文件格式,还支持把格式验证纳入动态类型检查,以及注册不同格式类型间的标准转换器。
在这个包里,我用__class_getitem__来指定文件内容的额外信息,比如Gzip[Png]表示gzip压缩的PNG文件,DicomSeries[Brain, T1Weighted]表示脑部T1加权MRI的DICOM序列。我还在混合类里实现了__subclasshook__,处理子类判定逻辑——比如让DicomSeries[Brain, T1Weighted]被视为DicomSeries[Brain]、DicomSeries[T1Weighted]和DicomSeries的子类。
目前包的运行效果很好,但适配静态类型检查工具Mypy时,发现非泛型类使用__class_getitem__是Mypy不推荐/不支持的功能,会触发警告。
如何编写带有get_type_analyze_hook的Mypy插件,消除上述警告,同时复现__subclasshook__里的子类判定规则?另外想明确:get_type_analyze_hook的作用是什么?是不是只要让它返回有效类型就行?能不能复用我现有代码里的逻辑来实现?
1. get_type_analyze_hook的核心作用
它是Mypy插件的核心钩子之一,负责拦截并自定义Mypy对特定类型构造的解析过程。当Mypy遇到X[Y,Z]这类类型表达式时,会调用对应插件的这个钩子,你可以在钩子函数里返回一个符合Mypy类型系统规范的类型对象,直接绕过Mypy对非泛型类使用__class_getitem__的默认检查逻辑。
2. 编写插件的核心步骤
步骤1:搭建插件基础结构
创建插件文件(比如fileformats_mypy_plugin.py),实现Mypy插件的入口并注册钩子:
from mypy.plugin import Plugin from mypy.nodes import TypeExpr from mypy.types import Type class FileFormatsPlugin(Plugin): def get_type_analyze_hook(self, fullname: str): # 仅处理FileFormats包下的类 if fullname.startswith("fileformats."): return self.analyze_fileformat_type return None def analyze_fileformat_type(self, ctx: TypeExpr) -> Type: # 后续实现自定义解析逻辑 pass def plugin(version: str): return FileFormatsPlugin
步骤2:解析类型参数并返回合法类型
在analyze_fileformat_type中,提取X[Y,Z]的主类型和参数,构造Mypy能识别的合法类型,消除警告:
from mypy.nodes import IndexExpr from mypy.types import Instance, TupleType def analyze_fileformat_type(self, ctx: TypeExpr) -> Type: # 只处理索引表达式(即X[Y,Z]形式) if not isinstance(ctx, IndexExpr): return self.named_type(ctx.fullname) # 获取主类型全名和参数表达式 base_type_fullname = ctx.base.fullname args_expr = ctx.args # 解析参数为Mypy类型对象 parsed_args = [] for arg in args_expr: # 处理参数为类型引用的情况 if hasattr(arg, "fullname"): parsed_args.append(self.named_type(arg.fullname)) # 处理其他参数类型(如字面量) else: parsed_args.append(self.type_map[arg]) # 构造主类型的Instance对象,传入解析后的参数 base_type = self.named_type(base_type_fullname) return Instance(base_type.type, parsed_args)
步骤3:复现__subclasshook__的子类判定规则
需要额外实现get_subtype_hook钩子,介入Mypy的子类型检查逻辑:
from mypy.subtypes import is_subtype class FileFormatsPlugin(Plugin): # ... 保留之前的get_type_analyze_hook ... def get_subtype_hook(self): return self.check_fileformat_subtype def check_fileformat_subtype(self, left: Type, right: Type) -> bool | None: # 仅处理FileFormats包下的Instance类型 if (isinstance(left, Instance) and isinstance(right, Instance) and left.type.fullname.startswith("fileformats.") and right.type.fullname.startswith("fileformats.")): # 主类型不同时,交给Mypy默认逻辑 if left.type.fullname != right.type.fullname: return None # 实现你的子类判定逻辑:比如右侧参数是左侧参数的子集 right_arg_names = {arg.type.fullname for arg in right.args} left_arg_names = {arg.type.fullname for arg in left.args} if right_arg_names.issubset(left_arg_names): return True # 返回None表示使用Mypy默认检查逻辑 return None
3. 复用现有代码逻辑
把你现有__subclasshook__中的判定逻辑提取成独立工具函数(比如放在fileformats.utils中),然后在插件中直接导入调用,避免重复代码:
# 在fileformats.utils中定义 def is_format_subtype(left_args: list[type], right_args: list[type]) -> bool: # 这里复用你原有的子类判定逻辑 right_arg_set = set(right_args) return right_arg_set.issubset(left_args) # 在插件中调用 from fileformats.utils import is_format_subtype def check_fileformat_subtype(self, left: Type, right: Type) -> bool | None: # ... 类型检查逻辑 ... # 将Mypy类型对象转换为你代码中的实际类型 left_real_args = [self.lookup_fully_qualified(arg.type.fullname).type for arg in left.args] right_real_args = [self.lookup_fully_qualified(arg.type.fullname).type for arg in right.args] return is_format_subtype(left_real_args, right_real_args)
4. 启用插件
在项目的mypy.ini或pyproject.toml中配置插件:
# mypy.ini [mypy] plugins = fileformats_mypy_plugin
内容的提问来源于stack exchange,提问作者Tom Close

