Coverage for src / lexigram / ui / core / context.py: 100%
21 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""Per-request UI context.
3Stores request-scoped rendering state (theme, locale, current user) using a
4``contextvars.ContextVar`` so that async handlers running concurrently each
5see their own isolated copy.
7Usage — middleware::
9 from lexigram.ui.core.context import UIContext, set_ui_context
11 class UIContextMiddleware:
12 def __init__(self, app):
13 self._app = app
15 async def __call__(self, scope, receive, send):
16 ctx = UIContext(
17 theme=scope.get("state", {}).get("theme", "default"),
18 locale=scope.get("state", {}).get("locale", "en"),
19 )
20 token = set_ui_context(ctx)
21 try:
22 await self._app(scope, receive, send)
23 finally:
24 reset_ui_context(token)
26Usage — component::
28 from lexigram.ui.core.context import get_ui_context
30 class ThemeAwareBadge(Component):
31 def render(self):
32 ctx = get_ui_context()
33 theme_class = f"badge-{ctx.theme}" if ctx else "badge-default"
34 return el("span", {"class": theme_class}, *self.children)
35"""
37from __future__ import annotations
39import contextvars
40import dataclasses
41from typing import Any
44@dataclasses.dataclass(frozen=True)
45class UIContext:
46 """Immutable per-request UI rendering context.
48 Attributes:
49 theme: Active theme name (e.g. ``"default"``, ``"dark"``).
50 locale: BCP-47 locale string (e.g. ``"en"``, ``"fr-FR"``).
51 user: Optional current user object; application-defined type.
52 extra: Arbitrary extra key-value pairs for application-specific state.
53 """
55 theme: str = "default"
56 locale: str = "en"
57 user: Any | None = None
58 extra: dict[str, Any] = dataclasses.field(default_factory=dict)
60 def __repr__(self) -> str:
61 user_repr = (
62 getattr(self.user, "id", None) or str(self.user) if self.user else None
63 )
64 return (
65 f"UIContext(theme={self.theme!r}, locale={self.locale!r}"
66 + (f", user={user_repr!r}" if user_repr is not None else "")
67 + ")"
68 )
71_ctx_var: contextvars.ContextVar[UIContext | None] = contextvars.ContextVar(
72 "lexigram_ui_context",
73 default=None,
74)
77def get_ui_context() -> UIContext | None:
78 """Return the current request-scoped :class:`UIContext`, or ``None`` outside a request.
80 Returns:
81 The active :class:`UIContext` set by :func:`set_ui_context`, or ``None``
82 if called outside of a request (e.g. during startup).
83 """
84 return _ctx_var.get()
87def set_ui_context(ctx: UIContext) -> contextvars.Token[UIContext | None]:
88 """Bind *ctx* as the active UI context for the current async task.
90 Args:
91 ctx: The :class:`UIContext` to set as active.
93 Returns:
94 A :class:`~contextvars.Token` that can be passed to :func:`reset_ui_context`
95 to restore the previous value.
96 """
97 return _ctx_var.set(ctx)
100def reset_ui_context(token: contextvars.Token[UIContext | None]) -> None:
101 """Restore the previous UI context using the token returned by :func:`set_ui_context`.
103 Args:
104 token: The token returned by :func:`set_ui_context`.
105 """
106 _ctx_var.reset(token)
109__all__ = [
110 "UIContext",
111 "get_ui_context",
112 "reset_ui_context",
113 "set_ui_context",
114]