From 4486fdee2ce6ca22935801de8205549cd2a5384f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 2 Jul 2026 11:25:16 +0300 Subject: [PATCH] chore: remove the modern-di agent skills Drop skills/modern-di/ (SKILL, common, fastapi, litestar, testing). The skill set was a partial, separately-maintained subset and is being retired; it was not wired into the docs build, nav, or packaging, so removal is self-contained. Co-Authored-By: Claude Opus 4.8 (1M context) --- skills/modern-di/SKILL.md | 62 -------------- skills/modern-di/common.md | 158 ----------------------------------- skills/modern-di/fastapi.md | 155 ---------------------------------- skills/modern-di/litestar.md | 128 ---------------------------- skills/modern-di/testing.md | 121 --------------------------- 5 files changed, 624 deletions(-) delete mode 100644 skills/modern-di/SKILL.md delete mode 100644 skills/modern-di/common.md delete mode 100644 skills/modern-di/fastapi.md delete mode 100644 skills/modern-di/litestar.md delete mode 100644 skills/modern-di/testing.md diff --git a/skills/modern-di/SKILL.md b/skills/modern-di/SKILL.md deleted file mode 100644 index f0e06ad..0000000 --- a/skills/modern-di/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: modern-di -description: > - Reference guide for the modern-di Python DI framework. Use this skill for ANY question about modern-di — - scope hierarchy (Scope.APP/REQUEST/SESSION/ACTION), Group/Factory/CacheSettings declaration, Container - resolution (resolve vs resolve_provider, build_child_container), ContextProvider for runtime values, - framework integration via ModernDIPlugin or setup_di/FromDI, testing with container.override/reset_override, - injecting Request objects into providers, and SQLAlchemy session wiring. Trigger on: modern-di, - providers.Factory, CacheSettings, Scope.REQUEST, Scope.APP, build_child_container, ModernDIPlugin, - setup_di, modern-di-litestar, modern-di-fastapi, container.override, resolve_provider. Don't skip this - skill for questions that seem simple — modern-di has specific idioms (no Singleton class, sync-only - resolution, hierarchical scopes) that differ from other DI frameworks. ---- - -# modern-di Skill - -modern-di is a **zero-dependency** Python DI framework. It wires object graphs from type annotations, -manages lifetimes through hierarchical scopes, and supports sync/async finalizers. - -## Navigation - -This skill is split across focused reference files. Read the one that matches the task: - -| File | When to read it | -|------|----------------| -| `common.md` | Core concepts: Group, Factory, Container, Scope, CacheSettings, ContextProvider | -| `litestar.md` | Litestar integration: ModernDIPlugin, FromDI, route handlers | -| `fastapi.md` | FastAPI integration: setup_di, FromDI, route handlers | -| `testing.md` | Testing: overrides, scope chains, pytest fixtures | - -Read `common.md` first if the user is new to modern-di. For framework-specific questions, go directly to -`litestar.md` or `fastapi.md`. For testing questions, read `testing.md` (and `common.md` if needed for -context). - -## Quick reference - -```python -from modern_di import Container, Group, Scope, providers - -class Dependencies(Group): - db_engine = providers.Factory( - scope=Scope.APP, - creator=create_async_engine, - cache_settings=providers.CacheSettings(finalizer=close_engine), - ) - session = providers.Factory( - scope=Scope.REQUEST, - creator=create_session, - cache_settings=providers.CacheSettings(finalizer=close_session), - ) - -container = Container(scope=Scope.APP, groups=[Dependencies]) -``` - -## Common errors - -| Error | Fix | -|---|---| -| `Provider of type X not registered` | Add `providers.Factory(creator=X)` to your Group | -| `Provider of scope REQUEST cannot be resolved in container of scope APP` | Build a child container first | -| `Provider is duplicated by type X` | Set `bound_type=None` on one of the conflicting providers | -| `Argument X of type Y cannot be resolved` | Register a provider for that type or pass via `kwargs` | diff --git a/skills/modern-di/common.md b/skills/modern-di/common.md deleted file mode 100644 index c36e74f..0000000 --- a/skills/modern-di/common.md +++ /dev/null @@ -1,158 +0,0 @@ -# modern-di — Core Concepts - -## Scope hierarchy - -```python -from modern_di import Scope -# IntEnum: APP=1, SESSION=2, REQUEST=3, ACTION=4, STEP=5 -``` - -A provider at scope N can only depend on providers at scopes 1..N. Higher number = more short-lived. -Typical split: `APP` for singletons, `REQUEST` for per-HTTP-request objects. - -WebSocket handlers use `SESSION` scope instead of `REQUEST`. - -## Group — provider namespace - -`Group` is a non-instantiable class. Declare providers as class attributes: - -```python -from modern_di import Group, Scope, providers - -class Dependencies(Group): - db_engine = providers.Factory( - scope=Scope.APP, - creator=create_async_engine, - cache_settings=providers.CacheSettings(finalizer=close_engine), - ) - session = providers.Factory( - scope=Scope.REQUEST, - creator=create_session, - cache_settings=providers.CacheSettings(finalizer=close_session), - ) - users_repo = providers.Factory( - scope=Scope.REQUEST, - creator=UsersRepository, - kwargs={"session": session}, # explicit reference; auto-wiring works when type is unambiguous - ) -``` - -## Factory — the main provider type - -```python -providers.Factory( - scope=Scope.APP, # default is Scope.APP - creator=MyClass, # any callable; return type inferred from annotation - bound_type=None, # override if auto-inference is wrong; set to None to exclude from registry - kwargs={"key": "value"}, # static overrides that bypass type-based resolution - cache_settings=providers.CacheSettings(...), # enables caching (singleton pattern) - skip_creator_parsing=False, # set True for lambdas or C-extension callables -) -``` - -**Auto-wiring**: at declaration time, `Factory` inspects the creator's type hints. At resolution, it -finds matching providers by type and resolves them recursively. Explicit `kwargs` override this. - -**Singleton pattern** — there is no separate `Singleton` class: -```python -providers.Factory( - creator=MyClass, - cache_settings=providers.CacheSettings( - clear_cache=False, # keep instance in cache after finalizer runs (default True) - finalizer=my_cleanup, # sync or async — auto-detected - ), -) -``` - -## Container - -```python -from modern_di import Container - -# Root container — create once at app startup -app_container = Container(scope=Scope.APP, groups=[Dependencies]) - -# Per-request child container — own cache, shares providers and overrides with parent -request_container = app_container.build_child_container(scope=Scope.REQUEST) - -# Resolve by type (any matching provider in registry) -instance = request_container.resolve(UsersRepository) - -# Resolve by provider reference — preferred, unambiguous -instance = request_container.resolve_provider(Dependencies.users_repo) - -# Cleanup — calls finalizers for all cached instances at this scope -await request_container.close_async() # or .close_sync() for sync apps -``` - -## ContextProvider — injecting runtime values - -For values that only exist at runtime (e.g. the HTTP request object): - -```python -class Dependencies(Group): - http_request = providers.ContextProvider(scope=Scope.REQUEST, context_type=Request) -``` - -Pass the value when building the child container: -```python -child = app_container.build_child_container( - scope=Scope.REQUEST, - context={Request: actual_request_object}, -) -``` - -Framework integrations (modern-di-litestar, modern-di-fastapi) do this automatically for -`litestar.Request` / `fastapi.Request`. - -## SQLAlchemy factory pattern - -This pattern is identical in both Litestar and FastAPI templates: - -```python -# app/resources/db.py -from sqlalchemy.ext import asyncio as sa - -def create_sa_engine() -> sa.AsyncEngine: - return sa.create_async_engine(url=DATABASE_URL, pool_size=10, pool_pre_ping=True) - -async def close_sa_engine(engine: sa.AsyncEngine) -> None: - await engine.dispose() - -def create_session(engine: sa.AsyncEngine) -> sa.AsyncSession: - return sa.AsyncSession(engine, expire_on_commit=False, autoflush=False) - -async def close_session(session: sa.AsyncSession) -> None: - await session.close() -``` - -```python -# app/ioc.py -from modern_di import Group, Scope, providers -from app.resources.db import close_sa_engine, close_session, create_sa_engine, create_session - -class Dependencies(Group): - database_engine = providers.Factory( - creator=create_sa_engine, - cache_settings=providers.CacheSettings(finalizer=close_sa_engine), - ) - session = providers.Factory( - scope=Scope.REQUEST, - creator=create_session, - cache_settings=providers.CacheSettings(finalizer=close_session), - ) - users_repository = providers.Factory( - scope=Scope.REQUEST, - creator=UsersRepository, - kwargs={"session": session}, - ) -``` - -## Key design rules - -- **No global state** — pass the Container explicitly (via `app.state` or framework plugin). -- **Resolution is synchronous** — `container.resolve()` is not awaitable. Async creators are supported; - the framework integration awaits them. -- **`skip_creator_parsing=True`** — use for lambdas or C-extension callables. Combine with explicit `kwargs`. -- **`container_provider`** — the Container itself is auto-registered, so any class with - `__init__(self, container: Container)` is auto-wired. diff --git a/skills/modern-di/fastapi.md b/skills/modern-di/fastapi.md deleted file mode 100644 index 23eeb0e..0000000 --- a/skills/modern-di/fastapi.md +++ /dev/null @@ -1,155 +0,0 @@ -# modern-di — FastAPI Integration - -Install: `pip install modern-di-fastapi` - -## Setup - -```python -import fastapi -import modern_di -import modern_di_fastapi - -from app import ioc - -def build_app() -> fastapi.FastAPI: - app = fastapi.FastAPI() - di_container = modern_di.Container(groups=[ioc.Dependencies]) - modern_di_fastapi.setup_di(app, di_container) - return app -``` - -`setup_di` handles everything: -- Stores the container in `app.state` -- Registers `fastapi.Request` as a `ContextProvider` at `Scope.REQUEST` -- Registers `fastapi.WebSocket` as a `ContextProvider` at `Scope.SESSION` -- Wraps the app lifespan to call `container.close_async()` on shutdown - -## Route handlers - -Unlike Litestar, FastAPI uses `FromDI` **inline** in the handler signature as a default value: - -```python -import typing -import fastapi -from modern_di_fastapi import FromDI - -from app import schemas -from app.repositories import DecksRepository - -ROUTER = fastapi.APIRouter(prefix="/api") - -@ROUTER.get("/decks/") -async def list_decks( - decks_repository: DecksRepository = FromDI(DecksRepository), -) -> schemas.Decks: - objects = await decks_repository.list() - return {"items": objects} - -@ROUTER.get("/decks/{deck_id}/") -async def get_deck( - deck_id: int, - decks_repository: DecksRepository = FromDI(DecksRepository), -) -> schemas.Deck: - instance = await decks_repository.get_one_or_none(...) - if not instance: - raise fastapi.HTTPException(status_code=404) - return schemas.Deck.model_validate(instance) - -@ROUTER.post("/decks/") -async def create_deck( - data: schemas.DeckCreate, - decks_repository: DecksRepository = FromDI(DecksRepository), -) -> schemas.Deck: - instance = await decks_repository.create(data) - return schemas.Deck.model_validate(instance) -``` - -You can also use `Annotated` style: -```python -async def list_decks( - decks_repository: typing.Annotated[DecksRepository, FromDI(DecksRepository)], -) -> schemas.Decks: - ... -``` - -## FromDI - -```python -modern_di_fastapi.FromDI(dependency, *, use_cache=True) -``` - -Accepts either: -- A **type** — resolved by looking it up in the providers registry -- A **provider reference** — resolved directly (`Dependencies.decks_repository`) - -Returns a `fastapi.Depends(...)` cast to the correct type. FastAPI handles calling it and injecting -the result. - -## Key difference from Litestar - -| | Litestar | FastAPI | -|---|---|---| -| Setup | `ModernDIPlugin(container)` in plugins | `setup_di(app, container)` call | -| Wiring dependencies | Centrally in `AppConfig.dependencies` | `FromDI(Type)` inline in each handler | -| Handler parameters | Plain type annotations — no markers | `= FromDI(...)` or `Annotated[T, FromDI(...)]` | - -## Accessing the Request inside a provider - -`setup_di` registers `fastapi.Request` automatically. Any creator that type-annotates a -`fastapi.Request` parameter gets it auto-injected: - -```python -def fetch_method(request: fastapi.Request) -> str: - return request.method - -class Dependencies(Group): - request_method = providers.Factory( - scope=Scope.REQUEST, - creator=fetch_method, - bound_type=None, # prevent registering str as a provider - ) - -@app.get("/") -async def endpoint( - method: str = FromDI(Dependencies.request_method), -) -> dict: - return {"method": method} -``` - -## WebSocket scoping - -WebSocket connections use `Scope.SESSION`. `setup_di` registers `fastapi.WebSocket` as a -`ContextProvider` at `Scope.SESSION` automatically. - -## ACTION scope (advanced) - -Inject the request container directly to build deeper scope chains: - -```python -from modern_di import Container -from modern_di_fastapi import build_di_container - -@app.get("/") -async def endpoint( - request_container: typing.Annotated[Container, fastapi.Depends(build_di_container)], -) -> dict: - action_container = request_container.build_child_container(scope=Scope.ACTION) - result = action_container.resolve_provider(Dependencies.action_factory) - await action_container.close_async() - return {"result": result} -``` - -## Full example (from fastapi-sqlalchemy-template) - -``` -app/ -├── ioc.py — Dependencies(Group) with engine, session, repositories -├── resources/ -│ └── db.py — create_sa_engine, close_sa_engine, create_session, close_session -├── repositories.py — DecksRepository, CardsRepository -├── api/ -│ └── decks.py — route handlers with FromDI() inline -└── application.py — build_app() with setup_di(app, container) -``` - -See `common.md` for the `ioc.py` and `db.py` content. diff --git a/skills/modern-di/litestar.md b/skills/modern-di/litestar.md deleted file mode 100644 index 2ee05a3..0000000 --- a/skills/modern-di/litestar.md +++ /dev/null @@ -1,128 +0,0 @@ -# modern-di — Litestar Integration - -Install: `pip install modern-di-litestar` - -## Setup - -### Option A — manual `FromDI` wiring - -```python -import modern_di -import modern_di_litestar -from litestar import Litestar - -from app import ioc, repositories - -def build_app() -> Litestar: - di_container = modern_di.Container(groups=[ioc.Dependencies]) - return Litestar( - plugins=[modern_di_litestar.ModernDIPlugin(di_container)], - dependencies={ - "decks_repository": modern_di_litestar.FromDI(repositories.DecksRepository), - "cards_repository": modern_di_litestar.FromDI(repositories.CardsRepository), - }, - route_handlers=[ROUTER], - ) -``` - -### Option B — `autowired_groups` (auto-wiring) - -Pass `autowired_groups` to register every provider in those groups as a Litestar dependency, keyed by -its attribute name. No per-route `FromDI` needed: - -```python -import modern_di -import modern_di_litestar -from litestar import Litestar - -from app import ioc - -GROUPS = [ioc.Dependencies] - -def build_app() -> Litestar: - di_container = modern_di.Container(groups=GROUPS) - return Litestar( - plugins=[modern_di_litestar.ModernDIPlugin(di_container, autowired_groups=GROUPS)], - route_handlers=[ROUTER], - ) -``` - -Route handlers can then declare providers by attribute name directly as parameters. -If the same name appears in multiple groups, a `UserWarning` is emitted and the later group wins. - ---- - -`ModernDIPlugin` handles: -- Registers `litestar.Request` and `litestar.WebSocket` as `ContextProvider`s (REQUEST and SESSION scope) -- Creates a per-request child container and closes it (calling finalizers) after each response -- Stores the container in `app.state.di_container` - -## Route handlers - -```python -import litestar -from app import models, schemas -from app.repositories import DecksRepository - -@litestar.get("/decks/") -async def list_decks(decks_repository: DecksRepository) -> schemas.Decks: - objects = await decks_repository.list() - return schemas.Decks(items=objects) - -@litestar.get("/decks/{deck_id:int}/") -async def get_deck(deck_id: int, decks_repository: DecksRepository) -> schemas.Deck: - instance = await decks_repository.get_one_or_none(models.Deck.id == deck_id) - if not instance: - raise litestar.exceptions.HTTPException(status_code=404) - return schemas.Deck.model_validate(instance) - -@litestar.post("/decks/") -async def create_deck(data: schemas.DeckCreate, decks_repository: DecksRepository) -> schemas.Deck: - instance = await decks_repository.create(data) - return schemas.Deck.model_validate(instance) -``` - -## FromDI - -```python -modern_di_litestar.FromDI(dependency) -``` - -Accepts either: -- A **type** — resolved by looking it up in the providers registry -- A **provider reference** — resolved directly (`Dependencies.users_repo`) - -Returns a Litestar `Provide` object with `use_cache=False` (always fresh per request). - -## Accessing the Request inside a provider - -`ModernDIPlugin` automatically registers `litestar.Request` as a context provider at `Scope.REQUEST`. -Any creator that type-annotates a `litestar.Request` parameter gets it auto-injected: - -```python -def build_audit_logger(request: litestar.Request) -> AuditLogger: - return AuditLogger(user_id=request.user.id) - -class Dependencies(Group): - audit_logger = providers.Factory(scope=Scope.REQUEST, creator=build_audit_logger) -``` - -## WebSocket scoping - -WebSocket connections use `Scope.SESSION` (not `Scope.REQUEST`). `ModernDIPlugin` registers -`litestar.WebSocket` as a `ContextProvider` at `Scope.SESSION` automatically. - -## Full example (from litestar-sqlalchemy-template) - -``` -app/ -├── ioc.py — Dependencies(Group) with engine, session, repositories -├── resources/ -│ └── db.py — create_sa_engine, close_sa_engine, create_session, close_session -├── repositories.py — DecksRepository, CardsRepository -├── api/ -│ └── decks.py — route handlers (plain parameters, no DI markers) -└── application.py — build_app() with ModernDIPlugin + FromDI in dependencies dict -``` - -See `common.md` for the `ioc.py` and `db.py` content. diff --git a/skills/modern-di/testing.md b/skills/modern-di/testing.md deleted file mode 100644 index e59e8e4..0000000 --- a/skills/modern-di/testing.md +++ /dev/null @@ -1,121 +0,0 @@ -# modern-di — Testing - -## Override a provider with a mock - -```python -from unittest.mock import Mock -from modern_di import Container, Scope -from app.ioc import Dependencies -from app.repositories import UsersRepository -from app.services import UserService - -def test_user_service_with_mock_repo(): - container = Container(scope=Scope.APP, groups=[Dependencies]) - mock_repo = Mock(spec=UsersRepository) - - container.override(Dependencies.users_repository, mock_repo) - service = container.resolve(UserService) - - assert service.repo is mock_repo - - container.reset_override(Dependencies.users_repository) -``` - -**Important**: override before the first resolve, or a cached instance will be returned instead -of the mock. - -`reset_override()` with no arguments clears all overrides. - -## pytest fixture for clean override teardown - -```python -import pytest -from unittest.mock import Mock -from modern_di import Container, Scope -from app.ioc import Dependencies - -@pytest.fixture -def mock_repo(): - container = Container(scope=Scope.APP, groups=[Dependencies]) - mock = Mock(spec=UsersRepository) - container.override(Dependencies.users_repository, mock) - yield mock - container.reset_override(Dependencies.users_repository) -``` - -Or use the container as a context manager — overrides are reset on exit: - -```python -def test_with_context_manager(): - with Container(scope=Scope.APP, groups=[Dependencies]) as container: - container.override(Dependencies.users_repository, Mock(spec=UsersRepository)) - service = container.resolve(UserService) - # overrides reset automatically on __exit__ -``` - -## Test scope chains - -```python -async def test_request_scope_caching(): - app = Container(scope=Scope.APP, groups=[Dependencies]) - req = app.build_child_container(scope=Scope.REQUEST) - - # Same instance within one request container - repo1 = req.resolve_provider(Dependencies.users_repository) - repo2 = req.resolve_provider(Dependencies.users_repository) - assert repo1 is repo2 - - await req.close_async() # triggers finalizers (e.g. session.close()) - await app.close_async() -``` - -```python -async def test_different_requests_get_different_instances(): - app = Container(scope=Scope.APP, groups=[Dependencies]) - req1 = app.build_child_container(scope=Scope.REQUEST) - req2 = app.build_child_container(scope=Scope.REQUEST) - - repo1 = req1.resolve_provider(Dependencies.users_repository) - repo2 = req2.resolve_provider(Dependencies.users_repository) - assert repo1 is not repo2 - - await req1.close_async() - await req2.close_async() - await app.close_async() -``` - -## Inject context in tests (ContextProvider) - -```python -async def test_with_request_context(): - mock_request = Mock(spec=fastapi.Request) - app = Container(scope=Scope.APP, groups=[Dependencies]) - req = app.build_child_container( - scope=Scope.REQUEST, - context={fastapi.Request: mock_request}, - ) - logger = req.resolve_provider(Dependencies.audit_logger) - assert logger.request is mock_request - await req.close_async() -``` - -## pytest config - -modern-di tests use `asyncio_mode = "auto"` — async test functions work without extra markers: - -```toml -# pyproject.toml -[tool.pytest.ini_options] -asyncio_mode = "auto" -``` - -## Validate wiring without instantiating - -Check that all dependencies can be resolved without creating real objects: - -```python -def test_wiring(): - container = Container(scope=Scope.APP, groups=[Dependencies]) - container.validate() - # Raises if any provider has missing dependencies, missing alias sources, or circular deps -```