将 numpy.get_include() 参数添加到没有预装 numpy 的 setuptools

Add numpy.get_include() argument to setuptools without preinstalled numpy

我目前正在开发一个使用 cythonnumpy 的 python 包,我希望可以使用 pip install 命令从干净的 python 安装。所有依赖项都应该自动安装。我正在使用 setuptools 和以下 setup.py:

import setuptools

my_c_lib_ext = setuptools.Extension(
    name="my_c_lib",
    sources=["my_c_lib/some_file.pyx"]
)

setuptools.setup(
    name="my_lib",
    version="0.0.1",
    author="Me",
    author_email="me@myself.com",
    description="Some python library",
    packages=["my_lib"],
    ext_modules=[my_c_lib_ext],
    setup_requires=["cython >= 0.29"],
    install_requires=["numpy >= 1.15"],
    classifiers=[
        "Programming Language :: Python :: 3",
        "Operating System :: OS Independent"
    ]
)

到目前为止效果很好。 pip install 命令为构建下载 cython,并且能够构建我的包并将其与 numpy.

一起安装

现在我想提高我的 cython 代码的性能,这导致我的 setup.py 发生了一些变化。我需要将 include_dirs=[numpy.get_include()] 添加到 setuptools.Extension(...)setuptools.setup(...) 的调用中,这意味着我还需要 import numpy。 (请参阅 http://docs.cython.org/en/latest/src/tutorial/numpy.html and Make distutils look for numpy header files in the correct place 了解有理数。)

这很糟糕。现在用户无法从干净的环境中调用 pip install,因为 import numpy 会失败。在安装我的库之前,用户需要 pip install numpy。即使我将 "numpy >= 1.15"install_requires 移动到 setup_requires 安装也会失败,因为 import numpy 被较早评估。

有没有办法在安装的后期评估 include_dirs,例如,在解决了 setup_requiresinstall_requires 的依赖关系之后?我真的很喜欢自动解决所有依赖关系,我不希望用户键入多个 pip install 命令。

以下代码片段有效,但未得到官方支持,因为它使用了未记录(且私有)的方法:

class NumpyExtension(setuptools.Extension):
    # setuptools calls this function after installing dependencies
    def _convert_pyx_sources_to_lang(self):
        import numpy
        self.include_dirs.append(numpy.get_include())
        super()._convert_pyx_sources_to_lang()

my_c_lib_ext = NumpyExtension(
    name="my_c_lib",
    sources=["my_c_lib/some_file.pyx"]
)

文章 How to Bootstrap numpy installation in setup.py 建议使用带有自定义 build_ext class 的 cmdclass。不幸的是,这破坏了 cython 扩展的构建,因为 cython 还自定义了 build_ext.

一个(hacky)建议是使用 extension.include_dirs 首先在 build_ext 中请求的事实,它在安装依赖项下载后调用。

class MyExt(setuptools.Extension):
    def __init__(self, *args, **kwargs):
        self.__include_dirs = []
        super().__init__(*args, **kwargs)

    @property
    def include_dirs(self):
        import numpy
        return self.__include_dirs + [numpy.get_include()]

    @include_dirs.setter
    def include_dirs(self, dirs):
        self.__include_dirs = dirs


my_c_lib_ext = MyExt(
    name="my_c_lib",
    sources=["my_c_lib/some_file.pyx"]
)

setup(
    ...,
    setup_requires=['cython', 'numpy'],
)

更新

另一个(更少,但我想仍然很老套)解决方案将覆盖 build 而不是 build_ext,因为我们知道 build_ext 是 [=14= 的子命令] 并将始终在安装时由 build 调用。这样,我们就不必触摸 build_ext 并将其留给 Cython。这在直接调用 build_ext 时也有效(例如,通过 python setup.py build_ext 在开发时就地重建扩展),因为 build_ext ensures all options of build are initialized, and by coincidence, Command.set_undefined_options first ensures the command has finalized (我知道,distutils 是一团糟)。

当然,现在我们误用了 build - 它运行属于 build_ext 终结的代码。但是,我仍然可能会使用此解决方案而不是第一个解决方案,以确保正确记录相关代码段。

import setuptools
from distutils.command.build import build as build_orig


class build(build_orig):

    def finalize_options(self):
        super().finalize_options()
        # I stole this line from ead's answer:
        __builtins__.__NUMPY_SETUP__ = False
        import numpy
        # or just modify my_c_lib_ext directly here, ext_modules should contain a reference anyway
        extension = next(m for m in self.distribution.ext_modules if m == my_c_lib_ext)
        extension.include_dirs.append(numpy.get_include())


my_c_lib_ext = setuptools.Extension(
    name="my_c_lib",
    sources=["my_c_lib/some_file.pyx"]
)

setuptools.setup(
    ...,
    ext_modules=[my_c_lib_ext],
    cmdclass={'build': build},
    ...
)

第一个问题,什么时候需要numpy?在设置期间(即调用 build_ext-functionality 时)和安装中使用模块时需要它。这意味着 numpy 应该在 setup_requires install_requires.

有以下替代方法可以解决设置问题:

  1. 使用 PEP 517/518(IMO 更直接)
  2. 使用 setupsetup_requires 参数并推迟导入 numpy 直到设置的要求得到满足(setup.py 开始时不是这种情况执行)

PEP 517/518-解决方案:

setup.py 旁边放一个 pyproject.toml-file ,内容如下:

[build-system]
requires = ["setuptools", "wheel", "Cython>=0.29", "numpy >= 1.15"]

定义构建所需的包,然后在setup.py文件夹中使用pip install .安装。这种方法的缺点是 python setup.py install 不再有效,因为它是 pip 读取 pyproject.toml。但是,我会尽可能使用这种方法。


推迟导入

这种方法比较复杂而且有点老套,但在没有 pip.

的情况下也可以工作

首先,让我们看一下到目前为止未成功的尝试:

pybind11-trick @chrisb 的“pybind11”技巧,可以在 here 中找到:在间接的帮助下,延迟对 import numpy 的调用,直到在设置阶段出现 numpy,即:

class get_numpy_include(object):

    def __str__(self):
        import numpy
        return numpy.get_include()
...
my_c_lib_ext = setuptools.Extension(
    ...
    include_dirs=[get_numpy_include()]
)

聪明!问题:它不适用于 Cython 编译器:在某个地方,Cython 将 get_numpy_include-object 传递给 os.path.join(...,...),它检查参数是否真的是一个字符串,它显然不是' t.

这可以通过从 str 继承来解决,但上面显示了长 运行 方法的危险 - 它不使用设计的机制,很脆弱并且很容易将来失败。

经典build_ext-solution

如下所示:

...
from setuptools.command.build_ext import build_ext as _build_ext

class build_ext(_build_ext):
    def finalize_options(self):
        _build_ext.finalize_options(self)
        # Prevent numpy from thinking it is still in its setup process:
        __builtins__.__NUMPY_SETUP__ = False
        import numpy
        self.include_dirs.append(numpy.get_include())

setupttools.setup(
    ...
    cmdclass={'build_ext':build_ext},
    ...
)

然而,此解决方案也不适用于 cython 扩展,因为 pyx 文件无法识别。

真正的问题是,pyx-文件最初是如何被识别的?答案是 this part of setuptools.command.build_ext:

...
try:
    # Attempt to use Cython for building extensions, if available
    from Cython.Distutils.build_ext import build_ext as _build_ext
    # Additionally, assert that the compiler module will load
    # also. Ref #1229.
    __import__('Cython.Compiler.Main')
except ImportError:
    _build_ext = _du_build_ext
...

这意味着 setuptools 如果可能会尝试使用 Cython 的 build_ext,并且由于模块的导入延迟到 build_ext 被调用,它发现 Cython 存在。

setup.py 开头导入 setuptools.command.build_ext 时情况有所不同 - Cython 尚不存在,并且使用了没有 cython 功能的回退。

混合 pybind11 技巧和经典解决方案

所以我们加一个间接寻址,这样就不用在setup.py开头直接导入setuptools.command.build_ext了:

....
# factory function
def my_build_ext(pars):
     # import delayed:
     from setuptools.command.build_ext import build_ext as _build_ext#
 
     # include_dirs adjusted: 
     class build_ext(_build_ext):
         def finalize_options(self):
             _build_ext.finalize_options(self)
             # Prevent numpy from thinking it is still in its setup process:
             __builtins__.__NUMPY_SETUP__ = False
             import numpy
             self.include_dirs.append(numpy.get_include())
     
    #object returned:
    return build_ext(pars)
...
setuptools.setup(
    ...
    cmdclass={'build_ext' : my_build_ext},
    ...
)

我在 this post 中找到了一个非常简单的解决方案:

Or you can stick to https://github.com/pypa/pip/issues/5761. Here you install cython and numpy using setuptools.dist before actual setup:

from setuptools import dist
dist.Distribution().fetch_build_eggs(['Cython>=0.15.1', 'numpy>=1.10'])

很适合我!