Anonymous avatar Anonymous committed 2f31956

Add coverage builder for Sphinx, written for GHOP by Josip Dzolonga.

Comments (0)

Files changed (3)

sphinx/addons/coverage.py

+# -*- coding: utf-8 -*-
+"""
+    sphinx.addons.coverage
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Check Python modules and C API for coverage.  Mostly written by Josip
+    Dzolonga for the Google Highly Open Participation contest.
+
+    :copyright: 2008 by Josip Dzolonga, Georg Brandl.
+    :license: BSD.
+"""
+
+import os
+import re
+import glob
+import inspect
+import cPickle as pickle
+from os import path
+
+from sphinx.builder import Builder
+
+
+# utility
+def write_header(f, text, char='-'):
+    f.write(text + '\n')
+    f.write(char * len(text) + '\n')
+
+def compile_regex_list(name, exps, warnfunc):
+    lst = []
+    for exp in exps:
+        try:
+            lst.append(re.compile(exp))
+        except Exception:
+            warnfunc('invalid regex %r in %s' % (exp, name))
+    return lst
+
+
+class CoverageBuilder(Builder):
+    """
+    Checks the completeness of Python's C-API documentation.
+    """
+
+    name = 'coverage'
+
+    def init(self):
+        self.c_sourcefiles = []
+        for pattern in self.config.coverage_c_path:
+            pattern = path.join(self.srcdir, pattern)
+            self.c_sourcefiles.extend(glob.glob(pattern))
+
+        self.c_regexes = []
+        for (name, exp) in self.config.coverage_c_regexes.items():
+            try:
+                self.c_regexes.append((name, re.compile(exp)))
+            except Exception:
+                warnfunc('invalid regex %r in coverage_c_regexes' % exp)
+
+        self.c_ignorexps = {}
+        for (name, exps) in self.config.coverage_ignore_c_items.iteritems():
+            self.c_ignorexps[name] = compile_regex_list('coverage_ignore_c_items',
+                                                        exps, self.warn)
+        self.mod_ignorexps = compile_regex_list('coverage_ignore_modules',
+                                                self.config.coverage_ignore_modules,
+                                                self.warn)
+        self.cls_ignorexps = compile_regex_list('coverage_ignore_classes',
+                                                self.config.coverage_ignore_classes,
+                                                self.warn)
+        self.fun_ignorexps = compile_regex_list('coverage_ignore_functions',
+                                                self.config.coverage_ignore_functions,
+                                                self.warn)
+
+    def get_outdated_docs(self):
+        return 'coverage overview'
+
+    def write(self, *ignored):
+        self.py_undoc = {}
+        self.build_py_coverage()
+        self.write_py_coverage()
+
+        if self.c_sourcefiles:
+            self.c_undoc = {}
+            self.build_c_coverage()
+            self.write_c_coverage()
+
+    def build_c_coverage(self):
+        # Fetch all the info from the header files
+        for filename in self.c_sourcefiles:
+            undoc = []
+            f = open(filename, 'r')
+            try:
+                for line in f:
+                    for key, regex in self.c_regexes:
+                        match = regex.match(line)
+                        if match:
+                            name = match.groups()[0]
+                            if name not in self.env.descrefs:
+                                for exp in self.c_ignorexps.get(key, ()):
+                                    if exp.match(name):
+                                        break
+                                else:
+                                    undoc.append((key, name))
+                            continue
+            finally:
+                f.close()
+            if undoc:
+                self.c_undoc[filename] = undoc
+
+    def write_c_coverage(self):
+        output_file = path.join(self.outdir, 'c.txt')
+        op = open(output_file, 'w')
+        try:
+            write_header(op, 'Undocumented C API elements', '=')
+            op.write('\n')
+
+            for filename, undoc in self.c_undoc.iteritems():
+                write_header(op, filename)
+                for typ, name in undoc:
+                    op.write(' * %-50s [%9s]\n' % (name, typ))
+                op.write('\n')
+        finally:
+            op.close()
+
+    def build_py_coverage(self):
+        for mod_name in self.env.modules:
+            ignore = False
+            for exp in self.mod_ignorexps:
+                if exp.match(mod_name):
+                    ignore = True
+                    break
+            if ignore:
+                continue
+
+            try:
+                mod = __import__(mod_name, fromlist=['foo'])
+            except ImportError, err:
+                self.warn('module %s could not be imported: %s' % (mod_name, err))
+                self.py_undoc[mod_name] = {'error': err}
+                continue
+
+            funcs = []
+            classes = {}
+
+            for name, obj in inspect.getmembers(mod):
+                # diverse module attributes are ignored:
+                if name[0] == '_':
+                    # begins in an underscore
+                    continue
+                if not hasattr(obj, '__module__'):
+                    # cannot be attributed to a module
+                    continue
+                if obj.__module__ != mod_name:
+                    # is not defined in this module
+                    continue
+
+                full_name = '%s.%s' % (mod_name, name)
+
+                if inspect.isfunction(obj):
+                    if full_name not in self.env.descrefs:
+                        for exp in self.fun_ignorexps:
+                            if exp.match(name):
+                                break
+                        else:
+                            funcs.append(name)
+                elif inspect.isclass(obj):
+                    for exp in self.cls_ignorexps:
+                        if exp.match(name):
+                            break
+                    else:
+                        if full_name not in self.env.descrefs:
+                            # not documented at all
+                            classes[name] = []
+                            continue
+
+                        attrs = []
+
+                        for attr_name, attr in inspect.getmembers(obj, inspect.ismethod):
+                            if attr_name[0] == '_':
+                                # starts with an underscore, ignore it
+                                continue
+
+                            full_attr_name = '%s.%s' % (full_name, attr_name)
+                            if full_attr_name not in self.env.descrefs:
+                                attrs.append(attr_name)
+
+                        if attrs:
+                            # some attributes are undocumented
+                            classes[name] = attrs
+
+            self.py_undoc[mod_name] = {'funcs': funcs, 'classes': classes}
+
+    def write_py_coverage(self):
+        output_file = path.join(self.outdir, 'python.txt')
+        op = open(output_file, 'w')
+        failed = []
+        try:
+            write_header(op, 'Undocumented Python objects', '=')
+
+            keys = self.py_undoc.keys()
+            keys.sort()
+            for name in keys:
+                undoc = self.py_undoc[name]
+                if 'error' in undoc:
+                    failed.append((name, undoc['error']))
+                else:
+                    if not undoc['classes'] and not undoc['funcs']:
+                        continue
+
+                    write_header(op, name)
+                    if undoc['funcs']:
+                        op.write('Functions:\n')
+                        op.writelines(' * %s\n' % x for x in undoc['funcs'])
+                        op.write('\n')
+                    if undoc['classes']:
+                        op.write('Classes:\n')
+                        for name, methods in undoc['classes'].iteritems():
+                            if not methods:
+                                op.write(' * %s\n' % name)
+                            else:
+                                op.write(' * %s -- missing methods:\n' % name)
+                                op.writelines('   - %s\n' % x for x in methods)
+                        op.write('\n')
+
+            write_header(op, 'Modules that failed to import')
+            op.writelines(' * %s -- %s\n' % x for x in failed)
+        finally:
+            op.close()
+
+    def finish(self):
+        # dump the coverage data to a pickle file too
+        picklepath = path.join(self.outdir, 'undoc.pickle')
+        dumpfile = open(picklepath, 'wb')
+        try:
+            pickle.dump((self.py_undoc, self.c_undoc), dumpfile)
+        finally:
+            dumpfile.close()
+
+
+def setup(app):
+    app.add_builder(CoverageBuilder)
+    app.add_config_value('coverage_c_path', [], False)
+    app.add_config_value('coverage_c_regexes', [], False)
+    app.add_config_value('coverage_ignore_modules', [], False)
+    app.add_config_value('coverage_ignore_functions', [], False)
+    app.add_config_value('coverage_ignore_classes', [], False)
+    app.add_config_value('coverage_ignore_c_items', [], False)
+
 
     def __getitem__(self, name):
         return getattr(self, name)
+
+    def __contains__(self, name):
+        return hasattr(self, name)

sphinx/util/__init__.py

 import os
 import sys
 import fnmatch
+import traceback
 from os import path
 
 
         self[key] = val
     def __delattr__(self, key):
         del self[key]
+
+
+def fmt_ex(ex):
+    """Format a single line with an exception description."""
+    return traceback.format_exception_only(ex.__class__, ex)[-1].strip()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.