Перейти к содержимому
К программе курса
Pytest: Профессиональные инструменты
2 / 825%

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

20 минут

Ваш проект растёт. Тесты начинают импортировать код странными путями: sys.path.append('../src'). В production код работает, локально — нет. Что не так?

Цель: Научиться правильно структурировать проекты с src layout.

Вы точно готовы?

Убедитесь, что понимаете:

# Базовые импорты Python
from mypackage import module
import mypackage.module
 
# Относительные импорты
from . import module
from .. import parent_module

Если импорты Python непонятны — освежите основы Python.

Проблема: flat layout

Плохая структура: flat layout

my-project/
├── mypackage/
│   ├── __init__.py
│   ├── models.py
│   └── services.py
├── tests/
│   └── test_services.py
├── setup.py
└── README.md

Что не так?

Проблема 1: Импорты не работают

# tests/test_services.py
 
from mypackage.services import UserService  # ❌ ModuleNotFoundError!

Почему: Python не знает где искать mypackage!

Плохие решения:

# ❌ ПЛОХО #1: sys.path хак
import sys
sys.path.append('../')
from mypackage.services import UserService
 
# ❌ ПЛОХО #2: Относительные пути
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from mypackage.services import UserService
 
# ❌ ПЛОХО #3: PYTHONPATH
# export PYTHONPATH=/path/to/project
from mypackage.services import UserService

Проблемы:

  • ❌ Хрупкие пути
  • ❌ Не работает в CI
  • ❌ Разные пути на разных машинах

Проблема 2: Тестируете неправильный код

# Запускаем тесты
pytest tests/
 
# ✅ Тесты проходят!

НО:

# Устанавливаем пакет
pip install .
 
# Импортируем установленную версию
python -c "from mypackage import services"
 
# ❌ Получаем старый код! (кеш .pyc или неактуальная установка)

Проблема: Тесты запускают код из рабочей директории, а production использует установленный пакет!

Проблема 3: Случайные импорты

# tests/test_services.py
 
# ❌ Может импортировать tests/models.py вместо mypackage/models.py!
from models import User

Проблема: Python ищет модули начиная с текущей директории!

Решение: src layout

Правильная структура: src layout

my-project/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       ├── models.py
│       └── services.py
├── tests/
│   └── test_services.py
├── pyproject.toml
└── README.md

Ключевое отличие: Код в src/mypackage/, не в корне!

Почему это лучше?

1. Импорты всегда из установленного пакета

# Установка в editable режиме
pip install -e .
 
# Теперь импорты работают ВЕЗДЕ
pytest tests/
python -c "from mypackage import services"  # ✅ Работает!

2. Невозможно случайно импортировать неустановленный код

# tests/test_services.py
 
from mypackage.services import UserService  # ✅ Всегда из src/mypackage/

3. Одинаковое поведение локально и в production

# Локально
pip install -e .
pytest
 
# Production
pip install .
python app.py
 
# ✅ Одинаковый код в обоих случаях!

Создание src layout проекта (5 минут)

Шаг 1: Создайте структуру

mkdir my-project
cd my-project
 
# Создайте src директорию
mkdir -p src/mypackage
touch src/mypackage/__init__.py
 
# Создайте tests директорию
mkdir tests
touch tests/__init__.py
 
# Создайте pyproject.toml
touch pyproject.toml

Структура:

my-project/
├── src/
│   └── mypackage/
│       └── __init__.py
├── tests/
│   └── __init__.py
└── pyproject.toml

Шаг 2: pyproject.toml

# pyproject.toml
 
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
 
[project]
name = "mypackage"
version = "0.1.0"
description = "My awesome package"
authors = [
    {name = "Your Name", email = "you@example.com"}
]
requires-python = ">=3.8"
dependencies = [
    "requests>=2.28.0",
    "pydantic>=2.0.0",
]
 
[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "pytest-xdist>=3.0.0",
]
 
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
 
[tool.setuptools.packages.find]
where = ["src"]

Ключевые части:

  • [project] — метаданные пакета
  • dependencies — зависимости для production
  • dev — зависимости для разработки (pytest, etc.)
  • [tool.setuptools.packages.find]где искать пакеты (src/)

Шаг 3: pip install -e .

# Установка в editable (разработческом) режиме
pip install -e .
 
# Или с dev зависимостями
pip install -e ".[dev]"

Что делает -e (editable):

  • Создаёт symlink на src/mypackage/
  • Изменения в коде сразу доступны (не нужно переустанавливать)
  • Работает как установленный пакет

Проверка:

# Импортируем из любого места
python -c "from mypackage import __version__"  # ✅ Работает!
 
# Запускаем тесты
pytest  # ✅ Импорты работают!

Миграция с flat на src layout (3 минуты)

До (flat layout)

my-project/
├── mypackage/
│   ├── models.py
│   └── services.py
└── tests/
    └── test_services.py

Миграция

# Создайте src директорию
mkdir src
 
# Переместите пакет в src
mv mypackage src/
 
# Создайте pyproject.toml (см. выше)
 
# Установите в editable режиме
pip install -e ".[dev]"
 
# Запустите тесты
pytest

После (src layout)

my-project/
├── src/
│   └── mypackage/
│       ├── models.py
│       └── services.py
├── tests/
│   └── test_services.py
└── pyproject.toml

Импорты работают без хаков!

Практический пример

Создайте код

# src/mypackage/__init__.py
 
__version__ = "0.1.0"
 
# src/mypackage/calculator.py
 
def add(a, b):
    """Складывает два числа"""
    return a + b
 
def multiply(a, b):
    """Умножает два числа"""
    return a * b

Напишите тесты

# tests/test_calculator.py
 
from mypackage.calculator import add, multiply
 
def test_add():
    assert add(2, 3) == 5
 
def test_multiply():
    assert multiply(4, 5) == 20

Запустите

# Установка (если ещё не сделали)
pip install -e ".[dev]"
 
# Тесты
pytest
 
# ✅ Работает!

Импортируйте из любого места

# Python REPL
python
 
>>> from mypackage import __version__
>>> print(__version__)
0.1.0
 
>>> from mypackage.calculator import add
>>> add(10, 20)
30

Всё работает как настоящий пакет!

Best practices

1. Всегда используйте src layout

✅ src/mypackage/
❌ mypackage/

2. pyproject.toml вместо setup.py

# ✅ СОВРЕМЕННО
# pyproject.toml
 
# ❌ УСТАРЕЛО
# setup.py

3. Editable install для разработки

# ✅ Разработка
pip install -e ".[dev]"
 
# ✅ Production
pip install .

4. Разделяйте зависимости

[project]
dependencies = [
    "requests",  # Production
]
 
[project.optional-dependencies]
dev = [
    "pytest",    # Только для dev
]

5. Никогда не используйте sys.path

# ❌ ПЛОХО
import sys
sys.path.append('../')
 
# ✅ ХОРОШО
# pip install -e .
from mypackage import module

Что вы изучили

  • Проблемы flat layout — импорты, тестирование неправильного кода
  • src layout — правильная структура проектов
  • pyproject.toml — современная конфигурация
  • pip install -e . — editable install для разработки
  • Миграция — как перейти с flat на src
  • Best practices — src/, pyproject.toml, no sys.path

Следующий урок

Отлично! Теперь проект структурирован правильно. Но как оптимизировать фикстуры для максимальной производительности?

Переходите к уроку 2: Продвинутые фикстуры: scope и autouse

В следующем уроке вы узнаете:

  • scope="session" для медленных фикстур
  • scope="module" vs scope="function"
  • autouse=True для автоматических setup
  • Стратегии оптимизации фикстур

Устранение неисправностей

Забыли `pip install -e .`:

cd /path/to/project
pip install -e .

Перезапустите Python процесс или переустановите:

pip install -e . --force-reinstall

Используйте `pyproject.toml` (современный стандарт PEP 517/518):

# ✅ pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

`setup.py` устарел для простых проектов!

# Обычная установка (копирует файлы)
pip install .

# Editable установка (symlink, для разработки)

pip install -e .

Для разработки всегда используйте `-e`!

Проверьте `pyproject.toml`:

[tool.pytest.ini_options]
testpaths = ["tests"] # ✅ Правильно

# НЕ указывайте src/ в testpaths!

pytest должен искать тесты в `tests/`, а импортировать код из установленного пакета (`src/mypackage/`).

src layout: Правильная структура проектов — Pytest: Профессиональные инструменты — Potapov.me