| # mako/parsetree.py |
| # Copyright 2006-2019 the Mako authors and contributors <see AUTHORS file> |
| # |
| # This module is part of Mako and is released under |
| # the MIT License: http://www.opensource.org/licenses/mit-license.php |
| |
| """defines the parse tree components for Mako templates.""" |
| |
| import re |
| |
| from mako import ast |
| from mako import compat |
| from mako import exceptions |
| from mako import filters |
| from mako import util |
| |
| |
| class Node(object): |
| |
| """base class for a Node in the parse tree.""" |
| |
| def __init__(self, source, lineno, pos, filename): |
| self.source = source |
| self.lineno = lineno |
| self.pos = pos |
| self.filename = filename |
| |
| @property |
| def exception_kwargs(self): |
| return { |
| "source": self.source, |
| "lineno": self.lineno, |
| "pos": self.pos, |
| "filename": self.filename, |
| } |
| |
| def get_children(self): |
| return [] |
| |
| def accept_visitor(self, visitor): |
| def traverse(node): |
| for n in node.get_children(): |
| n.accept_visitor(visitor) |
| |
| method = getattr(visitor, "visit" + self.__class__.__name__, traverse) |
| method(self) |
| |
| |
| class TemplateNode(Node): |
| |
| """a 'container' node that stores the overall collection of nodes.""" |
| |
| def __init__(self, filename): |
| super(TemplateNode, self).__init__("", 0, 0, filename) |
| self.nodes = [] |
| self.page_attributes = {} |
| |
| def get_children(self): |
| return self.nodes |
| |
| def __repr__(self): |
| return "TemplateNode(%s, %r)" % ( |
| util.sorted_dict_repr(self.page_attributes), |
| self.nodes, |
| ) |
| |
| |
| class ControlLine(Node): |
| |
| """defines a control line, a line-oriented python line or end tag. |
| |
| e.g.:: |
| |
| % if foo: |
| (markup) |
| % endif |
| |
| """ |
| |
| has_loop_context = False |
| |
| def __init__(self, keyword, isend, text, **kwargs): |
| super(ControlLine, self).__init__(**kwargs) |
| self.text = text |
| self.keyword = keyword |
| self.isend = isend |
| self.is_primary = keyword in ["for", "if", "while", "try", "with"] |
| self.nodes = [] |
| if self.isend: |
| self._declared_identifiers = [] |
| self._undeclared_identifiers = [] |
| else: |
| code = ast.PythonFragment(text, **self.exception_kwargs) |
| self._declared_identifiers = code.declared_identifiers |
| self._undeclared_identifiers = code.undeclared_identifiers |
| |
| def get_children(self): |
| return self.nodes |
| |
| def declared_identifiers(self): |
| return self._declared_identifiers |
| |
| def undeclared_identifiers(self): |
| return self._undeclared_identifiers |
| |
| def is_ternary(self, keyword): |
| """return true if the given keyword is a ternary keyword |
| for this ControlLine""" |
| |
| return keyword in { |
| "if": set(["else", "elif"]), |
| "try": set(["except", "finally"]), |
| "for": set(["else"]), |
| }.get(self.keyword, []) |
| |
| def __repr__(self): |
| return "ControlLine(%r, %r, %r, %r)" % ( |
| self.keyword, |
| self.text, |
| self.isend, |
| (self.lineno, self.pos), |
| ) |
| |
| |
| class Text(Node): |
| |
| """defines plain text in the template.""" |
| |
| def __init__(self, content, **kwargs): |
| super(Text, self).__init__(**kwargs) |
| self.content = content |
| |
| def __repr__(self): |
| return "Text(%r, %r)" % (self.content, (self.lineno, self.pos)) |
| |
| |
| class Code(Node): |
| |
| """defines a Python code block, either inline or module level. |
| |
| e.g.:: |
| |
| inline: |
| <% |
| x = 12 |
| %> |
| |
| module level: |
| <%! |
| import logger |
| %> |
| |
| """ |
| |
| def __init__(self, text, ismodule, **kwargs): |
| super(Code, self).__init__(**kwargs) |
| self.text = text |
| self.ismodule = ismodule |
| self.code = ast.PythonCode(text, **self.exception_kwargs) |
| |
| def declared_identifiers(self): |
| return self.code.declared_identifiers |
| |
| def undeclared_identifiers(self): |
| return self.code.undeclared_identifiers |
| |
| def __repr__(self): |
| return "Code(%r, %r, %r)" % ( |
| self.text, |
| self.ismodule, |
| (self.lineno, self.pos), |
| ) |
| |
| |
| class Comment(Node): |
| |
| """defines a comment line. |
| |
| # this is a comment |
| |
| """ |
| |
| def __init__(self, text, **kwargs): |
| super(Comment, self).__init__(**kwargs) |
| self.text = text |
| |
| def __repr__(self): |
| return "Comment(%r, %r)" % (self.text, (self.lineno, self.pos)) |
| |
| |
| class Expression(Node): |
| |
| """defines an inline expression. |
| |
| ${x+y} |
| |
| """ |
| |
| def __init__(self, text, escapes, **kwargs): |
| super(Expression, self).__init__(**kwargs) |
| self.text = text |
| self.escapes = escapes |
| self.escapes_code = ast.ArgumentList(escapes, **self.exception_kwargs) |
| self.code = ast.PythonCode(text, **self.exception_kwargs) |
| |
| def declared_identifiers(self): |
| return [] |
| |
| def undeclared_identifiers(self): |
| # TODO: make the "filter" shortcut list configurable at parse/gen time |
| return self.code.undeclared_identifiers.union( |
| self.escapes_code.undeclared_identifiers.difference( |
| set(filters.DEFAULT_ESCAPES.keys()) |
| ) |
| ).difference(self.code.declared_identifiers) |
| |
| def __repr__(self): |
| return "Expression(%r, %r, %r)" % ( |
| self.text, |
| self.escapes_code.args, |
| (self.lineno, self.pos), |
| ) |
| |
| |
| class _TagMeta(type): |
| |
| """metaclass to allow Tag to produce a subclass according to |
| its keyword""" |
| |
| _classmap = {} |
| |
| def __init__(cls, clsname, bases, dict_): |
| if getattr(cls, "__keyword__", None) is not None: |
| cls._classmap[cls.__keyword__] = cls |
| super(_TagMeta, cls).__init__(clsname, bases, dict_) |
| |
| def __call__(cls, keyword, attributes, **kwargs): |
| if ":" in keyword: |
| ns, defname = keyword.split(":") |
| return type.__call__( |
| CallNamespaceTag, ns, defname, attributes, **kwargs |
| ) |
| |
| try: |
| cls = _TagMeta._classmap[keyword] |
| except KeyError: |
| raise exceptions.CompileException( |
| "No such tag: '%s'" % keyword, |
| source=kwargs["source"], |
| lineno=kwargs["lineno"], |
| pos=kwargs["pos"], |
| filename=kwargs["filename"], |
| ) |
| return type.__call__(cls, keyword, attributes, **kwargs) |
| |
| |
| class Tag(compat.with_metaclass(_TagMeta, Node)): |
| """abstract base class for tags. |
| |
| e.g.:: |
| |
| <%sometag/> |
| |
| <%someothertag> |
| stuff |
| </%someothertag> |
| |
| """ |
| |
| __keyword__ = None |
| |
| def __init__( |
| self, |
| keyword, |
| attributes, |
| expressions, |
| nonexpressions, |
| required, |
| **kwargs |
| ): |
| r"""construct a new Tag instance. |
| |
| this constructor not called directly, and is only called |
| by subclasses. |
| |
| :param keyword: the tag keyword |
| |
| :param attributes: raw dictionary of attribute key/value pairs |
| |
| :param expressions: a set of identifiers that are legal attributes, |
| which can also contain embedded expressions |
| |
| :param nonexpressions: a set of identifiers that are legal |
| attributes, which cannot contain embedded expressions |
| |
| :param \**kwargs: |
| other arguments passed to the Node superclass (lineno, pos) |
| |
| """ |
| super(Tag, self).__init__(**kwargs) |
| self.keyword = keyword |
| self.attributes = attributes |
| self._parse_attributes(expressions, nonexpressions) |
| missing = [r for r in required if r not in self.parsed_attributes] |
| if len(missing): |
| raise exceptions.CompileException( |
| "Missing attribute(s): %s" |
| % ",".join([repr(m) for m in missing]), |
| **self.exception_kwargs |
| ) |
| self.parent = None |
| self.nodes = [] |
| |
| def is_root(self): |
| return self.parent is None |
| |
| def get_children(self): |
| return self.nodes |
| |
| def _parse_attributes(self, expressions, nonexpressions): |
| undeclared_identifiers = set() |
| self.parsed_attributes = {} |
| for key in self.attributes: |
| if key in expressions: |
| expr = [] |
| for x in re.compile(r"(\${.+?})", re.S).split( |
| self.attributes[key] |
| ): |
| m = re.compile(r"^\${(.+?)}$", re.S).match(x) |
| if m: |
| code = ast.PythonCode( |
| m.group(1).rstrip(), **self.exception_kwargs |
| ) |
| # we aren't discarding "declared_identifiers" here, |
| # which we do so that list comprehension-declared |
| # variables aren't counted. As yet can't find a |
| # condition that requires it here. |
| undeclared_identifiers = undeclared_identifiers.union( |
| code.undeclared_identifiers |
| ) |
| expr.append("(%s)" % m.group(1)) |
| else: |
| if x: |
| expr.append(repr(x)) |
| self.parsed_attributes[key] = " + ".join(expr) or repr("") |
| elif key in nonexpressions: |
| if re.search(r"\${.+?}", self.attributes[key]): |
| raise exceptions.CompileException( |
| "Attibute '%s' in tag '%s' does not allow embedded " |
| "expressions" % (key, self.keyword), |
| **self.exception_kwargs |
| ) |
| self.parsed_attributes[key] = repr(self.attributes[key]) |
| else: |
| raise exceptions.CompileException( |
| "Invalid attribute for tag '%s': '%s'" |
| % (self.keyword, key), |
| **self.exception_kwargs |
| ) |
| self.expression_undeclared_identifiers = undeclared_identifiers |
| |
| def declared_identifiers(self): |
| return [] |
| |
| def undeclared_identifiers(self): |
| return self.expression_undeclared_identifiers |
| |
| def __repr__(self): |
| return "%s(%r, %s, %r, %r)" % ( |
| self.__class__.__name__, |
| self.keyword, |
| util.sorted_dict_repr(self.attributes), |
| (self.lineno, self.pos), |
| self.nodes, |
| ) |
| |
| |
| class IncludeTag(Tag): |
| __keyword__ = "include" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| super(IncludeTag, self).__init__( |
| keyword, |
| attributes, |
| ("file", "import", "args"), |
| (), |
| ("file",), |
| **kwargs |
| ) |
| self.page_args = ast.PythonCode( |
| "__DUMMY(%s)" % attributes.get("args", ""), **self.exception_kwargs |
| ) |
| |
| def declared_identifiers(self): |
| return [] |
| |
| def undeclared_identifiers(self): |
| identifiers = self.page_args.undeclared_identifiers.difference( |
| set(["__DUMMY"]) |
| ).difference(self.page_args.declared_identifiers) |
| return identifiers.union( |
| super(IncludeTag, self).undeclared_identifiers() |
| ) |
| |
| |
| class NamespaceTag(Tag): |
| __keyword__ = "namespace" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| super(NamespaceTag, self).__init__( |
| keyword, |
| attributes, |
| ("file",), |
| ("name", "inheritable", "import", "module"), |
| (), |
| **kwargs |
| ) |
| |
| self.name = attributes.get("name", "__anon_%s" % hex(abs(id(self)))) |
| if "name" not in attributes and "import" not in attributes: |
| raise exceptions.CompileException( |
| "'name' and/or 'import' attributes are required " |
| "for <%namespace>", |
| **self.exception_kwargs |
| ) |
| if "file" in attributes and "module" in attributes: |
| raise exceptions.CompileException( |
| "<%namespace> may only have one of 'file' or 'module'", |
| **self.exception_kwargs |
| ) |
| |
| def declared_identifiers(self): |
| return [] |
| |
| |
| class TextTag(Tag): |
| __keyword__ = "text" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| super(TextTag, self).__init__( |
| keyword, attributes, (), ("filter"), (), **kwargs |
| ) |
| self.filter_args = ast.ArgumentList( |
| attributes.get("filter", ""), **self.exception_kwargs |
| ) |
| |
| def undeclared_identifiers(self): |
| return self.filter_args.undeclared_identifiers.difference( |
| filters.DEFAULT_ESCAPES.keys() |
| ).union(self.expression_undeclared_identifiers) |
| |
| |
| class DefTag(Tag): |
| __keyword__ = "def" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| expressions = ["buffered", "cached"] + [ |
| c for c in attributes if c.startswith("cache_") |
| ] |
| |
| super(DefTag, self).__init__( |
| keyword, |
| attributes, |
| expressions, |
| ("name", "filter", "decorator"), |
| ("name",), |
| **kwargs |
| ) |
| name = attributes["name"] |
| if re.match(r"^[\w_]+$", name): |
| raise exceptions.CompileException( |
| "Missing parenthesis in %def", **self.exception_kwargs |
| ) |
| self.function_decl = ast.FunctionDecl( |
| "def " + name + ":pass", **self.exception_kwargs |
| ) |
| self.name = self.function_decl.funcname |
| self.decorator = attributes.get("decorator", "") |
| self.filter_args = ast.ArgumentList( |
| attributes.get("filter", ""), **self.exception_kwargs |
| ) |
| |
| is_anonymous = False |
| is_block = False |
| |
| @property |
| def funcname(self): |
| return self.function_decl.funcname |
| |
| def get_argument_expressions(self, **kw): |
| return self.function_decl.get_argument_expressions(**kw) |
| |
| def declared_identifiers(self): |
| return self.function_decl.allargnames |
| |
| def undeclared_identifiers(self): |
| res = [] |
| for c in self.function_decl.defaults: |
| res += list( |
| ast.PythonCode( |
| c, **self.exception_kwargs |
| ).undeclared_identifiers |
| ) |
| return ( |
| set(res) |
| .union( |
| self.filter_args.undeclared_identifiers.difference( |
| filters.DEFAULT_ESCAPES.keys() |
| ) |
| ) |
| .union(self.expression_undeclared_identifiers) |
| .difference(self.function_decl.allargnames) |
| ) |
| |
| |
| class BlockTag(Tag): |
| __keyword__ = "block" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| expressions = ["buffered", "cached", "args"] + [ |
| c for c in attributes if c.startswith("cache_") |
| ] |
| |
| super(BlockTag, self).__init__( |
| keyword, |
| attributes, |
| expressions, |
| ("name", "filter", "decorator"), |
| (), |
| **kwargs |
| ) |
| name = attributes.get("name") |
| if name and not re.match(r"^[\w_]+$", name): |
| raise exceptions.CompileException( |
| "%block may not specify an argument signature", |
| **self.exception_kwargs |
| ) |
| if not name and attributes.get("args", None): |
| raise exceptions.CompileException( |
| "Only named %blocks may specify args", **self.exception_kwargs |
| ) |
| self.body_decl = ast.FunctionArgs( |
| attributes.get("args", ""), **self.exception_kwargs |
| ) |
| |
| self.name = name |
| self.decorator = attributes.get("decorator", "") |
| self.filter_args = ast.ArgumentList( |
| attributes.get("filter", ""), **self.exception_kwargs |
| ) |
| |
| is_block = True |
| |
| @property |
| def is_anonymous(self): |
| return self.name is None |
| |
| @property |
| def funcname(self): |
| return self.name or "__M_anon_%d" % (self.lineno,) |
| |
| def get_argument_expressions(self, **kw): |
| return self.body_decl.get_argument_expressions(**kw) |
| |
| def declared_identifiers(self): |
| return self.body_decl.allargnames |
| |
| def undeclared_identifiers(self): |
| return ( |
| self.filter_args.undeclared_identifiers.difference( |
| filters.DEFAULT_ESCAPES.keys() |
| ) |
| ).union(self.expression_undeclared_identifiers) |
| |
| |
| class CallTag(Tag): |
| __keyword__ = "call" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| super(CallTag, self).__init__( |
| keyword, attributes, ("args"), ("expr",), ("expr",), **kwargs |
| ) |
| self.expression = attributes["expr"] |
| self.code = ast.PythonCode(self.expression, **self.exception_kwargs) |
| self.body_decl = ast.FunctionArgs( |
| attributes.get("args", ""), **self.exception_kwargs |
| ) |
| |
| def declared_identifiers(self): |
| return self.code.declared_identifiers.union(self.body_decl.allargnames) |
| |
| def undeclared_identifiers(self): |
| return self.code.undeclared_identifiers.difference( |
| self.code.declared_identifiers |
| ) |
| |
| |
| class CallNamespaceTag(Tag): |
| def __init__(self, namespace, defname, attributes, **kwargs): |
| super(CallNamespaceTag, self).__init__( |
| namespace + ":" + defname, |
| attributes, |
| tuple(attributes.keys()) + ("args",), |
| (), |
| (), |
| **kwargs |
| ) |
| |
| self.expression = "%s.%s(%s)" % ( |
| namespace, |
| defname, |
| ",".join( |
| [ |
| "%s=%s" % (k, v) |
| for k, v in self.parsed_attributes.items() |
| if k != "args" |
| ] |
| ), |
| ) |
| self.code = ast.PythonCode(self.expression, **self.exception_kwargs) |
| self.body_decl = ast.FunctionArgs( |
| attributes.get("args", ""), **self.exception_kwargs |
| ) |
| |
| def declared_identifiers(self): |
| return self.code.declared_identifiers.union(self.body_decl.allargnames) |
| |
| def undeclared_identifiers(self): |
| return self.code.undeclared_identifiers.difference( |
| self.code.declared_identifiers |
| ) |
| |
| |
| class InheritTag(Tag): |
| __keyword__ = "inherit" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| super(InheritTag, self).__init__( |
| keyword, attributes, ("file",), (), ("file",), **kwargs |
| ) |
| |
| |
| class PageTag(Tag): |
| __keyword__ = "page" |
| |
| def __init__(self, keyword, attributes, **kwargs): |
| expressions = [ |
| "cached", |
| "args", |
| "expression_filter", |
| "enable_loop", |
| ] + [c for c in attributes if c.startswith("cache_")] |
| |
| super(PageTag, self).__init__( |
| keyword, attributes, expressions, (), (), **kwargs |
| ) |
| self.body_decl = ast.FunctionArgs( |
| attributes.get("args", ""), **self.exception_kwargs |
| ) |
| self.filter_args = ast.ArgumentList( |
| attributes.get("expression_filter", ""), **self.exception_kwargs |
| ) |
| |
| def declared_identifiers(self): |
| return self.body_decl.allargnames |