#hide
#default_exp showdoc
#default_cls_lvl 3
from nbdev.showdoc import show_doc
#export
from nbdev.imports import *
from nbdev.export import *
from nbdev.sync import *
from nbconvert import HTMLExporter
from fastcore.utils import IN_NOTEBOOK
if IN_NOTEBOOK:
from IPython.display import Markdown,display
from IPython.core import page
#export
def is_enum(cls):
"Check if `cls` is an enum or another type of class"
return type(cls) in (enum.Enum, enum.EnumMeta)
e = enum.Enum('e', 'a b')
assert is_enum(e)
assert not is_enum(e.__class__)
assert not is_enum(int)
#export
def is_lib_module(name):
"Test if `name` is a library module."
if name.startswith('_'): return False
try:
_ = importlib.import_module(f'{Config().lib_name}.{name}')
return True
except: return False
assert is_lib_module('export')
assert not is_lib_module('transform')
#export
re_digits_first = re.compile('^[0-9]+[a-z]*_')
#export
def try_external_doc_link(name, packages):
"Try to find a doc link for `name` in `packages`"
for p in packages:
try:
mod = importlib.import_module(f"{p}._nbdev")
try_pack = source_nb(name, is_name=True, mod=mod)
if try_pack:
page = re_digits_first.sub('', try_pack).replace('.ipynb', '')
return f'{mod.doc_url}{page}#{name}'
except ModuleNotFoundError: return None
test_eq(try_external_doc_link('get_name', ['nbdev']), 'https://nbdev.fast.ai/sync#get_name')
#export
def is_doc_name(name):
"Test if `name` corresponds to a notebook that could be converted to a doc page"
for f in Config().path("nbs_path").glob(f'*{name}.ipynb'):
if re_digits_first.sub('', f.name) == f'{name}.ipynb': return True
return False
test_eq(is_doc_name('flaags'),False)
test_eq(is_doc_name('export'),True)
test_eq(is_doc_name('index'),True)
#export
def doc_link(name, include_bt=True):
"Create link to documentation for `name`."
cname = f'`{name}`' if include_bt else name
try:
#Link to modules
if is_lib_module(name) and is_doc_name(name): return f"[{cname}]({Config().doc_baseurl}{name}.html)"
#Link to local functions
try_local = source_nb(name, is_name=True)
if try_local:
page = re_digits_first.sub('', try_local).replace('.ipynb', '')
return f'[{cname}]({Config().doc_baseurl}{page}.html#{name})'
##Custom links
mod = get_nbdev_module()
link = mod.custom_doc_links(name)
return f'[{cname}]({link})' if link is not None else cname
except: return cname
test_eq(doc_link('export'), f'[`export`](/export.html)')
test_eq(doc_link('DocsTestClass'), f'[`DocsTestClass`](/export.html#DocsTestClass)')
test_eq(doc_link('DocsTestClass.test'), f'[`DocsTestClass.test`](/export.html#DocsTestClass.test)')
test_eq(doc_link('Tenso'),'`Tenso`')
test_eq(doc_link('_nbdev'), f'`_nbdev`')
test_eq(doc_link('__main__'), f'`__main__`')
test_eq(doc_link('flags'), '`flags`') # we won't have a flags doc page even though we do have a flags module
#export
_re_backticks = re.compile(r"""
# Catches any link of the form \[`obj`\](old_link) or just `obj`,
# to either update old links or add the link to the docs of obj
\[` # Opening [ and `
([^`]*) # Catching group with anything but a `
`\] # ` then closing ]
(?: # Beginning of non-catching group
\( # Opening (
[^)]* # Anything but a closing )
\) # Closing )
) # End of non-catching group
| # OR
` # Opening `
([^`]*) # Anything but a `
` # Closing `
""", re.VERBOSE)
#export
def add_doc_links(text, elt=None):
"Search for doc links for any item between backticks in `text` and insert them"
def _replace_link(m):
try:
if m.group(2) in inspect.signature(elt).parameters: return f'`{m.group(2)}`'
except: pass
return doc_link(m.group(1) or m.group(2))
return _re_backticks.sub(_replace_link, text)
tst = add_doc_links('This is an example of `DocsTestClass`')
test_eq(tst, "This is an example of [`DocsTestClass`](/export.html#DocsTestClass)")
tst = add_doc_links('This is an example of [`DocsTestClass`](old_link.html)')
test_eq(tst, "This is an example of [`DocsTestClass`](/export.html#DocsTestClass)")
def t(a,export):
"Test func that uses 'export' as a parameter name and has `export` in its doc string"
assert '[`export`](/export.html)' not in add_doc_links(t.__doc__, t)
def t(a,export):
"Test func that uses 'export' as a parameter name and has [`export`]() in its doc string"
assert '[`export`](/export.html)' in add_doc_links(t.__doc__, t)
#hide
# if the name in backticks is not a param name, links will be added/updated like normal
def t(a,exp):
"Test func with `export` in its doc string"
assert '[`export`](/export.html)' in add_doc_links(t.__doc__, t)
def t(a,exp):
"Test func with [`export`]() in its doc string"
assert '[`export`](/export.html)' in add_doc_links(t.__doc__, t)
class T:
def __init__(self, add_doc_links): pass
test_eq(add_doc_links('Lets talk about `add_doc_links`'),
'Lets talk about [`add_doc_links`](/showdoc.html#add_doc_links)')
test_eq(add_doc_links('Lets talk about `add_doc_links`', T), 'Lets talk about `add_doc_links`')
test_eq(add_doc_links('Lets talk about `doc_link`'),
'Lets talk about [`doc_link`](/showdoc.html#doc_link)')
test_eq(add_doc_links('Lets talk about `doc_link`', T),
'Lets talk about [`doc_link`](/showdoc.html#doc_link)')
#export
def _is_type_dispatch(x): return type(x).__name__ == "TypeDispatch"
def _unwrapped_type_dispatch_func(x): return x.first() if _is_type_dispatch(x) else x
def _is_property(x): return type(x)==property
def _has_property_getter(x): return _is_property(x) and hasattr(x, 'fget') and hasattr(x.fget, 'func')
def _property_getter(x): return x.fget.func if _has_property_getter(x) else x
def _unwrapped_func(x):
x = _unwrapped_type_dispatch_func(x)
x = _property_getter(x)
return x
#export
def get_source_link(func):
"Return link to `func` in source code"
func = _unwrapped_func(func)
try: line = inspect.getsourcelines(func)[1]
except Exception: return ''
mod = inspect.getmodule(func)
module = mod.__name__.replace('.', '/') + '.py'
try:
nbdev_mod = importlib.import_module(mod.__package__.split('.')[0] + '._nbdev')
return f"{nbdev_mod.git_url}{module}#L{line}"
except: return f"{module}#L{line}"
#hide
assert get_source_link(DocsTestClass.test).startswith(Config().git_url + 'nbdev/export.py')
#hide
#test
from fastcore.foundation import L
assert get_source_link(L).startswith("https://github.com/fastai/fastcore/tree/master/fastcore/foundation.py")
#export
_re_header = re.compile(r"""
# Catches any header in markdown with the title in group 1
^\s* # Beginning of text followed by any number of whitespace
\#+ # One # or more
\s* # Any number of whitespace
(.*) # Catching group with anything
$ # End of text
""", re.VERBOSE)
#export
def colab_link(path):
"Get a link to the notebook at `path` on Colab"
cfg = Config()
res = f'https://colab.research.google.com/github/{cfg.user}/{cfg.lib_name}/blob/{cfg.branch}/{cfg.path("nbs_path").name}/{path}.ipynb'
display(Markdown(f'[Open `{path}` in Colab]({res})'))
colab_link('02_showdoc')
#export
def get_nb_source_link(func, local=False, is_name=None):
"Return a link to the notebook where `func` is defined."
func = _unwrapped_type_dispatch_func(func)
pref = '' if local else Config().git_url.replace('github.com', 'nbviewer.jupyter.org/github')+ Config().path("nbs_path").name+'/'
is_name = is_name or isinstance(func, str)
src = source_nb(func, is_name=is_name, return_all=True)
if src is None: return '' if is_name else get_source_link(func)
find_name,nb_name = src
nb = read_nb(nb_name)
pat = re.compile(f'^{find_name}\s+=|^(def|class)\s+{find_name}\s*\(', re.MULTILINE)
if len(find_name.split('.')) == 2:
clas,func = find_name.split('.')
pat2 = re.compile(f'@patch\s*\ndef\s+{func}\s*\([^:]*:\s*{clas}\s*(?:,|\))')
else: pat2 = None
for i,cell in enumerate(nb['cells']):
if cell['cell_type'] == 'code':
if re.search(pat, cell['source']): break
if pat2 is not None and re.search(pat2, cell['source']): break
if re.search(pat, cell['source']) is None and (pat2 is not None and re.search(pat2, cell['source']) is None):
return '' if is_name else get_function_source(func)
header_pat = re.compile(r'^\s*#+\s*(.*)$')
while i >= 0:
cell = nb['cells'][i]
if cell['cell_type'] == 'markdown' and _re_header.search(cell['source']):
title = _re_header.search(cell['source']).groups()[0]
anchor = '-'.join([s for s in title.split(' ') if len(s) > 0])
return f'{pref}{nb_name}#{anchor}'
i-=1
return f'{pref}{nb_name}'
test_eq(get_nb_source_link(DocsTestClass.test), get_nb_source_link(DocsTestClass))
test_eq(get_nb_source_link('DocsTestClass'), get_nb_source_link(DocsTestClass))
test_eq(get_nb_source_link(check_re, local=True), f'00_export.ipynb#Finding-patterns')
#export
def nb_source_link(func, is_name=None, disp=True, local=True):
"Show a relative link to the notebook where `func` is defined"
is_name = is_name or isinstance(func, str)
func_name = func if is_name else qual_name(func)
link = get_nb_source_link(func, local=local, is_name=is_name)
text = func_name if local else f'{func_name} (GitHub)'
if disp: display(Markdown(f'[{text}]({link})'))
else: return link
test_eq(nb_source_link(check_re, disp=False), f'00_export.ipynb#Finding-patterns')
test_eq(nb_source_link('check_re', disp=False), f'00_export.ipynb#Finding-patterns')
nb_source_link(check_re, local=False)
#export
from fastcore.script import Param
#export
def type_repr(t):
"Representation of type `t` (in a type annotation)"
if (isinstance(t, Param)): return f'"{t.help}"'
if getattr(t, '__args__', None):
args = t.__args__
if len(args)==2 and args[1] == type(None):
return f'`Optional`\[{type_repr(args[0])}\]'
reprs = ', '.join([type_repr(o) for o in args])
return f'{doc_link(get_name(t))}\[{reprs}\]'
else: return doc_link(get_name(t))
tst = type_repr(Optional[DocsTestClass])
test_eq(tst, '`Optional`\\[[`DocsTestClass`](/export.html#DocsTestClass)\\]')
tst = type_repr(Union[int, float])
test_eq(tst, '`Union`\\[`int`, `float`\\]')
test_eq(type_repr(Param("description")), '"description"')
#export
_arg_prefixes = {inspect._VAR_POSITIONAL: '\*', inspect._VAR_KEYWORD:'\*\*'}
def format_param(p):
"Formats function param to `param:Type=val` with font weights: param=bold, val=italic"
arg_prefix = _arg_prefixes.get(p.kind, '') # asterisk prefix for *args and **kwargs
res = f"**{arg_prefix}`{p.name}`**"
if hasattr(p, 'annotation') and p.annotation != p.empty: res += f':{type_repr(p.annotation)}'
if p.default != p.empty:
default = getattr(p.default, 'func', p.default) #For partials
if hasattr(default,'__name__'): default = getattr(default, '__name__')
else: default = repr(default)
if is_enum(default.__class__): #Enum have a crappy repr
res += f'=*`{default.__class__.__name__}.{default.name}`*'
else: res += f'=*`{default}`*'
return res
sig = inspect.signature(notebook2script)
params = [format_param(p) for _,p in sig.parameters.items()]
test_eq(params, ['**`fname`**=*`None`*', '**`silent`**=*`False`*', '**`to_dict`**=*`False`*', '**`bare`**=*`False`*', '**`recursive`**=*`None`*'])
#export
def _format_enum_doc(enum, full_name):
"Formatted `enum` definition to show in documentation"
vals = ', '.join(enum.__members__.keys())
return f'{full_name}
',f'Enum
= [{vals}]'
#hide
tst = _format_enum_doc(e, 'e')
test_eq(tst, ('e
', 'Enum
= [a, b]'))
#export
def _escape_chars(s):
return s.replace('_', '\_')
def _format_func_doc(func, full_name=None):
"Formatted `func` definition to show in documentation"
try:
sig = inspect.signature(func)
fmt_params = [format_param(param) for name,param
in sig.parameters.items() if name not in ('self','cls')]
except: fmt_params = []
name = f'{full_name or func.__name__}
'
arg_str = f"({', '.join(fmt_params)})"
f_name = f"class
{name}" if inspect.isclass(func) else name
return f'{f_name}',f'{name}{arg_str}'
#hide
test_eq(_format_func_doc(notebook2script), ('notebook2script
',
'notebook2script
(**`fname`**=*`None`*, **`silent`**=*`False`*, **`to_dict`**=*`False`*, **`bare`**=*`False`*, **`recursive`**=*`None`*)'))
#export
def _format_cls_doc(cls, full_name):
"Formatted `cls` definition to show in documentation"
parent_class = inspect.getclasstree([cls])[-1][0][1][0]
name,args = _format_func_doc(cls, full_name)
if parent_class != object: args += f' :: {doc_link(get_name(parent_class))}'
return name,args
#hide
test_eq(_format_cls_doc(DocsTestClass, 'DocsTestClass'), ('class
DocsTestClass
',
'DocsTestClass
()'))
#export
def show_doc(elt, doc_string=True, name=None, title_level=None, disp=True, default_cls_level=2):
"Show documentation for element `elt`. Supported types: class, function, and enum."
elt = getattr(elt, '__func__', elt)
qname = name or qual_name(elt)
if inspect.isclass(elt):
if is_enum(elt): name,args = _format_enum_doc(elt, qname)
else: name,args = _format_cls_doc (elt, qname)
elif callable(elt): name,args = _format_func_doc(elt, qname)
else: name,args = f"{qname}
", ''
link = get_source_link(elt)
source_link = f'[source]'
title_level = title_level or (default_cls_level if inspect.isclass(elt) else 4)
doc = f''
doc += f'\n\n> {args}\n\n' if len(args) > 0 else '\n\n'
if doc_string and inspect.getdoc(elt):
s = inspect.getdoc(elt)
# show_doc is used by doc so should not rely on Config
try: monospace = (Config().get('monospace_docstrings') == 'True')
except: monospace = False
# doc links don't work inside markdown pre/code blocks
s = f'```\n{s}\n```' if monospace else add_doc_links(s, elt)
doc += s
if disp: display(Markdown(doc))
else: return doc
show_doc(notebook2script)
#hide
def t(a,exp):
"Test func with `export` in its doc string"
assert '[`export`](/export.html)' in show_doc(t, disp=False)
def t(a,export):
"Test func that uses 'export' as a parameter name and has `export` in its doc string"
assert '[`export`](/export.html)' not in show_doc(t, disp=False)
#hide
show_doc(DocsTestClass)
#hide
show_doc(DocsTestClass.test)
#hide
show_doc(check_re)
#hide
show_doc(e)
#hide
def test_func_with_args_and_links(foo, bar):
"""
Doc link: `show_doc`.
Args:
foo: foo
bar: bar
Returns:
None
"""
pass
show_doc(test_func_with_args_and_links)
Config()["monospace_docstrings"] = "True"
show_doc(test_func_with_args_and_links)
Config()["monospace_docstrings"] = "False"
#export
def md2html(md):
"Convert markdown `md` to HTML code"
import nbconvert
if nbconvert.__version__ < '5.5.0': return HTMLExporter().markdown2html(md)
else: return HTMLExporter().markdown2html(collections.defaultdict(lambda: collections.defaultdict(dict)), md)
#export
def get_doc_link(func):
mod = inspect.getmodule(func)
module = mod.__name__.replace('.', '/') + '.py'
try:
nbdev_mod = importlib.import_module(mod.__package__.split('.')[0] + '._nbdev')
try_pack = source_nb(func, mod=nbdev_mod)
if try_pack:
page = '.'.join(try_pack.partition('_')[-1:]).replace('.ipynb', '')
return f'{nbdev_mod.doc_url}{page}#{qual_name(func)}'
except: return None
test_eq(get_doc_link(notebook2script), 'https://nbdev.fast.ai/export#notebook2script')
#test
from nbdev.sync import get_name
test_eq(get_doc_link(get_name), 'https://nbdev.fast.ai/sync#get_name')
#export
def doc(elt):
"Show `show_doc` info in preview window when used in a notebook"
md = show_doc(elt, disp=False)
doc_link = get_doc_link(elt)
if doc_link is not None:
md += f'\n\nShow in docs'
output = md2html(md)
if IN_COLAB: get_ipython().run_cell_magic(u'html', u'', output)
else:
try: page.page({'text/html': output})
except: display(Markdown(md))
#hide
from nbdev.export import notebook2script
notebook2script()