Source code for next.partial.render

"""Standalone zone rendering over the full page context."""

import time
from dataclasses import dataclass
from typing import TYPE_CHECKING

from django.template import Context as DjangoTemplateContext

from next.pages.manager import page
from next.static.collector import default_placeholders
from next.static.manager import default_manager

from .registry import zones_of
from .signals import zone_rendered
from .zone import render_zone_body


if TYPE_CHECKING:
    from collections.abc import Iterator, Mapping
    from pathlib import Path

    from django.http import HttpRequest

    from next.static import StaticCollector

    from .registry import ZoneInfo


[docs] class UnknownZoneError(LookupError): """Raised when a partial request names a zone the page does not declare. The unified view turns this into a 400 before any zone renders, so a typo or a stale client never trips a partial render. The message names the declared zones so a builder-path typo points at what is available. """
[docs] def __init__(self, zone_name: str, declared: tuple[str, ...] = ()) -> None: """Store the unknown zone name and the declared zone names available.""" self.zone_name = zone_name self.declared = declared if declared: names = ", ".join(repr(name) for name in declared) message = f'Unknown zone "{zone_name}". Declared zones: {names}.' else: message = f'Unknown zone "{zone_name}".' super().__init__(message)
[docs] @dataclass(frozen=True, slots=True) class ZoneRenderResult: """Rendered zones plus the assets their bodies collected. `html` maps each rendered zone name to its wrapped marker element and `bodies` maps it to the bare inner body. A morph or replace addresses the wrapped element, an append or prepend grafts the bare body into the live zone. `collector` carries the co-located assets the bodies registered so the caller can ship a manifest outward, past the no-op inject. """ html: dict[str, str] bodies: dict[str, str] collector: "StaticCollector"
[docs] def url_assets(self) -> "Iterator[tuple[str, str]]": """Yield the (kind, url) pair of each collected asset that has a URL.""" for slot in default_placeholders: for static_asset in self.collector.assets_in_slot(slot.name): if static_asset.url: yield static_asset.kind, static_asset.url
[docs] def inline_assets(self) -> "Iterator[tuple[str, str]]": """Yield the (kind, inline body) of each collected inline asset.""" for slot in default_placeholders: for static_asset in self.collector.assets_in_slot(slot.name): if static_asset.inline is not None: yield static_asset.kind, static_asset.inline
[docs] def js_context_delta(self) -> dict[str, object]: """Return the zone js-context as wire-ready values for a context patch.""" return self.collector.js_context_wire()
[docs] def render_zone( page_path: "Path", zone_names: tuple[str, ...], request: "HttpRequest", url_kwargs: dict[str, object] | None = None, overrides: dict[str, object] | None = None, *, context_data: dict[str, object] | None = None, ) -> ZoneRenderResult: """Render the named zones of a page with the full page context. The context is collected once for the whole batch of names through `build_render_context` and a fresh collector is seeded by the same convention as the canonical render path, so co-located assets of the zone bodies are gathered. A caller that already built the origin context passes it as `context_data` so it is reused rather than rebuilt. The manifest travels outward in the result rather than through inject, which is a no-op for fragments. Unknown zone names are skipped so one stale name never poisons a batch, but a batch of only unknown names raises so a single-zone request keeps its 400. """ start = time.perf_counter() kwargs = url_kwargs or {} template = page.composed_template_for(page_path) zones = zones_of(template) rendered_names = _renderable_zone_names(zone_names, zones) if context_data is None: context_data = page.build_render_context(page_path, request, **kwargs) if overrides: context_data.update(overrides) collector = _seed_collector(page_path, context_data) django_context = DjangoTemplateContext(context_data) html: dict[str, str] = {} bodies: dict[str, str] = {} for name in rendered_names: info = zones[name] body, wrapped = render_zone_body( info.partial, info.name, info.options, django_context ) # SafeString is a str, kept as-is to avoid copies on the hot path. html[name] = wrapped bodies[name] = body _emit_rendered(page_path, rendered_names, request, start) return ZoneRenderResult(html=html, bodies=bodies, collector=collector)
def _renderable_zone_names( zone_names: tuple[str, ...], zones: "Mapping[str, ZoneInfo]" ) -> tuple[str, ...]: """Return the declared names of a batch, deduplicated in request order. A name the page does not declare is dropped so one stale name never poisons the batch. A non-empty batch left with no declared name raises, naming the first unknown, so a single-zone request keeps its 400. An empty batch stays the no-op it always was. """ rendered = tuple(name for name in dict.fromkeys(zone_names) if name in zones) if zone_names and not rendered: raise UnknownZoneError(zone_names[0], tuple(sorted(zones))) return rendered def _seed_collector( page_path: "Path", context_data: dict[str, object] ) -> "StaticCollector": """Seed a fresh collector and bind it to the context like the page path. The collector is hydrated with the JS context that `build_render_context` left behind, page asset discovery runs, and the collector is bound under `_static_collector` so component widgets and co-located assets of the zone bodies register against it. """ collector: StaticCollector = default_manager.create_collector() js_context = context_data.pop("_next_js_context", {}) js_serializers = context_data.pop("_next_js_context_serializers", {}) if isinstance(js_context, dict): serializers = js_serializers if isinstance(js_serializers, dict) else {} for js_key, js_value in js_context.items(): collector.add_js_context( js_key, js_value, serializer=serializers.get(js_key) ) default_manager.discover_page_assets(page_path, collector) context_data["_static_collector"] = collector return collector def _emit_rendered( page_path: "Path", zone_names: tuple[str, ...], request: "HttpRequest", start: float ) -> None: """Announce each rendered zone when the signal has receivers.""" if not zone_rendered.receivers: return duration_ms = (time.perf_counter() - start) * 1000 for name in zone_names: zone_rendered.send( sender=ZoneRenderResult, zone_name=name, page_path=page_path, request=request, duration_ms=duration_ms, ) __all__ = ["UnknownZoneError", "ZoneRenderResult", "render_zone"]