Source code for next.pages.registry

"""Per-`page.py` context-callable registry and layout watch helpers.

`PageContextRegistry` stores the list of context functions bound to
each `page.py` path, and merges their return values (with keyed and
dict-merge semantics) at render time. The watch helpers list
`template.djx` and `layout.djx` files under page roots for the
autoreloader and for the static finder.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, NamedTuple

from django.http import HttpRequest

from next.deps import DependencyResolver, get_request_dep_cache, resolver

from .context import ContextResult
from .signals import context_registered
from .watch import get_pages_directories_for_watch


if TYPE_CHECKING:
    from collections.abc import Callable
    from pathlib import Path

    from next.static.serializers import JsContextSerializer


[docs] class PageContextEntry(NamedTuple): """One context callable registered for a `page.py` file. The optional `serializer` overrides the global JS context serializer for the value this callable produces, but only when `serialize` is true. Backed by `NamedTuple` so the hot `register_context` path allocates a plain tuple rather than a frozen dataclass instance. """ func: Callable[..., Any] inherit_context: bool serialize: bool serializer: JsContextSerializer | None = None
logger = logging.getLogger(__name__) _MAX_ANCESTOR_WALK_DEPTH = 64
[docs] def get_layout_djx_paths_for_watch() -> set[Path]: """Return every `layout.djx` path under page trees.""" result: set[Path] = set() for pages_path in get_pages_directories_for_watch(): try: for path in pages_path.rglob("layout.djx"): result.add(path.resolve()) except OSError as e: logger.debug("Cannot rglob layout.djx under %s: %s", pages_path, e) return result
[docs] def get_template_djx_paths_for_watch() -> set[Path]: """Return every `template.djx` path under page trees.""" result: set[Path] = set() for pages_path in get_pages_directories_for_watch(): try: for path in pages_path.rglob("template.djx"): result.add(path.resolve()) except OSError as e: logger.debug("Cannot rglob template.djx under %s: %s", pages_path, e) return result
[docs] class PageContextRegistry: """Register per-`page.py` context callables and merge their output."""
[docs] def __init__( self, resolver: DependencyResolver | None = None, ) -> None: """Initialise with an optional resolver and an empty registry.""" self._context_registry: dict[ Path, dict[str | None, PageContextEntry], ] = {} self._resolver = resolver
def _get_resolver(self) -> DependencyResolver: """Return the injected resolver or the shared singleton.""" if self._resolver is not None: return self._resolver return resolver
[docs] def register_context( # noqa: PLR0913 self, file_path: Path, key: str | None, func: Callable[..., Any], *, inherit_context: bool = False, serialize: bool = False, serializer: JsContextSerializer | None = None, ) -> None: """Bind `func` to `file_path` with keyed or dict-merge semantics.""" self._context_registry.setdefault(file_path, {})[key] = PageContextEntry( func=func, inherit_context=inherit_context, serialize=serialize, serializer=serializer, ) context_registered.send( sender=PageContextRegistry, file_path=file_path, key=key )
[docs] def collect_context( self, file_path: Path, *args: object, **kwargs: object, ) -> ContextResult: """Merge inherited ancestor page.py context with this file's context callables. Inherited context comes from ``@context(..., inherit_context=True)`` callables in ancestor ``page.py`` files, not from layout files. The returned `ContextResult` separates the full template context from the JavaScript-serializable subset. The js_context uses first-registration semantics so that page-level values always take priority over inherited ones. """ request = args[0] if args and isinstance(args[0], HttpRequest) else None context_data: dict[str, Any] = {} js_context: dict[str, Any] = {} js_context_serializers: dict[str, JsContextSerializer] = {} # Reuse the dispatch dep_cache during a validation-failure re-render # so Depends("name") providers resolved by the form action are not # recomputed when the same name is referenced from page or component # context callables. shared = get_request_dep_cache(request) dep_cache: dict[str, Any] = shared if shared is not None else {} dep_stack: list[str] = [] inherited_context = self._collect_inherited_context( file_path, request, kwargs, dep_cache, dep_stack ) context_data.update(inherited_context) registry = self._context_registry.get(file_path, {}) ordered = sorted( registry.items(), key=lambda item: (item[0] is not None, str(item[0] or "")), ) for key, entry in ordered: resolved = self._get_resolver().resolve_dependencies( entry.func, request=request, _cache=dep_cache, _stack=dep_stack, _context_data=context_data, **kwargs, ) result = entry.func(**resolved) if key is None: context_data.update(result) if entry.serialize: for k, v in result.items(): if k not in js_context: js_context[k] = v if entry.serializer is not None: js_context_serializers[k] = entry.serializer else: context_data[key] = result if entry.serialize and key not in js_context: js_context[key] = result if entry.serializer is not None: js_context_serializers[key] = entry.serializer return ContextResult( context_data=context_data, js_context=js_context, js_context_serializers=js_context_serializers, )
def _collect_inherited_context( self, file_path: Path, request: HttpRequest | None, url_kwargs: dict[str, object], dep_cache: dict[str, Any], dep_stack: list[str], ) -> dict[str, Any]: """Return values from ancestor `page.py` callables marked `inherit_context`. Walks ancestor directories that contain a `page.py` and runs every `@context(..., inherit_context=True)` callable registered there. A sibling `layout.djx` is not required — the shared HTML envelope can live one level up under ``DEFAULT_PAGE_BACKENDS["DIRS"]``, and pages declaring inheritable context should still surface it on descendant routes. """ inherited_context = {} current_dir = file_path.parent for _ in range(_MAX_ANCESTOR_WALK_DEPTH): if current_dir == current_dir.parent: break page_file = current_dir / "page.py" if page_file.exists(): for key, entry in self._context_registry.get( page_file, {}, ).items(): if entry.inherit_context: resolved = self._get_resolver().resolve_dependencies( entry.func, request=request, _cache=dep_cache, _stack=dep_stack, **url_kwargs, ) if key is None: inherited_context.update(entry.func(**resolved)) else: inherited_context[key] = entry.func(**resolved) current_dir = current_dir.parent return inherited_context