Продолжаю рассказывать, как open source проект Python Дайджест спустя 5 лет без обновлений удалось актуализировать по всему стэку технологий. В первой части рассказал, как удалось outdated проект с Python 3.4 обновить до Python 3.11 и Django 4.1.
В этой части расскажу, как удалось максимально дешево привести кодовую базу в актуальное состояние.

Содержание цикла статей
- 1 часть: Как обновиться с Python 3.4 до Python 3.11, если pip уже сломан
 - 
2 часть: Как актуализировать всю кодовую базу с помощью pre-commit
— вы здесь — - 3 часть: Как сделать CI для OpenSource проекта с Github Actions
 - 4 часть: Как ускорить Django проект до (почти) максимума
 
Состояние после обновления до Python 3.11
- Приложение запускается, тесты проходят.
 - Код написан для работы с Python 3.4 и имеет вставки для поддержки Python 2.7.
 - Куча неиспользуемых импортов, переменных, код не соблюдает даже части PEP8, форматирование строк сильно прыгает.
 - Нет времени и какого-либо желания руками переписывать код для поддержки 3.6+ фич.
 
Задача (часть 2) — перейти от ручного форматирования кода к автоматизированному
Приступим к решению.
План работ
- Подобрать автоформатер, который не ломает код
 - Настроить автозапуск перед коммитом
 - Отформатировать код
 
Зачем форматируем код
Исходный код — текстовое представление замысла разработчика. Можно понять смысл программы и на COW-language, только потребуется больше усилий, да и меньшее количество коллег захотят это делать. Человеку проще запомнить один способ чтения исходного кода программы, чем под каждый новый файл перенастраивать себя и свои глаза.
Начинающие специалисты еще не привыкли, что перечитывать код нужно чаще, чем писать его. Поэтому склонны к оформлению в стиле «как пойдет». И сложность не столько в нестандартном оформлении, сколько в очень частом изменении стиля. После года-двух специалист уже может держать один стиль дольше и жить становится проще.
В командах у специалистов разный уровень и, чтобы получить единый стиль у всех, выбирается единый code style. И дальше возникает вопрос — как его проверять или выполнять. Желательно автоматически.
Как делать автоформатирование кода
Когда говорим «автоформатирование Python кода», то сразу вспоминается black/autopep8/flake8/isort/redbaron/yapf и инструменты на их базе. Это инструменты, которые позволяют привести код к единому формату. Про них есть статьи на habr — раз, два, три, а sobolevn сделал много докладов на эту тему.
Одни инструменты редактируют только 1 вид кода, например, isort — импорты. Другие инструменты редактируют весь код на Python, а иногда могут применяться и для других языков.
В Python Дайджест нет «исторически сложившихся» ограничений, связанных с кодом, командой и процессом разработки, поэтому автоформатировать хочется все сразу и без лишних настроек. Значит, основным форматтером будет black, а остальные будут дополнять его.
Как обновить код под новую версию Python
Синтаксис Python расширяется/дорабатывается с каждой версией, меняются API даже стандартных библиотек, появляются конструкции, которые выглядят понятнее, устанавливаются более корректные значения «по умолчанию» для функций. Все это можно читать в changelog'ах к версиям Python.
Появление f-string в 3.6 — это наиболее заметное изменение в форматировании строк. Строки стали выглядеть легче и понятнее. При этом пройтись по всей кодовой базе и .format переделать на f-string — занятие на много часов. Подобные изменения можно делать автоматически.
Если раньше активно применяли six и future flags для плавного обновления кода, то теперь можно использовать pyupgrade.
pyupgrade — утилита, которая может обновить код с версии X до версии Y. Утилита аккуратно обновляет «старые» конструкции на «новые». Работает хорошо и не ломает код, поэтому можно запускать при каждом коммите.
Чем запускал скрипты форматирования — pre-commit
Раз инструменты есть, теперь нужно научиться не забывать их запускать. Для этого хорошо подходит концепт с pre-commit git hooks, чтобы непосредственно перед коммитом запускать форматирование и корректировать неточности. Общеупотребимой реализацией концепции является проект pre-commit. 
pre-commit — это расширяемый (за счет hook/плагинов) способ запускать различные скрипты перед коммитом. 
Про него на Хабре писали здесь и здесь.
Этот проект позволяет настроить автоматическую проверку чего только вздумаешь — можете самостоятельно ознакомиться со страницей плагинов.
Из всего перечня плагинов мне нужны:
- Форматирование кода с помощью black, удаление неиспользуемых импортов с isort и autoflake
 - Поиск случайно оставленных секретов/паролей в коде
 - Валидация всех конфигурационных файлов (JSON, YAML), в том числе для CI
 - Поиск устаревших пакетов на основе dependabot
 - Возможность автоматически сконвертировать код до Python 3.11 версии с помощью pyupgrade
 
Это все умеют плагины pre-commit. Вот конфигурация, которая получилась — .pre-commit-config.yaml, pyproject.yaml
Как настроил pre-commit
Для регулярного запуска я выбрал такие плагины для pre-commit:
# copy-paste .pre-commit-config.yaml
# ....
exclude: "^docs/|/migrations/"
default_stages: [commit]
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-merge-conflict
      - id: detect-private-key
      - id: debug-statements
  - repo: https://github.com/asottile/pyupgrade
    rev: v3.3.1
    hooks:
      - id: pyupgrade
        args: [--py311-plus]
  - repo: https://github.com/PyCQA/isort
    rev: 5.12.0
    hooks:
      - id: isort
  - repo: https://github.com/psf/black
    rev: 23.1.0
    hooks:
      - id: black
        exclude: ^.*\b(migrations)\b.*$
  -   repo: https://github.com/PyCQA/autoflake
      rev: v2.0.1
      hooks:
      -   id: autoflake
  - repo:  https://github.com/python-jsonschema/check-jsonschema
    rev: 0.21.0
    hooks:
      - id: check-github-workflows
      - id: check-dependabot
  - repo: https://github.com/pre-commit/pygrep-hooks
    rev: v1.10.0
    hooks:
      - id: python-use-type-annotations
      - id: python-check-blanket-noqa
  -   repo: https://github.com/Yelp/detect-secrets
      rev: v1.4.0
      hooks:
      -   id: detect-secrets
          args: ['--baseline', '.secrets.baseline']
          exclude: package.lock.json
А чтобы black и isort не ссорились — внес такие изменения в конфигурацию проекта.
# copy isort/black settings in pyproject.toml
# ...
[tool.black]
line-length = 119
target-version = ['py311']
include = '\.pyi?$'
[tool.isort]
profile = "black"
line_length = 119
multi_line_output = 3
include_trailing_comma = true
use_parentheses = true
ensure_newline_before_comments = true
Как использовал pre-commit
Чтобы использовать выполняем:
# install
poetry add pre-commit --group dev
pre-commit install
# run check
pre-commit run --show-diff-on-failure --color=always --all-files
Последнюю строку можно поместить в Makefile с названием check и руками запускать.
Установив pre-commit, теперь перед формированием коммита, выполнятся все скрипты.
Что отформатировал в коде
Сразу после первого запуска скрипты смогли изменить:
- Удалены строки 
from __future__ import unicode_literalsи# -*- encoding: utf-8 -*-.- # -*- coding: utf-8 -*- - from __future__ import unicode_literals - 
Скорректировано формирование строк — двойные кавычки для строк и f-string.
- name = '{} - {}'.format(package.name, package_version) + name = f"{package.name} - {package_version}"
 - Исправлено форматирование множественных импортов — теперь импорты организованы в группы — системные, django и из приложения.
 - 
Удалены неиспользуемые/модифицированы импорты
- from mock import patch + from unittest.mock import patch
 - 
Использована корректная форма Dictionary Comprehension, вместо преобразования через dict.
- headers = dict((n, n) for n in fieldnames) + headers = {n: n for n in fieldnames}
 - 
Удалены/добавлены переносы строк, где требуется.
- list_filter = ('is_activated', 'if_element', 'if_action', 'then_element', 'then_action',) + list_filter = ( "is_activated", "if_element", "if_action", "then_element", "then_action", )
 
И теперь разработчик может писать код как угодно — форматтеры приведут все в порядок.
Прелесть pre-commit, что его можно запускать в CI (про это в следующей части) и таким образом получать дополнительную проверку на соответствие формата кода. И тогда тот, кто «забыл» поставить себе pre-commit или предпочитает кодить из браузера, не сможет залить некачественное изменение на сервер.
Выводы
- Современные автоформатеры Python кода уже не склонны ломать код и можно их использовать активнее (не забудьте настроить свой формат).
 - pre-commit — это проект, который заметно уменьшает вероятность залить в репозиторий не то или не так. Готовых плагинов очень много и каждый для своей команды найдет, что взять оттуда. Форматирование кода и поиск секретов — берите точно.
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.