Почему src layout лучше flat, pip install -e ., pyproject.toml, правильные импорты без sys.path
Ваш проект растёт. Тесты начинают импортировать код странными путями: sys.path.append('../src'). В production код работает, локально — нет. Что не так?
Цель: Научиться правильно структурировать проекты с src layout.
Убедитесь, что понимаете:
# Базовые импорты Python
from mypackage import module
import mypackage.module
# Относительные импорты
from . import module
from .. import parent_moduleЕсли импорты Python непонятны — освежите основы Python.
my-project/
├── mypackage/
│ ├── __init__.py
│ ├── models.py
│ └── services.py
├── tests/
│ └── test_services.py
├── setup.py
└── README.md
Что не так?
# 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Проблемы:
# Запускаем тесты
pytest tests/
# ✅ Тесты проходят!НО:
# Устанавливаем пакет
pip install .
# Импортируем установленную версию
python -c "from mypackage import services"
# ❌ Получаем старый код! (кеш .pyc или неактуальная установка)Проблема: Тесты запускают код из рабочей директории, а production использует установленный пакет!
# tests/test_services.py
# ❌ Может импортировать tests/models.py вместо mypackage/models.py!
from models import UserПроблема: Python ищет модули начиная с текущей директории!
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
# ✅ Одинаковый код в обоих случаях!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
# 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 — зависимости для productiondev — зависимости для разработки (pytest, etc.)[tool.setuptools.packages.find] — где искать пакеты (src/)# Установка в editable (разработческом) режиме
pip install -e .
# Или с dev зависимостями
pip install -e ".[dev]"Что делает -e (editable):
src/mypackage/Проверка:
# Импортируем из любого места
python -c "from mypackage import __version__" # ✅ Работает!
# Запускаем тесты
pytest # ✅ Импорты работают!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]"
# Запустите тесты
pytestmy-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✅ Всё работает как настоящий пакет!
✅ src/mypackage/
❌ mypackage/
# ✅ СОВРЕМЕННО
# pyproject.toml
# ❌ УСТАРЕЛО
# setup.py# ✅ Разработка
pip install -e ".[dev]"
# ✅ Production
pip install .[project]
dependencies = [
"requests", # Production
]
[project.optional-dependencies]
dev = [
"pytest", # Только для dev
]# ❌ ПЛОХО
import sys
sys.path.append('../')
# ✅ ХОРОШО
# pip install -e .
from mypackage import moduleВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Продвинутые фикстуры: scope и autouse