| # mako/parsetree.py |
| # Copyright (C) 2006-2015 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.""" |
| |
| from mako import exceptions, ast, util, filters, compat |
| import re |
| |
| |
| 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. |
| |
| <%sometag/> |
| |
| <%someothertag> |
| stuff |
| </%someothertag> |
| |
| """ |
| __keyword__ = None |
| |
| def __init__(self, keyword, attributes, expressions, |
| nonexpressions, required, **kwargs): |
| """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 |