Разработка идеального pypi пакета с поддержкой разных версий python
- воскресенье, 12 января 2020 г. в 00:21:36
Это небольшой мануал/история о том, как создать "идеальный" pypi пакет для python, который каждый желающий сможет установить заветной командой:
pip install my-perfect-package
Ориентирована на новичков, но призываю и профессионалов высказать свое мнение, как можно улучшить "идеальный" пакет. Поэтому прошу под кат.
Буду исходить из следующих требований:
Хороших идей не было, поэтому тему выбрал избитую и очень популярную — работа со системами счисления. Первая версия должна уметь переводить числа в римские и обратно. Для мануала сложнее и не нужно. Ах, да, самое важное — это название: numsys — как расшифровка numeral systems. numeral-system-py.
Взял python3.7 и первым делом написал тесты с заглушками функций (мы ведь все за TDD) с использованием стандартного модуля unittests.
Делаю следующую структуру проекта:
src/
numeral-system/
__init__.py
roman.py
tests
__init__.py
test_roman.py
Тесты в пакет класть не буду, поэтому отделяю зёрна от плевел. Изначально папку src/
не создавал, но дальнейшее развитие показало, что мне так удобнее оперировать. Это не обязательно, поэтому по желанию.
Для запуска решил использовать pytest — он умеет отлично работать с тестами из стандартного модуля. Выглядит, возможно, немного нелогично, но стандартный модуль для тестов мне кажется казался чуть удобнее. Сейчас бы я советовал использовать pytest стиль написания.
Но, поговаривают, что ставить pytest
(как и любые другие зависимости) в системный python — не очень умная идея...
Можно использовать только virtualenv и requirements.txt
. Можно быть прогрессивным и использовать poetry. Я же, пожалуй, воспользуюсь tox — средство для упрощения автоматизации и тестирования, который также позволит мне управлять зависимостями.
Создаю простой конфиг tox.ini
и устанавливаю pytest
:
[tox]
envlist = py37 ; запускать на одной предопределенной среде, а именно python3.7
[testenv] ; секция описания тестового окружения
deps = секция `deps` описываются зависимости, которые требуется доставить.
-r requirements.txt ; доставить зависимости самого пакета
-r requirements-test.txt ;
commands = pytest ; запускаем тесты
Изначально, я явно указывал зависимости, но практика интеграции со сторонними сервисами показала, что лучшим способом будет все-таки хранение зависимостей в requirements.txt
файле.
Возникает очень тонкий момент. Фиксировать актуальную на момент разработки версию или всегда ставить последнюю?
Если фиксировать, то при установке могут возникнуть конфликты между пакетами из-за различных версий используемых зависимостей. Если же не фиксировать, то пакет может неожиданно перестать работать. Последняя ситуация очень неприятная для конечных продуктов, когда в одну ночь все билды могут "покраснеть" из-за минорного обновления неявной зависимости. И по закону Мерфи это произойдет в день релиза.
Поэтому для себя выработал правило:
Пишу тесты!
Заполняю тело функций, добавляю комментарии и заставляю тесты корректно выполняться.
На этом моменте обычно большинство разработчиков останавливается (я все-таки верю, что все пишут тесты =), публикуют пакет и отгребают баги. Но я иду дальше и прыгаю в кроличью нору.
В конфигурации tox
указываю запуск тестов на всех интересующих версиях python:
[tox]
envlist = py{27,35,36,37,38}
С помощью pyenv доставляю нужные версии к себе локально, чтобы tox
мог их найти и создать тестовые среды.
Добавлю замер покрытия кода — для этого есть отличный пакет coverage и не менее прекрасная интеграция с pytest — pytest-cov.
requirements-test.txt
выглядит теперь так:
six=1.13.0
pytest=4.6.7
pytest-cov=2.8.1
parameterized=0.7.1
Согласно вышеуказанному правилу, фиксирую версии пакетов, которые используются для запуска тестов.
Меняю команду запуска тестов:
deps =
-r requirements.txt
-r requirements-test.txt
commands = pytest \
--cov=src/ \
--cov-config="{toxinidir}/tox.ini" \
--cov-append
Делаю сбор статистики покрытия для всего кода из папки src/
— самого пакета (numeral_system/) и обязательно для кода тестов (tests/) — я же не хочу, чтобы сами тесты содержали не исполняющиеся части?
Командой --cov-append
всю собранную статистику для каждого вызова под различной версией python суммирую в одну, потому что покрытие для второго и третьего питона может быть различным (привет зависимый от версии код и модуль six!), но по итогу в сумме давать 100%. Простой пример:
if sys.version_info > (3, 0):
# Python 3 code in this block
else:
# Python 2 code in this block
Добавляю новую среду для создания coverage отчета.
[testenv:coverage_report]
deps = coverage
commands =
coverage html ; данные уже есть, построим отчет
coverage report --include="src/*" --fail-under=100 -m ; падать если не 100% покрытие
И добавляю в список сред после выполнения тестов на всех версиях питона.
[tox]
envlist =
py{27,35,36,37,38}
coverage_report
После запуска команды tox
в корне проекта должна появится папка htmlconv
содержащая файл index.html
с красивым отчетом.
Для заветного бейджа в 100% интегрирую с сервисом codecov, который сам уже сделает интеграцию с github
и позволит просмотреть историю изменения покрытия кода. Для этого, конечно же, придется завести там аккаунт.
Итоговая среда запуска выглядит следующим образом:
[testenv:coverage_report]
deps =
coverage==5.0.2
codecov==2.0.15
commands =
coverage html
coverage report --include="src/*" --fail-under=100 -m
coverage xml
codecov -f coverage.xml --token=2455dcfa-f9fc-4b3a-b94d-9765afe87f0f ; Токен моего проекта в codecov, смотреть в аккаунте
Осталось теперь только добавить ссылку на бейдж в README.rst
:
|Code Coverage|
.. |Code Coverage| image:: https://codecov.io/gh/zifter/numeral-system-py/branch/master/graph/badge.svg
:target: https://codecov.io/gh/zifter/numeral-system-py
Много анализаторов не бывает, потому что они, по большей части, дополняют друг другу. Поэтому буду интегрировать популярные статические анализаторы, которые проверят на соответствие PEP8, найдут потенциальные проблемы и пофиксят все баги единообразным образом отформатируют код.
Сразу следует продумать, где указывать параметры для тонкой настройки анализаторов. Для этого можно использовать файл tox.ini
, setup.cfg
, единый кастомный файл или же конкретные файлы для анализаторов. Я решил воспользоваться непосредственно tox.ini
, так как можно просто копировать tox.ini
для будущих проектов.
isort — утилита для форматирования импортов.
Создаю следующую среду для запуска isort
в режиме форматирования кода.
[testenv:isort]
changedir = {toxinidir}/src
deps = isort==4.3.21
commands = isort -y -sp={toxinidir}/tox.ini
К сожалению, isort
нельзя указать папку для форматирования. Поэтому приходится менять директорию запуска через changedir
и указывать путь к файлу с настройками -sp={toxinidir}/tox.ini
. Ключ -y
нужен, чтобы отключить интерактивный режим.
Для запуска в тестах нужен режим проверки — для этого есть флаг --check-only
:
[testenv:isort-check]
changedir = {toxinidir}/src
deps = isort==4.3.21
commands = isort --check-only -sp={toxinidir}/tox.ini
Далее интегрирую с форматером кода black. Делаю по аналогии с isort
:
[testenv:black]
deps = black==19.10b0
commands = black src/
[testenv:black-check]
deps = black==19.10b0
commands = black --check src/
Все хорошо работает, но возникает конфликт с isort
— есть различие в форматировании импортов.
В одном из комментариев нашел минимально совместимую настройку isort
, которой и воспользовался:
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
Далее интегрирую со статическими анализаторами flake8.
[testenv:flake8-check]
deps = flake8==3.7.9
commands = flake8 --config=tox.ini src/
Снова возникают проблемы с интеграцией с black
. Приходится добавить тонкую настройку, которую, собственно, и рекомендует сам black
:
[flake8]
max-line-length=88
ignore=E203
К сожалению, с первого раза не сработало. Упало с ошибкой E231 missing whitespace after ','
, пришлось добавить в игнор и эту ошибку:
[flake8]
max-line-length=88
ignore=E203,E231
Интегрирую со статическими анализаторами кода pylint
[testenv:pylint-check]
deps =
{[testenv]deps} # pylint проверят зависимости, поэтому следует их устанавливать
pylint==2.4.4
commands = pylint --rcfile=tox.ini src/
Сразу же сталкиваюсь со странными ограничениями — имена функций в 30 символов (да, я пишу очень длинные имена тестовых методов) и предупреждения на наличие TODO
в коде.
Приходится добавить пару исключений:
[MESSAGES CONTROL]
disable=fixme,invalid-name
Так же неприятный момент в том, что разработчики pylint
уже похоронили python2.7
и не развивают больше пакет для него. Поэтому проверки стоит запускать на актуальном пакете для python3.7
.
Добавляю соответствующую строчку в конфигурацию:
[tox]
envlist =
isort-check
black-check
flake8-check
pylint-check
py{27,35,36,37,38}
coverage_report
basepython = python3.7
Это так же важно для запуска тестов на различных платформах, так как дефолтная версия питона в системах CI различная.
Интегрирую с appveyor — CI под windows. Первичная настройка простая — все можно сделать в интерфейсе, затем скачать yaml файл и закоммитеть его в репозиторий.
version: 0.0.{build}
install:
- cmd: >-
C:\\Python37\\python -m pip install --upgrade pip
C:\\Python37\\pip install tox
build: off
test_script:
- cmd: C:\\Python37\\tox
Здесь я явно указываю версию python3.7
, так как по умолчанию будет использован python2.7
(и tox
так же будет использовать эту версию, хоть я явно и указал python3.7
).
Ссылка на бейдж, как обычно, добавляется в README.rst
|Build status Appveyor|
.. |Build status Appveyor| image:: https://ci.appveyor.com/api/projects/status/github/zifter/numeral-system-py?branch=master&svg=true
:target: https://ci.appveyor.com/project/zifter/numeral-system-py
После, интегрирую с Travis CI — CI под Linux (и под MacOS c Windows, но Python builds are not available on the macOS and Windows environments
. Настройка чуть сложнее, так как конфигурационный файл будет использоваться непосредственно из репозитория. Пару итераций проб и ошибок — конфигурация готова. Сквошу в один красивый коммит и merge request готов.
language: python
python: 3.8 #
dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069)
addons:
apt:
sources:
- deadsnakes
packages:
- python3.5
- python3.6
- python3.7
- pypy
install:
- pip install tox
script:
- tox
(Риторический вопрос: И почему CI проектам так нравится yaml формат?)
Указываю версию python3.8
, так как установить ее через addon
корректно не получилось, а Travis CI
успешно создает virtualenv
с указанной версии.
Люди, знакомые с Travis CI
, могут вопросить, почему таким образом явно не указать версии python? Ведь Travis CI
создает автоматически virtualenv
и выполнит нужные команды в нем.
Причина в том, что нам нужно собрать данные по покрытию кода со всех версий. Но тесты будут запущены в разных джобах параллельно, из-за чего собрать общий отчет по покрытию не получится.
Конечно же, я уверен, что чуть больше разобравшись и это можно исправить.
По традиции, ссылка на бейдж так же добавляется в README.rst
|Build Status Travis CI|
.. |Build Status Travis CI| image:: https://travis-ci.org/zifter/numeral-system-py.svg?branch=master
:target: https://travis-ci.org/zifter/numeral-system-py
Думаю, каждый python разработчик хоть раз пользовался сервисом — readthedocs.org. Мне кажется, что это лучший сервис для хостига своей документации.
Воспользуюсь стандартным средством для генерации документации Sphinx. Выполняю шаги из стартового мануала и получаю следующую структуру:
src/
docs/
build/ # здесь будет располагаться документация в html формате
source/ # исходники для генерации документации
_static/ # сюда положим статику, например, картинки
_templates/ # шаблоны для генерации документации
conf.py # настройка генерации документов
index.rst # описание стартовой страницы
make.bat
Makefile # make для сборки с помощью make
Далее нужно проделать минимальные шаги для настройки:
github
по умолчанию предлагает создать README.md
файл в формате Markdown, когда как sphinx
по умолчанию предлагает использовать ReStructuredText.Поэтому пришлось переписать его в формате .rst
. А если бы хоть раз дочитал до конца стартовый мануал, то понял, что .sphinx
умеет и в Markdown
Включаю файл README.rst
в index.rst
.. include:: ../../README.rst
conf.py
. Это позволит sphinx
делать импорты нашего кода для анализа.import os
import sys
sys.path.insert(0, os.path.abspath('./../../src'))
import numeral_system
docs/source/api-docs
и закидываю туда файл описания каждого модуля. Документация должна автомагически сгенерироваться из комментариев:Roman numeral system
=========================
.. automodule:: numeral_system.roman
:members:
После этого проект готов явить миру свое описание. Нужно создать аккаунт (лучше через аккаунт на github
) и импортировать свой проект, подробные шаги описаны в инструкции.
По традиции создаю среду в tox
:
[testenv:gen_docs]
deps = -r docs/requirements.txt
commands =
sphinx-build -b html docs/source/ docs/build/
Использую команду sphinx-build
явно, вместо make
, так как ее под Windows нет. А я не хочу нарушать принцип о кроссплатформенной разработке.
Как только сделанные изменения замержены, readthedocs.org
автоматически соберет документацию и опубликует.
Но… Build failed
. Я не зафиксировал версии sphinx
и sphinx_rtd_theme
, и ожидал что readthedocs.org
возьмет актуальные версии. Но это не так. Фиксирую:
sphinx==2.3.1
sphinx_rtd_theme==0.4.3
И создаю специальный конфиг файл .readthedocs.yml
для readthedocs.org
, в котором описываю среду для запуска билда:
python:
version: 3.7
install:
- requirements: docs/requirements.txt
- requirements: requirements.txt
Вот здесь как раз и пригодился тот факт, что зависимости лежат в requirements.txt
файлах. Дожидаюсь билда и документация становится доступной.
Снова добавляю бейдж:
|Docs|
.. |Docs| image:: https://readthedocs.org/projects/numeral-system-py/badge/?version=latest&style=flat
:target: https://numeral-system-py.readthedocs.io/en/latest/
Стоит подумать о выборе лицензии для пакета.
Это очень обширная тема, поэтому ознакомился c этой статьей. В принципе, выбор стоит между MIT и Apache 2.0. Мне понравилась удачно вырванная из контекста фраза:
MIT предлагают использовать лишь для небольших проектов
Согласен полностью, так и поступлю Если планы поменяются, можно без проблем сменить лицензию (правда, предыдущие версии будут под старой).
Опять же, добавлю бейдж богу бейджей:
|License|
.. |License| image:: https://img.shields.io/badge/License-MIT-yellow.svg
:target: https://opensource.org/licenses/MIT
Здесь можно найти бейджи для всех лицензий.
Для начала нужно завести аккаунт на pypi.org. Затем приступить к подготовке пакета.
Нужно корректно описать конфигурацию для установки\сборки пакета. Я следовал инструкции. Есть возможность задать данные через setup.py
, но некоторые параметры задать возможности нет. Поэтому воспользоваться setup.cfg
файлом, в котором можно указать все нюансы. Нашел небольшой шаблон того, как заполнять этот файл. По итогу использую и тот и тот файл — так удобнее.
Этот файл также можно использовать для конфигурации pylint
, flake8
и прочих настроек, но я так не делал.
Снова пишу среду, которая поможет мне собрать необходимый пакет:
[testenv:build_wheel]
skip_install = True
deps = ; зависимости для сборки проекта
wheel
docutils
pygments
commands =
python -c 'import shutil; (shutil.rmtree(p, ignore_errors=True) for p in ["build", "dist"]);'
python setup.py sdist bdist_wheel
Зачем я удаляю папки с помощью python? Хочу соблюсти требование о кроссплатформенности разработки, удобного пути сделать это под Windows и Unix нет.
Запускаю тестовую среду:
tox -e build_wheel
В результате в папке dist
получаю:
dist/
numeral-system_py-0.1.0.tar.gz
numeral_system-py-0.1.0-py2.py3-none-any.whl
Не совсем.
Для начала стоит проверить, что пакет работает корректным образом. Залью в тестовый репозиторий пакетов. Поэтому нужно завести еще один аккаунт, но уже на test.pypi.org.
Использую для этого пакет twine — инструмент для заливки артефактов в PyPi.
[testenv:test_upload]
skip_install = True
deps = twine ; ставлю последнюю версию
commands =
python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
Изначально проект назывался numsys
, но при попытке заливки столкнулся с тем, что пакет с таким именем уже есть! И что самое обидное — он тоже умеет конвертировать в римские цифры:) Сильно не расстроился и переименовал в numeral-system-py
.
Теперь нужно установить пакет из тестового окружения. Проверку так же стоит автоматизировать:
[testenv:test_venv]
skip_install = True ; не следует ставить текущий пакет в эту среду
deps = ; пустые зависимости
commands =
pip install -i https://test.pypi.org/simple/ numeral-system-py
Теперь нужно только запустить:
tox -e test_venv
...
test_venv: commands_succeeded
congratulations :)
Вроде работает :)
Да.
Создаю среду для заливки в production репозиторий.
[testenv:pypi_upload]
skip_install = True
deps =
twine
commands =
python -m twine upload dist/*
И среду для production проверки.
[testenv:pypi_venv]
skip_install = True
deps = ; не ставим зависимости
commands =
pip install numeral-system-py
Проверяю простыми командами:
> virtualenv venv
> source venv/bin/activate
(venv) > pip install numeral-system-py
(venv) > python
>>> import numeral_system
>>> numeral_system.roman.encode(7)
'VII'
Все отлично!
Срезаю релиз на github, собираю пакет и заливаю в продовский pypi.
За время подготовки этой статьи была выпущена новая версия pytest, в которой, по факту, дропнули поддержку python 3.4 (на самом деле в пакете colorama). Вариантов было два:
В пользу второго варианта последним аргументом стало то, что и pip
дропнул поддержку 3.4 в версии 19.1.
Так же есть зафиксированные зависимости в виде анализаторов, форматеров и прочих сервисов. Эти зависимости можно обновлять одновременно. Если повезет, то отделаетесь только подъёмом версией, если нет — то придется подправить код или даже дописать настройки.
Не поддерживает python для MacOS и Windows. Есть сложности с запуском tox
под все версии питона в рамках одной джобы.
Нужно придерживаться семантического версионирования, а именно формата:
MAJOR.MINOR.PATCH
Версию пакета и некоторые другие параметры требуется указывать для установки пакета (в setup.cfg
или setup.py
) и в документации. Чтобы избежать дублирования, сделал указание только в пакете numeral_system/__init__.py
:
__version__ = '0.2.0'
А затем в setup.py
явно использую эту переменную
setup(version=numeral_system.__version__)
Тоже верно и для в docs/source/conf.py
release = numeral_system.__version__
Вышеописанное справедливо для любой мета информации — REAMDE.rst
, описания проекта, лицензии, имена автора и прочего.
Правда, это приводит к тому, что происходит импорт пакета в момент сборки, что может быть нежелательным.
Вначале работы меня смущал тот факт, что мне нужно указывать зависимости для пакета в requirements.txt
и setup.cfg
.
Затем почитал отличную статью, которая разъяснила — указывать нужно только в setup.cfg
.
Есть так же дублирование версий анализаторов. Думаю, это можно исправить созданием файла requirements-dev.txt
в будущем.
Может быть спорным то, что я выделил исходники в папку src/
. Я исходил из следующих соображений:
Но столкнулся с некоторыми неудобствами:
PyCharm
(или что вы там используете?) — указать папку src
, как папку исходников. Во время разработки пакета совершенно забыл про очень важную вещь — адекватная история изменений.
Сейчас в истории есть комментарии, за которые не сильно стыдно:
Add badge with supported version
Support for py38
Но есть и совсем треш:
Try fix py38 env creating
Try fix py38 env creating
Try fix py38 env creating
Fix check
Да, 3 коммита с одним и тем же комментарием! Уж очень хотел пофиксить запуск тестов на Travis CI.
Есть отличная статья по тому, как правильно оформлять комментарии. От себя хочу добавить — часто имеет смысл сковшить изменения, чтобы не было кучи однотипных коммитов без смысловой нагрузки.
Стоит пристально следить за историей изменений и за коммитами, так как это очень важно для дальнейшей поддержки проекта. Для удобства можно так же вести CHANGELOG
.
Многие сервисы по умолчанию предполагают, что используется Markdown
. Поэтому советовал бы использовать его, если нет явной необходимости в rst
.
Бейджи помогают быстрее оценить проект — поддерживаемые версии, статус тестов, номер стабильной версии, контакты для связи и многое другое. Это мощный социальный инструмент, который способствует более высокому качеству проекта.
Советую взглянуть на описание coverage.
Разница в функционале, синтаксисе и прочем между версиями, к сожалению, велика. Если модуль six
поможет в общих конструкциях, то использовать функционал из последних версий такие, как type hint
, asyncio
и прочих чудес — нет.
Правильным решением будет отказ от python2.7
, учитывая что поддержка закончилась. По субъективному впечатлению, оставив поддержку только python3.5+, можно значительно упростить разработку пакета. Например, не придется собирать в одной джобе результаты покрытия кода, что ускорит запуск тестов в CI благодаря параллельному выполнению. Я уже не говорю о различных фичах в самом языке.
На этом останавливаться все равно не стоит, можно сделать еще улучшения:
pylint
, pep8
, mccabe
. Выглядит, что так же устарел;Придется написать не одну библиотеку и поконтрибьютить не в один open source проект, чтобы разобраться со всеми тонкостями.
Во время подготовки к статье я прочитал не один десяток мануалов, посмотрел еще больше python проектов на github
, протестировал разные сервисы и их варианты конфигурации.
Но больше всего времени я провел на stackoverflow.com
и issues используемых проектов на github
так все эти сервисы так и норовят упасть с очередной непонятной ошибкой.
Добавление новых зависимостей в проект требует время на интеграцию и поддержку. Поэтому стоит рационально оценивать необходимость интеграций. А когда следует остановиться с интеграциями?..
Тем, кто заинтересовался, советую так же прочитать статью с похожей тематикой.
Перед завершением своей статьи нашел очень похожую, которую так же рекомендую посмотреть.
Проект можно потрогать на github.
Спасибо!