Перейти к содержанию

Внедрение зависимостей

Внедрение зависимостей (DI) — это способ получить в обработчике готовые объекты — пул базы данных, настройки, текущего пользователя — не создавая их в каждом файле. Вы добавляете параметр в обработчик, EndoCore подставляет значение. Работает как в FastAPI: Depends(...) плюс провайдеры уровня приложения — та же идея, маленькая реализация (endocore/core/di.py, без отдельного DI-фреймворка внутри).

from endocore import Request, Response, Depends

async def db():
    return get_pool()                     # зависимость (sync или async)

async def current_user(request: Request, pool = Depends(db)):
    token = request.headers.get("authorization")
    return await pool.user_from(token)

async def handler(request: Request, user = Depends(current_user)):
    return Response.json({"user": user})

Зависимости могут зависеть от других зависимостей (вложенность, на любую глубину) и кэшируются на запрос — зависимость, использованная дважды в дереве разрешения одного запроса, выполнится один раз. Ключ кэша — сам объект функции-зависимости: два разных маркера Depends(db), указывающих на одну и ту же функцию db, разделят один закэшированный результат; Depends(db) и Depends(other_db) — никогда, даже если other_db возвращает внешне идентичный объект.

Как обработчик вообще диспетчеризуется

Прежде чем всё это заработает, диспетчер проверяет, не является ли ваш обработчик самой простой возможной формой: ровно один параметр, с именем request, без значения по умолчанию. Если да — вся машинерия DI ниже пропускается целиком, и handler(request) вызывается напрямую: этот «быстрый путь тривиального обработчика» существует именно ради частого случая, а результат самой проверки кэшируется на функцию, так что при повторных вызовах ничего не стоит. Как только вы добавляете второй параметр, значение по умолчанию или Depends(...), обработчик идёт через полное разрешение.

Порядок разрешения — точно

Для каждого параметра обработчика, в этом точном порядке (это буквально поток управления di._resolve, а не пересказ — порядок важен, потому что к одному параметру технически может подойти больше одного правила):

  1. Значение по умолчанию параметра — Depends(fn) → разрешить fn (рекурсивно, по тем же правилам) и закэшировать результат. Это проверяется раньше всего — раньше, чем специальная обработка request/websocket ниже. На практике это никогда не пересекается (никто не пишет request = Depends(...)), но полезно знать, какое правило реально победит, если вы всё же сделаете что-то необычное.
  2. Параметр называется request или аннотирован Request → текущий Request (или None внутри обработчика WebSocket — см. ниже).
  3. Параметр называется websocket или аннотирован WebSocket → текущий WebSocket (или None внутри HTTP-обработчика).
  4. Имя совпадает с захваченным path-параметром ([id] → параметр, буквально названный id) → это строковое значение. Работает и для HTTP, и для WebSocket-обработчиков — path_params берётся из того из request/websocket, который активен.
  5. Подходит провайдер уровня приложения — сначала проверяется по аннотации (типу), затем по имени параметра, если совпадения по типу нет. См. ниже.
  6. Аннотация — подкласс pydantic BaseModel → валидируется из JSON-тела запроса. Только для HTTP — для WebSocket-обработчиков это правило целиком пропускается (тела запроса там просто нет), так что параметр с аннотацией BaseModel в обработчике Socket.py провалится в следующее правило.
  7. У параметра есть собственное обычное значение по умолчанию (не Depends(...)) → используется как есть.
  8. Ничего из вышеперечисленного не подошло → поднимается DIError с именем параметра и функции — на практике это ошибка, близкая к времени старта, но не совсем: обработчики импортируются жадно, а реально неразрешимый параметр обычно всплывает при первом запросе к этому маршруту, а не при импорте (DI работает на каждый запрос, а не при импорте) — поэтому покрывайте новые обработчики хотя бы одним тестовым запросом, не полагаясь только на endo check.

Провайдеры уровня приложения

Зарегистрируйте синглтоны/сервисы один раз; инжектируйте по имени или типу:

# providers.py
from Services.db import make_pool
from Services.settings import Settings, get_settings

providers = {
    "db": make_pool,          # инжект параметра с именем `db`
    Settings: get_settings,   # инжект параметра с аннотацией `Settings`
}
async def handler(request, db, settings: Settings):
    ...

Провайдеры — синглтоны по умолчанию: ключ кэша — сама функция-фабрика (app._singletons[factory]), она строится один раз при первом использовании и переиспользуется всё время жизни процесса — не на каждый запрос, как результаты Depends(...). Форма словаря в providers.py всегда синглтон (там нет опции для отдельной записи); для фабрики, которая должна пересоздаваться при каждом разрешении, регистрируйте её в рантайме — app.provide("db", make_pool, singleton=False), например, из hooks.py или из setup(app) расширения. Фабрика провайдера сама может принимать параметры, разрешаемые по тем же правилам DI (включая Depends(...) и другие провайдеры) — провайдеры и Depends используют один резолвер, это не две разные системы.

Если провайдер зарегистрирован и под именем, и под типом, которые оба могли бы подойти одному параметру (необычно, но возможно), побеждает тип — это правило 5 выше, и это свойство ProviderRegistry.get(), а не порядка объявления в providers.py.

Pydantic-тела

С установленным endocore[pydantic] параметр, аннотированный BaseModel, валидируется из JSON-тела — при ошибке 422 с ошибками по каждому полю, а схема появляется в /docs:

from pydantic import BaseModel

class UserIn(BaseModel):
    name: str
    age: int

async def handler(request, data: UserIn):    # POST-тело -> валидированный UserIn
    return Response.json({"name": data.name}, status=201)

Поддерживаются и pydantic v1 (.parse_obj), и v2 (.model_validate) — какая установлена, та автоматически и определяется, никакой настройки не нужно. Ошибка валидации поднимает UnprocessableEntity (422), где detail — список записей {"field": "age", "message": "..."}, по одной на каждое несостоявшееся поле — та же форма, что Исключения документируют для любой другой HTTP-ошибки, так что клиентскому обработчику ошибок не нужен отдельный случай специально для ошибок валидации тела.

Производительность

Сигнатуры и разрешённые type-хинты кэшируются на функцию (inspect.signature и typing.get_type_hints недёшевы при вызове на каждый запрос), и проверка «это тривиальный обработчик» выше — тоже кэшированный булев флаг. На практике DI добавляет ничтожные накладные расходы даже для обработчиков с несколькими вложенными зависимостями — то, что реально стоит внимания, — это что делают ваши функции-зависимости (запрос к БД, HTTP-вызов), а не механизм разрешения вокруг них.