Як я повинен структурувати пакет Python, що містить код Cython


122

Я хотів би зробити пакет Python, що містить деякий код Cython . У мене Cython-код добре працює. Однак зараз я хочу знати, як найкраще його упакувати.

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

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

Як я повинен структурувати файли в пакеті для задоволення обох цих сценаріїв?

Документація на Cython дає невеликі вказівки . Але це не говорить про те, як зробити сингл, setup.pyякий обробляє як з / без справ Cython.


1
Я бачу, що питання отримує більше голосів, ніж будь-яка відповідь. Мені цікаво знати, чому люди можуть знайти відповіді незадовільними.
Крейг МакКуїн

4
Я знайшов цей розділ документації , який точно дає відповідь.
Буде чи

Відповіді:


72

Я це робив сам зараз у пакеті Python simplerandom( BitBucket repo - 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керувати версіями "trunk" (або "default" для 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
Дякую за вашу відповідь. Це розумно, хоча я вважаю за краще, якщо при встановленні Cython setup.pyможна будувати безпосередньо з .pyxфайлу. Моя відповідь це також реалізувала.
Крейг МакКуїн

Ну, ось і вся суть моєї відповіді. Це був просто не повний setup.py.
Леннарт Регебро

4

У тому числі (Cython) .c файли .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, навіть у конкретному цитоні в setup_requires, він не буде встановлений, у цьому випадку ви повинні розглянути можливість використання setuptools_cython.
МакКелвін

Дякую @McKelvin, це здається чудовим рішенням! Чи є якась причина, чому ми повинні використовувати інший підхід із попередньою цитонізацією вихідних файлів, поряд із цим? Я спробував ваш підхід, і він здається дещо повільним при установці (потрібна хвилина, щоб встановити, але будується за секунду)
Мартінсос

1
@Martinsos pip install wheel. Тоді це має бути причиною 1. Спершу встановіть колесо та спробуйте ще раз.
Маккельвін

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
  • імпорт з Cython.Build, що створює проблему між куркою та яйцем між необхідністю цитону через setup_requiresта імпортом.

Сучасне рішення - замість цього встановити setuptools, дивіться цю відповідь (автоматична обробка розширень Cython вимагає setuptools 18.0, тобто вона доступна вже багато років). Сучасний стандарт setup.pyз керуванням вимогами, точкою входу та модулем цитона може виглядати так:

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. Налаштування setuptools для компіляції pyx - це найкращий спосіб зробити це.
Карсон Іп

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 немає необхідності в явному імпорті спробу / захоплення Cython.Build, див. Мою відповідь.
bluenote10

0

Я думаю, що я знайшов досить вдалий спосіб зробити це, надавши власну build_extкоманду. Ідея така:

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

  2. Якщо cython доступний у системі, він check_extensions_list()підключається до методу команди та цитонізує всі застарілі модулі 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()аргументи, не турбуючись про імпорт та про те, чи є в наявності цитон:

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.