Dependency Injection¶
The dependency resolver inspects the signature of every callable that the framework invokes and fills each parameter from a registered provider. A page context function asks for a query string value, a layout asks for the current request, an action handler asks for the form and a URL parameter. The resolver answers all of those calls through the same pipeline.
Overview¶
The resolver runs at four call sites: page context functions, the page render function, component context functions, and @action form handlers.
Every call site shares one provider list and one set of markers.
Custom providers and tests can import resolver from next.deps and call resolver.resolve_dependencies.
Built In Providers¶
The framework registers a fixed list of providers at startup.
Each one carries an explicit priority value, the resolver consults them from lowest to highest, and the first match wins.
Named dependency provider (priority 10). A parameter with default
Depends(...)receives the resolved dependency.Context by default provider (priority 20). A parameter with a
Context(...)default receives the named context value.Context by name provider (priority 30). A parameter whose name matches a context key receives that context value.
Form provider (priority 40). A parameter named
formor annotatedDForm[FormClass]receives the bound form during action dispatch.HttpRequest provider (priority 50). A parameter annotated
HttpRequestorHttpRequest | Nonereceives the current request.URL annotation provider (priority 60). A parameter annotated
DUrl[T]reads the captured URL segment and coerces it toT.URL kwargs provider (priority 70). A parameter whose name matches a captured URL segment resolves to that value.
Query string provider (priority 80). A parameter annotated
DQuery[T]readsrequest.GETby parameter name and coerces toT.
The order makes the default-driven and marker-driven providers decisive.
Depends and Context look only at the parameter default.
DUrl and DQuery look only at the annotation.
The context-by-name provider sits ahead of the form, URL, and query providers because a context key under the same name is considered a deliberate publication.
The form provider matches the parameter name form, the marker DForm[FormClass], and any plain class annotation whose type the bound form is an instance of.
The URL-kwargs provider is the last fallback that matches on the bare parameter name, after every marker provider has had a chance to claim the parameter.
DUrl¶
The URL path provider coerces the captured segment to the requested type.
from notes.models import Note
from next.pages import context
from next.urls import DUrl
@context("note")
def note(note_id: DUrl[int]) -> Note:
return Note.objects.get(pk=note_id)
In the simplest form DUrl[T] matches the captured segment whose name
equals the parameter name, then coerces the captured value to T.
T may be str, int, bool, float, UUID, Decimal, date, or datetime.
A value that already satisfies T passes through untouched, so a Django converter that pre-coerced the segment, such as [uuid:id] producing a UUID, reaches the handler in that shape.
A failed parse falls back to the raw captured value rather than raising.
bool treats "1", "true", and "yes" as True and everything else as False.
date and datetime parse the ISO 8601 forms accepted by date.fromisoformat and datetime.fromisoformat.
For wildcard [[name]] segments the captured value is the matched path string. Annotate as DUrl[str] or leave it unannotated.
The marker has three forms.
DUrl[T].Reads the captured segment that shares the parameter name and coerces it to
T. Use it when the parameter name already matches the directory segment.DUrl["segment"].Reads the named captured segment and returns it in string form. Use it when the parameter name differs from the segment name and no type coercion is needed.
DUrl["segment", T].Reads the named captured segment and coerces it to
T. Use it when the parameter name differs from the segment name, for examplenote_id: DUrl["id", int]for an[id]directory.
The string in DUrl["segment"] and DUrl["segment", T] is the URL kwarg key the resolver looks up, not the Django converter label.
Hyphens in directory names are normalised to underscores in the kwarg, so a [my-id] directory is read as DUrl["my_id"].
Note
A DUrl[T] annotation is not the same thing as a Django URL converter label.
See Converter Segments below.
Converter Segments¶
[slug:name] and [uuid:name] in directory names are Django URL converter labels that control routing and validation.
See File Router for the routing detail.
They are not Python type annotations.
Django converts the captured value before the URL kwargs provider sees it.
Segment type |
Captured Python type |
Recommended annotation |
|---|---|---|
|
|
|
|
|
|
DQuery¶
The query provider reads from request.GET.
from notes.models import Note
from next.pages import context
from next.urls import DQuery
@context("results")
def results(query: DQuery[str] = "") -> list[Note]:
if not query:
return []
return list(Note.objects.filter(title__icontains=query))
The provider supports scalar types and lists.
Annotation |
Wire format |
Example URL |
|---|---|---|
|
single key |
|
|
single key with coercion |
|
|
single key, truthy values |
|
|
repeated keys |
|
|
bracket suffix |
|
|
comma format |
|
The provider returns the parameter default when the key is absent.
DQuery accepts the same scalar set as DUrl, namely str, int, bool, float, UUID, Decimal, date, and datetime, plus list[T] for any of those scalars.
A value that fails to parse falls back to the raw query string rather than raising.
Context Markers¶
Two markers fill parameters from distinct data sources.
Context reads from the per-render context dictionary.
Depends invokes a callable registered in the resolver’s process-wide dependency map.
Context("key").Returns the value of the named context key produced by an ancestor layout or by a context function earlier in the chain. See Context for the full set of
Contextshapes and how it relates to plain name matching.Depends("name").Returns the result of a callable registered through the
resolver.dependencydecorator.Dependsalso accepts a callable factory, a constant value, and a bareDepends()that falls back to the parameter name. See Dependency Resolver for the four forms.
from next.deps import Depends
from next.pages import Context, context
@context("ready_message")
def ready_message(
theme: dict | None = Depends("layout_theme"),
user_name: str = Context("user_name"),
) -> str:
return f"Hello {user_name}, theme is {theme}."
Registering Named Dependencies¶
Use resolver.dependency to register a callable that any handler can ask for through Depends("name").
from next.deps import resolver
@resolver.dependency("layout_theme")
def layout_theme() -> dict:
return {"name": "Notes", "version": "1.0"}
Import the module that defines the dependency from AppConfig.ready so the decorator runs before the first request.
The registered callable can take any provider-resolved parameters because it is itself dispatched through the resolver.
Diagnosing a Dependency Cycle¶
A named dependency may itself ask for other named dependencies through Depends.
When two of them ask for each other, directly or through a longer chain, resolution cannot terminate.
from next.deps import Depends, resolver
@resolver.dependency("profile")
def profile(settings: dict = Depends("settings")) -> dict:
return {"theme": settings["theme"]}
@resolver.dependency("settings")
def settings(profile: dict = Depends("profile")) -> dict:
return {"theme": profile.get("theme", "light")}
The resolver records each name on a stack as it enters the dependency and marks the cache entry in progress.
Re-entering a name that is already on the stack raises DependencyCycleError.
next.deps.cache.DependencyCycleError: Circular dependency: profile -> settings -> profile
The chain in the message is the resolution path that closed the loop, read left to right.
The traceback lists the fully qualified path next.deps.cache.DependencyCycleError because that is where the exception class is defined.
Import the exception from the public next.deps namespace with from next.deps import DependencyCycleError.
The deeper next.deps.cache path is an implementation detail and is not part of the supported import surface.
Break the cycle by removing one Depends edge.
Here settings does not need profile at all, so the fix is to drop that parameter.
from next.deps import Depends, resolver
@resolver.dependency("settings")
def settings() -> dict:
return {"theme": "light"}
@resolver.dependency("profile")
def profile(settings: dict = Depends("settings")) -> dict:
return {"theme": settings["theme"]}
When both dependencies genuinely need shared data, move that data into a third dependency and have both depend on it.
Writing a Custom Provider¶
For data sources that do not fit the built ins, register a parameter provider.
The base classes are RegisteredParameterProvider and DDependencyBase.
from typing import get_args, get_origin
from django.http import Http404
from notes.models import Note
from next.deps import DDependencyBase, RegisteredParameterProvider
class DNote[T](DDependencyBase[T]):
__slots__ = ()
class NoteProvider(RegisteredParameterProvider):
def can_handle(self, param, _context) -> bool:
return get_origin(param.annotation) is DNote
def resolve(self, param, context):
(model_cls,) = get_args(param.annotation)
pk = context.url_kwargs.get("id")
if pk is None and context.request is not None:
pk = context.request.POST.get("note_id")
try:
return model_cls.objects.get(pk=pk)
except model_cls.DoesNotExist as exc:
raise Http404 from exc
One marker can serve both a page render and a form action handler.
A page render captures the identifier in the URL, while a form action carries
it in the POST body. The resolve method above checks both sources, so the
same DNote[Note] parameter works in either call site.
The form template carries the identifier in a hidden input so the POST branch can read it.
See examples/kanban for a marker that serves both call sites.
Use the new marker.
from notes.models import Note
from notes.providers import DNote
from next.pages import context
@context("note")
def current_note(note: DNote[Note]) -> Note:
return note
Two rules apply to the marker class.
- Python 3.12 generic syntax.
class DNote[T](DDependencyBase[T])makesDNote[Note]a parameterised generic whoseget_originisDNote. A non genericclass DNote(DDependencyBase[Note])does not behave like a parameter marker.- Import before resolution.
Register the provider before the resolver caches its provider list. The natural place is
AppConfig.readyof the application that owns the provider.
A custom provider that does not declare priority inherits the RegisteredParameterProvider default of 100.
The built-in providers occupy the range 10 (named dependency) through 80 (query string), so the default keeps a custom provider after every built-in.
Set priority on the subclass when the new provider has to claim a parameter the built-ins would otherwise match, for example a value below 50 for an annotation that should outrank DUrl.
Resolution Cache¶
The resolver creates a fresh DependencyCache for each resolution pass.
The cache memoises Depends("name") callables only, keyed by the registered name.
Parameter providers run once per parameter per resolution pass and their results are not stored, so a second context function that asks for the same DQuery or DUrl parameter triggers another provider call.
A second context function in the same page render that asks for the same Depends("name") dependency receives the memoised value, not a fresh call.
The same cache is shared between the initial render of a form page and the re-render on validation failure.
FormActionDispatch attaches its dependency cache to the request, and get_request_dep_cache reads it back.
The function returns None outside a form dispatch, so callers handle the missing case.
from next.deps import get_request_dep_cache
def render(request) -> str:
cache = get_request_dep_cache(request)
if cache is None:
return "No form dispatch cache on this request."
return f"Cache has {len(cache)} entries."
The constant REQUEST_DEP_CACHE_ATTR names the request attribute that holds the cache.
More recipes for diagnosing missing markers and CSRF or dispatch errors live in Troubleshooting.
Avoid from __future__ import annotations in DI Modules¶
The resolver inspects real annotations, not strings.
A from __future__ import annotations import in a page.py or component.py turns every annotation into a string and typing.get_origin returns None.
Two rules.
- Do not use future annotations in modules with DI parameters.
page.py,component.py, andproviders.pyneed real annotations. Plain Python files that only import the framework can use future annotations freely.- Keep DI types runtime importable.
Most providers compare annotations through
typing.get_origin, which returnsNonefor string annotations and never imports the target type. TheHttpRequestprovider does calltyping.get_type_hintson the wrapped callable as a fallback, and that call evaluates string annotations. Types hidden behindif TYPE_CHECKINGare not visible to either path, so keep DI-touching annotations on classes that import at module top level.
Resolver Lifecycle¶
The resolver builds its provider registry on first use and reuses it, so register custom providers from AppConfig.ready and see Dependency Resolver for the full lifecycle.
A custom provider that wants to share one value across several context functions in the same render should publish it through Depends("name"), because the per-resolution cache memoises named dependencies but not raw provider calls.
Register a named callable through resolver.dependency("active_tenant") and have each @context function ask for Depends("active_tenant").
See Share Context Across Pages for a worked example.
See Also¶
See also
Context for the @context decorator and inheritance flow.
File Router for DUrl and captured URL parameters.
Troubleshooting for concrete resolver and dispatch errors.
Share Context Across Pages for the inherited context pattern.
Dependency Resolver for the resolver internals.
Dependency Injection Reference for the public API and cache contract.