Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
jantic
GitHub Repository: jantic/deoldify
Path: blob/master/fastai/gen_doc/nbdoc.py
781 views
1
"`gen_doc.nbdoc` generates notebook documentation from module functions and links to correct places"
2
3
import inspect,importlib,enum,os,re,nbconvert
4
from IPython.core.display import display, Markdown, HTML
5
from nbconvert import HTMLExporter
6
from IPython.core import page
7
from IPython import get_ipython
8
from typing import Dict, Any, AnyStr, List, Sequence, TypeVar, Tuple, Optional, Union
9
from .docstrings import *
10
from .core import *
11
from ..torch_core import *
12
from .nbtest import get_pytest_html
13
from ..utils.ipython import IS_IN_COLAB
14
15
__all__ = ['get_fn_link', 'link_docstring', 'show_doc', 'get_ft_names', 'md2html',
16
'get_exports', 'show_video', 'show_video_from_youtube', 'import_mod', 'get_source_link',
17
'is_enum', 'jekyll_note', 'jekyll_warn', 'jekyll_important', 'doc']
18
19
MODULE_NAME = 'fastai'
20
SOURCE_URL = 'https://github.com/fastai/fastai/blob/master/'
21
PYTORCH_DOCS = 'https://pytorch.org/docs/stable/'
22
FASTAI_DOCS = 'https://docs.fast.ai'
23
use_relative_links = True
24
25
_typing_names = {t:n for t,n in fastai_types.items() if t.__module__=='typing'}
26
arg_prefixes = {inspect._VAR_POSITIONAL: '\*', inspect._VAR_KEYWORD:'\*\*'}
27
28
29
def is_enum(cls): return cls == enum.Enum or cls == enum.EnumMeta
30
31
def link_type(arg_type, arg_name=None, include_bt:bool=True):
32
"Create link to documentation."
33
arg_name = arg_name or fn_name(arg_type)
34
if include_bt: arg_name = code_esc(arg_name)
35
if belongs_to_module(arg_type, 'torch') and ('Tensor' not in arg_name): return f'[{arg_name}]({get_pytorch_link(arg_type)})'
36
if is_fastai_class(arg_type): return f'[{arg_name}]({get_fn_link(arg_type)})'
37
return arg_name
38
39
def is_fastai_class(t): return belongs_to_module(t, MODULE_NAME)
40
41
def belongs_to_module(t, module_name):
42
"Check if `t` belongs to `module_name`."
43
if hasattr(t, '__func__'): return belongs_to_module(t.__func__, module_name)
44
if not inspect.getmodule(t): return False
45
return inspect.getmodule(t).__name__.startswith(module_name)
46
47
def code_esc(s): return f'`{s}`'
48
49
def type_repr(t):
50
if t in _typing_names: return link_type(t, _typing_names[t])
51
if isinstance(t, partial): return partial_repr(t)
52
if hasattr(t, '__forward_arg__'): return link_type(t.__forward_arg__)
53
elif getattr(t, '__args__', None):
54
args = t.__args__
55
if len(args)==2 and args[1] == type(None):
56
return f'`Optional`\[{type_repr(args[0])}\]'
57
reprs = ', '.join([type_repr(o) for o in args])
58
return f'{link_type(t)}\[{reprs}\]'
59
else: return link_type(t)
60
61
def partial_repr(t):
62
args = (t.func,) + t.args + tuple([f'{k}={v}' for k,v in t.keywords.items()])
63
reprs = ', '.join([link_type(o) for o in args])
64
return f'<code>partial(</code>{reprs}<code>)</code>'
65
66
def anno_repr(a): return type_repr(a)
67
68
def format_param(p):
69
"Formats function param to `param1:Type=val`. Font weights: param1=bold, val=bold+italic"
70
arg_prefix = arg_prefixes.get(p.kind, '') # asterisk prefix for *args and **kwargs
71
res = f"**{arg_prefix}{code_esc(p.name)}**"
72
if hasattr(p, 'annotation') and p.annotation != p.empty: res += f':{anno_repr(p.annotation)}'
73
if p.default != p.empty:
74
default = getattr(p.default, 'func', p.default)
75
default = getattr(default, '__name__', default)
76
res += f'=***`{repr(default)}`***'
77
return res
78
79
def format_ft_def(func, full_name:str=None)->str:
80
"Format and link `func` definition to show in documentation"
81
sig = inspect.signature(func)
82
name = f'<code>{full_name or func.__name__}</code>'
83
fmt_params = [format_param(param) for name,param
84
in sig.parameters.items() if name not in ('self','cls')]
85
arg_str = f"({', '.join(fmt_params)})"
86
if sig.return_annotation and (sig.return_annotation != sig.empty): arg_str += f" → {anno_repr(sig.return_annotation)}"
87
if is_fastai_class(type(func)): arg_str += f" :: {link_type(type(func))}"
88
f_name = f"<code>class</code> {name}" if inspect.isclass(func) else name
89
return f'{f_name}',f'{name}{arg_str}'
90
91
def get_enum_doc(elt, full_name:str)->str:
92
"Formatted enum documentation."
93
vals = ', '.join(elt.__members__.keys())
94
return f'{code_esc(full_name)}',f'<code>Enum</code> = [{vals}]'
95
96
def get_cls_doc(elt, full_name:str)->str:
97
"Class definition."
98
parent_class = inspect.getclasstree([elt])[-1][0][1][0]
99
name,args = format_ft_def(elt, full_name)
100
if parent_class != object: args += f' :: {link_type(parent_class, include_bt=True)}'
101
return name,args
102
103
def show_doc(elt, doc_string:bool=True, full_name:str=None, arg_comments:dict=None, title_level=None, alt_doc_string:str='',
104
ignore_warn:bool=False, markdown=True, show_tests=True):
105
"Show documentation for element `elt`. Supported types: class, Callable, and enum."
106
arg_comments = ifnone(arg_comments, {})
107
anchor_id = get_anchor(elt)
108
elt = getattr(elt, '__func__', elt)
109
full_name = full_name or fn_name(elt)
110
if inspect.isclass(elt):
111
if is_enum(elt.__class__): name,args = get_enum_doc(elt, full_name)
112
else: name,args = get_cls_doc(elt, full_name)
113
elif isinstance(elt, Callable): name,args = format_ft_def(elt, full_name)
114
else: raise Exception(f'doc definition not supported for {full_name}')
115
source_link = get_function_source(elt) if is_fastai_class(elt) else ""
116
test_link, test_modal = get_pytest_html(elt, anchor_id=anchor_id) if show_tests else ('', '')
117
title_level = ifnone(title_level, 2 if inspect.isclass(elt) else 4)
118
doc = f'<h{title_level} id="{anchor_id}" class="doc_header">{name}{source_link}{test_link}</h{title_level}>'
119
doc += f'\n\n> {args}\n\n'
120
doc += f'{test_modal}'
121
if doc_string and (inspect.getdoc(elt) or arg_comments):
122
doc += format_docstring(elt, arg_comments, alt_doc_string, ignore_warn) + ' '
123
if markdown: display(Markdown(doc))
124
else: return doc
125
126
def md2html(md):
127
if nbconvert.__version__ < '5.5.0': return HTMLExporter().markdown2html(md)
128
else: return HTMLExporter().markdown2html(defaultdict(lambda: defaultdict(dict)), md)
129
130
def doc(elt):
131
"Show `show_doc` info in preview window along with link to full docs."
132
global use_relative_links
133
use_relative_links = False
134
elt = getattr(elt, '__func__', elt)
135
md = show_doc(elt, markdown=False)
136
if is_fastai_class(elt):
137
md += f'\n\n<a href="{get_fn_link(elt)}" target="_blank" rel="noreferrer noopener">Show in docs</a>'
138
output = md2html(md)
139
use_relative_links = True
140
if IS_IN_COLAB: get_ipython().run_cell_magic(u'html', u'', output)
141
else:
142
try: page.page({'text/html': output})
143
except: display(Markdown(md))
144
145
def format_docstring(elt, arg_comments:dict={}, alt_doc_string:str='', ignore_warn:bool=False)->str:
146
"Merge and format the docstring definition with `arg_comments` and `alt_doc_string`."
147
parsed = ""
148
doc = parse_docstring(inspect.getdoc(elt))
149
description = alt_doc_string or f"{doc['short_description']} {doc['long_description']}"
150
if description: parsed += f'\n\n{link_docstring(inspect.getmodule(elt), description)}'
151
152
resolved_comments = {**doc.get('comments', {}), **arg_comments} # arg_comments takes priority
153
args = inspect.getfullargspec(elt).args if not is_enum(elt.__class__) else elt.__members__.keys()
154
if resolved_comments: parsed += '\n'
155
for a in resolved_comments:
156
parsed += f'\n- *{a}*: {resolved_comments[a]}'
157
if a not in args and not ignore_warn: warn(f'Doc arg mismatch: {a}')
158
159
return_comment = arg_comments.get('return') or doc.get('return')
160
if return_comment: parsed += f'\n\n*return*: {return_comment}'
161
return parsed
162
163
_modvars = {}
164
165
def replace_link(m):
166
keyword = m.group(1) or m.group(2)
167
elt = find_elt(_modvars, keyword)
168
if elt is None: return m.group()
169
return link_type(elt, arg_name=keyword)
170
171
# Finds all places with a backtick but only if it hasn't already been linked
172
BT_REGEX = re.compile("\[`([^`]*)`\](?:\([^)]*\))|`([^`]*)`") # matches [`key`](link) or `key`
173
def link_docstring(modules, docstring:str, overwrite:bool=False)->str:
174
"Search `docstring` for backticks and attempt to link those functions to respective documentation."
175
mods = listify(modules)
176
for mod in mods: _modvars.update(mod.__dict__) # concat all module definitions
177
return re.sub(BT_REGEX, replace_link, docstring)
178
179
def find_elt(modvars, keyword, match_last=False):
180
"Attempt to resolve keywords such as Learner.lr_find. `match_last` starts matching from last component."
181
keyword = strip_fastai(keyword)
182
if keyword in modvars: return modvars[keyword]
183
comps = keyword.split('.')
184
comp_elt = modvars.get(comps[0])
185
if hasattr(comp_elt, '__dict__'): return find_elt(comp_elt.__dict__, '.'.join(comps[1:]), match_last=match_last)
186
187
def import_mod(mod_name:str, ignore_errors=False):
188
"Return module from `mod_name`."
189
splits = str.split(mod_name, '.')
190
try:
191
if len(splits) > 1 : mod = importlib.import_module('.' + '.'.join(splits[1:]), splits[0])
192
else: mod = importlib.import_module(mod_name)
193
return mod
194
except:
195
if not ignore_errors: print(f"Module {mod_name} doesn't exist.")
196
197
def show_doc_from_name(mod_name, ft_name:str, doc_string:bool=True, arg_comments:dict={}, alt_doc_string:str=''):
198
"Show documentation for `ft_name`, see `show_doc`."
199
mod = import_mod(mod_name)
200
splits = str.split(ft_name, '.')
201
assert hasattr(mod, splits[0]), print(f"Module {mod_name} doesn't have a function named {splits[0]}.")
202
elt = getattr(mod, splits[0])
203
for i,split in enumerate(splits[1:]):
204
assert hasattr(elt, split), print(f"Class {'.'.join(splits[:i+1])} doesn't have a function named {split}.")
205
elt = getattr(elt, split)
206
show_doc(elt, doc_string, ft_name, arg_comments, alt_doc_string)
207
208
def get_exports(mod):
209
public_names = mod.__all__ if hasattr(mod, '__all__') else dir(mod)
210
#public_names.sort(key=str.lower)
211
return [o for o in public_names if not o.startswith('_')]
212
213
def get_ft_names(mod, include_inner=False)->List[str]:
214
"Return all the functions of module `mod`."
215
# If the module has an attribute __all__, it picks those.
216
# Otherwise, it returns all the functions defined inside a module.
217
fn_names = []
218
for elt_name in get_exports(mod):
219
elt = getattr(mod,elt_name)
220
#This removes the files imported from elsewhere
221
try: fname = inspect.getfile(elt)
222
except: continue
223
if mod.__file__.endswith('__init__.py'):
224
if inspect.ismodule(elt): fn_names.append(elt_name)
225
else: continue
226
else:
227
if (fname != mod.__file__): continue
228
if inspect.isclass(elt) or inspect.isfunction(elt): fn_names.append(elt_name)
229
else: continue
230
if include_inner and inspect.isclass(elt) and not is_enum(elt.__class__):
231
fn_names.extend(get_inner_fts(elt))
232
return fn_names
233
234
def get_inner_fts(elt)->List[str]:
235
"List the inner functions of a class."
236
fts = []
237
for ft_name in elt.__dict__.keys():
238
if ft_name.startswith('_'): continue
239
ft = getattr(elt, ft_name)
240
if inspect.isfunction(ft): fts.append(f'{elt.__name__}.{ft_name}')
241
if inspect.ismethod(ft): fts.append(f'{elt.__name__}.{ft_name}')
242
if inspect.isclass(ft): fts += [f'{elt.__name__}.{n}' for n in get_inner_fts(ft)]
243
return fts
244
245
def get_module_toc(mod_name):
246
"Display table of contents for given `mod_name`."
247
mod = import_mod(mod_name)
248
ft_names = mod.__all__ if hasattr(mod,'__all__') else get_ft_names(mod)
249
ft_names.sort(key = str.lower)
250
tabmat = ''
251
for ft_name in ft_names:
252
tabmat += f'- [{ft_name}](#{ft_name})\n'
253
elt = getattr(mod, ft_name)
254
if inspect.isclass(elt) and not is_enum(elt.__class__):
255
in_ft_names = get_inner_fts(elt)
256
for name in in_ft_names:
257
tabmat += f' - [{name}](#{name})\n'
258
display(Markdown(tabmat))
259
260
def show_video(url):
261
"Display video in `url`."
262
data = f'<iframe width="560" height="315" src="{url}" frameborder="0" allowfullscreen></iframe>'
263
return display(HTML(data))
264
265
def show_video_from_youtube(code, start=0):
266
"Display video from Youtube with a `code` and a `start` time."
267
url = f'https://www.youtube.com/embed/{code}?start={start}&amp;rel=0&amp;controls=0&amp;showinfo=0'
268
return show_video(url)
269
270
def get_anchor(fn)->str:
271
if hasattr(fn,'__qualname__'): return fn.__qualname__
272
if inspect.ismethod(fn): return fn_name(fn.__self__) + '.' + fn_name(fn)
273
return fn_name(fn)
274
275
def fn_name(ft)->str:
276
if ft.__hash__ and ft in _typing_names: return _typing_names[ft]
277
if hasattr(ft, '__name__'): return ft.__name__
278
elif hasattr(ft,'_name') and ft._name: return ft._name
279
elif hasattr(ft,'__origin__'): return str(ft.__origin__).split('.')[-1]
280
else: return str(ft).split('.')[-1]
281
282
def get_fn_link(ft)->str:
283
"Return function link to notebook documentation of `ft`. Private functions link to source code"
284
ft = getattr(ft, '__func__', ft)
285
anchor = strip_fastai(get_anchor(ft))
286
module_name = strip_fastai(get_module_name(ft))
287
base = '' if use_relative_links else FASTAI_DOCS
288
return f'{base}/{module_name}.html#{anchor}'
289
290
def get_module_name(ft)->str: return inspect.getmodule(ft).__name__
291
292
def get_pytorch_link(ft)->str:
293
"Returns link to pytorch docs of `ft`."
294
name = ft.__name__
295
ext = '.html'
296
if name == 'device': return f'{PYTORCH_DOCS}tensor_attributes{ext}#torch-device'
297
if name == 'Tensor': return f'{PYTORCH_DOCS}tensors{ext}#torch-tensor'
298
if name.startswith('torchvision'):
299
doc_path = get_module_name(ft).replace('.', '/')
300
if inspect.ismodule(ft): name = name.replace('.', '-')
301
return f'{PYTORCH_DOCS}{doc_path}{ext}#{name}'
302
if name.startswith('torch.nn') and inspect.ismodule(ft): # nn.functional is special case
303
nn_link = name.replace('.', '-')
304
return f'{PYTORCH_DOCS}nn{ext}#{nn_link}'
305
paths = get_module_name(ft).split('.')
306
if len(paths) == 1: return f'{PYTORCH_DOCS}{paths[0]}{ext}#{paths[0]}.{name}'
307
308
offset = 1 if paths[1] == 'utils' else 0 # utils is a pytorch special case
309
doc_path = paths[1+offset]
310
if inspect.ismodule(ft): return f'{PYTORCH_DOCS}{doc_path}{ext}#module-{name}'
311
fnlink = '.'.join(paths[:(2+offset)]+[name])
312
return f'{PYTORCH_DOCS}{doc_path}{ext}#{fnlink}'
313
314
def get_source_link(file, line, display_text="[source]", **kwargs)->str:
315
"Returns github link for given file"
316
link = f"{SOURCE_URL}{file}#L{line}"
317
if display_text is None: return link
318
return f'<a href="{link}" class="source_link" style="float:right">{display_text}</a>'
319
320
def get_function_source(ft, **kwargs)->str:
321
"Returns link to `ft` in source code."
322
try: line = inspect.getsourcelines(ft)[1]
323
except Exception: return ''
324
mod_path = get_module_name(ft).replace('.', '/') + '.py'
325
return get_source_link(mod_path, line, **kwargs)
326
327
def title_md(s:str, title_level:int, markdown=True):
328
res = '#' * title_level
329
if title_level: res += ' '
330
return Markdown(res+s) if markdown else (res+s)
331
332
def jekyll_div(s,c,h,icon=None):
333
icon = ifnone(icon,c)
334
res = f'<div markdown="span" class="alert alert-{c}" role="alert"><i class="fa fa-{c}-circle"></i> <b>{h}: </b>{s}</div>'
335
display(Markdown(res))
336
337
def jekyll_note(s): return jekyll_div(s,'info','Note')
338
def jekyll_warn(s): return jekyll_div(s,'danger','Warning', 'exclamation')
339
def jekyll_important(s): return jekyll_div(s,'warning','Important')
340
341