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