Coverage for tdom/processor_extension_test.py: 100%
49 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-23 04:35 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-23 04:35 +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 .scope import ScopedTemplate
13from .tnodes import TAttribute
16@dataclass(frozen=True, slots=True)
17class AppState:
18 theme_class: str
21AppStateCtx: ContextVar[AppState | None] = ContextVar("AppStateCtx", default=None)
24class TestComponentProcessor:
25 @dataclass
26 class Body:
27 children: Template
29 def __call__(self) -> Template:
30 return t"<body>{self.children}</body>"
32 @dataclass
33 class Header:
34 children: Template
36 app_state: AppState
38 hdr_class: str = "hdr"
40 def __call__(self) -> Template:
41 return t"<div class={self.hdr_class} class={self.app_state.theme_class}>{self.children}</div>"
43 @dataclass
44 class AppStateComponentProcessor(IComponentProcessor):
45 # Delegate to the default processor to reuse code.
46 default_component_processor_api: IComponentProcessor = field(
47 default_factory=ComponentProcessor
48 )
50 def process(
51 self,
52 template: Template,
53 last_ctx: ProcessContext,
54 component_callable: object,
55 attrs: tuple[TAttribute, ...],
56 component_template: Template,
57 provided_attrs: tuple[Attribute, ...] = (),
58 ) -> Template | ScopedTemplate:
59 # For now we just make the app state available to EVERY component
60 # a smarter strategy would be to only include it if asked via
61 # the callable's signature or even the callable's typehints.
62 # But for a test this is OK.
63 app_state = AppStateCtx.get()
64 extended_attrs = provided_attrs + (("app_state", app_state),)
65 return self.default_component_processor_api.process(
66 template=template,
67 last_ctx=last_ctx,
68 component_callable=component_callable,
69 attrs=attrs,
70 component_template=component_template,
71 provided_attrs=extended_attrs,
72 )
74 def _make_html(self):
75 app_state_processor = self.AppStateComponentProcessor()
76 tp = TemplateProcessor(component_processor_api=app_state_processor)
77 assume_ctx = ProcessContext()
79 def _html(template: Template, app_state: AppState | None = None) -> str:
80 if app_state is None:
81 app_state = AppState(theme_class="theme-default")
82 with AppStateCtx.set(app_state):
83 return tp.process(template, assume_ctx=assume_ctx)
85 return _html
87 def test_injected_app_state(self):
88 name = "App"
89 body_t = (
90 t"<{self.Body}><{self.Header}><h1>{name}</h1></{self.Header}></{self.Body}>"
91 )
92 html = self._make_html()
93 assert (
94 html(body_t, app_state=None)
95 == '<body><div class="hdr theme-default"><h1>App</h1></div></body>'
96 )
97 assert (
98 html(body_t, app_state=AppState(theme_class="theme-spring"))
99 == '<body><div class="hdr theme-spring"><h1>App</h1></div></body>'
100 )
101 assert (
102 html(body_t, app_state=None)
103 == '<body><div class="hdr theme-default"><h1>App</h1></div></body>'
104 )
106 def test_injected_works_with_kwargs(self):
107 """Test that provided attr is not injected into kwargs."""
109 def Comp(**kwargs):
110 return t"<div {kwargs}></div>"
112 html = self._make_html()
113 assert (
114 html(t"<{Comp}/>", app_state=AppState(theme_class="theme-spring"))
115 == "<div></div>"
116 )
117 assert (
118 html(
119 t'<{Comp} title="{"ok"}"/>',
120 app_state=AppState(theme_class="theme-spring"),
121 )
122 == '<div title="ok"></div>'
123 )