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 将图片发送到打印机