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

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

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

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

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

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

9 ответов

Решение

Я сделал это сам сейчас, в пакете Python simplerandom( Репозиторий BitBucket - РЕДАКТИРОВАТЬ: теперь 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 для управления версиями 'trunk' (или 'default' для Mercurial). Когда я делаю релиз, мне нужно помнить, чтобы сделать python setup.py build_ext во-первых, чтобы убедиться, что mycythonmodule.c присутствует и актуален для распространения исходного кода. Я также делаю ветку релиза и фиксирую файл C в ветке. Таким образом, у меня есть историческая запись файла C, который был распространен с этим выпуском.

Добавление к ответу Крейга МакКуина: см. Ниже о том, как переопределить 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

http://docs.cython.org/src/reference/compilation.html

Настоятельно рекомендуется распространять сгенерированные файлы.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"])]
)

Проще всего включить оба, но просто использовать 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

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

Включать (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,

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

  • Distutils
  • импорт из Cython.Build, которая создает проблему курицы и яйца между требованием Cython через 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'],
        ),
    ],
)

Простой взлом, который я придумал:

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, если он не может быть импортирован. Вероятно, не стоит делиться этим кодом, но для моих собственных зависимостей он достаточно хорош.

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

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

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

Самый простой способ, который я нашел, используя только 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)

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

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

  2. Если в системе доступен cython, он подключается к команде check_extensions_list() метод и по cythonize все устаревшие модули 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}
    )
Другие вопросы по тегам