Как развернуть пакет Cython в PyPI

Для меня это оказалось довольно трудно, и я подумал, что неплохо было бы поделиться своим опытом в статье.

Структура проекта

Вот файловая структура, которую мы будем использовать для этого проекта:

.
├── classification_library
│   ├── data
│   ├── __init__.pxd
│   ├── __init__.pyx
│   └── test_classification_library.py
├── MANIFEST.in
├── pyproject.toml
├── README.md
└── setup.py

Скрипт установки

setup.py  —  это скрипт установки пакетов Python. Он указывает инструментам для работы с пакетами, что делать с кодом.

Первым делом давайте импортируем необходимые функции из инструментов установки setuptools.

from setuptools import find_packages, setup

Думаю, вы знаете, что делает setup, раз уж имеете дело с пакетом Cython. Если же вы не работали раньше с find_packages, знайте: делает он именно то, о чём можно догадаться по его названию  —  выполняет поиск пакетов. Зачем он нам нужен, станет ясно, когда увидим вызов функции setup.

Но прежде чем вызывать setup, нужно импортировать кое-что ещё:

from Cython.Build import cythonize
import numpy as np

cythonize нужен, чтобы преобразовать код Cython в C, а numpy  —  потому что код на С, генерирующий Cython, зависит от него.

Кроме того, нам нужно загрузить README, который будет отображаться на странице пакета в PyPI. Это можно сделать с помощью, например, такого кода:

with open("README.md", 'r') as f:
    long_description = f.read()

После добавления вызова setup этот файл будет выглядеть вот так:

from setuptools import find_packages, setup

import numpy as np
from Cython.Build import cythonize

with open("README.md", 'r') as f:
    long_description = f.read()

setup(
    name="classification_library",
    version="0.0.3",
    packages=find_packages(),
    author="Arin Khare",
    description="A classification library using a novel audio-inspired algorithm.",
    long_description=long_description,
    long_description_content_type='text/markdown',
    url="https://github.com/lol-cubes/classification-library",
    ext_modules=cythonize(["classification_library/__init__.pyx"]),
    include_dirs=np.get_include(),
    install_requires=[
        'numpy>=1.19.2',
        'PyObjC;platform_system=="Darwin"',
        'PyGObject;platform_system=="Linux"',
        'playsound==1.2.2'
    ]
)

Обратите внимание на последние три аргумента вызова setup: ext_modules указывает расширения C для пакета; include_dirs необходимо указать, чтобы можно было скомпилировать зависимые от numpy расширения C; а install_requires указывает пакеты, от которых зависит classification_library.

Вместо Cython.Build.cythonize можно использовать setuptools.Extension, чтобы генерировать и компилировать C-расширения, но Cython установить всё равно придётся.

Указание зависимостей компоновки

Возможно, вас уже озадачило импортирование numpy и Cython в setup.py. Если вы ещё не успели об этом подумать, давайте разберёмся, почему это может быть проблемой.

Когда мы устанавливаем что-нибудь с помощью pip, временно загружается setup.py, в котором запускаются определённые команды (например, python3 setup.py <command>), чтобы нужные файлы оказались в нужном месте sys.path, доступные для импортирования. Но операторы импорта будут выдавать ошибки, если только у человека, который устанавливает пакет с помощью pip, случайно не окажутся Cython и numpy. Поэтому нужно указать их в pyproject.toml. Таким образом pip временно устанавливает эти пакеты, которые, в свою очередь, необходимы для установки нашего пакета. Вот как выглядит мой pyproject.toml:

[build-system]
requires = ["setuptools", "wheel", "numpy>=1.19.0", "Cython>=0.29.21"]
build-backend = "setuptools.build_meta"

Последняя строка фактически указывает диспетчеру пакетов на setuptools.build_meta, давая знать, что делать с другой информацией, которую мы здесь видим.

Возможности pyproject.toml не ограничиваются простым указанием зависимостей компоновки, но это уже тема другой статьи.

Включение файлов, не относящихся к Python

Наличие файла MANIFEST.in очень важно, ведь он указывает файлы, не относящиеся к Python. Если они не будут включены, то инструменты для работы с пакетами их проигнорируют. Не нужно включать файлы .pyx, потому что они компилируются в файлы .so, которые распознаются как файлы Python. А вот файлы .pxd будут проигнорированы, если их не включить. С учётом всего этого мой MANIFEST.in будет выглядеть примерно так:

include classification_library/__init__.pxd
include classification_library/data

Генерирование файлов Source Archives и Wheel

Если у вас ещё не установлен wheel, установите его с помощью:

pip install wheel

Теперь можно запустить команду:

python3 setup.py sdist bdist_wheel

Вы должны увидеть каталог dist с файлами, оканчивающимися на .tar.gz и .whl. Если вы на Linux, и файлы wheel оканчиваются на cp38-linux_x86_64.whl, придётся проделать немного дополнительной работы.

По техническим причинам PyPI не поддерживает загрузку такого рода файлов wheel. Чтобы поменять формат на правильный, надо установить auditwheel:

pip install auditwheel

Затем можно запустить такую команду:

auditwheel repair dist/<your_package_name>-cp38-cp38-linux_86_64.whl

А дальше перемещаем файлы, сгенерированные auditwheel, в каталог dist и удаляем старые wheels:

mv wheelhouse/* dist
rm dist/*-cp38-cp38-linux_x86_64.whl

Загрузка на PyPI

Загрузка файлов на PyPI осуществляется с помощью пакета twine.

Уверен, что вы уже запустили эту команду, но на всякий случай подскажу, как установить twine:

pip install twine

Создаём учётную запись, если у вас её ещё нет.

Вы можете указывать имя пользователя и пароль при каждой загрузке пакета, а можете поместить их в файл ~/.pypirc вот так:

[pypi]
username = <your_username>
password = <your_password>

Хотите обезопасить себя ещё больше? Тогда можете получить токен API из настроек своей учётной записи. Инструкции можно найти там же на странице настроек.

Теперь с помощью команды:

python3 -m twine upload --repository pypi dist/*

можно загружать файлы на PyPI. Если всё прошло гладко, вы сможете ввести в терминал:

pip install <your_cython_package>

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

Спасибо вам за внимание и удачи в приключениях с Cython!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Arin Khare: How to Deploy a Cython Package to PyPI