Coverage for tdom / processor_extension_test.py: 100%
42 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-03 21:23 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-03 21:23 +0000
1from contextvars import ContextVar
2from dataclasses import dataclass, field
3from string.templatelib import Template
5from .processor import (
6 Attribute,
7 ComponentProcessor,
8 IComponentProcessor,
9 ProcessContext,
10 TemplateProcessor,
11)
12from .tnodes import TAttribute
15@dataclass(frozen=True, slots=True)
16class AppState:
17 theme_class: str
20AppStateCtx: ContextVar[AppState | None] = ContextVar("AppStateCtx", default=None)
23class TestComponentProcessor:
24 @dataclass
25 class Body:
26 children: Template
28 def __call__(self) -> Template:
29 return t"<body>{self.children}</body>"
31 @dataclass
32 class Header:
33 children: Template
35 app_state: AppState
37 hdr_class: str = "hdr"
39 def __call__(self) -> Template:
40 return t"<div class={self.hdr_class} class={self.app_state.theme_class}>{self.children}</div>"
42 @dataclass
43 class AppStateComponentProcessor(IComponentProcessor):
44 # Delegate to the default processor to reuse code.
45 default_component_processor_api: IComponentProcessor = field(
46 default_factory=ComponentProcessor
47 )
49 def process(
50 self,
51 template: Template,
52 last_ctx: ProcessContext,
53 component_callable: object,
54 attrs: tuple[TAttribute, ...],
55 component_template: Template,
56 provided_attrs: tuple[Attribute, ...] = (),
57 ) -> Template:
58 # For now we just make the app state available to EVERY component
59 # a smarter strategy would be to only include it if asked via
60 # the callable's signature or even the callable's typehints.
61 # But for a test this is OK.
62 app_state = AppStateCtx.get()
63 extended_attrs = provided_attrs + (("app_state", app_state),)
64 return self.default_component_processor_api.process(
65 template=template,
66 last_ctx=last_ctx,
67 component_callable=component_callable,
68 attrs=attrs,
69 component_template=component_template,
70 provided_attrs=extended_attrs,
71 )
73 def _make_html(self):
74 app_state_processor = self.AppStateComponentProcessor()
75 tp = TemplateProcessor(component_processor_api=app_state_processor)
76 assume_ctx = ProcessContext()
78 def _html(template: Template, app_state: AppState | None = None) -> str:
79 if app_state is None:
80 app_state = AppState(theme_class="theme-default")
81 with AppStateCtx.set(app_state):
82 return tp.process(template, assume_ctx=assume_ctx)
84 return _html
86 def test_injected_app_state(self):
87 name = "App"
88 body_t = (
89 t"<{self.Body}><{self.Header}><h1>{name}</h1></{self.Header}></{self.Body}>"
90 )
91 html = self._make_html()
92 assert (
93 html(body_t, app_state=None)
94 == '<body><div class="hdr theme-default"><h1>App</h1></div></body>'
95 )
96 assert (
97 html(body_t, app_state=AppState(theme_class="theme-spring"))
98 == '<body><div class="hdr theme-spring"><h1>App</h1></div></body>'
99 )
100 assert (
101 html(body_t, app_state=None)
102 == '<body><div class="hdr theme-default"><h1>App</h1></div></body>'
103 )