Как мне структурировать пакет 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
команда. Идея такая:
Я добавляю заголовки numpy, переопределив
finalize_options()
и делаюimport numpy
в теле функции, что позволяет избежать проблемы, что numpy не был доступен раньшеsetup()
устанавливает его.Если в системе доступен 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}
)