From 2a435e9fb429a27e9125eb6fe07eea8b663d2d9b Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Sat, 23 Feb 2008 18:47:35 +0000 Subject: [PATCH] Add coverage builder for Sphinx, written for GHOP by Josip Dzolonga. --- sphinx/addons/coverage.py | 246 ++++++++++++++++++++++++++++++++++++++ sphinx/config.py | 3 + sphinx/util/__init__.py | 6 + 3 files changed, 255 insertions(+) create mode 100644 sphinx/addons/coverage.py diff --git a/sphinx/addons/coverage.py b/sphinx/addons/coverage.py new file mode 100644 index 000000000..08963e80e --- /dev/null +++ b/sphinx/addons/coverage.py @@ -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) + diff --git a/sphinx/config.py b/sphinx/config.py index 442aab0a7..40d48019e 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -87,3 +87,6 @@ class Config(object): def __getitem__(self, name): return getattr(self, name) + + def __contains__(self, name): + return hasattr(self, name) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index f96cf488f..3c63c7bdd 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -12,6 +12,7 @@ import os import sys import fnmatch +import traceback from os import path @@ -106,3 +107,8 @@ class attrdict(dict): 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()