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

1from contextvars import ContextVar 

2from dataclasses import dataclass, field 

3from string.templatelib import Template 

4 

5from .processor import ( 

6 Attribute, 

7 ComponentProcessor, 

8 IComponentProcessor, 

9 ProcessContext, 

10 TemplateProcessor, 

11) 

12from .tnodes import TAttribute 

13 

14 

15@dataclass(frozen=True, slots=True) 

16class AppState: 

17 theme_class: str 

18 

19 

20AppStateCtx: ContextVar[AppState | None] = ContextVar("AppStateCtx", default=None) 

21 

22 

23class TestComponentProcessor: 

24 @dataclass 

25 class Body: 

26 children: Template 

27 

28 def __call__(self) -> Template: 

29 return t"<body>{self.children}</body>" 

30 

31 @dataclass 

32 class Header: 

33 children: Template 

34 

35 app_state: AppState 

36 

37 hdr_class: str = "hdr" 

38 

39 def __call__(self) -> Template: 

40 return t"<div class={self.hdr_class} class={self.app_state.theme_class}>{self.children}</div>" 

41 

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 ) 

48 

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 ) 

72 

73 def _make_html(self): 

74 app_state_processor = self.AppStateComponentProcessor() 

75 tp = TemplateProcessor(component_processor_api=app_state_processor) 

76 assume_ctx = ProcessContext() 

77 

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) 

83 

84 return _html 

85 

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 )