Всем привет! Меня зовут Юра, я 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`

Итоги

Создание шаблона — это объёмная задача, которую можно разбить на несколько этапов, и на каждом из них можно извлечь пользу:

  1. Добавление линтера упрощает поддержку единого стиля в проекте и предотвращает часть ошибок. 

  2. Отдельный образ можно использовать в нескольких проектах, и задача на линт будет занимать меньше времени. 

  3. Шаблон для запуска образа позволяет экономить время на поддержке для нескольких команд. 

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

Комментарии (0)