|
1 # -*- coding: utf-8 -*- |
|
2 """ |
|
3 sphinx.ext.coverage |
|
4 ~~~~~~~~~~~~~~~~~~~ |
|
5 |
|
6 Check Python modules and C API for coverage. Mostly written by Josip |
|
7 Dzolonga for the Google Highly Open Participation contest. |
|
8 |
|
9 :copyright: 2008 by Josip Dzolonga, Georg Brandl. |
|
10 :license: BSD. |
|
11 """ |
|
12 |
|
13 import re |
|
14 import glob |
|
15 import inspect |
|
16 import cPickle as pickle |
|
17 from os import path |
|
18 |
|
19 from sphinx.builder import Builder |
|
20 |
|
21 |
|
22 # utility |
|
23 def write_header(f, text, char='-'): |
|
24 f.write(text + '\n') |
|
25 f.write(char * len(text) + '\n') |
|
26 |
|
27 def compile_regex_list(name, exps, warnfunc): |
|
28 lst = [] |
|
29 for exp in exps: |
|
30 try: |
|
31 lst.append(re.compile(exp)) |
|
32 except Exception: |
|
33 warnfunc('invalid regex %r in %s' % (exp, name)) |
|
34 return lst |
|
35 |
|
36 |
|
37 class CoverageBuilder(Builder): |
|
38 |
|
39 name = 'coverage' |
|
40 |
|
41 def init(self): |
|
42 self.c_sourcefiles = [] |
|
43 for pattern in self.config.coverage_c_path: |
|
44 pattern = path.join(self.srcdir, pattern) |
|
45 self.c_sourcefiles.extend(glob.glob(pattern)) |
|
46 |
|
47 self.c_regexes = [] |
|
48 for (name, exp) in self.config.coverage_c_regexes.items(): |
|
49 try: |
|
50 self.c_regexes.append((name, re.compile(exp))) |
|
51 except Exception: |
|
52 self.warn('invalid regex %r in coverage_c_regexes' % exp) |
|
53 |
|
54 self.c_ignorexps = {} |
|
55 for (name, exps) in self.config.coverage_ignore_c_items.iteritems(): |
|
56 self.c_ignorexps[name] = compile_regex_list('coverage_ignore_c_items', |
|
57 exps, self.warn) |
|
58 self.mod_ignorexps = compile_regex_list('coverage_ignore_modules', |
|
59 self.config.coverage_ignore_modules, |
|
60 self.warn) |
|
61 self.cls_ignorexps = compile_regex_list('coverage_ignore_classes', |
|
62 self.config.coverage_ignore_classes, |
|
63 self.warn) |
|
64 self.fun_ignorexps = compile_regex_list('coverage_ignore_functions', |
|
65 self.config.coverage_ignore_functions, |
|
66 self.warn) |
|
67 |
|
68 def get_outdated_docs(self): |
|
69 return 'coverage overview' |
|
70 |
|
71 def write(self, *ignored): |
|
72 self.py_undoc = {} |
|
73 self.build_py_coverage() |
|
74 self.write_py_coverage() |
|
75 |
|
76 self.c_undoc = {} |
|
77 self.build_c_coverage() |
|
78 self.write_c_coverage() |
|
79 |
|
80 def build_c_coverage(self): |
|
81 # Fetch all the info from the header files |
|
82 for filename in self.c_sourcefiles: |
|
83 undoc = [] |
|
84 f = open(filename, 'r') |
|
85 try: |
|
86 for line in f: |
|
87 for key, regex in self.c_regexes: |
|
88 match = regex.match(line) |
|
89 if match: |
|
90 name = match.groups()[0] |
|
91 if name not in self.env.descrefs: |
|
92 for exp in self.c_ignorexps.get(key, ()): |
|
93 if exp.match(name): |
|
94 break |
|
95 else: |
|
96 undoc.append((key, name)) |
|
97 continue |
|
98 finally: |
|
99 f.close() |
|
100 if undoc: |
|
101 self.c_undoc[filename] = undoc |
|
102 |
|
103 def write_c_coverage(self): |
|
104 output_file = path.join(self.outdir, 'c.txt') |
|
105 op = open(output_file, 'w') |
|
106 try: |
|
107 write_header(op, 'Undocumented C API elements', '=') |
|
108 op.write('\n') |
|
109 |
|
110 for filename, undoc in self.c_undoc.iteritems(): |
|
111 write_header(op, filename) |
|
112 for typ, name in undoc: |
|
113 op.write(' * %-50s [%9s]\n' % (name, typ)) |
|
114 op.write('\n') |
|
115 finally: |
|
116 op.close() |
|
117 |
|
118 def build_py_coverage(self): |
|
119 for mod_name in self.env.modules: |
|
120 ignore = False |
|
121 for exp in self.mod_ignorexps: |
|
122 if exp.match(mod_name): |
|
123 ignore = True |
|
124 break |
|
125 if ignore: |
|
126 continue |
|
127 |
|
128 try: |
|
129 mod = __import__(mod_name, fromlist=['foo']) |
|
130 except ImportError, err: |
|
131 self.warn('module %s could not be imported: %s' % (mod_name, err)) |
|
132 self.py_undoc[mod_name] = {'error': err} |
|
133 continue |
|
134 |
|
135 funcs = [] |
|
136 classes = {} |
|
137 |
|
138 for name, obj in inspect.getmembers(mod): |
|
139 # diverse module attributes are ignored: |
|
140 if name[0] == '_': |
|
141 # begins in an underscore |
|
142 continue |
|
143 if not hasattr(obj, '__module__'): |
|
144 # cannot be attributed to a module |
|
145 continue |
|
146 if obj.__module__ != mod_name: |
|
147 # is not defined in this module |
|
148 continue |
|
149 |
|
150 full_name = '%s.%s' % (mod_name, name) |
|
151 |
|
152 if inspect.isfunction(obj): |
|
153 if full_name not in self.env.descrefs: |
|
154 for exp in self.fun_ignorexps: |
|
155 if exp.match(name): |
|
156 break |
|
157 else: |
|
158 funcs.append(name) |
|
159 elif inspect.isclass(obj): |
|
160 for exp in self.cls_ignorexps: |
|
161 if exp.match(name): |
|
162 break |
|
163 else: |
|
164 if full_name not in self.env.descrefs: |
|
165 # not documented at all |
|
166 classes[name] = [] |
|
167 continue |
|
168 |
|
169 attrs = [] |
|
170 |
|
171 for attr_name, attr in inspect.getmembers(obj, inspect.ismethod): |
|
172 if attr_name[0] == '_': |
|
173 # starts with an underscore, ignore it |
|
174 continue |
|
175 |
|
176 full_attr_name = '%s.%s' % (full_name, attr_name) |
|
177 if full_attr_name not in self.env.descrefs: |
|
178 attrs.append(attr_name) |
|
179 |
|
180 if attrs: |
|
181 # some attributes are undocumented |
|
182 classes[name] = attrs |
|
183 |
|
184 self.py_undoc[mod_name] = {'funcs': funcs, 'classes': classes} |
|
185 |
|
186 def write_py_coverage(self): |
|
187 output_file = path.join(self.outdir, 'python.txt') |
|
188 op = open(output_file, 'w') |
|
189 failed = [] |
|
190 try: |
|
191 write_header(op, 'Undocumented Python objects', '=') |
|
192 |
|
193 keys = self.py_undoc.keys() |
|
194 keys.sort() |
|
195 for name in keys: |
|
196 undoc = self.py_undoc[name] |
|
197 if 'error' in undoc: |
|
198 failed.append((name, undoc['error'])) |
|
199 else: |
|
200 if not undoc['classes'] and not undoc['funcs']: |
|
201 continue |
|
202 |
|
203 write_header(op, name) |
|
204 if undoc['funcs']: |
|
205 op.write('Functions:\n') |
|
206 op.writelines(' * %s\n' % x for x in undoc['funcs']) |
|
207 op.write('\n') |
|
208 if undoc['classes']: |
|
209 op.write('Classes:\n') |
|
210 for name, methods in undoc['classes'].iteritems(): |
|
211 if not methods: |
|
212 op.write(' * %s\n' % name) |
|
213 else: |
|
214 op.write(' * %s -- missing methods:\n' % name) |
|
215 op.writelines(' - %s\n' % x for x in methods) |
|
216 op.write('\n') |
|
217 |
|
218 if failed: |
|
219 write_header(op, 'Modules that failed to import') |
|
220 op.writelines(' * %s -- %s\n' % x for x in failed) |
|
221 finally: |
|
222 op.close() |
|
223 |
|
224 def finish(self): |
|
225 # dump the coverage data to a pickle file too |
|
226 picklepath = path.join(self.outdir, 'undoc.pickle') |
|
227 dumpfile = open(picklepath, 'wb') |
|
228 try: |
|
229 pickle.dump((self.py_undoc, self.c_undoc), dumpfile) |
|
230 finally: |
|
231 dumpfile.close() |
|
232 |
|
233 |
|
234 def setup(app): |
|
235 app.add_builder(CoverageBuilder) |
|
236 app.add_config_value('coverage_ignore_modules', [], False) |
|
237 app.add_config_value('coverage_ignore_functions', [], False) |
|
238 app.add_config_value('coverage_ignore_classes', [], False) |
|
239 app.add_config_value('coverage_c_path', [], False) |
|
240 app.add_config_value('coverage_c_regexes', {}, False) |
|
241 app.add_config_value('coverage_ignore_c_items', {}, False) |