From 0a65c6a6a7ecf86fbf467f713d3605501d9b6c57 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Wed, 20 Nov 2019 22:44:26 +0000 Subject: [PATCH 01/15] bpo-38870: Expose a function to unparse an ast object in the ast module --- Doc/library/ast.rst | 13 + Lib/ast.py | 667 +++++++++++++++++ Lib/test/{test_tools => }/test_unparse.py | 19 +- .../2019-11-20-22-43-48.bpo-38870.rLVZEv.rst | 4 + Tools/parser/unparse.py | 704 ------------------ 5 files changed, 689 insertions(+), 718 deletions(-) rename Lib/test/{test_tools => }/test_unparse.py (95%) create mode 100644 Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst delete mode 100644 Tools/parser/unparse.py diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index b468f4235df300..a7e0729b902d2c 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -161,6 +161,19 @@ and classes for traversing abstract syntax trees: Added ``type_comments``, ``mode='func_type'`` and ``feature_version``. +.. function:: unparse(ast_obj) + + Unparse an :class:`ast.AST` object and generate a string with code + that would produce an equivalent :class:`ast.AST` object if parsed + back with :func:`ast.parse`. + + .. warning:: + The produced code string will not necesarily be equal to the original + code that generated the :class:`ast.AST` object. + + .. versionadded:: 3.9 + + .. function:: literal_eval(node_or_string) Safely evaluate an expression node or a string containing a Python literal or diff --git a/Lib/ast.py b/Lib/ast.py index 720dd48a761b6d..28293f90cff385 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -24,6 +24,8 @@ :copyright: Copyright 2008 by Armin Ronacher. :license: Python License. """ +import sys +import io from _ast import * @@ -551,6 +553,671 @@ def __new__(cls, *args, **kwargs): type(...): 'Ellipsis', } +# Large float and imaginary literals get turned into infinities in the AST. +# We unparse those infinities to INFSTR. +INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) + +def interleave(inter, f, seq): + """Call f on each item in seq, calling inter() in between. + """ + seq = iter(seq) + try: + f(next(seq)) + except StopIteration: + pass + else: + for x in seq: + inter() + f(x) + +class _Unparser: + """Methods in this class recursively traverse an AST and + output source code for the abstract syntax; original formatting + is disregarded. """ + + def __init__(self, tree, file = sys.stdout): + """Unparser(tree, file=sys.stdout) -> None. + Print the source for tree to file.""" + self.f = file + self._indent = 0 + self.dispatch(tree) + print("", file=self.f) + self.f.flush() + + def fill(self, text = ""): + "Indent a piece of text, according to the current indentation level" + self.f.write("\n"+" "*self._indent + text) + + def write(self, text): + "Append a piece of text to the current line." + self.f.write(text) + + def enter(self): + "Print ':', and increase the indentation." + self.write(":") + self._indent += 1 + + def leave(self): + "Decrease the indentation level." + self._indent -= 1 + + def dispatch(self, tree): + "Dispatcher function, dispatching tree type T to method _T." + if isinstance(tree, list): + for t in tree: + self.dispatch(t) + return + meth = getattr(self, "_"+tree.__class__.__name__) + meth(tree) + + + ############### Unparsing methods ###################### + # There should be one method per concrete grammar type # + # Constructors should be grouped by sum type. Ideally, # + # this would follow the order in the grammar, but # + # currently doesn't. # + ######################################################## + + def _Module(self, tree): + for stmt in tree.body: + self.dispatch(stmt) + + # stmt + def _Expr(self, tree): + self.fill() + self.dispatch(tree.value) + + def _NamedExpr(self, tree): + self.write("(") + self.dispatch(tree.target) + self.write(" := ") + self.dispatch(tree.value) + self.write(")") + + def _Import(self, t): + self.fill("import ") + interleave(lambda: self.write(", "), self.dispatch, t.names) + + def _ImportFrom(self, t): + self.fill("from ") + self.write("." * t.level) + if t.module: + self.write(t.module) + self.write(" import ") + interleave(lambda: self.write(", "), self.dispatch, t.names) + + def _Assign(self, t): + self.fill() + for target in t.targets: + self.dispatch(target) + self.write(" = ") + self.dispatch(t.value) + + def _AugAssign(self, t): + self.fill() + self.dispatch(t.target) + self.write(" "+self.binop[t.op.__class__.__name__]+"= ") + self.dispatch(t.value) + + def _AnnAssign(self, t): + self.fill() + if not t.simple and isinstance(t.target, Name): + self.write('(') + self.dispatch(t.target) + if not t.simple and isinstance(t.target, Name): + self.write(')') + self.write(": ") + self.dispatch(t.annotation) + if t.value: + self.write(" = ") + self.dispatch(t.value) + + def _Return(self, t): + self.fill("return") + if t.value: + self.write(" ") + self.dispatch(t.value) + + def _Pass(self, t): + self.fill("pass") + + def _Break(self, t): + self.fill("break") + + def _Continue(self, t): + self.fill("continue") + + def _Delete(self, t): + self.fill("del ") + interleave(lambda: self.write(", "), self.dispatch, t.targets) + + def _Assert(self, t): + self.fill("assert ") + self.dispatch(t.test) + if t.msg: + self.write(", ") + self.dispatch(t.msg) + + def _Global(self, t): + self.fill("global ") + interleave(lambda: self.write(", "), self.write, t.names) + + def _Nonlocal(self, t): + self.fill("nonlocal ") + interleave(lambda: self.write(", "), self.write, t.names) + + def _Await(self, t): + self.write("(") + self.write("await") + if t.value: + self.write(" ") + self.dispatch(t.value) + self.write(")") + + def _Yield(self, t): + self.write("(") + self.write("yield") + if t.value: + self.write(" ") + self.dispatch(t.value) + self.write(")") + + def _YieldFrom(self, t): + self.write("(") + self.write("yield from") + if t.value: + self.write(" ") + self.dispatch(t.value) + self.write(")") + + def _Raise(self, t): + self.fill("raise") + if not t.exc: + assert not t.cause + return + self.write(" ") + self.dispatch(t.exc) + if t.cause: + self.write(" from ") + self.dispatch(t.cause) + + def _Try(self, t): + self.fill("try") + self.enter() + self.dispatch(t.body) + self.leave() + for ex in t.handlers: + self.dispatch(ex) + if t.orelse: + self.fill("else") + self.enter() + self.dispatch(t.orelse) + self.leave() + if t.finalbody: + self.fill("finally") + self.enter() + self.dispatch(t.finalbody) + self.leave() + + def _ExceptHandler(self, t): + self.fill("except") + if t.type: + self.write(" ") + self.dispatch(t.type) + if t.name: + self.write(" as ") + self.write(t.name) + self.enter() + self.dispatch(t.body) + self.leave() + + def _ClassDef(self, t): + self.write("\n") + for deco in t.decorator_list: + self.fill("@") + self.dispatch(deco) + self.fill("class "+t.name) + self.write("(") + comma = False + for e in t.bases: + if comma: self.write(", ") + else: comma = True + self.dispatch(e) + for e in t.keywords: + if comma: self.write(", ") + else: comma = True + self.dispatch(e) + self.write(")") + + self.enter() + self.dispatch(t.body) + self.leave() + + def _FunctionDef(self, t): + self.__FunctionDef_helper(t, "def") + + def _AsyncFunctionDef(self, t): + self.__FunctionDef_helper(t, "async def") + + def __FunctionDef_helper(self, t, fill_suffix): + self.write("\n") + for deco in t.decorator_list: + self.fill("@") + self.dispatch(deco) + def_str = fill_suffix+" "+t.name + "(" + self.fill(def_str) + self.dispatch(t.args) + self.write(")") + if t.returns: + self.write(" -> ") + self.dispatch(t.returns) + self.enter() + self.dispatch(t.body) + self.leave() + + def _For(self, t): + self.__For_helper("for ", t) + + def _AsyncFor(self, t): + self.__For_helper("async for ", t) + + def __For_helper(self, fill, t): + self.fill(fill) + self.dispatch(t.target) + self.write(" in ") + self.dispatch(t.iter) + self.enter() + self.dispatch(t.body) + self.leave() + if t.orelse: + self.fill("else") + self.enter() + self.dispatch(t.orelse) + self.leave() + + def _If(self, t): + self.fill("if ") + self.dispatch(t.test) + self.enter() + self.dispatch(t.body) + self.leave() + # collapse nested ifs into equivalent elifs. + while (t.orelse and len(t.orelse) == 1 and + isinstance(t.orelse[0], If)): + t = t.orelse[0] + self.fill("elif ") + self.dispatch(t.test) + self.enter() + self.dispatch(t.body) + self.leave() + # final else + if t.orelse: + self.fill("else") + self.enter() + self.dispatch(t.orelse) + self.leave() + + def _While(self, t): + self.fill("while ") + self.dispatch(t.test) + self.enter() + self.dispatch(t.body) + self.leave() + if t.orelse: + self.fill("else") + self.enter() + self.dispatch(t.orelse) + self.leave() + + def _With(self, t): + self.fill("with ") + interleave(lambda: self.write(", "), self.dispatch, t.items) + self.enter() + self.dispatch(t.body) + self.leave() + + def _AsyncWith(self, t): + self.fill("async with ") + interleave(lambda: self.write(", "), self.dispatch, t.items) + self.enter() + self.dispatch(t.body) + self.leave() + + # expr + def _JoinedStr(self, t): + self.write("f") + string = io.StringIO() + self._fstring_JoinedStr(t, string.write) + self.write(repr(string.getvalue())) + + def _FormattedValue(self, t): + self.write("f") + string = io.StringIO() + self._fstring_FormattedValue(t, string.write) + self.write(repr(string.getvalue())) + + def _fstring_JoinedStr(self, t, write): + for value in t.values: + meth = getattr(self, "_fstring_" + type(value).__name__) + meth(value, write) + + def _fstring_Constant(self, t, write): + assert isinstance(t.value, str) + value = t.value.replace("{", "{{").replace("}", "}}") + write(value) + + def _fstring_FormattedValue(self, t, write): + write("{") + expr = io.StringIO() + Unparser(t.value, expr) + expr = expr.getvalue().rstrip("\n") + if expr.startswith("{"): + write(" ") # Separate pair of opening brackets as "{ {" + write(expr) + if t.conversion != -1: + conversion = chr(t.conversion) + assert conversion in "sra" + write(f"!{conversion}") + if t.format_spec: + write(":") + meth = getattr(self, "_fstring_" + type(t.format_spec).__name__) + meth(t.format_spec, write) + write("}") + + def _Name(self, t): + self.write(t.id) + + def _write_constant(self, value): + if isinstance(value, (float, complex)): + # Substitute overflowing decimal literal for AST infinities. + self.write(repr(value).replace("inf", INFSTR)) + else: + self.write(repr(value)) + + def _Constant(self, t): + value = t.value + if isinstance(value, tuple): + self.write("(") + if len(value) == 1: + self._write_constant(value[0]) + self.write(",") + else: + interleave(lambda: self.write(", "), self._write_constant, value) + self.write(")") + elif value is ...: + self.write("...") + else: + if t.kind == "u": + self.write("u") + self._write_constant(t.value) + + def _List(self, t): + self.write("[") + interleave(lambda: self.write(", "), self.dispatch, t.elts) + self.write("]") + + def _ListComp(self, t): + self.write("[") + self.dispatch(t.elt) + for gen in t.generators: + self.dispatch(gen) + self.write("]") + + def _GeneratorExp(self, t): + self.write("(") + self.dispatch(t.elt) + for gen in t.generators: + self.dispatch(gen) + self.write(")") + + def _SetComp(self, t): + self.write("{") + self.dispatch(t.elt) + for gen in t.generators: + self.dispatch(gen) + self.write("}") + + def _DictComp(self, t): + self.write("{") + self.dispatch(t.key) + self.write(": ") + self.dispatch(t.value) + for gen in t.generators: + self.dispatch(gen) + self.write("}") + + def _comprehension(self, t): + if t.is_async: + self.write(" async for ") + else: + self.write(" for ") + self.dispatch(t.target) + self.write(" in ") + self.dispatch(t.iter) + for if_clause in t.ifs: + self.write(" if ") + self.dispatch(if_clause) + + def _IfExp(self, t): + self.write("(") + self.dispatch(t.body) + self.write(" if ") + self.dispatch(t.test) + self.write(" else ") + self.dispatch(t.orelse) + self.write(")") + + def _Set(self, t): + assert(t.elts) # should be at least one element + self.write("{") + interleave(lambda: self.write(", "), self.dispatch, t.elts) + self.write("}") + + def _Dict(self, t): + self.write("{") + def write_key_value_pair(k, v): + self.dispatch(k) + self.write(": ") + self.dispatch(v) + + def write_item(item): + k, v = item + if k is None: + # for dictionary unpacking operator in dicts {**{'y': 2}} + # see PEP 448 for details + self.write("**") + self.dispatch(v) + else: + write_key_value_pair(k, v) + interleave(lambda: self.write(", "), write_item, zip(t.keys, t.values)) + self.write("}") + + def _Tuple(self, t): + self.write("(") + if len(t.elts) == 1: + elt = t.elts[0] + self.dispatch(elt) + self.write(",") + else: + interleave(lambda: self.write(", "), self.dispatch, t.elts) + self.write(")") + + unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} + def _UnaryOp(self, t): + self.write("(") + self.write(self.unop[t.op.__class__.__name__]) + self.write(" ") + self.dispatch(t.operand) + self.write(")") + + binop = { "Add":"+", "Sub":"-", "Mult":"*", "MatMult":"@", "Div":"/", "Mod":"%", + "LShift":"<<", "RShift":">>", "BitOr":"|", "BitXor":"^", "BitAnd":"&", + "FloorDiv":"//", "Pow": "**"} + def _BinOp(self, t): + self.write("(") + self.dispatch(t.left) + self.write(" " + self.binop[t.op.__class__.__name__] + " ") + self.dispatch(t.right) + self.write(")") + + cmpops = {"Eq":"==", "NotEq":"!=", "Lt":"<", "LtE":"<=", "Gt":">", "GtE":">=", + "Is":"is", "IsNot":"is not", "In":"in", "NotIn":"not in"} + def _Compare(self, t): + self.write("(") + self.dispatch(t.left) + for o, e in zip(t.ops, t.comparators): + self.write(" " + self.cmpops[o.__class__.__name__] + " ") + self.dispatch(e) + self.write(")") + + boolops = {And: 'and', Or: 'or'} + def _BoolOp(self, t): + self.write("(") + s = " %s " % self.boolops[t.op.__class__] + interleave(lambda: self.write(s), self.dispatch, t.values) + self.write(")") + + def _Attribute(self,t): + self.dispatch(t.value) + # Special case: 3.__abs__() is a syntax error, so if t.value + # is an integer literal then we need to either parenthesize + # it or add an extra space to get 3 .__abs__(). + if isinstance(t.value, Constant) and isinstance(t.value.value, int): + self.write(" ") + self.write(".") + self.write(t.attr) + + def _Call(self, t): + self.dispatch(t.func) + self.write("(") + comma = False + for e in t.args: + if comma: self.write(", ") + else: comma = True + self.dispatch(e) + for e in t.keywords: + if comma: self.write(", ") + else: comma = True + self.dispatch(e) + self.write(")") + + def _Subscript(self, t): + self.dispatch(t.value) + self.write("[") + self.dispatch(t.slice) + self.write("]") + + def _Starred(self, t): + self.write("*") + self.dispatch(t.value) + + # slice + def _Ellipsis(self, t): + self.write("...") + + def _Index(self, t): + self.dispatch(t.value) + + def _Slice(self, t): + if t.lower: + self.dispatch(t.lower) + self.write(":") + if t.upper: + self.dispatch(t.upper) + if t.step: + self.write(":") + self.dispatch(t.step) + + def _ExtSlice(self, t): + interleave(lambda: self.write(', '), self.dispatch, t.dims) + + # argument + def _arg(self, t): + self.write(t.arg) + if t.annotation: + self.write(": ") + self.dispatch(t.annotation) + + # others + def _arguments(self, t): + first = True + # normal arguments + all_args = t.posonlyargs + t.args + defaults = [None] * (len(all_args) - len(t.defaults)) + t.defaults + for index, elements in enumerate(zip(all_args, defaults), 1): + a, d = elements + if first:first = False + else: self.write(", ") + self.dispatch(a) + if d: + self.write("=") + self.dispatch(d) + if index == len(t.posonlyargs): + self.write(", /") + + # varargs, or bare '*' if no varargs but keyword-only arguments present + if t.vararg or t.kwonlyargs: + if first:first = False + else: self.write(", ") + self.write("*") + if t.vararg: + self.write(t.vararg.arg) + if t.vararg.annotation: + self.write(": ") + self.dispatch(t.vararg.annotation) + + # keyword-only arguments + if t.kwonlyargs: + for a, d in zip(t.kwonlyargs, t.kw_defaults): + if first:first = False + else: self.write(", ") + self.dispatch(a), + if d: + self.write("=") + self.dispatch(d) + + # kwargs + if t.kwarg: + if first:first = False + else: self.write(", ") + self.write("**"+t.kwarg.arg) + if t.kwarg.annotation: + self.write(": ") + self.dispatch(t.kwarg.annotation) + + def _keyword(self, t): + if t.arg is None: + self.write("**") + else: + self.write(t.arg) + self.write("=") + self.dispatch(t.value) + + def _Lambda(self, t): + self.write("(") + self.write("lambda ") + self.dispatch(t.args) + self.write(": ") + self.dispatch(t.body) + self.write(")") + + def _alias(self, t): + self.write(t.name) + if t.asname: + self.write(" as "+t.asname) + + def _withitem(self, t): + self.dispatch(t.context_expr) + if t.optional_vars: + self.write(" as ") + self.dispatch(t.optional_vars) + + +def unparse(ast_obj,): + string = io.StringIO() + _Unparser(ast_obj, string) + return string.getvalue() + def main(): import argparse diff --git a/Lib/test/test_tools/test_unparse.py b/Lib/test/test_unparse.py similarity index 95% rename from Lib/test/test_tools/test_unparse.py rename to Lib/test/test_unparse.py index a958ebb51cc3d2..f11dbc7cfd5747 100644 --- a/Lib/test/test_tools/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -4,19 +4,11 @@ import test.support import io import os +import pathlib import random import tokenize import ast -from test.test_tools import basepath, toolsdir, skip_if_missing - -skip_if_missing() - -parser_path = os.path.join(toolsdir, "parser") - -with test.support.DirsOnSysPath(parser_path): - import unparse - def read_pyfile(filename): """Read and return the contents of a Python source file (as a string), taking into account the file encoding.""" @@ -125,9 +117,7 @@ def assertASTEqual(self, ast1, ast2): def check_roundtrip(self, code1, filename="internal"): ast1 = compile(code1, filename, "exec", ast.PyCF_ONLY_AST) - unparse_buffer = io.StringIO() - unparse.Unparser(ast1, unparse_buffer) - code2 = unparse_buffer.getvalue() + code2 = ast.unparse(ast1) ast2 = compile(code2, filename, "exec", ast.PyCF_ONLY_AST) self.assertASTEqual(ast1, ast2) @@ -280,8 +270,9 @@ def get_names(cls): names = [] for d in cls.test_directories: + basepath = (pathlib.Path(__file__).parent / ".." / "..").resolve() test_dir = os.path.join(basepath, d) - for n in os.listdir(test_dir): + for n in os.listdir(test_dir): if n.endswith('.py') and not n.startswith('bad'): names.append(os.path.join(test_dir, n)) @@ -315,4 +306,4 @@ def test_files(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst b/Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst new file mode 100644 index 00000000000000..d6cc6fc78a5ca2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst @@ -0,0 +1,4 @@ +Expose :func:`ast.unparse` as a function of the :mod:`ast` module that can +be used to unparse an :class:`ast.AST` object and produce a string with code +that would produce an equivalent :class:`ast.AST` object when parsed. Patch +by Pablo Galindo. diff --git a/Tools/parser/unparse.py b/Tools/parser/unparse.py deleted file mode 100644 index a5cc000676b022..00000000000000 --- a/Tools/parser/unparse.py +++ /dev/null @@ -1,704 +0,0 @@ -"Usage: unparse.py " -import sys -import ast -import tokenize -import io -import os - -# Large float and imaginary literals get turned into infinities in the AST. -# We unparse those infinities to INFSTR. -INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) - -def interleave(inter, f, seq): - """Call f on each item in seq, calling inter() in between. - """ - seq = iter(seq) - try: - f(next(seq)) - except StopIteration: - pass - else: - for x in seq: - inter() - f(x) - -class Unparser: - """Methods in this class recursively traverse an AST and - output source code for the abstract syntax; original formatting - is disregarded. """ - - def __init__(self, tree, file = sys.stdout): - """Unparser(tree, file=sys.stdout) -> None. - Print the source for tree to file.""" - self.f = file - self._indent = 0 - self.dispatch(tree) - print("", file=self.f) - self.f.flush() - - def fill(self, text = ""): - "Indent a piece of text, according to the current indentation level" - self.f.write("\n"+" "*self._indent + text) - - def write(self, text): - "Append a piece of text to the current line." - self.f.write(text) - - def enter(self): - "Print ':', and increase the indentation." - self.write(":") - self._indent += 1 - - def leave(self): - "Decrease the indentation level." - self._indent -= 1 - - def dispatch(self, tree): - "Dispatcher function, dispatching tree type T to method _T." - if isinstance(tree, list): - for t in tree: - self.dispatch(t) - return - meth = getattr(self, "_"+tree.__class__.__name__) - meth(tree) - - - ############### Unparsing methods ###################### - # There should be one method per concrete grammar type # - # Constructors should be grouped by sum type. Ideally, # - # this would follow the order in the grammar, but # - # currently doesn't. # - ######################################################## - - def _Module(self, tree): - for stmt in tree.body: - self.dispatch(stmt) - - # stmt - def _Expr(self, tree): - self.fill() - self.dispatch(tree.value) - - def _NamedExpr(self, tree): - self.write("(") - self.dispatch(tree.target) - self.write(" := ") - self.dispatch(tree.value) - self.write(")") - - def _Import(self, t): - self.fill("import ") - interleave(lambda: self.write(", "), self.dispatch, t.names) - - def _ImportFrom(self, t): - self.fill("from ") - self.write("." * t.level) - if t.module: - self.write(t.module) - self.write(" import ") - interleave(lambda: self.write(", "), self.dispatch, t.names) - - def _Assign(self, t): - self.fill() - for target in t.targets: - self.dispatch(target) - self.write(" = ") - self.dispatch(t.value) - - def _AugAssign(self, t): - self.fill() - self.dispatch(t.target) - self.write(" "+self.binop[t.op.__class__.__name__]+"= ") - self.dispatch(t.value) - - def _AnnAssign(self, t): - self.fill() - if not t.simple and isinstance(t.target, ast.Name): - self.write('(') - self.dispatch(t.target) - if not t.simple and isinstance(t.target, ast.Name): - self.write(')') - self.write(": ") - self.dispatch(t.annotation) - if t.value: - self.write(" = ") - self.dispatch(t.value) - - def _Return(self, t): - self.fill("return") - if t.value: - self.write(" ") - self.dispatch(t.value) - - def _Pass(self, t): - self.fill("pass") - - def _Break(self, t): - self.fill("break") - - def _Continue(self, t): - self.fill("continue") - - def _Delete(self, t): - self.fill("del ") - interleave(lambda: self.write(", "), self.dispatch, t.targets) - - def _Assert(self, t): - self.fill("assert ") - self.dispatch(t.test) - if t.msg: - self.write(", ") - self.dispatch(t.msg) - - def _Global(self, t): - self.fill("global ") - interleave(lambda: self.write(", "), self.write, t.names) - - def _Nonlocal(self, t): - self.fill("nonlocal ") - interleave(lambda: self.write(", "), self.write, t.names) - - def _Await(self, t): - self.write("(") - self.write("await") - if t.value: - self.write(" ") - self.dispatch(t.value) - self.write(")") - - def _Yield(self, t): - self.write("(") - self.write("yield") - if t.value: - self.write(" ") - self.dispatch(t.value) - self.write(")") - - def _YieldFrom(self, t): - self.write("(") - self.write("yield from") - if t.value: - self.write(" ") - self.dispatch(t.value) - self.write(")") - - def _Raise(self, t): - self.fill("raise") - if not t.exc: - assert not t.cause - return - self.write(" ") - self.dispatch(t.exc) - if t.cause: - self.write(" from ") - self.dispatch(t.cause) - - def _Try(self, t): - self.fill("try") - self.enter() - self.dispatch(t.body) - self.leave() - for ex in t.handlers: - self.dispatch(ex) - if t.orelse: - self.fill("else") - self.enter() - self.dispatch(t.orelse) - self.leave() - if t.finalbody: - self.fill("finally") - self.enter() - self.dispatch(t.finalbody) - self.leave() - - def _ExceptHandler(self, t): - self.fill("except") - if t.type: - self.write(" ") - self.dispatch(t.type) - if t.name: - self.write(" as ") - self.write(t.name) - self.enter() - self.dispatch(t.body) - self.leave() - - def _ClassDef(self, t): - self.write("\n") - for deco in t.decorator_list: - self.fill("@") - self.dispatch(deco) - self.fill("class "+t.name) - self.write("(") - comma = False - for e in t.bases: - if comma: self.write(", ") - else: comma = True - self.dispatch(e) - for e in t.keywords: - if comma: self.write(", ") - else: comma = True - self.dispatch(e) - self.write(")") - - self.enter() - self.dispatch(t.body) - self.leave() - - def _FunctionDef(self, t): - self.__FunctionDef_helper(t, "def") - - def _AsyncFunctionDef(self, t): - self.__FunctionDef_helper(t, "async def") - - def __FunctionDef_helper(self, t, fill_suffix): - self.write("\n") - for deco in t.decorator_list: - self.fill("@") - self.dispatch(deco) - def_str = fill_suffix+" "+t.name + "(" - self.fill(def_str) - self.dispatch(t.args) - self.write(")") - if t.returns: - self.write(" -> ") - self.dispatch(t.returns) - self.enter() - self.dispatch(t.body) - self.leave() - - def _For(self, t): - self.__For_helper("for ", t) - - def _AsyncFor(self, t): - self.__For_helper("async for ", t) - - def __For_helper(self, fill, t): - self.fill(fill) - self.dispatch(t.target) - self.write(" in ") - self.dispatch(t.iter) - self.enter() - self.dispatch(t.body) - self.leave() - if t.orelse: - self.fill("else") - self.enter() - self.dispatch(t.orelse) - self.leave() - - def _If(self, t): - self.fill("if ") - self.dispatch(t.test) - self.enter() - self.dispatch(t.body) - self.leave() - # collapse nested ifs into equivalent elifs. - while (t.orelse and len(t.orelse) == 1 and - isinstance(t.orelse[0], ast.If)): - t = t.orelse[0] - self.fill("elif ") - self.dispatch(t.test) - self.enter() - self.dispatch(t.body) - self.leave() - # final else - if t.orelse: - self.fill("else") - self.enter() - self.dispatch(t.orelse) - self.leave() - - def _While(self, t): - self.fill("while ") - self.dispatch(t.test) - self.enter() - self.dispatch(t.body) - self.leave() - if t.orelse: - self.fill("else") - self.enter() - self.dispatch(t.orelse) - self.leave() - - def _With(self, t): - self.fill("with ") - interleave(lambda: self.write(", "), self.dispatch, t.items) - self.enter() - self.dispatch(t.body) - self.leave() - - def _AsyncWith(self, t): - self.fill("async with ") - interleave(lambda: self.write(", "), self.dispatch, t.items) - self.enter() - self.dispatch(t.body) - self.leave() - - # expr - def _JoinedStr(self, t): - self.write("f") - string = io.StringIO() - self._fstring_JoinedStr(t, string.write) - self.write(repr(string.getvalue())) - - def _FormattedValue(self, t): - self.write("f") - string = io.StringIO() - self._fstring_FormattedValue(t, string.write) - self.write(repr(string.getvalue())) - - def _fstring_JoinedStr(self, t, write): - for value in t.values: - meth = getattr(self, "_fstring_" + type(value).__name__) - meth(value, write) - - def _fstring_Constant(self, t, write): - assert isinstance(t.value, str) - value = t.value.replace("{", "{{").replace("}", "}}") - write(value) - - def _fstring_FormattedValue(self, t, write): - write("{") - expr = io.StringIO() - Unparser(t.value, expr) - expr = expr.getvalue().rstrip("\n") - if expr.startswith("{"): - write(" ") # Separate pair of opening brackets as "{ {" - write(expr) - if t.conversion != -1: - conversion = chr(t.conversion) - assert conversion in "sra" - write(f"!{conversion}") - if t.format_spec: - write(":") - meth = getattr(self, "_fstring_" + type(t.format_spec).__name__) - meth(t.format_spec, write) - write("}") - - def _Name(self, t): - self.write(t.id) - - def _write_constant(self, value): - if isinstance(value, (float, complex)): - # Substitute overflowing decimal literal for AST infinities. - self.write(repr(value).replace("inf", INFSTR)) - else: - self.write(repr(value)) - - def _Constant(self, t): - value = t.value - if isinstance(value, tuple): - self.write("(") - if len(value) == 1: - self._write_constant(value[0]) - self.write(",") - else: - interleave(lambda: self.write(", "), self._write_constant, value) - self.write(")") - elif value is ...: - self.write("...") - else: - if t.kind == "u": - self.write("u") - self._write_constant(t.value) - - def _List(self, t): - self.write("[") - interleave(lambda: self.write(", "), self.dispatch, t.elts) - self.write("]") - - def _ListComp(self, t): - self.write("[") - self.dispatch(t.elt) - for gen in t.generators: - self.dispatch(gen) - self.write("]") - - def _GeneratorExp(self, t): - self.write("(") - self.dispatch(t.elt) - for gen in t.generators: - self.dispatch(gen) - self.write(")") - - def _SetComp(self, t): - self.write("{") - self.dispatch(t.elt) - for gen in t.generators: - self.dispatch(gen) - self.write("}") - - def _DictComp(self, t): - self.write("{") - self.dispatch(t.key) - self.write(": ") - self.dispatch(t.value) - for gen in t.generators: - self.dispatch(gen) - self.write("}") - - def _comprehension(self, t): - if t.is_async: - self.write(" async for ") - else: - self.write(" for ") - self.dispatch(t.target) - self.write(" in ") - self.dispatch(t.iter) - for if_clause in t.ifs: - self.write(" if ") - self.dispatch(if_clause) - - def _IfExp(self, t): - self.write("(") - self.dispatch(t.body) - self.write(" if ") - self.dispatch(t.test) - self.write(" else ") - self.dispatch(t.orelse) - self.write(")") - - def _Set(self, t): - assert(t.elts) # should be at least one element - self.write("{") - interleave(lambda: self.write(", "), self.dispatch, t.elts) - self.write("}") - - def _Dict(self, t): - self.write("{") - def write_key_value_pair(k, v): - self.dispatch(k) - self.write(": ") - self.dispatch(v) - - def write_item(item): - k, v = item - if k is None: - # for dictionary unpacking operator in dicts {**{'y': 2}} - # see PEP 448 for details - self.write("**") - self.dispatch(v) - else: - write_key_value_pair(k, v) - interleave(lambda: self.write(", "), write_item, zip(t.keys, t.values)) - self.write("}") - - def _Tuple(self, t): - self.write("(") - if len(t.elts) == 1: - elt = t.elts[0] - self.dispatch(elt) - self.write(",") - else: - interleave(lambda: self.write(", "), self.dispatch, t.elts) - self.write(")") - - unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} - def _UnaryOp(self, t): - self.write("(") - self.write(self.unop[t.op.__class__.__name__]) - self.write(" ") - self.dispatch(t.operand) - self.write(")") - - binop = { "Add":"+", "Sub":"-", "Mult":"*", "MatMult":"@", "Div":"/", "Mod":"%", - "LShift":"<<", "RShift":">>", "BitOr":"|", "BitXor":"^", "BitAnd":"&", - "FloorDiv":"//", "Pow": "**"} - def _BinOp(self, t): - self.write("(") - self.dispatch(t.left) - self.write(" " + self.binop[t.op.__class__.__name__] + " ") - self.dispatch(t.right) - self.write(")") - - cmpops = {"Eq":"==", "NotEq":"!=", "Lt":"<", "LtE":"<=", "Gt":">", "GtE":">=", - "Is":"is", "IsNot":"is not", "In":"in", "NotIn":"not in"} - def _Compare(self, t): - self.write("(") - self.dispatch(t.left) - for o, e in zip(t.ops, t.comparators): - self.write(" " + self.cmpops[o.__class__.__name__] + " ") - self.dispatch(e) - self.write(")") - - boolops = {ast.And: 'and', ast.Or: 'or'} - def _BoolOp(self, t): - self.write("(") - s = " %s " % self.boolops[t.op.__class__] - interleave(lambda: self.write(s), self.dispatch, t.values) - self.write(")") - - def _Attribute(self,t): - self.dispatch(t.value) - # Special case: 3.__abs__() is a syntax error, so if t.value - # is an integer literal then we need to either parenthesize - # it or add an extra space to get 3 .__abs__(). - if isinstance(t.value, ast.Constant) and isinstance(t.value.value, int): - self.write(" ") - self.write(".") - self.write(t.attr) - - def _Call(self, t): - self.dispatch(t.func) - self.write("(") - comma = False - for e in t.args: - if comma: self.write(", ") - else: comma = True - self.dispatch(e) - for e in t.keywords: - if comma: self.write(", ") - else: comma = True - self.dispatch(e) - self.write(")") - - def _Subscript(self, t): - self.dispatch(t.value) - self.write("[") - self.dispatch(t.slice) - self.write("]") - - def _Starred(self, t): - self.write("*") - self.dispatch(t.value) - - # slice - def _Ellipsis(self, t): - self.write("...") - - def _Index(self, t): - self.dispatch(t.value) - - def _Slice(self, t): - if t.lower: - self.dispatch(t.lower) - self.write(":") - if t.upper: - self.dispatch(t.upper) - if t.step: - self.write(":") - self.dispatch(t.step) - - def _ExtSlice(self, t): - interleave(lambda: self.write(', '), self.dispatch, t.dims) - - # argument - def _arg(self, t): - self.write(t.arg) - if t.annotation: - self.write(": ") - self.dispatch(t.annotation) - - # others - def _arguments(self, t): - first = True - # normal arguments - all_args = t.posonlyargs + t.args - defaults = [None] * (len(all_args) - len(t.defaults)) + t.defaults - for index, elements in enumerate(zip(all_args, defaults), 1): - a, d = elements - if first:first = False - else: self.write(", ") - self.dispatch(a) - if d: - self.write("=") - self.dispatch(d) - if index == len(t.posonlyargs): - self.write(", /") - - # varargs, or bare '*' if no varargs but keyword-only arguments present - if t.vararg or t.kwonlyargs: - if first:first = False - else: self.write(", ") - self.write("*") - if t.vararg: - self.write(t.vararg.arg) - if t.vararg.annotation: - self.write(": ") - self.dispatch(t.vararg.annotation) - - # keyword-only arguments - if t.kwonlyargs: - for a, d in zip(t.kwonlyargs, t.kw_defaults): - if first:first = False - else: self.write(", ") - self.dispatch(a), - if d: - self.write("=") - self.dispatch(d) - - # kwargs - if t.kwarg: - if first:first = False - else: self.write(", ") - self.write("**"+t.kwarg.arg) - if t.kwarg.annotation: - self.write(": ") - self.dispatch(t.kwarg.annotation) - - def _keyword(self, t): - if t.arg is None: - self.write("**") - else: - self.write(t.arg) - self.write("=") - self.dispatch(t.value) - - def _Lambda(self, t): - self.write("(") - self.write("lambda ") - self.dispatch(t.args) - self.write(": ") - self.dispatch(t.body) - self.write(")") - - def _alias(self, t): - self.write(t.name) - if t.asname: - self.write(" as "+t.asname) - - def _withitem(self, t): - self.dispatch(t.context_expr) - if t.optional_vars: - self.write(" as ") - self.dispatch(t.optional_vars) - -def roundtrip(filename, output=sys.stdout): - with open(filename, "rb") as pyfile: - encoding = tokenize.detect_encoding(pyfile.readline)[0] - with open(filename, "r", encoding=encoding) as pyfile: - source = pyfile.read() - tree = compile(source, filename, "exec", ast.PyCF_ONLY_AST) - Unparser(tree, output) - - - -def testdir(a): - try: - names = [n for n in os.listdir(a) if n.endswith('.py')] - except OSError: - print("Directory not readable: %s" % a, file=sys.stderr) - else: - for n in names: - fullname = os.path.join(a, n) - if os.path.isfile(fullname): - output = io.StringIO() - print('Testing %s' % fullname) - try: - roundtrip(fullname, output) - except Exception as e: - print(' Failed to compile, exception is %s' % repr(e)) - elif os.path.isdir(fullname): - testdir(fullname) - -def main(args): - if args[0] == '--testdir': - for a in args[1:]: - testdir(a) - else: - for a in args: - roundtrip(a) - -if __name__=='__main__': - main(sys.argv[1:]) From 1014fa8505effccde7a08d8dff92c649c2081d21 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Wed, 20 Nov 2019 22:57:04 +0000 Subject: [PATCH 02/15] Update what's new --- Doc/whatsnew/3.9.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 281173edb895b3..39d5a4d854afd5 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -121,6 +121,11 @@ Added the *indent* option to :func:`~ast.dump` which allows it to produce a multiline indented output. (Contributed by Serhiy Storchaka in :issue:`37995`.) +Added the :func:`ast.unparse` as a function in the :mod:`ast` module that can +be used to unparse an :class:`ast.AST` object and produce a string with code +that would produce an equivalent :class:`ast.AST` object when parsed. +(Contributed by Pablo Galindo in :issue:`38870`.) + asyncio ------- From eb28c5d3a7b4305254e7e2d03fac4232ee6a808c Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Wed, 20 Nov 2019 22:57:59 +0000 Subject: [PATCH 03/15] Fix typo in class name --- Lib/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ast.py b/Lib/ast.py index 28293f90cff385..86f20d9feb0e94 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -909,7 +909,7 @@ def _fstring_Constant(self, t, write): def _fstring_FormattedValue(self, t, write): write("{") expr = io.StringIO() - Unparser(t.value, expr) + _Unparser(t.value, expr) expr = expr.getvalue().rstrip("\n") if expr.startswith("{"): write(" ") # Separate pair of opening brackets as "{ {" From 2d93781c62e03bdb8c191c1ef2662e9d78e93522 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 21 Nov 2019 16:47:35 +0300 Subject: [PATCH 04/15] Use NodeVisitor instead of handmade dispatch() method in _Unparser --- Lib/ast.py | 609 ++++++++++++++++++++++++++--------------------------- 1 file changed, 303 insertions(+), 306 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 86f20d9feb0e94..e31e40853f1b12 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -570,7 +570,7 @@ def interleave(inter, f, seq): inter() f(x) -class _Unparser: +class _Unparser(NodeVisitor): """Methods in this class recursively traverse an AST and output source code for the abstract syntax; original formatting is disregarded. """ @@ -580,7 +580,7 @@ def __init__(self, tree, file = sys.stdout): Print the source for tree to file.""" self.f = file self._indent = 0 - self.dispatch(tree) + self.visit(tree) print("", file=self.f) self.f.flush() @@ -601,331 +601,323 @@ def leave(self): "Decrease the indentation level." self._indent -= 1 - def dispatch(self, tree): - "Dispatcher function, dispatching tree type T to method _T." - if isinstance(tree, list): - for t in tree: - self.dispatch(t) - return - meth = getattr(self, "_"+tree.__class__.__name__) - meth(tree) - - - ############### Unparsing methods ###################### - # There should be one method per concrete grammar type # - # Constructors should be grouped by sum type. Ideally, # - # this would follow the order in the grammar, but # - # currently doesn't. # - ######################################################## + def visit(self, node): + if isinstance(node, list): + for item in node: + super().visit(item) + else: + super().visit(node) - def _Module(self, tree): - for stmt in tree.body: - self.dispatch(stmt) + def visit_Module(self, node): + for subnode in node.body: + self.visit(subnode) - # stmt - def _Expr(self, tree): + def visit_Expr(self, tree): self.fill() - self.dispatch(tree.value) + self.visit(tree.value) - def _NamedExpr(self, tree): + def visit_NamedExpr(self, tree): self.write("(") - self.dispatch(tree.target) + self.visit(tree.target) self.write(" := ") - self.dispatch(tree.value) + self.visit(tree.value) self.write(")") - def _Import(self, t): + def visit_Import(self, node): self.fill("import ") - interleave(lambda: self.write(", "), self.dispatch, t.names) + interleave(lambda: self.write(", "), self.visit, node.names) - def _ImportFrom(self, t): + def visit_ImportFrom(self, node): self.fill("from ") - self.write("." * t.level) - if t.module: - self.write(t.module) + self.write("." * node.level) + if node.module: + self.write(node.module) self.write(" import ") - interleave(lambda: self.write(", "), self.dispatch, t.names) + interleave(lambda: self.write(", "), self.visit, node.names) - def _Assign(self, t): + def visit_Assign(self, node): self.fill() - for target in t.targets: - self.dispatch(target) + for target in node.targets: + self.visit(target) self.write(" = ") - self.dispatch(t.value) + self.visit(node.value) - def _AugAssign(self, t): + def visit_AugAssign(self, node): self.fill() - self.dispatch(t.target) - self.write(" "+self.binop[t.op.__class__.__name__]+"= ") - self.dispatch(t.value) + self.visit(node.target) + self.write(" "+self.binop[node.op.__class__.__name__]+"= ") + self.visit(node.value) - def _AnnAssign(self, t): + def visit_AnnAssign(self, node): self.fill() - if not t.simple and isinstance(t.target, Name): + if not node.simple and isinstance(node.target, Name): self.write('(') - self.dispatch(t.target) - if not t.simple and isinstance(t.target, Name): + self.visit(node.target) + if not node.simple and isinstance(node.target, Name): self.write(')') self.write(": ") - self.dispatch(t.annotation) - if t.value: + self.visit(node.annotation) + if node.value: self.write(" = ") - self.dispatch(t.value) + self.visit(node.value) - def _Return(self, t): + def visit_Return(self, node): self.fill("return") - if t.value: + if node.value: self.write(" ") - self.dispatch(t.value) + self.visit(node.value) - def _Pass(self, t): + def visit_Pass(self, node): self.fill("pass") - def _Break(self, t): + def visit_Break(self, node): self.fill("break") - def _Continue(self, t): + def visit_Continue(self, node): self.fill("continue") - def _Delete(self, t): + def visit_Delete(self, node): self.fill("del ") - interleave(lambda: self.write(", "), self.dispatch, t.targets) + interleave(lambda: self.write(", "), self.visit, node.targets) - def _Assert(self, t): + def visit_Assert(self, node): self.fill("assert ") - self.dispatch(t.test) - if t.msg: + self.visit(node.test) + if node.msg: self.write(", ") - self.dispatch(t.msg) + self.visit(node.msg) - def _Global(self, t): + def visit_Global(self, node): self.fill("global ") - interleave(lambda: self.write(", "), self.write, t.names) + interleave(lambda: self.write(", "), self.write, node.names) - def _Nonlocal(self, t): + def visit_Nonlocal(self, node): self.fill("nonlocal ") - interleave(lambda: self.write(", "), self.write, t.names) + interleave(lambda: self.write(", "), self.write, node.names) - def _Await(self, t): + def visit_Await(self, node): self.write("(") self.write("await") - if t.value: + if node.value: self.write(" ") - self.dispatch(t.value) + self.visit(node.value) self.write(")") - def _Yield(self, t): + def visit_Yield(self, node): self.write("(") self.write("yield") - if t.value: + if node.value: self.write(" ") - self.dispatch(t.value) + self.visit(node.value) self.write(")") - def _YieldFrom(self, t): + def visit_YieldFrom(self, node): self.write("(") self.write("yield from") - if t.value: + if node.value: self.write(" ") - self.dispatch(t.value) + self.visit(node.value) self.write(")") - def _Raise(self, t): + def visit_Raise(self, node): self.fill("raise") - if not t.exc: - assert not t.cause + if not node.exc: + assert not node.cause return self.write(" ") - self.dispatch(t.exc) - if t.cause: + self.visit(node.exc) + if node.cause: self.write(" from ") - self.dispatch(t.cause) + self.visit(node.cause) - def _Try(self, t): + def visit_Try(self, node): self.fill("try") self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - for ex in t.handlers: - self.dispatch(ex) - if t.orelse: + for ex in node.handlers: + self.visit(ex) + if node.orelse: self.fill("else") self.enter() - self.dispatch(t.orelse) + self.visit(node.orelse) self.leave() - if t.finalbody: + if node.finalbody: self.fill("finally") self.enter() - self.dispatch(t.finalbody) + self.visit(node.finalbody) self.leave() - def _ExceptHandler(self, t): + def visit_ExceptHandler(self, node): self.fill("except") - if t.type: + if node.type: self.write(" ") - self.dispatch(t.type) - if t.name: + self.visit(node.type) + if node.name: self.write(" as ") - self.write(t.name) + self.write(node.name) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - def _ClassDef(self, t): + def visit_ClassDef(self, node): self.write("\n") - for deco in t.decorator_list: + for deco in node.decorator_list: self.fill("@") - self.dispatch(deco) - self.fill("class "+t.name) + self.visit(deco) + self.fill("class "+node.name) self.write("(") comma = False - for e in t.bases: - if comma: self.write(", ") - else: comma = True - self.dispatch(e) - for e in t.keywords: - if comma: self.write(", ") - else: comma = True - self.dispatch(e) + for e in node.bases: + if comma: + self.write(", ") + else: + comma = True + self.visit(e) + for e in node.keywords: + if comma: + self.write(", ") + else: + comma = True + self.visit(e) self.write(")") self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - def _FunctionDef(self, t): - self.__FunctionDef_helper(t, "def") + def visit_FunctionDef(self, node): + self.__FunctionDef_helper(node, "def") - def _AsyncFunctionDef(self, t): - self.__FunctionDef_helper(t, "async def") + def visit_AsyncFunctionDef(self, node): + self.__FunctionDef_helper(node, "async def") - def __FunctionDef_helper(self, t, fill_suffix): + def __FunctionDef_helper(self, node, fill_suffix): self.write("\n") - for deco in t.decorator_list: + for deco in node.decorator_list: self.fill("@") - self.dispatch(deco) - def_str = fill_suffix+" "+t.name + "(" + self.visit(deco) + def_str = fill_suffix+" "+node.name + "(" self.fill(def_str) - self.dispatch(t.args) + self.visit(node.args) self.write(")") - if t.returns: + if node.returns: self.write(" -> ") - self.dispatch(t.returns) + self.visit(node.returns) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - def _For(self, t): - self.__For_helper("for ", t) + def visit_For(self, node): + self.__For_helper("for ", node) - def _AsyncFor(self, t): - self.__For_helper("async for ", t) + def visit_AsyncFor(self, node): + self.__For_helper("async for ", node) - def __For_helper(self, fill, t): + def __For_helper(self, fill, node): self.fill(fill) - self.dispatch(t.target) + self.visit(node.target) self.write(" in ") - self.dispatch(t.iter) + self.visit(node.iter) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - if t.orelse: + if node.orelse: self.fill("else") self.enter() - self.dispatch(t.orelse) + self.visit(node.orelse) self.leave() - def _If(self, t): + def visit_If(self, node): self.fill("if ") - self.dispatch(t.test) + self.visit(node.test) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() # collapse nested ifs into equivalent elifs. - while (t.orelse and len(t.orelse) == 1 and - isinstance(t.orelse[0], If)): - t = t.orelse[0] + while (node.orelse and len(node.orelse) == 1 and + isinstance(node.orelse[0], If)): + node = node.orelse[0] self.fill("elif ") - self.dispatch(t.test) + self.visit(node.test) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() # final else - if t.orelse: + if node.orelse: self.fill("else") self.enter() - self.dispatch(t.orelse) + self.visit(node.orelse) self.leave() - def _While(self, t): + def visit_While(self, node): self.fill("while ") - self.dispatch(t.test) + self.visit(node.test) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - if t.orelse: + if node.orelse: self.fill("else") self.enter() - self.dispatch(t.orelse) + self.visit(node.orelse) self.leave() - def _With(self, t): + def visit_With(self, node): self.fill("with ") - interleave(lambda: self.write(", "), self.dispatch, t.items) + interleave(lambda: self.write(", "), self.visit, node.items) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - def _AsyncWith(self, t): + def visit_AsyncWith(self, node): self.fill("async with ") - interleave(lambda: self.write(", "), self.dispatch, t.items) + interleave(lambda: self.write(", "), self.visit, node.items) self.enter() - self.dispatch(t.body) + self.visit(node.body) self.leave() - # expr - def _JoinedStr(self, t): + def visit_JoinedStr(self, node): self.write("f") string = io.StringIO() - self._fstring_JoinedStr(t, string.write) + self._fstring_JoinedStr(node, string.write) self.write(repr(string.getvalue())) - def _FormattedValue(self, t): + def visit_FormattedValue(self, node): self.write("f") string = io.StringIO() - self._fstring_FormattedValue(t, string.write) + self._fstring_FormattedValue(node, string.write) self.write(repr(string.getvalue())) - def _fstring_JoinedStr(self, t, write): - for value in t.values: + def _fstring_JoinedStr(self, node, write): + for value in node.values: meth = getattr(self, "_fstring_" + type(value).__name__) meth(value, write) - def _fstring_Constant(self, t, write): - assert isinstance(t.value, str) - value = t.value.replace("{", "{{").replace("}", "}}") + def _fstring_Constant(self, node, write): + assert isinstance(node.value, str) + value = node.value.replace("{", "{{").replace("}", "}}") write(value) - def _fstring_FormattedValue(self, t, write): + def _fstring_FormattedValue(self, node, write): write("{") expr = io.StringIO() - _Unparser(t.value, expr) + _Unparser(node.value, expr) expr = expr.getvalue().rstrip("\n") if expr.startswith("{"): write(" ") # Separate pair of opening brackets as "{ {" write(expr) - if t.conversion != -1: - conversion = chr(t.conversion) + if node.conversion != -1: + conversion = chr(node.conversion) assert conversion in "sra" write(f"!{conversion}") - if t.format_spec: + if node.format_spec: write(":") - meth = getattr(self, "_fstring_" + type(t.format_spec).__name__) - meth(t.format_spec, write) + meth = getattr(self, "_fstring_" + type(node.format_spec).__name__) + meth(node.format_spec, write) write("}") - def _Name(self, t): - self.write(t.id) + def visit_Name(self, node): + self.write(node.id) def _write_constant(self, value): if isinstance(value, (float, complex)): @@ -934,8 +926,8 @@ def _write_constant(self, value): else: self.write(repr(value)) - def _Constant(self, t): - value = t.value + def visit_Constant(self, node): + value = node.value if isinstance(value, tuple): self.write("(") if len(value) == 1: @@ -947,78 +939,78 @@ def _Constant(self, t): elif value is ...: self.write("...") else: - if t.kind == "u": + if node.kind == "u": self.write("u") - self._write_constant(t.value) + self._write_constant(node.value) - def _List(self, t): + def visit_List(self, node): self.write("[") - interleave(lambda: self.write(", "), self.dispatch, t.elts) + interleave(lambda: self.write(", "), self.visit, node.elts) self.write("]") - def _ListComp(self, t): + def visit_ListComp(self, node): self.write("[") - self.dispatch(t.elt) - for gen in t.generators: - self.dispatch(gen) + self.visit(node.elt) + for gen in node.generators: + self.visit(gen) self.write("]") - def _GeneratorExp(self, t): + def visit_GeneratorExp(self, node): self.write("(") - self.dispatch(t.elt) - for gen in t.generators: - self.dispatch(gen) + self.visit(node.elt) + for gen in node.generators: + self.visit(gen) self.write(")") - def _SetComp(self, t): + def visit_SetComp(self, node): self.write("{") - self.dispatch(t.elt) - for gen in t.generators: - self.dispatch(gen) + self.visit(node.elt) + for gen in node.generators: + self.visit(gen) self.write("}") - def _DictComp(self, t): + def visit_DictComp(self, node): self.write("{") - self.dispatch(t.key) + self.visit(node.key) self.write(": ") - self.dispatch(t.value) - for gen in t.generators: - self.dispatch(gen) + self.visit(node.value) + for gen in node.generators: + self.visit(gen) self.write("}") - def _comprehension(self, t): - if t.is_async: + def visit_comprehension(self, node): + if node.is_async: self.write(" async for ") else: self.write(" for ") - self.dispatch(t.target) + self.visit(node.target) self.write(" in ") - self.dispatch(t.iter) - for if_clause in t.ifs: + self.visit(node.iter) + for if_clause in node.ifs: self.write(" if ") - self.dispatch(if_clause) + self.visit(if_clause) - def _IfExp(self, t): + def visit_IfExp(self, node): self.write("(") - self.dispatch(t.body) + self.visit(node.body) self.write(" if ") - self.dispatch(t.test) + self.visit(node.test) self.write(" else ") - self.dispatch(t.orelse) + self.visit(node.orelse) self.write(")") - def _Set(self, t): - assert(t.elts) # should be at least one element + def visit_Set(self, node): + assert(node.elts) # should be at least one element self.write("{") - interleave(lambda: self.write(", "), self.dispatch, t.elts) + interleave(lambda: self.write(", "), self.visit, node.elts) self.write("}") - def _Dict(self, t): + def visit_Dict(self, node): self.write("{") def write_key_value_pair(k, v): - self.dispatch(k) + self.visit(k) self.write(": ") - self.dispatch(v) + self.visit(v) def write_item(item): k, v = item @@ -1026,191 +1018,196 @@ def write_item(item): # for dictionary unpacking operator in dicts {**{'y': 2}} # see PEP 448 for details self.write("**") - self.dispatch(v) + self.visit(v) else: write_key_value_pair(k, v) - interleave(lambda: self.write(", "), write_item, zip(t.keys, t.values)) + interleave(lambda: self.write(", "), write_item, zip(node.keys, node.values)) self.write("}") - def _Tuple(self, t): + def visit_Tuple(self, node): self.write("(") - if len(t.elts) == 1: - elt = t.elts[0] - self.dispatch(elt) + if len(node.elts) == 1: + elt = node.elts[0] + self.visit(elt) self.write(",") else: - interleave(lambda: self.write(", "), self.dispatch, t.elts) + interleave(lambda: self.write(", "), self.visit, node.elts) self.write(")") unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} - def _UnaryOp(self, t): + def visit_UnaryOp(self, node): self.write("(") - self.write(self.unop[t.op.__class__.__name__]) + self.write(self.unop[node.op.__class__.__name__]) self.write(" ") - self.dispatch(t.operand) + self.visit(node.operand) self.write(")") binop = { "Add":"+", "Sub":"-", "Mult":"*", "MatMult":"@", "Div":"/", "Mod":"%", "LShift":"<<", "RShift":">>", "BitOr":"|", "BitXor":"^", "BitAnd":"&", "FloorDiv":"//", "Pow": "**"} - def _BinOp(self, t): + def visit_BinOp(self, node): self.write("(") - self.dispatch(t.left) - self.write(" " + self.binop[t.op.__class__.__name__] + " ") - self.dispatch(t.right) + self.visit(node.left) + self.write(" " + self.binop[node.op.__class__.__name__] + " ") + self.visit(node.right) self.write(")") cmpops = {"Eq":"==", "NotEq":"!=", "Lt":"<", "LtE":"<=", "Gt":">", "GtE":">=", "Is":"is", "IsNot":"is not", "In":"in", "NotIn":"not in"} - def _Compare(self, t): + def visit_Compare(self, node): self.write("(") - self.dispatch(t.left) - for o, e in zip(t.ops, t.comparators): + self.visit(node.left) + for o, e in zip(node.ops, node.comparators): self.write(" " + self.cmpops[o.__class__.__name__] + " ") - self.dispatch(e) + self.visit(e) self.write(")") boolops = {And: 'and', Or: 'or'} - def _BoolOp(self, t): + def visit_BoolOp(self, node): self.write("(") - s = " %s " % self.boolops[t.op.__class__] - interleave(lambda: self.write(s), self.dispatch, t.values) + s = " %s " % self.boolops[node.op.__class__] + interleave(lambda: self.write(s), self.visit, node.values) self.write(")") - def _Attribute(self,t): - self.dispatch(t.value) - # Special case: 3.__abs__() is a syntax error, so if t.value + def visit_Attribute(self,node): + self.visit(node.value) + # Special case: 3.__abs__() is a syntax error, so if node.value # is an integer literal then we need to either parenthesize # it or add an extra space to get 3 .__abs__(). - if isinstance(t.value, Constant) and isinstance(t.value.value, int): + if isinstance(node.value, Constant) and isinstance(node.value.value, int): self.write(" ") self.write(".") - self.write(t.attr) + self.write(node.attr) - def _Call(self, t): - self.dispatch(t.func) + def visit_Call(self, node): + self.visit(node.func) self.write("(") comma = False - for e in t.args: + for e in node.args: if comma: self.write(", ") else: comma = True - self.dispatch(e) - for e in t.keywords: + self.visit(e) + for e in node.keywords: if comma: self.write(", ") else: comma = True - self.dispatch(e) + self.visit(e) self.write(")") - def _Subscript(self, t): - self.dispatch(t.value) + def visit_Subscript(self, node): + self.visit(node.value) self.write("[") - self.dispatch(t.slice) + self.visit(node.slice) self.write("]") - def _Starred(self, t): + def visit_Starred(self, node): self.write("*") - self.dispatch(t.value) + self.visit(node.value) - # slice - def _Ellipsis(self, t): + def visit_Ellipsis(self, node): self.write("...") - def _Index(self, t): - self.dispatch(t.value) + def visit_Index(self, node): + self.visit(node.value) - def _Slice(self, t): - if t.lower: - self.dispatch(t.lower) + def visit_Slice(self, node): + if node.lower: + self.visit(node.lower) self.write(":") - if t.upper: - self.dispatch(t.upper) - if t.step: + if node.upper: + self.visit(node.upper) + if node.step: self.write(":") - self.dispatch(t.step) + self.visit(node.step) - def _ExtSlice(self, t): - interleave(lambda: self.write(', '), self.dispatch, t.dims) + def visit_ExtSlice(self, node): + interleave(lambda: self.write(', '), self.visit, node.dims) - # argument - def _arg(self, t): - self.write(t.arg) - if t.annotation: + def visit_arg(self, node): + self.write(node.arg) + if node.annotation: self.write(": ") - self.dispatch(t.annotation) + self.visit(node.annotation) - # others - def _arguments(self, t): + def visit_arguments(self, node): first = True # normal arguments - all_args = t.posonlyargs + t.args - defaults = [None] * (len(all_args) - len(t.defaults)) + t.defaults + all_args = node.posonlyargs + node.args + defaults = [None] * (len(all_args) - len(node.defaults)) + node.defaults for index, elements in enumerate(zip(all_args, defaults), 1): a, d = elements - if first:first = False - else: self.write(", ") - self.dispatch(a) + if first: + first = False + else: + self.write(", ") + self.visit(a) if d: self.write("=") - self.dispatch(d) - if index == len(t.posonlyargs): + self.visit(d) + if index == len(node.posonlyargs): self.write(", /") # varargs, or bare '*' if no varargs but keyword-only arguments present - if t.vararg or t.kwonlyargs: - if first:first = False - else: self.write(", ") + if node.vararg or node.kwonlyargs: + if first: + first = False + else: + self.write(", ") self.write("*") - if t.vararg: - self.write(t.vararg.arg) - if t.vararg.annotation: + if node.vararg: + self.write(node.vararg.arg) + if node.vararg.annotation: self.write(": ") - self.dispatch(t.vararg.annotation) + self.visit(node.vararg.annotation) # keyword-only arguments - if t.kwonlyargs: - for a, d in zip(t.kwonlyargs, t.kw_defaults): - if first:first = False - else: self.write(", ") - self.dispatch(a), + if node.kwonlyargs: + for a, d in zip(node.kwonlyargs, node.kw_defaults): + if first: + first = False + else: + self.write(", ") + self.visit(a), if d: self.write("=") - self.dispatch(d) + self.visit(d) # kwargs - if t.kwarg: - if first:first = False - else: self.write(", ") - self.write("**"+t.kwarg.arg) - if t.kwarg.annotation: + if node.kwarg: + if first: + first = False + else: + self.write(", ") + self.write("**"+node.kwarg.arg) + if node.kwarg.annotation: self.write(": ") - self.dispatch(t.kwarg.annotation) + self.visit(node.kwarg.annotation) - def _keyword(self, t): - if t.arg is None: + def visit_keyword(self, node): + if node.arg is None: self.write("**") else: - self.write(t.arg) + self.write(node.arg) self.write("=") - self.dispatch(t.value) + self.visit(node.value) - def _Lambda(self, t): + def visit_Lambda(self, node): self.write("(") self.write("lambda ") - self.dispatch(t.args) + self.visit(node.args) self.write(": ") - self.dispatch(t.body) + self.visit(node.body) self.write(")") - def _alias(self, t): - self.write(t.name) - if t.asname: - self.write(" as "+t.asname) + def visit_alias(self, node): + self.write(node.name) + if node.asname: + self.write(" as "+node.asname) - def _withitem(self, t): - self.dispatch(t.context_expr) - if t.optional_vars: + def visit_withitem(self, node): + self.visit(node.context_expr) + if node.optional_vars: self.write(" as ") - self.dispatch(t.optional_vars) + self.visit(node.optional_vars) def unparse(ast_obj,): From 838e7ef682cb824a67556b3751c32f4652ae826e Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 21 Nov 2019 16:52:20 +0300 Subject: [PATCH 05/15] make _Unparser.enter a context manager and remove leave --- Lib/ast.py | 81 ++++++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index e31e40853f1b12..057d466c59bc11 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -27,6 +27,7 @@ import sys import io from _ast import * +from contextlib import contextmanager def parse(source, filename='', mode='exec', *, @@ -592,13 +593,12 @@ def write(self, text): "Append a piece of text to the current line." self.f.write(text) + @contextmanager def enter(self): "Print ':', and increase the indentation." self.write(":") self._indent += 1 - - def leave(self): - "Decrease the indentation level." + yield self._indent -= 1 def visit(self, node): @@ -732,21 +732,18 @@ def visit_Raise(self, node): def visit_Try(self, node): self.fill("try") - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) for ex in node.handlers: self.visit(ex) if node.orelse: self.fill("else") - self.enter() - self.visit(node.orelse) - self.leave() + with self.enter(): + self.visit(node.orelse) if node.finalbody: self.fill("finally") - self.enter() - self.visit(node.finalbody) - self.leave() + with self.enter(): + self.visit(node.finalbody) def visit_ExceptHandler(self, node): self.fill("except") @@ -756,9 +753,8 @@ def visit_ExceptHandler(self, node): if node.name: self.write(" as ") self.write(node.name) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) def visit_ClassDef(self, node): self.write("\n") @@ -782,9 +778,8 @@ def visit_ClassDef(self, node): self.visit(e) self.write(")") - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) def visit_FunctionDef(self, node): self.__FunctionDef_helper(node, "def") @@ -804,9 +799,8 @@ def __FunctionDef_helper(self, node, fill_suffix): if node.returns: self.write(" -> ") self.visit(node.returns) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) def visit_For(self, node): self.__For_helper("for ", node) @@ -819,62 +813,53 @@ def __For_helper(self, fill, node): self.visit(node.target) self.write(" in ") self.visit(node.iter) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) if node.orelse: self.fill("else") - self.enter() - self.visit(node.orelse) - self.leave() + with self.enter(): + self.visit(node.orelse) def visit_If(self, node): self.fill("if ") self.visit(node.test) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) # collapse nested ifs into equivalent elifs. while (node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], If)): node = node.orelse[0] self.fill("elif ") self.visit(node.test) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) # final else if node.orelse: self.fill("else") - self.enter() - self.visit(node.orelse) - self.leave() + with self.enter(): + self.visit(node.orelse) def visit_While(self, node): self.fill("while ") self.visit(node.test) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) if node.orelse: self.fill("else") - self.enter() - self.visit(node.orelse) - self.leave() + with self.enter(): + self.visit(node.orelse) def visit_With(self, node): self.fill("with ") interleave(lambda: self.write(", "), self.visit, node.items) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) def visit_AsyncWith(self, node): self.fill("async with ") interleave(lambda: self.write(", "), self.visit, node.items) - self.enter() - self.visit(node.body) - self.leave() + with self.enter(): + self.visit(node.body) def visit_JoinedStr(self, node): self.write("f") From e9434a9e124e223214da94066b3607559c6b4c39 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 21 Nov 2019 16:55:25 +0300 Subject: [PATCH 06/15] Make INFSTR a private constant, add interleave as a method to _Unparser --- Lib/ast.py | 58 +++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 057d466c59bc11..ab5e88aa297c3d 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -556,25 +556,13 @@ def __new__(cls, *args, **kwargs): # Large float and imaginary literals get turned into infinities in the AST. # We unparse those infinities to INFSTR. -INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) +_INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) -def interleave(inter, f, seq): - """Call f on each item in seq, calling inter() in between. - """ - seq = iter(seq) - try: - f(next(seq)) - except StopIteration: - pass - else: - for x in seq: - inter() - f(x) class _Unparser(NodeVisitor): """Methods in this class recursively traverse an AST and output source code for the abstract syntax; original formatting - is disregarded. """ + is disregarded.""" def __init__(self, tree, file = sys.stdout): """Unparser(tree, file=sys.stdout) -> None. @@ -585,6 +573,18 @@ def __init__(self, tree, file = sys.stdout): print("", file=self.f) self.f.flush() + def interleave(self, inter, f, seq): + """Call f on each item in seq, calling inter() in between.""" + seq = iter(seq) + try: + f(next(seq)) + except StopIteration: + pass + else: + for x in seq: + inter() + f(x) + def fill(self, text = ""): "Indent a piece of text, according to the current indentation level" self.f.write("\n"+" "*self._indent + text) @@ -625,7 +625,7 @@ def visit_NamedExpr(self, tree): def visit_Import(self, node): self.fill("import ") - interleave(lambda: self.write(", "), self.visit, node.names) + self.interleave(lambda: self.write(", "), self.visit, node.names) def visit_ImportFrom(self, node): self.fill("from ") @@ -633,7 +633,7 @@ def visit_ImportFrom(self, node): if node.module: self.write(node.module) self.write(" import ") - interleave(lambda: self.write(", "), self.visit, node.names) + self.interleave(lambda: self.write(", "), self.visit, node.names) def visit_Assign(self, node): self.fill() @@ -678,7 +678,7 @@ def visit_Continue(self, node): def visit_Delete(self, node): self.fill("del ") - interleave(lambda: self.write(", "), self.visit, node.targets) + self.interleave(lambda: self.write(", "), self.visit, node.targets) def visit_Assert(self, node): self.fill("assert ") @@ -689,11 +689,11 @@ def visit_Assert(self, node): def visit_Global(self, node): self.fill("global ") - interleave(lambda: self.write(", "), self.write, node.names) + self.interleave(lambda: self.write(", "), self.write, node.names) def visit_Nonlocal(self, node): self.fill("nonlocal ") - interleave(lambda: self.write(", "), self.write, node.names) + self.interleave(lambda: self.write(", "), self.write, node.names) def visit_Await(self, node): self.write("(") @@ -851,13 +851,13 @@ def visit_While(self, node): def visit_With(self, node): self.fill("with ") - interleave(lambda: self.write(", "), self.visit, node.items) + self.interleave(lambda: self.write(", "), self.visit, node.items) with self.enter(): self.visit(node.body) def visit_AsyncWith(self, node): self.fill("async with ") - interleave(lambda: self.write(", "), self.visit, node.items) + self.interleave(lambda: self.write(", "), self.visit, node.items) with self.enter(): self.visit(node.body) @@ -907,7 +907,7 @@ def visit_Name(self, node): def _write_constant(self, value): if isinstance(value, (float, complex)): # Substitute overflowing decimal literal for AST infinities. - self.write(repr(value).replace("inf", INFSTR)) + self.write(repr(value).replace("inf", _INFSTR)) else: self.write(repr(value)) @@ -919,7 +919,7 @@ def visit_Constant(self, node): self._write_constant(value[0]) self.write(",") else: - interleave(lambda: self.write(", "), self._write_constant, value) + self.interleave(lambda: self.write(", "), self._write_constant, value) self.write(")") elif value is ...: self.write("...") @@ -930,7 +930,7 @@ def visit_Constant(self, node): def visit_List(self, node): self.write("[") - interleave(lambda: self.write(", "), self.visit, node.elts) + self.interleave(lambda: self.write(", "), self.visit, node.elts) self.write("]") def visit_ListComp(self, node): @@ -987,7 +987,7 @@ def visit_IfExp(self, node): def visit_Set(self, node): assert(node.elts) # should be at least one element self.write("{") - interleave(lambda: self.write(", "), self.visit, node.elts) + self.interleave(lambda: self.write(", "), self.visit, node.elts) self.write("}") def visit_Dict(self, node): @@ -1006,7 +1006,7 @@ def write_item(item): self.visit(v) else: write_key_value_pair(k, v) - interleave(lambda: self.write(", "), write_item, zip(node.keys, node.values)) + self.interleave(lambda: self.write(", "), write_item, zip(node.keys, node.values)) self.write("}") def visit_Tuple(self, node): @@ -1016,7 +1016,7 @@ def visit_Tuple(self, node): self.visit(elt) self.write(",") else: - interleave(lambda: self.write(", "), self.visit, node.elts) + self.interleave(lambda: self.write(", "), self.visit, node.elts) self.write(")") unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} @@ -1051,7 +1051,7 @@ def visit_Compare(self, node): def visit_BoolOp(self, node): self.write("(") s = " %s " % self.boolops[node.op.__class__] - interleave(lambda: self.write(s), self.visit, node.values) + self.interleave(lambda: self.write(s), self.visit, node.values) self.write(")") def visit_Attribute(self,node): @@ -1105,7 +1105,7 @@ def visit_Slice(self, node): self.visit(node.step) def visit_ExtSlice(self, node): - interleave(lambda: self.write(', '), self.visit, node.dims) + self.interleave(lambda: self.write(', '), self.visit, node.dims) def visit_arg(self, node): self.write(node.arg) From 59f1efc399cd580865aaae584ef9c88057bd6fd5 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 21 Nov 2019 17:24:11 +0300 Subject: [PATCH 07/15] Modernize assert checks into ValueErrors, test that cases --- Lib/ast.py | 12 ++++++++---- Lib/test/test_unparse.py | 21 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index ab5e88aa297c3d..c1d36724cc693d 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -722,7 +722,8 @@ def visit_YieldFrom(self, node): def visit_Raise(self, node): self.fill("raise") if not node.exc: - assert not node.cause + if node.cause: + raise ValueError(f"Node can't use cause without an exception.") return self.write(" ") self.visit(node.exc) @@ -879,7 +880,8 @@ def _fstring_JoinedStr(self, node, write): meth(value, write) def _fstring_Constant(self, node, write): - assert isinstance(node.value, str) + if not isinstance(node.value, str): + raise ValueError("Constants inside JoinedStr should be a string.") value = node.value.replace("{", "{{").replace("}", "}}") write(value) @@ -893,7 +895,8 @@ def _fstring_FormattedValue(self, node, write): write(expr) if node.conversion != -1: conversion = chr(node.conversion) - assert conversion in "sra" + if conversion not in "sra": + raise ValueError("Unknown f-string conversion.") write(f"!{conversion}") if node.format_spec: write(":") @@ -985,7 +988,8 @@ def visit_IfExp(self, node): self.write(")") def visit_Set(self, node): - assert(node.elts) # should be at least one element + if not node.elts: + raise ValueError("Set node should has at least one item") self.write("{") self.interleave(lambda: self.write(", "), self.visit, node.elts) self.write("}") diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index f11dbc7cfd5747..9f5c0a7c1affac 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -121,6 +121,9 @@ def check_roundtrip(self, code1, filename="internal"): ast2 = compile(code2, filename, "exec", ast.PyCF_ONLY_AST) self.assertASTEqual(ast1, ast2) + def check_invalid(self, node, raises=ValueError): + self.assertRaises(raises, ast.unparse, node) + class UnparseTestCase(ASTTestCase): # Tests for specific bugs found in earlier versions of unparse @@ -255,6 +258,22 @@ def test_dict_unpacking_in_dict(self): self.check_roundtrip(r"""{**{'y': 2}, 'x': 1}""") self.check_roundtrip(r"""{**{'y': 2}, **{'x': 1}}""") + def test_invalid_raise(self): + self.check_invalid(ast.Raise(exc=None, cause=ast.Name(id="X"))) + + def test_invalid_fstring_constant(self): + self.check_invalid(ast.JoinedStr(values=[ast.Constant(value=100)])) + + def test_invalid_fstring_conversion(self): + self.check_invalid(ast.FormattedValue( + value=ast.Constant(value='a', kind=None), + conversion=ord("Y"), # random character + format_spec=None + )) + + def test_invalid_set(self): + self.check_invalid(ast.Set(elts=[])) + class DirectoryTestCase(ASTTestCase): """Test roundtrip behaviour on all files in Lib and Lib/test.""" @@ -306,4 +325,4 @@ def test_files(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 0b39840db7124d6a907e096e02147246a395fe41 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 21 Nov 2019 17:36:14 +0300 Subject: [PATCH 08/15] Remove IO-like object API for _Unparser, convert it to node-based API --- Lib/ast.py | 270 +++++++++++++++++++++++++++-------------------------- 1 file changed, 139 insertions(+), 131 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index c1d36724cc693d..aab705d8f4cacc 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -25,7 +25,6 @@ :license: Python License. """ import sys -import io from _ast import * from contextlib import contextmanager @@ -564,14 +563,10 @@ class _Unparser(NodeVisitor): output source code for the abstract syntax; original formatting is disregarded.""" - def __init__(self, tree, file = sys.stdout): - """Unparser(tree, file=sys.stdout) -> None. - Print the source for tree to file.""" - self.f = file + def __init__(self): + self._source = [] + self._buffer = [] self._indent = 0 - self.visit(tree) - print("", file=self.f) - self.f.flush() def interleave(self, inter, f, seq): """Call f on each item in seq, calling inter() in between.""" @@ -586,46 +581,64 @@ def interleave(self, inter, f, seq): f(x) def fill(self, text = ""): - "Indent a piece of text, according to the current indentation level" - self.f.write("\n"+" "*self._indent + text) + """Indent a piece of text and append it, according to the current + indentation level""" + self.write("\n"+" "*self._indent + text) def write(self, text): - "Append a piece of text to the current line." - self.f.write(text) + """Append a piece of text""" + self._source.append(text) + + def buffer_writer(self, text): + self._buffer.append(text) + + @property + def buffer(self): + value = "".join(self._buffer) + self._buffer.clear() + return value @contextmanager def enter(self): - "Print ':', and increase the indentation." + """A context manager for printing ':' and increasing the indentation, + after exiting from context it will decrease the indentation.""" self.write(":") self._indent += 1 yield self._indent -= 1 - def visit(self, node): + def traverse(self, node): if isinstance(node, list): for item in node: super().visit(item) else: super().visit(node) + def visit(self, node): + """Outputs a source code that can be converted again and generate + the same AST with given tree.""" + self._source = [] + self.traverse(node) + return "".join(self._source) + def visit_Module(self, node): for subnode in node.body: - self.visit(subnode) + self.traverse(subnode) def visit_Expr(self, tree): self.fill() - self.visit(tree.value) + self.traverse(tree.value) def visit_NamedExpr(self, tree): self.write("(") - self.visit(tree.target) + self.traverse(tree.target) self.write(" := ") - self.visit(tree.value) + self.traverse(tree.value) self.write(")") def visit_Import(self, node): self.fill("import ") - self.interleave(lambda: self.write(", "), self.visit, node.names) + self.interleave(lambda: self.write(", "), self.traverse, node.names) def visit_ImportFrom(self, node): self.fill("from ") @@ -633,39 +646,39 @@ def visit_ImportFrom(self, node): if node.module: self.write(node.module) self.write(" import ") - self.interleave(lambda: self.write(", "), self.visit, node.names) + self.interleave(lambda: self.write(", "), self.traverse, node.names) def visit_Assign(self, node): self.fill() for target in node.targets: - self.visit(target) + self.traverse(target) self.write(" = ") - self.visit(node.value) + self.traverse(node.value) def visit_AugAssign(self, node): self.fill() - self.visit(node.target) + self.traverse(node.target) self.write(" "+self.binop[node.op.__class__.__name__]+"= ") - self.visit(node.value) + self.traverse(node.value) def visit_AnnAssign(self, node): self.fill() if not node.simple and isinstance(node.target, Name): self.write('(') - self.visit(node.target) + self.traverse(node.target) if not node.simple and isinstance(node.target, Name): self.write(')') self.write(": ") - self.visit(node.annotation) + self.traverse(node.annotation) if node.value: self.write(" = ") - self.visit(node.value) + self.traverse(node.value) def visit_Return(self, node): self.fill("return") if node.value: self.write(" ") - self.visit(node.value) + self.traverse(node.value) def visit_Pass(self, node): self.fill("pass") @@ -678,14 +691,14 @@ def visit_Continue(self, node): def visit_Delete(self, node): self.fill("del ") - self.interleave(lambda: self.write(", "), self.visit, node.targets) + self.interleave(lambda: self.write(", "), self.traverse, node.targets) def visit_Assert(self, node): self.fill("assert ") - self.visit(node.test) + self.traverse(node.test) if node.msg: self.write(", ") - self.visit(node.msg) + self.traverse(node.msg) def visit_Global(self, node): self.fill("global ") @@ -700,7 +713,7 @@ def visit_Await(self, node): self.write("await") if node.value: self.write(" ") - self.visit(node.value) + self.traverse(node.value) self.write(")") def visit_Yield(self, node): @@ -708,7 +721,7 @@ def visit_Yield(self, node): self.write("yield") if node.value: self.write(" ") - self.visit(node.value) + self.traverse(node.value) self.write(")") def visit_YieldFrom(self, node): @@ -716,7 +729,7 @@ def visit_YieldFrom(self, node): self.write("yield from") if node.value: self.write(" ") - self.visit(node.value) + self.traverse(node.value) self.write(")") def visit_Raise(self, node): @@ -726,42 +739,42 @@ def visit_Raise(self, node): raise ValueError(f"Node can't use cause without an exception.") return self.write(" ") - self.visit(node.exc) + self.traverse(node.exc) if node.cause: self.write(" from ") - self.visit(node.cause) + self.traverse(node.cause) def visit_Try(self, node): self.fill("try") with self.enter(): - self.visit(node.body) + self.traverse(node.body) for ex in node.handlers: - self.visit(ex) + self.traverse(ex) if node.orelse: self.fill("else") with self.enter(): - self.visit(node.orelse) + self.traverse(node.orelse) if node.finalbody: self.fill("finally") with self.enter(): - self.visit(node.finalbody) + self.traverse(node.finalbody) def visit_ExceptHandler(self, node): self.fill("except") if node.type: self.write(" ") - self.visit(node.type) + self.traverse(node.type) if node.name: self.write(" as ") self.write(node.name) with self.enter(): - self.visit(node.body) + self.traverse(node.body) def visit_ClassDef(self, node): self.write("\n") for deco in node.decorator_list: self.fill("@") - self.visit(deco) + self.traverse(deco) self.fill("class "+node.name) self.write("(") comma = False @@ -770,17 +783,17 @@ def visit_ClassDef(self, node): self.write(", ") else: comma = True - self.visit(e) + self.traverse(e) for e in node.keywords: if comma: self.write(", ") else: comma = True - self.visit(e) + self.traverse(e) self.write(")") with self.enter(): - self.visit(node.body) + self.traverse(node.body) def visit_FunctionDef(self, node): self.__FunctionDef_helper(node, "def") @@ -792,16 +805,16 @@ def __FunctionDef_helper(self, node, fill_suffix): self.write("\n") for deco in node.decorator_list: self.fill("@") - self.visit(deco) + self.traverse(deco) def_str = fill_suffix+" "+node.name + "(" self.fill(def_str) - self.visit(node.args) + self.traverse(node.args) self.write(")") if node.returns: self.write(" -> ") - self.visit(node.returns) + self.traverse(node.returns) with self.enter(): - self.visit(node.body) + self.traverse(node.body) def visit_For(self, node): self.__For_helper("for ", node) @@ -811,68 +824,66 @@ def visit_AsyncFor(self, node): def __For_helper(self, fill, node): self.fill(fill) - self.visit(node.target) + self.traverse(node.target) self.write(" in ") - self.visit(node.iter) + self.traverse(node.iter) with self.enter(): - self.visit(node.body) + self.traverse(node.body) if node.orelse: self.fill("else") with self.enter(): - self.visit(node.orelse) + self.traverse(node.orelse) def visit_If(self, node): self.fill("if ") - self.visit(node.test) + self.traverse(node.test) with self.enter(): - self.visit(node.body) + self.traverse(node.body) # collapse nested ifs into equivalent elifs. while (node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], If)): node = node.orelse[0] self.fill("elif ") - self.visit(node.test) + self.traverse(node.test) with self.enter(): - self.visit(node.body) + self.traverse(node.body) # final else if node.orelse: self.fill("else") with self.enter(): - self.visit(node.orelse) + self.traverse(node.orelse) def visit_While(self, node): self.fill("while ") - self.visit(node.test) + self.traverse(node.test) with self.enter(): - self.visit(node.body) + self.traverse(node.body) if node.orelse: self.fill("else") with self.enter(): - self.visit(node.orelse) + self.traverse(node.orelse) def visit_With(self, node): self.fill("with ") - self.interleave(lambda: self.write(", "), self.visit, node.items) + self.interleave(lambda: self.write(", "), self.traverse, node.items) with self.enter(): - self.visit(node.body) + self.traverse(node.body) def visit_AsyncWith(self, node): self.fill("async with ") - self.interleave(lambda: self.write(", "), self.visit, node.items) + self.interleave(lambda: self.write(", "), self.traverse, node.items) with self.enter(): - self.visit(node.body) + self.traverse(node.body) def visit_JoinedStr(self, node): self.write("f") - string = io.StringIO() - self._fstring_JoinedStr(node, string.write) - self.write(repr(string.getvalue())) + self._fstring_JoinedStr(node, self.buffer_writer) + self.write(repr(self.buffer)) def visit_FormattedValue(self, node): self.write("f") - string = io.StringIO() - self._fstring_FormattedValue(node, string.write) - self.write(repr(string.getvalue())) + self._fstring_FormattedValue(node, self.buffer_writer) + self.write(repr(self.buffer)) def _fstring_JoinedStr(self, node, write): for value in node.values: @@ -887,9 +898,7 @@ def _fstring_Constant(self, node, write): def _fstring_FormattedValue(self, node, write): write("{") - expr = io.StringIO() - _Unparser(node.value, expr) - expr = expr.getvalue().rstrip("\n") + expr = type(self)().visit(node.value).rstrip("\n") if expr.startswith("{"): write(" ") # Separate pair of opening brackets as "{ {" write(expr) @@ -933,37 +942,37 @@ def visit_Constant(self, node): def visit_List(self, node): self.write("[") - self.interleave(lambda: self.write(", "), self.visit, node.elts) + self.interleave(lambda: self.write(", "), self.traverse, node.elts) self.write("]") def visit_ListComp(self, node): self.write("[") - self.visit(node.elt) + self.traverse(node.elt) for gen in node.generators: - self.visit(gen) + self.traverse(gen) self.write("]") def visit_GeneratorExp(self, node): self.write("(") - self.visit(node.elt) + self.traverse(node.elt) for gen in node.generators: - self.visit(gen) + self.traverse(gen) self.write(")") def visit_SetComp(self, node): self.write("{") - self.visit(node.elt) + self.traverse(node.elt) for gen in node.generators: - self.visit(gen) + self.traverse(gen) self.write("}") def visit_DictComp(self, node): self.write("{") - self.visit(node.key) + self.traverse(node.key) self.write(": ") - self.visit(node.value) + self.traverse(node.value) for gen in node.generators: - self.visit(gen) + self.traverse(gen) self.write("}") def visit_comprehension(self, node): @@ -971,35 +980,35 @@ def visit_comprehension(self, node): self.write(" async for ") else: self.write(" for ") - self.visit(node.target) + self.traverse(node.target) self.write(" in ") - self.visit(node.iter) + self.traverse(node.iter) for if_clause in node.ifs: self.write(" if ") - self.visit(if_clause) + self.traverse(if_clause) def visit_IfExp(self, node): self.write("(") - self.visit(node.body) + self.traverse(node.body) self.write(" if ") - self.visit(node.test) + self.traverse(node.test) self.write(" else ") - self.visit(node.orelse) + self.traverse(node.orelse) self.write(")") def visit_Set(self, node): if not node.elts: raise ValueError("Set node should has at least one item") self.write("{") - self.interleave(lambda: self.write(", "), self.visit, node.elts) + self.interleave(lambda: self.write(", "), self.traverse, node.elts) self.write("}") def visit_Dict(self, node): self.write("{") def write_key_value_pair(k, v): - self.visit(k) + self.traverse(k) self.write(": ") - self.visit(v) + self.traverse(v) def write_item(item): k, v = item @@ -1007,7 +1016,7 @@ def write_item(item): # for dictionary unpacking operator in dicts {**{'y': 2}} # see PEP 448 for details self.write("**") - self.visit(v) + self.traverse(v) else: write_key_value_pair(k, v) self.interleave(lambda: self.write(", "), write_item, zip(node.keys, node.values)) @@ -1017,10 +1026,10 @@ def visit_Tuple(self, node): self.write("(") if len(node.elts) == 1: elt = node.elts[0] - self.visit(elt) + self.traverse(elt) self.write(",") else: - self.interleave(lambda: self.write(", "), self.visit, node.elts) + self.interleave(lambda: self.write(", "), self.traverse, node.elts) self.write(")") unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} @@ -1028,7 +1037,7 @@ def visit_UnaryOp(self, node): self.write("(") self.write(self.unop[node.op.__class__.__name__]) self.write(" ") - self.visit(node.operand) + self.traverse(node.operand) self.write(")") binop = { "Add":"+", "Sub":"-", "Mult":"*", "MatMult":"@", "Div":"/", "Mod":"%", @@ -1036,30 +1045,30 @@ def visit_UnaryOp(self, node): "FloorDiv":"//", "Pow": "**"} def visit_BinOp(self, node): self.write("(") - self.visit(node.left) + self.traverse(node.left) self.write(" " + self.binop[node.op.__class__.__name__] + " ") - self.visit(node.right) + self.traverse(node.right) self.write(")") cmpops = {"Eq":"==", "NotEq":"!=", "Lt":"<", "LtE":"<=", "Gt":">", "GtE":">=", "Is":"is", "IsNot":"is not", "In":"in", "NotIn":"not in"} def visit_Compare(self, node): self.write("(") - self.visit(node.left) + self.traverse(node.left) for o, e in zip(node.ops, node.comparators): self.write(" " + self.cmpops[o.__class__.__name__] + " ") - self.visit(e) + self.traverse(e) self.write(")") boolops = {And: 'and', Or: 'or'} def visit_BoolOp(self, node): self.write("(") s = " %s " % self.boolops[node.op.__class__] - self.interleave(lambda: self.write(s), self.visit, node.values) + self.interleave(lambda: self.write(s), self.traverse, node.values) self.write(")") def visit_Attribute(self,node): - self.visit(node.value) + self.traverse(node.value) # Special case: 3.__abs__() is a syntax error, so if node.value # is an integer literal then we need to either parenthesize # it or add an extra space to get 3 .__abs__(). @@ -1069,53 +1078,53 @@ def visit_Attribute(self,node): self.write(node.attr) def visit_Call(self, node): - self.visit(node.func) + self.traverse(node.func) self.write("(") comma = False for e in node.args: if comma: self.write(", ") else: comma = True - self.visit(e) + self.traverse(e) for e in node.keywords: if comma: self.write(", ") else: comma = True - self.visit(e) + self.traverse(e) self.write(")") def visit_Subscript(self, node): - self.visit(node.value) + self.traverse(node.value) self.write("[") - self.visit(node.slice) + self.traverse(node.slice) self.write("]") def visit_Starred(self, node): self.write("*") - self.visit(node.value) + self.traverse(node.value) def visit_Ellipsis(self, node): self.write("...") def visit_Index(self, node): - self.visit(node.value) + self.traverse(node.value) def visit_Slice(self, node): if node.lower: - self.visit(node.lower) + self.traverse(node.lower) self.write(":") if node.upper: - self.visit(node.upper) + self.traverse(node.upper) if node.step: self.write(":") - self.visit(node.step) + self.traverse(node.step) def visit_ExtSlice(self, node): - self.interleave(lambda: self.write(', '), self.visit, node.dims) + self.interleave(lambda: self.write(', '), self.traverse, node.dims) def visit_arg(self, node): self.write(node.arg) if node.annotation: self.write(": ") - self.visit(node.annotation) + self.traverse(node.annotation) def visit_arguments(self, node): first = True @@ -1128,10 +1137,10 @@ def visit_arguments(self, node): first = False else: self.write(", ") - self.visit(a) + self.traverse(a) if d: self.write("=") - self.visit(d) + self.traverse(d) if index == len(node.posonlyargs): self.write(", /") @@ -1146,7 +1155,7 @@ def visit_arguments(self, node): self.write(node.vararg.arg) if node.vararg.annotation: self.write(": ") - self.visit(node.vararg.annotation) + self.traverse(node.vararg.annotation) # keyword-only arguments if node.kwonlyargs: @@ -1155,10 +1164,10 @@ def visit_arguments(self, node): first = False else: self.write(", ") - self.visit(a), + self.traverse(a), if d: self.write("=") - self.visit(d) + self.traverse(d) # kwargs if node.kwarg: @@ -1169,7 +1178,7 @@ def visit_arguments(self, node): self.write("**"+node.kwarg.arg) if node.kwarg.annotation: self.write(": ") - self.visit(node.kwarg.annotation) + self.traverse(node.kwarg.annotation) def visit_keyword(self, node): if node.arg is None: @@ -1177,14 +1186,14 @@ def visit_keyword(self, node): else: self.write(node.arg) self.write("=") - self.visit(node.value) + self.traverse(node.value) def visit_Lambda(self, node): self.write("(") self.write("lambda ") - self.visit(node.args) + self.traverse(node.args) self.write(": ") - self.visit(node.body) + self.traverse(node.body) self.write(")") def visit_alias(self, node): @@ -1193,16 +1202,15 @@ def visit_alias(self, node): self.write(" as "+node.asname) def visit_withitem(self, node): - self.visit(node.context_expr) + self.traverse(node.context_expr) if node.optional_vars: self.write(" as ") - self.visit(node.optional_vars) + self.traverse(node.optional_vars) -def unparse(ast_obj,): - string = io.StringIO() - _Unparser(ast_obj, string) - return string.getvalue() +def unparse(ast_obj): + unparser = _Unparser() + return unparser.visit(ast_obj) def main(): From 68d1a329719fec62dfe2e30f59dbab7cf3c08d2e Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 21 Nov 2019 18:24:48 +0300 Subject: [PATCH 09/15] Add Batuhan Taskaya as a contributor to exposing unparse --- Doc/whatsnew/3.9.rst | 2 +- .../next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 39d5a4d854afd5..96ca6d9ffedd69 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -124,7 +124,7 @@ multiline indented output. Added the :func:`ast.unparse` as a function in the :mod:`ast` module that can be used to unparse an :class:`ast.AST` object and produce a string with code that would produce an equivalent :class:`ast.AST` object when parsed. -(Contributed by Pablo Galindo in :issue:`38870`.) +(Contributed by Pablo Galindo and Batuhan Taskaya in :issue:`38870`.) asyncio ------- diff --git a/Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst b/Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst index d6cc6fc78a5ca2..61af368ba556fe 100644 --- a/Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst +++ b/Misc/NEWS.d/next/Library/2019-11-20-22-43-48.bpo-38870.rLVZEv.rst @@ -1,4 +1,4 @@ Expose :func:`ast.unparse` as a function of the :mod:`ast` module that can be used to unparse an :class:`ast.AST` object and produce a string with code that would produce an equivalent :class:`ast.AST` object when parsed. Patch -by Pablo Galindo. +by Pablo Galindo and Batuhan Taskaya. From ba425c1d041e9ae12c5cd13d6a570f35371ef7d2 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 21 Nov 2019 18:41:45 +0300 Subject: [PATCH 10/15] Modernize test_unparse with using sole pathlib instead of mixing os.path --- Lib/ast.py | 10 ++++----- Lib/test/test_unparse.py | 44 ++++++++++++++++++---------------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index aab705d8f4cacc..ab4dcc5c03870b 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -625,15 +625,15 @@ def visit_Module(self, node): for subnode in node.body: self.traverse(subnode) - def visit_Expr(self, tree): + def visit_Expr(self, node): self.fill() - self.traverse(tree.value) + self.traverse(node.value) - def visit_NamedExpr(self, tree): + def visit_NamedExpr(self, node): self.write("(") - self.traverse(tree.target) + self.traverse(node.target) self.write(" := ") - self.traverse(tree.value) + self.traverse(node.value) self.write(")") def visit_Import(self, node): diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 9f5c0a7c1affac..47c4b0a9694c4f 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -3,7 +3,6 @@ import unittest import test.support import io -import os import pathlib import random import tokenize @@ -277,50 +276,47 @@ def test_invalid_set(self): class DirectoryTestCase(ASTTestCase): """Test roundtrip behaviour on all files in Lib and Lib/test.""" - NAMES = None + ITEMS = None - # test directories, relative to the root of the distribution - test_directories = 'Lib', os.path.join('Lib', 'test') + test_directory = pathlib.Path(__file__).parent / ".." + skip = {'test_fstring.py'} @classmethod def get_names(cls): - if cls.NAMES is not None: - return cls.NAMES + if cls.ITEMS is not None: + return cls.ITEMS - names = [] - for d in cls.test_directories: - basepath = (pathlib.Path(__file__).parent / ".." / "..").resolve() - test_dir = os.path.join(basepath, d) - for n in os.listdir(test_dir): - if n.endswith('.py') and not n.startswith('bad'): - names.append(os.path.join(test_dir, n)) + items = [] + for item in cls.test_directory.glob("**/*.py"): + if not item.name.startswith('bad'): + items.append(item) # Test limited subset of files unless the 'cpu' resource is specified. if not test.support.is_resource_enabled("cpu"): - names = random.sample(names, 10) + items = random.sample(items, 10) # bpo-31174: Store the names sample to always test the same files. # It prevents false alarms when hunting reference leaks. - cls.NAMES = names - return names + cls.ITEMS = items + return items def test_files(self): # get names of files to test - names = self.get_names() + items = self.get_names() - for filename in names: + for item in items: if test.support.verbose: - print('Testing %s' % filename) + print(f'Testing {item.name}') - # Some f-strings are not correctly round-tripped by + # Some f-strings are not correctly round-tripped by # Tools/parser/unparse.py. See issue 28002 for details. # We need to skip files that contain such f-strings. - if os.path.basename(filename) in ('test_fstring.py', ): + if item.name in self.skip: if test.support.verbose: - print(f'Skipping {filename}: see issue 28002') + print(f'Skipping {item.name}: see issue 28002') continue - with self.subTest(filename=filename): - source = read_pyfile(filename) + with self.subTest(filename=item.name): + source = read_pyfile(item) self.check_roundtrip(source) From e158d34908c4f45597f6e5984b53caca6efd5aea Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 22 Nov 2019 11:54:39 +0300 Subject: [PATCH 11/15] Instead of using super, call traverse in all items so in the future we can give preceding information for traverse --- Lib/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ast.py b/Lib/ast.py index ab4dcc5c03870b..fb22c3463b4b96 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -610,7 +610,7 @@ def enter(self): def traverse(self, node): if isinstance(node, list): for item in node: - super().visit(item) + self.traverse(item) else: super().visit(node) From fc0e8acc5853e47052cdd9fa646dff3352d9dcfc Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 22 Nov 2019 12:05:25 +0300 Subject: [PATCH 12/15] test only Lib/ and Lib/test (some tests in lib2to3 is including py2) --- Lib/test/test_unparse.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 47c4b0a9694c4f..12ec4678dd35c5 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -278,7 +278,8 @@ class DirectoryTestCase(ASTTestCase): """Test roundtrip behaviour on all files in Lib and Lib/test.""" ITEMS = None - test_directory = pathlib.Path(__file__).parent / ".." + start_dir = pathlib.Path(__file__).parent / ".." + test_directories = (start_dir, start_dir / "test") skip = {'test_fstring.py'} @classmethod @@ -287,9 +288,10 @@ def get_names(cls): return cls.ITEMS items = [] - for item in cls.test_directory.glob("**/*.py"): - if not item.name.startswith('bad'): - items.append(item) + for directory in cls.test_directories: + for item in directory.glob("*.py"): + if not item.name.startswith('bad'): + items.append(item) # Test limited subset of files unless the 'cpu' resource is specified. if not test.support.is_resource_enabled("cpu"): From 260cd5d63a894f5a093353c06c9b40fcbc0c6434 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 22 Nov 2019 16:37:56 +0300 Subject: [PATCH 13/15] use ast.parse() for consistency in future tests --- Lib/test/test_unparse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 12ec4678dd35c5..aabb1e438cc4c0 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -114,10 +114,10 @@ class ASTTestCase(unittest.TestCase): def assertASTEqual(self, ast1, ast2): self.assertEqual(ast.dump(ast1), ast.dump(ast2)) - def check_roundtrip(self, code1, filename="internal"): - ast1 = compile(code1, filename, "exec", ast.PyCF_ONLY_AST) + def check_roundtrip(self, code1): + ast1 = ast.parse(code1) code2 = ast.unparse(ast1) - ast2 = compile(code2, filename, "exec", ast.PyCF_ONLY_AST) + ast2 = ast.parse(code2) self.assertASTEqual(ast1, ast2) def check_invalid(self, node, raises=ValueError): From 28471e9eaf6703950432a8c6963a4d17ec55fdbc Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Sun, 24 Nov 2019 22:32:11 +0000 Subject: [PATCH 14/15] Add formating, simplify the test file and cosmetic changes --- Lib/ast.py | 89 +++++++++++++++++++++++++++------------- Lib/test/test_unparse.py | 72 ++++++++++++++++---------------- 2 files changed, 97 insertions(+), 64 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index fb22c3463b4b96..1d26ff46e0b2cc 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -557,7 +557,6 @@ def __new__(cls, *args, **kwargs): # We unparse those infinities to INFSTR. _INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) - class _Unparser(NodeVisitor): """Methods in this class recursively traverse an AST and output source code for the abstract syntax; original formatting @@ -580,10 +579,10 @@ def interleave(self, inter, f, seq): inter() f(x) - def fill(self, text = ""): + def fill(self, text=""): """Indent a piece of text and append it, according to the current indentation level""" - self.write("\n"+" "*self._indent + text) + self.write("\n" + " " * self._indent + text) def write(self, text): """Append a piece of text""" @@ -615,8 +614,8 @@ def traverse(self, node): super().visit(node) def visit(self, node): - """Outputs a source code that can be converted again and generate - the same AST with given tree.""" + """Outputs a source code string that if converted back to an ast + (using ast.parse) will generate an AST equivalent to *node*""" self._source = [] self.traverse(node) return "".join(self._source) @@ -658,16 +657,16 @@ def visit_Assign(self, node): def visit_AugAssign(self, node): self.fill() self.traverse(node.target) - self.write(" "+self.binop[node.op.__class__.__name__]+"= ") + self.write(" " + self.binop[node.op.__class__.__name__] + "= ") self.traverse(node.value) def visit_AnnAssign(self, node): self.fill() if not node.simple and isinstance(node.target, Name): - self.write('(') + self.write("(") self.traverse(node.target) if not node.simple and isinstance(node.target, Name): - self.write(')') + self.write(")") self.write(": ") self.traverse(node.annotation) if node.value: @@ -775,7 +774,7 @@ def visit_ClassDef(self, node): for deco in node.decorator_list: self.fill("@") self.traverse(deco) - self.fill("class "+node.name) + self.fill("class " + node.name) self.write("(") comma = False for e in node.bases: @@ -806,7 +805,7 @@ def __FunctionDef_helper(self, node, fill_suffix): for deco in node.decorator_list: self.fill("@") self.traverse(deco) - def_str = fill_suffix+" "+node.name + "(" + def_str = fill_suffix + " " + node.name + "(" self.fill(def_str) self.traverse(node.args) self.write(")") @@ -840,8 +839,7 @@ def visit_If(self, node): with self.enter(): self.traverse(node.body) # collapse nested ifs into equivalent elifs. - while (node.orelse and len(node.orelse) == 1 and - isinstance(node.orelse[0], If)): + while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], If): node = node.orelse[0] self.fill("elif ") self.traverse(node.test) @@ -1005,6 +1003,7 @@ def visit_Set(self, node): def visit_Dict(self, node): self.write("{") + def write_key_value_pair(k, v): self.traverse(k) self.write(": ") @@ -1019,7 +1018,10 @@ def write_item(item): self.traverse(v) else: write_key_value_pair(k, v) - self.interleave(lambda: self.write(", "), write_item, zip(node.keys, node.values)) + + self.interleave( + lambda: self.write(", "), write_item, zip(node.keys, node.values) + ) self.write("}") def visit_Tuple(self, node): @@ -1032,7 +1034,8 @@ def visit_Tuple(self, node): self.interleave(lambda: self.write(", "), self.traverse, node.elts) self.write(")") - unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} + unop = {"Invert": "~", "Not": "not", "UAdd": "+", "USub": "-"} + def visit_UnaryOp(self, node): self.write("(") self.write(self.unop[node.op.__class__.__name__]) @@ -1040,9 +1043,22 @@ def visit_UnaryOp(self, node): self.traverse(node.operand) self.write(")") - binop = { "Add":"+", "Sub":"-", "Mult":"*", "MatMult":"@", "Div":"/", "Mod":"%", - "LShift":"<<", "RShift":">>", "BitOr":"|", "BitXor":"^", "BitAnd":"&", - "FloorDiv":"//", "Pow": "**"} + binop = { + "Add": "+", + "Sub": "-", + "Mult": "*", + "MatMult": "@", + "Div": "/", + "Mod": "%", + "LShift": "<<", + "RShift": ">>", + "BitOr": "|", + "BitXor": "^", + "BitAnd": "&", + "FloorDiv": "//", + "Pow": "**", + } + def visit_BinOp(self, node): self.write("(") self.traverse(node.left) @@ -1050,8 +1066,19 @@ def visit_BinOp(self, node): self.traverse(node.right) self.write(")") - cmpops = {"Eq":"==", "NotEq":"!=", "Lt":"<", "LtE":"<=", "Gt":">", "GtE":">=", - "Is":"is", "IsNot":"is not", "In":"in", "NotIn":"not in"} + cmpops = { + "Eq": "==", + "NotEq": "!=", + "Lt": "<", + "LtE": "<=", + "Gt": ">", + "GtE": ">=", + "Is": "is", + "IsNot": "is not", + "In": "in", + "NotIn": "not in", + } + def visit_Compare(self, node): self.write("(") self.traverse(node.left) @@ -1060,14 +1087,15 @@ def visit_Compare(self, node): self.traverse(e) self.write(")") - boolops = {And: 'and', Or: 'or'} + boolops = {And: "and", Or: "or"} + def visit_BoolOp(self, node): self.write("(") s = " %s " % self.boolops[node.op.__class__] self.interleave(lambda: self.write(s), self.traverse, node.values) self.write(")") - def visit_Attribute(self,node): + def visit_Attribute(self, node): self.traverse(node.value) # Special case: 3.__abs__() is a syntax error, so if node.value # is an integer literal then we need to either parenthesize @@ -1082,12 +1110,16 @@ def visit_Call(self, node): self.write("(") comma = False for e in node.args: - if comma: self.write(", ") - else: comma = True + if comma: + self.write(", ") + else: + comma = True self.traverse(e) for e in node.keywords: - if comma: self.write(", ") - else: comma = True + if comma: + self.write(", ") + else: + comma = True self.traverse(e) self.write(")") @@ -1118,7 +1150,7 @@ def visit_Slice(self, node): self.traverse(node.step) def visit_ExtSlice(self, node): - self.interleave(lambda: self.write(', '), self.traverse, node.dims) + self.interleave(lambda: self.write(", "), self.traverse, node.dims) def visit_arg(self, node): self.write(node.arg) @@ -1175,7 +1207,7 @@ def visit_arguments(self, node): first = False else: self.write(", ") - self.write("**"+node.kwarg.arg) + self.write("**" + node.kwarg.arg) if node.kwarg.annotation: self.write(": ") self.traverse(node.kwarg.annotation) @@ -1199,7 +1231,7 @@ def visit_Lambda(self, node): def visit_alias(self, node): self.write(node.name) if node.asname: - self.write(" as "+node.asname) + self.write(" as " + node.asname) def visit_withitem(self, node): self.traverse(node.context_expr) @@ -1207,7 +1239,6 @@ def visit_withitem(self, node): self.write(" as ") self.traverse(node.optional_vars) - def unparse(ast_obj): unparser = _Unparser() return unparser.visit(ast_obj) diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index aabb1e438cc4c0..9197c8a4a9eaeb 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -7,6 +7,8 @@ import random import tokenize import ast +import functools + def read_pyfile(filename): """Read and return the contents of a Python source file (as a @@ -17,6 +19,7 @@ def read_pyfile(filename): source = pyfile.read() return source + for_else = """\ def f(): for x in range(10): @@ -110,6 +113,7 @@ class Foo: pass suite1 """ + class ASTTestCase(unittest.TestCase): def assertASTEqual(self, ast1, ast2): self.assertEqual(ast.dump(ast1), ast.dump(ast2)) @@ -123,6 +127,7 @@ def check_roundtrip(self, code1): def check_invalid(self, node, raises=ValueError): self.assertRaises(raises, ast.unparse, node) + class UnparseTestCase(ASTTestCase): # Tests for specific bugs found in earlier versions of unparse @@ -166,8 +171,8 @@ def test_huge_float(self): self.check_roundtrip("-1e1000j") def test_min_int(self): - self.check_roundtrip(str(-2**31)) - self.check_roundtrip(str(-2**63)) + self.check_roundtrip(str(-(2 ** 31))) + self.check_roundtrip(str(-(2 ** 63))) def test_imaginary_literals(self): self.check_roundtrip("7j") @@ -264,11 +269,13 @@ def test_invalid_fstring_constant(self): self.check_invalid(ast.JoinedStr(values=[ast.Constant(value=100)])) def test_invalid_fstring_conversion(self): - self.check_invalid(ast.FormattedValue( - value=ast.Constant(value='a', kind=None), - conversion=ord("Y"), # random character - format_spec=None - )) + self.check_invalid( + ast.FormattedValue( + value=ast.Constant(value="a", kind=None), + conversion=ord("Y"), # random character + format_spec=None, + ) + ) def test_invalid_set(self): self.check_invalid(ast.Set(elts=[])) @@ -276,51 +283,46 @@ def test_invalid_set(self): class DirectoryTestCase(ASTTestCase): """Test roundtrip behaviour on all files in Lib and Lib/test.""" - ITEMS = None - start_dir = pathlib.Path(__file__).parent / ".." - test_directories = (start_dir, start_dir / "test") - skip = {'test_fstring.py'} + lib_dir = pathlib.Path(__file__).parent / ".." + test_directories = (lib_dir, lib_dir / "test") + skip_files = {"test_fstring.py"} - @classmethod - def get_names(cls): - if cls.ITEMS is not None: - return cls.ITEMS + @functools.cached_property + def files_to_test(self): + # bpo-31174: Use cached_property to store the names sample + # to always test the same files. It prevents false alarms + # when hunting reference leaks. - items = [] - for directory in cls.test_directories: - for item in directory.glob("*.py"): - if not item.name.startswith('bad'): - items.append(item) + items = [ + item.resolve() + for directory in self.test_directories + for item in directory.glob("*.py") + if not item.name.startswith("bad") + ] # Test limited subset of files unless the 'cpu' resource is specified. if not test.support.is_resource_enabled("cpu"): items = random.sample(items, 10) - # bpo-31174: Store the names sample to always test the same files. - # It prevents false alarms when hunting reference leaks. - cls.ITEMS = items return items def test_files(self): - # get names of files to test - items = self.get_names() - - for item in items: + for item in self.files_to_test: if test.support.verbose: - print(f'Testing {item.name}') + print(f"Testing {item.absolute()}") - # Some f-strings are not correctly round-tripped by - # Tools/parser/unparse.py. See issue 28002 for details. - # We need to skip files that contain such f-strings. - if item.name in self.skip: + # Some f-strings are not correctly round-tripped by + # Tools/parser/unparse.py. See issue 28002 for details. + # We need to skip files that contain such f-strings. + if item.name in self.skip_files: if test.support.verbose: - print(f'Skipping {item.name}: see issue 28002') + print(f"Skipping {item.absolute()}: see issue 28002") continue - with self.subTest(filename=item.name): + with self.subTest(filename=item): source = read_pyfile(item) self.check_roundtrip(source) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 96c9aade03c5f7bb9bfe02a277c603f2a2f73be8 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Sun, 24 Nov 2019 22:35:55 +0000 Subject: [PATCH 15/15] Rewrite some docstrings --- Lib/ast.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 1d26ff46e0b2cc..97914ebc66858b 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -598,9 +598,10 @@ def buffer(self): return value @contextmanager - def enter(self): - """A context manager for printing ':' and increasing the indentation, - after exiting from context it will decrease the indentation.""" + def block(self): + """A context manager for preparing the source for blocks. It adds + the character':', increases the indentation on enter and decreases + the indentation on exit.""" self.write(":") self._indent += 1 yield @@ -614,7 +615,7 @@ def traverse(self, node): super().visit(node) def visit(self, node): - """Outputs a source code string that if converted back to an ast + """Outputs a source code string that, if converted back to an ast (using ast.parse) will generate an AST equivalent to *node*""" self._source = [] self.traverse(node) @@ -745,17 +746,17 @@ def visit_Raise(self, node): def visit_Try(self, node): self.fill("try") - with self.enter(): + with self.block(): self.traverse(node.body) for ex in node.handlers: self.traverse(ex) if node.orelse: self.fill("else") - with self.enter(): + with self.block(): self.traverse(node.orelse) if node.finalbody: self.fill("finally") - with self.enter(): + with self.block(): self.traverse(node.finalbody) def visit_ExceptHandler(self, node): @@ -766,7 +767,7 @@ def visit_ExceptHandler(self, node): if node.name: self.write(" as ") self.write(node.name) - with self.enter(): + with self.block(): self.traverse(node.body) def visit_ClassDef(self, node): @@ -791,7 +792,7 @@ def visit_ClassDef(self, node): self.traverse(e) self.write(")") - with self.enter(): + with self.block(): self.traverse(node.body) def visit_FunctionDef(self, node): @@ -812,7 +813,7 @@ def __FunctionDef_helper(self, node, fill_suffix): if node.returns: self.write(" -> ") self.traverse(node.returns) - with self.enter(): + with self.block(): self.traverse(node.body) def visit_For(self, node): @@ -826,51 +827,51 @@ def __For_helper(self, fill, node): self.traverse(node.target) self.write(" in ") self.traverse(node.iter) - with self.enter(): + with self.block(): self.traverse(node.body) if node.orelse: self.fill("else") - with self.enter(): + with self.block(): self.traverse(node.orelse) def visit_If(self, node): self.fill("if ") self.traverse(node.test) - with self.enter(): + with self.block(): self.traverse(node.body) # collapse nested ifs into equivalent elifs. while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], If): node = node.orelse[0] self.fill("elif ") self.traverse(node.test) - with self.enter(): + with self.block(): self.traverse(node.body) # final else if node.orelse: self.fill("else") - with self.enter(): + with self.block(): self.traverse(node.orelse) def visit_While(self, node): self.fill("while ") self.traverse(node.test) - with self.enter(): + with self.block(): self.traverse(node.body) if node.orelse: self.fill("else") - with self.enter(): + with self.block(): self.traverse(node.orelse) def visit_With(self, node): self.fill("with ") self.interleave(lambda: self.write(", "), self.traverse, node.items) - with self.enter(): + with self.block(): self.traverse(node.body) def visit_AsyncWith(self, node): self.fill("async with ") self.interleave(lambda: self.write(", "), self.traverse, node.items) - with self.enter(): + with self.block(): self.traverse(node.body) def visit_JoinedStr(self, node):