Source code for next.deps.resolver

"""Walk a callable's signature and fill its parameters from the context.

`DependencyResolver` is the orchestrator consumed by page views, form
actions, and component renderers. It instantiates every subclass
registered under `RegisteredParameterProvider._registry` on first use.
Providers register by simply importing their module, which runs the
`__init_subclass__` hook on `RegisteredParameterProvider`.
"""

from __future__ import annotations

import inspect
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast

from .cache import _CACHE_MISS, _IN_PROGRESS, DependencyCache, DependencyCycleError
from .context import RESERVED_KEYS, ResolutionContext
from .providers import ParameterProvider, RegisteredParameterProvider


if TYPE_CHECKING:
    from collections.abc import Callable

    from django.http import HttpRequest


T = TypeVar("T")


[docs] class DependencyResolver: """Build keyword-arguments for a callable by consulting registered providers.""" EXPLICIT_RESOLVE_KEYS: ClassVar[frozenset[str]] = RESERVED_KEYS
[docs] def __get__(self, obj: object, owner: type[object]) -> DependencyResolver: """Return the resolver itself when accessed as a descriptor.""" return self
[docs] def __init__( self, *providers: ParameterProvider | RegisteredParameterProvider ) -> None: """Initialise with explicit providers or defer to the auto-registry.""" self._dependency_callables: dict[str, Callable[..., Any]] = {} self._providers: list[ParameterProvider] = list(providers) self._providers_loaded = bool(providers) self._resolve_call_stack: list[Callable[..., Any]] = []
def _get_providers(self) -> list[ParameterProvider]: """Instantiate registered subclasses in ascending `priority` order.""" result: list[ParameterProvider] = [] ordered = sorted( RegisteredParameterProvider._registry, key=lambda cls: cls.priority ) for cls in ordered: sig = inspect.signature(cls) if "resolver" in sig.parameters: result.append(cast("Any", cls)(resolver=self)) else: result.append(cls()) return result def _ensure_providers(self) -> None: """Populate `_providers` from the auto-registry on first access.""" if not self._providers_loaded: self._providers.extend(self._get_providers()) self._providers_loaded = True def _should_skip_parameter(self, param: inspect.Parameter) -> bool: """Return True for `self` / `cls` and variadic parameters.""" return param.name in ("self", "cls") or param.kind in ( inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD, ) def _resolve_parameter( self, param: inspect.Parameter, context: ResolutionContext ) -> object: """Return the first provider result, otherwise the default or None.""" for provider in self._providers: if provider.can_handle(param, context): return provider.resolve(param, context) return None if param.default is inspect.Parameter.empty else param.default
[docs] def register_dependency( self, name: str, callable_dep: Callable[..., Any] ) -> Callable[..., Any]: """Register a callable as a dependency reachable through `Depends("name")`.""" self._dependency_callables[name] = callable_dep return callable_dep
[docs] def dependency( self, name: str ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Return a decorator that registers the callable under the given name.""" def decorator(func: Callable[..., Any]) -> Callable[..., Any]: self.register_dependency(name, func) return func return decorator
def _resolve_callable_dependency( self, name: str, context: ResolutionContext ) -> object: """Resolve a named dependency with cycle-safe memoisation.""" if name not in self._dependency_callables: return None callable_dep = self._dependency_callables[name] if name in context.stack: cycle_start = context.stack.index(name) cycle = [*context.stack[cycle_start:], name] raise DependencyCycleError(cycle) cached = context.cache.get(name) if cached is _IN_PROGRESS: raise DependencyCycleError([*context.stack, name]) if cached is not _CACHE_MISS: return cached context.stack.append(name) context.cache.mark_in_progress(name) try: resolved = self.resolve(callable_dep, context) value = callable_dep(**resolved) context.cache.set(name, value) return value finally: if context.stack and context.stack[-1] == name: context.stack.pop() context.cache.unmark_in_progress(name)
[docs] def add_provider(self, provider: ParameterProvider) -> None: """Append a provider after the existing list.""" self._providers.append(provider)
[docs] def register( self, provider: ParameterProvider | type[ParameterProvider], ) -> ParameterProvider | type[ParameterProvider]: """Register a provider, accepting either a class or an instance.""" if isinstance(provider, type): instance = provider() self.add_provider(instance) return provider self.add_provider(provider) return provider
[docs] def resolve( self, func: Callable[..., T], context: ResolutionContext ) -> dict[str, Any]: """Return keyword arguments ready to call `func` with the given context.""" self._ensure_providers() self._resolve_call_stack.append(func) try: try: sig = inspect.signature(func) except (ValueError, TypeError): return {} result: dict[str, Any] = {} for name, param in sig.parameters.items(): if self._should_skip_parameter(param): continue value = self._resolve_parameter(param, context) result[name] = value return result finally: self._resolve_call_stack.pop()
[docs] def resolve_dependencies( self, func: Callable[..., Any], **context: object ) -> dict[str, Any]: """Resolve `func` from a loose kwargs mapping and build a context object.""" self._ensure_providers() reserved = self.EXPLICIT_RESOLVE_KEYS url_kwargs = {k: v for k, v in context.items() if k not in reserved} cache_obj = context.get("_cache") if isinstance(cache_obj, dict): cache = DependencyCache(backing_dict=cache_obj) elif isinstance(cache_obj, DependencyCache): cache = cache_obj else: cache = DependencyCache() resolution_context = ResolutionContext( request=cast("Any", context.get("request")), form=context.get("form"), url_kwargs=url_kwargs, context_data=cast("Any", context.get("_context_data") or {}), cache=cache, stack=cast("list[str]", context.get("_stack") or []), ) return self.resolve(func, resolution_context)
[docs] def resolve_with_template_context( self, func: Callable[..., Any], *, request: HttpRequest | None = None, template_context: dict[str, Any] | None = None, _cache: dict[str, Any] | DependencyCache | None = None, _stack: list[str] | None = None, ) -> dict[str, Any]: """Resolve `func` for component callables using template context. Keys from `EXPLICIT_RESOLVE_KEYS` are stripped from the context data so that name-based providers cannot shadow dedicated providers such as `HttpRequestProvider` on a parameter literally named `request`. """ tc: dict[str, Any] = dict(template_context or {}) injectable = { k: v for k, v in tc.items() if k not in self.EXPLICIT_RESOLVE_KEYS } if isinstance(_cache, dict): cache = DependencyCache(backing_dict=_cache) elif isinstance(_cache, DependencyCache): cache = _cache else: cache = DependencyCache() context = ResolutionContext( request=request, form=tc.get("form"), url_kwargs={}, context_data=injectable, cache=cache, stack=_stack or [], ) return self.resolve(func, context)
resolver: DependencyResolver = DependencyResolver() RegisteredParameterProvider.resolver = resolver