Линтинг кода бывает очень долгим, а в ситуациях наличия большого legacy‑проекта, который решили «причесать», линтинг может причинять боль и страдания разработчикам. В этой статье мы найдем решение, которое позволит без проблем линтить код с любого этапа разработки и делать это супер быстро и инкрементально! Часто на проектах встречается проблема автоматического форматирования исходного кода для соответствия определенным стандартам стиля. Кроме того, хочется проверять свою разработку на наличие потенциальных проблем с безопасностью и общим качеством. Чтобы не тратить время на ручное ревью и форматирование кода разработчиком, возможно подключить так называемые линтеры — специальные пакеты для автоматизации вышеописанных проверок. Процесс таких проверок идет без выполнения исходного кода, поэтому он называется статическим анализом. Линтеры интегрируются в CI/CD‑процессы (Continuous Integration, Continuous Delivery — непрерывная интеграция и доставка) и выполняются в pipeline (поток автоматической интеграции и доставки). Шаг за шагом стартуют разные типы проверок: собирается образ, запускаются линтеры, далее идут автоматические тесты и деплой приложения в целевую среду доставки. Если проект начинается с нуля, то внедрить статический анализ кода проще: код будет отформатирован и проверен с самого начала, с первого коммита. При таком подходе весь код приложения всегда соответствует стандартам проекта и проверен линтерами. Однако встречаются и проекты, на которых линтеры решили внедрить уже после начала разработки. Например, это был MVP (Minimum Viable Product — минимально жизнеспособный продукт), поэтому решили не тратить время на дополнительную вспомогательную работу. Тогда, если над проектом работает множество разработчиков, возникает проблема синхронизации правил оформления кода и требуется некая стандартизация в правилах.  Правила могут быть зафиксированы в настройках линтеров. После осуществления предварительных настроек линтеры добавляются к проекту, но появляется другая сложность — как быть с уже существующим, еще не отформатированным кодом при изменении части модуля? Если «прогнать» линтер по модулю, то будет затронут код, который не относится к задаче разработчика. Это может усложнить и замедлить процессы код‑ревью и доставки кода. Логичным видится решение проблемы путем форматирования только сделанных изменений, то есть отслеживать код инкремента. Для этого каким‑то образом необходимо получить эту разницу в коде и передать её на вход линтеру. Так как у разработчиков в распоряжении всегда есть важный инструмент для работы с исходным кодом, а именно система контроля версий, то можно воспользоваться ей. git предоставляет подобную функциональность с помощью команды git diff, которая, согласно документации, позволяет получить разницу кода между коммитом и целевой веткой. Например, стандартный сценарий доставки кода следующий. Существует ветка master (или main), которая всегда доставляется до продакшн сервера. В эту ветку отправляется весь код, над которым работают разработчики в своих feature‑ветках. Значит, можно сопоставить версии кода из текущей feature‑ветки и целевой master‑ветки с получением искомой разницы. Именно эта разница и будет передаваться линтеру для проверки, что позволит отслеживать только измененный код и форматировать его. Подобный подход применим не ко всем типам линтеров.  Существует множество линтеров, нацеленных на решение определенных задач. Линтеры можно условно разделить на две группы: для автоматического форматирования и статического анализа. Линтеры, относящиеся к первой группе, исправляют оформление исходного кода согласно указанным в конфигурации стандартам. Линтеры из второй группы проверяют соответствие типов, наличие неиспользуемых импортов и переменных, оценивают качество кода и так далее.  Для инкрементального линтинга хорошо подходят линтеры‑форматировщики, так как они только форматируют код, поступающий к ним. То есть им можно передавать осмысленную (синтаксически корректную) часть кода и они её правильно обработают. Линтеры из второй группы не всегда способны правильно воспринять и интерпретировать лишь фрагменты кода без контекста их применения, так как они анализируют общую структуру, например, обнаруживают наличие неиспользуемых импортов модулей. Поэтому для такой группы линтеров инкрементальная проверка не применима в полной мере или не возможна. Однако допустимо передавать им список обновленных файлов для анализа, а не части затронутого изменениями кода.  При таком подходе (с передачей целиком файлов) может проверяться и legacy‑код (код, который ранее существовал в проекте), что не всегда является целью, потому что ревьювить такой код сложнее и появляются изменения, не относящиеся к задачам разработки. Тогда необходимо настроить конфигурацию линтера таким образом, чтобы степень строгости проверки была достаточной и в то же время оптимальной для конкретной ситуации и проекта. Исходя из этого, можно реализовать инкрементальный линтинг с гарантированным форматированием только измененного кода и опционально со статической проверкой файлов (возможно и с исправлением плохого кода), в которых были изменения. Примеры линтеров из первой группы: Black, autopep8, YAPF, isort. Примеры линтеров из второй группы: Pylint, Mypy, Flake8, Pyflakes. Для решения вышеизложенной проблемы (с ней сталкивается едва ли не каждый разработчик) были предприняты попытки создания open‑source продукта под названием darken, который впоследствии переродился в проект Darker. Darker — линтер для инкрементального форматирования кода. Он основан на Black и даже использует его в своем исходном коде. Для обеспечения совместимости авторы Darker постоянно отслеживают изменения в Black и вносят соответствующие доработки для корректной работы с новой версией Black. Darker под капотом реализует все необходимые действия с git‑ом (использует git diff), находит измененный код и передает его Black для форматирования. Результат работы линтера записывается в исходные файлы, тем самым форматируются только обновленные блоки кода. У Darker есть хорошая документация, которая позволит понять, как его настроить именно под нужды на вашем проекте. Так как Darker задействует git для получения информации о разнице кода можно задавать целевую ветку для сравнения. По умолчанию Darker использует последний коммит (HEAD — указатель на последний коммит в текущей ветке) в качестве отправной точки. Кроме того, есть возможность задать конкретные файлы для форматирования или отформатировать целиком весь проект. Чтобы попробовать Darker в действии достаточно его установить и запустить команду в корне репозитория с исходным кодом: В консоль будет выведена информация с демонстрацией инкремента изменения (diff) и предложением отформатировать конкретные строки кода. Исходный код при этом затронут не будет. Данная команда полезна для предварительного анализа перед запуском фактического форматирования.  Чтобы действительно отформатировать код достаточно запустить команду: В консоль будет выведена информация о том какие файлы были затронуты обновлением. Darker помимо своей основной функциональности интегрирован и с другими линтерами: flynt для обновления устаревшего стиля оформления строк на f‑строки, isort для сортировки импортов. Кроме того, Darker может работать с подключаемыми внешними линтерами. Список подходящих на момент написания статьи: Mypy, Pylint, Flake8, cov_to_lint.py (разработанный автором Darker). Для статического анализа кода в затронутых изменениями файлах рекомендую присмотреться к линтеру Ruff. Он разработан на языке Rust, включает в себя большое количество правил для анализа (более 700) и работает от 10 до 100 раз быстрее других линтеров, согласно заявлению его авторов. Он один способен заменить целый ряд линтеров: Flake8, isort, pydocstyle, yesqa, eradicate, pyupgrade, autoflake. Но специализированные инструменты для проверки типов (например, Mypy) Ruff в полной мере заменить не сможет. Если вы хотите автоматизировать процесс линтинга еще больше, то это возможно. Ruff умеет исправлять fixable‑ошибки (исправляемые ошибки) в коде. Например, удалить неиспользуемые импорты или переменные. Полный список замечаний, которые можно исправить с помощью Ruff, доступен в документации. Все нужные правила возможно описать в едином файле pyproject.toml.  Благодаря тонкой настройке линтера можно добиться необходимого вам результата, что позволит снизить количество рутинных действий и упростить жизнь разработчика. Хотелось бы иметь некое универсальное решение, обладающее преимуществами Darker и Ruff для инкрементального линтинга — быструю работу и простоту использования с гибкостью конфигурации. Darker предоставляет возможность передачи консольной команды для запуска линтера в качестве аргумента. Это позволяет попробовать запустить Ruff вместе с Darker: Ruff действительно работает только для измененных файлов и выводит предупреждения в соответствии с его настройками, но действует он не так быстро, как ожидалось согласно представлению о заявленной «молниеносной» работе. Такая команда при проведении эксперимента выполнялась 12 секунд в проекте с 42 тысячами строк кода. Причем сам Darker отработал и вывел результат за 2 секунды. Остальное время ушло на выполнение команды, которая указана в качестве значения аргумента ‑-lint. Если запустить данную команду отдельно, то получим результат за 70 миллисекунд (0.070 сек.), что в 142 раза быстрее!  Конкретные значения сильно зависят от контекста (объема исходного кода, количества изменений в нем и настроек линтера), но порядок величин понятен. Это значит, что если мы хотим по настоящему быстрого результата без раздражающего ожидания процесса линтинга, то Ruff необходимо запускать вне Darker. Да, в документации к Darker не заявлена поддержка Ruff, но попробовать стоило. Всё‑таки команда с запуском стороннего линтера удобна и доступна «из коробки».  Осталось решить вопрос запуска Ruff только для измененных файлов. Возможно разработать решение самостоятельно на базе команды git diff или попробовать найти что‑то готовое и подходящее. После недолгих поисков оказалось, что решение есть — git‑diff‑lint! Скрипт давно не обновлялся, но он минималистичен и работоспособен. Идея та же — скрипт получает относительные пути ко всем затронутым изменениями файлам и выводит их в консоль: git diff --name-only --diff-filter=d — получение путей ко всем измененным файлам (кроме удаленных). git merge‑base HEAD "origin/main" — получение общего предка между двумя коммитами (подробнее в документации git). grep -E "\.py$" — фильтрация по расширению .py. Вывод этой команды вполне можно передать на вход Ruff для последующего анализа: Действительно, это работает. Линтер с ожидаемой скоростью обработал все измененные файлы и вывел результат в консоль. Теперь осталось объединить все наработки вместе: взять Darker с его возможностью форматирования измененных строк кода и Ruff с его талантом быстро проводить статический анализ. Можно использовать следующую конструкцию для проверки кода (без внесения командой изменений в исходный код): Данная команда выведет в консоль все ошибки статического анализа, включая предложения линтера отформатировать код. Опция ‑-exit‑zero позволит выполниться следующей команде в случае наличия ошибок, обнаруженных Ruff.  Как уже известно Ruff достаточно способен, чтобы попытаться улучшить плохой код. Для того, чтобы исправить найденные ошибки, можно запустить команду: Но нужно быть осторожным и не переоценить его возможностей. Иногда изменения могут привести к критическим ошибкам в коде. Для того, чтобы этого избежать, необходимо тщательно настроить линтер и отключить те проверки, которые не требуются или потенциально могут привести к проблемам. Для конфигурации можно задать список исправляемых ошибок (fixable) и список неисправляемых ошибок (unfixable). Также в комплексном автоматизированном решении автоматические тесты способы решить эту задачу и выявить проблемы с кодом на раннем этапе. Вы же их пишете, правда? Для автоматизации команд и удобства внедрения в повседневные процессы разработки и в CI/CD можно подготовить необходимые sh‑скрипты и использовать Makefile. В нем нужно описать требуемые таргеты с вызовами скриптов. Это позволит в одной команде запустить комплексный процесс анализа кода. Команды можно настроить нужным вам образом. В примере ниже идея в том, чтобы проверять форматирование кода и вызывать ошибку при наличии расхождений с правилами и в то же время статический анализ линтером Ruff не будет приводить к ошибкам даже при обнаруженных замечаниях. Пример оформления таргетов в Makefile: Тогда всё, что нужно будет сделать разработчику до оформления очередного коммита, вызвать команду make fix, которая приведет к форматированию измененного кода с автоматическим исправлением некачественного кода в затронутых файлах. Причем команда выполнится довольно быстро.  Для запуска проверок в CI/CD‑процессах команду make lint или исходные команды под этим таргетом можно добавить в один из шагов pipeline до слияния веток. Тогда наличие ошибок будет препятствовать слиянию и сигнализировать разработчику, ответственному за ветку, разобраться в проблеме и устранить найденные недостатки. Впоследствии такой подход обеспечит инкрементальное улучшение качества кода с каждым очередным коммитом. Примеры скриптов можно найти на моем GitHub. Вы можете их адаптировать под свой проект и цели. Всем спасибо за внимание и молниеносного линтинга!  Python Developer