Делаем все правильно: проект на Python в 2021

Лучшие практики Python в 2021

Для начала

Цель данного Руководства – описать экосистему разработчика Python в 2021 году. Оно будет полезно любому, кто пришел в Python, зная другой язык программирования.

Они говорят, что ты должен вызубрить алгоритмы и структуры данных, что ты можешь выучить язык программирования за пару недель, ну, просто узнаешь синтаксис. Полностью согласен: алгоритмы и структуры данных – экстремально важны для разработчика, но когда говорим о языке программирования, то уточнить синтаксис – мало. Вокруг языка, как правило складывается, целая экосистема из инструментов, и “эталонных практик” – способов делать то-то и то-то наилучшим образом. Для кого-то пришедшего из другого языка, может быть утомительно изучать одновременно всю экосистему, особенно если учесть, что чаще всего ценнейшие данные по крупицам рассредоточены по разным местам.

Это моя, очень субъективная, попытка собрать в одном месте лучшие практики по настройке Python для локальной разработки. Также будут советы по интеграции этих тулзов с Visual Studio Code (однако не так уж обязательно работать именно в этом IDE). Гайд получился весьма большим, поскольку я подробно объясняю работу всех инструментов. В конечном итоге у тебя должно выйти – восхитительно настроенное окружение для Python.

Управление версиями Python в pyenv

Почему pyenv?

Видел десятки туториалов, начинающиеся с того же тупого совета: иди на python.org и скачай последнюю версию Python для своей платформы. Не делай так. Можно лучше, и вот почему.

Понятно, что версий Python – сильно больше одной, и тебе понадобится переключаться между ними, работая в разных проектах.

Возможно, есть версии Python, идущие в комплекте с операционкой. Например в Mac сейчас ставят по умолчанию версию 2.7, а некоторые дистрибутивы Windows поставляются с версией 3.х. Далее, в пакет Anaconda включена своя версия Python. Нужно учесть: никогда неизвестно, какая версия появится в ответе на введенную команду “python” в командной строке.

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

Да, такое есть. По ссылке

Как ставится pyenv?

Установка на Mac:

brew update
brew install pyenv

На Linux установка здесь:

curl https://pyenv.run | bash

На Windows – по ссылке.

Но все-таки лучше работать в Windows Subsystem for Linux (WSL), а затем установить “как правильно в Linux”.

Как работать с pyenv?

Сначала посмотрим на список всех ехе-шников Python, если они есть на компьютере. (По крайней мере, те которые pyenv может найти):

pyenv versions
* system (set by /Users/alex/.python-version)

Здесь показан вывод на моем компьютере. Звездочка-астериск обозначает текущую версию установленного Python. Если ввести команду:

python -V

то выведет сообщение, говорящее, что MacOS на моем компьютере все еще с Python 2.7.

Python 2.7.16

Теперь посмотрим, какие версии Python доступны:

pyenv install --list

Появится большой список, ты даже будешь удивлен, насколько большой. Имплементации CPython будут иметь версии типа 3.8.5, другие имплементации – что-то типа pypy 3.6-7.3.1.

Чтобы посмотреть только С-шные версии Питона, надо запустить следующую команду:

pyenv install --list | grep " 3\."

Если pyenv уже долго используется, и новых версий Питона как-то не выводится в pyenv, то вероятно, надо обновить сам pyenv:

brew upgrade pyenv

или так:

pyenv update

Как должно быть понятно, версии с приставкой -dev девелоперские.

Итак, я установил самый новый Питон и при этом самую стабильную версию (на момент написания поста):

pyenv install 3.8.5

Команда загружает исходник и компилирует его:

python-build: use openssl@1.1 from homebrew
python-build: use readline from homebrew
Downloading Python-3.8.5.tar.xz...
-> https://www.python.org/ftp/python/3.8.5/Python-3.8.5.tar.xz
Installing Python-3.8.5...
python-build: use readline from homebrew
python-build: use zlib from xcode sdk
Installed Python-3.8.5 to /Users/alex/.pyenv/versions/3.8.5

Если запустить команду pyenv versions еще раз, то увидим нашу выбранную версию в списке, но она еще не активирована:

* system (set by /Users/alex/.python-version)
  3.8.5

Надо верифицировать ее командой. Все еще нет:

Python 2.7.16

pyenv дает возможности установки любой версии Python как глобального Python-интерпретатора, но мы сейчас делать так не будем. У нас уже есть скрипты или другие инструменты, работающие на дефолтном интерпретаторе, поэтому давайте не менять эти настройки. Вместо этого, давайте настроим Python для каждого проекта отдельно. Итак, создадим проект в следующем разделе.

Есть и альтернативный инструмент для описанного выше – asdf, по ссылке.

Управление зависимостями в Poetry

Зачем мне нужен Poetry?

По умолчанию, пакеты Python ставятся командой pip install. В реальности никто так не делает. Эта команда приводит все зависимости к одному интерпретатору Python, что создает бардак.

Хорошей практикой является проставлять зависимости по-проектно. Таким образом проект имеет зависимости только нужные, и никаких других. Не возникают конфликты с другими питон-пакетами, позарез нужными в другим проектах.

Для этой проблемы придумано решение – виртуальные окружения. В них каждый проект имеет собственное виртуальное окружение с приписанной ему питон-версией, и фиксированными зависимостями, в каждом проекте своя.

Виртуальные окружения, которые возникли из старых venv, virtualenv, virtualenvwrapper – очень популярны и давно работают. Но есть некая неразбериха с pipenv, если очень надо, то можешь почитать здесь: Pipenv: promises a lot, delivers very little. Одной из видимых проблем pipenv является задержка релизов – например с 2018 года не было новых. Но в середине 2020 года проект pipenv внезапно “проснулся” и выпустил пару апдейтов. Poetry тоже применяется, к тому же заявляют что он лучше работает чем pipenv. Poetry может быть полезен в опенсорсных проектах, особенно в публикации пакетов.

Официальный сайт Poetry

Как ставится Poetry

Рекомендую ставить Poetry на системном уровне.

В MacOS, Linux и WSL вот так:

curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python

Если работаешь в Windows Powershell, есть специальный скрипт на официальном сайте. Но видимо лучше просто сделать в WSL.

Чтобы применить изменения для текущей версии оболочки, команда:

source $HOME/.poetry/env

Можно добавить это в авторан-скрипт типа .bashrc или .zshrc, если Poetry все еще нет в новой сессии:

export PATH="$HOME/.poetry/bin:$PATH"

Можно тут же включить Tab-автодополнение в оболочке. Описано тут.

Как создать проект в Poetry?

Poetry создает отдельные папки и переходит туда, поэтому надо убедиться, что выполнен переход из текущей папки в папку Python-проекта, и там запустить команду:

poetry new my-project

где my-project это имя твоего проекта.

Дальше посмотрим, что там создал Poetry:

cd my-project; ls

Как настроить версию Python по-проектно

Мы все еще находимся на версии 2.7, помнишь? В новом проекте мы будем работать в новой версии, поэтому перейдем в pyenv. Мы сейчас находимся в проектной папке, и мы задаем версию Python локально, для этой папки, командой:

pyenv local 3.8.5

Если мы запустим pyenv versions сейчас, то увидим что 3.8.5 обозначено звездочкой, как активная для этой папки. Учти, что вне этой папки версия Python остается не измененной, той же “по умолчанию”.

system
* 3.8.5 (set by /Users/alex/iCloud/dev/projects/my-project/.python-version)

Можно проверить наверняка, командой python -V.

Python 3.8.5

Теперь заставим Poetry “подхватить” текущую версию:

poetry env use python
Creating virtualenv my-project-PSaGJAu6-py3.8 in /Users/alex/Library/Caches/pypoetry/virtualenvs
Using virtualenv: /Users/alex/Library/Caches/pypoetry/virtualenvs/my-project-PSaGJAu6-py3.8

На последнем шаге настройки версий, давай-ка обновим  pyproject.toml[tool.poetry.dependencies]. Должно ответить, что поддерживается версия 3.8 или выше:

[tool.poetry]
name = "my-project"
version = "0.1.0"
description = ""
authors = ["Alex"]

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry-core>=1.0.0a5"]
build-backend = "poetry.core.masonry.api"

Как работать в Poetry

На этом этапе надо будет установить первую зависимость: какую-то библиотеку или фреймворк, “поверх” того что планировалось в проекте. Например:

poetry add aiohttp

где aiohttp – фреймворк который ставится.

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

poetry install

Обновить все зависимости к последним версиям:

poetry update

Как связать Poetry с VS Code

VS Code к сожалению не подхватывает Poetry автоматически. Но есть и хорошие новости, можно заставить VS Code находить и подхватывать виртуальное окружение, прописанное в Poetry.

Скачала активируем виртуальное окружение:

poetry shell
Spawning shell within /Users/alex/Library/Caches/pypoetry/virtualenvs/my-project-PSaGJAu6-py3.8
➜  my-project . /Users/alex/Library/Caches/pypoetry/virtualenvs/my-project-PSaGJAu6-py3.8/bin/activate
(my-project-PSaGJAu6-py3.8) ➜  my-project

Теперь мы вошли в виртуальное окружение, и можем вызывать VS Code из него:

code .

Надо будет также установить Pylance – питон-сервер для VS Code.

При открытии любого питон-файла, VS Code тут же спросит, какой интерпретатор задействовать?

Итак, выберем. Мы вызывали VS Code из виртуальной среды, поэтому нужный интерпретатор покажется в списке, и мы выберем его.

Заметь, что есть две опции для 3.8.5, мы должны выбрать одну, которая относится к виртуальному окружению (это видно по пути к файлу, там должно быть virtualenv).

Вообще, можно подписаться на соответствующую issue на Гитхабе, по связке VS Code с Python. А Брет Кэннон говорит, что разработчики VS Code уже переписывают код связки, и когда-нибудь, говорили они, Poetry будет полностью поддерживаться в VS Code.

Обновление Poetry

Просто запускай poetry self update, и получишь последнюю версию Poetry.

Если выплыла ошибка:

ImportError: No module named cleo

То надо переустановить Poetry. Сначала удалить:

curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py > get-poetry.py
python get-poetry.py --uninstall
python get-poetry.py
rm get-poetry.py

Как обновить версию Python в Poetry и pyenv?

Новые версии Python выходят регулярно, там новые функции и фиксы, и мы хотим обновить Python у себя. Это сделать достаточно просто.

Сначала мы загружаем, компилируем, и настраиваем новый Python-интерпретатор. Убедись, что запускаешь эти команды не из виртуального окружения, а откуда положено – из корневой папки проекта. В моем случае, надо было обновиться к CPython 3.9.2 (которая была новейшей на момент написания основной части этого текста), но в принципе ничего особо не отличается в любой другой версии CPython.

pyenv install 3.9.2
pyenv local 3.9.2
pyenv versions
my-project pyenv versions
  system
  3.8.5
* 3.9.2 (set by /Users/alex/iCloud/dev/projects/my-project/.python-version)

Дальше мы в Poetry передаем этот интерпретатор как “рабочий” для виртуальных окружений.

poetry env use python
Creating virtualenv my-project-PSaGJAu6-py3.9 in /Users/alex.mitelman/Library/Caches/pypoetry/virtualenvs
Using virtualenv: /Users/alex/Library/Caches/pypoetry/virtualenvs/my-project-PSaGJAu6-py3.9

Хэш проекта остается неизменным, а версия Python изменилась на 3.9.

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

poetry install

На следующем этапе надо активировать виртуальное окружение и внимательно проверить, что выбрана правильная версия Python. Итак, запускаем VS Code из виртуального окружения, и VS Code теперь может найти новое окружение (надо еще не забыть закрыть VS Code перед этим).

poetry shell
python -V
code .

И последний этап, в левом нижнем углу нажми на версии Python, и выбери нужную версию в списке. Проверь, что выбрал версию именно виртуального окружения.

На этих скринах показана версия 3.9.0, статья была обновлена с тех пор

Не забывай обновлять отдел [tool.poetry.dependencies] в файле pyproject.toml, чтобы корректно отображалась поддержка версий Python.

VS Code предложит выбрать линтер, форматировщик кода и прочее. Игнорируй. Сначала настрой описанное выше, остальное – потом (далее я опишу, как и что).

Можно все это сделать и альтернативным путем, командой venv.

Как тестировать код при помощи pytest

Зачем нужен pytest

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

pytest – популярный фреймворк, широко одобряемый в Python-сообществе. Он так популярен, что идет в комплекте с Poetry как средство тестирования. Итак, открываем раздел [tool.poetry.dev-dependencies] в pyproject.toml, и видим, что pytest уже в списке зависимостей.

Poetry прописывает за нас структуру тестовых папок.

cd tests; ls
__init__.py        __pycache__        test_my_project.py

Как работать с pytest в VS Code

Очень простой пример. Создаем и протестируем функцию, умножающую два числа. В соответствии с TDD сначала создаем тест.

Откроем test_my_project.py и добавляем импорт функции, а также простой тест:

from my_project.math import multiply_two_numbers

def test_multiply_two_numbers():
    result = multiply_two_numbers(2, 3)
    assert result == 6

Вместо запуска наших тестов из терминала, сделаем все в редакторе (на примере MacOS). Нажимаем комбинацию ⇧⌘P, далее начинаем вводить “Python: Discover Tests” и выбираем нужное в списке.

Тестовый фреймворк появился в правом нижнем углу. Нажимаем его:

и выбираем pytest в выпадающем списке:

На последнем этапе, надо задать папку, где будут наши тесты:

VS Code нашел наши тесты, и добавил кнопки для запуска и дебага тестов.

Так как мы работаем в VS Code, редактор выделит их красным, потому что эти функции еще не прописаны.

Итак, создаем файл math.py в папке my_project и далее пропишем нашу простую функцию:

def multiply_two_numbers(a, b):
    return a * b

Когда запустим тесты снова, они будут уже зелеными:

Альтернативный путь: запустить тесты из командной стройки:

poetry run pytest

или из активированного виртуального окружения, командой:

pytest

Или так: unittest – инструментом, включенным в стандартный дистрибутив Python. Но есть минусы: “не питоновский” camelcase API, неудобный синтаксис.

Как оценить покрытие тестами при помощи pytest-cov

Если есть тесты, надо знать покрытие ими кода. Можно поставить плагин pytest-cov для pytest :

poetry add --dev pytest-cov

Теперь запуск тестов с дополнительным параметром будет генерировать отчет по покрытию кода тестированием:

pytest --cov=my_project tests/
================================================= test session starts =============
platform darwin -- Python 3.9.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /Users/alex/iCloud/dev/projects/my-project
plugins: cov-2.10.1
collected 2 items                                                                                                     

tests/test_my_project.py ..                                                  [100%]

----------- coverage: platform darwin, python 3.9.2 -----------
Name                     Stmts   Miss  Cover
--------------------------------------------
my_project/__init__.py       1      0   100%
my_project/math.py           4      0   100%
--------------------------------------------
TOTAL                        5      0   100%

================================================== 2 passed in 0.08s ===============

Также сгенерируется файл .coverage, которого мы не хотим видеть в нашем контроле версий, поэтому не забудь добавить его в соответствующий списочек .gitignore:

echo '.coverage' > .gitignore

Как провести проверку до отправки на предварительный коммит

В Git

Мы еще не затрагивали здесь контроль версий, а теперь займемся этим. Конечно, в 2021 году все работают в Git. Установить последнюю версию (или обновить) в MacOS:

brew install git

Если в проекте еще нет репозитория, надо его создать. Github создает новый репозиторий по умолчанию с веткой main и делаем это:

git init -b main

Сначала уточним, мы должны иметь у себя файл .gitignore, и он не даст невпопад коммитить “временные” файлы или бинарники в наш Git-репозиторий. Можно вручную скопипейстить отсюда или просто запустить команду создающую .gitignore, и загрузить контент из указанной ссылки. При этом надо находиться в корневой папке проекта.

curl -s https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore >> .gitignore

Можно исключить папку настроек VS Code из контроля версий. Также можно исключить папку настроек PyCharm, если кто-то в проекте работает в этом IDE.

echo '.vscode/\n.idea/' >> .gitignore

Теперь добавим все нужное в контроль версий:

git add .

Зачем запускать проверку перед коммитами?

Как ты уже понял (и еще увидишь дальше), объем проекта большой. Легко что-то упустить перед тем как отправить код в репозиторий. В результате, код не проходит автоматическую проверку качества непрерывной интеграции кода (CI), твои коллеги или тестировщики могут заставлять корректировать код. Иногда все тормозится из-за того, что кто-то не поставил пробел или символ конца строки, а ведь это тоже входит в стандарт качества POSIX например, в чем можно убедиться по ссылке. Даже если помнишь все мелкие стандарты, это ни от чего не спасает, все равно легко ошибиться. Но есть удобный инструмент избежать “залипания” в этих легко автоматизируемых мелочах.

Для этого и существует  pre-commit. Это средство автоматической проверки каждого git-коммита. Оно простое и не требует рут-доступа. Написано на Python, но разумеется может работать в проектах с другими языками.

Установка и работа pre-commit

pre-commit может ставиться на системном уровне, но мы так делать не будем, именно потому, почему применяем pyenv – у нас много разных версий Python, и наши зависимости надо хранить в порядке. Мы ставим  pre-commit как зависимость:

poetry add pre-commit --dev

Далее мы “прикрепим” все наши “проверяльщики” (линты) и подобные инструменты для предварительной проверки кода. Создаем простой конфиг для проверки на наличие в положенном месте – символа конца строки (EoF, newline), о котором речь шла выше.

Есть команда в pre-commit для быстрого создания такого конфига. В корневой папке вводим:

pre-commit sample-config > .pre-commit-config.yaml

Создан небольшой файл конфигурации, содержащий такие строки:

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.4.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files

Здесь видно, что наш pre-commit уже сам прописал некоторые удобные хуки. Теперь смотрим, как оно работает. Запуск проверки вручную:

pre-commit run --all-files

После запуска pre-commit, становится понятно, что даже в файлах, автоматически создаваемых в VS Code, нет в конце этого символа (newline), между прочим требуемого стандартом!

Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook

Fixing my_project/math.py
Fixing .vscode/settings.json
Fixing tests/test_my_project.py

Check Yaml...............................................................Passed
Check for added large files..............................................Passed

А pre-commit пофиксил эту проблему за тебя, так что если запустить ту же команду еще раз, все проверки покажут ОК.

Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed

Самая важная часть тут – правильно “связать” Git с этими хуками, и запускать их перед каждым коммитом.

pre-commit install
pre-commit installed at .git/hooks/pre-commit

Теперь мы выполняем коммиты (то есть передаем свои правки на Git):

git commit -m 'Initial commit'
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
[master (root-commit) 7dc335f] Initial commit
 11 files changed, 559 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .pre-commit-config.yaml
 create mode 100644 .python-version
 create mode 100644 .vscode/settings.json
 create mode 100644 README.rst
 create mode 100644 my_project/__init__.py
 create mode 100644 my_project/math.py
 create mode 100644 poetry.lock
 create mode 100644 pyproject.toml
 create mode 100644 tests/__init__.py
 create mode 100644 tests/test_my_project.py

Хуки запускаются перед коммитом, как положено.

И последнее, можно заставить pre-commit периодически обновлять конфиги к последней версии:

pre-commit autoupdate

И он немедленно обновил хук, в моем случае:

Updating https://github.com/pre-commit/pre-commit-hooks ... updating v2.4.0 -> v3.2.0.

Анализ кода в линтере Flake8

Почему линтеры нужны в проектах на Python

Линтеры – твой заслон от багов и тупых ошибок-опечаток. Они сигнализируют об ошибках заблаговременно, до передачи кода на выполнение. Конечно же, все IDE-редакторы поддерживают в себе линтеры, и их не нужно запускать отдельно. Редактор с подключенным линтером подсвечивает некорректный код прямо после ввода.

В “Мире Python’а” существует много линтеров, основных два – Flake8 и Pylint.

Начнем с Pylint, это строгий и дотошный линтер. С ним работают люди в Google, в их Python-проектах, и это предусмотрено в их гугловских гайдлайнах. Из-за дотошности этого линтера, возможно, придется тратить время на разбор всех подробностей, и настройку срабатывания. Что, в принципе, неплохо – учишься писать удобочитаемый правильный код сразу же, “не отходя от кассы”. Безопасный код, однако, учиться писать намного дольше, как показывает опыт.

Популярнейший линтер с открытыми исходниками – Flake8. Но перед тем как начать, надо обсудить одну вещь.

Python Style Guide- PEP8

Гайдлайн по Питону, известен по номеру “Предложений по Улучшению №8” – PEP8. Все программисты на Питоне попросту обязаны ознакомиться с PEP8, и понятно, с Дзеном тоже (РЕР20).

После прочтения РЕР8, достаточно утомительного документа, возникает вопрос, а есть ли “тулз” автоматической проверки, и приведения в действие всех этих указаний? Это делает Flake8, и не только это. Он работает сразу после установки, и может настраиваться на специфические правила у каждого разработчика.

Flake8 не такой строгий как Pylint, и вполне сносно “несет службу на первом краю”, где твоя безалаберность обычно наивысшая.

Поэтому советую все-таки работать с Flake8. Можно иметь в запасе и Pylint как запасной вариант, но в Flake8 работать обязательно.

Как ставится Flake8

VS Code обязательно спросит при установке, оп, а какой будет линтер? На Маке нажимаем ⇧⌘P, дальше вводим “linter”, выбираем в списке “Python: Select Linter”.

И выбираем “flake8”

VS Code подхватит виртуальное окружение и установит Flake8 как зависимость.

Если так не получилось, или если хочется поставить Flake из командной строки:

poetry add flake8 --dev

Как работать в Flake8

Итак, вернемся к коду в предыдущей главе. VS Code подсвечивает красным ошибки, с точки зрения линтера. Подводим мышку к ошибке, видим сообщение с номером и описанием ошибки (можно тут же посмотреть в интернете, о чем это), и название линтера.

Можно запускать Flake8 вручную:

flake8 .
./tests/test_my_project.py:4:1: E302 expected 2 blank lines, found 1
./tests/test_my_project.py:7:1: E302 expected 2 blank lines, found 1

Как “подвязать” Flake8 к хукам Git?

О красной подсветке ошибок в IDE иногда забывают, так же как о запуске команды flake8 перед отправкой кода в репозиторий. Поэтому существуют хуки. Добавим Flake8 в список хуков, и ко будет проверяться автоматически.

Для этого откроем файл  .pre-commit-config.yaml и добавляем следующее:

-   repo: https://gitlab.com/pycqa/flake8
    rev: 3.8.3
    hooks:
    -   id: flake8

Теперь внимательно – на строчку с rev. Здесь не обязательно версия 3.8.3. Узнать текущую версию Flake8 можно, открыв pyproject.toml, там найти версию Flake8, и скопировать-вставить ее в конфиг-файл пре-коммита, как показано выше.

Помни: мы раньше заставили pre-commit обновлять версии, поэтому Flake8 будет постоянно обновляться к последней версии. Но мы уже используем самую новую версию линтера, и ничего не будет происходить. pre-commit загрузит эту версию Flake8, потому что он проводит свои проверки в отдельном окружении. Это значит, что у нас получится две разных версии Flake8, и надо убедиться, что сейчас работает правильная. Обычно обновление к последней версии не составляет проблем,но если они возникают, советую отключить автоматическое обновление для pre-commit.

После редактирования конфиг-файла делаем так:

git add .pre-commit-config.yaml

И передаем его:

git commit -m 'Add Flake8 to git hooks'
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
flake8...............................................(no files to check)Skipped
[master 1d25c9f] Add Flake8 to git hooks
 1 file changed, 4 insertions(+)

Давайте попробуем вручную запустить pre-commit еще раз, чтобы проверить, как работает Flake8 в связке с Git:

pre-commit run --all-files
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
flake8...................................................................Failed
- hook id: flake8
- exit code: 1

tests/test_my_project.py:4:1: E302 expected 2 blank lines, found 1
tests/test_my_project.py:7:1: E302 expected 2 blank lines, found 1

Не спеши фиксить эту ошибку, чтобы “снять” ее из линтера. Надо пробовать фиксить ее в автоматическом режиме.

Форматирование кода в Black

Каждый программист обладает собственным стилем. Даже на Питоне, который принудительно заставляет делать отступы, можно писать код по своему.

Иногда, глядя на кусок кода, я могу сказать, кто из моей команды писал его. Когда код пишут одновременно несколько человек, он становится “лоскутным”.

Как этого избежать: по идее, перед написанием кода надо посоветоваться, иначе дело замедлится. Вообще же, пререкания насчет стиля – есть дело личного вкуса. В конце рабочего дня люди ссорятся о мелочах, о том как внешне выглядит код, а не о том как он работает.

Круто, когда код одинаково выглядит во всем проекте, как если бы его писал один человек. Есть такое решение, такой инструмент, он называется Black.

Этот софт форматирует код “в одном ключе”, и код по крайней мере внешне – выглядит однообразно. В Black можно настраивать мало что, но здесь не тот случай, когда есть выбор. Просто поставь его, и пользуйся без претензий. В конце концов, РЕР8 и Black создавались для “причесывания” всего под единый образец, и тут уж ничего не поделать, претензии излишни.

Сразу заметно, что сам Black игнорирует правило из РЕР8 о длине строки. Вообще, в РЕР8 сказано, что строка может быть максимально длиной в 79 символов. Это имеет исторические корни, удивительно, от перфокарт IBM и первых UNIX-терминалов. РЕР8 писали в далеком 2001 году, с того времени много чего изменилось. Люди начали ставить под сомнение это правило. “Не читаю код на UNIX-терминале”, говорят они. “У меня 27-дюймовый IPX-монитор”, говорят они. Однако, есть и люди, которые пишут код на маленьком 13-дюймовом лэптопе. И, например, искать глазами разницу между двумя текстовыми файлами, с длиной строки всего 79 символов – им довольно удобно. Представить только, горизонтальную перемотку огромного файла с большой длиной строки на таком экране. Поэтому “правило 79 символов” сохранено.

И да, хотя РЕР8 составлен в 2001, он неоднократно обновлялся (список новшеств), и все-таки правило, еще раз обсудив, не трогали.

И это правило Black дает нам настроить.

Как поставить Black

В VS Code открываем Python-файл в нашем новом проекте. Например test_my_project.py, о котором так беспокоится наш Flake8. Нажимаем и вводим “format”, дальше выбираем “Format Document”.

Всплывает диалог выбора форматировщика.

Выбираем форматировщик: “Use Black”.

VS Code обнаружит, что уже есть Poetry, и автоматически поставит Black.

Или же можно поставить вручную, командой:

poetry add --dev black --allow-prereleases

Опция –allow-prereleases присутствует, потому что Black все еще бета (весной-летом 2021 по крайней мере). Когда-то в 2019 думали, что закончат с бетами в том же году, но как говорится. Впрочем, многие серьезные продакшены и опенсорсные проекты “причесывают” свой код с Black, поэтому можно считать, что этот “дефолтный” форматировщик – в достаточной мере стабилен и надежен.

Чтобы настроить Black, открываем pyproject.toml и вставляем следующий раздел:

[tool.black]
line-length = 79
target-version = ['py38']
include = '\.pyi?$'
exclude = '''

(
  /(
      \.eggs         # exclude a few common directories in the
    | \.git          # root of the project
    | \.hg
    | \.mypy_cache
    | \.tox
    | \.venv
    | _build
    | buck-out
    | build
    | dist
  )/
  | foo.py           # also separately exclude a file named foo.py in
                     # the root of the project
)
'''

Самая важная часть здесь – установить, что максимальная длина строки равна 79.

Если работаешь и в других версиях Python, то обновляй их тоже, но убедись, что они поддерживаются в Black.

Также понадобится “притушить” некоторые ошибки в Flake8, чтобы Flake8 нормально работал с Black. Для этого создаем конфиг-файл setup.cfg (то есть файл конфигурации Flake8), и вставляем там это:

[flake8]
extend-ignore = E203

Как работает Black

Очень просто он работает. Запускается и форматирует код:

black .
reformatted /projects/my-project/my_project/__init__.py
reformatted /projects/my-project/tests/test_my_project.py
All done! ✨ 🍰 ✨
2 files reformatted, 2 files left unchanged.

Flake8 жаловался на отсутствие символов перевода строки? (об этом шла речь выше.) Запускаем Git-хук, и видим что все ОК.

pre-commit run --all-files
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
flake8...................................................................Passed

Как добавить Black в git-хуки?

Так же как Flake8, работа Black отлично автоматизируется, так что не волнуемся. Вставляем в конфиг .pre-commit-config.yaml следующий кусок:

-   repo: https://github.com/psf/black
    rev: 20.8b1
    hooks:
      - id: black

Опять запускаем pre-commit run –all-files. Видим что загружается Black, и работает:

[INFO] Initializing environment for https://github.com/psf/black.
[INFO] Installing environment for https://github.com/psf/black.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
flake8...................................................................Passed
black....................................................................Passed

Сохранение изменений в Git описывать не буду, это одинаковое с Flake8.

Есть еще альтернатива – yapf.

Бонус!

Можно сделать в VS Code вертикальную линейку (ruler), показывающую тот самый лимит 79 символов. Открываем  settings.json в папке .vscode и вставляем туда:

"[python]": {
        "editor.rulers": [72, 79]
    }

Линейка будет видна только в питон-коде. В docstrings будет только 72 символа.

Статические типы в Mypy

Применение проверки статических типов в Python

Споры о типах полыхали еще в 1970-е. IBM со своим Smalltalk – против Sun с их Java. Как мы знаем, выиграла Java и концепция “strong-type”, однако и Smalltalk не исчез со своим динамическим типированием; это рассматривалось как преимущество, по крайней мере в некоторых крупных компаниях, так как этим обеспечивалась быстрота разработки. Конечно, строгое типирование хранит от многих багов и runtime-исключений; но от них можно уберечься и 100%-ным покрытием тестами (как считает Uncle Bob, это и есть главная причина успешности Python). Но давайте также не забывать, что не все проекты можно 100% покрыть тестами.

А можно заиметь лучшее из обеих концепций. Можно соединить безопасные типы, и быстроту разработки. И в этом поможет Mypy – инструмент анализа статического кода, на предмет его безопасности (type-safe check). Проект превратился в такой ценный, что на него обратил внимание сам Его Величество творец Python Гвидо ван Россум, и добавил аннотации по типам в Python, и присоединился к разработке Mypy. Аннотации типов делают код удобочитаемым, а также удобным для IDE в плане быстроты авто-дополнения кода.

Как установить mypy

В VS Code открываем файл settings.json в папке .vscode. Устанавливаем параметр “python.linting.mypyEnabled”: в положение :true. Затем открываем любой питон-файл из проекта, например тот же math.py. VS Code сразу сообщает, что линтер Mypy еще не установлен.

Нажимаем “Install”, и Mypy поставится автоматически вместе с Poetry.

Или же, можно поставить Mypy вручную:

poetry add --dev mypy

Как работает Mypy

Теперь надо опробовать этот “тулз” в своем проекте.

mypy .
Success: no issues found in 4 source files

Вроде как все хорошо, у нас даже не надо ничего корректировать, Mypy автоматически проверяет типы.

Как упоминалось выше, аннотации типов помогают лучше читать код и понимать его, и IDE лучше авто-дополняет код, так что давайте пробовать. Открываем конфиг-файл setup.cfg и вставляем туда этот кусок:

[mypy]
follow_imports = silent
strict_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
disallow_any_generics = True
check_untyped_defs = True
no_implicit_reexport = True
disallow_untyped_defs = True
ignore_missing_imports = True

Тут самая важная строчка – disallow_untyped_defs = True. Она заставляет тебя “описывать функции типами”, то есть принудительное присвоение типов. В существующих старых проектах это можно отключить, но у нас новый проект, поэтому будет полезно убедиться, что аннотации типов не пропущены.

Может понадобиться временно отключить Mуpу для тестирования, для этого вставляем следующий кусок в настройки:

[mypy-tests.*]
ignore_errors = True

Еще лучшую совместимость дают плагины для Mypy. К примеру, если нужно применять pydantic для валидации данных или их сериализации, конфиг-файл будет содержать такие строчки:

[mypy]
plugins = pydantic.mypy

follow_imports = silent
strict_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
disallow_any_generics = True
check_untyped_defs = True
no_implicit_reexport = True
disallow_untyped_defs = True
ignore_missing_imports = True

[pydantic-mypy]
init_forbid_extra = True
init_typed = True
warn_required_dynamic_aliases = True
warn_untyped_fields = True

После вставки такого куска, на выводе получим:

my_project/math.py:1: error: Function is missing a type annotation
Found 1 errors in 1 files (checked 4 source files)

Помимо этого, в VS Code Mypy работает в качестве линтера, подчеркивая ошибки:

Чтобы пофиксить их, вставляем аннотации типов в нашу функцию:

from numbers import Real
from typing import Union

def multiply_two_numbers(a: Union[int, Real], b: Union[int, Real]) -> Union[int, Real]:
    return a * b

Как видишь, описание функции слишком длинное. Нажимаем ⇧⌥F, и VS Code отформатирует код при помощи Black, сократив длину строки до 79 символов:

from numbers import Real
from typing import Union

def multiply_two_numbers(
    a: Union[int, Real], b: Union[int, Real]
) -> Union[int, Real]:
    return a * b

Коммитим внесенные правки:

git commit -m 'Add Mypy'

Как добавить mypy в список git-хуков

Надо проверить, что Mypy запускается до коммита кода. Вставляем в файл .pre-commit-config.yaml следующий кусок:

-   repo: https://github.com/pre-commit/mirrors-mypy
    rev: v0.782
    hooks:
        - id: mypy
          additional_dependencies: [pydantic]  # add if use pydantic

Вводим команду pre-commit run –all-files, она устанавливает запуск Mypy для предварительных коммитов и проводит проверки:

[INFO] Installing environment for https://github.com/pre-commit/mirrors-mypy.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
flake8...................................................................Passed
black....................................................................Passed
mypy.....................................................................Passed

Сортирование импортов при помощи isort

Ну и последнее. Импорты.

Зачем вообще сортировать импорты?

РЕР8 говорит, что импорты должны быть отсортированы, причем в следующем порядке: стандартная библиотека, third party библиотека, локальная библиотека. Кроме того, импорты должны быть “красивыми, хорошо читаемыми”.

Есть и такой инструмент! Называется isort, как бы сокращение от “import sort”. Здесь по ссылке описание.

До сортировки импорты выглядят так:

from my_lib import Object

import os

from my_lib import Object3

from my_lib import Object2

import sys

from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14

import sys

from __future__ import absolute_import

from third_party import lib3

После – так:

from __future__ import absolute_import

import os
import sys

from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8,
                         lib9, lib10, lib11, lib12, lib13, lib14, lib15)

from my_lib import Object, Object2, Object3

Лучше, правда?

Как поставить isort, добавить в список git-хуков и работать с ним

VS Code (с установленным Python-расширением) запускает isort “автоматически”, так что не надо какой-то дополнительной настройки. Если ты не будешь работать с isort из командной строки, то даже не надо отдельно устанавливать его, потому что pre-commit автоматически устанавливает все зависимости в окружениях.

Но, если понадобится работать с isort вне VS Code и pre-commit, то устанавливать надо так:

poetry add --dev isort

Чтобы isort корректно “сотрудничал” с Black, в pyproject.toml надо добавить строчки:

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
line_length = 79

В VS Code на Маке нажимаем ⇧⌘P, вводим “sort imports”, и увидим:

Или же, если isort уже установлен, вводим, находясь в корневой папке проекта:

isort .
Skipped 2 files

Ну и добавляем isort в список хуков в конфиг-файле .pre-commit-config.yaml:

-   repo: https://github.com/PyCQA/isort
    rev: 5.4.2
    hooks:
    -   id: isort

Вот как оно работает например с pre-commit run –all-files:

Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
flake8...................................................................Passed
black....................................................................Passed
mypy.....................................................................Passed
isort....................................................................Passed

Fast Track

Вот как можно создать полностью сконфигурированный проект за пару минут (если pyenv и poetry уже установлены):

poetry new my-project; cd my-project; ls
pyenv local 3.9.2
poetry env use python
poetry add --dev pytest-cov pre-commit flake8 mypy isort
poetry add --dev --allow-prereleases black
poetry shell
code .

Добавляем настройки в pyproject.toml:

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
line_length = 79

[tool.black]
line-length = 79
target-version = ['py38']
include = '\.pyi?$'
exclude = '''

(
  /(
      \.eggs         # exclude a few common directories in the
    | \.git          # root of the project
    | \.hg
    | \.mypy_cache
    | \.tox
    | \.venv
    | _build
    | buck-out
    | build
    | dist
  )/
  | foo.py           # also separately exclude a file named foo.py in
                     # the root of the project
)
'''

Создаем setup.cfg:

[flake8]
extend-ignore = E203

[mypy]
follow_imports = silent
strict_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
disallow_any_generics = True
check_untyped_defs = True
no_implicit_reexport = True
disallow_untyped_defs = True
ignore_missing_imports = True

[mypy-tests.*]
ignore_errors = True

Создаем .pre-commit-config.yaml:

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.4.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files
-   repo: https://gitlab.com/pycqa/flake8
    rev: 3.8.4
    hooks:
    -   id: flake8
-   repo: https://github.com/psf/black
    rev: 20.8b1
    hooks:
      - id: black
-   repo: https://github.com/pre-commit/mirrors-mypy
    rev: v0.812
    hooks:
        - id: mypy
          additional_dependencies: [pydantic]  # add if use pydantic
-   repo: https://github.com/PyCQA/isort
    rev: 5.7.0
    hooks:
    -   id: isort
echo '.coverage' > .gitignore
echo '.vscode/\n.idea/' >> .gitignore
curl -s https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore >> .gitignore
git init -b main
git add .
git commit -m 'Initial commit'
pre-commit install
pre-commit autoupdate
pre-commit run --all-files
pre-commit run --all-files

Итоги

Сегодня ты потрудился на славу, можно поздравить, если добрался сюда. Конечно, это было утомительно. Перед нами стояла цель: настроить свой IDE и Python так, чтобы создание нового проекта требовало несколько минут. Если все сделать как описано выше, так и будет.

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Scroll to Top