
Всем привет! Меня зовут Юра, я Python-разработчик в Точке. В статье я покажу, как написать шаблон с линтером для Gitlab CI, чтобы при старте нового проекта (или уже запущенного) было легко добавить линтер в пайплайны.
Мотивация
В своих проектах я активно использую flakeheaven (современная обёртка над flake8) с большим количеством плагинов — это позволяет избежать бесполезных дискуссий на код-ревью, поддерживать порядок в проектах и предотвращать некоторые ошибки и уязвимости. За решение этих проблем я плачу временем на прекоммит-хуки и задачи в CI.
Когда плагинов становится слишком много, проверка занимает больше времени и, соответственно, сильнее вырывает из потока. Поэтому я обратил внимание на ruff — реализацию правил из flake8 и нескольких самых популярных плагинов на Rust. С помощью ruff проверка объёмных проектов проходит очень быстро: например, в моём проекте запуск flakeheaven занимает минуту, а ruff — около секунды.
В Точке довольно большое Python-сообщество, которое занимается не только поддержкой внутренних библиотек, но и инструментов для разработчиков, таких как шаблоны для сборки и анализа кода. Поэтому, когда я настроил ruff в своём проекте, я решил сделать шаблон для Gitlab CI, который можно было бы использовать в других проектах.
Gitlab CI
У нас используется плагин для Gitlab CI, который позволяет запускать джобы в кластере k8s, поэтому сделаем проект с образом Docker и в шаблоне будем использовать его.
Дисклеймер
Как у любой приличной компании, у нас внутри развёрнут docker registry, но для простоты дальше в коде я вместо него буду использовать dockerhub и прятать его под переменной окружения $CI_REGISTRY.
В Dockerfile возьмём последнюю версию python, установим линтер и загрузим его в хранилище:
# Dockerfile FROM python:3.12-slim RUN python3 -m pip install ruff==0.1.0
Для запуска ruff требуется версия python 3.7, но для анализа используются значение requires-python в pyproject.toml и аргумент запуска --target-version, поэтому мы используем самый свежий образ python. Для версионирования используем следующую схему: когда обновляем Dockerfile с новой версией линтера, помечаем коммит тегом, совпадающим с версией ruff. В gitlab-ci при появлении нового тега будем загружать новую версию образа в хранилище
# gitlab-ci.yaml variables: CI_REGISTRY: ... stages: - build build: stage: build rules: - if: $CI_COMMIT_TAG # before_script: docker login script: - docker build -t "ruff:$CI_COMMIT_TAG" - docker tag "ruff:$CI_COMMIT_TAG" "$CI_REGISTRY/ruff-template/ruff:$CI_COMMIT_TAG" - docker push "$CI_REGISTRY/ruff-template/ruff:$CI_COMMIT_TAG"
Теперь, когда у нас появился образ с линтером в хранилище, пришло время написать шаблон, в котором мы будем его запускать:
# linters.yaml .lint-ruff: variables: RUFF_VERSION: latest RUFF_SOURCE: . CI_REGISTRY: ... image: $CI_REGISTRY/ruff-template/ruff:$RUFF_VERSION script: - | set -ex RUFF_ARGS="" if [[ -n $RUFF_TARGET_VERSION ]]; then RUFF_ARGS="$RUFF_ARGS --target-version $RUFF_TARGET_VERSION" fi echo "ruff args: $RUFF_ARGS" echo "ruff source: $RUFF_SOURCE" ruff $RUFF_ARGS $RUFF_SOURCE
С помощью переменной RUFF_TARGET_VERSION пользователь сможет указывать целевую версию языка для запуска. Аналогично можно добавить поддержку остальных параметров запуска, если появится необходимость. Теперь, когда у нас есть образ и шаблон, мы можем его использовать в своих проектах:
# .gitlab-ci.yaml include: - project: ruff-template file: linters.yaml stages: - test lint-ruff: extends: .lint-ruff stage: test variables: RUFF_TARGET_VERSION: py312
У меня в проекте используется Python 3.12, поэтому я указываю переменную окружения, чтобы правильно работали проверки импортов из future и новые фишки языка.
Настройка
По умолчанию ruff проверяет код на соответствие стандарту PEP8 и очевидных проблем вроде неиспользованных импортов с помощью правил pyflakes и pycodestyle. В отличие от flake8, ruff не поддерживает 3rd party плагины, поэтому все дополнительные правила реализованы в проекте. На данный момент реализовано более 700 правил, поэтому с высокой долей вероятности ваш любимый плагин уже в их числе. Например:
-
DTZне даёт создать объектtimestampбез таймзоны -
PTпомогает поддерживать одинаковый стиль в тестах -
TRYнужен для соблюдения лучших практик в работе с исключениями
Полный список правил можно посмотреть в документации.
Ниже готовый конфиг без «вкусовщины»: его можно скопировать в pyproject.toml и начать использовать ruff на полную катушку.
[tool.ruff] src = ["src", "tests"] target-version = "py311" line-length = 90 extend-select = [ "C90", # mccabe "N", # pep8-naming "UP", # pyupgrade "S", # bandit "BLE", # flake8-blind-except "B", # bugbear "C4", # comprehensions "DTZ", # datetimez "EM", # error-messages "FA", # future-annotations "ISC", # implicit string concat "PIE", # flake8-pie "PT", # flake8-pytest-style "SLF", # flake8-self "SIM", # flake8-simplify "ARG", # flake8-unused-argument "ERA", # eradicate commented out code "TRY", # tryceratops ] [tool.ruff.extend-per-file-ignores] "migrations/**.py" = ["E501"] # line length "tests/**.py" = ["S101"] # use of `assert`
Итоги
Создание шаблона — это объёмная задача, которую можно разбить на несколько этапов, и на каждом из них можно извлечь пользу:
-
Добавление линтера упрощает поддержку единого стиля в проекте и предотвращает часть ошибок.
-
Отдельный образ можно использовать в нескольких проектах, и задача на линт будет занимать меньше времени.
-
Шаблон для запуска образа позволяет экономить время на поддержке для нескольких команд.
Я надеюсь, что благодаря статье вы увидели, что общие инструменты для разработчиков — это не привилегия мегакорпораций, и их можно начинать использовать, когда у вас становится больше одного репозитория.
ссылка на оригинал статьи https://habr.com/ru/articles/768292/
Добавить комментарий