ActiveState Code

Recipe 496702: Templite


A light-weight, fully functional, general purpose templating engine, in just 40 lines of code

Python
  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
import re


class Templite(object):
    delimiter = re.compile(r"\$\{(.*?)\}\$", re.DOTALL)
    
    def __init__(self, template):
        self.tokens = self.compile(template)
    
    @classmethod
    def from_file(cls, file):
        """
        loads a template from a file. `file` can be either a string, specifying
        a filename, or a file-like object, supporting read() directly
        """
        if isinstance(file, basestring):
            file = open(file)
        return cls(file.read())
    
    @classmethod
    def compile(cls, template):
        tokens = []
        for i, part in enumerate(cls.delimiter.split(template)):
            if i % 2 == 0:
                if part:
                    tokens.append((False, part.replace("$\\{", "${")))
            else:
                if not part.strip():
                    continue
                lines = part.replace("}\\$", "}$").splitlines()
                margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip())
                realigned = "\n".join(l[margin:] for l in lines)
                code = compile(realigned, "<templite %r>" % (realigned[:20],), "exec")
                tokens.append((True, code))
        return tokens
    
    def render(__self, __namespace = None, **kw):
        """
        renders the template according to the given namespace. 
        __namespace - a dictionary serving as a namespace for evaluation
        **kw - keyword arguments which are added to the namespace
        """
        namespace = {}
        if __namespace: namespace.update(__namespace)
        if kw: namespace.update(kw)
        
        def emitter(*args):
            for a in args: output.append(str(a))
        def fmt_emitter(fmt, *args):
            output.append(fmt % args)
        namespace["emit"] = emitter
        namespace["emitf"] = fmt_emitter
        
        output = []
        for is_code, value in __self.tokens:
            if is_code:
                eval(value, namespace)
            else:
                output.append(value)
        return "".join(output)
    
    # shorthand
    __call__ = render

---------
example:
---------
>>> from templite import Templite
>>>
>>> demo = r"""
... <html>
...     <body>
...         ${
...         def say_hello(arg):
...             emit("hello ", arg, "<br>")
...         }$
...
...         <table>
...             ${
...                 for i in range(10):
...                     emit("<tr><td> ")
...                     say_hello(i)
...                     emit(" </tr></td>\n")
...             }$
...         </table>
...
...         ${emit("hi")}$
...
...         tralala ${if x > 7:
...             say_hello("big x")}$ lala
...
...         $\{this is escaped starting delimiter
...
...         ${emit("this }\$ is an escaped ending delimiter")}$
...
...         ${# this is a python comment }$
...
...     </body>
... </html>
... """
>>>
>>> t = Templite(demo)
>>> print t(x = 8)

<html>
    <body>


        <table>
            <tr><td> hello 0<br> </tr></td>
<tr><td> hello 1<br> </tr></td>
<tr><td> hello 2<br> </tr></td>
<tr><td> hello 3<br> </tr></td>
<tr><td> hello 4<br> </tr></td>
<tr><td> hello 5<br> </tr></td>
<tr><td> hello 6<br> </tr></td>
<tr><td> hello 7<br> </tr></td>
<tr><td> hello 8<br> </tr></td>
<tr><td> hello 9<br> </tr></td>

        </table>

        hi

        tralala hello big x<br> lala

        ${this is escaped starting delimiter

        this }$ is an escaped ending delimiter



    </body>
</html>

>>>

Discussion

Templite -- A light-weight, fully functional, general purpose templating engine, allowing you to embed python code directly into your text. This engine is suitable for any templating (not only HTML/XML), and is minimal (40 lines of code!) and fast (all preprocessing is done in "compile time")

All text between ${ and }$ is considered python code, and is evaluated when the Templite is rendered. You can escape the ${ delimiter by $\{ and the }$ delimiter by }\$.

Emitting output is done with the emit() function, which accepts any number of arguments, converts them to a string, and appends to the output where the template was located.

Security notice: IT'S NOT SECURE, as the template-generating code is arbitrary python code. So be sure you don't just evaluate user-provided Templites, at least not before taking a look at them. It's meant to be a light and fast templating engine, for trusted server side code (generating reports, etc.).

Comments

  1. 1. At 8:46 a.m. on 15 may 2006, Oscar Micheli said:

    There are a couple of errors in published code. I've found in the following lines a missing '[' and ']' in string comprehension

    1. padding = min([len(l) - len(l.lstrip()) for l in lines if l.strip()])

    2. unpadded = "\n".join([l[padding:] for l in lines])

    Thanks for the code. Nice indeed

    Oscar

  2. 2. At 2:48 p.m. on 15 may 2006, Ori Peleg said:

    Shrewd! And totally cool.

  3. 3. At 9:52 a.m. on 16 may 2006, tomer filiba (the author) said:

    these are generator expressions. python 2.4 has generator expressions, i.e.

    >>> (x for x in range(10))
    

    <generator object at 0x009ED0F8>

    -tomer

  4. 4. At 11:42 p.m. on 17 may 2006, Tal Einat said:

    Escaping delimiters. Having to escape only '${' outside code and only '}$' inside code can be a bit confusing, and IMO should be more noticeably and clearly noted in the documentation (or changed). I admit that the choice of delimiters is such that not much escaping will probably be needed in day-to-day use; still, someone getting the escaping wrong could have a time understanding why.

    Personally I would prefer to have to escape both '${' and '}$' everywhere, for better uniformity. Of course, this is a personal preference.

  5. 5. At 10:28 p.m. on 6 aug 2006, Josiah Carlson said:

    Very nice. That's just the right amount of markup for embedding Python into html documents.

  6. 6. At 3:39 p.m. on 17 sep 2006, Ehud Ben-Reuven said:

    exec instead of eval. According to Python doc "eval(value, namespace)" should only be used for value compiled as "eval" and for "exec" you should use instead "exec value in namespace". Which worked.

Sign in to comment