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

491 statements  

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 

198Security Considerations 

199----------------------- 

200 

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. 

204 

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

211 

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 

222 

223from tornado import escape 

224from tornado.log import app_log 

225from tornado.util import ObjectDict, exec_in, unicode_type 

226 

227_DEFAULT_AUTOESCAPE = "xhtml_escape" 

228 

229 

230class _UnsetMarker: 

231 pass 

232 

233 

234_UNSET = _UnsetMarker() 

235 

236 

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

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

239 

240 Available modes are: 

241 

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. 

247 

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) 

260 

261 

262class Template: 

263 """A compiled template. 

264 

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

266 the template from variables with generate(). 

267 """ 

268 

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. 

282 

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. 

295 

296 .. versionchanged:: 4.3 

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

298 """ 

299 self.name = escape.native_str(name) 

300 

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

318 

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 

325 

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 

346 

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

373 

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

388 

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 

400 

401 

402class BaseLoader: 

403 """Base class for template loaders. 

404 

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

409 

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. 

417 

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. 

427 

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

441 

442 def reset(self) -> None: 

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

444 with self.lock: 

445 self.templates = {} 

446 

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

450 

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] 

458 

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

460 raise NotImplementedError() 

461 

462 

463class Loader(BaseLoader): 

464 """A template loader that loads from the filesystem. 

465 

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

471 

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

473 super().__init__(**kwargs) 

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

475 

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 

489 

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 

495 

496 

497class DictLoader(BaseLoader): 

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

499 

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

501 super().__init__(**kwargs) 

502 self.dict = dict 

503 

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 

514 

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

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

517 

518 

519class _Node: 

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

521 return () 

522 

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

524 raise NotImplementedError() 

525 

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) 

531 

532 

533class _File(_Node): 

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

535 self.template = template 

536 self.body = body 

537 self.line = 0 

538 

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) 

546 

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

548 return (self.body,) 

549 

550 

551class _ChunkList(_Node): 

552 def __init__(self, chunks: list[_Node]) -> None: 

553 self.chunks = chunks 

554 

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

556 for chunk in self.chunks: 

557 chunk.generate(writer) 

558 

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

560 return self.chunks 

561 

562 

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 

569 

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

571 return (self.body,) 

572 

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) 

577 

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) 

583 

584 

585class _ExtendsBlock(_Node): 

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

587 self.name = name 

588 

589 

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 

595 

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) 

602 

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) 

608 

609 

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 

615 

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

617 return (self.body,) 

618 

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 ) 

631 

632 

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 

638 

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

640 return (self.body,) 

641 

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) 

648 

649 

650class _IntermediateControlBlock(_Node): 

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

652 self.statement = statement 

653 self.line = line 

654 

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) 

659 

660 

661class _Statement(_Node): 

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

663 self.statement = statement 

664 self.line = line 

665 

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

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

668 

669 

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 

675 

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) 

691 

692 

693class _Module(_Expression): 

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

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

696 

697 

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 

703 

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

705 value = self.value 

706 

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) 

711 

712 if value: 

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

714 

715 

716class ParseError(Exception): 

717 """Raised for template syntax errors. 

718 

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

720 indicating the position of the error. 

721 

722 .. versionchanged:: 4.3 

723 Added ``filename`` and ``lineno`` attributes. 

724 """ 

725 

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 

734 

735 def __str__(self) -> str: 

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

737 

738 

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 

754 

755 def indent_size(self) -> int: 

756 return self._indent 

757 

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

759 class Indenter: 

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

761 self._indent += 1 

762 return self 

763 

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

765 assert self._indent > 0 

766 self._indent -= 1 

767 

768 return Indenter() 

769 

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

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

772 self.current_template = template 

773 

774 class IncludeTemplate: 

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

776 return self 

777 

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

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

780 

781 return IncludeTemplate() 

782 

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) 

795 

796 

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 

804 

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 

818 

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 

827 

828 def remaining(self) -> int: 

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

830 

831 def __len__(self) -> int: 

832 return self.remaining() 

833 

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] 

849 

850 def __str__(self) -> str: 

851 return self.text[self.pos :] 

852 

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

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

855 

856 

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

861 

862 

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 

901 

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

906 

907 start_brace = reader.consume(2) 

908 line = reader.line 

909 

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 

918 

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 

927 

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 

939 

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

949 

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

951 suffix = suffix.strip() 

952 

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 

970 

971 # End tag 

972 elif operator == "end": 

973 if not in_block: 

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

975 return body 

976 

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 

1027 

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) 

1038 

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 

1051 

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 

1059 

1060 else: 

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