mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Add coverage builder for Sphinx, written for GHOP by Josip Dzolonga.
This commit is contained in:
parent
7d9721dd85
commit
2a435e9fb4
246
sphinx/addons/coverage.py
Normal file
246
sphinx/addons/coverage.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# -*- 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)
|
||||||
|
|
@ -87,3 +87,6 @@ class Config(object):
|
|||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
return getattr(self, name)
|
return getattr(self, name)
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return hasattr(self, name)
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
import traceback
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
|
|
||||||
@ -106,3 +107,8 @@ class attrdict(dict):
|
|||||||
self[key] = val
|
self[key] = val
|
||||||
def __delattr__(self, key):
|
def __delattr__(self, key):
|
||||||
del 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()
|
||||||
|
Loading…
Reference in New Issue
Block a user