Основная статья на хабре: 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.
- нужно больше статик анализаторов. Что есть еще? Ваши предложения?
