The other day I was complaining about writing html, forms, etc., for Python cgi and/or web programming. I had pointed out a selection of three examples, the first of which ended up being very much like Nevow.stan . Thinking a bit about it, I realized that stan had issues in that you couldn't really re-use pre-defined tags with attributes via map, and keyword arguments were just too darn convenient to swap the calling and getitem syntax.
Instead, I hacked together a mechanism that supports: T.tagname("content", T.tagname(...), ..., attr1='value', ...) T.tagname(attr1='value', ...)("content", T.tagname(...), ...) x = T.tagname(attr1='value', ...) y = T.tagname(*map(x, ['content', ...])) ... and many other options.
Essentially, you can mix and match calls as much as you want, with three memory and sanity saving semantics: 1. Creating a new tag object via T.tagname, or any call of such, will create a shallow copy of the object you are accessing. 2. smallred = T.font(size='-1', color='red');bigred = smallred(size='+1') Works exactly the way you expect it to. If it doesn't work the way you expect it to, then your expectations are confused. 3. If you are adding content that sites within the tag, the content is replaced, not updated, like attributes.
This simple version handles auto-indentation of content as necessary (or desireable), auto-escaping of text elements, and includes an (I believe) nearly complete listing of entities which don't require closing tags.
I don't know where this is going, whether it can or will expand into something more, or what, but I believe what I have managed to hack together is better than other similar packages available elsewhere (including this recipe over here http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/366000 , which I discovered after writing my own). Funny how these things work out. Astute observers will note that I borrow nevow.stan's meme of using T.tagname for generating tag objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | import sys
import cStringIO
#handle character escaping...
import re
from htmlentitydefs import codepoint2name
character2name = {}
for i,j in codepoint2name.iteritems():
if i <= 127:
character2name[chr(i)] = '&%s;'%j
else:
character2name[unichr(i)] = '&#%d;'%i
del i;del j;del codepoint2name
escape = re.compile('(%s)'%('|'.join(list(character2name))))
def repl(matchobj):
return character2name.get(matchobj.group(0), '?')
#handle special tags
no_ends = dict.fromkeys(('br p input img area base basefont col '
'frame hr isindex link meta param iframe').split())
no_escape = dict.fromkeys('script raw'.split())
raw = dict.fromkeys('pre'.split())
#the base tag generator
class T(object):
def __getattr__(self, tagname):
return tag(tagname)
T = T()
class tag(object):
__slots__ = ['name', 'attrs', 'contents']
def __init__(self, name, attrs=None, contents=None):
self.name = name.lower()
self.attrs = attrs
self.contents = contents
def __call__(self, *args, **kwargs):
if kwargs and self.attrs:
d = dict(self.attrs)
d.update(kwargs)
kwargs = d
__klass = kwargs.pop('klass', None)
if __klass:
kwargs['class'] = __klass
if args and kwargs:
return tag(self.name, kwargs, args)
elif kwargs:
return tag(self.name, kwargs, self.contents)
elif args:
return tag(self.name, self.attrs, args)
return self
def __setitem__(self, key, value):
if isinstance(key, basestring):
if self.attrs is None:
self.attrs = {}
self.attrs[key] = value
else:
raise TypeError('attribute assignments must only be to named attributes')
def __getitem__(self, key):
if isinstance(key, (int, long)):
if not self.contents:
raise IndexError('tuple index out of range')
return self.contents[key]
raise TypeError('content fetch must only be from indexed attributes')
def render(self, where=None, called=0):
if where is None:
x = cStringIO.StringIO()
self.render(x)
x.seek(0)
return x.read()
if self.name != 'raw':
if self.attrs:
x = []
for key, value in self.attrs.iteritems():
x.append("%s='%s'"%(key, value))
where.write('\n' + called*' ' + '<%s %s>'%(self.name, ' '.join(x).encode('utf-8')))
else:
where.write('\n' + called*' ' + '<%s>'%self.name)
x = where.tell()
if self.contents:
c2n = character2name
for i in self.contents:
if hasattr(i, 'render'):
i.render(where, called+1)
elif self.name in no_escape:
where.write(str(i).encode('utf-8'))
else:
st = str(i)
chrs = dict.fromkeys(st)
for i in chrs:
if i in c2n:
break
else:
chrs = None
if chrs:
#we found something that needs to be translated
st = escape.sub(repl, st)
where.write(st.encode('utf-8'))
if self.name != 'raw' and self.name not in no_ends:
if self.name not in raw and where.tell()-x > 25:
where.write('\n' + called*' ' +'</%s>'%self.name)
else:
where.write('</%s>'%self.name)
if not called:
where.write('\n')
'''
>>> print T.html(
... T.body(
... "hello world", T.br, "how are you?", T.br,
... T.table(*[T.tr(*map(T.td, map(str, range(0+i, 3+i)))) for i in xrang
e(3)])
... )).render()
<html>
<body>hello world
<br>how are you?
<br>
<table>
<tr>
<td>0</td>
<td>1</td>
<td>2</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>3</td>
<td>4</td>
</tr>
</table>
</body>
</html>
>>>
>>> x = T.html(
... T.body(bgcolor='red')(
... T.font(size='+1')('Welcome to this wonderful web page!'),
... T.br, "How are you doing today?",
... T.br, T.input(type='text', size='25', value='say something')
... )).render()
>>> print x
<html>
<body bgcolor='red'>
<font size='+1'>Welcome to this wonderful web page!
</font>
<br>How are you doing today?
<br>
<input type='text' value='say something' size='25'>
</body>
</html>
>>>
>>> print T.html(
... T.body(
... T.pre(x))).render()
<html>
<body>
<pre>
<html>
<body bgcolor='red'>
<font size='+1'>Welcome to this wonderful web page!
</font>
<br>How are you doing today?
<br>
<input type='text' value='say something' size='25'>
</body>
</html>
</pre>
</body>
</html>
>>>
>>> def generate_something():
... return T.b("I was generated from a function")
...
>>> print T.html(T.body(generate_something())).render()
<html>
<body>
<b>I was generated from a function
</b>
</body>
</html>
>>>
'''
|
After describing a similar syntax to the above, and seeing Nevow.stan, I took some time and hacked together the above. After finishing, I took a wander through the cookbook and found a few recipes, links, etc., many of whom implement a very similar method, though none really manage to capture multiple calling semantics, and/or the very convenient re-use of pre-attributed tags as I do.
With the use of the non-XHTML tag of 'raw', one can pass through pre-generated html (perhaps embedded ReST -> html, etc.), sets of containers of objects, and various other interesting things. One could even signal to a form processor or somesuch that a particular input needs to be bounds checked on return, etc.
Attributes that are Python keywords. I'd like to use the HTML generated from this recipe with CSS classes. The problem is that you can't do T.tag(class="whatevercssclass") because "class" is a Python keyword and using it this way generates an error. Is there a clean way to get around this?
to add attribute 'class' add this chunk of code after line 46 of the code:
then just do this:
spelt with a 'k' so Python does not catch it as a reserved word, and it will output with the correct class="aclass" attribute
I've added this modification to the code. Thank you.
This is a useful bit of code. I really like this recipe, and I even used it in a internal project of mine that never really went anywhere.
Before I mothball my project I wanted to share back with you my rendition of your code.
I made some small changes to the general code to add some extra name-spacing. I also dropped the need for using tell() to determine if the renderer should wrap, instead relying on counting the number of children in an element and whether those children are themselves tags.
With the dropped dependency on tell(), render() can now be used with sys.stdout passed in as the file object.
I also made some other slight changes to the HTML that is generated to make it more XHTML like (although I do no such verification of this).
Anyway, here is the code. I figure it better to post it in a comment, rather than a recipe (even though comment code formatting sucks), since this is really not a new recipe.
(comment continued...)
(...continued from previous comment)
(comment continued...)
(...continued from previous comment)