""" T is new Python template language inspired by James Casbon's:
https://gist.github.com/1461441
Extremely useful in situations requiring generation of HTML within
python code.
Nothing new to learn, does not 'invent' a new language or DSL and as
pythonic as it can be.
"""
from string import Template
import datetime
TAB = " "
class T(object):
""" A template object has a name, attributes and contents.
The contents may contain sub template objects.
Attributes are kept in order.
1. use the '<' operator to add content to a template object.
2. element attributes can be set in the following ways:
body.style = "some style"; where body is an element object
body.h1(style = "style for h1 element"); where body is an element type.
'class' and 'id' are 2 attributes in very common use
and can be passed as positional arguments to the element
constructor:
body.h1("someclass", "someid"); where body is an element object.
Unfortunately element attributes could occasionally have a
form which is not a valid python identifier. Such attributes
may be set using the element method .set() or provided in a
dict 'attr' in the element constructor:
body._set('non-valid-name', 'attribute_value') or
body.h1(attr = {'non-valid-name': 'attribute_value'})
"""
def __init__(self, name = None, enable_interpolation = False):
""" 'name' of element. Root object will usually have an emoty name.
'enable_interpolation' enables string substitution to the
final document using the rules of the standard python
library string.Template. If enabled the ._render(**
parameters) method applies the '** parameters' received
to the string.Template object.
"""
self.__name = name
self.__multi_line = False
self.__contents = []
self.__attributes = []
self.__enable_interpolation = enable_interpolation
def __open(self, level = -1, **namespace):
out = ["{0}<{1}".format(TAB * level, self.__name)]
for (name, value) in self.__attributes:
out.append(' {0}="{1}"'.format(name, value))
out.append(">")
if self.__multi_line:
out.append("\n")
templ = ''.join(out)
if self.__enable_interpolation:
txt = Template(templ).substitute(** namespace)
else:
txt = templ
return txt
def __close(self, level = -1, **namespace):
if self.__multi_line:
txt = "\n{0}</{1}>\n".format(TAB * level, self.__name)
else:
txt = "</{0}>\n".format(self.__name)
return txt
# public API
def _render(self, level = -1, **namespace):
out = []
out_contents = []
contents = self.__contents
for item in contents:
if item is None:
continue
## do some default type conversions here
if isinstance(item, T):
self.__multi_line = True
out_contents.append(item._render(level = level + 1, **namespace))
elif type(item) is datetime.datetime:
out_contents.append(item.strftime("%Y-%m-%d %H:%M:%S"))
elif type(item) is float or type(item) is int:
out_contents.append(str(item))
## assume string or string.Template
else:
if self.__enable_interpolation:
txt = Template(item).substitute(**namespace)
else:
txt = item
out_contents.append(
"{0}{1}".format(
TAB * level,
txt,
)
)
txt_contents = ''.join(out_contents)
if not self.__multi_line:
txt_contents = txt_contents.strip()
else:
txt_contents = txt_contents.rstrip()
if self.__name:
out.append(self.__open(level, **namespace))
out.append(txt_contents)
out.append(self.__close(level, **namespace))
else:
out.append(txt_contents)
return ''.join(out)
def __getattr__(self, name):
t = self.__class__(
name,
enable_interpolation = self.__enable_interpolation,
)
self < t
return t
def __setattr__(self, name, value):
if name.startswith('_'):
self.__dict__[name] = value
else:
## everything else is an element attribute
## strip trailing underscores
self.__attributes.append((name.rstrip('_'), value))
def _set(self, name, value):
""" settings of attributes when attribure name is not a valid python
identifier.
"""
self.__attributes.append((name.rstrip('_'), value))
def __lt__(self, other):
self.__contents.append(other)
return self
def __call__(self, _class = None, _id = None, attr = None, **kws):
other = {}
if attr:
other.update(attr)
if kws:
other.update(kws)
# explcitly providing the class and id attributes has priority
# over dict provided info.
if _class:
other.pop('class', None)
self._set('class', _class)
if _id:
other.pop('id', None)
self._set('id', _id)
if other:
keys = other.keys()
keys.sort()
for key in keys:
self._set(key, other[key])
return self
## with interface
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
return False
def example():
doc = T(enable_interpolation = True)
doc < """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
\n"""
## we can create a second template object and add it to any other template object.
footer = T()
with footer.div('footer', 'foot1').h3.p.pre as pre:
pre.style = 'some style'
pre < 'Copyright T inc'
with doc.html as html:
with html.head as head:
## element attributes are set the usual way.
head.title = 'Good morning ${name}!'
with html.body as body:
## there is no need to use the with statement. It is useful for
## provide=ing structure and clarity to the code.
body.h3('main', attr = {'non-valid-python-attribute-name': 'warning'}) < "Header 3"
body.h4('main', valid_python_name = "ok") < "Header 4"
body.h5('main', valid_python_name = "ok", attr = {'non-valid-python-attribute-name': 'warning'}) < "Header 5"
## with statement
with body.p as p:
p.class_ ="some class"
p < "First paragraph"
## same as above but without the 'with' statement
body.p("some class") < "First paragraph"
with body.ul as ul:
for i in range(10):
ul.li < str(i)
with body.p as p:
p < "test inline html"
p.b("bold")
## append a template object
body < footer
return doc
if __name__ == "__main__":
doc = example()
html = doc._render(name = 'Clio')
print html
Diff to Previous Revision
--- revision 3 2013-04-21 17:12:43
+++ revision 4 2013-06-21 14:47:21
@@ -19,19 +19,34 @@
class T(object):
- """ A template object has a name, attributes and content.
+ """ A template object has a name, attributes and contents.
The contents may contain sub template objects.
Attributes are kept in order.
- The only things one has to remember:
-
- * use the '<' operator to add content to a template object.
-
- * pass element attributes that are not valid python names in
- the constructor of an element or use the ._set() method.
+ 1. use the '<' operator to add content to a template object.
+
+ 2. element attributes can be set in the following ways:
+
+ body.style = "some style"; where body is an element object
+ body.h1(style = "style for h1 element"); where body is an element type.
+
+ 'class' and 'id' are 2 attributes in very common use
+ and can be passed as positional arguments to the element
+ constructor:
+
+ body.h1("someclass", "someid"); where body is an element object.
+
+ Unfortunately element attributes could occasionally have a
+ form which is not a valid python identifier. Such attributes
+ may be set using the element method .set() or provided in a
+ dict 'attr' in the element constructor:
+
+ body._set('non-valid-name', 'attribute_value') or
+ body.h1(attr = {'non-valid-name': 'attribute_value'})
+
"""
def __init__(self, name = None, enable_interpolation = False):
@@ -40,9 +55,9 @@
'enable_interpolation' enables string substitution to the
final document using the rules of the standard python
- library string.Template. If enabled the ._render() method
- applies any parameters passed to the string.Template
- object.
+ library string.Template. If enabled the ._render(**
+ parameters) method applies the '** parameters' received
+ to the string.Template object.
"""
@@ -96,7 +111,7 @@
continue
## do some default type conversions here
- if type(item) is T:
+ if isinstance(item, T):
self.__multi_line = True
out_contents.append(item._render(level = level + 1, **namespace))
@@ -169,14 +184,28 @@
return self
- def __call__(self, _class = None, _id = None, **kws):
+ def __call__(self, _class = None, _id = None, attr = None, **kws):
+
+ other = {}
+ if attr:
+ other.update(attr)
+ if kws:
+ other.update(kws)
+
+ # explcitly providing the class and id attributes has priority
+ # over dict provided info.
if _class:
+ other.pop('class', None)
self._set('class', _class)
if _id:
- self.id = _id
-
- for (key, value) in kws.items():
- self._set(key, value)
+ other.pop('id', None)
+ self._set('id', _id)
+
+ if other:
+ keys = other.keys()
+ keys.sort()
+ for key in keys:
+ self._set(key, other[key])
return self
@@ -218,7 +247,11 @@
## there is no need to use the with statement. It is useful for
## provide=ing structure and clarity to the code.
- body.h3('main') < "Header 3"
+ body.h3('main', attr = {'non-valid-python-attribute-name': 'warning'}) < "Header 3"
+
+ body.h4('main', valid_python_name = "ok") < "Header 4"
+
+ body.h5('main', valid_python_name = "ok", attr = {'non-valid-python-attribute-name': 'warning'}) < "Header 5"
## with statement
with body.p as p: