/
render.py
191 lines (161 loc) · 7.07 KB
/
render.py
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
# Copyright (c) 2011, Daniel Crosta
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
__all__ = ('return_response', 'template_filter', 'Template',
'RenderEngine', 'InvalidTemplate')
import compiler
from compiler.ast import Import, From
import jinja2
import os, os.path
class InvalidTemplate(Exception):
"""Indicates that a .ks template has more than one separator."""
class TemplateNotFound(Exception):
"""Indicates that a .ks template by the given name does not exist."""
class StopViewFunc(Exception):
"""Raised by return_response() to prevent template rendering."""
def __init__(self, body):
self.body = body
def return_response(body):
"""Passed into viewlocals to allow view code to immediately
respond, bypassing templates (e.g. to return binary content
from a file or database).
"""
if isinstance(body, basestring):
body = (body, )
raise StopViewFunc(body)
def template_filter(func):
"""Register a Jinja2 filter function. The name of the function
will become the name of the filter in the template environment.
"""
# by the time this is called (from within Python modules in the
# application, the RenderEngine, and thus the Jinja Environment,
# have already been created
jinja_env.filters[func.__name__] = func
return func
class Template(object):
"""Holds a template body, viewfunc, mtime, and valid methods."""
def __init__(self, viewfunc, body, mtime=None, name=None):
self.viewfunc = viewfunc
self.body = body
self.mtime = mtime
self.name = name
self.urlparams = {}
def copy(self):
return Template(self.viewfunc, self.body, self.mtime, self.name)
jinja_env = None
class RenderEngine(object):
def __init__(self, app):
self.app = app
self.templates = {}
global jinja_env
jinja_env = jinja2.Environment(
loader=jinja2.FunctionLoader(self.get_template_body))
def parse(self, fileobj):
"""Parse a .ks file into a view callable and a template
string. If there is no separator ("----") then the first
part is treated as the template, and the view callable is
a no-op.
"""
first, second = [], []
active = first
for lineno, line in enumerate(fileobj):
if line.strip() == '----':
if active is second:
raise InvalidTemplate(
'Line %d: separator already seen on line %d' % (lineno, len(first)))
active = second
else:
active.append(line)
if active is first:
return Template(
viewfunc=lambda x: x,
body=''.join(first))
viewcode, viewglobals = self.compile(''.join(first), fileobj.name)
def viewfunc(viewlocals):
exec viewcode in viewglobals, viewlocals
return viewlocals
return Template(
viewfunc=viewfunc,
body=''.join(second))
def compile(self, viewcode_str, filename):
"""Compile the view code and return a code object
and dictionary of globals needed by the code object.
"""
viewcode = compile(viewcode_str, filename, 'exec')
# scan top-level code only for "import foo" and
# "from foo import *" and "from foo import bar, baz"
viewglobals = {'__builtins__': __builtins__}
for stmt in compiler.parse(viewcode_str).node:
if isinstance(stmt, Import):
modname, asname = stmt.names[0]
if asname is None:
asname = modname
viewglobals[asname] = __import__(modname)
elif isinstance(stmt, From):
fromlist = [x[0] for x in stmt.names]
module = __import__(stmt.modname, {}, {}, fromlist)
for name, asname in stmt.names:
if name == '*':
for starname in getattr(module, '__all__', dir(module)):
viewglobals[starname] = getattr(module, starname)
else:
if asname is None:
asname = name
viewglobals[asname] = getattr(module, name)
return viewcode, viewglobals
def refresh_if_needed(self, name):
"""Update the cached modification time, view func,
and template body for the .ks template at the given
path relative to the app_dir."""
filename = os.path.abspath(os.path.join(self.app.app_dir, name))
if not os.path.isfile(filename):
raise TemplateNotFound('could not find template %s' % name)
mtime = os.stat(filename).st_mtime
template = self.templates.get(name)
if template is None or template.mtime < mtime:
template = self.parse(file(filename, 'rb'))
template.mtime = mtime
template.name = name
self.templates[name] = template
def render(self, template, viewlocals):
"""Template rendering entry point."""
jinja_template = jinja_env.get_template(template.name)
viewlocals.update(template.urlparams)
try:
return jinja_template.generate(**template.viewfunc(viewlocals))
except StopViewFunc, stop:
return stop.body
def get_template(self, name):
self.refresh_if_needed(name)
return self.templates.get(name)
def get_template_body(self, name):
"""Jinja2 template loader function."""
self.refresh_if_needed(name)
template = self.templates[name]
cached_mtime = template.mtime
def uptodate():
self.refresh_if_needed(name)
template = self.templates[name]
return template and template.mtime and cached_mtime and template.mtime <= cached_mtime
return template.body, name, uptodate