1# mako/template.py
2# Copyright 2006-2023 the Mako authors and contributors <see AUTHORS file>
3#
4# This module is part of Mako and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
7"""Provides the Template class, a facade for parsing, generating and executing
8template strings, as well as template runtime operations."""
9
10import json
11import os
12import re
13import shutil
14import stat
15import tempfile
16import types
17import weakref
18
19from mako import cache
20from mako import codegen
21from mako import compat
22from mako import exceptions
23from mako import runtime
24from mako import util
25from mako.lexer import Lexer
26
27
28class Template:
29    r"""Represents a compiled template.
30
31    :class:`.Template` includes a reference to the original
32    template source (via the :attr:`.source` attribute)
33    as well as the source code of the
34    generated Python module (i.e. the :attr:`.code` attribute),
35    as well as a reference to an actual Python module.
36
37    :class:`.Template` is constructed using either a literal string
38    representing the template text, or a filename representing a filesystem
39    path to a source file.
40
41    :param text: textual template source.  This argument is mutually
42     exclusive versus the ``filename`` parameter.
43
44    :param filename: filename of the source template.  This argument is
45     mutually exclusive versus the ``text`` parameter.
46
47    :param buffer_filters: string list of filters to be applied
48     to the output of ``%def``\ s which are buffered, cached, or otherwise
49     filtered, after all filters
50     defined with the ``%def`` itself have been applied. Allows the
51     creation of default expression filters that let the output
52     of return-valued ``%def``\ s "opt out" of that filtering via
53     passing special attributes or objects.
54
55    :param cache_args: Dictionary of cache configuration arguments that
56     will be passed to the :class:`.CacheImpl`.   See :ref:`caching_toplevel`.
57
58    :param cache_dir:
59
60     .. deprecated:: 0.6
61        Use the ``'dir'`` argument in the ``cache_args`` dictionary.
62        See :ref:`caching_toplevel`.
63
64    :param cache_enabled: Boolean flag which enables caching of this
65     template.  See :ref:`caching_toplevel`.
66
67    :param cache_impl: String name of a :class:`.CacheImpl` caching
68     implementation to use.   Defaults to ``'beaker'``.
69
70    :param cache_type:
71
72     .. deprecated:: 0.6
73        Use the ``'type'`` argument in the ``cache_args`` dictionary.
74        See :ref:`caching_toplevel`.
75
76    :param cache_url:
77
78     .. deprecated:: 0.6
79        Use the ``'url'`` argument in the ``cache_args`` dictionary.
80        See :ref:`caching_toplevel`.
81
82    :param default_filters: List of string filter names that will
83     be applied to all expressions.  See :ref:`filtering_default_filters`.
84
85    :param enable_loop: When ``True``, enable the ``loop`` context variable.
86     This can be set to ``False`` to support templates that may
87     be making usage of the name "``loop``".   Individual templates can
88     re-enable the "loop" context by placing the directive
89     ``enable_loop="True"`` inside the ``<%page>`` tag -- see
90     :ref:`migrating_loop`.
91
92    :param encoding_errors: Error parameter passed to ``encode()`` when
93     string encoding is performed. See :ref:`usage_unicode`.
94
95    :param error_handler: Python callable which is called whenever
96     compile or runtime exceptions occur. The callable is passed
97     the current context as well as the exception. If the
98     callable returns ``True``, the exception is considered to
99     be handled, else it is re-raised after the function
100     completes. Is used to provide custom error-rendering
101     functions.
102
103     .. seealso::
104
105        :paramref:`.Template.include_error_handler` - include-specific
106        error handler function
107
108    :param format_exceptions: if ``True``, exceptions which occur during
109     the render phase of this template will be caught and
110     formatted into an HTML error page, which then becomes the
111     rendered result of the :meth:`.render` call. Otherwise,
112     runtime exceptions are propagated outwards.
113
114    :param imports: String list of Python statements, typically individual
115     "import" lines, which will be placed into the module level
116     preamble of all generated Python modules. See the example
117     in :ref:`filtering_default_filters`.
118
119    :param future_imports: String list of names to import from `__future__`.
120     These will be concatenated into a comma-separated string and inserted
121     into the beginning of the template, e.g. ``futures_imports=['FOO',
122     'BAR']`` results in ``from __future__ import FOO, BAR``.  If you're
123     interested in using features like the new division operator, you must
124     use future_imports to convey that to the renderer, as otherwise the
125     import will not appear as the first executed statement in the generated
126     code and will therefore not have the desired effect.
127
128    :param include_error_handler: An error handler that runs when this template
129     is included within another one via the ``<%include>`` tag, and raises an
130     error.  Compare to the :paramref:`.Template.error_handler` option.
131
132     .. versionadded:: 1.0.6
133
134     .. seealso::
135
136        :paramref:`.Template.error_handler` - top-level error handler function
137
138    :param input_encoding: Encoding of the template's source code.  Can
139     be used in lieu of the coding comment. See
140     :ref:`usage_unicode` as well as :ref:`unicode_toplevel` for
141     details on source encoding.
142
143    :param lookup: a :class:`.TemplateLookup` instance that will be used
144     for all file lookups via the ``<%namespace>``,
145     ``<%include>``, and ``<%inherit>`` tags. See
146     :ref:`usage_templatelookup`.
147
148    :param module_directory: Filesystem location where generated
149     Python module files will be placed.
150
151    :param module_filename: Overrides the filename of the generated
152     Python module file. For advanced usage only.
153
154    :param module_writer: A callable which overrides how the Python
155     module is written entirely.  The callable is passed the
156     encoded source content of the module and the destination
157     path to be written to.   The default behavior of module writing
158     uses a tempfile in conjunction with a file move in order
159     to make the operation atomic.   So a user-defined module
160     writing function that mimics the default behavior would be:
161
162     .. sourcecode:: python
163
164         import tempfile
165         import os
166         import shutil
167
168         def module_writer(source, outputpath):
169             (dest, name) = \\
170                 tempfile.mkstemp(
171                     dir=os.path.dirname(outputpath)
172                 )
173
174             os.write(dest, source)
175             os.close(dest)
176             shutil.move(name, outputpath)
177
178         from mako.template import Template
179         mytemplate = Template(
180                         filename="index.html",
181                         module_directory="/path/to/modules",
182                         module_writer=module_writer
183                     )
184
185     The function is provided for unusual configurations where
186     certain platform-specific permissions or other special
187     steps are needed.
188
189    :param output_encoding: The encoding to use when :meth:`.render`
190     is called.
191     See :ref:`usage_unicode` as well as :ref:`unicode_toplevel`.
192
193    :param preprocessor: Python callable which will be passed
194     the full template source before it is parsed. The return
195     result of the callable will be used as the template source
196     code.
197
198    :param lexer_cls: A :class:`.Lexer` class used to parse
199     the template.   The :class:`.Lexer` class is used by
200     default.
201
202     .. versionadded:: 0.7.4
203
204    :param strict_undefined: Replaces the automatic usage of
205     ``UNDEFINED`` for any undeclared variables not located in
206     the :class:`.Context` with an immediate raise of
207     ``NameError``. The advantage is immediate reporting of
208     missing variables which include the name.
209
210     .. versionadded:: 0.3.6
211
212    :param uri: string URI or other identifier for this template.
213     If not provided, the ``uri`` is generated from the filesystem
214     path, or from the in-memory identity of a non-file-based
215     template. The primary usage of the ``uri`` is to provide a key
216     within :class:`.TemplateLookup`, as well as to generate the
217     file path of the generated Python module file, if
218     ``module_directory`` is specified.
219
220    """
221
222    lexer_cls = Lexer
223
224    def __init__(
225        self,
226        text=None,
227        filename=None,
228        uri=None,
229        format_exceptions=False,
230        error_handler=None,
231        lookup=None,
232        output_encoding=None,
233        encoding_errors="strict",
234        module_directory=None,
235        cache_args=None,
236        cache_impl="beaker",
237        cache_enabled=True,
238        cache_type=None,
239        cache_dir=None,
240        cache_url=None,
241        module_filename=None,
242        input_encoding=None,
243        module_writer=None,
244        default_filters=None,
245        buffer_filters=(),
246        strict_undefined=False,
247        imports=None,
248        future_imports=None,
249        enable_loop=True,
250        preprocessor=None,
251        lexer_cls=None,
252        include_error_handler=None,
253    ):
254        if uri:
255            self.module_id = re.sub(r"\W", "_", uri)
256            self.uri = uri
257        elif filename:
258            self.module_id = re.sub(r"\W", "_", filename)
259            drive, path = os.path.splitdrive(filename)
260            path = os.path.normpath(path).replace(os.path.sep, "/")
261            self.uri = path
262        else:
263            self.module_id = "memory:" + hex(id(self))
264            self.uri = self.module_id
265
266        u_norm = self.uri
267        if u_norm.startswith("/"):
268            u_norm = u_norm[1:]
269        u_norm = os.path.normpath(u_norm)
270        if u_norm.startswith(".."):
271            raise exceptions.TemplateLookupException(
272                'Template uri "%s" is invalid - '
273                "it cannot be relative outside "
274                "of the root path." % self.uri
275            )
276
277        self.input_encoding = input_encoding
278        self.output_encoding = output_encoding
279        self.encoding_errors = encoding_errors
280        self.enable_loop = enable_loop
281        self.strict_undefined = strict_undefined
282        self.module_writer = module_writer
283
284        if default_filters is None:
285            self.default_filters = ["str"]
286        else:
287            self.default_filters = default_filters
288        self.buffer_filters = buffer_filters
289
290        self.imports = imports
291        self.future_imports = future_imports
292        self.preprocessor = preprocessor
293
294        if lexer_cls is not None:
295            self.lexer_cls = lexer_cls
296
297        # if plain text, compile code in memory only
298        if text is not None:
299            (code, module) = _compile_text(self, text, filename)
300            self._code = code
301            self._source = text
302            ModuleInfo(module, None, self, filename, code, text, uri)
303        elif filename is not None:
304            # if template filename and a module directory, load
305            # a filesystem-based module file, generating if needed
306            if module_filename is not None:
307                path = module_filename
308            elif module_directory is not None:
309                path = os.path.abspath(
310                    os.path.join(
311                        os.path.normpath(module_directory), u_norm + ".py"
312                    )
313                )
314            else:
315                path = None
316            module = self._compile_from_file(path, filename)
317        else:
318            raise exceptions.RuntimeException(
319                "Template requires text or filename"
320            )
321
322        self.module = module
323        self.filename = filename
324        self.callable_ = self.module.render_body
325        self.format_exceptions = format_exceptions
326        self.error_handler = error_handler
327        self.include_error_handler = include_error_handler
328        self.lookup = lookup
329
330        self.module_directory = module_directory
331
332        self._setup_cache_args(
333            cache_impl,
334            cache_enabled,
335            cache_args,
336            cache_type,
337            cache_dir,
338            cache_url,
339        )
340
341    @util.memoized_property
342    def reserved_names(self):
343        if self.enable_loop:
344            return codegen.RESERVED_NAMES
345        else:
346            return codegen.RESERVED_NAMES.difference(["loop"])
347
348    def _setup_cache_args(
349        self,
350        cache_impl,
351        cache_enabled,
352        cache_args,
353        cache_type,
354        cache_dir,
355        cache_url,
356    ):
357        self.cache_impl = cache_impl
358        self.cache_enabled = cache_enabled
359        self.cache_args = cache_args or {}
360        # transfer deprecated cache_* args
361        if cache_type:
362            self.cache_args["type"] = cache_type
363        if cache_dir:
364            self.cache_args["dir"] = cache_dir
365        if cache_url:
366            self.cache_args["url"] = cache_url
367
368    def _compile_from_file(self, path, filename):
369        if path is not None:
370            util.verify_directory(os.path.dirname(path))
371            filemtime = os.stat(filename)[stat.ST_MTIME]
372            if (
373                not os.path.exists(path)
374                or os.stat(path)[stat.ST_MTIME] < filemtime
375            ):
376                data = util.read_file(filename)
377                _compile_module_file(
378                    self, data, filename, path, self.module_writer
379                )
380            module = compat.load_module(self.module_id, path)
381            if module._magic_number != codegen.MAGIC_NUMBER:
382                data = util.read_file(filename)
383                _compile_module_file(
384                    self, data, filename, path, self.module_writer
385                )
386                module = compat.load_module(self.module_id, path)
387            ModuleInfo(module, path, self, filename, None, None, None)
388        else:
389            # template filename and no module directory, compile code
390            # in memory
391            data = util.read_file(filename)
392            code, module = _compile_text(self, data, filename)
393            self._source = None
394            self._code = code
395            ModuleInfo(module, None, self, filename, code, None, None)
396        return module
397
398    @property
399    def source(self):
400        """Return the template source code for this :class:`.Template`."""
401
402        return _get_module_info_from_callable(self.callable_).source
403
404    @property
405    def code(self):
406        """Return the module source code for this :class:`.Template`."""
407
408        return _get_module_info_from_callable(self.callable_).code
409
410    @util.memoized_property
411    def cache(self):
412        return cache.Cache(self)
413
414    @property
415    def cache_dir(self):
416        return self.cache_args["dir"]
417
418    @property
419    def cache_url(self):
420        return self.cache_args["url"]
421
422    @property
423    def cache_type(self):
424        return self.cache_args["type"]
425
426    def render(self, *args, **data):
427        """Render the output of this template as a string.
428
429        If the template specifies an output encoding, the string
430        will be encoded accordingly, else the output is raw (raw
431        output uses `StringIO` and can't handle multibyte
432        characters). A :class:`.Context` object is created corresponding
433        to the given data. Arguments that are explicitly declared
434        by this template's internal rendering method are also
435        pulled from the given ``*args``, ``**data`` members.
436
437        """
438        return runtime._render(self, self.callable_, args, data)
439
440    def render_unicode(self, *args, **data):
441        """Render the output of this template as a unicode object."""
442
443        return runtime._render(
444            self, self.callable_, args, data, as_unicode=True
445        )
446
447    def render_context(self, context, *args, **kwargs):
448        """Render this :class:`.Template` with the given context.
449
450        The data is written to the context's buffer.
451
452        """
453        if getattr(context, "_with_template", None) is None:
454            context._set_with_template(self)
455        runtime._render_context(self, self.callable_, context, *args, **kwargs)
456
457    def has_def(self, name):
458        return hasattr(self.module, "render_%s" % name)
459
460    def get_def(self, name):
461        """Return a def of this template as a :class:`.DefTemplate`."""
462
463        return DefTemplate(self, getattr(self.module, "render_%s" % name))
464
465    def list_defs(self):
466        """return a list of defs in the template.
467
468        .. versionadded:: 1.0.4
469
470        """
471        return [i[7:] for i in dir(self.module) if i[:7] == "render_"]
472
473    def _get_def_callable(self, name):
474        return getattr(self.module, "render_%s" % name)
475
476    @property
477    def last_modified(self):
478        return self.module._modified_time
479
480
481class ModuleTemplate(Template):
482
483    """A Template which is constructed given an existing Python module.
484
485    e.g.::
486
487         t = Template("this is a template")
488         f = file("mymodule.py", "w")
489         f.write(t.code)
490         f.close()
491
492         import mymodule
493
494         t = ModuleTemplate(mymodule)
495         print(t.render())
496
497    """
498
499    def __init__(
500        self,
501        module,
502        module_filename=None,
503        template=None,
504        template_filename=None,
505        module_source=None,
506        template_source=None,
507        output_encoding=None,
508        encoding_errors="strict",
509        format_exceptions=False,
510        error_handler=None,
511        lookup=None,
512        cache_args=None,
513        cache_impl="beaker",
514        cache_enabled=True,
515        cache_type=None,
516        cache_dir=None,
517        cache_url=None,
518        include_error_handler=None,
519    ):
520        self.module_id = re.sub(r"\W", "_", module._template_uri)
521        self.uri = module._template_uri
522        self.input_encoding = module._source_encoding
523        self.output_encoding = output_encoding
524        self.encoding_errors = encoding_errors
525        self.enable_loop = module._enable_loop
526
527        self.module = module
528        self.filename = template_filename
529        ModuleInfo(
530            module,
531            module_filename,
532            self,
533            template_filename,
534            module_source,
535            template_source,
536            module._template_uri,
537        )
538
539        self.callable_ = self.module.render_body
540        self.format_exceptions = format_exceptions
541        self.error_handler = error_handler
542        self.include_error_handler = include_error_handler
543        self.lookup = lookup
544        self._setup_cache_args(
545            cache_impl,
546            cache_enabled,
547            cache_args,
548            cache_type,
549            cache_dir,
550            cache_url,
551        )
552
553
554class DefTemplate(Template):
555
556    """A :class:`.Template` which represents a callable def in a parent
557    template."""
558
559    def __init__(self, parent, callable_):
560        self.parent = parent
561        self.callable_ = callable_
562        self.output_encoding = parent.output_encoding
563        self.module = parent.module
564        self.encoding_errors = parent.encoding_errors
565        self.format_exceptions = parent.format_exceptions
566        self.error_handler = parent.error_handler
567        self.include_error_handler = parent.include_error_handler
568        self.enable_loop = parent.enable_loop
569        self.lookup = parent.lookup
570
571    def get_def(self, name):
572        return self.parent.get_def(name)
573
574
575class ModuleInfo:
576
577    """Stores information about a module currently loaded into
578    memory, provides reverse lookups of template source, module
579    source code based on a module's identifier.
580
581    """
582
583    _modules = weakref.WeakValueDictionary()
584
585    def __init__(
586        self,
587        module,
588        module_filename,
589        template,
590        template_filename,
591        module_source,
592        template_source,
593        template_uri,
594    ):
595        self.module = module
596        self.module_filename = module_filename
597        self.template_filename = template_filename
598        self.module_source = module_source
599        self.template_source = template_source
600        self.template_uri = template_uri
601        self._modules[module.__name__] = template._mmarker = self
602        if module_filename:
603            self._modules[module_filename] = self
604
605    @classmethod
606    def get_module_source_metadata(cls, module_source, full_line_map=False):
607        source_map = re.search(
608            r"__M_BEGIN_METADATA(.+?)__M_END_METADATA", module_source, re.S
609        ).group(1)
610        source_map = json.loads(source_map)
611        source_map["line_map"] = {
612            int(k): int(v) for k, v in source_map["line_map"].items()
613        }
614        if full_line_map:
615            f_line_map = source_map["full_line_map"] = []
616            line_map = source_map["line_map"]
617
618            curr_templ_line = 1
619            for mod_line in range(1, max(line_map)):
620                if mod_line in line_map:
621                    curr_templ_line = line_map[mod_line]
622                f_line_map.append(curr_templ_line)
623        return source_map
624
625    @property
626    def code(self):
627        if self.module_source is not None:
628            return self.module_source
629        else:
630            return util.read_python_file(self.module_filename)
631
632    @property
633    def source(self):
634        if self.template_source is None:
635            data = util.read_file(self.template_filename)
636            if self.module._source_encoding:
637                return data.decode(self.module._source_encoding)
638            else:
639                return data
640
641        elif self.module._source_encoding and not isinstance(
642            self.template_source, str
643        ):
644            return self.template_source.decode(self.module._source_encoding)
645        else:
646            return self.template_source
647
648
649def _compile(template, text, filename, generate_magic_comment):
650    lexer = template.lexer_cls(
651        text,
652        filename,
653        input_encoding=template.input_encoding,
654        preprocessor=template.preprocessor,
655    )
656    node = lexer.parse()
657    source = codegen.compile(
658        node,
659        template.uri,
660        filename,
661        default_filters=template.default_filters,
662        buffer_filters=template.buffer_filters,
663        imports=template.imports,
664        future_imports=template.future_imports,
665        source_encoding=lexer.encoding,
666        generate_magic_comment=generate_magic_comment,
667        strict_undefined=template.strict_undefined,
668        enable_loop=template.enable_loop,
669        reserved_names=template.reserved_names,
670    )
671    return source, lexer
672
673
674def _compile_text(template, text, filename):
675    identifier = template.module_id
676    source, lexer = _compile(
677        template, text, filename, generate_magic_comment=False
678    )
679
680    cid = identifier
681    module = types.ModuleType(cid)
682    code = compile(source, cid, "exec")
683
684    # this exec() works for 2.4->3.3.
685    exec(code, module.__dict__, module.__dict__)
686    return (source, module)
687
688
689def _compile_module_file(template, text, filename, outputpath, module_writer):
690    source, lexer = _compile(
691        template, text, filename, generate_magic_comment=True
692    )
693
694    if isinstance(source, str):
695        source = source.encode(lexer.encoding or "ascii")
696
697    if module_writer:
698        module_writer(source, outputpath)
699    else:
700        # make tempfiles in the same location as the ultimate
701        # location.   this ensures they're on the same filesystem,
702        # avoiding synchronization issues.
703        (dest, name) = tempfile.mkstemp(dir=os.path.dirname(outputpath))
704
705        os.write(dest, source)
706        os.close(dest)
707        shutil.move(name, outputpath)
708
709
710def _get_module_info_from_callable(callable_):
711    return _get_module_info(callable_.__globals__["__name__"])
712
713
714def _get_module_info(filename):
715    return ModuleInfo._modules[filename]
716