使用mypy提高签名质量

初步

PySide的Python接口文件是由几个脚本生成的。 当.pyi文件在2017年启动时,可以进行最小的语法检查, 因为这些文件可以在Python本身中运行。

.pyi文件格式的一些更改使得这变得不可能,导致PySide.pyi文件多年来几乎未被检查。只有所有函数的正确解析才能通过生成器进行检查。

引入mypy工具作为生成文件的严格错误检查器带来了许多改进,但也有一些意外。

运行mypy测试

mypy 测试由 Qt 公司的 CI 框架(COIN)自动运行。 当你安装了 mypy 时,测试会在构建时运行。 在调试模式下,这可能需要超过 30 秒,因此我们提供了翻译选项

--skip-mypy-test

可以在重复翻译时使用。但请注意,mypy 有一个很好的缓存,可以抑制对未更改的 .pyi 文件的分析。

mypy 错误类型

重复错误

许多函数具有多个签名,这些签名随后会在.pyi文件中被转换为多个typing.overload版本。由于C++函数到Python的映射,有时会发生类似的C++函数变成Python中的重复项。这很容易过滤掉,但mypy仍然会发现仅在参数名称上有所不同的重复项。现在,这由模块layout中的函数remove_ambiguous_signatures()处理,该函数比较所谓的annotations,这些注释会忽略参数名称。

阴影错误

一个相当微妙的错误类型是多个签名的遮蔽。这是由于.pyi文件的顺序性质造成的:

* In ``C++``, the order of functions does not matter at all. The best fit is
  automatically used.

* In Python stub files, the alternatives of multiple signatures are sequentially
  checked in ``@typing.overload`` chains of functions.
  This can produce shadowing when an annotation contains another.

一个例子:PySide6.QtCore.QCborSimpleType 当 int 列在前面时,会被 int 遮蔽。这是由于方法解析顺序 mro() 的原因:

* int.mro()              [<class 'int'>, <class 'object'>]

* QCborSimpleType.mro()  [<enum 'QCborSimpleType'>, <enum 'IntEnum'>,
                          <class 'int'>, <enum 'ReprEnum'>,
                          <enum 'Enum'>, <class 'object'>]

你可以看到mro()对多个签名有排序效果。 枚举继承自int,应该排在int条目之前。 将多个签名带入无冲突顺序的整个任务是一种拓扑排序

我们使用参数注释的mro长度和一些额外的启发式方法构建一个排序键。它们可以在模块layout中的sort_by_inheritance()调用的函数get_ordering_key()中进行检查。

无法解决的错误

一些由mypy指出的错误我们无法解决。我们唯一的机会是部分或完全禁用这些错误。它们在.pyi文件中被标记,见下文。

与Qt的矛盾

mypy 发现错误的地方,Qt 有不同的看法。错误类型“override”和“overload-overlap”需要被禁用,因为我们无法改变 Qt 认为正确的内容。

示例:

Error code "override" cannot be fixed because the problem
is situated in Qt itself:

    Signature of "open" incompatible with supertype "QFile"

Error code "overload-overlap" also cannot be fixed because
we have no chance to modify return-types:

    Overloaded function signatures 1 and 6 overlap with
    incompatible return types

它们通过以下注释全局禁用:

# mypy: disable-error-code="override, overload-overlap"

其他错误如“misc”过于宽泛,不能过早禁用。 请参见下面我们如何处理它们。

与 __add__ 和 __iadd__ 的分歧

有一些内部规则适用于Python,这些规则只有在mypy将其标记为“misc”时才能被识别。有些函数是成对出现的:

__add__, __iadd__, __sub__, __isub__, __mul__, __imul__, ...

以及更多。有这条规则:

if __add__ and __iadd__ exist in a type, the signatures must be the same.

在95%的情况下,这条规则是满足的,但在少数情况下并不满足。在这些情况下,我们必须计算这些情况,如果它们不一致,我们会生成一个禁用的mypy内联注释“# type: ignore[misc]”。你可以在模块enum_sigExactEnumerator.klass中看到这个功能。

与不一致的重载存在分歧

如果存在方法和静态或类方法的混合重载,mypy认为这是一个错误。在第一个版本中,我们通过抑制这种“杂项”错误来修复这种罕见的情况。但在转向正确的位置参数(PEP 570)时,这种抑制导致了无法解决的后续错误。更干净的解决方案是移除静态方法并优先使用普通方法。

查看模块 layout 中的函数 is_inconsistent_overload(),该函数检查“self”是否始终或从不作为参数。

结论与未来

这项工作已将报告的mypy错误从601个减少到零,这确实是一个改进。但还可以做得更多。尽管我们现在知道我们生成的文件在语法和语义上都非常正确,但我们仍然不知道实际类型是否真正满足mypy的要求。

mypy中有一个stubtest模块,我们或许可以用它来进行更多的测试。这些测试将检查实现和存根文件是否一致。

文献