Create HTML With Strings and Dictionaries in Python

Write HTML with simple strings and dictionaries in Python.

I have been working on an experimental Python library I call "chamelboots" because it combines the Python library Chameleon and Bootstrap to create Boostrap HTML with a minimum of input.

I am lazy as in the definition in the three virtues of a great programmer definition.

I despise writing HTML and anything that looks like it, e.g. JSX and XML.

I have to move my fingers to the rarely used characters of "<>/".

Other templating libraries just add to the complexity with the "{% %}" notation.

The power of Chameleon is that it uses valid HTML as its template.

And it is possible to use tools like lxml to programatically create HTML strings without having to type them out by hand.

Special attributes tell the Chameleon templater what to do.

In a Chameleon HTML template, Python code can be a value to an attribute on HTML.

That is power.

In [5]:
from IPython.display import HTML
from chamelboots import ChameleonTemplate, TalStatement
In [6]:
HTML(
    ChameleonTemplate(
        "a", tal_statements=(TalStatement("attributes", "attrib"),)
    ).render(attrib=dict(href="interpolate-inner", id="todolist"))
)
Out[6]:

TODO

  • Allow for multiple "tal:{value}" attributes. Just noticed this is possible in the Chameleon documentation.
  • Improve names for keyword arguments.
  • Create a Bootstrap HTML template programatically. * Move the template creation code to a better place.
  • Write a method that outputs parameterized HTML with the parent element removed, something similar to this.
  • Create CLI command to write out a Boostrap file template programmatically from starter template, e.g. parameterize the creation of the template for customization.
In [7]:
from chamelboots import ChameleonTemplate, TalStatement
from IPython.display import display, HTML

from chameleon import PageTemplate

Create a class called "Brian" and use an instance of it inside of a Chameleon template.

NB type is a callable class of type "type" that returns classes dynamically when passed 3 arguments of name, parents, and attributes.

In [8]:
eggs_header = ChameleonTemplate(
    tag="h1", tal_statements=(TalStatement("content", "brian.spam"),)
).render(brian=type("Brian", (), dict(spam="eggs"))())
print(eggs_header)
display(HTML(eggs_header))
<h1>eggs</h1>

eggs

Import uuid.uuid4 from Python standard library and define it as content.

In [9]:
h1 = ChameleonTemplate(
    "h1",
    (
        TalStatement("define", "uuid4 import: uuid.uuid4"),
        TalStatement("content", "uuid4().hex"),
    ),
)

Render the executed Python code into the HTML.

In [10]:
# TODO possibly integrate into the ChameleonTemplate as a way of
# creating inner content and avoiding the escaping problem.
div = ChameleonTemplate(
    tal_statements=(
        TalStatement("content", "structure content"),  # This preserves the literal HTML
    ),
).render(content=h1.html_string)
div
Out[10]:
'<div><h1 tal:define="uuid4 import: uuid.uuid4" tal:content="uuid4().hex"></h1></div>'

Render with Chameleon PageTemplate

This executes the Python code and inserts the evaluation of the expression into the content of the tag.

In [11]:
print(PageTemplate(div).render())
display(HTML(PageTemplate(div).render()))
<div><h1>dcb1817a759b472f856ab8b89bfb09a5</h1></div>

684a6cb64d6841738abf189d48b69475

In [14]:
from chamelboots.constants import FAKE

Output of ChameleonTemplate??

Init signature:
ChameleonTemplate(
    tag: str = 'div',
    tal_statements: Sequence[chamelboots.TalStatement] = (),
    inner_content: str = '',
)
In [15]:
P = "p"
In [16]:
# tal values as taken from Chameleon docs.
ChameleonTemplate.valid_statements
Out[16]:
{'define': 'tal:define',
 'switch': 'tal:switch',
 'condition': 'tal:condition',
 'repeat': 'tal:repeat',
 'case': 'tal:case',
 'content': 'tal:content',
 'replace': 'tal:replace',
 'omit-tag': 'tal:omit-tag',
 'attributes': 'tal:attributes',
 'on-error': 'tal:on-error'}
In [17]:
paragraph = ChameleonTemplate(
    tag=P,
    tal_statements=[
        TalStatement(name, value)
        for name, value in zip(("content", "repeat",), ("item", "item items"))
    ],
)
paragraph.html_string
Out[17]:
'<p tal:content="item" tal:repeat="item items"></p>'

The string is valid HTML.

In [18]:
from lxml import etree
In [19]:
[
    etree.tostring(e, method="html").decode()
    for e in etree.fromstring(paragraph.html_string, etree.HTMLParser()).iter()
]
Out[19]:
['<html><body><p tal:content="item" tal:repeat="item items"></p></body></html>',
 '<body><p tal:content="item" tal:repeat="item items"></p></body>',
 '<p tal:content="item" tal:repeat="item items"></p>']

The string is a chameleon template.

In [20]:
print(paragraph.render(items=(FAKE.catch_phrase() for _ in range(10))))
<p>Future-proofed interactive productivity</p>
<p>Universal impactful migration</p>
<p>Operative impactful groupware</p>
<p>Optimized hybrid pricing structure</p>
<p>Profound solution-oriented strategy</p>
<p>Phased human-resource methodology</p>
<p>Quality-focused optimizing forecast</p>
<p>Secured optimal intranet</p>
<p>Integrated exuding moderator</p>
<p>Multi-lateral 4thgeneration solution</p>
In [21]:
display(HTML(paragraph.render(items=(FAKE.catch_phrase() for _ in range(10)))))

Universal executive migration

Face-to-face multimedia success

Synergized coherent throughput

Multi-lateral 24/7 task-force

Fundamental local open architecture

Vision-oriented contextually-based definition

Advanced leadingedge budgetary management

Streamlined foreground migration

Multi-channeled systemic attitude

Ergonomic dynamic contingency

Create a simple unordered list with Python range function.

In [22]:
from chameleon import PageTemplate
In [23]:
repeat_template = ChameleonTemplate(
    tag="li",
    tal_statements=[
        TalStatement(name, value)
        for name, value in zip(("repeat", "content"), ("item range(10)", "item"))
    ],
)
print(PageTemplate(repeat_template.html_string).render())
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
In [24]:
# __getattr__ returns attrs from this.page_template
list_items = repeat_template.render()
print(list_items)
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>

Without "structure" type, the html is escaped into HTML entities.

Chameleon Docs on types

In [25]:
display(
    (
        ChameleonTemplate(
            tag="ul",
            tal_statements=(
                TalStatement("content", "list_items",),
            ),  # keeps it as HTML
            inner_content="${list_items}",
        ).render(list_items=repeat_template.render())
    )
)
'<ul>&lt;li&gt;0&lt;/li&gt;\n&lt;li&gt;1&lt;/li&gt;\n&lt;li&gt;2&lt;/li&gt;\n&lt;li&gt;3&lt;/li&gt;\n&lt;li&gt;4&lt;/li&gt;\n&lt;li&gt;5&lt;/li&gt;\n&lt;li&gt;6&lt;/li&gt;\n&lt;li&gt;7&lt;/li&gt;\n&lt;li&gt;8&lt;/li&gt;\n&lt;li&gt;9&lt;/li&gt;</ul>'
In [26]:
# Create an anchor tag for a link above

HTML(
    ChameleonTemplate(
        "a",
        tal_statements=(TalStatement("attributes", "attrib"),),
        inner_content="back to todo list",
    ).render(
        attrib=dict(id="interpolate-inner", href="#todolist", style="font-size: 2rem;")
    )
)
# Displayed and functional in this Jupyter notebook.
In [27]:
display(
    HTML(
        ChameleonTemplate(
            tag="ul",
            tal_statements=(
                TalStatement("content", "structure list_items",),
            ),  # structure Chameleon type keeps it as HTML
            inner_content="${list_items}",
        ).render(list_items=repeat_template.render())
    )
)
  • 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
In [28]:
%%timeit

ChameleonTemplate(
    tag="ul",
    tal_statements=(TalStatement("content", "structure list_items",),),
    inner_content="${list_items}",
).render(list_items=list_items)
40.1 ms ± 4.97 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [29]:
from chamelboots import VALID_STATEMENTS, ChameleonTemplate, TalStatement
In [30]:
tal = TalStatement("attributes", "attrib")
In [31]:
isinstance(tal, TalStatement)
Out[31]:
True
In [32]:
VALID_STATEMENTS
Out[32]:
{'define': 'tal:define',
 'switch': 'tal:switch',
 'condition': 'tal:condition',
 'repeat': 'tal:repeat',
 'case': 'tal:case',
 'content': 'tal:content',
 'replace': 'tal:replace',
 'omit-tag': 'tal:omit-tag',
 'attributes': 'tal:attributes',
 'on-error': 'tal:on-error'}
In [33]:
_tal_statements = (
    ("attributes", "attrib"),
    ("define", "company_name 'Zope Corp, Inc.'"),
)
tal_statements = [TalStatement(name, value) for name, value in _tal_statements]
In [34]:
# This is False when run inside of Class ChameleonTemplate
# I am missing something about how isinstance works.
[isinstance(item, TalStatement) for item in tal_statements]
Out[34]:
[True, True]
In [35]:
ChameleonTemplate(tag="p")
Out[35]:
<ChameleonTemplate: '<p></p>'>
In [36]:
ChameleonTemplate(tag="p", tal_statements=tal_statements).html_string
Out[36]:
'<p tal:attributes="attrib" tal:define="company_name \'Zope Corp, Inc.\'"></p>'

This demonstrates igorance on my part of "namespace" in HTML attributes.

lxml is able to namespace attributes.

I was inserting them by using string replacements on the rendered HTML string.

I knew this string replacement had a code smell about it.

from lxml docs:

Because etree is built on top of libxml2, which is namespace prefix aware, etree preserves namespaces declarations and prefixes while ElementTree tends to come up with its own prefixes (ns0, ns1, etc). When no namespace prefix is given, however, etree creates ElementTree style prefixes as well.

HTML attribute namespace on Stack Overflow

Namespace resolution of arbitrary prefixes is an XML feature, not an HTML one. So if you served your page with an application/xhtml+xml mime type, that would cause browsers to use an XML parser, and your namespace would resolve as you intend.

How to create HTML elements in xlml

The previous code I wrote did not take advantage of this convenience.

In [37]:
from lxml.html import builder as E
import lxml.html
from lxml import etree


html = E.HTML(
    E.HEAD(
        E.LINK(rel="stylesheet", href="great.css", type="text/css"),
        E.TITLE("Best Page Ever"),
    ),
    E.BODY(
        E.H1(E.CLASS("heading"), "Top News"),
        E.P("World News only on this page", style="font-size: 200%"),
        "Ah, and here's some more text, by the way.",
        lxml.html.fromstring("<p>... and this is a parsed fragment ...</p>"),
    ),
)
[(e, e.attrib,) for e in html.iter()]
Out[37]:
[(<Element html at 0x7fd2a0d26170>, {}),
 (<Element head at 0x7fd2a0d87ef0>, {}),
 (<Element link at 0x7fd2a0d876b0>,
  {'rel': 'stylesheet', 'href': 'great.css', 'type': 'text/css'}),
 (<Element title at 0x7fd2a0d0c890>, {}),
 (<Element body at 0x7fd2a0d74890>, {}),
 (<Element h1 at 0x7fd2a0d748f0>, {'class': 'heading'}),
 (<Element p at 0x7fd2a0d745f0>, {'style': 'font-size: 200%'}),
 (<Element p at 0x7fd2a0d74d10>, {})]

Bingo!

It is possible to create namedspaced HTML elements using the from lxml.html import builder.

In [38]:
import itertools as it

for attr in it.islice((item for item in dir(E) if not item.startswith("_")), 0, 20, 2):
    try:
        element_f = getattr(E, attr)(**{"tal:repeat": "item items"})
        print(etree.tostring(element_f, method="html").decode())
    except TypeError as err:
        pass
<a tal:repeat="item items"></a>
<acronym tal:repeat="item items"></acronym>
<applet tal:repeat="item items"></applet>
<base tal:repeat="item items">
<bdo tal:repeat="item items"></bdo>
<blockquote tal:repeat="item items"></blockquote>
<br tal:repeat="item items">
<caption tal:repeat="item items"></caption>
<cite tal:repeat="item items"></cite>
In [39]:
html_string = ChameleonTemplate(
    tag="p", tal_statements=tal_statements, inner_content="${foo}"
).html_string
assert "foo" in html_string, f"No inner content foo: '{html_string}'"
html_string
Out[39]:
'<p tal:attributes="attrib" tal:define="company_name \'Zope Corp, Inc.\'">${foo}</p>'
In [40]:
try:
    ChameleonTemplate(tag="p", tal_statements=tuple(range(10)))
except ValueError as err:
    print(err)
All tal_statements must be a sequence of constants.TalStatement instances
In [41]:
tal_statements  # defined above
Out[41]:
[TalStatement(name='attributes', value='attrib'),
 TalStatement(name='define', value="company_name 'Zope Corp, Inc.'")]
In [42]:
%%timeit
ChameleonTemplate(tag="p", tal_statements=tal_statements).html_string
28.9 ms ± 280 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)