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

This is a simple template engine which allows you to replace macros within text. This engine allows for attributes and filters. The default implementation provides the entire string module as filters. Trying to use arguments will of course not work (since the framework supports no other arguments for the filter other than the filtered string itself).

Python, 127 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
"""
Name-Value Object: $obj.attr.attr2|filter|filter2|filterN
Numbered Argument: $[0].attr.attr2|filter|filter2|filterN

To create new filters, subclass and create do_ method.
do_ALL is the general method for any filters not found. It should raise FilterError if the
filter is still not available.

Use $$ to avoid replacement
"""

import re
from string import capwords

class FilterError(Exception): pass

r_macro = re.compile(r"""
                    \$                                                  # must start with $, not $$
                    (                                                   # start group
                    \[\d+\][.|][a-zA-Z0-9]+[a-zA-Z0-9|.]*               # opening [number] + attrs / filters
                    |                                                   # 2nd case
                    \[\d+\]                                             # opening [number] + filters
                    |                                                   # 3rd case
                    [a-zA-Z][a-zA-Z0-9]+[.|][a-zA-Z0-9]+[a-zA-Z0-9|.]*  # obj + attrs / filters
                    |                                                   # 4th case
                    [a-zA-Z][a-zA-Z0-9]+                                # obj + attrs / filters
                    )                                                   # done
                    """, re.VERBOSE)

class Template:

    def __init__(self, string=''):
        self.buffer = string
        self.filterregister = {'length': len}

    def load(self, string):
        self.buffer += string

    def register_filter(self, name, func):
        self.filterregister[name] = func

    def remove_filter(self, name):
        func = self.filterregister[name]
        del self.filterregister[name]
        return func

    def do_capwords(self, string):
        return capwords(string)

    def do_capfirst(self, string):
        return string[0].capitalize() + string[1:]

    def do_ALL(self, string, filter_name):
        """General filter for all filter types not found"""
        if hasattr(str, filter_name):
            return getattr(str, filter_name)(string)
        else:
            raise FilterError('No such filter as %r'%filter_name)

    def render(self, *args, **kwargs):
        """Render the template, replacing macros along the way"""
        bufferstring = str(self.buffer)
        for matchedstring in r_macro.findall(self.buffer):
            # this list should contain the name of the object first, then the attrs needed
            obj_and_attrs = matchedstring.split('.')
            # remove the filters from the attr string
            obj_and_attrs[-1] = obj_and_attrs[-1].split('|', 1)[0]
            obj, attrs = obj_and_attrs[0], obj_and_attrs[1:]

            if obj.startswith('['):
                index = int(obj[1:-1])
                if not index < len(args):
                    #raise ValueError("No object at index %s!"%index)
                    bufferstring = bufferstring.replace('$'+matchedstring, '')
                    continue
                else:
                    obj = args[index]
            
            else:
                if obj not in kwargs:
                    #raise ValueError("No object with name %r"%obj)
                    bufferstring = bufferstring.replace('$'+matchedstring, '')
                    continue
                obj = kwargs[obj]

            value = obj
            if len(attrs) > 0:
                for attr in attrs:
                    if not attr:
                        raise ValueError("Cannot have empty attr!")
                    try:
                        value = getattr(value, attr)
                    except AttributeError:
                        value = value[attr]

            filters = matchedstring.split('|')[1:]

            for filtername in filters:
                if not filtername:
                    raise ValueError("Cannot have empty filter!")
                try:
                    value = getattr(self, "do_"+filtername)(value)
                except AttributeError:
                    try:
                        value = self.filterregister[filtername](value)
                    except KeyError:
                        value = self.do_ALL(value, filtername)

            bufferstring = bufferstring.replace('$'+matchedstring, str(value))
                    
        return bufferstring.replace('$$', '$').replace('\.', '.')


def render_from_string(templatestring, *args, **kwargs):
    """Shortcut function"""
    t = Template(templatestring)
    return t.render(*args, **kwargs)
    
if __name__ == "__main__":
    tempstr = """Hello $fullname|capwords,
I am writing to inform you that your child, $firstname|capitalize $lastname|capitalize has recieved
a grade of $grade% in this $coursename course. I strongly believe that your child has much potential
and could do much better if he/she tried.

I require $$100.00 for the field trip next week. We will be going to $[0] on $[1]\."""

    print render_from_string(tempstr, "Edworthy Park", "Tuesday", fullname="Bob joe", firstname="James", lastname="Clark maxwell", grade=75, coursename="Astronomy")

All of the below documentation is provided (in less words) within the source code. That way you can always refer to it.

This engine seeks out dollar signs ($) within the text. Use $objname to access named objects. Use $[0] to access numbered arguments. See examples below. To create new filters, subclass Template and create a new do_ method. do_ALL is the general filter method and should raise FilterError when it cannot complete the filter.

Use $$ to avoid replacement.

Ex: Name-Value Object:

$obj.attr.attr2|filter|filter2|filterN

Numbered Argument:

$[0].attr.attr2|filter|filter2|filterN

Here is a full example:

tempstr = """Hello $fullname|capwords, I am writing to inform you that your child, $firstname|capitalize $lastname|capitalize has recieved a grade of $grade% in this $coursename course. I strongly believe that your child has much potential and could do much better if he/she tried.\n\nI require $$100.00 for the field trip next week. We will be going to $[0] on $[1]\."""

print render_from_string(tempstr, "Edworthy Park", "Tuesday", fullname="Bob joe", firstname="James", lastname="Clark maxwell", grade=75, coursename="Astronomy")

Executing the example 1 million times takes 55.315 seconds. Which means that executing it 100 times will take about .55315 seconds.

>>> timeit('render_from_string(tempstr, "Edworthy Park", "Tuesday", fullname="Bob joe", firstname="James", lastname="Clark maxwell", grade=75, coursename="Astronomy")', "from template import render_from_string\n" r"tempstr = 'Hello $fullname|capwords,\nI am writing to inform you that your child, $firstname|capitalize $lastname|capitalize has recieved\na grade of $grade% in this $coursename course. I strongly believe that your child has much potential\nand could do much better if he/she tried.\n\nI require $$100.00 for the field trip next week. We will be going to $[0] on $[1].'")
55.315001132816704