tangled
alpha
login
or
join now
trendels.name
/
minihtml
1
fork
atom
Simple HTML Generation https://minihtml.trendels.name/
1
fork
atom
overview
issues
pulls
pipelines
make template rendering lazy
Stanis Trendelenburg
1 year ago
538d587f
857b9ac6
+81
-44
3 changed files
expand all
collapse all
unified
split
src
minihtml
__init__.py
_template.py
tests
test_template.py
+2
-1
src/minihtml/__init__.py
reviewed
···
15
15
safe,
16
16
text,
17
17
)
18
18
-
from ._template import component_scripts, component_styles, template
18
18
+
from ._template import Template, component_scripts, component_styles, template
19
19
20
20
__all__ = [
21
21
"CircularReferenceError",
···
31
31
"PrototypeNonEmpty",
32
32
"SlotContext",
33
33
"Slots",
34
34
+
"Template",
34
35
"Text",
35
36
"component",
36
37
"component_scripts",
+52
-33
src/minihtml/_template.py
reviewed
···
13
13
TemplateImplLayout: TypeAlias = Callable[Concatenate[Component, P], None]
14
14
15
15
16
16
-
def _render(result: Node | HasNodes, doctype: bool) -> str:
17
17
-
buf = io.StringIO()
18
18
-
if doctype:
19
19
-
buf.write("<!doctype html>\n")
20
20
-
Node.render_list(buf, iter_nodes([result]))
21
21
-
buf.write("\n")
22
22
-
return buf.getvalue()
16
16
+
class Template:
17
17
+
def __init__(self, callback: Callable[[], list[Node]]):
18
18
+
self._callback = callback
19
19
+
20
20
+
def render(self, *, doctype: bool = True) -> str:
21
21
+
nodes = self._callback()
22
22
+
buf = io.StringIO()
23
23
+
if doctype:
24
24
+
buf.write("<!doctype html>\n")
25
25
+
Node.render_list(buf, nodes)
26
26
+
buf.write("\n")
27
27
+
return buf.getvalue()
23
28
24
29
25
30
@overload
26
26
-
def template(
27
27
-
*, doctype: bool = ...
28
28
-
) -> Callable[[TemplateImpl[P]], Callable[P, str]]: ...
31
31
+
def template() -> Callable[[TemplateImpl[P]], Callable[P, Template]]: ...
29
32
30
33
31
34
@overload
32
35
def template(
33
33
-
layout: ComponentWrapper[...], *, doctype: bool = ...
34
34
-
) -> Callable[[TemplateImplLayout[P]], Callable[P, str]]: ...
36
36
+
*, layout: ComponentWrapper[...]
37
37
+
) -> Callable[[TemplateImplLayout[P]], Callable[P, Template]]: ...
35
38
36
39
37
40
def template(
38
38
-
layout: ComponentWrapper[...] | None = None,
39
39
-
*,
40
40
-
doctype: bool = True,
41
41
+
*, layout: ComponentWrapper[...] | None = None
41
42
) -> (
42
42
-
Callable[[TemplateImpl[P]], Callable[P, str]]
43
43
-
| Callable[[TemplateImplLayout[P]], Callable[P, str]]
43
43
+
Callable[[TemplateImpl[P]], Callable[P, Template]]
44
44
+
| Callable[[TemplateImplLayout[P]], Callable[P, Template]]
44
45
):
45
46
if layout is None:
46
47
47
47
-
def plain_decorator(fn: TemplateImpl[P]) -> Callable[P, str]:
48
48
+
def plain_decorator(fn: TemplateImpl[P]) -> Callable[P, Template]:
48
49
@wraps(fn)
49
49
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
50
50
-
with template_context():
51
51
-
result = fn(*args, **kwargs)
52
52
-
return _render(result, doctype=doctype)
50
50
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Template:
51
51
+
def callback() -> list[Node]:
52
52
+
with template_context():
53
53
+
result = fn(*args, **kwargs)
54
54
+
return list(iter_nodes([result]))
55
55
+
56
56
+
return Template(callback)
53
57
54
58
return wrapper
55
59
···
57
61
58
62
else:
59
63
60
60
-
def layout_decorator(fn: TemplateImplLayout[P]) -> Callable[P, str]:
64
64
+
def layout_decorator(fn: TemplateImplLayout[P]) -> Callable[P, Template]:
61
65
@wraps(fn)
62
62
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
63
63
-
with template_context():
64
64
-
with layout() as result:
65
65
-
fn(result, *args, **kwargs)
66
66
-
return _render(result, doctype=doctype)
66
66
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Template:
67
67
+
def callback() -> list[Node]:
68
68
+
with template_context():
69
69
+
with layout() as result:
70
70
+
fn(result, *args, **kwargs)
71
71
+
return list(iter_nodes([result]))
72
72
+
73
73
+
return Template(callback)
67
74
68
75
return wrapper
69
76
···
71
78
72
79
73
80
class ResourceWrapper(Node):
74
74
-
def __init__(self, callback: Callable[[], Iterable[Node]]):
75
75
-
self._callback = callback
81
81
+
def __init__(self, nodes: Iterable[Node]):
82
82
+
self._nodes = nodes
76
83
self._inline = False
77
84
78
85
def write(self, f: TextIO, indent: int = 0) -> None:
79
79
-
nodes = list(self._callback())
86
86
+
nodes = list(self._nodes)
80
87
n = len(nodes)
81
88
for i, node in enumerate(nodes):
82
89
node.write(f, indent)
···
86
93
87
94
88
95
def component_styles() -> ResourceWrapper:
89
89
-
wrapper = ResourceWrapper(lambda: get_template_context().styles)
96
96
+
"""
97
97
+
Placeholder element for component styles.
98
98
+
99
99
+
Can only be used in code called from a :deco:`template`. Inserts the style
100
100
+
nodes collected from all components used in the current template.
101
101
+
"""
102
102
+
wrapper = ResourceWrapper(get_template_context().styles)
90
103
register_with_context(wrapper)
91
104
return wrapper
92
105
93
106
94
107
def component_scripts() -> ResourceWrapper:
95
95
-
wrapper = ResourceWrapper(lambda: get_template_context().scripts)
108
108
+
"""
109
109
+
Placeholder element for component scripts.
110
110
+
111
111
+
Can only be used in code called from a :deco:`template`. Inserts the script
112
112
+
nodes collected from all components used in the current template.
113
113
+
"""
114
114
+
wrapper = ResourceWrapper(get_template_context().scripts)
96
115
register_with_context(wrapper)
97
116
return wrapper
+27
-10
tests/test_template.py
reviewed
···
2
2
3
3
from minihtml import (
4
4
Component,
5
5
+
Context,
5
6
Element,
6
7
Slots,
7
8
component,
···
13
14
from minihtml.tags import body, div, head, html, main, script, style, title
14
15
15
16
16
16
-
def test_template_returns_html_with_doctype_and_trailing_newline():
17
17
+
def test_template_renders_as_html_with_doctype_and_trailing_newline():
17
18
@template()
18
19
def my_template(message: str) -> Element:
19
20
return div(message)
20
21
21
21
-
assert my_template("hello") == dedent("""\
22
22
+
assert my_template("hello").render() == dedent("""\
22
23
<!doctype html>
23
24
<div>hello</div>
24
25
""")
25
26
26
27
27
28
def test_template_can_disable_doctype():
28
28
-
@template(doctype=False)
29
29
+
@template()
29
30
def my_template(message: str) -> Element:
30
31
return div(message)
31
32
32
32
-
assert my_template("hello") == dedent("""\
33
33
-
<div>hello</div>
34
34
-
""")
33
33
+
assert my_template("hello").render(doctype=False) == "<div>hello</div>\n"
35
34
36
35
37
36
def test_template_with_layout_component():
···
52
51
text("my title")
53
52
div(message)
54
53
55
55
-
assert my_template("hello") == dedent("""\
54
54
+
assert my_template("hello").render() == dedent("""\
56
55
<!doctype html>
57
56
<html>
58
57
<head>
···
67
66
""")
68
67
69
68
# Test that layout is not cached
70
70
-
assert my_template("goodbye") == dedent("""\
69
69
+
assert my_template("goodbye").render() == dedent("""\
71
70
<!doctype html>
72
71
<html>
73
72
<head>
···
113
112
my_component()
114
113
my_component()
115
114
116
116
-
assert my_template() == dedent("""\
115
115
+
assert my_template().render() == dedent("""\
117
116
<!doctype html>
118
117
<html>
119
118
<head>
···
157
156
),
158
157
)
159
158
160
160
-
assert my_template() == dedent("""\
159
159
+
assert my_template().render() == dedent("""\
161
160
<!doctype html>
162
161
<html>
163
162
<head>
···
171
170
</body>
172
171
</html>
173
172
""")
173
173
+
174
174
+
175
175
+
def test_template_is_rendered_lazily():
176
176
+
class MyContext(Context):
177
177
+
def __init__(self, name: str):
178
178
+
self.name = name
179
179
+
180
180
+
@template()
181
181
+
def my_template() -> Element:
182
182
+
return div(MyContext.get().name)
183
183
+
184
184
+
t = my_template()
185
185
+
186
186
+
with MyContext(name="fred"):
187
187
+
assert t.render(doctype=False) == "<div>fred</div>\n"
188
188
+
189
189
+
with MyContext(name="barney"):
190
190
+
assert t.render(doctype=False) == "<div>barney</div>\n"