Skip to content

Commit ea9b60b

Browse files
committed
added check_api_doc.py module in doc/source. This module generates report text files in doc/build/check_api helping to maintain the synchronization between the public API and the documentation API (api.rst file)
1 parent 79733fb commit ea9b60b

File tree

1 file changed

+396
-0
lines changed

1 file changed

+396
-0
lines changed

doc/source/check_api_doc.py

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
from __future__ import print_function
2+
3+
import sys
4+
import os
5+
import inspect
6+
import importlib
7+
import warnings
8+
9+
from sphinx.util.inspect import safe_getattr
10+
11+
_current_dir = os.path.dirname(__file__)
12+
_lib_path = os.path.abspath(os.path.join(_current_dir, '..', '..', 'larray'))
13+
_outdir = os.path.abspath(os.path.join(_current_dir, '..', 'build', 'check_api'))
14+
15+
sys.path.insert(0, _lib_path)
16+
from larray import __version__
17+
18+
_modules = ['larray', 'larray.random', 'larray.core.constants']
19+
_exclude = ['absolute_import', 'division', 'print_function', 'renamed_to']
20+
21+
22+
def _is_deprecated(obj):
23+
if not (inspect.isfunction(obj) or inspect.ismethod(obj)):
24+
return False
25+
return 'renamed_to' in obj.__qualname__
26+
27+
28+
def _write_header(f, text, char='-', indent=''):
29+
f.write(indent + text + '\n')
30+
f.write(indent + char * len(text) + '\n')
31+
32+
33+
class AbstractItem:
34+
def __init__(self, name):
35+
self.name = name
36+
37+
def copy(self):
38+
raise NotImplementedError
39+
40+
def __len__(self):
41+
raise NotImplementedError()
42+
43+
def insert_element(self, name, obj):
44+
raise NotImplementedError()
45+
46+
def auto_discovery(self):
47+
raise NotImplementedError()
48+
49+
def remove_deprecated(self):
50+
raise NotImplementedError()
51+
52+
def only_deprecated(self):
53+
raise NotImplementedError()
54+
55+
def diff(self, other):
56+
raise NotImplementedError()
57+
58+
def write(self, ofile):
59+
raise NotImplementedError()
60+
61+
62+
class ClassItem(AbstractItem):
63+
def __init__(self, class_):
64+
super().__init__(class_.__name__)
65+
self.class_ = class_
66+
self.attrs = {}
67+
self.methods = {}
68+
self.deprecated_methods = {}
69+
70+
def copy(self):
71+
new_item = ClassItem(self.class_)
72+
new_item.attrs = self.attrs.copy()
73+
new_item.methods = self.methods.copy()
74+
new_item.deprecated_methods = self.deprecated_methods.copy()
75+
return new_item
76+
77+
def __len__(self):
78+
return len(self.attrs) + len(self.methods) + len(self.deprecated_methods)
79+
80+
def insert_element(self, name, obj):
81+
# See function build_py_coverage() in sphinx/sphinx/ext/coverage.py file
82+
# from sphinx github repository
83+
if name[0] == '_':
84+
# starts with an underscore, ignore it
85+
return
86+
if name in _exclude:
87+
return
88+
try:
89+
attr = safe_getattr(self.class_, name)
90+
if inspect.isfunction(attr):
91+
if _is_deprecated(obj):
92+
self.deprecated_methods[name] = obj
93+
else:
94+
self.methods[name] = obj
95+
else:
96+
self.attrs[name] = obj
97+
except AttributeError:
98+
pass
99+
100+
def auto_discovery(self):
101+
for attr_name, attr_obj in inspect.getmembers(self.class_):
102+
self.insert_element(attr_name, attr_obj)
103+
104+
def remove_deprecated(self):
105+
without_deprecated = self.copy()
106+
without_deprecated.deprecated_methods = {}
107+
return without_deprecated
108+
109+
def only_deprecated(self):
110+
deprecated = ClassItem(self.class_)
111+
deprecated.deprecated_methods = self.deprecated_methods.copy()
112+
return deprecated
113+
114+
def diff(self, other):
115+
if not isinstance(other, ClassItem):
116+
raise TypeError("Expect a {} instance as argument. "
117+
"Got a {} instance instead".format(ClassItem.__name__, type(other).__name__))
118+
diff_item = ClassItem(self.class_)
119+
diff_item.attrs = {k: v for k, v in self.attrs.items() if k not in other.attrs}
120+
diff_item.methods = {k: v for k, v in self.methods.items() if k not in other.methods}
121+
diff_item.deprecated_methods = {k: v for k, v in self.deprecated_methods.items()
122+
if k not in other.deprecated_methods}
123+
return diff_item
124+
125+
def write(self, ofile):
126+
if len(self):
127+
indent = ' '
128+
_write_header(ofile, self.name, '=', indent=indent)
129+
if self.attrs:
130+
_write_header(ofile, 'Attributes', indent=indent)
131+
ofile.writelines(indent + ' * {}\n'.format(attr) for attr in self.attrs.keys())
132+
ofile.write('\n')
133+
if self.methods:
134+
_write_header(ofile, 'Methods', indent=indent)
135+
ofile.writelines(indent + ' * {}\n'.format(method) for method in self.methods.keys())
136+
ofile.write('\n')
137+
if self.deprecated_methods:
138+
_write_header(ofile, 'Deprecated Methods', indent=indent)
139+
ofile.writelines(indent + ' * {}\n'.format(method) for method in self.deprecated_methods.keys())
140+
ofile.write('\n')
141+
ofile.write('\n')
142+
143+
144+
class ModuleItem(AbstractItem):
145+
def __init__(self, module):
146+
super().__init__(module.__name__)
147+
self.module = module
148+
self.others = {}
149+
self.funcs = {}
150+
self.deprecated_items = {}
151+
self.classes = {}
152+
153+
def copy(self):
154+
new_item = ModuleItem(self.module)
155+
new_item.others = self.others.copy()
156+
new_item.funcs = self.funcs.copy()
157+
new_item.deprecated_items = self.deprecated_items.copy()
158+
new_item.classes = self.classes.copy()
159+
return new_item
160+
161+
def __len__(self):
162+
return len(self.others) + len(self.funcs) + len(self.deprecated_items) + len(self.classes)
163+
164+
def insert_element(self, name, obj):
165+
# See function build_py_coverage() in sphinx/sphinx/ext/coverage.py file
166+
# from sphinx github repository
167+
168+
# diverse module attributes are ignored:
169+
if name[0] == '_':
170+
# begins in an underscore
171+
return
172+
if not hasattr(obj, '__module__'):
173+
# cannot be attributed to a module
174+
return
175+
if name in _exclude:
176+
return
177+
178+
if inspect.isfunction(obj):
179+
if _is_deprecated(obj):
180+
self.deprecated_items[name] = obj
181+
else:
182+
self.funcs[name] = obj
183+
elif inspect.isclass(obj):
184+
if name in self.classes:
185+
warnings.warn("Class '{}' was already present in '{}' module item and will be replaced")
186+
class_ = getattr(self.module, name)
187+
class_item = ClassItem(class_)
188+
self.classes[name] = class_item
189+
else:
190+
self.others[name] = obj
191+
192+
def auto_discovery(self):
193+
for name, obj in inspect.getmembers(self.module):
194+
self.insert_element(name, obj)
195+
if inspect.isclass(obj):
196+
self.classes[name].auto_discovery()
197+
198+
def remove_deprecated(self):
199+
without_deprecated = self.copy()
200+
without_deprecated.deprecated_items = {}
201+
without_deprecated.classes = {k: v.remove_deprecated() for k, v in self.classes.items()}
202+
return without_deprecated
203+
204+
def only_deprecated(self):
205+
deprecated = ModuleItem(self.module)
206+
deprecated.deprecated_items = self.deprecated_items.copy()
207+
classes = {k: v.only_deprecated() for k, v in self.classes.items()}
208+
deprecated.classes = {k: v for k, v in classes.items() if len(v)}
209+
return deprecated
210+
211+
def diff(self, other):
212+
if not isinstance(other, ModuleItem):
213+
raise TypeError("Expect a {} instance as argument. "
214+
"Got a {} instance instead".format(ModuleItem.__name__, type(other).__name__))
215+
diff_item = ModuleItem(self.module)
216+
diff_item.others = {k: v for k, v in self.others.items() if k not in other.others}
217+
diff_item.funcs = {k: v for k, v in self.funcs.items() if k not in other.funcs}
218+
diff_item.deprecated_items = {k: v for k, v in self.deprecated_items.items()
219+
if k not in other.deprecated_items}
220+
diff_item.classes = {k: v.diff(other.classes[k]) if k in other.classes else v
221+
for k, v in self.classes.items()}
222+
return diff_item
223+
224+
def write(self, ofile):
225+
if len(self):
226+
_write_header(ofile, 'module <' + self.name + '>', '~')
227+
ofile.write('\n')
228+
if self.others:
229+
_write_header(ofile, 'Miscellaneous', '=')
230+
ofile.writelines(' * {}\n'.format(other) for other in self.others.keys())
231+
ofile.write('\n')
232+
if self.funcs:
233+
_write_header(ofile, 'Functions', '=')
234+
ofile.writelines(' * {}\n'.format(func) for func in self.funcs.keys())
235+
ofile.write('\n')
236+
if self.deprecated_items:
237+
_write_header(ofile, 'Deprecated Functions or Classes', '=')
238+
ofile.writelines(' * {}\n'.format(func) for func in self.deprecated_items.keys())
239+
ofile.write('\n')
240+
if self.classes:
241+
_write_header(ofile, 'Classes', '=')
242+
ofile.write('\n')
243+
for class_item in self.classes.values():
244+
class_item.write(ofile)
245+
ofile.write('\n')
246+
ofile.write('\n')
247+
248+
249+
def make_diff(left_api, right_api, include_deprecated=False):
250+
if not include_deprecated:
251+
left_api = {k: v.remove_deprecated() for k, v in left_api.items()}
252+
right_api = {k: v.remove_deprecated() for k, v in right_api.items()}
253+
254+
diff_api = {}
255+
for left_module_name, left_module_item in left_api.items():
256+
if left_module_name in right_api:
257+
right_module_item = right_api[left_module_name]
258+
diff_api[left_module_name] = left_module_item.diff(right_module_item)
259+
else:
260+
diff_api[left_module_name] = left_module_item
261+
return diff_api
262+
263+
264+
def get_public_api():
265+
public_api = {}
266+
for module_name in _modules:
267+
try:
268+
module = importlib.import_module(module_name)
269+
module_item = ModuleItem(module)
270+
module_item.auto_discovery()
271+
public_api[module_name] = module_item
272+
except ImportError as err:
273+
print('module {} could not be imported: {}'.format(module_name, err))
274+
public_api[module_name] = err
275+
return public_api
276+
277+
278+
# See file sphinx/sphinx/ext/autosummary/generate.py from sphinx github repository
279+
def get_autosummary_api():
280+
import shutil
281+
from sphinx.ext.autosummary import import_by_name
282+
from sphinx.ext.autosummary.generate import DummyApplication, setup_documenters, generate_autosummary_docs
283+
284+
sources = ['api.rst']
285+
output_dir = './tmp_generated'
286+
app = DummyApplication()
287+
setup_documenters(app)
288+
generate_autosummary_docs(sources, output_dir, app=app)
289+
290+
autosummary_api = {}
291+
for module_name in _modules:
292+
try:
293+
module = importlib.import_module(module_name)
294+
module_item = ModuleItem(module)
295+
autosummary_api[module_name] = module_item
296+
except ImportError as err:
297+
print('module {} could not be imported: {}'.format(module_name, err))
298+
autosummary_api[module_name] = err
299+
300+
for generated_rst_file in os.listdir(output_dir):
301+
qualname, ext = os.path.splitext(generated_rst_file)
302+
qualname, obj, parent, module_name = import_by_name(qualname)
303+
module_item = autosummary_api[module_name]
304+
name = qualname.split('.')[-1]
305+
if inspect.isclass(obj) and name in module_item.classes:
306+
continue
307+
if inspect.isclass(parent):
308+
class_name = parent.__name__
309+
if class_name not in module_item.classes:
310+
module_item.insert_element(class_name, parent)
311+
class_item = module_item.classes[class_name]
312+
class_item.insert_element(name, obj)
313+
else:
314+
module_item.insert_element(name, obj)
315+
316+
if os.path.exists(output_dir):
317+
shutil.rmtree(output_dir)
318+
319+
return autosummary_api
320+
321+
322+
def write_api(filepath, api, header='API', version=True):
323+
if not os.path.exists(_outdir):
324+
os.mkdir(_outdir)
325+
output_file = os.path.join(_outdir, filepath)
326+
327+
failed = []
328+
with open(output_file, 'w') as ofile:
329+
if version:
330+
header = '{} [{}]'.format(header, __version__)
331+
_write_header(ofile, header, '~')
332+
ofile.write('\n')
333+
keys = sorted(api.keys())
334+
335+
for module_name in keys:
336+
module_item = api[module_name]
337+
if not isinstance(module_item, ModuleItem):
338+
failed.append((module_name, module_item))
339+
else:
340+
module_item.write(ofile)
341+
342+
if failed:
343+
_write_header(ofile, 'Modules that failed to import')
344+
ofile.writelines(' * {} -- {}\n'.format(module_name, error) for module_name, error in failed)
345+
346+
347+
def get_items_from_api_doc():
348+
from sphinx.ext.autosummary import Autosummary
349+
from docutils.core import publish_doctree
350+
from docutils.parsers.rst import directives
351+
import docutils.nodes
352+
353+
def add_item(item, api_doc_items):
354+
item = item.astext().strip().split()
355+
if len(item) > 0:
356+
# if item comes from a hand written table (like the Exploring section of Axis in api.rst)
357+
# we select the text from the left column
358+
if isinstance(item, list):
359+
item = item[0]
360+
api_doc_items.append(item)
361+
362+
api_doc_items = []
363+
directives.register_directive('autosummary', Autosummary)
364+
cleanup_line = lambda line: line.replace(':attr:', ' ').replace('`', ' ')
365+
check_line = lambda line: len(line) and not (line.startswith('..') or ':' in line)
366+
with open('./api.rst', mode='r') as f:
367+
content = [cleanup_line(line) for line in f.readlines()]
368+
content = '\n'.join([line for line in content if check_line(line)])
369+
document = publish_doctree(content)
370+
nodes = list(document)
371+
for node in nodes:
372+
if isinstance(node, docutils.nodes.block_quote):
373+
for item in node.children:
374+
add_item(item, api_doc_items)
375+
if isinstance(node, docutils.nodes.table):
376+
for item in node[0][2].children:
377+
add_item(item, api_doc_items)
378+
return api_doc_items
379+
380+
381+
if __name__ == '__main__':
382+
public_api = get_public_api()
383+
write_api('public_api.txt', public_api, header='PUBLIC API')
384+
385+
public_api_only_deprecated = {k: v.only_deprecated() for k, v in public_api.items()}
386+
write_api('public_api_only_deprecated.txt', public_api_only_deprecated, header='DEPRECATED API')
387+
388+
api_reference = get_autosummary_api()
389+
write_api('api_reference.txt', api_reference, header='API REFERENCE')
390+
391+
api_reference_only_deprecated = {k: v.only_deprecated() for k, v in api_reference.items()}
392+
write_api('api_reference_only_deprecated.txt', api_reference_only_deprecated,
393+
header='DEPRECATED ITEMS IN API REFERENCE')
394+
395+
missing_in_api_ref = make_diff(public_api, api_reference)
396+
write_api('missing_api_items_in_doc.txt', missing_in_api_ref, header='MISSING ITEMS IN API REFERENCE')

0 commit comments

Comments
 (0)