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

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. 

15 

16"""A simple template system that compiles templates to Python code. 

17 

18Basic usage looks like:: 

19 

20 t = template.Template("<html>{{ myvalue }}</html>") 

21 print(t.generate(myvalue="XXX")) 

22 

23`Loader` is a class that loads templates from a root directory and caches 

24the compiled templates:: 

25 

26 loader = template.Loader("/home/btaylor") 

27 print(loader.load("test.html").generate(myvalue="XXX")) 

28 

29We compile all templates to raw Python. Error-reporting is currently... uh, 

30interesting. Syntax for the templates:: 

31 

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> 

47 

48 ### bold.html 

49 {% extends "base.html" %} 

50 

51 {% block title %}A bolder title{% end %} 

52 

53 {% block student %} 

54 <li><span style="bold">{{ escape(student.name) }}</span></li> 

55 {% end %} 

56 

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:: 

60 

61 {% for student in [p for p in people if p.student and p.age > 23] %} 

62 <li>{{ escape(student.name) }}</li> 

63 {% end %} 

64 

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`):: 

69 

70 ### Python code 

71 def add(x, y): 

72 return x + y 

73 template.execute(add=add) 

74 

75 ### The template 

76 {{ add(1, 2) }} 

77 

78We provide the functions `escape() <.xhtml_escape>`, `.url_escape()`, 

79`.json_encode()`, and `.squeeze()` to all templates by default. 

80 

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. 

86 

87Variable names beginning with ``_tt_`` are reserved by the template 

88system and should not be used by application code. 

89 

90Syntax Reference 

91---------------- 

92 

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 ``{% %}``. 

97 

98To comment out a section so that it is omitted from the output, surround it 

99with ``{# ... #}``. 

100 

101 

102To include a literal ``{{``, ``{%``, or ``{#`` in the output, escape them as 

103``{{!``, ``{%!``, and ``{#!``, respectively. 

104 

105 

106``{% apply *function* %}...{% end %}`` 

107 Applies a function to the output of all template code between ``apply`` 

108 and ``end``:: 

109 

110 {% apply linkify %}{{name}} said: {{message}}{% end %} 

111 

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. 

116 

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`.:: 

122 

123 {% autoescape xhtml_escape %} 

124 {% autoescape None %} 

125 

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.:: 

130 

131 <!-- base.html --> 

132 <title>{% block title %}Default title{% end %}</title> 

133 

134 <!-- mypage.html --> 

135 {% extends "base.html" %} 

136 {% block title %}My page title{% end %} 

137 

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. 

142 

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. 

148 

149``{% for *var* in *expr* %}...{% end %}`` 

150 Same as the python ``for`` statement. ``{% break %}`` and 

151 ``{% continue %}`` may be used inside the loop. 

152 

153``{% from *x* import *y* %}`` 

154 Same as the python ``import`` statement. 

155 

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) 

159 

160``{% import *module* %}`` 

161 Same as the python ``import`` statement. 

162 

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. 

169 

170``{% module *expr* %}`` 

171 Renders a `~tornado.web.UIModule`. The output of the ``UIModule`` is 

172 not escaped:: 

173 

174 {% module Template("foo.html", arg=42) %} 

175 

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. 

179 

180``{% raw *expr* %}`` 

181 Outputs the result of the given expression without autoescaping. 

182 

183``{% set *x* = *y* %}`` 

184 Sets a local variable. 

185 

186``{% try %}...{% except %}...{% else %}...{% finally %}...{% end %}`` 

187 Same as the python ``try`` statement. 

188 

189``{% while *condition* %}... {% end %}`` 

190 Same as the python ``while`` statement. ``{% break %}`` and 

191 ``{% continue %}`` may be used inside the loop. 

192 

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""" 

198 

199import datetime 

200from io import StringIO 

201import linecache 

202import os.path 

203import posixpath 

204import re 

205import threading 

206 

207from tornado import escape 

208from tornado.log import app_log 

209from tornado.util import ObjectDict, exec_in, unicode_type 

210 

211from typing import Any, Union, Callable, List, Dict, Iterable, Optional, TextIO 

212import typing 

213 

214if typing.TYPE_CHECKING: 

215 from typing import Tuple, ContextManager # noqa: F401 

216 

217_DEFAULT_AUTOESCAPE = "xhtml_escape" 

218 

219 

220class _UnsetMarker: 

221 pass 

222 

223 

224_UNSET = _UnsetMarker() 

225 

226 

227def filter_whitespace(mode: str, text: str) -> str: 

228 """Transform whitespace in ``text`` according to ``mode``. 

229 

230 Available modes are: 

231 

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. 

237 

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) 

250 

251 

252class Template(object): 

253 """A compiled template. 

254 

255 We compile into Python from the given template_string. You can generate 

256 the template from variables with generate(). 

257 """ 

258 

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. 

272 

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. 

285 

286 .. versionchanged:: 4.3 

287 Added ``whitespace`` parameter; deprecated ``compress_whitespace``. 

288 """ 

289 self.name = escape.native_str(name) 

290 

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, "") 

308 

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 

315 

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 

336 

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() 

363 

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() 

378 

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 

390 

391 

392class BaseLoader(object): 

393 """Base class for template loaders. 

394 

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 """ 

399 

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. 

407 

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. 

417 

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() 

431 

432 def reset(self) -> None: 

433 """Resets the cache of compiled templates.""" 

434 with self.lock: 

435 self.templates = {} 

436 

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() 

440 

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] 

448 

449 def _create_template(self, name: str) -> Template: 

450 raise NotImplementedError() 

451 

452 

453class Loader(BaseLoader): 

454 """A template loader that loads from a single root directory.""" 

455 

456 def __init__(self, root_directory: str, **kwargs: Any) -> None: 

457 super().__init__(**kwargs) 

458 self.root = os.path.abspath(root_directory) 

459 

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 

473 

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 

479 

480 

481class DictLoader(BaseLoader): 

482 """A template loader that loads from a dictionary.""" 

483 

484 def __init__(self, dict: Dict[str, str], **kwargs: Any) -> None: 

485 super().__init__(**kwargs) 

486 self.dict = dict 

487 

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 

498 

499 def _create_template(self, name: str) -> Template: 

500 return Template(self.dict[name], name=name, loader=self) 

501 

502 

503class _Node(object): 

504 def each_child(self) -> Iterable["_Node"]: 

505 return () 

506 

507 def generate(self, writer: "_CodeWriter") -> None: 

508 raise NotImplementedError() 

509 

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) 

515 

516 

517class _File(_Node): 

518 def __init__(self, template: Template, body: "_ChunkList") -> None: 

519 self.template = template 

520 self.body = body 

521 self.line = 0 

522 

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) 

530 

531 def each_child(self) -> Iterable["_Node"]: 

532 return (self.body,) 

533 

534 

535class _ChunkList(_Node): 

536 def __init__(self, chunks: List[_Node]) -> None: 

537 self.chunks = chunks 

538 

539 def generate(self, writer: "_CodeWriter") -> None: 

540 for chunk in self.chunks: 

541 chunk.generate(writer) 

542 

543 def each_child(self) -> Iterable["_Node"]: 

544 return self.chunks 

545 

546 

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 

553 

554 def each_child(self) -> Iterable["_Node"]: 

555 return (self.body,) 

556 

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) 

561 

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) 

567 

568 

569class _ExtendsBlock(_Node): 

570 def __init__(self, name: str) -> None: 

571 self.name = name 

572 

573 

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 

579 

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) 

586 

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) 

592 

593 

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 

599 

600 def each_child(self) -> Iterable["_Node"]: 

601 return (self.body,) 

602 

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 ) 

615 

616 

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 

622 

623 def each_child(self) -> Iterable[_Node]: 

624 return (self.body,) 

625 

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) 

632 

633 

634class _IntermediateControlBlock(_Node): 

635 def __init__(self, statement: str, line: int) -> None: 

636 self.statement = statement 

637 self.line = line 

638 

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) 

643 

644 

645class _Statement(_Node): 

646 def __init__(self, statement: str, line: int) -> None: 

647 self.statement = statement 

648 self.line = line 

649 

650 def generate(self, writer: "_CodeWriter") -> None: 

651 writer.write_line(self.statement, self.line) 

652 

653 

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 

659 

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) 

675 

676 

677class _Module(_Expression): 

678 def __init__(self, expression: str, line: int) -> None: 

679 super().__init__("_tt_modules." + expression, line, raw=True) 

680 

681 

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 

687 

688 def generate(self, writer: "_CodeWriter") -> None: 

689 value = self.value 

690 

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) 

695 

696 if value: 

697 writer.write_line("_tt_append(%r)" % escape.utf8(value), self.line) 

698 

699 

700class ParseError(Exception): 

701 """Raised for template syntax errors. 

702 

703 ``ParseError`` instances have ``filename`` and ``lineno`` attributes 

704 indicating the position of the error. 

705 

706 .. versionchanged:: 4.3 

707 Added ``filename`` and ``lineno`` attributes. 

708 """ 

709 

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 

718 

719 def __str__(self) -> str: 

720 return "%s at %s:%d" % (self.message, self.filename, self.lineno) 

721 

722 

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 

738 

739 def indent_size(self) -> int: 

740 return self._indent 

741 

742 def indent(self) -> "ContextManager": 

743 class Indenter(object): 

744 def __enter__(_) -> "_CodeWriter": 

745 self._indent += 1 

746 return self 

747 

748 def __exit__(_, *args: Any) -> None: 

749 assert self._indent > 0 

750 self._indent -= 1 

751 

752 return Indenter() 

753 

754 def include(self, template: Template, line: int) -> "ContextManager": 

755 self.include_stack.append((self.current_template, line)) 

756 self.current_template = template 

757 

758 class IncludeTemplate(object): 

759 def __enter__(_) -> "_CodeWriter": 

760 return self 

761 

762 def __exit__(_, *args: Any) -> None: 

763 self.current_template = self.include_stack.pop()[0] 

764 

765 return IncludeTemplate() 

766 

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) 

779 

780 

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 

788 

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 

802 

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 

811 

812 def remaining(self) -> int: 

813 return len(self.text) - self.pos 

814 

815 def __len__(self) -> int: 

816 return self.remaining() 

817 

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] 

833 

834 def __str__(self) -> str: 

835 return self.text[self.pos :] 

836 

837 def raise_parse_error(self, msg: str) -> None: 

838 raise ParseError(msg, self.name, self.line) 

839 

840 

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)]) 

845 

846 

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 

885 

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)) 

890 

891 start_brace = reader.consume(2) 

892 line = reader.line 

893 

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 

902 

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 

911 

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 

923 

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 ({% %})") 

933 

934 operator, space, suffix = contents.partition(" ") 

935 suffix = suffix.strip() 

936 

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 

956 

957 # End tag 

958 elif operator == "end": 

959 if not in_block: 

960 reader.raise_parse_error("Extra {% end %} block") 

961 return body 

962 

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 

1013 

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) 

1024 

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 

1037 

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 

1045 

1046 else: 

1047 reader.raise_parse_error("unknown operator: %r" % operator)