Настройка для больших проектов
exclude, target-version, монорепо, per-directory конфигурация — тонкая настройка под масштаб.
Особенности больших проектов
Когда проект вырастает до десятков тысяч строк кода, сотен файлов и нескольких команд разработчиков, настройка линтера перестаёт быть тривиальной задачей. Возникают вопросы:
- Как исключить из проверки сгенерированные файлы, миграции, вендорные библиотеки?
- Как настроить разные правила для разных частей проекта (src, tests, scripts)?
- Как постепенно внедрить Ruff в legacy-проект, где тысячи ошибок?
- Как работать с монорепозиторием, где у каждого пакета свои настройки?
- Как указать минимальную версию Python, чтобы Ruff предлагал только совместимый синтаксис?
В этом уроке мы разберём каждую из этих задач и покажем, как Ruff решает их через гибкую систему конфигурации. Правильная настройка Ruff для большого проекта — это не просто «поставить и забыть», а продуманная стратегия, которая учитывает структуру проекта, процессы команды и legacy-код.
ruff.toml или pyproject.toml в каждой директории, начиная от текущей и двигаясь вверх. Это позволяет иметь глобальные настройки на уровне проекта и локальные — для отдельных пакетов.
exclude и extend-exclude
В больших проектах всегда есть файлы и директории, которые не нужно проверять линтером: сгенерированный код, миграции базы данных, вендорные библиотеки, устаревшие модули. Ruff предоставляет два способа исключения файлов: exclude и extend-exclude.
Дефолтные исключения Ruff
Ruff по умолчанию игнорирует следующие директории и файлы:
.git/— git-директория.gitignore— git-файл.direnv/,.env/— виртуальные окружения.venv/,venv/,env/— виртуальные окружения__pycache__/— кеш Pythonnode_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:
| Правило | Что предлагает | Минимальная версия |
|---|---|---|
| UP006 | List[X] → list[X] | py39 |
| UP007 | Optional[X] → X | None | py310 |
| UP012 | open().read() → Path.read_text() | py36 |
| UP031 | % форматирование → f-string | py36 |
| UP036 | @asyncio.coroutine → async/await | py36 |
| UP038 | isinstance(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, он ищет конфигурацию в следующем порядке:
- Сначала ищет в текущей директории
packages/api/src/api/ - Затем поднимается выше:
packages/api/src/ - Затем
packages/api/— находитruff.toml, применяет его - Продолжает подниматься до корня:
packages/→ корень — находитruff.toml - Объединяет настройки, при этом локальные переопределяют глобальные
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: постепенно повышаем планку
# для всего проекта
Стратегия 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 .
Лучшие практики для больших проектов
📋 Рекомендации:
- Всегда указывайте
target-version— это предотвращает предложения несовместимого синтаксиса. - Используйте
extend-excludeвместоexclude— сохраняет дефолтные исключения Ruff. - Используйте
src = ["src"]— для src layout или монорепозитория. - Настройте
per-file-ignores— для тестов, миграций, скриптов. - Внедряйте Ruff постепенно — начните с F, добавляйте правила шаг за шагом.
- Используйте
--add-noqaдля baseline — но не злоупотребляйте, постепенно исправляйте noqa. - Для монорепозитория — локальные ruff.toml — каждый пакет может иметь свои настройки.
- Проверяйте
ruff check --show-files .— убедитесь, что exclude работает как ожидается.
📌 Что важно запомнить:
extend-excludeдобавляет к дефолтам,excludeпереопределяет ихtarget-versionопределяет минимальную версию Pythonsrcуказывает корни 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 вопросов