Добавьте аргумент numpy.get_include () в setuptools без предварительно установленного numpy - PullRequest
0 голосов
/ 09 января 2019

В настоящее время я разрабатываю пакет python, который использует cython и numpy, и я хочу, чтобы пакет можно было установить с помощью команды 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 и Заставьте distutils искать пустые заголовочные файлы в правильном месте для рациональных вариантов.)

Это плохо. Теперь пользователь не может вызвать 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"]
)

Статья Как начать установку numpy в setup.py предлагает использовать cmdclass с пользовательским классом build_ext. К сожалению, это нарушает сборку расширения cython, поскольку cython также настраивает build_ext.

Ответы [ 2 ]

0 голосов
/ 11 января 2019

Я бы, вероятно, согласился с решением @hoefling: небольшой недостаток - дополнительная неявная зависимость в порядке выполнения (include_dirs должна идти после setup_requires), вероятно, является чисто академической проблемой.

Тем не менее, я бы добавил еще одно хакерское решение. Но сначала давайте разберемся, почему другие решения терпят неудачу.

Первый вопрос, когда numpy нужен? Это необходимо во время установки (т. Е. Когда вызывается build_ext-funcionality) и при установке, когда используется модуль. Это означает, что numpy должно быть в setup_requires и в install_requires.

Давайте пока посмотрим на неудачные попытки:

pybind11-трик

@ chrisb's "pybind11" -трюк, который можно найти здесь : с помощью косвенного обращения вы задерживаете вызов на 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 -объект в os.path.join(...,...), который проверяет, является ли аргумент действительно строкой, что, очевидно, не является.

Это можно исправить, унаследовав от str, но приведенное выше показывает опасности подхода в долгосрочной перспективе - он не использует разработанную механику, хрупок и может легко потерпеть неудачу в будущем.

классический build_ext -раствор

Что выглядит следующим образом:

...
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:

...
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 пытается использовать build_ext Cython, если это возможно, и поскольку импорт модуля задерживается до вызова 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},
    ...
)

Итак, в конце: выберите свой яд - кажется, есть только хакерские способы сделать это!

0 голосов
/ 10 января 2019

Одно (хакерское) предложение будет использовать тот факт, что 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 является подкомандой build и всегда будет вызываться build по установке. Таким образом, нам не нужно трогать build_ext и оставлять это Cython. Это также будет работать при непосредственном вызове build_ext (например, через python setup.py build_ext для перестроения расширений на месте во время разработки), поскольку build_ext гарантирует, что все опции build инициализируются , и, по совпадению, Command.set_undefined_options сначала гарантирует, что команда завершила (я знаю, 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},
    ...
)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...