1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
|
# 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(
"-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])
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(f"{err}")
return
if options.dis:
print("Main code:")
disassemble(pyparser)
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 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(pyparser):
"""Disassemble code, for ad-hoc experimenting."""
code = compile(pyparser.text, "", "exec")
for code_obj in all_code_objects(code):
if pyparser.text:
srclines = pyparser.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:])
|