# -*- coding: utf-8 -*- """ Part of the astor library for Python AST manipulation. License: 3-clause BSD Copyright (c) 2015 Patrick Maupin Pretty-print source -- post-process for the decompiler The goals of the initial cut of this engine are: 1) Do a passable, if not PEP8, job of line-wrapping. 2) Serve as an example of an interface to the decompiler for anybody who wants to do a better job. :) """ def pretty_source(source): """ Prettify the source. """ return ''.join(split_lines(source)) def split_lines(source, maxline=79): """Split inputs according to lines. If a line is short enough, just yield it. Otherwise, fix it. """ result = [] extend = result.extend append = result.append line = [] multiline = False count = 0 find = str.find for item in source: index = find(item, '\n') if index: line.append(item) multiline = index > 0 count += len(item) else: if line: if count <= maxline or multiline: extend(line) else: wrap_line(line, maxline, result) count = 0 multiline = False line = [] append(item) return result def count(group, slen=str.__len__): return sum([slen(x) for x in group]) def wrap_line(line, maxline=79, result=[], count=count): """ We have a line that is too long, so we're going to try to wrap it. """ # Extract the indentation append = result.append extend = result.extend indentation = line[0] lenfirst = len(indentation) indent = lenfirst - len(indentation.strip()) assert indent in (0, lenfirst) indentation = line.pop(0) if indent else '' # Get splittable/non-splittable groups dgroups = list(delimiter_groups(line)) unsplittable = dgroups[::2] splittable = dgroups[1::2] # If the largest non-splittable group won't fit # on a line, try to add parentheses to the line. if max(count(x) for x in unsplittable) > maxline - indent: line = add_parens(line, maxline, indent) dgroups = list(delimiter_groups(line)) unsplittable = dgroups[::2] splittable = dgroups[1::2] # Deal with the first (always unsplittable) group, and # then set up to deal with the remainder in pairs. first = unsplittable[0] append(indentation) extend(first) if not splittable: return result pos = indent + count(first) indentation += ' ' indent += 4 if indent >= maxline/2: maxline = maxline/2 + indent for sg, nsg in zip(splittable, unsplittable[1:]): if sg: # If we already have stuff on the line and even # the very first item won't fit, start a new line if pos > indent and pos + len(sg[0]) > maxline: append('\n') append(indentation) pos = indent # Dump lines out of the splittable group # until the entire thing fits csg = count(sg) while pos + csg > maxline: ready, sg = split_group(sg, pos, maxline) if ready[-1].endswith(' '): ready[-1] = ready[-1][:-1] extend(ready) append('\n') append(indentation) pos = indent csg = count(sg) # Dump the remainder of the splittable group if sg: extend(sg) pos += csg # Dump the unsplittable group, optionally # preceded by a linefeed. cnsg = count(nsg) if pos > indent and pos + cnsg > maxline: append('\n') append(indentation) pos = indent extend(nsg) pos += cnsg def split_group(source, pos, maxline): """ Split a group into two subgroups. The first will be appended to the current line, the second will start the new line. Note that the first group must always contain at least one item. The original group may be destroyed. """ first = [] source.reverse() while source: tok = source.pop() first.append(tok) pos += len(tok) if source: tok = source[-1] allowed = (maxline + 1) if tok.endswith(' ') else (maxline - 4) if pos + len(tok) > allowed: break source.reverse() return first, source begin_delim = set('([{') end_delim = set(')]}') end_delim.add('):') def delimiter_groups(line, begin_delim=begin_delim, end_delim=end_delim): """Split a line into alternating groups. The first group cannot have a line feed inserted, the next one can, etc. """ text = [] line = iter(line) while True: # First build and yield an unsplittable group for item in line: text.append(item) if item in begin_delim: break if not text: break yield text # Now build and yield a splittable group level = 0 text = [] for item in line: if item in begin_delim: level += 1 elif item in end_delim: level -= 1 if level < 0: yield text text = [item] break text.append(item) else: assert not text, text break statements = set(['del ', 'return', 'yield ', 'if ', 'while ']) def add_parens(line, maxline, indent, statements=statements, count=count): """Attempt to add parentheses around the line in order to make it splittable. """ if line[0] in statements: index = 1 if not line[0].endswith(' '): index = 2 assert line[1] == ' ' line.insert(index, '(') if line[-1] == ':': line.insert(-1, ')') else: line.append(')') # That was the easy stuff. Now for assignments. groups = list(get_assign_groups(line)) if len(groups) == 1: # So sad, too bad return line counts = list(count(x) for x in groups) didwrap = False # If the LHS is large, wrap it first if sum(counts[:-1]) >= maxline - indent - 4: for group in groups[:-1]: didwrap = False # Only want to know about last group if len(group) > 1: group.insert(0, '(') group.insert(-1, ')') didwrap = True # Might not need to wrap the RHS if wrapped the LHS if not didwrap or counts[-1] > maxline - indent - 10: groups[-1].insert(0, '(') groups[-1].append(')') return [item for group in groups for item in group] # Assignment operators ops = list('|^&+-*/%@~') + '<< >> // **'.split() + [''] ops = set(' %s= ' % x for x in ops) def get_assign_groups(line, ops=ops): """ Split a line into groups by assignment (including augmented assignment) """ group = [] for item in line: group.append(item) if item in ops: yield group group = [] yield group