blob: 836cf51247218922f324458689af91aa0e9e8b82 [file] [log] [blame]
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""Parser.py: a main for invoking code in coverage/parser.py"""
import collections
import dis
import glob
import optparse
import os
import re
import sys
import textwrap
import types
from coverage.parser import PythonParser
from coverage.python import get_python_source
class ParserMain:
"""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(
"-R", action="store_true", dest="recursive", help="Recurse to find source files"
)
parser.add_option("-q", action="store_true", dest="quiet", help="Suppress output")
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"):
if not options.quiet:
print(f"Parsing {f}")
self.one_file(options, f)
elif not args:
parser.print_help()
else:
self.one_file(options, args[0])
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.
if match := re.search(r"^(.*):(\d+)-(\d+)$", filename):
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(f"{err}")
return
if options.dis:
print("Main code:")
disassemble(pyparser.text)
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.text.splitlines(), start=1):
marks = [" "] * 6
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_excluded:
marks[4] = "X"
elif lineno in pyparser.excluded:
marks[4] = "×"
if lineno in pyparser.multiline_map.values():
marks[5] = "o"
elif lineno in pyparser.multiline_map.keys():
marks[5] = "."
if arc_chars:
a = arc_chars[lineno].ljust(arc_width)
else:
a = ""
if not options.quiet:
print("%4d %s%s %s" % (lineno, "".join(marks), a, ltext))
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 all_code_objects(code):
"""Iterate over all the code objects in `code`."""
stack = [code]
while stack:
# We're going to return the code object on the stack, but first
# push its children for later returning.
code = stack.pop()
stack.extend(c for c in code.co_consts if isinstance(c, types.CodeType))
yield code
def disassemble(text):
"""Disassemble code, for ad-hoc experimenting."""
code = compile(text, "", "exec", dont_inherit=True)
for code_obj in all_code_objects(code):
if text:
srclines = text.splitlines()
else:
srclines = None
print("\n%s: " % code_obj)
upto = None
for inst in dis.get_instructions(code_obj):
if inst.starts_line is not None:
if srclines:
upto = upto or inst.starts_line - 1
while upto <= inst.starts_line - 1:
print("{:>100}{}".format("", srclines[upto]))
upto += 1
elif inst.offset > 0:
print("")
line = inst._disassemble()
print(f"{line:<70}")
print("")
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 {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:])