C++/Go/Python
Game Developer

Идеальный pypi package с поддержкой разных версий питона

Основная статья на хабре: https://habr.com/ru/post/483512/ Далее - черновик

Очень давно хотел создать полезный open-source пакет для питона, который каждый желающий сможет установить заветной командой:

pip install my-perfect-package

Это небольшой мануал/история о том как это сделать в немного неформальном стиле с подробностями за ссылками. Расчитана на новичков и с желанием привлечь профессионалов с целью улучшения “идеального”.

Что значит идеальный?

В моем понимании, такой пакет должет удовлетворять следующим требованиям:

  • open source на github;
    каждый должен иметь возможность внести свой вклад в развитие и поблагодарить автора

  • поддержка всех актуальных версий питона (2.7, 3.3, 3.4, 3.5, 3.6, 3.7); питоны бывают разные, и где-то до сих пор активно пишут на 2.7, нужно быть полезным всем, а нам ведь не сложно

  • 100% покрытие юнит тестами;
    юнит тесты улучшает архитектуру, позволяет делать автоматизировать регрессионные проверки
    бейдж с заветным числом повышает ЧСВ и задает планку другим

  • использование CI
    автоматические проверки - это очень удобно! а еще куча клевых бейджей

    • запуск юнит тестов на всех платформах и на всех версиях питона;
      не стоит верить тем, кто утверждает что питон и устанавливаемые пакеты кроссплатформенные.

    • проверка код стайла;
      единый стиль улучшает читаемость и уменьшает количество пустых дискуссий в ревью

    • статический анализатора кода;
      автоматический поиск багов в коде? дайте два!

  • пакет полезен и делает мир лучше.
    самое сложно требование, так как судя по количеству пакетов в pypi (~180к) разработчики - дикие альтруисты и многое уже написано.

С чего начать?

Хороших идей не было, поэтому тему выбрал избитую и очень популярную - работа со системами счисления. Первая версия должна уметь переводить числа в римские и обратно. Для мануала сложнее и не нужно. Ах, да, самое важное - это название: numsys - как расшифровка numeral systems. numeral-system.

Тесты

Взял python3.7 и первым делом написал тесты с заглушками фукнций (мы ведь за TDD) с использованием стандартного модуля unittests. Для запуска решил использовать pytest (выглядит немного не логично, но стадартные тесты мне кажутся более практичными (ИМХО), а pytest умеет в плагины). Делаю следующую структуру:

numeral-system/
    __init__.py
    roman.py
tests
    __init__.py
    test_roman.py

Тесты в пакет класть не будем, поэтому отделяем ~зёрна от плевел~. И тут сразу встал вопрос, как запускать-то?

Как управлять зависимостями?

Можно быть старовером и использовать virtualenv. Можно быть прогрессивным и использовать poetry. Я буду чуть более консервативен и воспользуюсь tox. Создаю простой конфиг.

[tox]
envlist = py37  ; запускать на одной из предопределенной среде

[testenv]  ; секция описания
deps = pytest  ; ставим последнюю версию pytest
commands = pytest  ; запускаем

Далее заполняю тело функций и заставляю тесты проходить. На этом моменте обычно большинство разработчиков останавливается, публикуют пакет и отгребают баги. Но мы не такие, мы идем дальше.

Как управлять версиями?

В конфигурации tox указываю запуск тестов на всех актуальных версиях питона:

[tox]
envlist = py{27,34,35,36,37,py}

С помощью pyenv доставляю нужные версии к себе локально, чтобы tox их нашел и создал тестовые среды.

Где заветные 100%?

Добавлю замер покрытия кода - для этого есть отличный пакет coverage и не менее прекрасная интеграция с pytest - pytest-cov. Меняю команду запуска теста:

commands = pytest \           
    --cov=numeral-system/ \           
    --cov=tests/ \           
    --cov-config "{toxinidir}/tox.ini" \           
    --cov-append

Делаю сбор статистики покрытия для кода самого пакета (numeral-system/) и обязательно для кода тестов (tests/) - я же не хочу, чтобы сами тесты содержали неисполняющиеся части? Коммандой --cov-append всю собранную статистику для каждого вызова под различной версией питона суммирую в одну, потому что покрытие для второго и третьего питона может быть различным (привет зависимый от версии код и модуль 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="numeral-system/*,tests/*" --fail-under=100 -m

И добавляю в список сред после всех тестов.

[tox]
envlist =
    py{27,34,35,36,37,py}
    coverage_report

Для заветного бейджа в 100% интегрирую с codecov, который поможет также просмотреть историю измений покрытия и сделает интеграцию с github.

Как анализировать код?

Интегрирую со статическими анализаторами кода pylint и flake8 - они не только ищут проблемы в коде, но проверяют на соответствие PEP8. Много анализаторов не бывает, потому что они по большей части дополняют друг другу.

Интеграция элементарная - добавляю новые тестовые среды:

[tox]
envlist =
    flake8
    pylint
    py{27,34,35,36,37,py}
    coverage_report

[testenv:flake8]
deps = flake8
commands = flake8

[testenv:pylint]
deps = pylint
commands = pylint --rcfile=tox.ini numeral_system/ tests/

Сразу же напарываюсь на странные ограничения - 100 символов в строке, имена функций в 30 символов (да, я пишу очень длинные имена тестовых методов) и предупрждения на наличие TODO в коде. Приходится слегка подтюнить пару игноров:

[MESSAGES CONTROL]
disable=fixme,invalid-name

[flake8]
max-line-length = 120

Так же неприятный момент в том, что разработчики pylint уже похранили python2.7 и не развивают большего пакет для него. Поэтому проверки стоит запускать на актуальном пакете для python3.7. Добавляю соответствующую строчку в конфигурацию.

[tox]
envlist =
    flake8
    pylint
    py{27,34,35,36,37,py}
    coverage_report
basepython = python3.7

Кстати, это так же важно для запуска тестов на различных платформах, так как дефолтная версия питона в системах различная.

Что там с CI?

Интегрирую с appveyor - CI под виндой. Настройка простая - все можно сделать в интерфейсе, затем скачать yaml файл и закоммитеть его в репозиторий.

version: 0.0.{build}
init:
- cmd: choco install python.pypy
install:
- cmd: >-
    python -m pip install --upgrade pip
    pip install tox
build: off
test_script:
- cmd: tox

После интегрирую с travis - CI под линуксом и маком. Настройка чуть сложнее, так как конфигурационный файл будет использоваться уже закоммиченный в репозиторий. Пару итераций проб и ошибок - конфигурация готова. Сквошим и один красивый коммит уже в репозитории.

language: python
python: 3.7

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.4
      - python3.5
      - python3.6
      - pypy

install:
  - pip install tox

script:
  - tox

(И почему CI проектам так нравится yaml формат?)

Как залить на pypi?

Далее делаю среды для сборки wheel пакета и заливки в pypi:

[testenv:build_wheel]
deps =    
    wheel
    docutils
whitelist_externals =    
    /bin/sh    
    /bin/rm
commands =    
    /bin/rm -rf build dist venv_upload    
    python setup.py sdist bdist_wheel

[testenv:test_upload]
deps = twine
commands =    
     python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*

[testenv:pypi_upload]
skip_install = True
deps =
    twine
commands =
    python -m twine upload dist/*

Изначально проект назывался numsys, но при попытке заливки сталкнулся с тем, что пакет с таким именем уже есть! И что самое обидное, что он тоже умеет конвертировать в римские цифры :) Сильно не расстроился и переименовал все numeral-system. Срезаю релиз на github, собираю пакет и заливаю в продовский pypi.

А вcе работает?

Проверяю простыми командами:

> virtualenv venv
> source venv/bin/activate
(venv) > pip install numeral-system
(venv) > python
>>> import numeral_system
>>> numeral_system.roman.encode(7)
'VII'

Все отлично!

А можно лучше?

На этом останавливаться не стоит, можно сделать следующие улучшения:

  • добавить перфоманс регрессию;
  • добавить поддержку MacOS;
  • 100% покрытие кода далеко не показатель качества, тесты должны покрывать все ветки исполнения. Как это мерить?
  • зависимости обновляются, это следует отслеживать;
  • добавить модную документацию на Sphinx.
  • нужно больше статик анализаторов. Что есть еще? Ваши предложения?

Проект можно посмотреть на github