在setup.py中配置SWIG模块

SWIG的一个优点是,受到了distutils/setuptools的原生支持。 在setup.py文件中,可以很方便地直接把*.i文件作为源文件进行配置,而不用手写编译脚本。

本文通过一个Demo项目,来讲解如何在setup.py中对SWIG进行配置。

Demo项目

为了解释SWIG模块的配置,必须要先说明其项目结构,这有些麻烦。 为此,孤特地写了一个Demo项目swig-python-demo,作为参考。

同时,这个项目也适合作为一个SWIG项目的初始化参考。

项目结构

脱离具体的项目结构,谈不了setup.py配置。 以下是swig-python-demo的项目结构。

swig-python-demo
├── LICENSE
├── Makefile
├── setup.cfg
├── setup.py
├── src
│   └── example
│       ├── example.c
│       ├── example.h
│       ├── example.i
│       ├── __init__.py
│       ├── _meta.py
│       ├── stl_example.cpp
│       ├── stl_example.hpp
│       └── stl_example.i
└── tests
    ├── test_example.py
    └── test_stl_example.py

它虽然只有一个example模块,但额外包括了example.istl_example.i两个SWIG模块。 前者是官方C语言的样例,后者是孤特别提供的C++语言STL容器样例。

setup.py

from setuptools import Extension, find_packages, setup

EXAMPLE_EXT = Extension(
    name='_example',
    sources=[
        'src/example/example.c',
        'src/example/example.i',
    ],
)

STD_EXT = Extension(
    name='_stl_example',
    swig_opts=['-c++'],
    sources=[
        'src/example/stl_example.cpp',
        'src/example/stl_example.i',
    ],
    include_dirs=[
        'src/example',
    ],
    extra_compile_args=[  # The g++ (4.8) in Travis needs this
        '-std=c++11',
    ]
)

setup(
    ...
    packages=find_packages('src'),
    package_dir={'': 'src'},
    ext_modules=[EXAMPLE_EXT, STD_EXT],
    ...
)

EXAMPLE_EXTSTD_EXT分别是两个SWIGExtension。 当然,它们可以合并成一个,这里是为了分别展示C项目与C++项目。

setup函数中,ext_modules参数是一个列表(实际上可以是一个Iterable),用来设置这个包所包含的Extension

注意:包名都以下划线_开头,这是SWIG自动生成的模块所约定的。 例如,在example.i中定义的模块名是example,那么自动生成的Python文件就是example.py,而SWIG的接口模块则是_example,编译后的文件也是_example.*.so

生成的py文件丢失

在使用SWIG时,有一个隐藏的问题。 无论是用bdistbdist_wheel还是bdist_rpm,这类以编译产物打包的命令,都会丢失SWIG生成的py文件。 比如这里,会丢失example.pystl_example.py两个文件。

之所以说『隐藏』,是因为在这个问题在本地开发时难以发现。 它只有在首次编译打包时,才会出现;如果在不删除这两个生成的py文件的前提下,再次编译、打包,就不会有这个问题。

根本原因在于,在执行setup函数进行编译、打包时,build_ext是在build_py之后执行的。 由于SWIG项目的特殊性,这个默认的执行顺序会让自动生成的Python文件在Python部分打包后才生成,所以在程序包里丢失。 再次打包不会遇到问题,也是因为首次编译时已经执行了build_ext

这里提供一个简单的解决方案,让build_extbuild_py之前执行。

# Build extensions before python modules,
# or the generated SWIG python files will be missing.
class BuildPy(build_py):
    def run(self):
        self.run_command('build_ext')
        super(build_py, self).run()


setup(
    ...
    cmdclass={
        'build_py': BuildPy,
    },
    ...
)

实际上,在Issue 7562: Custom order for the subcommands of build - Python tracker就描述了这个问题,而以下两个问题也记录了一些解决方案。 如果对此处提供的解决方案不满意,可以进一步参考它们。

pytest插件配置

在使用SWIG时,pytest的一些插件需要做额外配置,否则会有问题。 这些问题,主要都是由自动生成的Python文件——如本例中的example.pystl_example.py——所导致的。

pytest-cov

pytest-cov是用来计算测试覆盖率的插件。 它当然不支持C/C++的代码,但是却会自动计算自动生成的Python文件。 为了避免这个不是手写的文件影响结果,可以把它忽略掉。

[coverage:run]
omit = **/example.py
       **/stl_example.py

pytest-pep8

pytest-pep8是自动检查Python代码是否符合PEP8规范。 当然,SWIG自动生成的Python文件,至少在SWIG的3.0版本,还不符合PEP8规范。 为了避免测试失败,必须忽略它。

pep8ignore = example.py ALL
             stl_example.py ALL

pytest-flakes

pytest-flakes是对Python代码做一些基本的语法与缺陷检查。 SWIG自动生成的Python文件……

flakes-ignore = example.py ALL
                stl_example.py ALL
                __init__.py UnusedImport
                tests/* ALL

这里也忽略了所有的测试代码,因为pytest测试的某些写法不被这个插件所认可。 还有__init__.py里的UnusedImport类型,这是一种常见的误报。

后来,上述两个插件改为pytest-flake8。 并且,还会配上更多测试插件,详见swig-python-demo

总结

更多的细节,包括.travis.yml.appveyor.yml等CI配置,还是直接看swig-python-demo比较方便。


相关笔记