Python 模块应该如何使用代码生成?

How should a Python module use code generation?

我有一个 Python 模块,它是围绕用 C 编写的本机扩展构建的。此扩展包括使用 GNU Bison 和(非 GNU)Flex 工具生成的代码。这意味着我的 C 扩展的构建过程涉及调用这些工具,然后将它们的输出(C 源文件)包含在扩展源中。

为了在调用 python setup.py install 时使其正常工作,我扩展了 setuptools.command.build_ext class 以调用 Flex 和 Bison,然后在调用之前将生成的源添加到扩展源超级class运行方法。

这意味着我的 setup.py 看起来像:

import os
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext

c_extension = Extension('_mymod',
               include_dirs = ['inc'],
               sources = [
                          os.path.join('src', 'lib.c'),
                          os.path.join('src', 'etc.c')
                         ])

class MyBuild(build_ext):
    def run(self):
        parser_dir = os.path.join(self.build_temp, 'parser')
        # add the parser directory to include_dirs
        self.include_dirs.append(parser_dir)
        # add the source files to the sources
        self.extensions[0].sources.extend([os.path.join(parser_dir, 'lex.yy.c'), os.path.join(parser_dir, 'parse.tab.c')])
        
        # honor the --dry-run flag
        if not self.dry_run:
            self.mkpath(parser_dir)

            os.system('flex -o ' + os.path.join(parser_dir, 'lex.yy.c') + ' ' + os.path.join('src', 'lex.l'))
            os.system('bison -d -o ' + os.path.join(parser_dir, 'parse.tab.c') + ' ' + os.path.join('src', 'parse.y'))

        # call the super class method
        return build_ext.run(self)

setup (name = 'MyMod',
       version = '0.1',
       description = 'A module that uses external code generation tools',
       author = 'Sean Kauffman',
       packages = ['MyMod'],
       ext_modules = [c_extension],
       cmdclass={'build_ext': MyBuild},
       python_requires='>=3',
       zip_safe=False)

但是,现在我正在尝试打包此模块以进行分发,但我遇到了问题。要安装我的软件包的用户需要安装 Bison 和 Flex,或者我在构建源代码分发时需要 运行 这些工具。我看到两种可能的解决方案:

  1. 我验证了 flex 和 bison 在系统执行路径中。这使自定义构建器保持原样。我没有找到暗示我可以验证系统文件是否存在的文档,例如 bison 和 flex。最接近的是使用扩展的 libraries 字段,但似乎我需要一些真正的 hackery 来检查整个 PATH 的可执行文件。我还没有尝试过这个,因为我的第一选择是选项 2。
  2. 我将代码生成移动到创建源分发时发生。这意味着源代码分发将包含来自 bison 和 flex 的输出文件,因此安装包的人不需要这些工具。这似乎是更清洁的选择。我试过像上面那样扩展 sdist 命令而不是 build_ext,但是我不清楚如何将生成的文件添加到 MANIFEST 以便将它们包含在内。此外,我想确保它仍然可以使用 python setup.py install 进行构建,但我认为此命令在构建之前不会 运行 sdist。

任何解决方案都只适用于 Linux 和 OS X。

分发需要 (f)lex 和 bison/yacc 的代码的通常解决方案是捆绑生成的扫描器和解析器,但如果它们不存在,请准备好生成它们。第二部分使开发更容易一些,并且如果人们觉得有充分的理由这样做,还可以让人们选择使用他们自己的 flex/bison 版本。我想这个建议也适用于 Python 模块。

(IANAL 但我的理解是 bison 生成的代码有一个许可证例外,这使得即使在 non-GPL 项目中也可以分发。Flex 不是 GPL,而且据我所知有没有分发限制。)

要在源代码分发中有条件地构建扫描器和解析器,您可以在验证生成的文件不存在后使用您已经提供的代码。 (理想情况下,您会检查生成的文件是否不存在或是否比相应的源文件更新。这取决于文件日期在通过存档的过程中未被更改。这将在 Linux 和OS X 但它可能不完全便携。)

假设包是在执行 sdist 命令之前构建的。 sdist 通常应该排除在源代码树中构建的目标文件,因此不需要手动清理源代码。但是,如果您想确保在执行 sdist 时生成的文件存在,您可以在 setup.py 中以与覆盖 build_ext 相同的方式覆盖它,在之前调用 bison 和 flex调用基础 sdist 命令。