我应该如何构建包含 Cython 代码的 Python 包

发布于 2024-10-08 10:08:08 字数 564 浏览 8 评论 0原文

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

对于大多数只想安装软件包的人来说,我想包含 Cython 创建的 .c 文件,并安排 setup.py 编译该文件以生成该模块。那么用户不需要安装 Cython 来安装该包。

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

我应该如何构建包中的文件以满足这两种情况?

Cython 文档提供了一些指导。但它没有说明如何制作一个单独的 setup.py 来处理有/没有 Cython 情况。

I'd like to make a Python package containing some Cython code. I've got the the Cython code working nicely. However, now I want to know how best to package it.

For most people who just want to install the package, I'd like to include the .c file that Cython creates, and arrange for setup.py to compile that to produce the module. Then the user doesn't need Cython installed in order to install the package.

But for people who may want to modify the package, I'd also like to provide the Cython .pyx files, and somehow also allow for setup.py to build them using Cython (so those users would need Cython installed).

How should I structure the files in the package to cater for both these scenarios?

The Cython documentation gives a little guidance. But it doesn't say how to make a single setup.py that handles both the with/without Cython cases.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

负佳期 2024-10-15 10:08:08

我现在已经在 Python 包 simplerandom 中自己完成了此操作( BitBucket 存储库 - 编辑:现在 github)(我不认为这是一个流行的包,但这是学习 Cython 的好机会)。

此方法依赖于这样一个事实:使用 Cython.Distutils.build_ext 构建 .pyx 文件(至少使用 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 的“默认”)。当我发布版本时,我需要记住首先执行 python setup.py build_ext,以确保 mycythonmodule.c 存在并且是最新的源代码分发。我还创建了一个发布分支,并将 C 文件提交到该分支中。这样我就有了随该版本分发的 C 文件的历史记录。

I've done this myself now, in a Python package simplerandom (BitBucket repo - EDIT: now github) (I don't expect this to be a popular package, but it was a good chance to learn Cython).

This method relies on the fact that building a .pyx file with Cython.Distutils.build_ext (at least with Cython version 0.14) always seems to create a .c file in the same directory as the source .pyx file.

Here is a cut-down version of setup.py which I hope shows the essentials:

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,
    ...
)

I also edited MANIFEST.in to ensure that mycythonmodule.c is included in a source distribution (a source distribution that is created with python setup.py sdist):

...
recursive-include cython *
...

I don't commit mycythonmodule.c to version control 'trunk' (or 'default' for Mercurial). When I make a release, I need to remember to do a python setup.py build_ext first, to ensure that mycythonmodule.c is present and up-to-date for the source code distribution. I also make a release branch, and commit the C file into the branch. That way I have a historical record of the C file that was distributed with that release.

无边思念无边月 2024-10-15 10:08:08

添加 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

Adding to Craig McQueen's answer: see below for how to override the sdist command to have Cython automatically compile your source files before creating a source distribution.

That way your run no risk of accidentally distributing outdated C sources. It also helps in the case where you have limited control over the distribution process e.g. when automatically creating distributions from continuous integration etc.

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
指尖凝香 2024-10-15 10:08:08

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 文件,对于我们将使用的基本示例:

来自 distutils.core 导入设置
从 distutils.extension 导入扩展
 
设置(
    ext_modules = [扩展("example", ["example.c"])]
)

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

It is strongly recommended that you distribute the generated .c files as well as your Cython sources, so that users can install your module without needing to have Cython available.

It is also recommended that Cython compilation not be enabled by default in the version you distribute. Even if the user has Cython installed, he probably doesn’t want to use it just to install your module. Also, the version he has may not be the same one you used, and may not compile your sources correctly.

This simply means that the setup.py file that you ship with will just be a normal distutils file on the generated .c files, for the basic example we would have instead:

from distutils.core import setup
from distutils.extension import Extension
 
setup(
    ext_modules = [Extension("example", ["example.c"])]
)
要走就滚别墨迹 2024-10-15 10:08:08

最简单的方法是包含两者,但只使用 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

应该处理它。

The easiest is to include both but just use the c-file? Including the .pyx file is nice, but it's not needed once you have the .c file anyway. People who want to recompile the .pyx can install Pyrex and do it manually.

Otherwise you need to have a custom build_ext command for distutils that builds the C file first. Cython already includes one. http://docs.cython.org/src/userguide/source_files_and_compilation.html

What that documentation doesn't do is say how to make this conditional, but

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

Should handle it.

放赐 2024-10-15 10:08:08

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

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


更新(2017-01-05):

setuptools 18.0 开始,无需使用 setuptools_cython这里是一个从头开始构建 Cython 项目的示例,无需 setuptools_cython< /代码>。

Including (Cython) generated .c files are pretty weird. Especially when we include that in git. I'd prefer to use setuptools_cython. When Cython is not available, it will build an egg which has built-in Cython environment, and then build your code using the egg.

A possible example: https://github.com/douban/greenify/blob/master/setup.py


Update(2017-01-05):

Since setuptools 18.0, there's no need to use setuptools_cython. Here is an example to build Cython project from scratch without setuptools_cython.

随遇而安 2024-10-15 10:08:08

所有其他答案要么依赖于

  • distutils
  • 从 Cython.Build 导入

,这会在通过 setup_requires 要求 cython 和导入它之间产生先有鸡还是先有蛋的问题。现代解决方案是使用 setuptools 代替,请参阅此答案(自动处理 Cython 扩展需要 setuptools 18.0,即它可用已经很多年了)。具有需求处理、入口点和 cython 模块的现代标准 setup.py 可能如下所示:

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'],
        ),
    ],
)

All other answers either rely on

  • distutils
  • importing from Cython.Build, which creates a chicken-and-egg problem between requiring cython via setup_requires and importing it.

A modern solution is to use setuptools instead, see this answer (automatic handling of Cython extensions requires setuptools 18.0, i.e., it's available for many years already). A modern standard setup.py with requirements handling, an entry point, and a cython module could look like this:

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'],
        ),
    ],
)
相权↑美人 2024-10-15 10:08:08

我想出的简单技巧是:

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,只需安装它即可。人们可能不应该共享这段代码,但对于我自己的依赖项来说,它已经足够好了。

The simple hack I came up with:

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(…)

Just install Cython if it could not be imported. One should probably not share this code, but for my own dependencies it's good enough.

自此以后,行同陌路 2024-10-15 10:08:08

这是我编写的一个设置脚本,它可以更轻松地在构建中包含嵌套目录。需要从包内的文件夹运行它。

给出这样的结构:

__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

编译愉快;)

This is a setup script I wrote which makes it easier to include nested directories inside the build. One needs to run it from folder within a package.

Givig structure like this:

__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

Happy compiling ;)

孤寂小茶 2024-10-15 10:08:08

我发现仅使用 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)

The easiest way I found using only setuptools instead of the feature limited distutils is

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)
撩人痒 2024-10-15 10:08:08

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

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

  2. 如果 cython 在系统上可用,它会挂钩到命令的 check_extensions_list() 方法,并通过 cythonizes 所有过时的 cython 模块,将它们替换为稍后可以由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}
    )

I think I found a pretty good way of doing this by providing a custom build_ext command. The idea is the following:

  1. I add the numpy headers by overriding finalize_options() and doing import numpy in the body of the function, which nicely avoids the problem of numpy not being available before setup() installs it.

  2. If cython is available on the system, it hooks into the command's check_extensions_list() method and by cythonizes all out-of-date cython modules, replacing them with C extensions that can later handled by the build_extension() method. We just provide the latter part of the functionality in our module too: this means that if cython is not available but we have a C extension present, it still works, which allows you to do source distributions.

Here's the code:

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())

This allows one to just write the setup() arguments without worrying about imports and whether one has cython available:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文