我目前正在开发一个使用cython
和numpy
的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.html和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_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
。
答案 0 :(得分:4)
我可能会采用@hoefling的解决方案:一个小瑕疵-执行顺序上的其他隐式依赖性(include_dirs
之后必须是setup_requires
)可能仅仅是一个学术问题。
不过,我还是要添加另一个hacky解决方案。但是首先,让我们了解为什么其他解决方案会失败。
第一个问题,何时需要numpy
?在安装过程中(即在调用build_ext-funcionality时)以及在安装过程中(使用模块时)都需要它。这意味着numpy
应该在setup_requires
的{{1}}中,而应该在install_requires
中。
让我们看一下迄今为止失败的尝试:
@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功能的后退。
因此,我们添加一个间接访问,因此我们不必直接在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_ext
是build
的子命令,并且在安装时将始终由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'])
对我来说很好!