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

Posted

技术标签:

【中文标题】Python 模块应该如何使用代码生成?【英文标题】:How should a Python module use code generation? 【发布时间】:2021-02-16 09:17:16 【问题描述】:

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

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

这意味着我的 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,或者我需要在构建源代码分发时运行这些工具。我看到了两种可能的解决方案:

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

任何解决方案都可以只在 Linux 和 OS X 上运行。

【问题讨论】:

【参考方案1】:

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

(IANAL,但我的理解是,bison 生成的代码存在许可证例外,即使在非 GPL 项目中也可以分发。Flex 一开始就不是 GPL,而且 afaik 没有分发限制。 )

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

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

【讨论】:

感谢您的建议。许可证绝对兼容。您能否评论一下 如何 捆绑生成的代码但如果它不存在仍会生成它?调用flex/bison的代码应该如何集成到setup.py文件中? @seanmk:我远非 distutils 专家,但我试图在编辑答案时解决这个问题。很抱歉第一次跳过它。 这似乎可以解决问题:覆盖 build_ext 和 sdist 并在文件丢失或过时时都生成文件。一个重要的技巧:我发现 build 目录在 MANIFEST.in 被解析后会被 setuptools 自动删除,所以生成的代码需要放在 build 以外的地方 @seanmk:啊,很高兴知道。我会用这些信息来修正答案。

以上是关于Python 模块应该如何使用代码生成?的主要内容,如果未能解决你的问题,请参考以下文章

没有名为 urllib.parse 的模块(我应该如何安装它?)

如何在 macOS 上使用 python 将图片发送到打印机

python模块布局

如何将图像添加到 Python 包索引文档中?

如何使用 Python 的 Requests 模块“登录”到网站?

如何从命令行调试使用 python -m 运行的 Python 模块?