Прочтите оригинальную статью в моем блоге

В этом третьем выпуске серии Hypermodern Python я собираюсь обсудить, как добавить линтинг, форматирование кода и статический анализ в ваш проект. ¹ Ранее мы обсуждали Автоматическое тестирование. (Если вы начнете читать здесь, вы также можете скачать код из предыдущей главы.)

Вот темы, затронутые в этой главе, посвященной линтингу в Python:

Вот полный список статей из этой серии:

У этого руководства есть дополнительный репозиторий: cjolowicz / hypermodern-python. Каждая статья в руководстве соответствует набору коммитов в репозитории GitHub:

Линтинг Flake8

Линтеры анализируют исходный код, чтобы пометить ошибки программирования, ошибки, стилистические ошибки и подозрительные конструкции. Наиболее распространенными для Python являются pylint и агрегаторы линтера flake8, pylama и prospector. Также существуют многоязычные линтерные фреймворки, такие как pre-commit и coala. В этой главе мы используем Flake8 и предварительную фиксацию.

Добавьте сеанс Nox для запуска Flake8 в своей кодовой базе:

# noxfile.py
locations = "src", "tests", "noxfile.py"

@nox.session(python=["3.8", "3.7"])
def lint(session):
    args = session.posargs or locations
    session.install("flake8")
    session.run("flake8", *args)

По умолчанию мы запускаем Flake8 в трех местах: в дереве исходных текстов пакетов, в тестовом наборе и в самом noxfile.py. Вы можете переопределить это, передав определенные исходные файлы, отделенные от собственных параметров Nox --. Метод session.install устанавливает Flake8 в виртуальную среду через pip.

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

  • F - это ошибки, о которых сообщает pyflakes, инструмент, который анализирует исходные файлы и находит недопустимый код Python.
  • W и E - это предупреждения и ошибки, о которых сообщает pycodestyle, который проверяет ваш код Python на соответствие некоторым стилевым соглашениям в PEP 8.
  • C - это нарушения, о которых сообщает mccabe, который проверяет сложность кода вашего пакета Python на соответствие настроенному пределу.

Настройте Flake8 с помощью файла конфигурации .flake8, включив все встроенные классы нарушений и установив предел сложности:

# .flake8
[flake8]
select = C,E,F,W
max-complexity = 10

По умолчанию Nox запускает все сеансы, определенные в noxfile.py. Используйте параметр --session (-s), чтобы ограничить его определенным сеансом:

nox -rs lint

Есть много крутых расширений Flake8. Некоторые из них будут представлены в следующих разделах.

Форматирование кода черным цветом

Следующее дополнение к нашему набору инструментов - Black, бескомпромиссное средство форматирования кода Python. Одна из его главных особенностей - отсутствие возможности настройки. Почерневший код выглядит одинаково независимо от проекта, который вы читаете.

Добавить Black в качестве сеанса Nox просто:

# noxfile.py
@nox.session(python="3.8")
def black(session):
    args = session.posargs or locations
    session.install("black")
    session.run("black", *args)

Установив сеанс Nox, вы можете переформатировать свой код следующим образом:

$ nox -rs black
nox > Running session black
nox > Creating virtual environment (virtualenv) using python3.8 in .nox/black
nox > pip install black
nox > black src tests noxfile.py
All done! ✨ 🍰 ✨
5 files left unchanged.
nox > Session black was successful.

Вызов nox без аргументов запускает все сеансы, включая Black. Было бы лучше проверять только стиль кодирования, не изменяя конфликтующие файлы. Исключите Black из сеансов, запускаемых по умолчанию, установив nox.options.sessions вверху:

# noxfile.py
nox.options.sessions = "lint", "tests"

Вместо этого проверьте соблюдение стиля кода Black внутри сеанса линтера. Плагин flake8-black выдает предупреждения, если обнаруживает, что Блэк переформатирует исходный файл:

# noxfile.py
@nox.session(python=["3.8", "3.7"])
def lint(session):
    args = session.posargs or locations
    session.install("flake8", "flake8-black")
    session.run("flake8", *args)

Настройте Flake8, чтобы включить flake8-black предупреждений с префиксом BLK. Кроме того, некоторые встроенные предупреждения плохо сочетаются с черным. Вам нужно игнорировать предупреждения E203 (пробел перед ‘: ') и W503 (разрыв строки перед двоичным оператором) и установить для максимальной длины строки более допустимое значение:

# .flake8
[flake8]
select = BLK,C,E,F,W
ignore = E203,W503
max-line-length = 88

Проверка импорта с помощью flake8-import-order

Плагин flake8-import-order проверяет, что операторы импорта сгруппированы и упорядочены согласованным и совместимым с PEP 8 способом. Импорт следует разделить на три группы, например:

# standard library
import time
# third-party packages
import click
# local packages
from hypermodern_python import wikipedia

Установите плагин в сессию линтера:

# noxfile.py
@nox.session(python=["3.8", "3.7"])
def lint(session):
    args = session.posargs or locations
    session.install("flake8", "flake8-black", "flake8-import-order")
    session.run("flake8", *args)

Включите предупреждения, выдаваемые подключаемым модулем (I например, import).

# .flake8
[flake8]
select = BLK,C,E,F,I,W

Сообщите плагину имена пакетов, которые считаются локальными:

# .flake8
[flake8]
application-import-names = hypermodern_python,tests

Примите Руководство по стилю Google в отношении деталей группировки и заказа:

# .flake8
[flake8]
import-order-style = google 

Рекомендовать импортный линтер в 2020 году - непростая задача, так как в настоящее время в этой области наблюдается много изменений. Превосходный плагин, рекомендуемый в этом разделе, переведен в режим обслуживания. Альтернативой является isort, который поставляется с интеграцией Flake8 через flake8-isort и дополнительно поддерживает перезапись файлов. isort пользуется широкой популярностью, но также вызвал много критики (на которую его автор намерен ответить в следующем крупном выпуске). Если вы ищете инструмент для перезаписи импорта, вам также следует взглянуть на asottile / reorder-python-imports и sqlalchemyorg / zimports.

Поиск ошибок с помощью flake8-bugbear

Плагин flake8-bugbear помогает находить различные ошибки и проблемы проектирования в ваших программах. Добавьте плагин в сеанс линтера в свой noxfile.py:

# noxfile.py
@nox.session(python=["3.8", "3.7"])
def lint(session):
    args = session.posargs or locations
    session.install("flake8", "flake8-black", "flake8-bugbear", "flake8-import-order")
    session.run("flake8", *args)

Включите предупреждения плагина в файле конфигурации Flake8 (B как bugbear):

# .flake8
[flake8]
select = B,B9,BLK,C,E,F,I,W

B9 требуется для более категоричных предупреждений Bugbear, которые по умолчанию отключены. В частности, B950 проверяет максимальную длину строки, как встроенный E501, но с допуском 10%. Не обращайте внимания на встроенную ошибку E501 и установите максимальную длину строки на разумное значение:

# .flake8
[flake8]
ignore = E203,E501,W503
max-line-length = 80

Выявление проблем безопасности с Bandit

Bandit - это инструмент, предназначенный для поиска общих проблем безопасности в коде Python. Устанавливаем через плагин flake8-bandit:

# noxfile.py
@nox.session(python=["3.8", "3.7"])
def lint(session):
    args = session.posargs or locations
    session.install(
        "flake8",
        "flake8-bandit",
        "flake8-black",
        "flake8-bugbear",
        "flake8-import-order",
    )
    session.run("flake8", *args)

Включите предупреждения плагина в файле конфигурации Flake8 (S например, security):

# .flake8
[flake8]
select = B,B9,BLK,C,E,F,I,S,W
...

Bandit flags использует assert для обеспечения соблюдения ограничений интерфейса, поскольку утверждения удаляются при компиляции в оптимизированный байт-код. Вы должны отключить это предупреждение для своего набора тестов, поскольку Pytest использует утверждения для проверки ожиданий в тестах:

# .flake8
[flake8]
per-file-ignores = tests/*:S101
... 

Bandit находит известные проблемы, которые можно обнаружить с помощью статической проверки файлов. Если вас очень беспокоит безопасность, вам следует подумать об использовании дополнительных инструментов, например, инструмента фаззинга, такого как python-afl.

Обнаружение уязвимостей безопасности в зависимостях с безопасностью

Безопасность проверяет зависимости вашего проекта на наличие известных уязвимостей безопасности, используя тщательно подобранную базу данных небезопасных пакетов Python. Добавьте следующий сеанс Nox, чтобы запустить безопасность в вашем проекте:

import tempfile

@nox.session(python="3.8")
def safety(session):
    with tempfile.NamedTemporaryFile() as requirements:
        session.run(
            "poetry",
            "export",
            "--dev",
            "--format=requirements.txt",
            "--without-hashes",
            f"--output={requirements.name}",
            external=True,
        )
        session.install("safety")
        session.run("safety", "check", f"--file={requirements.name}", "--full-report")

Сеанс использует команду экспорт поэзии для преобразования файла блокировки Poetry в файл требований для использования службой безопасности. Стандартный модуль tempfile используется для создания временного файла под требования.

Включите безопасность в сеансы Nox по умолчанию, добавив ее в nox.options.sessions:

# noxfile.py
nox.options.sessions = "lint", "safety", "tests"

Чтобы увидеть, как работает безопасность, установите печально известный insecure-package:

poetry add insecure-package

Вот что по этому поводу говорит служба безопасности:

Не забудьте удалить этого монстра (шучу, это пустой пакет, помеченный Safety DB для тестирования):

poetry remove insecure-package

Не стесняйтесь повторно запустить Безопасность через Nox.

Управление зависимостями в сеансах Nox с помощью Poetry

В этом разделе я описываю, как использовать Poetry для управления зависимостями разработки в ваших сессиях Nox и как сделать ваши сессии Nox более воспроизводимыми.

В первой главе мы увидели, что Poetry записывает точную версию каждой зависимости пакета в файл с именем poetry.lock. То же самое делается для зависимостей разработки, таких как pytest. Это называется закреплением и позволяет вам создавать и тестировать ваш пакет предсказуемым и детерминированным способом.

Напротив, до сих пор мы устанавливали пакеты в сеансы Nox следующим образом:

session.install("flake8")

Версия не указана! Nox установит все, что pip считает последней версией, при запуске сеанса. Проверки могут быть успешными, когда вы запускаете их на своем локальном компьютере, но внезапно ломаются на машине другого разработчика или на сервере непрерывной интеграции из-за изменения Flake8 или одной из его зависимостей. Это происходит постоянно, и проблема быстро накапливается по мере роста зависимостей вашего проекта.

Вы можете закрепить Flake8, используя что-то вроде следующего:

session.install("flake8==3.7.9")

Такой подход улучшает ситуацию, но имеет некоторые ограничения:

  • Мы вернулись к обработке требований вручную, вместо того, чтобы использовать богатую поддержку Poetry для управления зависимостями.
  • Проверка по-прежнему не является детерминированной, потому что зависимости зависимостей остаются незакрепленными. (Flake8 является хорошим примером для этого: по своей сути он объединяет несколько более специализированных инструментов. Хотя Flake8 защищает вас от критических изменений этих инструментов, их точные версии все еще остаются на волю случая.)

Как насчет того, чтобы объявить Flake8 как зависимость разработки нашего проекта, как мы это сделали с Pytest в предыдущей главе? Затем мы можем воспользоваться Poetry как диспетчером зависимостей и записать версии Flake8 и его зависимости в его файл блокировки. - Что ж, тут есть загвоздка. Посмотрите, как мы устанавливали зависимости разработки в сеансе Nox для тестирования:

session.run("poetry", "install", external=True)

Эта команда устанавливает кучу вещей, которые не нужны нашему сеансу линтинга:

  • пакет в разработке
  • зависимости пакета
  • несвязанные зависимости разработки (например, Pytest)

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

Разве не было бы замечательно, если бы вы могли устанавливать отдельные пакеты с session.install, но каким-то образом использовать файл блокировки Poetry, чтобы ограничить их версии? К счастью, есть функция pip, которая позволяет вам делать именно это: ограничивает файлы. Если вы раньше использовали файл requirements.txt, формат точно такой же. А в Poetry есть команда для экспорта файла блокировки в формат требований. Итак, у нас есть все строительные блоки для решения.

Функция install_with_constraints ниже является оболочкой для session.install. Он генерирует файл ограничений, выполняя экспорт поэзии, и передает этот файл в pip, используя параметр --constraint. Функция использует модуль tempfile для создания временного файла для ограничений.

# noxfile.py
def install_with_constraints(session, *args, **kwargs):
    with tempfile.NamedTemporaryFile() as requirements:
        session.run(
            "poetry",
            "export",
            "--dev",
            "--format=requirements.txt",
            f"--output={requirements.name}",
            external=True,
        )
        session.install(f"--constraint={requirements.name}", *args, **kwargs)

Измените сеансы Nox для вызова install_with_constraints оболочки вместо прямого вызова session.install:

@nox.session(python="3.8")
def black(session):
    args = session.posargs or locations
    install_with_constraints(session, "black")
    session.run("black", *args)

@nox.session(python=["3.8", "3.7"])
def lint(session):
    args = session.posargs or locations
    install_with_constraints(
        session,
        "flake8",
        "flake8-bandit",
        "flake8-black",
        "flake8-bugbear",
        "flake8-import-order",
    )
    session.run("flake8", *args)

@nox.session(python="3.8")
def safety(session):
    with tempfile.NamedTemporaryFile() as requirements:
        session.run(
            "poetry",
            "export",
            "--dev",
            "--format=requirements.txt",
            "--without-hashes",
            f"--output={requirements.name}",
            external=True,
        )
        install_with_constraints(session, "safety")
        session.run("safety", "check", f"--file={requirements.name}", "--full-report")

Теперь вы можете использовать Poetry для управления Black, Flake8 и другими инструментами в качестве зависимостей разработки:

poetry add --dev \
    black \
    flake8 \
    flake8-bandit \
    flake8-black \
    flake8-bugbear \
    flake8-import-order \
    safety

Вам также следует адаптировать сеанс тестирования. Для этого сеанса нужны только пакеты, необходимые для запуска набора тестов, и они не должны быть загромождены чем-либо еще. Вместо простого вызова poetry install передайте параметр --no-dev. Это исключает зависимости разработки и устанавливает только сам пакет и его зависимости. Затем установите требования к тесту с помощью install_with_constraints. Вот переписанный сеанс Nox:

@nox.session(python=["3.8", "3.7"])
def tests(session):
    args = session.posargs or ["--cov", "-m", "not e2e"]
    session.run("poetry", "install", "--no-dev", external=True)
    install_with_constraints(
        session, "coverage[toml]", "pytest", "pytest-cov", "pytest-mock"
    )
    session.run("pytest", *args)

Проверки линтера теперь детерминированы, а сеансы Nox извлекают выгоду из удобного и надежного управления зависимостями Poetry. ✌

Управление хуками Git с предварительной фиксацией

Git предоставляет хуки, которые позволяют запускать пользовательские команды при выполнении важных действий, таких как фиксация или отправка. Вы можете использовать это для запуска автоматических проверок при фиксации изменений. Pre-commit - это структура для управления и поддержки таких хуков. Используйте его, чтобы интегрировать в свой рабочий процесс лучшие стандартные линтеры, даже написанные на языке, отличном от Python.

Установите предварительную фиксацию через pip или pipx:

pip install --user --upgrade pre-commit

Настройте предварительную фиксацию с помощью файла конфигурации .pre-commit-config.yaml в каталоге верхнего уровня вашего репозитория. Начнем со следующей примерной конфигурации:

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/psf/black
    rev: 19.3b0
    hooks:
    -   id: black

Установите хуки, выполнив следующую команду:

pre-commit install

Перехватчики запускаются автоматически каждый раз, когда вы вызываете git commit, применяя проверки ко всем вновь созданным или измененным файлам. Когда вы добавляете новые хуки (как сейчас), вы можете запускать их вручную для всех файлов, используя следующую команду:

$ pre-commit run --all-files
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Initializing environment for https://github.com/psf/black.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
[INFO] Installing environment for https://github.com/psf/black.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Check Yaml....................................................Passed
Fix End of Files..............................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing LICENSE
Trim Trailing Whitespace......................................Passed
black.........................................................Passed 

Как видно из выходных данных, ловушка end-of-file-fixer завершилась неудачно, потому что в файле лицензии отсутствовала последняя строка новой строки. Хук уже добавил недостающую новую строку к файлу, поэтому вы можете просто зафиксировать файл:

git commit --message="Fix missing newline at end of LICENSE" LICENSE

Однако есть проблема: образец конфигурации привязывает черный к определенной версии, как и файл блокировки Poetry. Эта настройка требует, чтобы вы выравнивали версии вручную, и может привести к неудачным проверкам, когда среды, управляемые pre-commit, Poetry и Nox, расходятся.

Давайте заменим запись Black на ловушку локального репозитория и запустим Black в среде разработки, созданной Poetry:

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: local
    hooks:
    -   id: black
        name: black
        entry: poetry run black
        language: system
        types: [python]

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

Используйте ту же технику для запуска Flake8 из обработчика pre-commit:

# .pre-commit-config.yaml
-   repo: local
    hooks:
    -   id: black
        ...
    -   id: flake8
        name: flake8
        entry: poetry run flake8
        language: system
        types: [python]

Проверки выполняются несколько быстрее, чем соответствующие сеансы Nox, по двум причинам:

  • Они работают только с файлами, измененными рассматриваемой фиксацией.
  • Они предполагают, что инструменты уже установлены.

Спасибо за прочтение!

В следующей главе мы расскажем о добавлении аннотаций типов и проверке статических типов в ваш проект.

  1. Изображения в этой главе взяты из серии футуристических картин Жана-Марка Коте и других художников, выпущенных во Франции в 1899, 1900, 1901 и 1910 годах (источник: Wikimedia Commons через The Public Domain Review).