1# mako/parsetree.py 2# Copyright 2006-2023 the Mako authors and contributors <see AUTHORS file> 3# 4# This module is part of Mako and is released under 5# the MIT License: http://www.opensource.org/licenses/mit-license.php 6 7"""defines the parse tree components for Mako templates.""" 8 9import re 10 11from mako import ast 12from mako import exceptions 13from mako import filters 14from mako import util 15 16 17class Node: 18 19 """base class for a Node in the parse tree.""" 20 21 def __init__(self, source, lineno, pos, filename): 22 self.source = source 23 self.lineno = lineno 24 self.pos = pos 25 self.filename = filename 26 27 @property 28 def exception_kwargs(self): 29 return { 30 "source": self.source, 31 "lineno": self.lineno, 32 "pos": self.pos, 33 "filename": self.filename, 34 } 35 36 def get_children(self): 37 return [] 38 39 def accept_visitor(self, visitor): 40 def traverse(node): 41 for n in node.get_children(): 42 n.accept_visitor(visitor) 43 44 method = getattr(visitor, "visit" + self.__class__.__name__, traverse) 45 method(self) 46 47 48class TemplateNode(Node): 49 50 """a 'container' node that stores the overall collection of nodes.""" 51 52 def __init__(self, filename): 53 super().__init__("", 0, 0, filename) 54 self.nodes = [] 55 self.page_attributes = {} 56 57 def get_children(self): 58 return self.nodes 59 60 def __repr__(self): 61 return "TemplateNode(%s, %r)" % ( 62 util.sorted_dict_repr(self.page_attributes), 63 self.nodes, 64 ) 65 66 67class ControlLine(Node): 68 69 """defines a control line, a line-oriented python line or end tag. 70 71 e.g.:: 72 73 % if foo: 74 (markup) 75 % endif 76 77 """ 78 79 has_loop_context = False 80 81 def __init__(self, keyword, isend, text, **kwargs): 82 super().__init__(**kwargs) 83 self.text = text 84 self.keyword = keyword 85 self.isend = isend 86 self.is_primary = keyword in ["for", "if", "while", "try", "with"] 87 self.nodes = [] 88 if self.isend: 89 self._declared_identifiers = [] 90 self._undeclared_identifiers = [] 91 else: 92 code = ast.PythonFragment(text, **self.exception_kwargs) 93 self._declared_identifiers = code.declared_identifiers 94 self._undeclared_identifiers = code.undeclared_identifiers 95 96 def get_children(self): 97 return self.nodes 98 99 def declared_identifiers(self): 100 return self._declared_identifiers 101 102 def undeclared_identifiers(self): 103 return self._undeclared_identifiers 104 105 def is_ternary(self, keyword): 106 """return true if the given keyword is a ternary keyword 107 for this ControlLine""" 108 109 cases = { 110 "if": {"else", "elif"}, 111 "try": {"except", "finally"}, 112 "for": {"else"}, 113 } 114 115 return keyword in cases.get(self.keyword, set()) 116 117 def __repr__(self): 118 return "ControlLine(%r, %r, %r, %r)" % ( 119 self.keyword, 120 self.text, 121 self.isend, 122 (self.lineno, self.pos), 123 ) 124 125 126class Text(Node): 127 """defines plain text in the template.""" 128 129 def __init__(self, content, **kwargs): 130 super().__init__(**kwargs) 131 self.content = content 132 133 def __repr__(self): 134 return "Text(%r, %r)" % (self.content, (self.lineno, self.pos)) 135 136 137class Code(Node): 138 """defines a Python code block, either inline or module level. 139 140 e.g.:: 141 142 inline: 143 <% 144 x = 12 145 %> 146 147 module level: 148 <%! 149 import logger 150 %> 151 152 """ 153 154 def __init__(self, text, ismodule, **kwargs): 155 super().__init__(**kwargs) 156 self.text = text 157 self.ismodule = ismodule 158 self.code = ast.PythonCode(text, **self.exception_kwargs) 159 160 def declared_identifiers(self): 161 return self.code.declared_identifiers 162 163 def undeclared_identifiers(self): 164 return self.code.undeclared_identifiers 165 166 def __repr__(self): 167 return "Code(%r, %r, %r)" % ( 168 self.text, 169 self.ismodule, 170 (self.lineno, self.pos), 171 ) 172 173 174class Comment(Node): 175 """defines a comment line. 176 177 # this is a comment 178 179 """ 180 181 def __init__(self, text, **kwargs): 182 super().__init__(**kwargs) 183 self.text = text 184 185 def __repr__(self): 186 return "Comment(%r, %r)" % (self.text, (self.lineno, self.pos)) 187 188 189class Expression(Node): 190 """defines an inline expression. 191 192 ${x+y} 193 194 """ 195 196 def __init__(self, text, escapes, **kwargs): 197 super().__init__(**kwargs) 198 self.text = text 199 self.escapes = escapes 200 self.escapes_code = ast.ArgumentList(escapes, **self.exception_kwargs) 201 self.code = ast.PythonCode(text, **self.exception_kwargs) 202 203 def declared_identifiers(self): 204 return [] 205 206 def undeclared_identifiers(self): 207 # TODO: make the "filter" shortcut list configurable at parse/gen time 208 return self.code.undeclared_identifiers.union( 209 self.escapes_code.undeclared_identifiers.difference( 210 filters.DEFAULT_ESCAPES 211 ) 212 ).difference(self.code.declared_identifiers) 213 214 def __repr__(self): 215 return "Expression(%r, %r, %r)" % ( 216 self.text, 217 self.escapes_code.args, 218 (self.lineno, self.pos), 219 ) 220 221 222class _TagMeta(type): 223 """metaclass to allow Tag to produce a subclass according to 224 its keyword""" 225 226 _classmap = {} 227 228 def __init__(cls, clsname, bases, dict_): 229 if getattr(cls, "__keyword__", None) is not None: 230 cls._classmap[cls.__keyword__] = cls 231 super().__init__(clsname, bases, dict_) 232 233 def __call__(cls, keyword, attributes, **kwargs): 234 if ":" in keyword: 235 ns, defname = keyword.split(":") 236 return type.__call__( 237 CallNamespaceTag, ns, defname, attributes, **kwargs 238 ) 239 240 try: 241 cls = _TagMeta._classmap[keyword] 242 except KeyError: 243 raise exceptions.CompileException( 244 "No such tag: '%s'" % keyword, 245 source=kwargs["source"], 246 lineno=kwargs["lineno"], 247 pos=kwargs["pos"], 248 filename=kwargs["filename"], 249 ) 250 return type.__call__(cls, keyword, attributes, **kwargs) 251 252 253class Tag(Node, metaclass=_TagMeta): 254 """abstract base class for tags. 255 256 e.g.:: 257 258 <%sometag/> 259 260 <%someothertag> 261 stuff 262 </%someothertag> 263 264 """ 265 266 __keyword__ = None 267 268 def __init__( 269 self, 270 keyword, 271 attributes, 272 expressions, 273 nonexpressions, 274 required, 275 **kwargs, 276 ): 277 r"""construct a new Tag instance. 278 279 this constructor not called directly, and is only called 280 by subclasses. 281 282 :param keyword: the tag keyword 283 284 :param attributes: raw dictionary of attribute key/value pairs 285 286 :param expressions: a set of identifiers that are legal attributes, 287 which can also contain embedded expressions 288 289 :param nonexpressions: a set of identifiers that are legal 290 attributes, which cannot contain embedded expressions 291 292 :param \**kwargs: 293 other arguments passed to the Node superclass (lineno, pos) 294 295 """ 296 super().__init__(**kwargs) 297 self.keyword = keyword 298 self.attributes = attributes 299 self._parse_attributes(expressions, nonexpressions) 300 missing = [r for r in required if r not in self.parsed_attributes] 301 if len(missing): 302 raise exceptions.CompileException( 303 ( 304 "Missing attribute(s): %s" 305 % ",".join(repr(m) for m in missing) 306 ), 307 **self.exception_kwargs, 308 ) 309 310 self.parent = None 311 self.nodes = [] 312 313 def is_root(self): 314 return self.parent is None 315 316 def get_children(self): 317 return self.nodes 318 319 def _parse_attributes(self, expressions, nonexpressions): 320 undeclared_identifiers = set() 321 self.parsed_attributes = {} 322 for key in self.attributes: 323 if key in expressions: 324 expr = [] 325 for x in re.compile(r"(\${.+?})", re.S).split( 326 self.attributes[key] 327 ): 328 m = re.compile(r"^\${(.+?)}$", re.S).match(x) 329 if m: 330 code = ast.PythonCode( 331 m.group(1).rstrip(), **self.exception_kwargs 332 ) 333 # we aren't discarding "declared_identifiers" here, 334 # which we do so that list comprehension-declared 335 # variables aren't counted. As yet can't find a 336 # condition that requires it here. 337 undeclared_identifiers = undeclared_identifiers.union( 338 code.undeclared_identifiers 339 ) 340 expr.append("(%s)" % m.group(1)) 341 elif x: 342 expr.append(repr(x)) 343 self.parsed_attributes[key] = " + ".join(expr) or repr("") 344 elif key in nonexpressions: 345 if re.search(r"\${.+?}", self.attributes[key]): 346 raise exceptions.CompileException( 347 "Attribute '%s' in tag '%s' does not allow embedded " 348 "expressions" % (key, self.keyword), 349 **self.exception_kwargs, 350 ) 351 self.parsed_attributes[key] = repr(self.attributes[key]) 352 else: 353 raise exceptions.CompileException( 354 "Invalid attribute for tag '%s': '%s'" 355 % (self.keyword, key), 356 **self.exception_kwargs, 357 ) 358 self.expression_undeclared_identifiers = undeclared_identifiers 359 360 def declared_identifiers(self): 361 return [] 362 363 def undeclared_identifiers(self): 364 return self.expression_undeclared_identifiers 365 366 def __repr__(self): 367 return "%s(%r, %s, %r, %r)" % ( 368 self.__class__.__name__, 369 self.keyword, 370 util.sorted_dict_repr(self.attributes), 371 (self.lineno, self.pos), 372 self.nodes, 373 ) 374 375 376class IncludeTag(Tag): 377 __keyword__ = "include" 378 379 def __init__(self, keyword, attributes, **kwargs): 380 super().__init__( 381 keyword, 382 attributes, 383 ("file", "import", "args"), 384 (), 385 ("file",), 386 **kwargs, 387 ) 388 self.page_args = ast.PythonCode( 389 "__DUMMY(%s)" % attributes.get("args", ""), **self.exception_kwargs 390 ) 391 392 def declared_identifiers(self): 393 return [] 394 395 def undeclared_identifiers(self): 396 identifiers = self.page_args.undeclared_identifiers.difference( 397 {"__DUMMY"} 398 ).difference(self.page_args.declared_identifiers) 399 return identifiers.union(super().undeclared_identifiers()) 400 401 402class NamespaceTag(Tag): 403 __keyword__ = "namespace" 404 405 def __init__(self, keyword, attributes, **kwargs): 406 super().__init__( 407 keyword, 408 attributes, 409 ("file",), 410 ("name", "inheritable", "import", "module"), 411 (), 412 **kwargs, 413 ) 414 415 self.name = attributes.get("name", "__anon_%s" % hex(abs(id(self)))) 416 if "name" not in attributes and "import" not in attributes: 417 raise exceptions.CompileException( 418 "'name' and/or 'import' attributes are required " 419 "for <%namespace>", 420 **self.exception_kwargs, 421 ) 422 if "file" in attributes and "module" in attributes: 423 raise exceptions.CompileException( 424 "<%namespace> may only have one of 'file' or 'module'", 425 **self.exception_kwargs, 426 ) 427 428 def declared_identifiers(self): 429 return [] 430 431 432class TextTag(Tag): 433 __keyword__ = "text" 434 435 def __init__(self, keyword, attributes, **kwargs): 436 super().__init__(keyword, attributes, (), ("filter"), (), **kwargs) 437 self.filter_args = ast.ArgumentList( 438 attributes.get("filter", ""), **self.exception_kwargs 439 ) 440 441 def undeclared_identifiers(self): 442 return self.filter_args.undeclared_identifiers.difference( 443 filters.DEFAULT_ESCAPES.keys() 444 ).union(self.expression_undeclared_identifiers) 445 446 447class DefTag(Tag): 448 __keyword__ = "def" 449 450 def __init__(self, keyword, attributes, **kwargs): 451 expressions = ["buffered", "cached"] + [ 452 c for c in attributes if c.startswith("cache_") 453 ] 454 455 super().__init__( 456 keyword, 457 attributes, 458 expressions, 459 ("name", "filter", "decorator"), 460 ("name",), 461 **kwargs, 462 ) 463 name = attributes["name"] 464 if re.match(r"^[\w_]+$", name): 465 raise exceptions.CompileException( 466 "Missing parenthesis in %def", **self.exception_kwargs 467 ) 468 self.function_decl = ast.FunctionDecl( 469 "def " + name + ":pass", **self.exception_kwargs 470 ) 471 self.name = self.function_decl.funcname 472 self.decorator = attributes.get("decorator", "") 473 self.filter_args = ast.ArgumentList( 474 attributes.get("filter", ""), **self.exception_kwargs 475 ) 476 477 is_anonymous = False 478 is_block = False 479 480 @property 481 def funcname(self): 482 return self.function_decl.funcname 483 484 def get_argument_expressions(self, **kw): 485 return self.function_decl.get_argument_expressions(**kw) 486 487 def declared_identifiers(self): 488 return self.function_decl.allargnames 489 490 def undeclared_identifiers(self): 491 res = [] 492 for c in self.function_decl.defaults: 493 res += list( 494 ast.PythonCode( 495 c, **self.exception_kwargs 496 ).undeclared_identifiers 497 ) 498 return ( 499 set(res) 500 .union( 501 self.filter_args.undeclared_identifiers.difference( 502 filters.DEFAULT_ESCAPES.keys() 503 ) 504 ) 505 .union(self.expression_undeclared_identifiers) 506 .difference(self.function_decl.allargnames) 507 ) 508 509 510class BlockTag(Tag): 511 __keyword__ = "block" 512 513 def __init__(self, keyword, attributes, **kwargs): 514 expressions = ["buffered", "cached", "args"] + [ 515 c for c in attributes if c.startswith("cache_") 516 ] 517 518 super().__init__( 519 keyword, 520 attributes, 521 expressions, 522 ("name", "filter", "decorator"), 523 (), 524 **kwargs, 525 ) 526 name = attributes.get("name") 527 if name and not re.match(r"^[\w_]+$", name): 528 raise exceptions.CompileException( 529 "%block may not specify an argument signature", 530 **self.exception_kwargs, 531 ) 532 if not name and attributes.get("args", None): 533 raise exceptions.CompileException( 534 "Only named %blocks may specify args", **self.exception_kwargs 535 ) 536 self.body_decl = ast.FunctionArgs( 537 attributes.get("args", ""), **self.exception_kwargs 538 ) 539 540 self.name = name 541 self.decorator = attributes.get("decorator", "") 542 self.filter_args = ast.ArgumentList( 543 attributes.get("filter", ""), **self.exception_kwargs 544 ) 545 546 is_block = True 547 548 @property 549 def is_anonymous(self): 550 return self.name is None 551 552 @property 553 def funcname(self): 554 return self.name or "__M_anon_%d" % (self.lineno,) 555 556 def get_argument_expressions(self, **kw): 557 return self.body_decl.get_argument_expressions(**kw) 558 559 def declared_identifiers(self): 560 return self.body_decl.allargnames 561 562 def undeclared_identifiers(self): 563 return ( 564 self.filter_args.undeclared_identifiers.difference( 565 filters.DEFAULT_ESCAPES.keys() 566 ) 567 ).union(self.expression_undeclared_identifiers) 568 569 570class CallTag(Tag): 571 __keyword__ = "call" 572 573 def __init__(self, keyword, attributes, **kwargs): 574 super().__init__( 575 keyword, attributes, ("args"), ("expr",), ("expr",), **kwargs 576 ) 577 self.expression = attributes["expr"] 578 self.code = ast.PythonCode(self.expression, **self.exception_kwargs) 579 self.body_decl = ast.FunctionArgs( 580 attributes.get("args", ""), **self.exception_kwargs 581 ) 582 583 def declared_identifiers(self): 584 return self.code.declared_identifiers.union(self.body_decl.allargnames) 585 586 def undeclared_identifiers(self): 587 return self.code.undeclared_identifiers.difference( 588 self.code.declared_identifiers 589 ) 590 591 592class CallNamespaceTag(Tag): 593 def __init__(self, namespace, defname, attributes, **kwargs): 594 super().__init__( 595 namespace + ":" + defname, 596 attributes, 597 tuple(attributes.keys()) + ("args",), 598 (), 599 (), 600 **kwargs, 601 ) 602 603 self.expression = "%s.%s(%s)" % ( 604 namespace, 605 defname, 606 ",".join( 607 "%s=%s" % (k, v) 608 for k, v in self.parsed_attributes.items() 609 if k != "args" 610 ), 611 ) 612 613 self.code = ast.PythonCode(self.expression, **self.exception_kwargs) 614 self.body_decl = ast.FunctionArgs( 615 attributes.get("args", ""), **self.exception_kwargs 616 ) 617 618 def declared_identifiers(self): 619 return self.code.declared_identifiers.union(self.body_decl.allargnames) 620 621 def undeclared_identifiers(self): 622 return self.code.undeclared_identifiers.difference( 623 self.code.declared_identifiers 624 ) 625 626 627class InheritTag(Tag): 628 __keyword__ = "inherit" 629 630 def __init__(self, keyword, attributes, **kwargs): 631 super().__init__( 632 keyword, attributes, ("file",), (), ("file",), **kwargs 633 ) 634 635 636class PageTag(Tag): 637 __keyword__ = "page" 638 639 def __init__(self, keyword, attributes, **kwargs): 640 expressions = [ 641 "cached", 642 "args", 643 "expression_filter", 644 "enable_loop", 645 ] + [c for c in attributes if c.startswith("cache_")] 646 647 super().__init__(keyword, attributes, expressions, (), (), **kwargs) 648 self.body_decl = ast.FunctionArgs( 649 attributes.get("args", ""), **self.exception_kwargs 650 ) 651 self.filter_args = ast.ArgumentList( 652 attributes.get("expression_filter", ""), **self.exception_kwargs 653 ) 654 655 def declared_identifiers(self): 656 return self.body_decl.allargnames 657