We're going to roll our own HTML table generator here. Most programmers have had fun at least once in their lives manually writing HTML. Once the practice is mastered, however, it's not something they want to continue to do - they're content to let the front-end design people handle it (if even they do any of it manually these days). But sometimes that's not an option, which is why many Python web frameworks provide HTML generation for forms, tables, and other kinds of markup.
We'll write an HTML table generator here to illustrate some interesting Python concepts such as decorators, closures, and context managers.
We'll be using Python 3 on Ubuntu.
We're going to use a decorator function to implement our table generator. A Python decorator (no relationship to the Decorator pattern) generally is a callable that accepts a function and then returns a function that calls the passed-in function. The returned function generally calls the passed-in function as one part of doing its job. If the name of the passed-in function is rebound to the function returned from the decorator, then the passed-in function is no longer directly callable - it's only callable indirectly by calling the returned function. In a sense it has been transformed or wrapped or decorated.
It's easier to demonstrate than to explain - we'll do it here with a decorator function (although a decorator class is also possible).
>>> def d(f): # d is the decorator function
... def c():
... print("entering")
... f()
... print("exiting")
... return c
...
>>> def f(): # f is the passed in function
... print("hello world")
...
>>> f()
hello world
>>> f = d(f) # manually decorate f
>>> f()
entering
hello world
exiting
Using Python's decorator syntax makes the decoration step simpler.
>>> def d(f): # d is the decorator function
... def c():
... print("entering")
... f()
... print("exiting")
... return c
...
>>> @d # automagically decorate f
... def f(): # f is the passed in function
... print("hello world")
...
>>> f()
entering
hello world
exiting
We've named the nested and returned function in the above example c
because it's a closure. A closure is a function containing a reference to an object in an enclosing namespace that existed when the function was created i.e. it's a function with state. A closure is usually created by defining a function inside another function with the inner function referencing free i.e. non-local variables and with the inner function serving as the return value of the outer function. The returned function is said to be closed over its free variables and called a closure and the enclosing namespace persists along with the closure.
When used in a decorator scenario, the closure holds a reference to the function passed to the decorator - that's how the closure can call the passed-in function.
We'll use a decorator in the same way that a lot of Python web frameworks do - the function being decorated must be "filled in" by the user in order to allow the framework to do its job. This is how a framework implements a kind of inversion-of-control (IoC).
In our case, the function must provide the data to be displayed in the table and a logical table definition. We'll simulate a database call here and explicitly create a tuple of dictionaries for our data. The logical table definition is also a tuple of dictionaries that specifies the HTML table column name and the database column name of each column to display in the table. The "protocol" key is used to create certain kinds of links within the table. We'll simply use print
to display the resulting output, although we'd normally use this code to write a response on the backend. So we'll start tg.py
with:
def gettable():
data = (
{"id":"1", "name":"homer", "fullname":"Homer Simpson", "company":"Simpson Inc.", "orgunit":"", "external":"No", "email":"homer@simpson.biz"},
{"id":"2", "name":"marge", "fullname":"Marge Simpson", "company":"Simpson Inc.", "orgunit":"", "external":"No", "email":"marge@simpson.biz"},
)
return (
data,
(
{"prompt":"Name", "name":"name", "protocol":"id"},
{"prompt":"Full Name", "name":"fullname"},
{"prompt":"Company", "name":"company"},
{"prompt":"Organizational Unit", "name":"orgunit"},
{"prompt":"External", "name":"external"},
{"prompt":"e-Mail Address", "name":"email", "protocol":"mailto"},
),
)
print(gettable())
$ python3 tg.py
(({'id': '1', 'name': 'homer', 'fullname': 'Homer Simpson', 'company': 'Simpson Inc.', 'orgunit': '', 'external': 'No', 'email': 'homer@simpson.biz'}, {'id': '2', 'name': 'marge', 'fullname': 'Marge Simpson', 'company': 'Simpson Inc.', 'orgunit': '', 'external': 'No', 'email': 'marge@simpson.biz'}), ({'prompt': 'Name', 'name': 'name', 'protocol': 'id'}, {'prompt': 'Full Name', 'name': 'fullname'}, {'prompt': 'Company', 'name': 'company'}, {'prompt': 'Organizational Unit', 'name': 'orgunit'}, {'prompt': 'External', 'name': 'external'}, {'prompt': 'e-Mail Address', 'name': 'email', 'protocol': 'mailto'}))
Calling print(gettable())
at this point simply displays the returned tuples. That's because we haven't yet decorated the function.
Our decorator function gentable
is straightforward - we call gettable
to get the data and logical table definition that we need to do the job and then write our markup. We use an io.StringIO
object for output to avoid string concatenation. We use Python 3.6's f-strings for formatted output. We don't do any styling. The closure simply returns the markup. We'll leave our body empty for now - we want to tackle that with a context manager.
import io
def gentable(f):
def c():
data, tabdef = f()
out = io.StringIO()
write = out.write # avoid dots
#####
#
# table
#
write("<table>\n")
write("<colgroup><col/></colgroup>\n")
#####
#
# thead
#
write("<thead>\n")
write("<tr>\n")
write("<td/>\n")
for coldef in tabdef:
write(f"""<td>{coldef["prompt"]}</td>\n""")
write("</tr>\n")
write("</thead>\n")
#
# thead
#
#####
#####
#
# tfoot
#
write("<tfoot>\n")
write(f"""<td colspan="{len(tabdef)}">{len(data)} object(s)</td>\n""")
write("</tfoot>\n")
#
# tfoot
#
#####
#####
#
# tbody
#
write("<tbody>\n")
write("</tbody>\n")
#
# tbody
#
#####
write("</table>\n")
#
# table
#
#####
return out.getvalue()
return c
@gentable
def gettable():
data = (
{"id":"1", "name":"homer", "fullname":"Homer Simpson", "company":"Simpson Inc.", "orgunit":"", "external":"No", "email":"homer@simpson.biz"},
{"id":"2", "name":"marge", "fullname":"Marge Simpson", "company":"Simpson Inc.", "orgunit":"", "external":"No", "email":"marge@simpson.biz"},
)
return (
data,
(
{"prompt":"Name", "name":"name", "protocol":"id", "key":"name"},
{"prompt":"Full Name", "name":"fullname", "key":"fullname"},
{"prompt":"Company", "name":"company", "key":"company"},
{"prompt":"Organizational Unit", "name":"orgunit", "key":"orgunit;name"},
{"prompt":"External", "name":"external", "key":"external"},
{"prompt":"e-Mail Address", "name":"email", "protocol":"mailto", "key":"email"},
),
)
print(gettable())
$ python3 tg.py
<table>
<colgroup><col/></colgroup>
<thead>
<tr>
<td/>
<td>Name</td>
<td>Full Name</td>
<td>Company</td>
<td>Organizational Unit</td>
<td>External</td>
<td>e-Mail Address</td>
</tr>
</thead>
<tfoot>
<td colspan="6">2 object(s)</td>
</tfoot>
<tbody>
</tbody>
</table>
We'll use a with
statement and a context manager object of class Rowgen
to generate each row of the table body. We could do this directly in gentable
, but doing it this way is more interesting :) and it keeps the code in gentable
more generic.
The with
statement requires an object that can function as a context manager, i.e. an object of a class that defines __enter__
and __exit__
methods that get called automatically before and after the execution of the code in the with
suite (block). These methods thus effectively function as wrapper methods. This is similar to the wrapping offered by decorators, and in fact a context manager can be used as a decorator.
We pass the logical table definition to a Rowgen
object in the constructor call. We then loop through our data and pass each row to the public markup
method, where most of the work is done. The __enter__
and __exit__
methods simply provide the <tr>
and </tr>
tags. We use the html
and urllib.parse
modules to handle escaping and quoting (note: some JavaScript code isn't shown).
import io
import html
import urllib.parse
class Rowgen():
def __init__(self, tabdef):
self.__tabdef = tabdef
self.__markup = ""
def __enter__(self):
self.__markup += "<tr>\n"
return self
def __exit__(self, *exc):
self.__markup += "</tr>\n"
return False
def setmarkup(self, row):
self.__markup += f"""<td><input type="checkbox" onclick="updateidlist(this.checked, '{row["id"]}')"></td>\n"""
for coldef in self.__tabdef:
if "protocol" not in coldef or coldef["protocol"] == "":
self.__markup += f"""<td>{html.escape(row[coldef["name"]])}</td>\n"""
elif coldef["protocol"] == "id":
self.__markup += f"""<td><a onclick="location = './user/{row["id"]}'; return false;" href="">{html.escape(row[coldef["name"]])}</a><td>\n"""
elif coldef["protocol"] == "mailto":
self.__markup += f"""<td><a href="mailto:{urllib.parse.quote(row[coldef["name"]])}" target="_blank">{html.escape(row[coldef["name"]])}</a></td>\n"""
def getmarkup(self):
return self.__markup
def gentable(f):
def closure():
data, tabdef = f()
out = io.StringIO()
write = out.write
#####
#
# table
#
write("<table>\n")
write("<colgroup><col/></colgroup>\n")
#####
#
# thead
#
write("<thead>\n")
write("<tr>\n")
write("<td/>\n")
for coldef in tabdef:
write(f"""<td>{coldef["prompt"]}</td>\n""")
write("</tr>\n")
write("</thead>\n")
#
# thead
#
#####
#####
#
# tfoot
#
write("<tfoot>\n")
write(f"""<td colspan="{len(tabdef)}">{len(data)} object(s)</td>\n""")
write("</tfoot>\n")
#
# tfoot
#
#####
#####
#
# tbody
#
write("<tbody>\n")
with Rowgen(tabdef) as rg:
for row in data:
rg.setmarkup(row)
write(rg.getmarkup())
write("</tbody>\n")
#
# tbody
#
#####
write("</table>\n")
#
# table
#
#####
return out.getvalue()
return closure
@gentable
def gettable():
data = (
{"id":"1", "name":"homer", "fullname":"Homer Simpson", "company":"Simpson Inc.", "orgunit":"", "external":"No", "email":"homer@simpson.biz"},
{"id":"2", "name":"marge", "fullname":"Marge Simpson", "company":"Simpson Inc.", "orgunit":"", "external":"No", "email":"marge@simpson.biz"},
)
return (
data,
(
{"prompt":"Name", "name":"name", "protocol":"id", "key":"name"},
{"prompt":"Full Name", "name":"fullname", "key":"fullname"},
{"prompt":"Company", "name":"company", "key":"company"},
{"prompt":"Organizational Unit", "name":"orgunit", "key":"orgunit;name"},
{"prompt":"External", "name":"external", "key":"external"},
{"prompt":"e-Mail Address", "name":"email", "protocol":"mailto", "key":"email"},
),
)
print(gettable())
$ python3 tg.py
<table>
<colgroup><col/></colgroup>
<thead>
<tr>
<td/>
<td>Name</td>
<td>Full Name</td>
<td>Company</td>
<td>Organizational Unit</td>
<td>External</td>
<td>e-Mail Address</td>
</tr>
</thead>
<tfoot>
<td colspan="6">2 object(s)</td>
</tfoot>
<tbody>
<tr>
<td><input type="checkbox" onclick="updateidlist(this.checked, '1')"></td>
<td><a onclick="location = './user/1'; return false;" href="">homer</a><td>
<td>Homer Simpson</td>
<td>Simpson Inc.</td>
<td></td>
<td>No</td>
<td><a href="mailto:homer%40simpson.biz" target="_blank">>homer@simpson.biz</a></td>
<tr>
<td><input type="checkbox" onclick="updateidlist(this.checked, '1')"></td>
<td><a onclick="location = './user/1'; return false;" href="">homer</a><td>
<td>Homer Simpson</td>
<td>Simpson Inc.</td>
<td></td>
<td>No</td>
<td><a href="mailto:homer%40simpson.biz" target="_blank">homer@simpson.biz</a></td>
<td><input type="checkbox" onclick="updateidlist(this.checked, '2')"></td>
<td><a onclick="location = './user/2'; return false;" href="">marge</a><td>
<td>Marge Simpson</td>
<td>Simpson Inc.</td>
<td></td>
<td>No</td>
<td><a href="mailto:marge%40simpson.biz" target="_blank">marge@simpson.biz</a></td>
</tbody>
</table>
And there we are. An HTML table generator in Python.
Share on Twitter Share on Facebook