$ sudo teach IT
Модуль 7 · Продвинутое использование

Настройка для больших проектов

exclude, target-version, монорепо, per-directory конфигурация — тонкая настройка под масштаб.

⚙️ Конфигурация 🕐 ~40 минут
🎯

Особенности больших проектов

Когда проект вырастает до десятков тысяч строк кода, сотен файлов и нескольких команд разработчиков, настройка линтера перестаёт быть тривиальной задачей. Возникают вопросы:

  • Как исключить из проверки сгенерированные файлы, миграции, вендорные библиотеки?
  • Как настроить разные правила для разных частей проекта (src, tests, scripts)?
  • Как постепенно внедрить Ruff в legacy-проект, где тысячи ошибок?
  • Как работать с монорепозиторием, где у каждого пакета свои настройки?
  • Как указать минимальную версию Python, чтобы Ruff предлагал только совместимый синтаксис?

В этом уроке мы разберём каждую из этих задач и покажем, как Ruff решает их через гибкую систему конфигурации. Правильная настройка Ruff для большого проекта — это не просто «поставить и забыть», а продуманная стратегия, которая учитывает структуру проекта, процессы команды и legacy-код.

💡 Ruff из коробки поддерживает иерархическую конфигурацию: он ищет ruff.toml или pyproject.toml в каждой директории, начиная от текущей и двигаясь вверх. Это позволяет иметь глобальные настройки на уровне проекта и локальные — для отдельных пакетов.
🚫

exclude и extend-exclude

В больших проектах всегда есть файлы и директории, которые не нужно проверять линтером: сгенерированный код, миграции базы данных, вендорные библиотеки, устаревшие модули. Ruff предоставляет два способа исключения файлов: exclude и extend-exclude.

Дефолтные исключения Ruff

Ruff по умолчанию игнорирует следующие директории и файлы:

  • .git/ — git-директория
  • .gitignore — git-файл
  • .direnv/, .env/ — виртуальные окружения
  • .venv/, venv/, env/ — виртуальные окружения
  • __pycache__/ — кеш Python
  • node_modules/ — если есть Node.js зависимости
  • .mypy_cache/, .pytest_cache/, .ruff_cache/ — кеш инструментов

extend-exclude (рекомендуется)

extend-exclude добавляет пути к дефолтным исключениям Ruff. Это рекомендуемый способ, так как он сохраняет разумные умолчания и добавляет только то, что нужно вашему проекту.

[tool.ruff]
extend-exclude = [
    "migrations/",          # миграции БД (Django, Alembic)
    "docs/conf.py",         # сгенерированная документация
    "legacy/",              # устаревший код
    "vendor/",              # сторонние зависимости
    "generated/",           # сгенерированный код
    "*/_pb2.py",            # protobuf-файлы
    "*.egg-info/",          # пакетная информация
    "scripts/archive/",     # архивные скрипты
]

exclude (переопределение)

exclude полностью переопределяет дефолтные исключения. Это не рекомендуется, так как вы можете случайно исключить слишком много или забыть про стандартные паттерны.

[tool.ruff]
# ПОЛНОСТЬЮ переопределяет дефолты — будьте осторожны!
exclude = [
    ".venv",
    "__pycache__",
    "migrations/",
    "vendor/",
]

Глоббинг в exclude

Ruff поддерживает glob-паттерны для исключения файлов:

[tool.ruff]
extend-exclude = [
    # Все файлы в директории и поддиректориях
    "migrations/*.py",
    # Конкретные файлы
    "src/old_module.py",
    "src/legacy_*.py",
    # Все protobuf файлы
    "**/*_pb2.py",
    "**/*_pb2_grpc.py",
    # Все файлы в папках test_data
    "**/test_data/*.py",
]
💡 Используй extend-exclude вместо exclude — первый добавляет к умным дефолтам Ruff, второй их полностью заменяет. Разница критична: с exclude вы можете случайно начать проверять .venv или .git, если забудете их указать.

Проверка exclude-правил

Чтобы проверить, какие файлы Ruff будет проверять, используйте флаг --show-files:

# Показать все файлы, которые Ruff будет проверять
ruff check --show-files .

# Показать только файлы с определённым паттерном
ruff check --show-files . | grep "migrations"

# Посмотреть статистику
ruff check --show-files . | wc -l
🐍

target-version

target-version — одна из самых важных настроек для больших проектов. Она указывает Ruff минимальную поддерживаемую версию Python. Это критически влияет на правила категории UP (pyupgrade): Ruff не будет предлагать синтаксис, который недоступен в указанной версии.

[tool.ruff]
target-version = "py311"   # минимально поддерживаемая версия

# возможные значения: "py37", "py38", "py39",
#                     "py310", "py311", "py312", "py313"

Как target-version влияет на правила

Вот как target-version меняет поведение Ruff:

Правило Что предлагает Минимальная версия
UP006List[X] → list[X]py39
UP007Optional[X] → X | Nonepy310
UP012open().read() → Path.read_text()py36
UP031% форматирование → f-stringpy36
UP036@asyncio.coroutine → async/awaitpy36
UP038isinstance(x, (A, B)) → isinstance(x, A | B)py310

Пример: если target-version = "py311", Ruff разрешит X | None вместо Optional[X] (UP007) и list[X] вместо List[X] (UP006), но не будет предлагать match/case рефакторинг (доступен только с py310).

⚠️ Всегда указывайте target-version явно. По умолчанию Ruff использует py38, что может привести к тому, что Ruff не будет предлагать современный синтаксис, даже если ваш проект использует Python 3.12.
📁

src layout — правильная структура проекта

Многие Python-проекты используют src layout — структуру, в которой весь исходный код находится в директории src/. Это предотвращает случайный импорт из репозитория вместо установленного пакета и улучшает изоляцию.

my_project/
├── pyproject.toml
├── src/
│   └── my_package/
│       ├── __init__.py
│       ├── core.py
│       └── utils.py
├── tests/
│   ├── test_core.py
│   └── test_utils.py
├── docs/
├── scripts/
└── migrations/

Для корректной работы с src layout настройте параметр src:

[tool.ruff]
src = ["src"]   # корень исходников для разрешения импортов

Без src = ["src"] Ruff может неправильно разрешать импорты, особенно если вы используете правила I (isort) для сортировки импортов. Указав src, вы говорите Ruff, какие директории содержат корни Python-пакетов.

💡 Параметр src может принимать несколько путей. Например, для монорепозитория: src = ["packages/api/src", "packages/worker/src"].
🏗️

Монорепозиторий

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

Структура монорепозитория с Ruff

monorepo/
├── ruff.toml               # общие настройки для всего репозитория
├── packages/
│   ├── api/
│   │   ├── ruff.toml       # переопределения для api
│   │   ├── src/
│   │   │   └── api/
│   │   │       ├── __init__.py
│   │   │       └── main.py
│   │   └── tests/
│   │       └── test_api.py
│   └── worker/
│       ├── ruff.toml       # переопределения для worker
│       ├── src/
│       │   └── worker/
│       │       ├── __init__.py
│       │       └── tasks.py
│       └── tests/
│           └── test_tasks.py
└── shared/
    ├── ruff.toml           # переопределения для shared
    └── src/
        └── shared/
            └── utils.py

Корневой ruff.toml

Корневой конфигурационный файл задаёт общие настройки для всего репозитория:

# monorepo/ruff.toml
line-length = 120
target-version = "py311"

[lint]
select = ["E", "F", "I", "N", "UP", "B", "S", "SIM"]
ignore = ["E203", "W503"]

[lint.per-file-ignores]
"tests/**/*.py" = ["S101"]   # разрешить assert в тестах

Локальный ruff.toml для пакета

Локальный конфиг переопределяет или дополняет корневой:

# monorepo/packages/api/ruff.toml
# Для API-пакета используем более строгие правила
line-length = 88   # переопределяем длину строки

[lint]
select = ["ALL"]   # все правила для API (критический сервис)

[lint.per-file-ignores]
"*/migrations/*" = ["ALL"]   # игнорировать миграции

Для пакета worker — свои настройки:

# monorepo/packages/worker/ruff.toml
# Для worker-пакета — более мягкие правила

[lint]
select = ["E", "F", "I", "N"]
ignore = ["E501"]   # разрешить длинные строки (логи)

Как Ruff ищет конфигурацию

Когда Ruff проверяет файл packages/api/src/api/main.py, он ищет конфигурацию в следующем порядке:

  1. Сначала ищет в текущей директории packages/api/src/api/
  2. Затем поднимается выше: packages/api/src/
  3. Затем packages/api/ — находит ruff.toml, применяет его
  4. Продолжает подниматься до корня: packages/ → корень — находит ruff.toml
  5. Объединяет настройки, при этом локальные переопределяют глобальные
💡 Ruff использует ближайший конфигурационный файл. Если в packages/api/ruff.toml указан line-length = 100, а в корневом line-length = 120, то для файлов в packages/api/ будет использоваться 100.
📂

Per-directory конфигурация

Помимо отдельных конфигурационных файлов, Ruff поддерживает настройку правил для разных директорий через per-file-ignores. Это позволяет гибко настраивать правила без создания множества ruff.toml.

[tool.ruff.lint.per-file-ignores]
# Игнорировать S101 (assert) во всех тестовых файлах
"tests/**/*.py" = ["S101"]

# Игнорировать F401 (неиспользуемые импорты) в __init__.py
"__init__.py" = ["F401"]

# Игнорировать все правила в миграциях
"migrations/*.py" = ["ALL"]

# Игнорировать D (docstring) в скриптах
"scripts/**/*.py" = ["D"]

# Игнорировать специфические правила в примерах
"examples/**/*.py" = ["N801", "N802", "N803"]

# Более строгие правила для core-модулей
"src/core/**/*.py" = []  # не игнорировать ничего!

Пример настройки для типичного Django-проекта:

[tool.ruff.lint.per-file-ignores]
# Django-миграции
"*/migrations/*.py" = ["ALL"]

# Тесты
"*test*.py" = ["S101", "D"]
"tests/**/*.py" = ["S101", "D"]

# Django management commands
"*/management/commands/*.py" = ["D"]

# Настройки Django
"settings*.py" = ["E501"]

# Файлы конфигурации
"conf.py" = ["D"]
"settings*.py" = ["D", "E501"]
💡 per-file-ignores — мощный инструмент, но не злоупотребляйте им. Если вы замечаете, что игнорируете одни и те же правила во многих файлах, возможно, эти правила не подходят для вашего проекта и их лучше отключить глобально через ignore.
📈

Постепенное внедрение в legacy-проекте

Внедрение линтера в существующий большой проект — задача, которая требует стратегии. Если запустить Ruff с полным набором правил на legacy-проекте, вы получите тысячи ошибок, исправление которых займёт недели. Вот проверенная стратегия постепенного внедрения.

Стратегия 1: Поэтапное расширение правил

Начните с малого набора правил и постепенно расширяйте его. Каждый этап — отдельный PR с понятными изменениями.

# Шаг 1 — только критические ошибки (Pyflakes)
# Неиспользуемые импорты, неопределённые переменные
select = ["F"]

# Шаг 2 — добавить стиль PEP 8 (pycodestyle)
# Пробелы, отступы, пустые строки
select = ["E", "F"]

# Шаг 3 — добавить сортировку импортов
select = ["E", "F", "I"]

# Шаг 4 — добавить именование (pep8-naming)
select = ["E", "F", "I", "N"]

# Шаг 5 — добавить pyupgrade (современный синтаксис)
select = ["E", "F", "I", "N", "UP"]

# Шаг 6 — добавить bugbear (распространённые баги)
select = ["E", "F", "I", "N", "UP", "B"]

# Шаг 7 — добавить упрощение кода
select = ["E", "F", "I", "N", "UP", "B", "SIM"]

Стратегия 2: Игнорирование существующих ошибок

Если вы хотите включить все правила сразу, но не хотите исправлять существующие ошибки, используйте сочетание select = ["ALL"] и ignore для самых частых нарушений, или создайте baseline через --show-settings:

# Быстрый старт: ALL, но с ignore самых частых нарушений
select = ["ALL"]
ignore = [
    "D",      # docstring — слишком много нарушений
    "ANN",    # аннотации — не везде нужны
    "S101",   # assert в тестах — разрешить
    "E501",   # длинные строки — legacy код
    "N802",   # имена функций в CamelCase — legacy
    "N803",   # аргументы в CamelCase — legacy
]

# Постепенно убирайте правила из ignore
# по мере исправления кода

Стратегия 3: Поэтапное внедрение по директориям

Начните с новых модулей, постепенно подключая старые:

# Шаг 1: новый код проверяем строго
# (создаём ruff.toml в src/new_module/)
select = ["ALL"]

# Шаг 2: старый код — минимальная проверка
# (корневой ruff.toml)
select = ["F"]   # только критические ошибки

# Шаг 3: постепенно повышаем планку
# для всего проекта
💡 Каждый шаг — отдельный PR. Так ревью остаётся читаемым и изменения можно откатить по одному. Не пытайтесь внедрить все правила за один день — это приведёт к огромному PR, который никто не сможет нормально ревьюить.

Стратегия 4: Использование --add-noqa

Ruff умеет автоматически добавлять # noqa комментарии ко всем текущим ошибкам. Это позволяет зафиксировать текущее состояние кода и начать проверку новых изменений:

# Добавить # noqa ко всем текущим ошибкам
ruff check --add-noqa --select ALL .

# После этого можно запускать ruff check --select ALL
# и видеть только новые ошибки

# В свободное время команда может постепенно
# исправлять ошибки и удалять # noqa
⚠️ --add-noqa — мощный, но временный инструмент. Не оставляйте # noqa в коде надолго. Это маскирует проблемы и снижает пользу от линтера. Используйте подход «исправил ошибку — удалил noqa».
📝

Полный пример конфигурации для production-проекта

Вот пример production-готовой конфигурации Ruff для среднего/крупного проекта, который объединяет все рассмотренные концепции:

[tool.ruff]
# Базовая конфигурация
line-length = 100
target-version = "py311"
src = ["src"]

# Исключения
extend-exclude = [
    "migrations/",
    "vendor/",
    "generated/",
    "**/*_pb2.py",
    "docs/conf.py",
]

# Форматирование
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "lf"

# Линтинг
[tool.ruff.lint]
select = [
    "E",    # pycodestyle
    "W",    # pycodestyle warnings
    "F",    # Pyflakes
    "I",    # isort
    "N",    # pep8-naming
    "UP",   # pyupgrade
    "B",    # flake8-bugbear
    "SIM",  # flake8-simplify
    "C4",   # flake8-comprehensions
    "S",    # flake8-bandit (безопасность)
    "D",    # pydocstyle
    "ANN",  # flake8-annotations
    "RUF",  # Ruff-specific
]
ignore = [
    "E203",   # пробелы в срезах (конфликт с black)
    "W503",   # перенос операторов (конфликт с black)
    "D100",   # docstring в модулях — не обязательно
    "D104",   # docstring в __init__.py
    "ANN101", # self в методах
    "ANN102", # cls в методах
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "D", "ANN"]
"__init__.py" = ["F401"]
"migrations/*.py" = ["ALL"]
"scripts/**/*.py" = ["D", "ANN"]
"examples/**/*.py" = ["D", "ANN", "N801"]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.isort]
known-first-party = ["my_project"]
known-third-party = ["django", "flask", "sqlalchemy", "pydantic"]
extra-standard-library = ["tomllib", "zoneinfo"]

[tool.ruff.lint.mccabe]
max-complexity = 12

[tool.ruff.lint.flake8-annotations]
my-py-version = "3.11"
suppress-none-returning = true
🐛

Типичные проблемы и их решение

❌ Ruff проверяет слишком много файлов

Проверьте extend-exclude — добавьте директории с generated-кодом. Используйте ruff check --show-files . для просмотра проверяемых файлов.

❌ Ruff не находит конфигурацию

Убедитесь, что файл называется ruff.toml (для Ruff-only конфига) или pyproject.toml (с секцией [tool.ruff]). Ruff ищет конфиги от текущей директории вверх.

❌ Per-file-ignores не работают

Проверьте синтаксис: "tests/**/*.py" = ["S101"]. Кавычки вокруг ключа (glob-паттерна) обязательны, если в нём есть спецсимволы.

❌ Ruff предлагает синтаксис, недоступный в моей версии Python

Проверьте target-version. Если он не указан или указан неверно, Ruff может предлагать синтаксис, недоступный в вашей минимальной версии.

❌ Ruff путает first-party и third-party импорты

Настройте src = ["src"] и known-first-party в isort секции. Убедитесь, что путь к src-директории правильный.

🛠️

Полезные команды для больших проектов

Посмотреть, какие файлы проверяются

ruff check --show-files .

Посмотреть текущую конфигурацию

ruff check --show-settings .

Статистика по ошибкам

ruff check --statistics .

Добавить noqa ко всем текущим ошибкам

ruff check --add-noqa .

Список всех поддерживаемых правил

ruff check --show-settings .

Где Ruff ищет конфигурацию

ruff check --verbose .
✅

Лучшие практики для больших проектов

📋 Рекомендации:

  1. Всегда указывайте target-version — это предотвращает предложения несовместимого синтаксиса.
  2. Используйте extend-exclude вместо exclude — сохраняет дефолтные исключения Ruff.
  3. Используйте src = ["src"] — для src layout или монорепозитория.
  4. Настройте per-file-ignores — для тестов, миграций, скриптов.
  5. Внедряйте Ruff постепенно — начните с F, добавляйте правила шаг за шагом.
  6. Используйте --add-noqa для baseline — но не злоупотребляйте, постепенно исправляйте noqa.
  7. Для монорепозитория — локальные ruff.toml — каждый пакет может иметь свои настройки.
  8. Проверяйте ruff check --show-files . — убедитесь, что exclude работает как ожидается.

📌 Что важно запомнить:

  • extend-exclude добавляет к дефолтам, exclude переопределяет их
  • target-version определяет минимальную версию Python
  • src указывает корни Python-пакетов для правильного разрешения импортов
  • Ruff поддерживает иерархическую конфигурацию — от корня до подпапок
  • Для монорепозитория используйте локальные ruff.toml в каждом пакете
  • Постепенное внедрение — ключ к успеху в legacy-проектах
  • per-file-ignores гибко настраивает правила для разных директорий
  • --add-noqa помогает зафиксировать текущее состояние, но не злоупотребляйте им
📋

Что дальше

Теперь вы знаете, как настроить Ruff для больших проектов и монорепозиториев. Вы освоили все ключевые концепции: exclude, target-version, src layout, per-directory конфигурацию и стратегии постепенного внедрения. Примените эти знания на практике — настройте Ruff в вашем проекте, начните с малого и постепенно повышайте планку качества кода.

Поздравляем с завершением курса по Ruff! Вы узнали всё необходимое для эффективного использования Ruff: от установки и базовых команд до интеграции в CI/CD и настройки для масштабных проектов.

Урок 7.2: Настройка для больших проектов

5 вопросов