Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tornado/template.py: 21%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# Copyright 2009 Facebook
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16"""A simple template system that compiles templates to Python code.
18Basic usage looks like::
20 t = template.Template("<html>{{ myvalue }}</html>")
21 print(t.generate(myvalue="XXX"))
23`Loader` is a class that loads templates from a root directory and caches
24the compiled templates::
26 loader = template.Loader("/home/btaylor")
27 print(loader.load("test.html").generate(myvalue="XXX"))
29We compile all templates to raw Python. Error-reporting is currently... uh,
30interesting. Syntax for the templates::
32 ### base.html
33 <html>
34 <head>
35 <title>{% block title %}Default title{% end %}</title>
36 </head>
37 <body>
38 <ul>
39 {% for student in students %}
40 {% block student %}
41 <li>{{ escape(student.name) }}</li>
42 {% end %}
43 {% end %}
44 </ul>
45 </body>
46 </html>
48 ### bold.html
49 {% extends "base.html" %}
51 {% block title %}A bolder title{% end %}
53 {% block student %}
54 <li><span style="bold">{{ escape(student.name) }}</span></li>
55 {% end %}
57Unlike most other template systems, we do not put any restrictions on the
58expressions you can include in your statements. ``if`` and ``for`` blocks get
59translated exactly into Python, so you can do complex expressions like::
61 {% for student in [p for p in people if p.student and p.age > 23] %}
62 <li>{{ escape(student.name) }}</li>
63 {% end %}
65Translating directly to Python means you can apply functions to expressions
66easily, like the ``escape()`` function in the examples above. You can pass
67functions in to your template just like any other variable
68(In a `.RequestHandler`, override `.RequestHandler.get_template_namespace`)::
70 ### Python code
71 def add(x, y):
72 return x + y
73 template.execute(add=add)
75 ### The template
76 {{ add(1, 2) }}
78We provide the functions `escape() <.xhtml_escape>`, `.url_escape()`,
79`.json_encode()`, and `.squeeze()` to all templates by default.
81Typical applications do not create `Template` or `Loader` instances by
82hand, but instead use the `~.RequestHandler.render` and
83`~.RequestHandler.render_string` methods of
84`tornado.web.RequestHandler`, which load templates automatically based
85on the ``template_path`` `.Application` setting.
87Variable names beginning with ``_tt_`` are reserved by the template
88system and should not be used by application code.
90Syntax Reference
91----------------
93Template expressions are surrounded by double curly braces: ``{{ ... }}``.
94The contents may be any python expression, which will be escaped according
95to the current autoescape setting and inserted into the output. Other
96template directives use ``{% %}``.
98To comment out a section so that it is omitted from the output, surround it
99with ``{# ... #}``.
102To include a literal ``{{``, ``{%``, or ``{#`` in the output, escape them as
103``{{!``, ``{%!``, and ``{#!``, respectively.
106``{% apply *function* %}...{% end %}``
107 Applies a function to the output of all template code between ``apply``
108 and ``end``::
110 {% apply linkify %}{{name}} said: {{message}}{% end %}
112 Note that as an implementation detail apply blocks are implemented
113 as nested functions and thus may interact strangely with variables
114 set via ``{% set %}``, or the use of ``{% break %}`` or ``{% continue %}``
115 within loops.
117``{% autoescape *function* %}``
118 Sets the autoescape mode for the current file. This does not affect
119 other files, even those referenced by ``{% include %}``. Note that
120 autoescaping can also be configured globally, at the `.Application`
121 or `Loader`.::
123 {% autoescape xhtml_escape %}
124 {% autoescape None %}
126``{% block *name* %}...{% end %}``
127 Indicates a named, replaceable block for use with ``{% extends %}``.
128 Blocks in the parent template will be replaced with the contents of
129 the same-named block in a child template.::
131 <!-- base.html -->
132 <title>{% block title %}Default title{% end %}</title>
134 <!-- mypage.html -->
135 {% extends "base.html" %}
136 {% block title %}My page title{% end %}
138``{% comment ... %}``
139 A comment which will be removed from the template output. Note that
140 there is no ``{% end %}`` tag; the comment goes from the word ``comment``
141 to the closing ``%}`` tag.
143``{% extends *filename* %}``
144 Inherit from another template. Templates that use ``extends`` should
145 contain one or more ``block`` tags to replace content from the parent
146 template. Anything in the child template not contained in a ``block``
147 tag will be ignored. For an example, see the ``{% block %}`` tag.
149``{% for *var* in *expr* %}...{% end %}``
150 Same as the python ``for`` statement. ``{% break %}`` and
151 ``{% continue %}`` may be used inside the loop.
153``{% from *x* import *y* %}``
154 Same as the python ``import`` statement.
156``{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}``
157 Conditional statement - outputs the first section whose condition is
158 true. (The ``elif`` and ``else`` sections are optional)
160``{% import *module* %}``
161 Same as the python ``import`` statement.
163``{% include *filename* %}``
164 Includes another template file. The included file can see all the local
165 variables as if it were copied directly to the point of the ``include``
166 directive (the ``{% autoescape %}`` directive is an exception).
167 Alternately, ``{% module Template(filename, **kwargs) %}`` may be used
168 to include another template with an isolated namespace.
170``{% module *expr* %}``
171 Renders a `~tornado.web.UIModule`. The output of the ``UIModule`` is
172 not escaped::
174 {% module Template("foo.html", arg=42) %}
176 ``UIModules`` are a feature of the `tornado.web.RequestHandler`
177 class (and specifically its ``render`` method) and will not work
178 when the template system is used on its own in other contexts.
180``{% raw *expr* %}``
181 Outputs the result of the given expression without autoescaping.
183``{% set *x* = *y* %}``
184 Sets a local variable.
186``{% try %}...{% except %}...{% else %}...{% finally %}...{% end %}``
187 Same as the python ``try`` statement.
189``{% while *condition* %}... {% end %}``
190 Same as the python ``while`` statement. ``{% break %}`` and
191 ``{% continue %}`` may be used inside the loop.
193``{% whitespace *mode* %}``
194 Sets the whitespace mode for the remainder of the current file
195 (or until the next ``{% whitespace %}`` directive). See
196 `filter_whitespace` for available options. New in Tornado 4.3.
198Security Considerations
199-----------------------
201Tornado templates may include arbitrary Python code to be run in the
202server process, which means they should generally be controlled and
203deployed with the same security practices you would use for source code.
205Autoescaping is only performed when processing expressions using ``{{ }}``.
206Other template directives, including ``{% module %}`` do not perform
207autoescaping - it is the responsibility of the module to do its own
208escaping (which may be done automatically if the module uses ``render_string``
209and ``{{ }}``).
210"""
212import datetime
213import linecache
214import os.path
215import posixpath
216import re
217import threading
218import typing
219from collections.abc import Callable, Iterable
220from io import StringIO
221from typing import Any, ContextManager, Optional, TextIO
223from tornado import escape
224from tornado.log import app_log
225from tornado.util import ObjectDict, exec_in, unicode_type
227_DEFAULT_AUTOESCAPE = "xhtml_escape"
230class _UnsetMarker:
231 pass
234_UNSET = _UnsetMarker()
237def filter_whitespace(mode: str, text: str) -> str:
238 """Transform whitespace in ``text`` according to ``mode``.
240 Available modes are:
242 * ``all``: Return all whitespace unmodified.
243 * ``single``: Collapse consecutive whitespace with a single whitespace
244 character, preserving newlines.
245 * ``oneline``: Collapse all runs of whitespace into a single space
246 character, removing all newlines in the process.
248 .. versionadded:: 4.3
249 """
250 if mode == "all":
251 return text
252 elif mode == "single":
253 text = re.sub(r"([\t ]+)", " ", text)
254 text = re.sub(r"(\s*\n\s*)", "\n", text)
255 return text
256 elif mode == "oneline":
257 return re.sub(r"(\s+)", " ", text)
258 else:
259 raise Exception("invalid whitespace mode %s" % mode)
262class Template:
263 """A compiled template.
265 We compile into Python from the given template_string. You can generate
266 the template from variables with generate().
267 """
269 # note that the constructor's signature is not extracted with
270 # autodoc because _UNSET looks like garbage. When changing
271 # this signature update website/sphinx/template.rst too.
272 def __init__(
273 self,
274 template_string: str | bytes,
275 name: str = "<string>",
276 loader: Optional["BaseLoader"] = None,
277 compress_whitespace: bool | _UnsetMarker = _UNSET,
278 autoescape: str | _UnsetMarker | None = _UNSET,
279 whitespace: str | None = None,
280 ) -> None:
281 """Construct a Template.
283 :arg str template_string: the contents of the template file.
284 :arg str name: the filename from which the template was loaded
285 (used for error message).
286 :arg tornado.template.BaseLoader loader: the `~tornado.template.BaseLoader` responsible
287 for this template, used to resolve ``{% include %}`` and ``{% extend %}`` directives.
288 :arg bool compress_whitespace: Deprecated since Tornado 4.3.
289 Equivalent to ``whitespace="single"`` if true and
290 ``whitespace="all"`` if false.
291 :arg str autoescape: The name of a function in the template
292 namespace, or ``None`` to disable escaping by default.
293 :arg str whitespace: A string specifying treatment of whitespace;
294 see `filter_whitespace` for options.
296 .. versionchanged:: 4.3
297 Added ``whitespace`` parameter; deprecated ``compress_whitespace``.
298 """
299 self.name = escape.native_str(name)
301 if compress_whitespace is not _UNSET:
302 # Convert deprecated compress_whitespace (bool) to whitespace (str).
303 if whitespace is not None:
304 raise Exception("cannot set both whitespace and compress_whitespace")
305 whitespace = "single" if compress_whitespace else "all"
306 if whitespace is None:
307 if loader and loader.whitespace:
308 whitespace = loader.whitespace
309 else:
310 # Whitespace defaults by filename.
311 if name.endswith(".html") or name.endswith(".js"):
312 whitespace = "single"
313 else:
314 whitespace = "all"
315 # Validate the whitespace setting.
316 assert whitespace is not None
317 filter_whitespace(whitespace, "")
319 if not isinstance(autoescape, _UnsetMarker):
320 self.autoescape: str | None = autoescape
321 elif loader:
322 self.autoescape = loader.autoescape
323 else:
324 self.autoescape = _DEFAULT_AUTOESCAPE
326 self.namespace = loader.namespace if loader else {}
327 reader = _TemplateReader(name, escape.native_str(template_string), whitespace)
328 self.file = _File(self, _parse(reader, self))
329 self.code = self._generate_python(loader)
330 self.loader = loader
331 try:
332 # Under python2.5, the fake filename used here must match
333 # the module name used in __name__ below.
334 # The dont_inherit flag prevents template.py's future imports
335 # from being applied to the generated code.
336 self.compiled = compile(
337 escape.to_unicode(self.code),
338 "%s.generated.py" % self.name.replace(".", "_"),
339 "exec",
340 dont_inherit=True,
341 )
342 except Exception:
343 formatted_code = _format_code(self.code).rstrip()
344 app_log.error("%s code:\n%s", self.name, formatted_code)
345 raise
347 def generate(self, **kwargs: Any) -> bytes:
348 """Generate this template with the given arguments."""
349 namespace = {
350 "escape": escape.xhtml_escape,
351 "xhtml_escape": escape.xhtml_escape,
352 "url_escape": escape.url_escape,
353 "json_encode": escape.json_encode,
354 "squeeze": escape.squeeze,
355 "linkify": escape.linkify,
356 "datetime": datetime,
357 "_tt_utf8": escape.utf8, # for internal use
358 "_tt_string_types": (unicode_type, bytes),
359 # __name__ and __loader__ allow the traceback mechanism to find
360 # the generated source code.
361 "__name__": self.name.replace(".", "_"),
362 "__loader__": ObjectDict(get_source=lambda name: self.code),
363 }
364 namespace.update(self.namespace)
365 namespace.update(kwargs)
366 exec_in(self.compiled, namespace)
367 execute = typing.cast(Callable[[], bytes], namespace["_tt_execute"])
368 # Clear the traceback module's cache of source data now that
369 # we've generated a new template (mainly for this module's
370 # unittests, where different tests reuse the same name).
371 linecache.clearcache()
372 return execute()
374 def _generate_python(self, loader: Optional["BaseLoader"]) -> str:
375 buffer = StringIO()
376 try:
377 # named_blocks maps from names to _NamedBlock objects
378 named_blocks: dict[str, _NamedBlock] = {}
379 ancestors = self._get_ancestors(loader)
380 ancestors.reverse()
381 for ancestor in ancestors:
382 ancestor.find_named_blocks(loader, named_blocks)
383 writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template)
384 ancestors[0].generate(writer)
385 return buffer.getvalue()
386 finally:
387 buffer.close()
389 def _get_ancestors(self, loader: Optional["BaseLoader"]) -> list["_File"]:
390 ancestors = [self.file]
391 for chunk in self.file.body.chunks:
392 if isinstance(chunk, _ExtendsBlock):
393 if not loader:
394 raise ParseError(
395 "{% extends %} block found, but no " "template loader"
396 )
397 template = loader.load(chunk.name, self.name)
398 ancestors.extend(template._get_ancestors(loader))
399 return ancestors
402class BaseLoader:
403 """Base class for template loaders.
405 You must use a template loader to use template constructs like
406 ``{% extends %}`` and ``{% include %}``. The loader caches all
407 templates after they are loaded the first time.
408 """
410 def __init__(
411 self,
412 autoescape: str | None = _DEFAULT_AUTOESCAPE,
413 namespace: dict[str, Any] | None = None,
414 whitespace: str | None = None,
415 ) -> None:
416 """Construct a template loader.
418 :arg str autoescape: The name of a function in the template
419 namespace, such as "xhtml_escape", or ``None`` to disable
420 autoescaping by default.
421 :arg dict namespace: A dictionary to be added to the default template
422 namespace, or ``None``.
423 :arg str whitespace: A string specifying default behavior for
424 whitespace in templates; see `filter_whitespace` for options.
425 Default is "single" for files ending in ".html" and ".js" and
426 "all" for other files.
428 .. versionchanged:: 4.3
429 Added ``whitespace`` parameter.
430 """
431 self.autoescape = autoescape
432 self.namespace = namespace or {}
433 self.whitespace = whitespace
434 self.templates: dict[str, Template] = {}
435 # self.lock protects self.templates. It's a reentrant lock
436 # because templates may load other templates via `include` or
437 # `extends`. Note that thanks to the GIL this code would be safe
438 # even without the lock, but could lead to wasted work as multiple
439 # threads tried to compile the same template simultaneously.
440 self.lock = threading.RLock()
442 def reset(self) -> None:
443 """Resets the cache of compiled templates."""
444 with self.lock:
445 self.templates = {}
447 def resolve_path(self, name: str, parent_path: str | None = None) -> str:
448 """Converts a possibly-relative path to absolute (used internally)."""
449 raise NotImplementedError()
451 def load(self, name: str, parent_path: str | None = None) -> Template:
452 """Loads a template."""
453 name = self.resolve_path(name, parent_path=parent_path)
454 with self.lock:
455 if name not in self.templates:
456 self.templates[name] = self._create_template(name)
457 return self.templates[name]
459 def _create_template(self, name: str) -> Template:
460 raise NotImplementedError()
463class Loader(BaseLoader):
464 """A template loader that loads from the filesystem.
466 The ``load()`` method accepts both absolute and relative paths,
467 with relative paths being resolved relative to the ``root_directory``.
468 Relative paths in other template directives (such as ``{% include %}``)
469 are resolved relative to the including file's directory.
470 """
472 def __init__(self, root_directory: str, **kwargs: Any) -> None:
473 super().__init__(**kwargs)
474 self.root = os.path.abspath(root_directory)
476 def resolve_path(self, name: str, parent_path: str | None = None) -> str:
477 if (
478 parent_path
479 and not parent_path.startswith("<")
480 and not parent_path.startswith("/")
481 and not name.startswith("/")
482 ):
483 current_path = os.path.join(self.root, parent_path)
484 file_dir = os.path.dirname(os.path.abspath(current_path))
485 relative_path = os.path.abspath(os.path.join(file_dir, name))
486 if relative_path.startswith(self.root):
487 name = relative_path[len(self.root) + 1 :]
488 return name
490 def _create_template(self, name: str) -> Template:
491 path = os.path.join(self.root, name)
492 with open(path, "rb") as f:
493 template = Template(f.read(), name=name, loader=self)
494 return template
497class DictLoader(BaseLoader):
498 """A template loader that loads from a dictionary."""
500 def __init__(self, dict: dict[str, str], **kwargs: Any) -> None:
501 super().__init__(**kwargs)
502 self.dict = dict
504 def resolve_path(self, name: str, parent_path: str | None = None) -> str:
505 if (
506 parent_path
507 and not parent_path.startswith("<")
508 and not parent_path.startswith("/")
509 and not name.startswith("/")
510 ):
511 file_dir = posixpath.dirname(parent_path)
512 name = posixpath.normpath(posixpath.join(file_dir, name))
513 return name
515 def _create_template(self, name: str) -> Template:
516 return Template(self.dict[name], name=name, loader=self)
519class _Node:
520 def each_child(self) -> Iterable["_Node"]:
521 return ()
523 def generate(self, writer: "_CodeWriter") -> None:
524 raise NotImplementedError()
526 def find_named_blocks(
527 self, loader: BaseLoader | None, named_blocks: dict[str, "_NamedBlock"]
528 ) -> None:
529 for child in self.each_child():
530 child.find_named_blocks(loader, named_blocks)
533class _File(_Node):
534 def __init__(self, template: Template, body: "_ChunkList") -> None:
535 self.template = template
536 self.body = body
537 self.line = 0
539 def generate(self, writer: "_CodeWriter") -> None:
540 writer.write_line("def _tt_execute():", self.line)
541 with writer.indent():
542 writer.write_line("_tt_buffer = []", self.line)
543 writer.write_line("_tt_append = _tt_buffer.append", self.line)
544 self.body.generate(writer)
545 writer.write_line("return _tt_utf8('').join(_tt_buffer)", self.line)
547 def each_child(self) -> Iterable["_Node"]:
548 return (self.body,)
551class _ChunkList(_Node):
552 def __init__(self, chunks: list[_Node]) -> None:
553 self.chunks = chunks
555 def generate(self, writer: "_CodeWriter") -> None:
556 for chunk in self.chunks:
557 chunk.generate(writer)
559 def each_child(self) -> Iterable["_Node"]:
560 return self.chunks
563class _NamedBlock(_Node):
564 def __init__(self, name: str, body: _Node, template: Template, line: int) -> None:
565 self.name = name
566 self.body = body
567 self.template = template
568 self.line = line
570 def each_child(self) -> Iterable["_Node"]:
571 return (self.body,)
573 def generate(self, writer: "_CodeWriter") -> None:
574 block = writer.named_blocks[self.name]
575 with writer.include(block.template, self.line):
576 block.body.generate(writer)
578 def find_named_blocks(
579 self, loader: BaseLoader | None, named_blocks: dict[str, "_NamedBlock"]
580 ) -> None:
581 named_blocks[self.name] = self
582 _Node.find_named_blocks(self, loader, named_blocks)
585class _ExtendsBlock(_Node):
586 def __init__(self, name: str) -> None:
587 self.name = name
590class _IncludeBlock(_Node):
591 def __init__(self, name: str, reader: "_TemplateReader", line: int) -> None:
592 self.name = name
593 self.template_name = reader.name
594 self.line = line
596 def find_named_blocks(
597 self, loader: BaseLoader | None, named_blocks: dict[str, _NamedBlock]
598 ) -> None:
599 assert loader is not None
600 included = loader.load(self.name, self.template_name)
601 included.file.find_named_blocks(loader, named_blocks)
603 def generate(self, writer: "_CodeWriter") -> None:
604 assert writer.loader is not None
605 included = writer.loader.load(self.name, self.template_name)
606 with writer.include(included, self.line):
607 included.file.body.generate(writer)
610class _ApplyBlock(_Node):
611 def __init__(self, method: str, line: int, body: _Node) -> None:
612 self.method = method
613 self.line = line
614 self.body = body
616 def each_child(self) -> Iterable["_Node"]:
617 return (self.body,)
619 def generate(self, writer: "_CodeWriter") -> None:
620 method_name = "_tt_apply%d" % writer.apply_counter
621 writer.apply_counter += 1
622 writer.write_line("def %s():" % method_name, self.line)
623 with writer.indent():
624 writer.write_line("_tt_buffer = []", self.line)
625 writer.write_line("_tt_append = _tt_buffer.append", self.line)
626 self.body.generate(writer)
627 writer.write_line("return _tt_utf8('').join(_tt_buffer)", self.line)
628 writer.write_line(
629 f"_tt_append(_tt_utf8({self.method}({method_name}())))", self.line
630 )
633class _ControlBlock(_Node):
634 def __init__(self, statement: str, line: int, body: _Node) -> None:
635 self.statement = statement
636 self.line = line
637 self.body = body
639 def each_child(self) -> Iterable[_Node]:
640 return (self.body,)
642 def generate(self, writer: "_CodeWriter") -> None:
643 writer.write_line("%s:" % self.statement, self.line)
644 with writer.indent():
645 self.body.generate(writer)
646 # Just in case the body was empty
647 writer.write_line("pass", self.line)
650class _IntermediateControlBlock(_Node):
651 def __init__(self, statement: str, line: int) -> None:
652 self.statement = statement
653 self.line = line
655 def generate(self, writer: "_CodeWriter") -> None:
656 # In case the previous block was empty
657 writer.write_line("pass", self.line)
658 writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1)
661class _Statement(_Node):
662 def __init__(self, statement: str, line: int) -> None:
663 self.statement = statement
664 self.line = line
666 def generate(self, writer: "_CodeWriter") -> None:
667 writer.write_line(self.statement, self.line)
670class _Expression(_Node):
671 def __init__(self, expression: str, line: int, raw: bool = False) -> None:
672 self.expression = expression
673 self.line = line
674 self.raw = raw
676 def generate(self, writer: "_CodeWriter") -> None:
677 writer.write_line("_tt_tmp = %s" % self.expression, self.line)
678 writer.write_line(
679 "if isinstance(_tt_tmp, _tt_string_types):" " _tt_tmp = _tt_utf8(_tt_tmp)",
680 self.line,
681 )
682 writer.write_line("else: _tt_tmp = _tt_utf8(str(_tt_tmp))", self.line)
683 if not self.raw and writer.current_template.autoescape is not None:
684 # In python3 functions like xhtml_escape return unicode,
685 # so we have to convert to utf8 again.
686 writer.write_line(
687 "_tt_tmp = _tt_utf8(%s(_tt_tmp))" % writer.current_template.autoescape,
688 self.line,
689 )
690 writer.write_line("_tt_append(_tt_tmp)", self.line)
693class _Module(_Expression):
694 def __init__(self, expression: str, line: int) -> None:
695 super().__init__("_tt_modules." + expression, line, raw=True)
698class _Text(_Node):
699 def __init__(self, value: str, line: int, whitespace: str) -> None:
700 self.value = value
701 self.line = line
702 self.whitespace = whitespace
704 def generate(self, writer: "_CodeWriter") -> None:
705 value = self.value
707 # Compress whitespace if requested, with a crude heuristic to avoid
708 # altering preformatted whitespace.
709 if "<pre>" not in value:
710 value = filter_whitespace(self.whitespace, value)
712 if value:
713 writer.write_line("_tt_append(%r)" % escape.utf8(value), self.line)
716class ParseError(Exception):
717 """Raised for template syntax errors.
719 ``ParseError`` instances have ``filename`` and ``lineno`` attributes
720 indicating the position of the error.
722 .. versionchanged:: 4.3
723 Added ``filename`` and ``lineno`` attributes.
724 """
726 def __init__(
727 self, message: str, filename: str | None = None, lineno: int = 0
728 ) -> None:
729 self.message = message
730 # The names "filename" and "lineno" are chosen for consistency
731 # with python SyntaxError.
732 self.filename = filename
733 self.lineno = lineno
735 def __str__(self) -> str:
736 return "%s at %s:%d" % (self.message, self.filename, self.lineno)
739class _CodeWriter:
740 def __init__(
741 self,
742 file: TextIO,
743 named_blocks: dict[str, _NamedBlock],
744 loader: BaseLoader | None,
745 current_template: Template,
746 ) -> None:
747 self.file = file
748 self.named_blocks = named_blocks
749 self.loader = loader
750 self.current_template = current_template
751 self.apply_counter = 0
752 self.include_stack: list[tuple[Template, int]] = []
753 self._indent = 0
755 def indent_size(self) -> int:
756 return self._indent
758 def indent(self) -> "ContextManager":
759 class Indenter:
760 def __enter__(_) -> "_CodeWriter":
761 self._indent += 1
762 return self
764 def __exit__(_, *args: Any) -> None:
765 assert self._indent > 0
766 self._indent -= 1
768 return Indenter()
770 def include(self, template: Template, line: int) -> "ContextManager":
771 self.include_stack.append((self.current_template, line))
772 self.current_template = template
774 class IncludeTemplate:
775 def __enter__(_) -> "_CodeWriter":
776 return self
778 def __exit__(_, *args: Any) -> None:
779 self.current_template = self.include_stack.pop()[0]
781 return IncludeTemplate()
783 def write_line(
784 self, line: str, line_number: int, indent: int | None = None
785 ) -> None:
786 if indent is None:
787 indent = self._indent
788 line_comment = " # %s:%d" % (self.current_template.name, line_number)
789 if self.include_stack:
790 ancestors = [
791 "%s:%d" % (tmpl.name, lineno) for (tmpl, lineno) in self.include_stack
792 ]
793 line_comment += " (via %s)" % ", ".join(reversed(ancestors))
794 print(" " * indent + line + line_comment, file=self.file)
797class _TemplateReader:
798 def __init__(self, name: str, text: str, whitespace: str) -> None:
799 self.name = name
800 self.text = text
801 self.whitespace = whitespace
802 self.line = 1
803 self.pos = 0
805 def find(self, needle: str, start: int = 0, end: int | None = None) -> int:
806 assert start >= 0, start
807 pos = self.pos
808 start += pos
809 if end is None:
810 index = self.text.find(needle, start)
811 else:
812 end += pos
813 assert end >= start
814 index = self.text.find(needle, start, end)
815 if index != -1:
816 index -= pos
817 return index
819 def consume(self, count: int | None = None) -> str:
820 if count is None:
821 count = len(self.text) - self.pos
822 newpos = self.pos + count
823 self.line += self.text.count("\n", self.pos, newpos)
824 s = self.text[self.pos : newpos]
825 self.pos = newpos
826 return s
828 def remaining(self) -> int:
829 return len(self.text) - self.pos
831 def __len__(self) -> int:
832 return self.remaining()
834 def __getitem__(self, key: int | slice) -> str:
835 if isinstance(key, slice):
836 size = len(self)
837 start, stop, step = key.indices(size)
838 if start is None:
839 start = self.pos
840 else:
841 start += self.pos
842 if stop is not None:
843 stop += self.pos
844 return self.text[slice(start, stop, step)]
845 elif key < 0:
846 return self.text[key]
847 else:
848 return self.text[self.pos + key]
850 def __str__(self) -> str:
851 return self.text[self.pos :]
853 def raise_parse_error(self, msg: str) -> None:
854 raise ParseError(msg, self.name, self.line)
857def _format_code(code: str) -> str:
858 lines = code.splitlines()
859 format = "%%%dd %%s\n" % len(repr(len(lines) + 1))
860 return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
863def _parse(
864 reader: _TemplateReader,
865 template: Template,
866 in_block: str | None = None,
867 in_loop: str | None = None,
868) -> _ChunkList:
869 body = _ChunkList([])
870 while True:
871 # Find next template directive
872 curly = 0
873 while True:
874 curly = reader.find("{", curly)
875 if curly == -1 or curly + 1 == reader.remaining():
876 # EOF
877 if in_block:
878 reader.raise_parse_error(
879 "Missing {%% end %%} block for %s" % in_block
880 )
881 body.chunks.append(
882 _Text(reader.consume(), reader.line, reader.whitespace)
883 )
884 return body
885 # If the first curly brace is not the start of a special token,
886 # start searching from the character after it
887 if reader[curly + 1] not in ("{", "%", "#"):
888 curly += 1
889 continue
890 # When there are more than 2 curlies in a row, use the
891 # innermost ones. This is useful when generating languages
892 # like latex where curlies are also meaningful
893 if (
894 curly + 2 < reader.remaining()
895 and reader[curly + 1] == "{"
896 and reader[curly + 2] == "{"
897 ):
898 curly += 1
899 continue
900 break
902 # Append any text before the special token
903 if curly > 0:
904 cons = reader.consume(curly)
905 body.chunks.append(_Text(cons, reader.line, reader.whitespace))
907 start_brace = reader.consume(2)
908 line = reader.line
910 # Template directives may be escaped as "{{!" or "{%!".
911 # In this case output the braces and consume the "!".
912 # This is especially useful in conjunction with jquery templates,
913 # which also use double braces.
914 if reader.remaining() and reader[0] == "!":
915 reader.consume(1)
916 body.chunks.append(_Text(start_brace, line, reader.whitespace))
917 continue
919 # Comment
920 if start_brace == "{#":
921 end = reader.find("#}")
922 if end == -1:
923 reader.raise_parse_error("Missing end comment #}")
924 contents = reader.consume(end).strip()
925 reader.consume(2)
926 continue
928 # Expression
929 if start_brace == "{{":
930 end = reader.find("}}")
931 if end == -1:
932 reader.raise_parse_error("Missing end expression }}")
933 contents = reader.consume(end).strip()
934 reader.consume(2)
935 if not contents:
936 reader.raise_parse_error("Empty expression")
937 body.chunks.append(_Expression(contents, line))
938 continue
940 # Block
941 assert start_brace == "{%", start_brace
942 end = reader.find("%}")
943 if end == -1:
944 reader.raise_parse_error("Missing end block %}")
945 contents = reader.consume(end).strip()
946 reader.consume(2)
947 if not contents:
948 reader.raise_parse_error("Empty block tag ({% %})")
950 operator, space, suffix = contents.partition(" ")
951 suffix = suffix.strip()
953 # Intermediate ("else", "elif", etc) blocks
954 intermediate_blocks = {
955 "else": {"if", "for", "while", "try"},
956 "elif": {"if"},
957 "except": {"try"},
958 "finally": {"try"},
959 }
960 allowed_parents = intermediate_blocks.get(operator)
961 if allowed_parents is not None:
962 if not in_block:
963 reader.raise_parse_error(f"{operator} outside {allowed_parents} block")
964 if in_block not in allowed_parents:
965 reader.raise_parse_error(
966 f"{operator} block cannot be attached to {in_block} block"
967 )
968 body.chunks.append(_IntermediateControlBlock(contents, line))
969 continue
971 # End tag
972 elif operator == "end":
973 if not in_block:
974 reader.raise_parse_error("Extra {% end %} block")
975 return body
977 elif operator in (
978 "extends",
979 "include",
980 "set",
981 "import",
982 "from",
983 "comment",
984 "autoescape",
985 "whitespace",
986 "raw",
987 "module",
988 ):
989 if operator == "comment":
990 continue
991 if operator == "extends":
992 suffix = suffix.strip('"').strip("'")
993 if not suffix:
994 reader.raise_parse_error("extends missing file path")
995 block: _Node = _ExtendsBlock(suffix)
996 elif operator in ("import", "from"):
997 if not suffix:
998 reader.raise_parse_error("import missing statement")
999 block = _Statement(contents, line)
1000 elif operator == "include":
1001 suffix = suffix.strip('"').strip("'")
1002 if not suffix:
1003 reader.raise_parse_error("include missing file path")
1004 block = _IncludeBlock(suffix, reader, line)
1005 elif operator == "set":
1006 if not suffix:
1007 reader.raise_parse_error("set missing statement")
1008 block = _Statement(suffix, line)
1009 elif operator == "autoescape":
1010 fn: str | None = suffix.strip()
1011 if fn == "None":
1012 fn = None
1013 template.autoescape = fn
1014 continue
1015 elif operator == "whitespace":
1016 mode = suffix.strip()
1017 # Validate the selected mode
1018 filter_whitespace(mode, "")
1019 reader.whitespace = mode
1020 continue
1021 elif operator == "raw":
1022 block = _Expression(suffix, line, raw=True)
1023 elif operator == "module":
1024 block = _Module(suffix, line)
1025 body.chunks.append(block)
1026 continue
1028 elif operator in ("apply", "block", "try", "if", "for", "while"):
1029 # parse inner body recursively
1030 if operator in ("for", "while"):
1031 block_body = _parse(reader, template, operator, operator)
1032 elif operator == "apply":
1033 # apply creates a nested function so syntactically it's not
1034 # in the loop.
1035 block_body = _parse(reader, template, operator, None)
1036 else:
1037 block_body = _parse(reader, template, operator, in_loop)
1039 if operator == "apply":
1040 if not suffix:
1041 reader.raise_parse_error("apply missing method name")
1042 block = _ApplyBlock(suffix, line, block_body)
1043 elif operator == "block":
1044 if not suffix:
1045 reader.raise_parse_error("block missing name")
1046 block = _NamedBlock(suffix, block_body, template, line)
1047 else:
1048 block = _ControlBlock(contents, line, block_body)
1049 body.chunks.append(block)
1050 continue
1052 elif operator in ("break", "continue"):
1053 if not in_loop:
1054 reader.raise_parse_error(
1055 "{} outside {} block".format(operator, {"for", "while"})
1056 )
1057 body.chunks.append(_Statement(contents, line))
1058 continue
1060 else:
1061 reader.raise_parse_error("unknown operator: %r" % operator)