在不预先安装numpy的情况下将numpy.get_include()参数添加到setuptools

时间:2019-01-09 20:23:50

标签: python-3.x numpy cython setuptools

我目前正在开发一个使用cythonnumpy的python软件包,并且希望该软件包可以通过全新python安装中的pip install命令安装。所有依赖项应自动安装。我将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.htmlMake 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_dirssetup_requires的依赖关系之后,是否有办法在安装的后期评估install_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建议将cmdclass与自定义build_ext类一起使用。不幸的是,这破坏了cython扩展名的构建,因为cython也自定义了build_ext

3 个答案:

答案 0 :(得分:4)

我可能会采用@hoefling的解决方案:一个小瑕疵-执行顺序上的其他隐式依赖性(include_dirs之后必须是setup_requires)可能仅仅是一个学术问题。

不过,我还是要添加另一个hacky解决方案。但是首先,让我们了解为什么其他解决方案会失败。​​

第一个问题,何时需要numpy?在安装过程中(即在调用build_ext-funcionality时)以及在安装过程中(使用模块时)都需要它。这意味着numpy应该在setup_requires的{​​{1}}中,而应该在install_requires中。

让我们看一下迄今为止失败的尝试:

pybind11技巧

@chrisb的“ pybind11”技巧,可以在here中找到:在间接方法的帮助下,一个延迟到import numpy的调用,直到在设置阶段出现numpy为止,即: / p>

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对象传递到os.path.join(...,...),后者检查参数是否真的是字符串,显然不是。 t。

可以通过继承str来解决此问题,但是从长远来看,上面的方法显示了该方法的危险-它不使用设计的机制,很脆弱,将来很容易失败。 / p>

经典的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文件是如何首先被识别的?答案是setuptools.command.build_ext中的this part

...
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存在。

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

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

因此,我们添加一个间接访问,因此我们不必直接在setuptools.command.build_ext的开头导入setup.py

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

所以最后:选择您的毒药-似乎只有骇人听闻的方法来做!

答案 1 :(得分:1)

一个(不可靠的)建议是利用这样一个事实:首先在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'],
)

更新

另一种(较少,但我想还是很hacky)的解决方案将覆盖build而不是build_ext,因为我们知道build_extbuild的子命令,并且在安装时将始终由build调用。这样,我们不必触摸build_ext并将其留给Cython。当直接调用build_ext时(例如在开发过程中通过python setup.py build_ext重建扩展名时),这也是可行的,因为build_ext ensures all options of build are initialized和巧合的是Command.set_undefined_options first ensures the command has finalized(我知道,{ {1}}一团糟。

当然,现在我们误用了distutils-它运行属于build完成的代码。但是,我可能仍会选择该解决方案,而不是第一个解决方案,以确保正确记录了相关代码。

build_ext

答案 2 :(得分:1)

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

或者您可以坚持使用https://github.com/pypa/pip/issues/5761。在这里,您需要在实际安装之前使用setuptools.dist安装cython和numpy:

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

对我来说很好!