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

1"""Per-request UI context. 

2 

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. 

6 

7Usage — middleware:: 

8 

9 from lexigram.ui.core.context import UIContext, set_ui_context 

10 

11 class UIContextMiddleware: 

12 def __init__(self, app): 

13 self._app = app 

14 

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) 

25 

26Usage — component:: 

27 

28 from lexigram.ui.core.context import get_ui_context 

29 

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""" 

36 

37from __future__ import annotations 

38 

39import contextvars 

40import dataclasses 

41from typing import Any 

42 

43 

44@dataclasses.dataclass(frozen=True) 

45class UIContext: 

46 """Immutable per-request UI rendering context. 

47 

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 """ 

54 

55 theme: str = "default" 

56 locale: str = "en" 

57 user: Any | None = None 

58 extra: dict[str, Any] = dataclasses.field(default_factory=dict) 

59 

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 ) 

69 

70 

71_ctx_var: contextvars.ContextVar[UIContext | None] = contextvars.ContextVar( 

72 "lexigram_ui_context", 

73 default=None, 

74) 

75 

76 

77def get_ui_context() -> UIContext | None: 

78 """Return the current request-scoped :class:`UIContext`, or ``None`` outside a request. 

79 

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() 

85 

86 

87def set_ui_context(ctx: UIContext) -> contextvars.Token[UIContext | None]: 

88 """Bind *ctx* as the active UI context for the current async task. 

89 

90 Args: 

91 ctx: The :class:`UIContext` to set as active. 

92 

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) 

98 

99 

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`. 

102 

103 Args: 

104 token: The token returned by :func:`set_ui_context`. 

105 """ 

106 _ctx_var.reset(token) 

107 

108 

109__all__ = [ 

110 "UIContext", 

111 "get_ui_context", 

112 "reset_ui_context", 

113 "set_ui_context", 

114]