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

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 .scope import ScopedTemplate 

13from .tnodes import TAttribute 

14 

15 

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

17class AppState: 

18 theme_class: str 

19 

20 

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

22 

23 

24class TestComponentProcessor: 

25 @dataclass 

26 class Body: 

27 children: Template 

28 

29 def __call__(self) -> Template: 

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

31 

32 @dataclass 

33 class Header: 

34 children: Template 

35 

36 app_state: AppState 

37 

38 hdr_class: str = "hdr" 

39 

40 def __call__(self) -> Template: 

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

42 

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 ) 

49 

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 ) 

73 

74 def _make_html(self): 

75 app_state_processor = self.AppStateComponentProcessor() 

76 tp = TemplateProcessor(component_processor_api=app_state_processor) 

77 assume_ctx = ProcessContext() 

78 

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) 

84 

85 return _html 

86 

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 ) 

105 

106 def test_injected_works_with_kwargs(self): 

107 """Test that provided attr is not injected into kwargs.""" 

108 

109 def Comp(**kwargs): 

110 return t"<div {kwargs}></div>" 

111 

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 )