Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/chameleon/template.py : 38%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import with_statement
3import os
4import sys
5import hashlib
6import logging
7import tempfile
8import inspect
10try:
11 RecursionError
12except NameError:
13 RecursionError = RuntimeError
15def get_package_versions():
16 try:
17 import pkg_resources
18 except ImportError:
19 logging.info("Setuptools not installed. Unable to determine version.")
20 return []
22 versions = dict()
23 for path in sys.path:
24 for distribution in pkg_resources.find_distributions(path):
25 if distribution.has_version():
26 versions.setdefault(
27 distribution.project_name,
28 distribution.version,
29 )
31 return sorted(versions.items())
34pkg_digest = hashlib.sha1(__name__.encode('utf-8'))
35for name, version in get_package_versions():
36 pkg_digest.update(name.encode('utf-8'))
37 pkg_digest.update(version.encode('utf-8'))
40from .exc import RenderError
41from .exc import TemplateError
42from .exc import ExceptionFormatter
43from .compiler import Compiler
44from .config import DEBUG_MODE
45from .config import AUTO_RELOAD
46from .config import EAGER_PARSING
47from .config import CACHE_DIRECTORY
48from .loader import ModuleLoader
49from .loader import MemoryLoader
50from .nodes import Module
51from .utils import DebuggingOutputStream
52from .utils import Scope
53from .utils import join
54from .utils import mangle
55from .utils import create_formatted_exception
56from .utils import read_bytes
57from .utils import raise_with_traceback
58from .utils import byte_string
59from .utils import value_repr
62log = logging.getLogger('chameleon.template')
65def _make_module_loader():
66 remove = False
67 if CACHE_DIRECTORY:
68 path = CACHE_DIRECTORY
69 else:
70 path = tempfile.mkdtemp()
71 remove = True
73 return ModuleLoader(path, remove)
76class BaseTemplate(object):
77 """Template base class.
79 Takes a string input which must be one of the following:
81 - a unicode string (or string on Python 3);
82 - a utf-8 encoded byte string;
83 - a byte string for an XML document that defines an encoding
84 in the document premamble;
85 - an HTML document that specifies the encoding via the META tag.
87 Note that the template input is decoded, parsed and compiled on
88 initialization.
89 """
91 default_encoding = "utf-8"
93 # This attribute is strictly informational in this template class
94 # and is used in exception formatting. It may be set on
95 # initialization using the optional ``filename`` keyword argument.
96 filename = '<string>'
98 _cooked = False
100 if DEBUG_MODE or CACHE_DIRECTORY:
101 loader = _make_module_loader()
102 else:
103 loader = MemoryLoader()
105 if DEBUG_MODE:
106 output_stream_factory = DebuggingOutputStream
107 else:
108 output_stream_factory = list
110 debug = DEBUG_MODE
112 # The ``builtins`` dictionary can be used by a template class to
113 # add symbols which may not be redefined and which are (cheaply)
114 # available in the template variable scope
115 builtins = {}
117 # The ``builtins`` dictionary is updated with this dictionary at
118 # cook time. Note that it can be provided at class initialization
119 # using the ``extra_builtins`` keyword argument.
120 extra_builtins = {}
122 # Expression engine must be provided by subclass
123 engine = None
125 # When ``strict`` is set, expressions must be valid at compile
126 # time. When not set, this is only required at evaluation time.
127 strict = True
129 # This should return a value string representation for exception
130 # formatting.
131 value_repr = staticmethod(value_repr)
133 def __init__(self, body=None, **config):
134 self.__dict__.update(config)
136 if body is not None:
137 self.write(body)
139 # This is only necessary if the ``debug`` flag was passed as a
140 # keyword argument
141 if self.__dict__.get('debug') is True:
142 self.loader = _make_module_loader()
144 def __call__(self, **kwargs):
145 return self.render(**kwargs)
147 def __repr__(self):
148 return "<%s %s>" % (self.__class__.__name__, self.filename)
150 @property
151 def keep_body(self):
152 # By default, we only save the template body if we're
153 # in debugging mode (to save memory).
154 return self.__dict__.get('keep_body', DEBUG_MODE)
156 @property
157 def keep_source(self):
158 # By default, we only save the generated source code if we're
159 # in debugging mode (to save memory).
160 return self.__dict__.get('keep_source', DEBUG_MODE)
162 def cook(self, body):
163 builtins_dict = self.builtins.copy()
164 builtins_dict.update(self.extra_builtins)
165 names, builtins = zip(*sorted(builtins_dict.items()))
166 digest = self.digest(body, names)
167 program = self._cook(body, digest, names)
169 initialize = program['initialize']
170 functions = initialize(*builtins)
172 for name, function in functions.items():
173 setattr(self, "_" + name, function)
175 self._cooked = True
177 if self.keep_body:
178 self.body = body
180 def cook_check(self):
181 assert self._cooked
183 def parse(self, body):
184 raise NotImplementedError("Must be implemented by subclass.")
186 def render(self, **__kw):
187 econtext = Scope(__kw)
188 rcontext = {}
189 self.cook_check()
190 stream = self.output_stream_factory()
191 try:
192 self._render(stream, econtext, rcontext)
193 except RecursionError:
194 raise
195 except:
196 cls, exc, tb = sys.exc_info()
197 errors = rcontext.get('__error__')
198 if errors:
199 formatter = exc.__str__
200 if isinstance(formatter, ExceptionFormatter):
201 if errors is not formatter._errors:
202 formatter._errors.extend(errors)
203 raise
205 formatter = ExceptionFormatter(errors, econtext, rcontext, self.value_repr)
207 try:
208 exc = create_formatted_exception(
209 exc, cls, formatter, RenderError
210 )
211 except TypeError:
212 pass
214 raise_with_traceback(exc, tb)
216 raise
218 return join(stream)
220 def write(self, body):
221 if isinstance(body, byte_string):
222 body, encoding, content_type = read_bytes(
223 body, self.default_encoding
224 )
225 else:
226 content_type = body.startswith('<?xml')
227 encoding = None
229 self.content_type = content_type
230 self.content_encoding = encoding
232 self.cook(body)
234 def _get_module_name(self, name):
235 return "%s.py" % name
237 def _cook(self, body, name, builtins):
238 filename = self._get_module_name(name)
239 cooked = self.loader.get(filename)
240 if cooked is None:
241 try:
242 source = self._compile(body, builtins)
243 if self.debug:
244 source = "# template: %s\n#\n%s" % (
245 self.filename, source)
246 if self.keep_source:
247 self.source = source
248 cooked = self.loader.build(source, filename)
249 except TemplateError:
250 exc = sys.exc_info()[1]
251 exc.token.filename = self.filename
252 raise
253 elif self.keep_source:
254 module = sys.modules.get(cooked.get('__name__'))
255 if module is not None:
256 self.source = inspect.getsource(module)
257 else:
258 self.source = None
259 return cooked
261 def digest(self, body, names):
262 class_name = type(self).__name__.encode('utf-8')
263 sha = pkg_digest.copy()
264 sha.update(body.encode('utf-8', 'ignore'))
265 sha.update(class_name)
266 digest = sha.hexdigest()
268 if self.filename is not BaseTemplate.filename:
269 digest = os.path.splitext(self.filename)[0] + '-' + digest
271 return digest
273 def _compile(self, body, builtins):
274 program = self.parse(body)
275 module = Module("initialize", program)
276 compiler = Compiler(
277 self.engine, module, self.filename, body,
278 builtins, strict=self.strict
279 )
280 return compiler.code
283class BaseTemplateFile(BaseTemplate):
284 """File-based template base class.
286 Relative path names are supported only when a template loader is
287 provided as the ``loader`` parameter.
288 """
290 # Auto reload is not enabled by default because it's a significant
291 # performance hit
292 auto_reload = AUTO_RELOAD
294 def __init__(self, filename, auto_reload=None, **config):
295 # Normalize filename
296 filename = os.path.abspath(
297 os.path.normpath(os.path.expanduser(filename))
298 )
300 self.filename = filename
302 # Override reload setting only if value is provided explicitly
303 if auto_reload is not None:
304 self.auto_reload = auto_reload
306 super(BaseTemplateFile, self).__init__(**config)
308 if EAGER_PARSING:
309 self.cook_check()
311 def cook_check(self):
312 if self.auto_reload:
313 mtime = self.mtime()
315 if mtime != self._v_last_read:
316 self._v_last_read = mtime
317 self._cooked = False
319 if self._cooked is False:
320 body = self.read()
321 log.debug("cooking %r (%d bytes)..." % (self.filename, len(body)))
322 self.cook(body)
324 def mtime(self):
325 try:
326 return os.path.getmtime(self.filename)
327 except (IOError, OSError):
328 return 0
330 def read(self):
331 with open(self.filename, "rb") as f:
332 data = f.read()
334 body, encoding, content_type = read_bytes(
335 data, self.default_encoding
336 )
338 # In non-XML mode, we support various platform-specific line
339 # endings and convert them to the UNIX newline character
340 if content_type != "text/xml" and '\r' in body:
341 body = body.replace('\r\n', '\n').replace('\r', '\n')
343 self.content_type = content_type
344 self.content_encoding = encoding
346 return body
348 def _get_module_name(self, name):
349 filename = os.path.basename(self.filename)
350 mangled = mangle(filename)
351 return "%s_%s.py" % (mangled, name)
353 def _get_filename(self):
354 return self.__dict__.get('filename')
356 def _set_filename(self, filename):
357 self.__dict__['filename'] = filename
358 self._v_last_read = None
359 self._cooked = False
361 filename = property(_get_filename, _set_filename)