Login

testdata tag for templates

Author:
showell
Posted:
May 4, 2009
Language:
Python
Version:
1.0
Tags:
templates testing doctest custom-template-tag
Score:
0 (after 0 ratings)

The "testdata" tag allows you to inline test data into your templates, similar in spirit to Python doctests. There are two sections--the test data and the actual template to be rendered. In non-test mode your template renders normally from whatever views call it, and there is very little overhead to skip over the test data section (happens at parse time).

Here are the goals:

  1. Provide convenient way to test templates without surrounding infrastructure.
  2. Make templates be self-documenting in terms of expected data.
  3. Allow insertion of test data at arbitrary places in template structure.

Hello-world looks like this:

{% load handytags %}
{% testdata %}
    {
        'greeting': 'Hello',
        'planet': 'World',
    }
{% --- %}
    {# This is where the actual template begins #}
    {{ greeting }} <b>{{ planet }}</b>
{% endtestdata %}

To invoke it, set up urls.py with something like this:

url(r'^testdata/(?P<template_path>.*)', test_template)

def test_template(request, template_path):
    context = {'testdata_use': True}
    # put request vars into context to help choose
    # which test data we want to render
    for field in request.GET:
        context[field] = request.GET[field]
    return render_with_request(template_path, context, request)

Then call:

http://127.0.0.1:8000/testdata/hello_world.html

Features:

  1. The testdata tag's rendering will expose missing variables a bit more aggressively than Django normally does.
  2. You have the full power of the template language to set the test data (which ultimately gets eval'ed as a Python expression).
  3. As mentioned above, the tag is mostly unobtrusive.

Limitations/caveats:

  1. Right now the only data format I support is pure Python, but the tag could be modified pretty easily to support JSON or YAML.
  2. The VerboseContext class is pretty heavy-handed--I really just want a hook into Django to tell it to render a section with more strictness about variables. Suggestions welcome.
  3. You can put the testdata tag pretty much anywhere, but the normal rules apply...for example, if you are in a template that has the extend tag, you'll want to put the testdata tag in individual blocks.
 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
{% load handytags %}
Example code for testdata tag
<hr />
{% testdata %}
	{% if earth %}
		{
	    	'greeting': 'Hello',
	    	'planet': 'World',
		}
	{% endif %}
	{% if mars %}
		{
	    	'greeting': 'Aloha',
	    	'planet': 'Mars',
		}
	{% endif %}
{% --- %}
	{# This is where the actual template begins #}
	{{ greeting }} <b>{{ planet }}</b>
{% endtestdata %}

################

class VerboseContext(object):
    # We proxy the context to allow us to 
    # intercept things that it quiets down.
    def __init__(self, dict, context):
        self.dict = dict
        self.context = context
        self.autoescape = context.autoescape

    def push(self): return self.context.push()
    def pop(self): return self.context.pop()

    def __setitem__(self, key, value):
        self.context[key] = value
        
    def __getitem__(self, key):
        if key in self.dict:
            value = self.dict[key]
        elif key in self.context:
            value = self.context[key]
        else:
            raise TemplateSyntaxError('%s NOT FOUND' % key)
        return value
    
    def __contains__(self, key):
        if key not in ['forloop',]:
            return True # so we can complain later
        return key in self.context
    
class TestDataNode(Node):
    def __init__(self, nodelist, data_nodelist):
        self.nodelist = nodelist
        self.data_nodelist = data_nodelist
    
    def render(self, context):
        if 'testdata_use' in context and context['testdata_use']:
            rendered_text = ''
            # We are treating data as a template,
            # mostly to allow includes to work, but people
            # can be creative.
            data_text = self.data_nodelist.render(context).strip()
            if not data_text:
                raise TemplateSyntaxError, "no data provided for testdata tag"
            raw_data = eval(data_text)
            proxy_context = VerboseContext(raw_data, context)
            rendered_text += self.nodelist.render(proxy_context)
            return rendered_text    
        else:
            # From normal code paths we just render the inner stuff
            # with the existing context.
            return self.nodelist.render(context)

@register.tag
def testdata(parser, token):
    bits = token.split_contents()
    if len(bits) != 1:
        raise TemplateSyntaxError, "%r tag takes no parameters" % bits[0]
    data_nodelist = parser.parse(('---',))
    parser.delete_first_token()
    nodelist = parser.parse(('endtestdata',))
    parser.delete_first_token()
    return TestDataNode(nodelist, data_nodelist)

More like this

  1. Repeat blocks with new context / simple Jinja-like macro system by miracle2k 7 years, 11 months ago
  2. Pretty Print Template Tag by paulvrutledge 1 year, 1 month ago
  3. outliner template tag by showell 6 years, 2 months ago
  4. Paginator TemplateTag by trbs 7 years, 3 months ago
  5. Markdown and Syntax Highlighting in Django by blinks 8 years, 3 months ago

Comments

Please login first before commenting.