You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
657 lines
19 KiB
657 lines
19 KiB
# mako/parsetree.py
|
|
# Copyright 2006-2022 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 exceptions
|
|
from mako import filters
|
|
from mako import util
|
|
|
|
|
|
class Node:
|
|
|
|
"""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().__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().__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"""
|
|
|
|
cases = {
|
|
"if": {"else", "elif"},
|
|
"try": {"except", "finally"},
|
|
"for": {"else"},
|
|
}
|
|
|
|
return keyword in cases.get(self.keyword, set())
|
|
|
|
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().__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().__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().__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().__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(
|
|
filters.DEFAULT_ESCAPES
|
|
)
|
|
).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().__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(Node, metaclass=_TagMeta):
|
|
"""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().__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))
|
|
elif 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(
|
|
"Attribute '%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().__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(
|
|
{"__DUMMY"}
|
|
).difference(self.page_args.declared_identifiers)
|
|
return identifiers.union(super().undeclared_identifiers())
|
|
|
|
|
|
class NamespaceTag(Tag):
|
|
__keyword__ = "namespace"
|
|
|
|
def __init__(self, keyword, attributes, **kwargs):
|
|
super().__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().__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().__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().__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().__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().__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().__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().__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
|