【问题标题】:Add numpy.get_include() argument to setuptools without preinstalled numpy在没有预安装 numpy 的情况下将 numpy.get_include() 参数添加到 setuptools
【发布时间】:2019-06-04 16:18:36
【问题描述】:

我目前正在开发一个使用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.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_dirs,例如,在解决了来自setup_requiresinstall_requires 的依赖项之后?我真的很喜欢自动解决所有依赖关系,我不希望用户输入多个pip install 命令。

以下 sn-p 有效,但未得到官方支持,因为它使用了一种未记录(和私有)的方法:

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 类的cmdclass。不幸的是,这破坏了cython 扩展的构建,因为cython 还自定义了build_ext

【问题讨论】:

  • @ead 这不起作用,因为cython 还自定义了build_ext。如果我使用建议的解决方案,cython 构建失败并显示Don't know how to compile my_c_lib/some_file.pyx,这意味着不再使用cython 的自定义build_ext 命令。
  • 我看不是那么直截了当...
  • 看看 pybind11 here 做了什么来推迟导入 - 尚未测试,但我认为类似的工作在这里。
  • pybind11 的诀窍在进行小改动后就可以工作了。我必须从os.PathLike 继承而不是object,因为cython 需要strbytesPathLike 对象,而str/bytes 不起作用,因为它们是不可变的。你想从你的评论中创建一个答案,我应该自己回答我的问题吗?

标签: python-3.x numpy cython setuptools


【解决方案1】:

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

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

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

PEP 517/518-解决方案:

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

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

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


推迟导入

这种方法更复杂,也有点老套,但在没有pip 的情况下也可以。

首先,让我们看一下迄今为止的不成功尝试:

pybind11 技巧 @chrisb 的“pybind11”-trick,可以在 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(...,...),它检查参数是否真的是一个字符串,显然不是。

这可以通过从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-extensions,因为 pyx-files 无法识别。

真正的问题是,pyx-files 最初是如何被识别的?答案是this partsetuptools.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-trick 和经典解决方案

所以让我们添加一个间接寻址,这样我们就不用在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},
    ...
)

【讨论】:

  • 子类化 build_ext 实际上是一个非常巧妙的想法 - 这让我想到了另一个建议,子类化 build 而不是 build_ext。并不是说它不是一种解决方法(我也更喜欢build_ext 的自定义最终确定),但是嗯。
  • 我很兴奋,因为您的pyproject.toml 解决方案似乎要简单得多,但是当我尝试使用它时,我仍然遇到numpy 由于尚未安装而无法导入的问题当setup.py 被处理时。我的 pyproject.toml: ```
  • @DanBlanchard 在这种情况下,您必须使用 pip 进行安装。
【解决方案2】:

一个 (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_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(我知道,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},
    ...
)

【讨论】:

  • 这是对我有用的解决方案,我想要pip install 和运行python setup.py build_ext
  • 在最新的python版本中,您可能需要将__builtins__.__NUMPY_SETUP__ = False替换为import builtins; builtins.__NUMPY_SETUP__ = False
【解决方案3】:

我在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'])

很适合我!

【讨论】:

    猜你喜欢
    • 2014-09-05
    • 2019-11-30
    • 2016-02-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-04
    • 1970-01-01
    • 2014-04-23
    • 1970-01-01
    相关资源
    最近更新 更多