Skip to content

Commit 050797c

Browse files
committed
Use coverage checker instead of automatic member extraction
1 parent 53f5f3c commit 050797c

File tree

4 files changed

+313
-7
lines changed

4 files changed

+313
-7
lines changed

website/Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
SPHINXOPTS=-d sphinx/build/doctrees sphinx
12
.PHONY: sphinx
23
sphinx:
3-
sphinx-build -b html -d sphinx/build/doctrees sphinx sphinx/build/html
4+
sphinx-build -b html $(SPHINXOPTS) sphinx/build/html
5+
6+
.PHONY: coverage
7+
coverage:
8+
sphinx-build -b coverage ${SPHINXOPTS} sphinx/build/coverage
49

510
clean:
611
rm -rf sphinx/build

website/sphinx/conf.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@
44
sys.path.insert(0, os.path.abspath("../.."))
55
import tornado
66

7-
print tornado.__file__
7+
# For our version of sphinx_coverage.py. The version in sphinx 1.0.7
8+
# has too many false positives; this version comes from upstream HG.
9+
sys.path.append(os.path.abspath("."))
810

911
master_doc = "index"
1012

1113
project = "Tornado"
1214
copyright = "2011, Facebook"
1315

14-
import tornado
1516
version = release = tornado.version
1617

17-
extensions = ["sphinx.ext.autodoc"]
18+
extensions = ["sphinx.ext.autodoc", "sphinx_coverage"]
1819

1920
autodoc_member_order = "bysource"
20-
autodoc_default_flags = ["members", "undoc-members"]
21+
22+
coverage_skip_undoc_in_source = True
23+
# I wish this could go in a per-module file...
24+
coverage_ignore_classes = [
25+
# tornado.web
26+
"ChunkedTransferEncoding",
27+
"GZipContentEncoding",
28+
"OutputTransform",
29+
"TemplateModule",
30+
"url",
31+
]
32+

website/sphinx/sphinx_coverage.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
10+
:license: BSD, see LICENSE for details.
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.builders 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(
57+
'coverage_ignore_c_items', exps, self.warn)
58+
self.mod_ignorexps = compile_regex_list(
59+
'coverage_ignore_modules', self.config.coverage_ignore_modules,
60+
self.warn)
61+
self.cls_ignorexps = compile_regex_list(
62+
'coverage_ignore_classes', self.config.coverage_ignore_classes,
63+
self.warn)
64+
self.fun_ignorexps = compile_regex_list(
65+
'coverage_ignore_functions', 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+
c_objects = self.env.domaindata['c']['objects']
83+
for filename in self.c_sourcefiles:
84+
undoc = []
85+
f = open(filename, 'r')
86+
try:
87+
for line in f:
88+
for key, regex in self.c_regexes:
89+
match = regex.match(line)
90+
if match:
91+
name = match.groups()[0]
92+
if name not in c_objects:
93+
for exp in self.c_ignorexps.get(key, ()):
94+
if exp.match(name):
95+
break
96+
else:
97+
undoc.append((key, name))
98+
continue
99+
finally:
100+
f.close()
101+
if undoc:
102+
self.c_undoc[filename] = undoc
103+
104+
def write_c_coverage(self):
105+
output_file = path.join(self.outdir, 'c.txt')
106+
op = open(output_file, 'w')
107+
try:
108+
if self.config.coverage_write_headline:
109+
write_header(op, 'Undocumented C API elements', '=')
110+
op.write('\n')
111+
112+
for filename, undoc in self.c_undoc.iteritems():
113+
write_header(op, filename)
114+
for typ, name in undoc:
115+
op.write(' * %-50s [%9s]\n' % (name, typ))
116+
op.write('\n')
117+
finally:
118+
op.close()
119+
120+
def build_py_coverage(self):
121+
objects = self.env.domaindata['py']['objects']
122+
modules = self.env.domaindata['py']['modules']
123+
124+
skip_undoc = self.config.coverage_skip_undoc_in_source
125+
126+
for mod_name in modules:
127+
ignore = False
128+
for exp in self.mod_ignorexps:
129+
if exp.match(mod_name):
130+
ignore = True
131+
break
132+
if ignore:
133+
continue
134+
135+
try:
136+
mod = __import__(mod_name, fromlist=['foo'])
137+
except ImportError, err:
138+
self.warn('module %s could not be imported: %s' %
139+
(mod_name, err))
140+
self.py_undoc[mod_name] = {'error': err}
141+
continue
142+
143+
funcs = []
144+
classes = {}
145+
146+
for name, obj in inspect.getmembers(mod):
147+
# diverse module attributes are ignored:
148+
if name[0] == '_':
149+
# begins in an underscore
150+
continue
151+
if not hasattr(obj, '__module__'):
152+
# cannot be attributed to a module
153+
continue
154+
if obj.__module__ != mod_name:
155+
# is not defined in this module
156+
continue
157+
158+
full_name = '%s.%s' % (mod_name, name)
159+
160+
if inspect.isfunction(obj):
161+
if full_name not in objects:
162+
for exp in self.fun_ignorexps:
163+
if exp.match(name):
164+
break
165+
else:
166+
if skip_undoc and not obj.__doc__:
167+
continue
168+
funcs.append(name)
169+
elif inspect.isclass(obj):
170+
for exp in self.cls_ignorexps:
171+
if exp.match(name):
172+
break
173+
else:
174+
if full_name not in objects:
175+
if skip_undoc and not obj.__doc__:
176+
continue
177+
# not documented at all
178+
classes[name] = []
179+
continue
180+
181+
attrs = []
182+
183+
for attr_name in dir(obj):
184+
if attr_name not in obj.__dict__:
185+
continue
186+
attr = getattr(obj, attr_name)
187+
if not (inspect.ismethod(attr) or
188+
inspect.isfunction(attr)):
189+
continue
190+
if attr_name[0] == '_':
191+
# starts with an underscore, ignore it
192+
continue
193+
if skip_undoc and not attr.__doc__:
194+
# skip methods without docstring if wished
195+
continue
196+
197+
full_attr_name = '%s.%s' % (full_name, attr_name)
198+
if full_attr_name not in objects:
199+
attrs.append(attr_name)
200+
201+
if attrs:
202+
# some attributes are undocumented
203+
classes[name] = attrs
204+
205+
self.py_undoc[mod_name] = {'funcs': funcs, 'classes': classes}
206+
207+
def write_py_coverage(self):
208+
output_file = path.join(self.outdir, 'python.txt')
209+
op = open(output_file, 'w')
210+
failed = []
211+
try:
212+
if self.config.coverage_write_headline:
213+
write_header(op, 'Undocumented Python objects', '=')
214+
keys = self.py_undoc.keys()
215+
keys.sort()
216+
for name in keys:
217+
undoc = self.py_undoc[name]
218+
if 'error' in undoc:
219+
failed.append((name, undoc['error']))
220+
else:
221+
if not undoc['classes'] and not undoc['funcs']:
222+
continue
223+
224+
write_header(op, name)
225+
if undoc['funcs']:
226+
op.write('Functions:\n')
227+
op.writelines(' * %s\n' % x for x in undoc['funcs'])
228+
op.write('\n')
229+
if undoc['classes']:
230+
op.write('Classes:\n')
231+
for name, methods in sorted(undoc['classes'].iteritems()):
232+
if not methods:
233+
op.write(' * %s\n' % name)
234+
else:
235+
op.write(' * %s -- missing methods:\n' % name)
236+
op.writelines(' - %s\n' % x for x in methods)
237+
op.write('\n')
238+
239+
if failed:
240+
write_header(op, 'Modules that failed to import')
241+
op.writelines(' * %s -- %s\n' % x for x in failed)
242+
finally:
243+
op.close()
244+
245+
def finish(self):
246+
# dump the coverage data to a pickle file too
247+
picklepath = path.join(self.outdir, 'undoc.pickle')
248+
dumpfile = open(picklepath, 'wb')
249+
try:
250+
pickle.dump((self.py_undoc, self.c_undoc), dumpfile)
251+
finally:
252+
dumpfile.close()
253+
254+
255+
def setup(app):
256+
app.add_builder(CoverageBuilder)
257+
app.add_config_value('coverage_ignore_modules', [], False)
258+
app.add_config_value('coverage_ignore_functions', [], False)
259+
app.add_config_value('coverage_ignore_classes', [], False)
260+
app.add_config_value('coverage_c_path', [], False)
261+
app.add_config_value('coverage_c_regexes', {}, False)
262+
app.add_config_value('coverage_ignore_c_items', {}, False)
263+
app.add_config_value('coverage_write_headline', True, False)
264+
app.add_config_value('coverage_skip_undoc_in_source', False, False)

website/sphinx/web.rst

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
===============
33

44
.. automodule:: tornado.web
5-
:exclude-members: RequestHandler, Application, asynchronous, addslash, removeslash, URLSpec, url
65

76
Request handlers
87
----------------
98
.. autoclass:: RequestHandler
10-
:exclude-members: initialize, prepare, get, post, put, delete, head, options, get_argument, get_arguments, decode_argument, set_status, set_header, write, flush, finish, render, render_string, send_error, get_error_html, cookies, get_cookie, set_cookie, clear_cookie, clear_all_cookies, get_secure_cookie, set_secure_cookie, create_signed_value
119

1210
**Entry points**
1311

@@ -39,8 +37,11 @@
3937
.. automethod:: finish
4038
.. automethod:: render
4139
.. automethod:: render_string
40+
.. automethod:: redirect
4241
.. automethod:: send_error
4342
.. automethod:: get_error_html
43+
.. automethod:: clear
44+
4445

4546
**Cookies**
4647

@@ -55,11 +56,26 @@
5556

5657
**Other**
5758

59+
.. automethod:: async_callback
60+
.. automethod:: check_xsrf_cookie
61+
.. automethod:: compute_etag
62+
.. automethod:: get_browser_locale
63+
.. automethod:: get_current_user
64+
.. automethod:: get_login_url
65+
.. automethod:: get_status
66+
.. automethod:: get_template_path
67+
.. automethod:: get_user_locale
68+
.. automethod:: on_connection_close
69+
.. automethod:: require_setting
70+
.. automethod:: static_url
71+
.. automethod:: xsrf_form_html
72+
5873

5974

6075
Application configuration
6176
-----------------------------
6277
.. autoclass:: Application
78+
:members:
6379

6480
.. autoclass:: URLSpec
6581

@@ -74,3 +90,12 @@
7490

7591
Everything else
7692
---------------
93+
.. autoexception:: HTTPError
94+
.. autoclass:: UIModule
95+
:members:
96+
97+
.. autoclass:: ErrorHandler
98+
.. autoclass:: FallbackHandler
99+
.. autoclass:: RedirectHandler
100+
.. autoclass:: StaticFileHandler
101+
:members:

0 commit comments

Comments
 (0)