Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/tornado/template.py: 21%
489 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
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.
197"""
199import datetime
200from io import StringIO
201import linecache
202import os.path
203import posixpath
204import re
205import threading
207from tornado import escape
208from tornado.log import app_log
209from tornado.util import ObjectDict, exec_in, unicode_type
211from typing import Any, Union, Callable, List, Dict, Iterable, Optional, TextIO
212import typing
214if typing.TYPE_CHECKING:
215 from typing import Tuple, ContextManager # noqa: F401
217_DEFAULT_AUTOESCAPE = "xhtml_escape"
220class _UnsetMarker:
221 pass
224_UNSET = _UnsetMarker()
227def filter_whitespace(mode: str, text: str) -> str:
228 """Transform whitespace in ``text`` according to ``mode``.
230 Available modes are:
232 * ``all``: Return all whitespace unmodified.
233 * ``single``: Collapse consecutive whitespace with a single whitespace
234 character, preserving newlines.
235 * ``oneline``: Collapse all runs of whitespace into a single space
236 character, removing all newlines in the process.
238 .. versionadded:: 4.3
239 """
240 if mode == "all":
241 return text
242 elif mode == "single":
243 text = re.sub(r"([\t ]+)", " ", text)
244 text = re.sub(r"(\s*\n\s*)", "\n", text)
245 return text
246 elif mode == "oneline":
247 return re.sub(r"(\s+)", " ", text)
248 else:
249 raise Exception("invalid whitespace mode %s" % mode)
252class Template(object):
253 """A compiled template.
255 We compile into Python from the given template_string. You can generate
256 the template from variables with generate().
257 """
259 # note that the constructor's signature is not extracted with
260 # autodoc because _UNSET looks like garbage. When changing
261 # this signature update website/sphinx/template.rst too.
262 def __init__(
263 self,
264 template_string: Union[str, bytes],
265 name: str = "<string>",
266 loader: Optional["BaseLoader"] = None,
267 compress_whitespace: Union[bool, _UnsetMarker] = _UNSET,
268 autoescape: Optional[Union[str, _UnsetMarker]] = _UNSET,
269 whitespace: Optional[str] = None,
270 ) -> None:
271 """Construct a Template.
273 :arg str template_string: the contents of the template file.
274 :arg str name: the filename from which the template was loaded
275 (used for error message).
276 :arg tornado.template.BaseLoader loader: the `~tornado.template.BaseLoader` responsible
277 for this template, used to resolve ``{% include %}`` and ``{% extend %}`` directives.
278 :arg bool compress_whitespace: Deprecated since Tornado 4.3.
279 Equivalent to ``whitespace="single"`` if true and
280 ``whitespace="all"`` if false.
281 :arg str autoescape: The name of a function in the template
282 namespace, or ``None`` to disable escaping by default.
283 :arg str whitespace: A string specifying treatment of whitespace;
284 see `filter_whitespace` for options.
286 .. versionchanged:: 4.3
287 Added ``whitespace`` parameter; deprecated ``compress_whitespace``.
288 """
289 self.name = escape.native_str(name)
291 if compress_whitespace is not _UNSET:
292 # Convert deprecated compress_whitespace (bool) to whitespace (str).
293 if whitespace is not None:
294 raise Exception("cannot set both whitespace and compress_whitespace")
295 whitespace = "single" if compress_whitespace else "all"
296 if whitespace is None:
297 if loader and loader.whitespace:
298 whitespace = loader.whitespace
299 else:
300 # Whitespace defaults by filename.
301 if name.endswith(".html") or name.endswith(".js"):
302 whitespace = "single"
303 else:
304 whitespace = "all"
305 # Validate the whitespace setting.
306 assert whitespace is not None
307 filter_whitespace(whitespace, "")
309 if not isinstance(autoescape, _UnsetMarker):
310 self.autoescape = autoescape # type: Optional[str]
311 elif loader:
312 self.autoescape = loader.autoescape
313 else:
314 self.autoescape = _DEFAULT_AUTOESCAPE
316 self.namespace = loader.namespace if loader else {}
317 reader = _TemplateReader(name, escape.native_str(template_string), whitespace)
318 self.file = _File(self, _parse(reader, self))
319 self.code = self._generate_python(loader)
320 self.loader = loader
321 try:
322 # Under python2.5, the fake filename used here must match
323 # the module name used in __name__ below.
324 # The dont_inherit flag prevents template.py's future imports
325 # from being applied to the generated code.
326 self.compiled = compile(
327 escape.to_unicode(self.code),
328 "%s.generated.py" % self.name.replace(".", "_"),
329 "exec",
330 dont_inherit=True,
331 )
332 except Exception:
333 formatted_code = _format_code(self.code).rstrip()
334 app_log.error("%s code:\n%s", self.name, formatted_code)
335 raise
337 def generate(self, **kwargs: Any) -> bytes:
338 """Generate this template with the given arguments."""
339 namespace = {
340 "escape": escape.xhtml_escape,
341 "xhtml_escape": escape.xhtml_escape,
342 "url_escape": escape.url_escape,
343 "json_encode": escape.json_encode,
344 "squeeze": escape.squeeze,
345 "linkify": escape.linkify,
346 "datetime": datetime,
347 "_tt_utf8": escape.utf8, # for internal use
348 "_tt_string_types": (unicode_type, bytes),
349 # __name__ and __loader__ allow the traceback mechanism to find
350 # the generated source code.
351 "__name__": self.name.replace(".", "_"),
352 "__loader__": ObjectDict(get_source=lambda name: self.code),
353 }
354 namespace.update(self.namespace)
355 namespace.update(kwargs)
356 exec_in(self.compiled, namespace)
357 execute = typing.cast(Callable[[], bytes], namespace["_tt_execute"])
358 # Clear the traceback module's cache of source data now that
359 # we've generated a new template (mainly for this module's
360 # unittests, where different tests reuse the same name).
361 linecache.clearcache()
362 return execute()
364 def _generate_python(self, loader: Optional["BaseLoader"]) -> str:
365 buffer = StringIO()
366 try:
367 # named_blocks maps from names to _NamedBlock objects
368 named_blocks = {} # type: Dict[str, _NamedBlock]
369 ancestors = self._get_ancestors(loader)
370 ancestors.reverse()
371 for ancestor in ancestors:
372 ancestor.find_named_blocks(loader, named_blocks)
373 writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template)
374 ancestors[0].generate(writer)
375 return buffer.getvalue()
376 finally:
377 buffer.close()
379 def _get_ancestors(self, loader: Optional["BaseLoader"]) -> List["_File"]:
380 ancestors = [self.file]
381 for chunk in self.file.body.chunks:
382 if isinstance(chunk, _ExtendsBlock):
383 if not loader:
384 raise ParseError(
385 "{% extends %} block found, but no " "template loader"
386 )
387 template = loader.load(chunk.name, self.name)
388 ancestors.extend(template._get_ancestors(loader))
389 return ancestors
392class BaseLoader(object):
393 """Base class for template loaders.
395 You must use a template loader to use template constructs like
396 ``{% extends %}`` and ``{% include %}``. The loader caches all
397 templates after they are loaded the first time.
398 """
400 def __init__(
401 self,
402 autoescape: str = _DEFAULT_AUTOESCAPE,
403 namespace: Optional[Dict[str, Any]] = None,
404 whitespace: Optional[str] = None,
405 ) -> None:
406 """Construct a template loader.
408 :arg str autoescape: The name of a function in the template
409 namespace, such as "xhtml_escape", or ``None`` to disable
410 autoescaping by default.
411 :arg dict namespace: A dictionary to be added to the default template
412 namespace, or ``None``.
413 :arg str whitespace: A string specifying default behavior for
414 whitespace in templates; see `filter_whitespace` for options.
415 Default is "single" for files ending in ".html" and ".js" and
416 "all" for other files.
418 .. versionchanged:: 4.3
419 Added ``whitespace`` parameter.
420 """
421 self.autoescape = autoescape
422 self.namespace = namespace or {}
423 self.whitespace = whitespace
424 self.templates = {} # type: Dict[str, Template]
425 # self.lock protects self.templates. It's a reentrant lock
426 # because templates may load other templates via `include` or
427 # `extends`. Note that thanks to the GIL this code would be safe
428 # even without the lock, but could lead to wasted work as multiple
429 # threads tried to compile the same template simultaneously.
430 self.lock = threading.RLock()
432 def reset(self) -> None:
433 """Resets the cache of compiled templates."""
434 with self.lock:
435 self.templates = {}
437 def resolve_path(self, name: str, parent_path: Optional[str] = None) -> str:
438 """Converts a possibly-relative path to absolute (used internally)."""
439 raise NotImplementedError()
441 def load(self, name: str, parent_path: Optional[str] = None) -> Template:
442 """Loads a template."""
443 name = self.resolve_path(name, parent_path=parent_path)
444 with self.lock:
445 if name not in self.templates:
446 self.templates[name] = self._create_template(name)
447 return self.templates[name]
449 def _create_template(self, name: str) -> Template:
450 raise NotImplementedError()
453class Loader(BaseLoader):
454 """A template loader that loads from a single root directory."""
456 def __init__(self, root_directory: str, **kwargs: Any) -> None:
457 super().__init__(**kwargs)
458 self.root = os.path.abspath(root_directory)
460 def resolve_path(self, name: str, parent_path: Optional[str] = None) -> str:
461 if (
462 parent_path
463 and not parent_path.startswith("<")
464 and not parent_path.startswith("/")
465 and not name.startswith("/")
466 ):
467 current_path = os.path.join(self.root, parent_path)
468 file_dir = os.path.dirname(os.path.abspath(current_path))
469 relative_path = os.path.abspath(os.path.join(file_dir, name))
470 if relative_path.startswith(self.root):
471 name = relative_path[len(self.root) + 1 :]
472 return name
474 def _create_template(self, name: str) -> Template:
475 path = os.path.join(self.root, name)
476 with open(path, "rb") as f:
477 template = Template(f.read(), name=name, loader=self)
478 return template
481class DictLoader(BaseLoader):
482 """A template loader that loads from a dictionary."""
484 def __init__(self, dict: Dict[str, str], **kwargs: Any) -> None:
485 super().__init__(**kwargs)
486 self.dict = dict
488 def resolve_path(self, name: str, parent_path: Optional[str] = None) -> str:
489 if (
490 parent_path
491 and not parent_path.startswith("<")
492 and not parent_path.startswith("/")
493 and not name.startswith("/")
494 ):
495 file_dir = posixpath.dirname(parent_path)
496 name = posixpath.normpath(posixpath.join(file_dir, name))
497 return name
499 def _create_template(self, name: str) -> Template:
500 return Template(self.dict[name], name=name, loader=self)
503class _Node(object):
504 def each_child(self) -> Iterable["_Node"]:
505 return ()
507 def generate(self, writer: "_CodeWriter") -> None:
508 raise NotImplementedError()
510 def find_named_blocks(
511 self, loader: Optional[BaseLoader], named_blocks: Dict[str, "_NamedBlock"]
512 ) -> None:
513 for child in self.each_child():
514 child.find_named_blocks(loader, named_blocks)
517class _File(_Node):
518 def __init__(self, template: Template, body: "_ChunkList") -> None:
519 self.template = template
520 self.body = body
521 self.line = 0
523 def generate(self, writer: "_CodeWriter") -> None:
524 writer.write_line("def _tt_execute():", self.line)
525 with writer.indent():
526 writer.write_line("_tt_buffer = []", self.line)
527 writer.write_line("_tt_append = _tt_buffer.append", self.line)
528 self.body.generate(writer)
529 writer.write_line("return _tt_utf8('').join(_tt_buffer)", self.line)
531 def each_child(self) -> Iterable["_Node"]:
532 return (self.body,)
535class _ChunkList(_Node):
536 def __init__(self, chunks: List[_Node]) -> None:
537 self.chunks = chunks
539 def generate(self, writer: "_CodeWriter") -> None:
540 for chunk in self.chunks:
541 chunk.generate(writer)
543 def each_child(self) -> Iterable["_Node"]:
544 return self.chunks
547class _NamedBlock(_Node):
548 def __init__(self, name: str, body: _Node, template: Template, line: int) -> None:
549 self.name = name
550 self.body = body
551 self.template = template
552 self.line = line
554 def each_child(self) -> Iterable["_Node"]:
555 return (self.body,)
557 def generate(self, writer: "_CodeWriter") -> None:
558 block = writer.named_blocks[self.name]
559 with writer.include(block.template, self.line):
560 block.body.generate(writer)
562 def find_named_blocks(
563 self, loader: Optional[BaseLoader], named_blocks: Dict[str, "_NamedBlock"]
564 ) -> None:
565 named_blocks[self.name] = self
566 _Node.find_named_blocks(self, loader, named_blocks)
569class _ExtendsBlock(_Node):
570 def __init__(self, name: str) -> None:
571 self.name = name
574class _IncludeBlock(_Node):
575 def __init__(self, name: str, reader: "_TemplateReader", line: int) -> None:
576 self.name = name
577 self.template_name = reader.name
578 self.line = line
580 def find_named_blocks(
581 self, loader: Optional[BaseLoader], named_blocks: Dict[str, _NamedBlock]
582 ) -> None:
583 assert loader is not None
584 included = loader.load(self.name, self.template_name)
585 included.file.find_named_blocks(loader, named_blocks)
587 def generate(self, writer: "_CodeWriter") -> None:
588 assert writer.loader is not None
589 included = writer.loader.load(self.name, self.template_name)
590 with writer.include(included, self.line):
591 included.file.body.generate(writer)
594class _ApplyBlock(_Node):
595 def __init__(self, method: str, line: int, body: _Node) -> None:
596 self.method = method
597 self.line = line
598 self.body = body
600 def each_child(self) -> Iterable["_Node"]:
601 return (self.body,)
603 def generate(self, writer: "_CodeWriter") -> None:
604 method_name = "_tt_apply%d" % writer.apply_counter
605 writer.apply_counter += 1
606 writer.write_line("def %s():" % method_name, self.line)
607 with writer.indent():
608 writer.write_line("_tt_buffer = []", self.line)
609 writer.write_line("_tt_append = _tt_buffer.append", self.line)
610 self.body.generate(writer)
611 writer.write_line("return _tt_utf8('').join(_tt_buffer)", self.line)
612 writer.write_line(
613 "_tt_append(_tt_utf8(%s(%s())))" % (self.method, method_name), self.line
614 )
617class _ControlBlock(_Node):
618 def __init__(self, statement: str, line: int, body: _Node) -> None:
619 self.statement = statement
620 self.line = line
621 self.body = body
623 def each_child(self) -> Iterable[_Node]:
624 return (self.body,)
626 def generate(self, writer: "_CodeWriter") -> None:
627 writer.write_line("%s:" % self.statement, self.line)
628 with writer.indent():
629 self.body.generate(writer)
630 # Just in case the body was empty
631 writer.write_line("pass", self.line)
634class _IntermediateControlBlock(_Node):
635 def __init__(self, statement: str, line: int) -> None:
636 self.statement = statement
637 self.line = line
639 def generate(self, writer: "_CodeWriter") -> None:
640 # In case the previous block was empty
641 writer.write_line("pass", self.line)
642 writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1)
645class _Statement(_Node):
646 def __init__(self, statement: str, line: int) -> None:
647 self.statement = statement
648 self.line = line
650 def generate(self, writer: "_CodeWriter") -> None:
651 writer.write_line(self.statement, self.line)
654class _Expression(_Node):
655 def __init__(self, expression: str, line: int, raw: bool = False) -> None:
656 self.expression = expression
657 self.line = line
658 self.raw = raw
660 def generate(self, writer: "_CodeWriter") -> None:
661 writer.write_line("_tt_tmp = %s" % self.expression, self.line)
662 writer.write_line(
663 "if isinstance(_tt_tmp, _tt_string_types):" " _tt_tmp = _tt_utf8(_tt_tmp)",
664 self.line,
665 )
666 writer.write_line("else: _tt_tmp = _tt_utf8(str(_tt_tmp))", self.line)
667 if not self.raw and writer.current_template.autoescape is not None:
668 # In python3 functions like xhtml_escape return unicode,
669 # so we have to convert to utf8 again.
670 writer.write_line(
671 "_tt_tmp = _tt_utf8(%s(_tt_tmp))" % writer.current_template.autoescape,
672 self.line,
673 )
674 writer.write_line("_tt_append(_tt_tmp)", self.line)
677class _Module(_Expression):
678 def __init__(self, expression: str, line: int) -> None:
679 super().__init__("_tt_modules." + expression, line, raw=True)
682class _Text(_Node):
683 def __init__(self, value: str, line: int, whitespace: str) -> None:
684 self.value = value
685 self.line = line
686 self.whitespace = whitespace
688 def generate(self, writer: "_CodeWriter") -> None:
689 value = self.value
691 # Compress whitespace if requested, with a crude heuristic to avoid
692 # altering preformatted whitespace.
693 if "<pre>" not in value:
694 value = filter_whitespace(self.whitespace, value)
696 if value:
697 writer.write_line("_tt_append(%r)" % escape.utf8(value), self.line)
700class ParseError(Exception):
701 """Raised for template syntax errors.
703 ``ParseError`` instances have ``filename`` and ``lineno`` attributes
704 indicating the position of the error.
706 .. versionchanged:: 4.3
707 Added ``filename`` and ``lineno`` attributes.
708 """
710 def __init__(
711 self, message: str, filename: Optional[str] = None, lineno: int = 0
712 ) -> None:
713 self.message = message
714 # The names "filename" and "lineno" are chosen for consistency
715 # with python SyntaxError.
716 self.filename = filename
717 self.lineno = lineno
719 def __str__(self) -> str:
720 return "%s at %s:%d" % (self.message, self.filename, self.lineno)
723class _CodeWriter(object):
724 def __init__(
725 self,
726 file: TextIO,
727 named_blocks: Dict[str, _NamedBlock],
728 loader: Optional[BaseLoader],
729 current_template: Template,
730 ) -> None:
731 self.file = file
732 self.named_blocks = named_blocks
733 self.loader = loader
734 self.current_template = current_template
735 self.apply_counter = 0
736 self.include_stack = [] # type: List[Tuple[Template, int]]
737 self._indent = 0
739 def indent_size(self) -> int:
740 return self._indent
742 def indent(self) -> "ContextManager":
743 class Indenter(object):
744 def __enter__(_) -> "_CodeWriter":
745 self._indent += 1
746 return self
748 def __exit__(_, *args: Any) -> None:
749 assert self._indent > 0
750 self._indent -= 1
752 return Indenter()
754 def include(self, template: Template, line: int) -> "ContextManager":
755 self.include_stack.append((self.current_template, line))
756 self.current_template = template
758 class IncludeTemplate(object):
759 def __enter__(_) -> "_CodeWriter":
760 return self
762 def __exit__(_, *args: Any) -> None:
763 self.current_template = self.include_stack.pop()[0]
765 return IncludeTemplate()
767 def write_line(
768 self, line: str, line_number: int, indent: Optional[int] = None
769 ) -> None:
770 if indent is None:
771 indent = self._indent
772 line_comment = " # %s:%d" % (self.current_template.name, line_number)
773 if self.include_stack:
774 ancestors = [
775 "%s:%d" % (tmpl.name, lineno) for (tmpl, lineno) in self.include_stack
776 ]
777 line_comment += " (via %s)" % ", ".join(reversed(ancestors))
778 print(" " * indent + line + line_comment, file=self.file)
781class _TemplateReader(object):
782 def __init__(self, name: str, text: str, whitespace: str) -> None:
783 self.name = name
784 self.text = text
785 self.whitespace = whitespace
786 self.line = 1
787 self.pos = 0
789 def find(self, needle: str, start: int = 0, end: Optional[int] = None) -> int:
790 assert start >= 0, start
791 pos = self.pos
792 start += pos
793 if end is None:
794 index = self.text.find(needle, start)
795 else:
796 end += pos
797 assert end >= start
798 index = self.text.find(needle, start, end)
799 if index != -1:
800 index -= pos
801 return index
803 def consume(self, count: Optional[int] = None) -> str:
804 if count is None:
805 count = len(self.text) - self.pos
806 newpos = self.pos + count
807 self.line += self.text.count("\n", self.pos, newpos)
808 s = self.text[self.pos : newpos]
809 self.pos = newpos
810 return s
812 def remaining(self) -> int:
813 return len(self.text) - self.pos
815 def __len__(self) -> int:
816 return self.remaining()
818 def __getitem__(self, key: Union[int, slice]) -> str:
819 if isinstance(key, slice):
820 size = len(self)
821 start, stop, step = key.indices(size)
822 if start is None:
823 start = self.pos
824 else:
825 start += self.pos
826 if stop is not None:
827 stop += self.pos
828 return self.text[slice(start, stop, step)]
829 elif key < 0:
830 return self.text[key]
831 else:
832 return self.text[self.pos + key]
834 def __str__(self) -> str:
835 return self.text[self.pos :]
837 def raise_parse_error(self, msg: str) -> None:
838 raise ParseError(msg, self.name, self.line)
841def _format_code(code: str) -> str:
842 lines = code.splitlines()
843 format = "%%%dd %%s\n" % len(repr(len(lines) + 1))
844 return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
847def _parse(
848 reader: _TemplateReader,
849 template: Template,
850 in_block: Optional[str] = None,
851 in_loop: Optional[str] = None,
852) -> _ChunkList:
853 body = _ChunkList([])
854 while True:
855 # Find next template directive
856 curly = 0
857 while True:
858 curly = reader.find("{", curly)
859 if curly == -1 or curly + 1 == reader.remaining():
860 # EOF
861 if in_block:
862 reader.raise_parse_error(
863 "Missing {%% end %%} block for %s" % in_block
864 )
865 body.chunks.append(
866 _Text(reader.consume(), reader.line, reader.whitespace)
867 )
868 return body
869 # If the first curly brace is not the start of a special token,
870 # start searching from the character after it
871 if reader[curly + 1] not in ("{", "%", "#"):
872 curly += 1
873 continue
874 # When there are more than 2 curlies in a row, use the
875 # innermost ones. This is useful when generating languages
876 # like latex where curlies are also meaningful
877 if (
878 curly + 2 < reader.remaining()
879 and reader[curly + 1] == "{"
880 and reader[curly + 2] == "{"
881 ):
882 curly += 1
883 continue
884 break
886 # Append any text before the special token
887 if curly > 0:
888 cons = reader.consume(curly)
889 body.chunks.append(_Text(cons, reader.line, reader.whitespace))
891 start_brace = reader.consume(2)
892 line = reader.line
894 # Template directives may be escaped as "{{!" or "{%!".
895 # In this case output the braces and consume the "!".
896 # This is especially useful in conjunction with jquery templates,
897 # which also use double braces.
898 if reader.remaining() and reader[0] == "!":
899 reader.consume(1)
900 body.chunks.append(_Text(start_brace, line, reader.whitespace))
901 continue
903 # Comment
904 if start_brace == "{#":
905 end = reader.find("#}")
906 if end == -1:
907 reader.raise_parse_error("Missing end comment #}")
908 contents = reader.consume(end).strip()
909 reader.consume(2)
910 continue
912 # Expression
913 if start_brace == "{{":
914 end = reader.find("}}")
915 if end == -1:
916 reader.raise_parse_error("Missing end expression }}")
917 contents = reader.consume(end).strip()
918 reader.consume(2)
919 if not contents:
920 reader.raise_parse_error("Empty expression")
921 body.chunks.append(_Expression(contents, line))
922 continue
924 # Block
925 assert start_brace == "{%", start_brace
926 end = reader.find("%}")
927 if end == -1:
928 reader.raise_parse_error("Missing end block %}")
929 contents = reader.consume(end).strip()
930 reader.consume(2)
931 if not contents:
932 reader.raise_parse_error("Empty block tag ({% %})")
934 operator, space, suffix = contents.partition(" ")
935 suffix = suffix.strip()
937 # Intermediate ("else", "elif", etc) blocks
938 intermediate_blocks = {
939 "else": set(["if", "for", "while", "try"]),
940 "elif": set(["if"]),
941 "except": set(["try"]),
942 "finally": set(["try"]),
943 }
944 allowed_parents = intermediate_blocks.get(operator)
945 if allowed_parents is not None:
946 if not in_block:
947 reader.raise_parse_error(
948 "%s outside %s block" % (operator, allowed_parents)
949 )
950 if in_block not in allowed_parents:
951 reader.raise_parse_error(
952 "%s block cannot be attached to %s block" % (operator, in_block)
953 )
954 body.chunks.append(_IntermediateControlBlock(contents, line))
955 continue
957 # End tag
958 elif operator == "end":
959 if not in_block:
960 reader.raise_parse_error("Extra {% end %} block")
961 return body
963 elif operator in (
964 "extends",
965 "include",
966 "set",
967 "import",
968 "from",
969 "comment",
970 "autoescape",
971 "whitespace",
972 "raw",
973 "module",
974 ):
975 if operator == "comment":
976 continue
977 if operator == "extends":
978 suffix = suffix.strip('"').strip("'")
979 if not suffix:
980 reader.raise_parse_error("extends missing file path")
981 block = _ExtendsBlock(suffix) # type: _Node
982 elif operator in ("import", "from"):
983 if not suffix:
984 reader.raise_parse_error("import missing statement")
985 block = _Statement(contents, line)
986 elif operator == "include":
987 suffix = suffix.strip('"').strip("'")
988 if not suffix:
989 reader.raise_parse_error("include missing file path")
990 block = _IncludeBlock(suffix, reader, line)
991 elif operator == "set":
992 if not suffix:
993 reader.raise_parse_error("set missing statement")
994 block = _Statement(suffix, line)
995 elif operator == "autoescape":
996 fn = suffix.strip() # type: Optional[str]
997 if fn == "None":
998 fn = None
999 template.autoescape = fn
1000 continue
1001 elif operator == "whitespace":
1002 mode = suffix.strip()
1003 # Validate the selected mode
1004 filter_whitespace(mode, "")
1005 reader.whitespace = mode
1006 continue
1007 elif operator == "raw":
1008 block = _Expression(suffix, line, raw=True)
1009 elif operator == "module":
1010 block = _Module(suffix, line)
1011 body.chunks.append(block)
1012 continue
1014 elif operator in ("apply", "block", "try", "if", "for", "while"):
1015 # parse inner body recursively
1016 if operator in ("for", "while"):
1017 block_body = _parse(reader, template, operator, operator)
1018 elif operator == "apply":
1019 # apply creates a nested function so syntactically it's not
1020 # in the loop.
1021 block_body = _parse(reader, template, operator, None)
1022 else:
1023 block_body = _parse(reader, template, operator, in_loop)
1025 if operator == "apply":
1026 if not suffix:
1027 reader.raise_parse_error("apply missing method name")
1028 block = _ApplyBlock(suffix, line, block_body)
1029 elif operator == "block":
1030 if not suffix:
1031 reader.raise_parse_error("block missing name")
1032 block = _NamedBlock(suffix, block_body, template, line)
1033 else:
1034 block = _ControlBlock(contents, line, block_body)
1035 body.chunks.append(block)
1036 continue
1038 elif operator in ("break", "continue"):
1039 if not in_loop:
1040 reader.raise_parse_error(
1041 "%s outside %s block" % (operator, set(["for", "while"]))
1042 )
1043 body.chunks.append(_Statement(contents, line))
1044 continue
1046 else:
1047 reader.raise_parse_error("unknown operator: %r" % operator)