blob: 0393d209fa05cb000c13261dc7165a3c9f7a375a [file] [log] [blame]
# 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:])