# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt """Parser.py: a main for invoking code in coverage/parser.py""" from __future__ import division import collections import glob import optparse import os import re import sys import textwrap import disgen from coverage.parser import PythonParser from coverage.python import get_python_source opcode_counts = collections.Counter() class ParserMain(object): """A main for code parsing experiments.""" def main(self, args): """A main function for trying the code from the command line.""" parser = optparse.OptionParser() parser.add_option( "-d", action="store_true", dest="dis", help="Disassemble" ) parser.add_option( "-H", action="store_true", dest="histogram", help="Count occurrences of opcodes" ) parser.add_option( "-R", action="store_true", dest="recursive", help="Recurse to find source files" ) parser.add_option( "-s", action="store_true", dest="source", help="Show analyzed source" ) parser.add_option( "-t", action="store_true", dest="tokens", help="Show tokens" ) options, args = parser.parse_args() if options.recursive: if args: root = args[0] else: root = "." for root, _, _ in os.walk(root): for f in glob.glob(root + "/*.py"): self.one_file(options, f) elif not args: parser.print_help() else: self.one_file(options, args[0]) if options.histogram: total = sum(opcode_counts.values()) print("{0} total opcodes".format(total)) for opcode, number in opcode_counts.most_common(): print("{0:20s} {1:6d} {2:.1%}".format(opcode, number, number/total)) def one_file(self, options, filename): """Process just one file.""" # `filename` can have a line number suffix. In that case, extract those # lines, dedent them, and use that. This is for trying test cases # embedded in the test files. match = re.search(r"^(.*):(\d+)-(\d+)$", filename) if match: filename, start, end = match.groups() start, end = int(start), int(end) else: start = end = None try: text = get_python_source(filename) if start is not None: lines = text.splitlines(True) text = textwrap.dedent("".join(lines[start-1:end]).replace("\\\\", "\\")) pyparser = PythonParser(text, filename=filename, exclude=r"no\s*cover") pyparser.parse_source() except Exception as err: print("%s" % (err,)) return if options.dis: print("Main code:") self.disassemble(pyparser.byte_parser, histogram=options.histogram) arcs = pyparser.arcs() if options.source or options.tokens: pyparser.show_tokens = options.tokens pyparser.parse_source() if options.source: arc_chars = self.arc_ascii_art(arcs) if arc_chars: arc_width = max(len(a) for a in arc_chars.values()) exit_counts = pyparser.exit_counts() for lineno, ltext in enumerate(pyparser.lines, start=1): marks = [' ', ' ', ' ', ' ', ' '] a = ' ' if lineno in pyparser.raw_statements: marks[0] = '-' if lineno in pyparser.statements: marks[1] = '=' exits = exit_counts.get(lineno, 0) if exits > 1: marks[2] = str(exits) if lineno in pyparser.raw_docstrings: marks[3] = '"' if lineno in pyparser.raw_classdefs: marks[3] = 'C' if lineno in pyparser.raw_excluded: marks[4] = 'x' if arc_chars: a = arc_chars[lineno].ljust(arc_width) else: a = "" print("%4d %s%s %s" % (lineno, "".join(marks), a, ltext)) def disassemble(self, byte_parser, histogram=False): """Disassemble code, for ad-hoc experimenting.""" for bp in byte_parser.child_parsers(): if bp.text: srclines = bp.text.splitlines() else: srclines = None print("\n%s: " % bp.code) upto = None for disline in disgen.disgen(bp.code): if histogram: opcode_counts[disline.opcode] += 1 continue if disline.first: if srclines: upto = upto or disline.lineno-1 while upto <= disline.lineno-1: print("%100s%s" % ("", srclines[upto])) upto += 1 elif disline.offset > 0: print("") line = disgen.format_dis_line(disline) print("%-70s" % (line,)) print("") def arc_ascii_art(self, arcs): """Draw arcs as ascii art. Returns a dictionary mapping line numbers to ascii strings to draw for that line. """ plus_ones = set() arc_chars = collections.defaultdict(str) for lfrom, lto in sorted(arcs): if lfrom < 0: arc_chars[lto] += 'v' elif lto < 0: arc_chars[lfrom] += '^' else: if lfrom == lto - 1: plus_ones.add(lfrom) arc_chars[lfrom] += "" # ensure this line is in arc_chars continue if lfrom < lto: l1, l2 = lfrom, lto else: l1, l2 = lto, lfrom w = first_all_blanks(arc_chars[l] for l in range(l1, l2+1)) for l in range(l1, l2+1): if l == lfrom: ch = '<' elif l == lto: ch = '>' else: ch = '|' arc_chars[l] = set_char(arc_chars[l], w, ch) # Add the plusses as the first character for lineno, arcs in arc_chars.items(): arc_chars[lineno] = ( ("+" if lineno in plus_ones else " ") + arcs ) return arc_chars def set_char(s, n, c): """Set the nth char of s to be c, extending s if needed.""" s = s.ljust(n) return s[:n] + c + s[n+1:] def blanks(s): """Return the set of positions where s is blank.""" return set(i for i, c in enumerate(s) if c == " ") def first_all_blanks(ss): """Find the first position that is all blank in the strings ss.""" ss = list(ss) blankss = blanks(ss[0]) for s in ss[1:]: blankss &= blanks(s) if blankss: return min(blankss) else: return max(len(s) for s in ss) if __name__ == '__main__': ParserMain().main(sys.argv[1:])