如何构造包含Cython代码的Python包


122

我想制作一个包含一些Cython代码的Python包。我的Cython代码运行良好。但是,现在我想知道如何最好地打包它。

对于大多数只想安装软件包的人,我想包括.cCython创建的文件,并安排对其setup.py进行编译以生成模块。然后,用户不需要安装Cython即可安装软件包。

但是对于那些可能想要修改程序包的人,我也想提供Cython .pyx文件,并以某种方式还允许setup.py使用Cython构建它们(因此这些用户需要安装Cython)。

我应该如何构造软件包中的文件以适应这两种情况?

用Cython文档提供了一些指导。但这并没有说明如何制作一个setup.py处理Cython情况的单例。


1
我认为问题比任何答案都更具投票意义。我很好奇,为什么人们会发现答案不尽人意。
Craig McQueen 2014年

4
在文档的这一部分中找到了确切的答案。
2014年

Answers:


72

我现在已经在Python程序包simplerandomBitBucket repo-编辑:now github)中亲自完成了这个任务(我不希望这是一个受欢迎的程序包,但这是学习Cython的好机会)。

此方法依赖于以下事实:.pyx使用Cython.Distutils.build_ext(至少使用Cython版本0.14)构建文件似乎总是.c在与源.pyx文件相同的目录中创建文件。

这是一个精简版setup.py,我希望其中显示要点:

from distutils.core import setup
from distutils.extension import Extension

try:
    from Cython.Distutils import build_ext
except ImportError:
    use_cython = False
else:
    use_cython = True

cmdclass = {}
ext_modules = []

if use_cython:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.pyx"]),
    ]
    cmdclass.update({'build_ext': build_ext})
else:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.c"]),
    ]

setup(
    name='mypackage',
    ...
    cmdclass=cmdclass,
    ext_modules=ext_modules,
    ...
)

我还进行了编辑,MANIFEST.in以确保将mycythonmodule.c其包含在源分发中(使用创建的源分发python setup.py sdist):

...
recursive-include cython *
...

我不承诺mycythonmodule.c版本控制“ trunk”(或Mercurial的“ default”)。发布时,我需要记住先进行操作python setup.py build_ext,以确保mycythonmodule.c该源代码是最新的并且是最新的。我还创建了一个release分支,并将C文件提交到该分支中。这样,我就拥有与该发行版一起分发的C文件的历史记录。


谢谢,这正是我要打开的Pyrex项目所需要的!MANIFEST.in使我绊了一下,但我只需要那条线。我出于兴趣将C文件包括在源代码控制中,但是我认为您的观点是不必要的。
chmullig

我已经编辑了答案,以解释C文件如何不在trunk / default中,而是如何添加到release分支中。
Craig McQueen

1
@CraigMcQueen感谢您的出色回答,对我有很大帮助!但是,我想知道,使用Cython是否是期望的行为?在我看来,默认情况下最好使用预先生成的c文件,除非用户明确希望使用Cython,在这种情况下,他可以设置环境变量或其他内容。这将使安装更加稳定/可靠,因为用户可能会根据安装的Cython版本而获得不同的结果-他甚至可能不知道自己已安装Cython,并且正在影响软件包的构建。
Martinsos

20

克雷格·麦昆(Craig McQueen)的答案有所添加:请参见下文,了解如何覆盖sdist命令以使Cython在创建源代码分发之前自动编译源文件。

这样一来,您就可以避免意外分发过期C资源的风险。在您对分发过程的控制有限的情况下(例如,通过持续集成自动创建分发时),这也很有帮助。

from distutils.command.sdist import sdist as _sdist

...

class sdist(_sdist):
    def run(self):
        # Make sure the compiled Cython files in the distribution are up-to-date
        from Cython.Build import cythonize
        cythonize(['cython/mycythonmodule.pyx'])
        _sdist.run(self)
cmdclass['sdist'] = sdist

19

http://docs.cython.org/en/latest/src/userguide/source_files_and_compilation.html#distributing-cython-modules

强烈建议您分发生成的.c文件以及Cython源,以便用户无需使用Cython即可安装模块。

还建议您分发的版本中默认不启用Cython编译。即使用户安装了Cython,他也可能不想仅使用它来安装模块。另外,他使用的版本可能与您使用的版本不同,并且可能无法正确编译您的源代码。

这只是意味着您附带的setup.py文件将只是生成的.c文件上的常规distutils文件,对于基本示例,我们将使用:

from distutils.core import setup
from distutils.extension import Extension
 
setup(
    ext_modules = [Extension("example", ["example.c"])]
)

7

最简单的方法是同时包含两者,而仅使用c文件?包括.pyx文件是不错的选择,但是无论如何只要有了.c文件就不需要了。想要重新编译.pyx的人可以安装Pyrex并手动进行。

否则,您需要有一个用于distutils的自定义build_ext命令,该命令首先生成C文件。Cython已经包含一个。http://docs.cython.org/src/userguide/source_files_and_compilation.html

该文档没有做的是说如何使其成为条件,但是

try:
     from Cython.distutils import build_ext
except ImportError:
     from distutils.command import build_ext

应该处理。


1
感谢您的回答。这是合理的,尽管我更喜欢在安装Cython时setup.py可以直接从.pyx文件生成。我的回答也实现了这一点。
Craig McQueen 2010年

好吧,这就是我回答的重点。这只是一个不完整的setup.py。
Lennart Regebro

4

包含(Cython)生成的.c文件非常奇怪。尤其是当我们在git中包含它时。我更喜欢使用setuptools_cython。当Cython不可用时,它将构建一个具有内置Cython环境的鸡蛋,然后使用该鸡蛋构建代码。

一个可能的示例:https : //github.com/douban/greenify/blob/master/setup.py


更新(2017-01-05):

因为setuptools 18.0,没有必要使用setuptools_cython是一个从头开始构建Cython项目而无需的示例setuptools_cython


这样可以解决即使您在setup_requires中指定了Cython也无法安装的问题?
卡米尔·辛迪

还不能放入'setuptools>=18.0'setup_requires而不是创建方法is_installed吗?
卡米尔·辛迪

1
@capitalistpug首先,您需要确保setuptools>=18.0已安装,然后只需放入,然后'Cython >= 0.18'setup_requires安装过程中将安装Cython。但是,如果您使用的是setuptools <18.0,即使您在setup_requires中使用了特定的cython,也不会安装它,在这种情况下,您应该考虑使用use setuptools_cython
McKelvin

感谢@McKelvin,这似乎是一个不错的解决方案!出于什么原因,在此之后,为什么我们要使用另一种方法来预先对源文件进行cythonize?我尝试了您的方法,但安装时似乎确实有些慢(安装需要花费一分钟,而第二秒即可构建)。
Martinsos

1
@Martinsos pip install wheel。这可能是原因1。请先安装车轮,然后再试一次。
麦凯文'17

2

这是我编写的安装脚本,它使在构建中包括嵌套目录更加容易。需要从一个程序包中的文件夹运行它。

Givig结构如下:

__init__.py
setup.py
test.py
subdir/
      __init__.py
      anothertest.py

setup.py

from setuptools import setup, Extension
from Cython.Distutils import build_ext
# from os import path
ext_names = (
    'test',
    'subdir.anothertest',       
) 

cmdclass = {'build_ext': build_ext}
# for modules in main dir      
ext_modules = [
    Extension(
        ext,
        [ext + ".py"],            
    ) 
    for ext in ext_names if ext.find('.') < 0] 
# for modules in subdir ONLY ONE LEVEL DOWN!! 
# modify it if you need more !!!
ext_modules += [
    Extension(
        ext,
        ["/".join(ext.split('.')) + ".py"],     
    )
    for ext in ext_names if ext.find('.') > 0]

setup(
    name='name',
    ext_modules=ext_modules,
    cmdclass=cmdclass,
    packages=["base", "base.subdir"],
)
#  Build --------------------------
#  python setup.py build_ext --inplace

编译愉快;)


2

我想到的简单技巧:

from distutils.core import setup

try:
    from Cython.Build import cythonize
except ImportError:
    from pip import pip

    pip.main(['install', 'cython'])

    from Cython.Build import cythonize


setup(…)

如果无法导入,只需安装Cython。一个人可能不应该共享此代码,但是对于我自己的依赖关系来说已经足够了。


2

所有其他答案都依赖

  • 发行版
  • 从导入Cython.Build,在通过cython setup_requires导入和导入cython之间会产生鸡与蛋的问题。

一种现代的解决方案是改用setuptools,请参见以下答案(自动处理Cython扩展需要setuptools 18.0,即,它已经可用了很多年)。setup.py具有需求处理,入口点和cython模块的现代标准可能如下所示:

from setuptools import setup, Extension

with open('requirements.txt') as f:
    requirements = f.read().splitlines()

setup(
    name='MyPackage',
    install_requires=requirements,
    setup_requires=[
        'setuptools>=18.0',  # automatically handles Cython extensions
        'cython>=0.28.4',
    ],
    entry_points={
        'console_scripts': [
            'mymain = mypackage.main:main',
        ],
    },
    ext_modules=[
        Extension(
            'mypackage.my_cython_module',
            sources=['mypackage/my_cython_module.pyx'],
        ),
    ],
)

Cython.Build在设置时从导入对我来说会导致ImportError。拥有setuptools来编译pyx是最好的方法。
卡森·叶

1

我发现仅使用setuptools而非功能受限的distutils的最简单方法是

from setuptools import setup
from setuptools.extension import Extension
try:
    from Cython.Build import cythonize
except ImportError:
    use_cython = False
else:
    use_cython = True

ext_modules = []
if use_cython:
    ext_modules += cythonize('package/cython_module.pyx')
else:
    ext_modules += [Extension('package.cython_module',
                              ['package/cython_modules.c'])]

setup(name='package_name', ext_modules=ext_modules)

实际上,使用setuptools无需从中进行显式的try / catched导入Cython.Build,请参阅我的答案。
bluenote10 '18年

0

我想我通过提供自定义build_ext命令找到了一种很好的方法。这个想法如下:

  1. 我通过重写finalize_options()import numpy在函数的主体中添加numpy标头,很好地避免了numpy在setup()安装之前不可用的问题。

  2. 如果cython在系统上可用,它将挂接到命令的check_extensions_list()方法中,并通过cython化所有过时的cython模块,将其替换为C扩展,稍后可通过该build_extension() 方法处理。我们也只是在模块中提供功能的后一部分:这意味着,如果cython不可用,但是我们有C扩展名,它仍然可以工作,从而可以进行源代码分发。

这是代码:

import re, sys, os.path
from distutils import dep_util, log
from setuptools.command.build_ext import build_ext

try:
    import Cython.Build
    HAVE_CYTHON = True
except ImportError:
    HAVE_CYTHON = False

class BuildExtWithNumpy(build_ext):
    def check_cython(self, ext):
        c_sources = []
        for fname in ext.sources:
            cname, matches = re.subn(r"(?i)\.pyx$", ".c", fname, 1)
            c_sources.append(cname)
            if matches and dep_util.newer(fname, cname):
                if HAVE_CYTHON:
                    return ext
                raise RuntimeError("Cython and C module unavailable")
        ext.sources = c_sources
        return ext

    def check_extensions_list(self, extensions):
        extensions = [self.check_cython(ext) for ext in extensions]
        return build_ext.check_extensions_list(self, extensions)

    def finalize_options(self):
        import numpy as np
        build_ext.finalize_options(self)
        self.include_dirs.append(np.get_include())

这样一来,人们就可以编写setup()参数而不必担心导入以及是否有可用的cython的问题:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.