Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/bin/sage-coverage
8815 views
#!/usr/bin/env python

from __future__ import print_function

import os
import sys
import token
from tokenize import *


if len(sys.argv) == 1:
    print("Usage: sage --coverage <files>")
    print("Give info about doctest coverage of files.")
    sys.exit(2)


# 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


first = True

def go(filename):
    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

    global first
    if first:
        print('-'*72)
        first = False
    CoverageResults(filename).check_file(open(filename)).report()
    print('-'*72)

for arg in sys.argv[1:]:
    go(arg)