Как мне структурировать пакет Python, содержащий код Cython


122

Я хотел бы создать пакет Python, содержащий код Cython . У меня код Cython работает нормально. Однако теперь я хочу знать, как лучше его упаковать.

Для большинства людей, которые просто хотят установить пакет, я хотел бы включить .cфайл, который создает Cython, и организовать setup.pyего компиляцию для создания модуля. Тогда пользователю не нужно устанавливать Cython для установки пакета.

Но для людей, которые могут захотеть изменить пакет, я также хотел бы предоставить .pyxфайлы Cython и каким-то образом также разрешить setup.pyсоздавать их с помощью Cython (чтобы этим пользователям был установлен Cython).

Как мне структурировать файлы в пакете, чтобы удовлетворить оба этих сценария?

Документация Cython дает небольшое руководство . Но в нем не говорится, как сделать сингл, setup.pyкоторый обрабатывает как случаи с Cython, так и без него.


1
Я вижу, что вопрос набирает больше голосов, чем любой из ответов. Мне любопытно узнать, почему люди могут находить ответы неудовлетворительными.
Craig McQueen

4
Я нашел этот раздел документации , который дает точный ответ.
Уилл

Ответы:


72

Я сам сделал это сейчас в пакете Python simplerandom( репозиторий BitBucket - EDIT: теперь github ) (я не ожидаю, что это будет популярный пакет, но это был хороший шанс изучить Cython).

Этот метод основан на том факте, что при создании .pyxфайла Cython.Distutils.build_ext(по крайней мере, с Cython версии 0.14) всегда создается впечатление, что .cфайл создается в том же каталоге, что и исходный .pyxфайл.

Вот сокращенная версия, setup.pyкоторая, я надеюсь, показывает самое главное:

from distutils.core import setup
from distutils.extension import Extension

try:
    from Cython.Distutils import build_ext
except ImportError:
    use_cython = False
else:
    use_cython = True

cmdclass = {}
ext_modules = []

if use_cython:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.pyx"]),
    ]
    cmdclass.update({'build_ext': build_ext})
else:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.c"]),
    ]

setup(
    name='mypackage',
    ...
    cmdclass=cmdclass,
    ext_modules=ext_modules,
    ...
)

Я также отредактировал, MANIFEST.inчтобы убедиться, что mycythonmodule.cон включен в исходный дистрибутив (исходный дистрибутив, созданный с помощью python setup.py sdist):

...
recursive-include cython *
...

Я не использую mycythonmodule.c"ствол" контроля версий (или "по умолчанию" для Mercurial). Когда я делаю выпуск, мне нужно не забыть сделать python setup.py build_extпервый, чтобы убедиться, что mycythonmodule.cон присутствует и обновлен для распространения исходного кода. Я также делаю релизную ветку и фиксирую C-файл в ветке. Таким образом, у меня есть историческая запись файла C, который был распространен с этим выпуском.


Спасибо, это именно то, что мне нужно для проекта Pyrex, который я открываю! MANIFEST.in сбил меня с толку на секунду, но мне нужна была всего одна строчка. Я включаю файл C в систему управления версиями из интереса, но я понимаю, что вы считаете, что это не нужно.
chmullig

Я отредактировал свой ответ, чтобы объяснить, как файл C находится не в магистрали / по умолчанию, а добавлен в ветку выпуска.
Крейг Маккуин

1
@CraigMcQueen спасибо за отличный ответ, он мне очень помог! Однако мне интересно, желательно ли использовать Cython, когда он доступен? Мне кажется, что было бы лучше по умолчанию использовать предварительно сгенерированные файлы c, если пользователь явно не хочет использовать Cython, и в этом случае он может установить переменную окружения или что-то в этом роде. Это сделает установку более стабильной / надежной, потому что пользователь может получить разные результаты в зависимости от того, какую версию Cython он установил - он может даже не знать, что он установлен и что это влияет на сборку пакета.
Мартинсос

20

Добавление к ответу Крейга Маккуина: см. Ниже, как переопределить sdistкоманду, чтобы Cython автоматически компилировал ваши исходные файлы перед созданием исходного дистрибутива.

Таким образом, вы не рискуете случайно распространить устаревшие Cисточники. Это также помогает в случае, когда у вас ограниченный контроль над процессом распределения, например, при автоматическом создании распределений из непрерывной интеграции и т. Д.

from distutils.command.sdist import sdist as _sdist

...

class sdist(_sdist):
    def run(self):
        # Make sure the compiled Cython files in the distribution are up-to-date
        from Cython.Build import cythonize
        cythonize(['cython/mycythonmodule.pyx'])
        _sdist.run(self)
cmdclass['sdist'] = sdist

19

http://docs.cython.org/en/latest/src/userguide/source_files_and_compilation.html#distributing-cython-modules

Настоятельно рекомендуется распространять сгенерированные файлы .c, а также исходные коды Cython, чтобы пользователи могли установить ваш модуль без необходимости наличия Cython.

Также рекомендуется, чтобы компиляция Cython не была включена по умолчанию в распространяемой вами версии. Даже если у пользователя установлен Cython, он, вероятно, не захочет использовать его только для установки вашего модуля. Кроме того, его версия может отличаться от той, которую вы использовали, и может некорректно компилировать ваши исходники.

Это просто означает, что файл setup.py, который вы отправляете, будет обычным файлом distutils для сгенерированных файлов .c, для базового примера, который у нас был бы вместо этого:

from distutils.core import setup
from distutils.extension import Extension
 
setup(
    ext_modules = [Extension("example", ["example.c"])]
)

7

Самый простой - включить оба, но просто использовать c-файл? Включение файла .pyx - это хорошо, но в любом случае это не нужно, если у вас есть файл .c. Люди, которые хотят перекомпилировать .pyx, могут установить Pyrex и сделать это вручную.

В противном случае вам понадобится специальная команда build_ext для distutils, которая сначала создает файл C. Cython уже включает один. http://docs.cython.org/src/userguide/source_files_and_compilation.html

В этой документации не говорится, как сделать это условным, но

try:
     from Cython.distutils import build_ext
except ImportError:
     from distutils.command import build_ext

Должен справиться с этим.


1
Спасибо за Ваш ответ. Это разумно, хотя я предпочитаю, чтобы setup.pyможно было собирать непосредственно из .pyxфайла при установке Cython. Мой ответ тоже реализовал это.
Craig McQueen,

Что ж, в этом весь смысл моего ответа. Просто это был не полный setup.py.
Lennart Regebro

4

Включение сгенерированных (Cython) файлов .c довольно странно. Особенно, когда мы включаем это в git. Я бы предпочел использовать setuptools_cython . Когда Cython недоступен, он создаст яйцо со встроенной средой Cython, а затем построит ваш код, используя яйцо.

Возможный пример: https://github.com/douban/greenify/blob/master/setup.py


Обновление (2017-01-05):

Поскольку setuptools 18.0в использовании нет необходимости setuptools_cython. Вот пример создания проекта Cython с нуля без использования setuptools_cython.


устраняет ли это проблему, из-за которой Cython не устанавливается, даже если вы указываете это в setup_requires?
Камил Синди

также невозможно вставить 'setuptools>=18.0'setup_requires вместо создания метода is_installed?
Камил Синди

1
@capitalistpug Прежде всего , необходимо убедиться , что setuptools>=18.0установлен, то вам нужно только положить 'Cython >= 0.18'в setup_requires, и Cython будут установлены в процессе установки прогресса. Но если вы используете setuptools <18.0, даже если у вас конкретный cython в setup_requires, он не будет установлен, в этом случае вам следует рассмотреть возможность использования setuptools_cython.
McKelvin

Спасибо @McKelvin, это отличное решение! Есть ли какая-то причина, по которой мы должны использовать другой подход, с предварительной цитонизацией исходных файлов, рядом с этим? Я пробовал ваш подход, и он кажется несколько медленным при установке (установка занимает минуту, но сборка занимает секунду).
Мартинсос

1
@Martinsos pip install wheel. Тогда это должна быть причина 1. Сначала установите колесо и попробуйте снова.
McKelvin

2

Это сценарий установки, который я написал, который упрощает включение вложенных каталогов в сборку. Его нужно запускать из папки внутри пакета.

Структура Givig такая:

__init__.py
setup.py
test.py
subdir/
      __init__.py
      anothertest.py

setup.py

from setuptools import setup, Extension
from Cython.Distutils import build_ext
# from os import path
ext_names = (
    'test',
    'subdir.anothertest',       
) 

cmdclass = {'build_ext': build_ext}
# for modules in main dir      
ext_modules = [
    Extension(
        ext,
        [ext + ".py"],            
    ) 
    for ext in ext_names if ext.find('.') < 0] 
# for modules in subdir ONLY ONE LEVEL DOWN!! 
# modify it if you need more !!!
ext_modules += [
    Extension(
        ext,
        ["/".join(ext.split('.')) + ".py"],     
    )
    for ext in ext_names if ext.find('.') > 0]

setup(
    name='name',
    ext_modules=ext_modules,
    cmdclass=cmdclass,
    packages=["base", "base.subdir"],
)
#  Build --------------------------
#  python setup.py build_ext --inplace

Удачной компиляции;)


2

Я придумал простой способ:

from distutils.core import setup

try:
    from Cython.Build import cythonize
except ImportError:
    from pip import pip

    pip.main(['install', 'cython'])

    from Cython.Build import cythonize


setup(…)

Просто установите Cython, если его не удалось импортировать. Вероятно, не стоит делиться этим кодом, но для моих собственных зависимостей он достаточно хорош.


2

Все остальные ответы либо полагаются на

  • Distutils
  • importing from Cython.Build, что создает проблему с курицей и яйцом между запросом cython via setup_requiresи его импортом.

Современное решение - использовать вместо этого setuptools, см. Этот ответ (для автоматической обработки расширений Cython требуется setuptools 18.0, т. Е. Он доступен уже много лет). Современный стандарт setup.pyс обработкой требований, точкой входа и модулем cython может выглядеть так:

from setuptools import setup, Extension

with open('requirements.txt') as f:
    requirements = f.read().splitlines()

setup(
    name='MyPackage',
    install_requires=requirements,
    setup_requires=[
        'setuptools>=18.0',  # automatically handles Cython extensions
        'cython>=0.28.4',
    ],
    entry_points={
        'console_scripts': [
            'mymain = mypackage.main:main',
        ],
    },
    ext_modules=[
        Extension(
            'mypackage.my_cython_module',
            sources=['mypackage/my_cython_module.pyx'],
        ),
    ],
)

Импорт из Cython.Buildво время настройки вызывает у меня ImportError. Установочные инструменты для компиляции pyx - лучший способ сделать это.
Carson Ip

1

Самый простой способ, который я нашел, используя только setuptools вместо distutils с ограниченными возможностями, - это

from setuptools import setup
from setuptools.extension import Extension
try:
    from Cython.Build import cythonize
except ImportError:
    use_cython = False
else:
    use_cython = True

ext_modules = []
if use_cython:
    ext_modules += cythonize('package/cython_module.pyx')
else:
    ext_modules += [Extension('package.cython_module',
                              ['package/cython_modules.c'])]

setup(name='package_name', ext_modules=ext_modules)

Фактически, с помощью setuptools нет необходимости в явном импорте try / catched из Cython.Build, см. Мой ответ.
bluenote10

0

Я думаю, что нашел довольно хороший способ сделать это, предоставив специальную build_extкоманду. Идея такая:

  1. Я добавляю заголовки numpy, переопределяя finalize_options()и выполняя их import numpyв теле функции, что позволяет избежать проблемы недоступности numpy до setup()его установки.

  2. Если cython доступен в системе, он check_extensions_list()подключается к методу команды и путем cythonизирует все устаревшие модули cython, заменяя их расширениями C, которые впоследствии могут обрабатываться этим build_extension() методом. Мы просто предоставляем последнюю часть функциональности в нашем модуле: это означает, что если cython недоступен, но у нас есть расширение C, оно все равно работает, что позволяет вам делать исходные дистрибутивы.

Вот код:

import re, sys, os.path
from distutils import dep_util, log
from setuptools.command.build_ext import build_ext

try:
    import Cython.Build
    HAVE_CYTHON = True
except ImportError:
    HAVE_CYTHON = False

class BuildExtWithNumpy(build_ext):
    def check_cython(self, ext):
        c_sources = []
        for fname in ext.sources:
            cname, matches = re.subn(r"(?i)\.pyx$", ".c", fname, 1)
            c_sources.append(cname)
            if matches and dep_util.newer(fname, cname):
                if HAVE_CYTHON:
                    return ext
                raise RuntimeError("Cython and C module unavailable")
        ext.sources = c_sources
        return ext

    def check_extensions_list(self, extensions):
        extensions = [self.check_cython(ext) for ext in extensions]
        return build_ext.check_extensions_list(self, extensions)

    def finalize_options(self):
        import numpy as np
        build_ext.finalize_options(self)
        self.include_dirs.append(np.get_include())

Это позволяет просто записывать setup()аргументы, не беспокоясь об импорте и о том, доступен ли cython:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.