injector Modules, @provider, singleton scope and ClassAssistedBuilder: working versions (tested 2026-10-18)
Wire a small app with injector Modules, a @provider method, singleton-scoped services and ClassAssistedBuilder assisted construction, then override bindings with a test module.
passed6 passed · 0 failed2026-10-18 03:56 UTC- Status
- passed
- Last passed
- 2026-10-18 03:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 39095370682
The code
Exactly the files that passed. Your agent can pull them with get_recipe("injector-di-modules") (install), or see them on GitHub.
injector==0.24.0 pytest==9.1.1
from __future__ import annotations
import sqlite3
from dataclasses import dataclass
from injector import (
Binder,
ClassAssistedBuilder,
Injector,
Module,
NoInject,
inject,
provider,
singleton,
)
@dataclass(frozen=True)
class Settings:
dsn: str = ":memory:"
prefix: str = "evt"
class Database:
def __init__(self, dsn: str) -> None:
self._conn = sqlite3.connect(dsn)
self._conn.execute(
"CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, kind TEXT)"
)
def add(self, kind: str) -> int:
cur = self._conn.execute("INSERT INTO events(kind) VALUES (?)", (kind,))
self._conn.commit()
return int(cur.lastrowid)
def count(self) -> int:
return int(self._conn.execute("SELECT COUNT(*) FROM events").fetchone()[0])
@singleton
class EventStore:
@inject
def __init__(self, db: Database) -> None:
self._db = db
def record(self, kind: str) -> int:
return self._db.add(kind)
def total(self) -> int:
return self._db.count()
class Sink:
def __init__(self, prefix: str) -> None:
self.prefix = prefix
self.lines: list[str] = []
def write(self, message: str) -> None:
self.lines.append(f"{self.prefix}: {message}")
class Report:
@inject
def __init__(self, store: EventStore, label: NoInject[str]) -> None:
self.store = store
self.label = label
def render(self) -> str:
return f"{self.label} ({self.store.total()})"
class AppModule(Module):
def configure(self, binder: Binder) -> None:
settings = Settings()
binder.bind(Settings, to=settings)
binder.bind(Database, to=Database(settings.dsn))
binder.bind(EventStore, to=EventStore)
@provider
@singleton
def provide_sink(self, settings: Settings) -> Sink:
return Sink(prefix=settings.prefix)
import injector
import pytest
from injector import Binder, ClassAssistedBuilder, Injector, Module, singleton
from app import AppModule, EventStore, Report, Sink
class InMemoryStore:
def __init__(self) -> None:
self.kinds: list[str] = []
def record(self, kind: str) -> int:
self.kinds.append(kind)
return len(self.kinds)
def total(self) -> int:
return len(self.kinds)
class TestModule(Module):
def configure(self, binder: Binder) -> None:
binder.bind(EventStore, to=InMemoryStore, scope=singleton)
def test_singleton_scope_is_per_injector():
inj = Injector([AppModule()])
store = inj.get(EventStore)
store.record("login")
assert inj.get(EventStore) is store
assert Injector([AppModule()]).get(EventStore) is not store
def test_provider_builds_singleton_sink_from_settings():
inj = Injector([AppModule()])
sink = inj.get(Sink)
sink.write("boot")
assert inj.get(Sink) is sink
assert sink.lines == ["evt: boot"]
def test_class_assisted_builder_injects_deps_and_takes_runtime_arg():
inj = Injector([AppModule()])
inj.get(EventStore).record("login")
report = inj.get(ClassAssistedBuilder[Report]).build(label="weekly")
assert report.render() == "weekly (1)"
def test_assisted_builder_requires_the_noinject_argument():
inj = Injector([AppModule()])
with pytest.raises(injector.CallError):
inj.get(ClassAssistedBuilder[Report]).build()
def test_override_binding_with_a_module():
inj = Injector([AppModule(), TestModule()])
store = inj.get(EventStore)
store.record("signup")
assert isinstance(store, InMemoryStore)
assert inj.get(EventStore) is store
def test_constructor_injection_needs_the_inject_decorator():
class NeedsStore:
def __init__(self, store: EventStore) -> None:
self.store = store
with pytest.raises(injector.CallError):
Injector([AppModule()]).get(NeedsStore)
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
BeautifulSoup 4 + soupsieve CSS selector scraping
Scrape fixture HTML with BeautifulSoup and soupsieve CSS selectors to pull table cells, links and data attributes, plus the html.parser tbody gotcha.
Capture loguru logs in pytest caplog via a propagate sink and structured extra
Forward loguru records into stdlib logging so pytest caplog sees them, keep bound context as structured extra, and test it offline without a real log backend.
FastAPI dependency_overrides tested offline with httpx ASGITransport
Override FastAPI dependencies (including nested ones) and drive the app in-process with httpx.ASGITransport, no network and no running server.
FastAPI metrics with prometheus_client scraped as text
Instrument FastAPI with prometheus_client using a per-app registry, label by route template to bound cardinality, and scrape the text exposition format offline through httpx.ASGITransport.