Welcome, guest | Sign In | My Account | Store | Cart

I was looking for a simple way to generate HTML directly in python that does not require learning a new template 'language' nor requires the installation of a big complex package. Closest thing I found was James Casbon's attempt(https://gist.github.com/1461441). This is my version of the same idea.

(2013-04-21) added some simplifications and support for switching off string interpolation. Added to github:

https://github.com/pavlos-christoforou/web

Python, 284 lines
  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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
""" 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
    

A very pythonic way to generate HTML that places few restrictions and makes as few assumptions as possible.

1 comment

amir naghavi 11 years ago  # | flag

very nice

Created by Pavlos on Mon, 28 Jan 2013 (MIT)
Python recipes (4591)
Pavlos's recipes (1)

Required Modules

Other Information and Tasks