Раскладываем код и тесты так, чтобы pytest находил всё без ModuleNotFoundError. Понимаем flat vs src layout, настраиваем editable install
⚠️ Урок устарел. Свежая версия в курсе pytest-basics: Базовая структура: где хранить тесты.
✅ Научиться организовывать код, когда модулей больше одного
✅ Запускать тесты без ModuleNotFoundError одной командой
✅ Настроить IDE один раз и забыть про проблемы с импортами
Пока был один файл — всё работало. Но в реальности появляется второй модуль.
bank-app/
src/
bank_account.py # основной класс
transaction_log.py # логирование операций ← новый модуль
email_sender.py # уведомления ← ещё один
tests/
test_bank_account.py
src/transaction_log.py:
class TransactionLog:
def __init__(self):
self.transactions = []
def log(self, message):
self.transactions.append(message)src/bank_account.py:
from .transaction_log import TransactionLog
class BankAccount:
def __init__(self):
self.balance = 0
self.transaction_log = TransactionLog()
def deposit(self, amount):
self.balance += amount
self.transaction_log.log(f"Deposited {amount}")Без понятной структуры модули не смогли бы импортировать друг друга.
Создайте базовый скелет:
bank-app/
src/
bank_account.py
tests/
test_bank_account.py
tests/test_bank_account.py:
from src.bank_account import BankAccount # импорт через src!
def test_deposit():
account = BankAccount()
account.deposit(100)
assert account.balance == 100Запуск ВСЕГДА из корня проекта:
# PYTHONPATH=. говорит Python: «ищи модули в текущей папке»
# Без этого: Python не видит src/ как пакет
# С этим: работает import from src.bank_account ...
PYTHONPATH=. pytest -vVS Code:
# В корне проекта создайте .env
echo "PYTHONPATH=." > .env
# Перезапустите VS Code — готовоPyCharm:
1) Правый клик на корне проекта
2) Mark Directory as → Sources Root
3) Готово — импорты работают
Добавьте в ~/.zshrc или ~/.bashrc:
alias pt='PYTHONPATH=. pytest -v'Теперь вместо длинной команды — просто pt.
❌ Неправильно: запускать pytest из tests/
✅ Правильно: из корня проекта (pwd показывает папку с src/ и tests/)
❌ Неправильно: from bank_account import BankAccount
✅ Правильно: from src.bank_account import BankAccount
Проблема flat layout: тесты могут случайно импортировать локальные модули вместо установленного пакета. При установке через pip install . пакет может вести себя иначе, чем локально.
Решение: Src layout изолирует исходный код в отдельной папке src/.
my_project/
my_package/
__init__.py
app.py
tests/
test_app.py
pyproject.toml
Плюсы: Просто, быстро начать Минусы: Тесты могут импортировать неустановленный код
my_project/
src/
my_package/
__init__.py
app.py
tests/
test_app.py
pyproject.toml
Плюсы:
Минусы: Требует pip install -e . для разработки
До (flat):
bank-app/
bank_account.py
tests/
test_bank_account.py
После (src):
bank-app/
src/
bank_app/
__init__.py
bank_account.py
tests/
test_bank_account.py
pyproject.toml
Тесты меняются:
# До
from bank_account import BankAccount
# После
from bank_app.bank_account import BankAccountПроблема: При src layout нужно устанавливать пакет после каждого изменения.
Решение: Editable install (pip install -e .) — один раз установили, код обновляется автоматически.
# pyproject.toml в корне проекта
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "bank-app"
version = "0.1.0"
dependencies = []
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
]# Editable install (один раз)
pip install -e .
# Установите dev-зависимости
pip install -e ".[dev]"
# Теперь pytest работает без PYTHONPATH
pytest -vЧто произошло:
| Подход | Команда запуска | Плюсы | Минусы |
|---|---|---|---|
| PYTHONPATH | PYTHONPATH=. pytest | Быстро начать | Нестабильно, не работает везде |
| Editable install | pytest (один раз настроил) | Профессионально, стабильно | Нужен pyproject.toml |
Задача: Создайте проект с src layout и editable install.
# 1. Создайте структуру
mkdir -p my_todo/src/todo my_todo/tests
cd my_todo
# 2. Создайте __init__.py
touch src/todo/__init__.py
# 3. Создайте код
cat > src/todo/app.py << 'EOF'
class TodoList:
def __init__(self):
self.tasks = []
def add(self, task):
self.tasks.append(task)
return len(self.tasks)
EOF
# 4. Создайте тест
cat > tests/test_app.py << 'EOF'
from todo.app import TodoList
def test_add_task():
todo = TodoList()
count = todo.add("Learn pytest")
assert count == 1
assert todo.tasks == ["Learn pytest"]
EOF
# 5. Создайте pyproject.toml
cat > pyproject.toml << 'EOF'
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "todo"
version = "0.1.0"
[project.optional-dependencies]
dev = ["pytest>=7.0"]
EOF
# 6. Установите в editable режиме
pip install -e ".[dev]"
# 7. Запустите тесты (без PYTHONPATH!)
pytest -vРезультат: Тесты проходят, импорты работают, код готов к production.
✅ Понимаю разницу между flat и src layout
✅ Создал pyproject.toml для своего проекта
✅ Установил пакет через pip install -e .
✅ Тесты запускаются без PYTHONPATH
✅ Импорты работают в терминале, IDE и pytest
❌ Неправильно: запускать pytest из tests/
✅ Правильно: из корня проекта (pwd показывает папку с src/ и tests/)
❌ Неправильно: PYTHONPATH=. pytest для каждого запуска
✅ Правильно: один раз pip install -e ., потом просто pytest
Вопрос 1: Обязательно ли использовать src layout для обучения? Ответ: Нет. Flat layout проще для начала. Но для реальных проектов лучше сразу src.
Вопрос 2: Что делать, если pip install -e . не работает?
Ответ: Проверьте:
pyproject.toml в корне проектаbuild-backend (setuptools)Вопрос 3: Нужно ли переустанавливать после изменения кода?
Ответ: Нет! Editable install (-e) автоматически видит изменения.
Далее: Тестируем классы и ошибки: pytest.raises