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 several call sites.
Page context functions, the page render function, component context functions, and the form dispatch all pass through it.
The form dispatch resolves the form-class factory, get_initial, @action handlers, on_valid, and wizard.done.
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 discovers a fixed list of providers and instantiates them on first use.
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.Cleaned data provider (priority 40). A parameter named
cleaned_datareceives the merged wizard cleaned data on a wizarddone()handler.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 by-name fallback after the Depends, Context, form, request, and DUrl providers.
It runs before the DQuery provider, so a DQuery parameter that shares a captured segment name receives the URL value, not the query value.
Note
This ordering is a publish-once, read-by-name escape hatch.
An ancestor that publishes a key under inherit_context=True lets a descendant page read it by bare parameter name in preference to a same-named URL kwarg.
# routes/shop/[category]/page.py
@context("category", inherit_context=True)
def category() -> str:
return "books"
# routes/shop/[category]/products/page.py
@context("listing")
def products(category: str) -> str:
return f"Listing for {category}."
The products callable receives the published category value, not the raw [category] segment.
See Share context across pages for the full pattern.
DUrl#
The URL path provider coerces the captured segment to the requested type.
from notes.models import Note
from next 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.
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 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.Contexttakes four forms, namely no argument that reads the parameter name, a string key, a callable, and a constant, with adefault=fallback when the key is absent.
Depends takes one of four forms, selected by its argument.
Depends("name").The argument is a string. The resolver looks up the callable registered under that name through
resolver.dependencyand invokes it with its own parameters resolved.Depends(callable).The argument is a callable. The resolver resolves the callable’s own parameters and calls it as a factory.
Depends(value).The argument is any other object. That object is injected directly as a constant.
Depends().No argument. The marker falls back to the parameter name and resolves it as the named form.
See Dependency resolver for the cycle and cache mechanics.
from next import Depends, context
from next.pages import 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 import Depends
from next.deps import 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 import Depends
from next.deps import 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 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 nine built-in providers occupy the range 10 (named dependency) through 80 (query string), so the default keeps a custom provider after every built-in.
FormProvider and CleanedDataProvider share priority 40.
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 60 for an annotation that should outrank DUrl.
Resolution cache#
Each resolution pass wraps a per-render dependency cache in a fresh DependencyCache.
The wrapper is new per call, but the backing store is shared across every @context callable in one page render, so a Depends("name") value resolved by one callable is reused by the next.
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.
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.
To share one value across several context functions in the same render, publish it through a named dependency such as Depends("active_tenant").
The cache memoises named dependencies but not raw provider calls.
See Share context across pages for a worked example.
The cache lives for one form dispatch.
Every stage of that POST, from get_initial through the validation-failure re-render, shares it.
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 django.http import HttpRequest
from next.deps import get_request_dep_cache
def render(request: HttpRequest) -> 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 evaluates annotations throughtyping.get_type_hintsfirst and falls back to the raw annotation, and theget_type_hintscall 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.
See Dependency resolver for the full lifecycle.
See also#
See also
Context for the @context decorator and inheritance flow.
File router for DUrl and captured URL parameters.
Testing for the override_dependency and override_provider test helpers.
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.