签名C扩展链接到本标题

本模块是CPython 3.5及更高版本和CPython 2.7的C扩展,其目的是为内置PyCFunction对象的__signature__属性提供支持。

关于主题的简要介绍链接到本标题

从CPython 3.5开始,Python函数开始增加了__signature__属性,这对于正常的Python函数来说完全是可选的,但这是一个很好的特性。

另一方面,PySide非常需要__signature__,因为15000多个PySide函数的类型信息实实在在是缺失的,能够直接获得这些信息将是非常好的。

支持签名的想法链接到本标题

我们希望所有PySide方法中都增加额外的__signature__属性,而不需要修改大量的生成代码。因此,我们没有改变任何现有的数据结构,而是通过全局字典来支持新的属性。

当请求__signature__属性时,会调用一个方法在该全局字典中进行查找。这是一个灵活的方法,对项目的其他部分影响很小。与直接属性访问相比,其开销非常有限,但对于偶尔需要访问签名的需求来说,这是一个适当的折衷方案。

此代码的工作方式链接到本标题

仅支持常规Python函数的签名。为PyCFunction对象创建签名将需要在Python中付出相当大的努力。

幸运的是,我们发现了一种特殊的技术,这样可以节省我们大部分的劳动力。

基本思路是创建一个具有varnamesdefaultsannotations属性的模拟Python函数,然后使用inspect模块创建一个签名对象。该对象作为真实PyCFunction对象__signature__属性的计算结果返回。

有一件事实际上改变了Python一点

  • 我们给每个函数都添加了__signature__属性。

这是一个不会造成伤害的小改动,但它帮我们节省了在整个模块早期版本中所需的代码。

内部工作分为两个步骤

  • 类中所有函数在模块导入时都获得一个签名文本。这只是启动时间中增加的很小的一部分开销。对整个类来说,这仅是一个字符串。

  • 实际的签名对象是在真正请求属性时创建的。签名被缓存,并且只在第一次访问时创建。

示例

查询PyCFunction QtWidgets.QApplication.palette的签名。这意味着调用了pyside_sm_get___signature__()。它调用GetSignature_Function,如果找到签名则返回。

为什么这段代码很快#

由于这些签名字符对象超过25000个,因此运行每个签名字符对象需要一些时间(可能6秒)。但在特殊应用中,签名字符对象很少被访问。正常情况下只有少量访问,而且它们运行得相当快。

使这个签名模块快速的关键是尽可能避免计算。当没有使用签名字符对象时,那么初始化中几乎不会损失时间。只有在导入PySide6时,才会另外加载上述提到的字符串和一些支持模块。当使用签名时,则会使用晚期初始化并进行缓存。这种技术在haskell中也称为完全惰性

实际发生晚期初始化的地点有两个

  • dict 可以不是字典而是一个元组。这是在模块加载时由 PySide_BuildSignatureArgs 指定的参数元组。如果是这样,则在 parser.py 中的 pyside_type_init 将被调用,它将解析字符串并创建字典。

  • props 可以是空的。那么将在 loader.py 中调用 create_signature,它使用假函数和一个模块的实例来创建签名。

始终进行的初始化仅仅是每个类写两个字典,我们大约有1000个类。为了测量附加开销,我们模拟了执行 from PySide6 import * 时会发生什么。结果发现,开销低于0.5毫秒。

签名包结构#

涉及签名字符模块的C++代码完全在文件 shiboken6/libshiboken/signature.cpp 中。所有其他功能都在签名 Python 包中实现。它的结构如下

sources/shiboken6/shibokenmodule/files.dir/shibokensupport
├── __init__.py
├── feature.py
├── fix-complaints.py
├── shibokensupport.pyproject
└── signature
    ├── PSF-3.7.0.txt
    ├── __init__.py
    ├── errorhandler.py
    ├── importhandler.py
    ├── layout.py
    ├── lib
    │   ├── __init__.py
    │   ├── enum_sig.py
    │   ├── pyi_generator.py
    │   └── tool.py
    ├── loader.py
    ├── mapping.py
    ├── parser.py
    └── qt_attribution.json

最重要的是 parsermappingerrorhandlerenum_siglayoutloader 模块。其余的用于创建Python 2兼容性或与内嵌和安装程序兼容。

loader.py

此模块组装并导入 inspect 模块,然后导出 create_signature 函数。此函数接受一个假函数和一些属性,使用 inspect 模块构建一个 __signature__ 对象。

parser.py

此模块接收从C++中获取的类签名字符串,将其解析为 create_signature 函数所需的属性。它的入口点是 pyside_type_init 函数,它从 C 模块通过 loader.py 被调用。

mapping.py

映射模块的目的是维护一个替换字串列表,这些字串将C中的签名文本映射到Python所需要的属性字符串。在parser.py中,大量的映射是通过较复杂的表达式来解决的,但这里最好明确定义几百种情况。

errorhandler.py

Qt For Python 5.12以来,我们不再使用C++内置的类型错误消息。相反,通过签名模块可以得到更好的结果。同时,这也强制支持shiboken,签名模块不再可选。

enum_sig.py

签名模块的广泛应用都需要遍历模块、类和函数。为了集中化枚举,这个过程被提取为上下文管理器。用户只需提供执行实际格式化的函数。

例如,查看.pyi生成器pyside6/PySide6/support/generate_pyi.py

layout.py

随着更多应用程序使用签名模块,对签名格式的需求也各不相同。为了支持这一点,我们创建了create_signature函数,它有一个参数可以从预定义的布局中进行选择。

typing27.py

Python 2根本没有typing模块。这是一个最小程度需要的技术传输。

backport_inspect.py

Python 2有一个inspect模块,但它完全缺少签名函数。这个模块添加了缺失的功能,这些功能在运行时合并到inspect模块中。

多重参数#

直到目前为止,被忽略的一个方面是多重参数:当函数有多个签名时如何处理?

我未找到有关如何在Python中处理多个签名任何注释,但以下简单规则似乎运作良好

  • 如果有列表,则它是多签名。

  • 否则,它是一个简单签名。

签名模块的影响#

签名模块对其他PySide模块产生了一些影响,这是由于其存在而产生的,未来将会有更多。

existence_test.py#

文件pyside6/tests/registry/existence_test.py使用签名模块的签名编写。理念是有大约15000个具有特定签名的函数。

这些函数不应该因为某些错误的检查而丢失。因此,将现有签名的列表作为模块,构建成一个字典。检查函数的存在,以及确切参数数量。

这个模块为每个PySide版本和每个平台都存在。初始模块生成一次,并保存为exists_{plat}_{version}.py

通常错误仅报告为警告,但

与Coin模块的交互#

当测试程序在COIN中运行时,警告将被转换为错误。原因是只有在COIN中,我们才有稳定配置的PySide模块可以可靠地比较。

这些模块有名称exists_{platf}_{version}_ci.py,并且作为生成代码的一个大例外,这些文件是有意被签入的。

缺少列表时会发生什么?#

当创建PySide的新版本时,存在测试文件最初不存在。

当运行COIN测试时,它将投诉错误并在标准输出中创建缺失的模块。但由于COIN测试会多次运行,第一次测试生成的输出仍然会在后续运行中存在。(如果COIN实现得当,我们就不能利用这个优势,并且需要将其作为额外的异常实现。)

因此,缺失的模块将被报告为部分成功的测试(称为“不可靠的”)。为了避免进一步的不可靠测试并激活真正的测试,我们现在可以捕获COIN的错误输出并在其中检查生成的模块。

显式强制重建#

以前重建注册表的文件的方法是删除文件并检查它们。这产生了预期效果,但创建了庞大的差异。作为更有效的方法,我们在第一行准备了一个包含“重建”一词的注释。通过取消注释此行,会触发一个NameError,它具有相同的效果。

init_platform.py#

为了生成exists_{platf}_{version}模块,编写了模块pyside6/tests/registry/init_platform.py。它可以从命令行独立使用,以直接检查某些更改的兼容性。

scrape_testresults.py#

为了简化并自动化提取exists_{platf}_{version}_ci.py文件的过程,已编写脚本pyside6/tests/registry/scrape_testresults.py

该脚本会扫描整个PySide测试结果网站,即

https://testresults.qt.io/coin/api/results/pyside/pyside-setup/

首次扫描时,脚本运行不到30分钟。之后,将生成一个缓存,扫描工作将大大加速。测试结果将放置在文件夹pyside6/tests/registry/testresults/embedded/中,并以唯一名称保存,便于排序。例如

testresults/embedded/2018_09_10_10_40_34-test_1536891759-exists_linux_5_11_2_ci.py

这些文件只创建一次。如果它们已经存在,则不会再次更改。文件pyside6/tests/registry/known_urls.json在成功扫描后保存所有扫描的URL。可以将testresults/embedded文件夹保留以供参考或删除。重要的是只有json文件。

扫描结果随后直接放入pyside6/tests/registry/文件夹。应进行审查,然后最终进行检查。

generate_pyi.py#

pyside6/PySide6/support/generate_pyi.py仍在开发中。这个模块为PySide与众多Python IDE的集成生成所谓的提示存根。

尽管该模块创建存根作为附加功能,但它对签名模块质量的影响相当大

该模块必须创建语法正确的.pyi文件,这些文件不仅包含签名,还包含所有PySide模块的常量和枚举。这增加了额外的挑战,但具有非常积极的影响,可以提高签名的完整性和正确性。

该模块有一个--feature选项用于生成修改过的.pyi文件。此命令的快捷方式是pyside6-genpyi

将所有.pyi文件更改以使用所有功能的实用命令

pyside6-genpyi all --feature snake_case true_property

pyi_generator.py#

shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py 已从 generate_pyi.py 提取出来。它允许从使用 shiboken 创建的任意扩展模块生成 .pyi 文件。

此命令的快捷方式为 shiboken6-genpyi

当前扩展#

在签名模块被编写之前,已经存在了签名这一概念,但主要是以 C++ 为中心的。从那时起,就存在错误信息,这些信息是在函数获取错误参数类型时创建的。

这些错误信息被签名模块按需生成的文本所替换,以实现更一致和正确。此功能在 Qt For Python 5.12.0 中实现。

此外,PySide 方法的 __doc__ 属性未设置。通过将签名作为 docstring 的默认内容创建,很容易获得漂亮的 help() 功能。此功能在 Qt For Python 5.12.1 中实现。

Literature#

个人评论:此模块献给我们的爱鸟“Püppi”,它于 2017-09-15 去世。