| # mako/template.py |
| # Copyright (C) 2006-2015 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.""" |
| |
| from mako.lexer import Lexer |
| from mako import runtime, util, exceptions, codegen, cache, compat |
| import os |
| import re |
| import shutil |
| import stat |
| import sys |
| import tempfile |
| import types |
| import weakref |
| |
| |
| class Template(object): |
| |
| """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. |
| |
| :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 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): |
| 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) |
| 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.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) |
| 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) |
| 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, |
| ): |
| 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) |
| |
| self.callable_ = self.module.render_body |
| self.format_exceptions = format_exceptions |
| self.error_handler = 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.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): |
| self.module = module |
| self.module_filename = module_filename |
| self.template_filename = template_filename |
| self.module_source = module_source |
| self.template_source = template_source |
| 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 = compat.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] |