31 Oct, 2024
Here's some code for rendering HTML in Python fast, and correctly:
from html import escape
class SafelyEscapedHTML(str):
pass
def _dynamic_escape(s):
if type(s) is SafelyEscapedHTML:
return str(s)
else:
return escape(s)
class Tag:
def __init__(self, name, needs_closing=True):
self.name = name
self.needs_closing = needs_closing
self.attrs = {}
self.children = []
def __call__(self, *class_, **attrs):
assert not self.children
new_tag = Tag(self.name, self.needs_closing)
if class_:
assert len(class_) == 1
assert 'class' not in 'attrs'
new_tag.attrs['class'] = class_[0]
new_tag.attrs.update(attrs)
return new_tag
def __getitem__(self, children):
new_tag = Tag(self.name, self.needs_closing)
new_tag.attrs = self.attrs
if type(children) is tuple:
for child in children:
new_tag.children.append(child)
else:
new_tag.children = [children]
return new_tag
def __repr__(self):
return f'<{self.name} {repr(self.attrs)} children={len(self.children)} needs_closing={self.needs_closing} id={id(self)}>'
def __str__(self):
_dynamic_escaped_name = _dynamic_escape(self.name)
result = f'<{_dynamic_escaped_name}'
for name, value in self.attrs.items():
result += f' {_dynamic_escape(name)}="{_dynamic_escape(value)}"'
result += '>'
for child in self.children:
if isinstance(child, Tag):
result += str(child)
else:
result += _dynamic_escape(child)
if self.needs_closing:
result += f'</{_dynamic_escaped_name}>'
return result
def template(self):
strings = []
placeholders = []
_dynamic_escaped_name = _dynamic_escape(self.name)
strings.append(f'<{_dynamic_escaped_name}')
for name, value in self.attrs.items():
if isinstance(value, Placeholder):
strings[-1] += f' {_dynamic_escape(name)}="'
placeholders.append(value.name)
strings.append('"')
else:
strings[-1] += f' {_dynamic_escape(name)}="{_dynamic_escape(value)}"'
strings[-1] += '>'
for child in self.children:
if isinstance(child, Tag):
child_strings, child_placeholders = child.template()
strings[-1] += child_strings[0]
strings += child_strings[1:]
placeholders += child_placeholders
elif isinstance(child, Placeholder):
strings.append('')
placeholders.append(child.name)
else:
strings[-1] += _dynamic_escape(child)
if self.needs_closing:
strings[-1] += f'</{_dynamic_escaped_name}>'
return (strings, placeholders)
class Template:
def __init__(self, page):
strings, placeholders = page.template()
self.strings = strings
self.placeholders = placeholders
def render(self, **values):
return ''.join(_render((self.strings, self.placeholders), **values))
class Placeholder:
def __init__(self, name):
self.name = name
def _render(template, **values):
strings, placeholders = template
replaced = []
for placeholder_name in template[1]:
value = ''
if placeholder_name in values:
value = values[placeholder_name]
if type(value) is not SafelyEscapedHTML:
value = escape(value)
replaced.append(value)
else:
raise Exception(f'Placeholder {placeholder_name} not found.')
yield strings[0]
for i in range(len(placeholders)):
yield replaced[i]
yield strings[i+1]
def example():
html = Tag('html')
head = Tag('head')
title = Tag('title')
body = Tag('body')
p = Tag('p')
a = Tag('a')
link = Tag('link', needs_closing=False)
br = Tag('br', needs_closing=False)
def main(link_, href='here'):
return html[
head[
title['Hello, world!'],
link(**{'type':"stylesheet"})
],
body[
p('main')[
link_,
a(href=href)['This &<> is a link'],
'.'
],
p['Hello', br, br],
]
]
import time
m = main('th"ere')
print('Page direct:', str(m))
num = 100000
start = time.time()
for x in range(num):
str(main('th"ere'))
duration = time.time() - start
rps = int(num/duration)
print('Page direct renders per second:', rps)
template = Template(main(Placeholder('one'), Placeholder('two')))
print('Via template with placeholders:', template.render(one='o&ne', two=SafelyEscapedHTML('t<b>w</b>o')))
num = 1000000
start = time.time()
for x in range(num):
template.render(one='o&ne', two=SafelyEscapedHTML('t<b>w</b>o'))
duration = time.time() - start
rps = int(num/duration)
print('Template renders per second:', rps)
if __name__ == '__main__':
example()
Be the first to comment.
Copyright James Gardner 1996-2020 All Rights Reserved. Admin.