# mako/template.py
# Copyright 2006-2020 the Mako authors and contributors <see AUTHORS file>
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""Provides the Template class, a facade for parsing, generating and executing
template strings,
as well
as template runtime operations.
"""
import json
import os
import re
import shutil
import stat
import sys
import tempfile
import types
import weakref
from mako
import cache
from mako
import codegen
from mako
import compat
from mako
import exceptions
from mako
import runtime
from mako
import util
from mako.lexer
import Lexer
class Template(object):
r
"""Represents a compiled template.
:
class:`.Template` includes a reference to the original
template source (via the :attr:`.source` attribute)
as well
as the source code of the
generated Python module (i.e. the :attr:`.code` attribute),
as well
as a reference to an actual Python module.
:
class:`.Template`
is constructed using either a literal string
representing the template text,
or a filename representing a filesystem
path to a source file.
:param text: textual template source. This argument
is mutually
exclusive versus the ``filename`` parameter.
:param filename: filename of the source template. This argument
is
mutually exclusive versus the ``text`` parameter.
:param buffer_filters: string list of filters to be applied
to the output of ``%
def``\ s which are buffered, cached,
or otherwise
filtered, after all filters
defined
with the ``%
def`` itself have been applied. Allows the
creation of default expression filters that let the output
of return-valued ``%
def``\ s
"opt out" of that filtering via
passing special attributes
or objects.
:param bytestring_passthrough: When ``
True``,
and ``output_encoding``
is
set to ``
None``,
and :meth:`.Template.render`
is used to render,
the `StringIO`
or `cStringIO` buffer will be used instead of the
default
"fast" buffer. This allows raw bytestrings
in the
output stream, such
as in expressions, to
pass straight
through to the buffer. This flag
is forced
to ``
True``
if ``disable_unicode``
is also configured.
.. versionadded:: 0.4
Added to provide the same behavior
as that of the previous series.
:param cache_args: Dictionary of cache configuration arguments that
will be passed to the :
class:`.CacheImpl`. See :ref:`caching_toplevel`.
:param cache_dir:
.. deprecated:: 0.6
Use the ``
'dir'`` argument
in the ``cache_args`` dictionary.
See :ref:`caching_toplevel`.
:param cache_enabled: Boolean flag which enables caching of this
template. See :ref:`caching_toplevel`.
:param cache_impl: String name of a :
class:`.CacheImpl` caching
implementation to use. Defaults to ``
'beaker'``.
:param cache_type:
.. deprecated:: 0.6
Use the ``
'type'`` argument
in the ``cache_args`` dictionary.
See :ref:`caching_toplevel`.
:param cache_url:
.. deprecated:: 0.6
Use the ``
'url'`` argument
in the ``cache_args`` dictionary.
See :ref:`caching_toplevel`.
:param default_filters: List of string filter names that will
be applied to all expressions. See :ref:`filtering_default_filters`.
:param disable_unicode: Disables all awareness of Python Unicode
objects. See :ref:`unicode_disabled`.
:param enable_loop: When ``
True``, enable the ``loop`` context variable.
This can be set to ``
False`` to support templates that may
be making usage of the name
"``loop``". Individual templates can
re-enable the
"loop" context by placing the directive
``enable_loop=
"True"`` inside the ``<%page>`` tag -- see
:ref:`migrating_loop`.
:param encoding_errors: Error parameter passed to ``encode()`` when
string encoding
is performed. See :ref:`usage_unicode`.
:param error_handler: Python callable which
is called whenever
compile
or runtime exceptions occur. The callable
is passed
the current context
as well
as the exception.
If the
callable returns ``
True``, the exception
is considered to
be handled,
else it
is re-raised after the function
completes.
Is used to provide custom error-rendering
functions.
.. seealso::
:paramref:`.Template.include_error_handler` - include-specific
error handler function
:param format_exceptions:
if ``
True``, exceptions which occur during
the render phase of this template will be caught
and
formatted into an HTML error page, which then becomes the
rendered result of the :meth:`.render` call. Otherwise,
runtime exceptions are propagated outwards.
:param imports: String list of Python statements, typically individual
"import" lines, which will be placed into the module level
preamble of all generated Python modules. See the example
in :ref:`filtering_default_filters`.
:param future_imports: String list of names to
import from `__future__`.
These will be concatenated into a comma-separated string
and inserted
into the beginning of the template, e.g. ``futures_imports=[
'FOO',
'BAR']`` results
in ``
from __future__
import FOO, BAR``.
If you
're
interested
in using features like the new division operator, you must
use future_imports to convey that to the renderer,
as otherwise the
import will
not appear
as the first executed statement
in the generated
code
and will therefore
not have the desired effect.
:param include_error_handler: An error handler that runs when this template
is included within another one via the ``<%include>`` tag,
and raises an
error. Compare to the :paramref:`.Template.error_handler` option.
.. versionadded:: 1.0.6
.. seealso::
:paramref:`.Template.error_handler` - top-level error handler function
:param input_encoding: Encoding of the template
's source code. Can
be used
in lieu of the coding comment. See
:ref:`usage_unicode`
as well
as :ref:`unicode_toplevel`
for
details on source encoding.
:param lookup: a :
class:`.TemplateLookup` instance that will be used
for all file lookups via the ``<%namespace>``,
``<%include>``,
and ``<%inherit>`` tags. See
:ref:`usage_templatelookup`.
:param module_directory: Filesystem location where generated
Python module files will be placed.
:param module_filename: Overrides the filename of the generated
Python module file.
For advanced usage only.
:param module_writer: A callable which overrides how the Python
module
is written entirely. The callable
is passed the
encoded source content of the module
and the destination
path to be written to. The default behavior of module writing
uses a tempfile
in conjunction
with a file move
in order
to make the operation atomic. So a user-defined module
writing function that mimics the default behavior would be:
.. sourcecode:: python
import tempfile
import os
import shutil
def module_writer(source, outputpath):
(dest, name) = \\
tempfile.mkstemp(
dir=os.path.dirname(outputpath)
)
os.write(dest, source)
os.close(dest)
shutil.move(name, outputpath)
from mako.template
import Template
mytemplate = Template(
filename=
"index.html",
module_directory=
"/path/to/modules",
module_writer=module_writer
)
The function
is provided
for unusual configurations where
certain platform-specific permissions
or other special
steps are needed.
:param output_encoding: The encoding to use when :meth:`.render`
is called.
See :ref:`usage_unicode`
as well
as :ref:`unicode_toplevel`.
:param preprocessor: Python callable which will be passed
the full template source before it
is parsed. The
return
result of the callable will be used
as the template source
code.
:param lexer_cls: A :
class:`.Lexer`
class used to parse
the template. The :
class:`.Lexer`
class is used by
default.
.. versionadded:: 0.7.4
:param strict_undefined: Replaces the automatic usage of
``UNDEFINED``
for any undeclared variables
not located
in
the :
class:`.Context`
with an immediate
raise of
``NameError``. The advantage
is immediate reporting of
missing variables which include the name.
.. versionadded:: 0.3.6
:param uri: string URI
or other identifier
for this template.
If not provided, the ``uri``
is generated
from the filesystem
path,
or from the in-memory identity of a non-file-based
template. The primary usage of the ``uri``
is to provide a key
within :
class:`.TemplateLookup`,
as well
as to generate the
file path of the generated Python module file,
if
``module_directory``
is specified.
"""
lexer_cls = Lexer
def __init__(
self,
text=
None,
filename=
None,
uri=
None,
format_exceptions=
False,
error_handler=
None,
lookup=
None,
output_encoding=
None,
encoding_errors=
"strict",
module_directory=
None,
cache_args=
None,
cache_impl=
"beaker",
cache_enabled=
True,
cache_type=
None,
cache_dir=
None,
cache_url=
None,
module_filename=
None,
input_encoding=
None,
disable_unicode=
False,
module_writer=
None,
bytestring_passthrough=
False,
default_filters=
None,
buffer_filters=(),
strict_undefined=
False,
imports=
None,
future_imports=
None,
enable_loop=
True,
preprocessor=
None,
lexer_cls=
None,
include_error_handler=
None,
):
if uri:
self.module_id = re.sub(r
"\W",
"_", uri)
self.uri = uri
elif filename:
self.module_id = re.sub(r
"\W",
"_", filename)
drive, path = os.path.splitdrive(filename)
path = os.path.normpath(path).replace(os.path.sep,
"/")
self.uri = path
else:
self.module_id =
"memory:" + hex(id(self))
self.uri = self.module_id
u_norm = self.uri
if u_norm.startswith(
"/"):
u_norm = u_norm[1:]
u_norm = os.path.normpath(u_norm)
if u_norm.startswith(
".."):
raise exceptions.TemplateLookupException(
'Template uri "%s" is invalid - '
"it cannot be relative outside "
"of the root path." % self.uri
)
self.input_encoding = input_encoding
self.output_encoding = output_encoding
self.encoding_errors = encoding_errors
self.disable_unicode = disable_unicode
self.bytestring_passthrough = bytestring_passthrough
or disable_unicode
self.enable_loop = enable_loop
self.strict_undefined = strict_undefined
self.module_writer = module_writer
if compat.py3k
and disable_unicode:
raise exceptions.UnsupportedError(
"Mako for Python 3 does not " "support disabling Unicode"
)
elif output_encoding
and disable_unicode:
raise exceptions.UnsupportedError(
"output_encoding must be set to "
"None when disable_unicode is used."
)
if default_filters
is None:
if compat.py3k
or self.disable_unicode:
self.default_filters = [
"str"]
else:
self.default_filters = [
"unicode"]
else:
self.default_filters = default_filters
self.buffer_filters = buffer_filters
self.imports = imports
self.future_imports = future_imports
self.preprocessor = preprocessor
if lexer_cls
is not None:
self.lexer_cls = lexer_cls
# if plain text, compile code in memory only
if text
is not None:
(code, module) = _compile_text(self, text, filename)
self._code = code
self._source = text
ModuleInfo(module,
None, self, filename, code, text, uri)
elif filename
is not None:
# if template filename and a module directory, load
# a filesystem-based module file, generating if needed
if module_filename
is not None:
path = module_filename
elif module_directory
is not None:
path = os.path.abspath(
os.path.join(
os.path.normpath(module_directory), u_norm +
".py"
)
)
else:
path =
None
module = self._compile_from_file(path, filename)
else:
raise exceptions.RuntimeException(
"Template requires text or filename"
)
self.module = module
self.filename = filename
self.callable_ = self.module.render_body
self.format_exceptions = format_exceptions
self.error_handler = error_handler
self.include_error_handler = include_error_handler
self.lookup = lookup
self.module_directory = module_directory
self._setup_cache_args(
cache_impl,
cache_enabled,
cache_args,
cache_type,
cache_dir,
cache_url,
)
@util.memoized_property
def reserved_names(self):
if self.enable_loop:
return codegen.RESERVED_NAMES
else:
return codegen.RESERVED_NAMES.difference([
"loop"])
def _setup_cache_args(
self,
cache_impl,
cache_enabled,
cache_args,
cache_type,
cache_dir,
cache_url,
):
self.cache_impl = cache_impl
self.cache_enabled = cache_enabled
if cache_args:
self.cache_args = cache_args
else:
self.cache_args = {}
# transfer deprecated cache_* args
if cache_type:
self.cache_args[
"type"] = cache_type
if cache_dir:
self.cache_args[
"dir"] = cache_dir
if cache_url:
self.cache_args[
"url"] = cache_url
def _compile_from_file(self, path, filename):
if path
is not None:
util.verify_directory(os.path.dirname(path))
filemtime = os.stat(filename)[stat.ST_MTIME]
if (
not os.path.exists(path)
or os.stat(path)[stat.ST_MTIME] < filemtime
):
data = util.read_file(filename)
_compile_module_file(
self, data, filename, path, self.module_writer
)
module = compat.load_module(self.module_id, path)
del sys.modules[self.module_id]
if module._magic_number != codegen.MAGIC_NUMBER:
data = util.read_file(filename)
_compile_module_file(
self, data, filename, path, self.module_writer
)
module = compat.load_module(self.module_id, path)
del sys.modules[self.module_id]
ModuleInfo(module, path, self, filename,
None,
None,
None)
else:
# template filename and no module directory, compile code
# in memory
data = util.read_file(filename)
code, module = _compile_text(self, data, filename)
self._source =
None
self._code = code
ModuleInfo(module,
None, self, filename, code,
None,
None)
return module
@property
def source(self):
"""Return the template source code for this :class:`.Template`."""
return _get_module_info_from_callable(self.callable_).source
@property
def code(self):
"""Return the module source code for this :class:`.Template`."""
return _get_module_info_from_callable(self.callable_).code
@util.memoized_property
def cache(self):
return cache.Cache(self)
@property
def cache_dir(self):
return self.cache_args[
"dir"]
@property
def cache_url(self):
return self.cache_args[
"url"]
@property
def cache_type(self):
return self.cache_args[
"type"]
def render(self, *args, **data):
"""Render the output of this template as a string.
If the template specifies an output encoding, the string
will be encoded accordingly,
else the output
is raw (raw
output uses `cStringIO`
and can
't handle multibyte
characters). A :
class:`.Context` object
is created corresponding
to the given data. Arguments that are explicitly declared
by this template
's internal rendering method are also
pulled
from the given ``*args``, ``**data`` members.
"""
return runtime._render(self, self.callable_, args, data)
def render_unicode(self, *args, **data):
"""Render the output of this template as a unicode object."""
return runtime._render(
self, self.callable_, args, data, as_unicode=
True
)
def render_context(self, context, *args, **kwargs):
"""Render this :class:`.Template` with the given context.
The data
is written to the context
's buffer.
"""
if getattr(context,
"_with_template",
None)
is None:
context._set_with_template(self)
runtime._render_context(self, self.callable_, context, *args, **kwargs)
def has_def(self, name):
return hasattr(self.module,
"render_%s" % name)
def get_def(self, name):
"""Return a def of this template as a :class:`.DefTemplate`."""
return DefTemplate(self, getattr(self.module,
"render_%s" % name))
def list_defs(self):
"""return a list of defs in the template.
.. versionadded:: 1.0.4
"""
return [i[7:]
for i
in dir(self.module)
if i[:7] ==
"render_"]
def _get_def_callable(self, name):
return getattr(self.module,
"render_%s" % name)
@property
def last_modified(self):
return self.module._modified_time
class ModuleTemplate(Template):
"""A Template which is constructed given an existing Python module.
e.g.::
t = Template(
"this is a template")
f = file(
"mymodule.py",
"w")
f.write(t.code)
f.close()
import mymodule
t = ModuleTemplate(mymodule)
print(t.render())
"""
def __init__(
self,
module,
module_filename=
None,
template=
None,
template_filename=
None,
module_source=
None,
template_source=
None,
output_encoding=
None,
encoding_errors=
"strict",
disable_unicode=
False,
bytestring_passthrough=
False,
format_exceptions=
False,
error_handler=
None,
lookup=
None,
cache_args=
None,
cache_impl=
"beaker",
cache_enabled=
True,
cache_type=
None,
cache_dir=
None,
cache_url=
None,
include_error_handler=
None,
):
self.module_id = re.sub(r
"\W",
"_", module._template_uri)
self.uri = module._template_uri
self.input_encoding = module._source_encoding
self.output_encoding = output_encoding
self.encoding_errors = encoding_errors
self.disable_unicode = disable_unicode
self.bytestring_passthrough = bytestring_passthrough
or disable_unicode
self.enable_loop = module._enable_loop
if compat.py3k
and disable_unicode:
raise exceptions.UnsupportedError(
"Mako for Python 3 does not " "support disabling Unicode"
)
elif output_encoding
and disable_unicode:
raise exceptions.UnsupportedError(
"output_encoding must be set to "
"None when disable_unicode is used."
)
self.module = module
self.filename = template_filename
ModuleInfo(
module,
module_filename,
self,
template_filename,
module_source,
template_source,
module._template_uri,
)
self.callable_ = self.module.render_body
self.format_exceptions = format_exceptions
self.error_handler = error_handler
self.include_error_handler = include_error_handler
self.lookup = lookup
self._setup_cache_args(
cache_impl,
cache_enabled,
cache_args,
cache_type,
cache_dir,
cache_url,
)
class DefTemplate(Template):
"""A :class:`.Template` which represents a callable def in a parent
template.
"""
def __init__(self, parent, callable_):
self.parent = parent
self.callable_ = callable_
self.output_encoding = parent.output_encoding
self.module = parent.module
self.encoding_errors = parent.encoding_errors
self.format_exceptions = parent.format_exceptions
self.error_handler = parent.error_handler
self.include_error_handler = parent.include_error_handler
self.enable_loop = parent.enable_loop
self.lookup = parent.lookup
self.bytestring_passthrough = parent.bytestring_passthrough
def get_def(self, name):
return self.parent.get_def(name)
class ModuleInfo(object):
"""Stores information about a module currently loaded into
memory, provides reverse lookups of template source, module
source code based on a module
's identifier.
"""
_modules = weakref.WeakValueDictionary()
def __init__(
self,
module,
module_filename,
template,
template_filename,
module_source,
template_source,
template_uri,
):
self.module = module
self.module_filename = module_filename
self.template_filename = template_filename
self.module_source = module_source
self.template_source = template_source
self.template_uri = template_uri
self._modules[module.__name__] = template._mmarker = self
if module_filename:
self._modules[module_filename] = self
@classmethod
def get_module_source_metadata(cls, module_source, full_line_map=
False):
source_map = re.search(
r
"__M_BEGIN_METADATA(.+?)__M_END_METADATA", module_source, re.S
).group(1)
source_map = json.loads(source_map)
source_map[
"line_map"] = dict(
(int(k), int(v))
for k, v
in source_map[
"line_map"].items()
)
if full_line_map:
f_line_map = source_map[
"full_line_map"] = []
line_map = source_map[
"line_map"]
curr_templ_line = 1
for mod_line
in range(1, max(line_map)):
if mod_line
in line_map:
curr_templ_line = line_map[mod_line]
f_line_map.append(curr_templ_line)
return source_map
@property
def code(self):
if self.module_source
is not None:
return self.module_source
else:
return util.read_python_file(self.module_filename)
@property
def source(self):
if self.template_source
is not None:
if self.module._source_encoding
and not isinstance(
self.template_source, compat.text_type
):
return self.template_source.decode(
self.module._source_encoding
)
else:
return self.template_source
else:
data = util.read_file(self.template_filename)
if self.module._source_encoding:
return data.decode(self.module._source_encoding)
else:
return data
def _compile(template, text, filename, generate_magic_comment):
lexer = template.lexer_cls(
text,
filename,
disable_unicode=template.disable_unicode,
input_encoding=template.input_encoding,
preprocessor=template.preprocessor,
)
node = lexer.parse()
source = codegen.compile(
node,
template.uri,
filename,
default_filters=template.default_filters,
buffer_filters=template.buffer_filters,
imports=template.imports,
future_imports=template.future_imports,
source_encoding=lexer.encoding,
generate_magic_comment=generate_magic_comment,
disable_unicode=template.disable_unicode,
strict_undefined=template.strict_undefined,
enable_loop=template.enable_loop,
reserved_names=template.reserved_names,
)
return source, lexer
def _compile_text(template, text, filename):
identifier = template.module_id
source, lexer = _compile(
template,
text,
filename,
generate_magic_comment=template.disable_unicode,
)
cid = identifier
if not compat.py3k
and isinstance(cid, compat.text_type):
cid = cid.encode()
module = types.ModuleType(cid)
code = compile(source, cid,
"exec")
# this exec() works for 2.4->3.3.
exec(code, module.__dict__, module.__dict__)
return (source, module)
def _compile_module_file(template, text, filename, outputpath, module_writer):
source, lexer = _compile(
template, text, filename, generate_magic_comment=
True
)
if isinstance(source, compat.text_type):
source = source.encode(lexer.encoding
or "ascii")
if module_writer:
module_writer(source, outputpath)
else:
# make tempfiles in the same location as the ultimate
# location. this ensures they're on the same filesystem,
# avoiding synchronization issues.
(dest, name) = tempfile.mkstemp(dir=os.path.dirname(outputpath))
os.write(dest, source)
os.close(dest)
shutil.move(name, outputpath)
def _get_module_info_from_callable(callable_):
if compat.py3k:
return _get_module_info(callable_.__globals__[
"__name__"])
else:
return _get_module_info(callable_.func_globals[
"__name__"])
def _get_module_info(filename):
return ModuleInfo._modules[filename]