/usr/share/sagemath/bin/sage-coverage is in sagemath-common 8.1-7ubuntu1.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
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 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | #!/usr/bin/env python
from __future__ import print_function
import os
import sys
import token
from tokenize import *
import argparse
parser = argparse.ArgumentParser(description='Look into Sage files for wrong doctests.')
parser.add_argument('filename', type=str, nargs='+', help='filename or a directory')
parser.add_argument('--only-bad', action='store_true', help='only print info for bad formatted files')
parser.add_argument('--summary', action='store_true', help='only print a short summary')
args = parser.parse_args()
# Collect coverage results for one file
class CoverageResults:
def __init__(self, filename=""):
"""
INPUT:
- ``filename`` -- name of the file, only for display purposes.
"""
self.no_doc = []
self.no_test = []
self.good = []
self.possibly_wrong = []
self.filename = filename
def report(self):
"""
Print coverage results.
"""
num_functions = len(self.good) + len(self.no_doc) + len(self.no_test)
if not num_functions:
print("No functions in", self.filename)
return
score = (100.0 * len(self.good)) / float(num_functions)
print("SCORE {}: {:.1f}% ({} of {})".format(self.filename, score, len(self.good), num_functions))
if self.no_doc:
print("\nMissing documentation:")
for f in self.no_doc:
print(" *", f)
if self.no_test:
print("\nMissing doctests:")
for f in self.no_test:
print(" *", f)
if self.possibly_wrong:
print("\nPossibly wrong (function name doesn't occur in doctests):")
for f in self.possibly_wrong:
print(" *", f)
def handle_function(self, name, fullname, docstring):
"""
Check coverage of one function and store result.
INPUT:
- ``name`` -- bare function name (e.g. "foo")
- ``fullname`` -- complete function definition (e.g. "def foo(arg=None)")
- ``docstring`` -- the docstring, or ``None`` if there is no docstring
"""
# Skip certain names
if name in ['__dealloc__', '__new__', '_']:
return
if not docstring:
self.no_doc.append(fullname)
return
if not "sage: " in docstring:
self.no_test.append(fullname)
return
# If the name is of the form _xxx_, then the doctest is always
# considered indirect.
if name[0] == "_" and name[-1] == "_":
is_indirect = True
else:
is_indirect = "indirect doctest" in docstring
if not is_indirect and not name in docstring:
self.possibly_wrong.append(fullname)
self.good.append(fullname)
def check_file(self, f):
"""
Check the coverage of one file.
INPUT:
- ``f``: an open file
OUTPUT: ``self``
"""
# Where are we in a function definition?
BEGINOFLINE = 0 # Beginning of new logical line
UNKNOWN = -99 # Not at all in a function definition
DEFNAMES = 1 # In function definition before first open paren
DEFARGS = 2 # In function arguments or between closing paren and final colon
DOCSTRING = -1 # Looking for docstring
state = BEGINOFLINE
# Previous token type seen
prevtyp = NEWLINE
# Indentation level
indent = 0
# Indentation level of last "def" statement
# or None if no such statement.
defindent = None
for (typ, tok, start, end, logical_line) in generate_tokens(f.readline):
# Completely ignore comments or continuation newlines
if typ == COMMENT or typ == NL:
continue
# Handle indentation
if typ == INDENT:
indent += 1
continue
elif typ == DEDENT:
indent -= 1
if (defindent is not None and indent <= defindent):
defindent = None
continue
# Check for "def" or "cpdef" ("cdef" functions don't need to be documented).
# Skip nested functions (with indent > defindent).
if state == BEGINOFLINE:
if typ == NAME and (tok in ["def", "cpdef"]) and (defindent is None or indent <= defindent):
state = DEFNAMES
deffullname = "line %s: "%start[0]
defparen = 0 # Number of open parentheses
else:
state = UNKNOWN
if state == DOCSTRING:
if typ != NEWLINE:
docstring = None
if typ == STRING:
docstring = tok
self.handle_function(defname, deffullname, docstring)
state = UNKNOWN
if state == DEFNAMES:
if typ == NAME:
if tok == "class": # Make sure that cdef classes are ignored
state = UNKNOWN
# Last NAME token before opening parenthesis is
# the function name.
defname = tok
elif tok == '(':
state = DEFARGS
else:
state = UNKNOWN
if state == DEFARGS:
if tok == '(':
defparen += 1
elif tok == ')':
defparen -= 1
elif defparen == 0 and tok == ':':
state = DOCSTRING
defindent = indent
elif typ == NEWLINE:
state = UNKNOWN
if state > 0:
# Append tok string to deffullname
if prevtyp == NAME and typ == NAME:
deffullname += ' '
elif prevtyp == OP and deffullname[-1] in ",":
deffullname += ' '
deffullname += tok
# New line?
if state == UNKNOWN and typ == NEWLINE:
state = BEGINOFLINE
prevtyp = typ
return self
# Data reported by --summary
good = 0
no_doc = 0
no_test = 0
possibly_wrong = 0
bad_files = []
first = True
def go(filename):
r"""
If ``filename`` is a file, launch the inspector on this file. If
``filename`` is a directory then recursively launch this function on the
files it contains.
"""
if os.path.isdir(filename):
for F in sorted(os.listdir(filename)):
go(os.path.join(filename, F))
if not os.path.exists(filename):
print("File %s does not exist."%filename, file=sys.stderr)
sys.exit(1)
if not (filename.endswith('.py')
or filename.endswith('.pyx')
or filename.endswith('.sage')):
return
with open(filename, 'r') as f:
cr = CoverageResults(filename).check_file(f)
bad = cr.no_doc or cr.no_test or cr.possibly_wrong
# Update the global variables
if args.summary:
global good, no_doc, no_test, possibly_wrong, bad_files
no_doc += len(cr.no_doc)
no_test += len(cr.no_test)
possibly_wrong += len(cr.possibly_wrong)
good += len(cr.good)
if bad: bad_files.append(filename)
return
if not bad and args.only_bad:
return
global first
if first:
print('-'*72)
first = False
cr.report() # Print the report
print('-'*72)
for arg in args.filename:
go(arg)
if args.summary:
num_functions = good + no_doc + no_test
score = (100.0 * good) / float(num_functions)
print("Global score: {:.1f}% ({} of {})\n".format(score, good, num_functions))
print("{} files with wrong documentation".format(len(bad_files)))
print("{} functions with no doc".format(no_doc))
print("{} functions with no test".format(no_test))
print("{} doctest are potentially wrong".format(possibly_wrong))
print("\nFiles with wrong documentation:")
print("-------------------------------")
print("\n".join(" {}".format(filename) for filename in bad_files))
|