| # mako/_ast_util.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 |
| |
| """ |
| ast |
| ~~~ |
| |
| This is a stripped down version of Armin Ronacher's ast module. |
| |
| :copyright: Copyright 2008 by Armin Ronacher. |
| :license: Python License. |
| """ |
| |
| |
| from _ast import Add |
| from _ast import And |
| from _ast import AST |
| from _ast import BitAnd |
| from _ast import BitOr |
| from _ast import BitXor |
| from _ast import Div |
| from _ast import Eq |
| from _ast import FloorDiv |
| from _ast import Gt |
| from _ast import GtE |
| from _ast import If |
| from _ast import In |
| from _ast import Invert |
| from _ast import Is |
| from _ast import IsNot |
| from _ast import LShift |
| from _ast import Lt |
| from _ast import LtE |
| from _ast import Mod |
| from _ast import Mult |
| from _ast import Name |
| from _ast import Not |
| from _ast import NotEq |
| from _ast import NotIn |
| from _ast import Or |
| from _ast import PyCF_ONLY_AST |
| from _ast import RShift |
| from _ast import Sub |
| from _ast import UAdd |
| from _ast import USub |
| |
| from mako.compat import arg_stringname |
| |
| BOOLOP_SYMBOLS = {And: "and", Or: "or"} |
| |
| BINOP_SYMBOLS = { |
| Add: "+", |
| Sub: "-", |
| Mult: "*", |
| Div: "/", |
| FloorDiv: "//", |
| Mod: "%", |
| LShift: "<<", |
| RShift: ">>", |
| BitOr: "|", |
| BitAnd: "&", |
| BitXor: "^", |
| } |
| |
| CMPOP_SYMBOLS = { |
| Eq: "==", |
| Gt: ">", |
| GtE: ">=", |
| In: "in", |
| Is: "is", |
| IsNot: "is not", |
| Lt: "<", |
| LtE: "<=", |
| NotEq: "!=", |
| NotIn: "not in", |
| } |
| |
| UNARYOP_SYMBOLS = {Invert: "~", Not: "not", UAdd: "+", USub: "-"} |
| |
| ALL_SYMBOLS = {} |
| ALL_SYMBOLS.update(BOOLOP_SYMBOLS) |
| ALL_SYMBOLS.update(BINOP_SYMBOLS) |
| ALL_SYMBOLS.update(CMPOP_SYMBOLS) |
| ALL_SYMBOLS.update(UNARYOP_SYMBOLS) |
| |
| |
| def parse(expr, filename="<unknown>", mode="exec"): |
| """Parse an expression into an AST node.""" |
| return compile(expr, filename, mode, PyCF_ONLY_AST) |
| |
| |
| def iter_fields(node): |
| """Iterate over all fields of a node, only yielding existing fields.""" |
| # CPython 2.5 compat |
| if not hasattr(node, "_fields") or not node._fields: |
| return |
| for field in node._fields: |
| try: |
| yield field, getattr(node, field) |
| except AttributeError: |
| pass |
| |
| |
| class NodeVisitor(object): |
| |
| """ |
| Walks the abstract syntax tree and call visitor functions for every node |
| found. The visitor functions may return values which will be forwarded |
| by the `visit` method. |
| |
| Per default the visitor functions for the nodes are ``'visit_'`` + |
| class name of the node. So a `TryFinally` node visit function would |
| be `visit_TryFinally`. This behavior can be changed by overriding |
| the `get_visitor` function. If no visitor function exists for a node |
| (return value `None`) the `generic_visit` visitor is used instead. |
| |
| Don't use the `NodeVisitor` if you want to apply changes to nodes during |
| traversing. For this a special visitor exists (`NodeTransformer`) that |
| allows modifications. |
| """ |
| |
| def get_visitor(self, node): |
| """ |
| Return the visitor function for this node or `None` if no visitor |
| exists for this node. In that case the generic visit function is |
| used instead. |
| """ |
| method = "visit_" + node.__class__.__name__ |
| return getattr(self, method, None) |
| |
| def visit(self, node): |
| """Visit a node.""" |
| f = self.get_visitor(node) |
| if f is not None: |
| return f(node) |
| return self.generic_visit(node) |
| |
| def generic_visit(self, node): |
| """Called if no explicit visitor function exists for a node.""" |
| for field, value in iter_fields(node): |
| if isinstance(value, list): |
| for item in value: |
| if isinstance(item, AST): |
| self.visit(item) |
| elif isinstance(value, AST): |
| self.visit(value) |
| |
| |
| class NodeTransformer(NodeVisitor): |
| |
| """ |
| Walks the abstract syntax tree and allows modifications of nodes. |
| |
| The `NodeTransformer` will walk the AST and use the return value of the |
| visitor functions to replace or remove the old node. If the return |
| value of the visitor function is `None` the node will be removed |
| from the previous location otherwise it's replaced with the return |
| value. The return value may be the original node in which case no |
| replacement takes place. |
| |
| Here an example transformer that rewrites all `foo` to `data['foo']`:: |
| |
| class RewriteName(NodeTransformer): |
| |
| def visit_Name(self, node): |
| return copy_location(Subscript( |
| value=Name(id='data', ctx=Load()), |
| slice=Index(value=Str(s=node.id)), |
| ctx=node.ctx |
| ), node) |
| |
| Keep in mind that if the node you're operating on has child nodes |
| you must either transform the child nodes yourself or call the generic |
| visit function for the node first. |
| |
| Nodes that were part of a collection of statements (that applies to |
| all statement nodes) may also return a list of nodes rather than just |
| a single node. |
| |
| Usually you use the transformer like this:: |
| |
| node = YourTransformer().visit(node) |
| """ |
| |
| def generic_visit(self, node): |
| for field, old_value in iter_fields(node): |
| old_value = getattr(node, field, None) |
| if isinstance(old_value, list): |
| new_values = [] |
| for value in old_value: |
| if isinstance(value, AST): |
| value = self.visit(value) |
| if value is None: |
| continue |
| elif not isinstance(value, AST): |
| new_values.extend(value) |
| continue |
| new_values.append(value) |
| old_value[:] = new_values |
| elif isinstance(old_value, AST): |
| new_node = self.visit(old_value) |
| if new_node is None: |
| delattr(node, field) |
| else: |
| setattr(node, field, new_node) |
| return node |
| |
| |
| class SourceGenerator(NodeVisitor): |
| |
| """ |
| This visitor is able to transform a well formed syntax tree into python |
| sourcecode. For more details have a look at the docstring of the |
| `node_to_source` function. |
| """ |
| |
| def __init__(self, indent_with): |
| self.result = [] |
| self.indent_with = indent_with |
| self.indentation = 0 |
| self.new_lines = 0 |
| |
| def write(self, x): |
| if self.new_lines: |
| if self.result: |
| self.result.append("\n" * self.new_lines) |
| self.result.append(self.indent_with * self.indentation) |
| self.new_lines = 0 |
| self.result.append(x) |
| |
| def newline(self, n=1): |
| self.new_lines = max(self.new_lines, n) |
| |
| def body(self, statements): |
| self.new_line = True |
| self.indentation += 1 |
| for stmt in statements: |
| self.visit(stmt) |
| self.indentation -= 1 |
| |
| def body_or_else(self, node): |
| self.body(node.body) |
| if node.orelse: |
| self.newline() |
| self.write("else:") |
| self.body(node.orelse) |
| |
| def signature(self, node): |
| want_comma = [] |
| |
| def write_comma(): |
| if want_comma: |
| self.write(", ") |
| else: |
| want_comma.append(True) |
| |
| padding = [None] * (len(node.args) - len(node.defaults)) |
| for arg, default in zip(node.args, padding + node.defaults): |
| write_comma() |
| self.visit(arg) |
| if default is not None: |
| self.write("=") |
| self.visit(default) |
| if node.vararg is not None: |
| write_comma() |
| self.write("*" + arg_stringname(node.vararg)) |
| if node.kwarg is not None: |
| write_comma() |
| self.write("**" + arg_stringname(node.kwarg)) |
| |
| def decorators(self, node): |
| for decorator in node.decorator_list: |
| self.newline() |
| self.write("@") |
| self.visit(decorator) |
| |
| # Statements |
| |
| def visit_Assign(self, node): |
| self.newline() |
| for idx, target in enumerate(node.targets): |
| if idx: |
| self.write(", ") |
| self.visit(target) |
| self.write(" = ") |
| self.visit(node.value) |
| |
| def visit_AugAssign(self, node): |
| self.newline() |
| self.visit(node.target) |
| self.write(BINOP_SYMBOLS[type(node.op)] + "=") |
| self.visit(node.value) |
| |
| def visit_ImportFrom(self, node): |
| self.newline() |
| self.write("from %s%s import " % ("." * node.level, node.module)) |
| for idx, item in enumerate(node.names): |
| if idx: |
| self.write(", ") |
| self.write(item) |
| |
| def visit_Import(self, node): |
| self.newline() |
| for item in node.names: |
| self.write("import ") |
| self.visit(item) |
| |
| def visit_Expr(self, node): |
| self.newline() |
| self.generic_visit(node) |
| |
| def visit_FunctionDef(self, node): |
| self.newline(n=2) |
| self.decorators(node) |
| self.newline() |
| self.write("def %s(" % node.name) |
| self.signature(node.args) |
| self.write("):") |
| self.body(node.body) |
| |
| def visit_ClassDef(self, node): |
| have_args = [] |
| |
| def paren_or_comma(): |
| if have_args: |
| self.write(", ") |
| else: |
| have_args.append(True) |
| self.write("(") |
| |
| self.newline(n=3) |
| self.decorators(node) |
| self.newline() |
| self.write("class %s" % node.name) |
| for base in node.bases: |
| paren_or_comma() |
| self.visit(base) |
| # XXX: the if here is used to keep this module compatible |
| # with python 2.6. |
| if hasattr(node, "keywords"): |
| for keyword in node.keywords: |
| paren_or_comma() |
| self.write(keyword.arg + "=") |
| self.visit(keyword.value) |
| if getattr(node, "starargs", None): |
| paren_or_comma() |
| self.write("*") |
| self.visit(node.starargs) |
| if getattr(node, "kwargs", None): |
| paren_or_comma() |
| self.write("**") |
| self.visit(node.kwargs) |
| self.write(have_args and "):" or ":") |
| self.body(node.body) |
| |
| def visit_If(self, node): |
| self.newline() |
| self.write("if ") |
| self.visit(node.test) |
| self.write(":") |
| self.body(node.body) |
| while True: |
| else_ = node.orelse |
| if len(else_) == 1 and isinstance(else_[0], If): |
| node = else_[0] |
| self.newline() |
| self.write("elif ") |
| self.visit(node.test) |
| self.write(":") |
| self.body(node.body) |
| else: |
| self.newline() |
| self.write("else:") |
| self.body(else_) |
| break |
| |
| def visit_For(self, node): |
| self.newline() |
| self.write("for ") |
| self.visit(node.target) |
| self.write(" in ") |
| self.visit(node.iter) |
| self.write(":") |
| self.body_or_else(node) |
| |
| def visit_While(self, node): |
| self.newline() |
| self.write("while ") |
| self.visit(node.test) |
| self.write(":") |
| self.body_or_else(node) |
| |
| def visit_With(self, node): |
| self.newline() |
| self.write("with ") |
| self.visit(node.context_expr) |
| if node.optional_vars is not None: |
| self.write(" as ") |
| self.visit(node.optional_vars) |
| self.write(":") |
| self.body(node.body) |
| |
| def visit_Pass(self, node): |
| self.newline() |
| self.write("pass") |
| |
| def visit_Print(self, node): |
| # XXX: python 2.6 only |
| self.newline() |
| self.write("print ") |
| want_comma = False |
| if node.dest is not None: |
| self.write(" >> ") |
| self.visit(node.dest) |
| want_comma = True |
| for value in node.values: |
| if want_comma: |
| self.write(", ") |
| self.visit(value) |
| want_comma = True |
| if not node.nl: |
| self.write(",") |
| |
| def visit_Delete(self, node): |
| self.newline() |
| self.write("del ") |
| for idx, target in enumerate(node): |
| if idx: |
| self.write(", ") |
| self.visit(target) |
| |
| def visit_TryExcept(self, node): |
| self.newline() |
| self.write("try:") |
| self.body(node.body) |
| for handler in node.handlers: |
| self.visit(handler) |
| |
| def visit_TryFinally(self, node): |
| self.newline() |
| self.write("try:") |
| self.body(node.body) |
| self.newline() |
| self.write("finally:") |
| self.body(node.finalbody) |
| |
| def visit_Global(self, node): |
| self.newline() |
| self.write("global " + ", ".join(node.names)) |
| |
| def visit_Nonlocal(self, node): |
| self.newline() |
| self.write("nonlocal " + ", ".join(node.names)) |
| |
| def visit_Return(self, node): |
| self.newline() |
| self.write("return ") |
| self.visit(node.value) |
| |
| def visit_Break(self, node): |
| self.newline() |
| self.write("break") |
| |
| def visit_Continue(self, node): |
| self.newline() |
| self.write("continue") |
| |
| def visit_Raise(self, node): |
| # XXX: Python 2.6 / 3.0 compatibility |
| self.newline() |
| self.write("raise") |
| if hasattr(node, "exc") and node.exc is not None: |
| self.write(" ") |
| self.visit(node.exc) |
| if node.cause is not None: |
| self.write(" from ") |
| self.visit(node.cause) |
| elif hasattr(node, "type") and node.type is not None: |
| self.visit(node.type) |
| if node.inst is not None: |
| self.write(", ") |
| self.visit(node.inst) |
| if node.tback is not None: |
| self.write(", ") |
| self.visit(node.tback) |
| |
| # Expressions |
| |
| def visit_Attribute(self, node): |
| self.visit(node.value) |
| self.write("." + node.attr) |
| |
| def visit_Call(self, node): |
| want_comma = [] |
| |
| def write_comma(): |
| if want_comma: |
| self.write(", ") |
| else: |
| want_comma.append(True) |
| |
| self.visit(node.func) |
| self.write("(") |
| for arg in node.args: |
| write_comma() |
| self.visit(arg) |
| for keyword in node.keywords: |
| write_comma() |
| self.write(keyword.arg + "=") |
| self.visit(keyword.value) |
| if getattr(node, "starargs", None): |
| write_comma() |
| self.write("*") |
| self.visit(node.starargs) |
| if getattr(node, "kwargs", None): |
| write_comma() |
| self.write("**") |
| self.visit(node.kwargs) |
| self.write(")") |
| |
| def visit_Name(self, node): |
| self.write(node.id) |
| |
| def visit_NameConstant(self, node): |
| self.write(str(node.value)) |
| |
| def visit_arg(self, node): |
| self.write(node.arg) |
| |
| def visit_Str(self, node): |
| self.write(repr(node.s)) |
| |
| def visit_Bytes(self, node): |
| self.write(repr(node.s)) |
| |
| def visit_Num(self, node): |
| self.write(repr(node.n)) |
| |
| # newly needed in Python 3.8 |
| def visit_Constant(self, node): |
| self.write(repr(node.value)) |
| |
| def visit_Tuple(self, node): |
| self.write("(") |
| idx = -1 |
| for idx, item in enumerate(node.elts): |
| if idx: |
| self.write(", ") |
| self.visit(item) |
| self.write(idx and ")" or ",)") |
| |
| def sequence_visit(left, right): |
| def visit(self, node): |
| self.write(left) |
| for idx, item in enumerate(node.elts): |
| if idx: |
| self.write(", ") |
| self.visit(item) |
| self.write(right) |
| |
| return visit |
| |
| visit_List = sequence_visit("[", "]") |
| visit_Set = sequence_visit("{", "}") |
| del sequence_visit |
| |
| def visit_Dict(self, node): |
| self.write("{") |
| for idx, (key, value) in enumerate(zip(node.keys, node.values)): |
| if idx: |
| self.write(", ") |
| self.visit(key) |
| self.write(": ") |
| self.visit(value) |
| self.write("}") |
| |
| def visit_BinOp(self, node): |
| self.write("(") |
| self.visit(node.left) |
| self.write(" %s " % BINOP_SYMBOLS[type(node.op)]) |
| self.visit(node.right) |
| self.write(")") |
| |
| def visit_BoolOp(self, node): |
| self.write("(") |
| for idx, value in enumerate(node.values): |
| if idx: |
| self.write(" %s " % BOOLOP_SYMBOLS[type(node.op)]) |
| self.visit(value) |
| self.write(")") |
| |
| def visit_Compare(self, node): |
| self.write("(") |
| self.visit(node.left) |
| for op, right in zip(node.ops, node.comparators): |
| self.write(" %s " % CMPOP_SYMBOLS[type(op)]) |
| self.visit(right) |
| self.write(")") |
| |
| def visit_UnaryOp(self, node): |
| self.write("(") |
| op = UNARYOP_SYMBOLS[type(node.op)] |
| self.write(op) |
| if op == "not": |
| self.write(" ") |
| self.visit(node.operand) |
| self.write(")") |
| |
| def visit_Subscript(self, node): |
| self.visit(node.value) |
| self.write("[") |
| self.visit(node.slice) |
| self.write("]") |
| |
| def visit_Slice(self, node): |
| if node.lower is not None: |
| self.visit(node.lower) |
| self.write(":") |
| if node.upper is not None: |
| self.visit(node.upper) |
| if node.step is not None: |
| self.write(":") |
| if not (isinstance(node.step, Name) and node.step.id == "None"): |
| self.visit(node.step) |
| |
| def visit_ExtSlice(self, node): |
| for idx, item in node.dims: |
| if idx: |
| self.write(", ") |
| self.visit(item) |
| |
| def visit_Yield(self, node): |
| self.write("yield ") |
| self.visit(node.value) |
| |
| def visit_Lambda(self, node): |
| self.write("lambda ") |
| self.signature(node.args) |
| self.write(": ") |
| self.visit(node.body) |
| |
| def visit_Ellipsis(self, node): |
| self.write("Ellipsis") |
| |
| def generator_visit(left, right): |
| def visit(self, node): |
| self.write(left) |
| self.visit(node.elt) |
| for comprehension in node.generators: |
| self.visit(comprehension) |
| self.write(right) |
| |
| return visit |
| |
| visit_ListComp = generator_visit("[", "]") |
| visit_GeneratorExp = generator_visit("(", ")") |
| visit_SetComp = generator_visit("{", "}") |
| del generator_visit |
| |
| def visit_DictComp(self, node): |
| self.write("{") |
| self.visit(node.key) |
| self.write(": ") |
| self.visit(node.value) |
| for comprehension in node.generators: |
| self.visit(comprehension) |
| self.write("}") |
| |
| def visit_IfExp(self, node): |
| self.visit(node.body) |
| self.write(" if ") |
| self.visit(node.test) |
| self.write(" else ") |
| self.visit(node.orelse) |
| |
| def visit_Starred(self, node): |
| self.write("*") |
| self.visit(node.value) |
| |
| def visit_Repr(self, node): |
| # XXX: python 2.6 only |
| self.write("`") |
| self.visit(node.value) |
| self.write("`") |
| |
| # Helper Nodes |
| |
| def visit_alias(self, node): |
| self.write(node.name) |
| if node.asname is not None: |
| self.write(" as " + node.asname) |
| |
| def visit_comprehension(self, node): |
| self.write(" for ") |
| self.visit(node.target) |
| self.write(" in ") |
| self.visit(node.iter) |
| if node.ifs: |
| for if_ in node.ifs: |
| self.write(" if ") |
| self.visit(if_) |
| |
| def visit_excepthandler(self, node): |
| self.newline() |
| self.write("except") |
| if node.type is not None: |
| self.write(" ") |
| self.visit(node.type) |
| if node.name is not None: |
| self.write(" as ") |
| self.visit(node.name) |
| self.write(":") |
| self.body(node.body) |