Coverage for src/minihtml/_template.py: 100%

56 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-22 21:44 +0100

1import io 

2from collections.abc import Iterable 

3from functools import wraps 

4from typing import Callable, Concatenate, ParamSpec, TextIO, TypeAlias, overload 

5 

6from ._component import Component 

7from ._core import HasNodes, Node, iter_nodes, register_with_context 

8from ._template_context import get_template_context, template_context 

9 

10P = ParamSpec("P") 

11 

12TemplateImpl: TypeAlias = Callable[P, Node | HasNodes] 

13TemplateImplLayout: TypeAlias = Callable[Concatenate[Component, P], None] 

14 

15 

16def _render(result: Node | HasNodes, doctype: bool) -> str: 

17 buf = io.StringIO() 

18 if doctype: 

19 buf.write("<!doctype html>\n") 

20 Node.render_list(buf, iter_nodes([result])) 

21 buf.write("\n") 

22 return buf.getvalue() 

23 

24 

25@overload 

26def template( 

27 *, doctype: bool = ... 

28) -> Callable[[TemplateImpl[P]], Callable[P, str]]: ... 

29 

30 

31@overload 

32def template( 

33 layout: Component, *, doctype: bool = ... 

34) -> Callable[[TemplateImplLayout[P]], Callable[P, str]]: ... 

35 

36 

37def template( 

38 layout: Component | None = None, 

39 *, 

40 doctype: bool = True, 

41) -> ( 

42 Callable[[TemplateImpl[P]], Callable[P, str]] 

43 | Callable[[TemplateImplLayout[P]], Callable[P, str]] 

44): 

45 if layout is None: 

46 

47 def plain_decorator(fn: TemplateImpl[P]) -> Callable[P, str]: 

48 @wraps(fn) 

49 def wrapper(*args: P.args, **kwargs: P.kwargs) -> str: 

50 with template_context(): 

51 result = fn(*args, **kwargs) 

52 return _render(result, doctype=doctype) 

53 

54 return wrapper 

55 

56 return plain_decorator 

57 

58 else: 

59 

60 def layout_decorator(fn: TemplateImplLayout[P]) -> Callable[P, str]: 

61 @wraps(fn) 

62 def wrapper(*args: P.args, **kwargs: P.kwargs) -> str: 

63 with template_context(): 

64 with layout as result: 

65 fn(layout, *args, **kwargs) 

66 return _render(result, doctype=doctype) 

67 

68 return wrapper 

69 

70 return layout_decorator 

71 

72 

73class ResourceWrapper(Node): 

74 def __init__(self, callback: Callable[[], Iterable[Node]]): 

75 self._callback = callback 

76 self._inline = False 

77 

78 def write(self, f: TextIO, indent: int = 0) -> None: 

79 nodes = list(self._callback()) 

80 n = len(nodes) 

81 for i, node in enumerate(nodes): 

82 node.write(f, indent) 

83 if i < n - 1: 

84 f.write("\n") 

85 f.write(" " * indent) 

86 

87 

88def component_styles() -> ResourceWrapper: 

89 wrapper = ResourceWrapper(lambda: get_template_context().styles) 

90 register_with_context(wrapper) 

91 return wrapper 

92 

93 

94def component_scripts() -> ResourceWrapper: 

95 wrapper = ResourceWrapper(lambda: get_template_context().scripts) 

96 register_with_context(wrapper) 

97 return wrapper