Skip to content

Commit e84f793

Browse files
keithasaurusKeith PhilpottKeith Philpott
authored
prerender (#29)
* wip * wip * wip * wip * wip * wip * basic test * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * string contains * wip * wip * wip * wip * wip * wip * type check * wip * wi * wip * wip * wip * wip * wip * wip * wi * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * spacing * punct * wip * wip * wip * wip --------- Co-authored-by: Keith Philpott <keith@Keiths-Mac-Studio.local> Co-authored-by: Keith Philpott <keith@example.com>
1 parent 36e0b5c commit e84f793

9 files changed

Lines changed: 711 additions & 515 deletions

File tree

.github/workflows/push.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
strategy:
77
fail-fast: false
88
matrix:
9-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
9+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1010
poetry-version: [2.1]
1111
os: [ubuntu-24.04, macos-latest, windows-latest]
1212
runs-on: ${{ matrix.os }}
@@ -26,7 +26,7 @@ jobs:
2626
- name: run bench (pure python)
2727
run: poetry run python -m bench.run
2828
- name: mypyc
29-
run: poetry run mypyc simple_html/utils.py
29+
run: poetry run mypyc simple_html/core.py
3030
- name: run tests
3131
run: poetry run pytest
3232
- name: run bench (compiled)

README.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Why use it?
44
- clean syntax
55
- fully-typed
6-
- speed -- faster even than jinja
6+
- speed -- often faster than jinja
77
- zero dependencies
88
- escaped by default
99
- usually renders fewer bytes than templating
@@ -18,6 +18,7 @@
1818
```python
1919
from simple_html import h1, render
2020

21+
2122
node = h1("Hello World!")
2223

2324
render(node)
@@ -36,6 +37,7 @@ Here's a fuller-featured example:
3637
```python
3738
from simple_html import render, DOCTYPE_HTML5, html, head, title, body, h1, div, p, br, ul, li
3839

40+
3941
render(
4042
DOCTYPE_HTML5,
4143
html(
@@ -79,6 +81,7 @@ As you might have noticed, there are several ways to use `Tag`s:
7981
```python
8082
from simple_html import br, div, h1, img, span, render
8183

84+
8285
# raw node renders to empty tag
8386
render(br)
8487
# <br/>
@@ -106,6 +109,7 @@ escaped by default; `SafeString`s can be used to bypass escaping.
106109
```python
107110
from simple_html import br, p, SafeString, render
108111

112+
109113
node = p("Escaped & stuff",
110114
br,
111115
SafeString("Not escaped & stuff"))
@@ -121,6 +125,7 @@ that Tag attributes with `None` as the value will only render the attribute name
121125
```python
122126
from simple_html import div, render
123127

128+
124129
node = div({"empty-str-attribute": "",
125130
"key-only-attr": None})
126131

@@ -133,6 +138,7 @@ String attributes are escaped by default -- both keys and values. You can use `S
133138
```python
134139
from simple_html import div, render, SafeString
135140

141+
136142
render(
137143
div({"<bad>":"</also bad>"})
138144
)
@@ -149,6 +155,7 @@ You can also use `int`, `float`, and `Decimal` instances for attribute values.
149155
from decimal import Decimal
150156
from simple_html import div, render, SafeString
151157

158+
152159
render(
153160
div({"x": 1, "y": 2.3, "z": Decimal('3.45')})
154161
)
@@ -161,6 +168,7 @@ You can render inline CSS styles with `render_styles`:
161168
```python
162169
from simple_html import div, render, render_styles
163170

171+
164172
styles = render_styles({"min-width": "25px"})
165173

166174
node = div({"style": styles}, "cool")
@@ -184,6 +192,7 @@ You can pass many items as a `Tag`'s children using `*args`, lists or generators
184192
from typing import Generator
185193
from simple_html import div, render, Node, br, p
186194

195+
187196
div(
188197
*["neat", br], p("cool")
189198
)
@@ -214,6 +223,7 @@ For convenience, most common tags are provided, but you can also create your own
214223
```python
215224
from simple_html import Tag, render
216225

226+
217227
custom_elem = Tag("custom-elem")
218228

219229
# works the same as any other tag
@@ -225,3 +235,89 @@ node = custom_elem(
225235
render(node)
226236
# <custom-elem id="some-custom-elem-id">Wow</custom-elem>
227237
```
238+
239+
### Optimization
240+
241+
#### `prerender`
242+
243+
`prerender` is a very simple function. It just `render`s a `Node` and puts the resulting string inside
244+
a `SafeString` (so its contents won't be escaped again). It's most useful for prerendering at the module level,
245+
which ensures the render operation happens only once. A simple use case might be a website's footer:
246+
247+
```python
248+
from simple_html import SafeString, prerender, footer, div, a, head, body, title, h1, html, render
249+
250+
251+
prerendered_footer: SafeString = prerender(
252+
footer(
253+
div(a({"href": "/about"}, "About Us")),
254+
div(a({"href": "/blog"}, "Blog")),
255+
div(a({"href": "/contact"}, "Contact"))
256+
)
257+
)
258+
259+
260+
def render_page(page_title: str) -> str:
261+
return render(
262+
html(
263+
head(title(page_title)),
264+
body(
265+
h1(page_title),
266+
prerendered_footer # this is extremely fast to render
267+
)
268+
)
269+
)
270+
```
271+
This greatly reduces the amount of work `render` needs to do on the prerendered content when outputting HTML.
272+
273+
#### Caching
274+
You may want to cache rendered content. This is easy to do; the main thing to keep in
275+
mind is you'll likely want to return a `SafeString`. For example, here's how you might cache with `lru_cache`:
276+
277+
```python
278+
from simple_html import prerender, SafeString, h1
279+
from functools import lru_cache
280+
281+
282+
@lru_cache
283+
def greeting(name: str) -> SafeString:
284+
return prerender(
285+
h1(f"Hello, {name}")
286+
)
287+
```
288+
289+
One thing to remember is that not all variants of `Node` are hashable, and thus cannot be passed directly to a function
290+
where the arguments constitute the cache key -- e.g. lists and generators are not hashable, but they can be
291+
valid `Node`s. Another way to use `prerender` in combination with a caching function is to prerender arguments:
292+
293+
```python
294+
from simple_html import prerender, SafeString, h1, div, html, body, head, ul, li
295+
from functools import lru_cache
296+
297+
298+
@lru_cache
299+
def cached_content(children: SafeString) -> SafeString:
300+
return prerender(
301+
div(
302+
h1("This content is cached according to the content of the children"),
303+
children,
304+
# presumably this function would have a lot more elements for it to be worth
305+
# the caching overhead
306+
)
307+
)
308+
309+
def page(words_to_render: list[str]):
310+
return html(
311+
head,
312+
body(
313+
cached_content(
314+
prerender(ul([
315+
li(word) for word in words_to_render
316+
]))
317+
)
318+
)
319+
)
320+
```
321+
Keep in mind that using `prerender` on dynamic content -- not at the module level -- still incurs all the overhead
322+
of `render` each time that content is rendered, so, for this approach to make sense, the prerendered content should
323+
be a small portion of the full content of the `cached_content` function.

0 commit comments

Comments
 (0)