#|default_exp showdoc
Display symbol documentation in notebook and website
#|export
from __future__ import annotations
from nbdev.doclinks import *
from nbdev.read import get_config
from fastcore.dispatch import TypeDispatch
from fastcore.docments import *
from fastcore.utils import *
from importlib import import_module
import inspect, sys
from inspect import Signature, Parameter
from collections import OrderedDict
from dataclasses import dataclass, is_dataclass
from textwrap import fill
#|hide
from fastcore.test import *
Render nicely formatted tables that shows docments
for any function or method.
#|export
def _non_empty_keys(d:dict): return L([k for k,v in d.items() if v != inspect._empty])
def _bold(s): return f'**{s}**' if s.strip() else s
#|export
def _maybe_nm(o):
if (o == inspect._empty): return ''
else: return o.__name__ if hasattr(o, '__name__') else str(o)
test_eq(_maybe_nm(list), 'list')
test_eq(_maybe_nm('fastai'), 'fastai')
#|export
def _list2row(l:list): return '| '+' | '.join([_maybe_nm(o) for o in l]) + ' |'
#|hide
test_eq(_list2row(['Hamel', 'Jeremy']), '| Hamel | Jeremy |')
test_eq(_list2row([inspect._empty, bool, 'foo']), '| | bool | foo |')
#|export
class DocmentTbl:
# this is the column order we want these items to appear
_map = OrderedDict({'anno':'Type', 'default':'Default', 'docment':'Details'})
def __init__(self, obj, verbose=True, returns=True):
"Compute the docment table string"
self.verbose = verbose
self.returns = False if isdataclass(obj) else returns
if isinstance_str(obj, 'property'): self.params = []
else:
try: self.params = L(signature_ex(obj, eval_str=True).parameters.keys())
except (ValueError,TypeError): self.params=[]
try: _dm = docments(obj, full=True, returns=returns)
except: _dm = {}
if 'self' in _dm: del _dm['self']
for d in _dm.values(): d['docment'] = ifnone(d['docment'], inspect._empty)
self.dm = _dm
@property
def _columns(self):
"Compute the set of fields that have at least one non-empty value so we don't show tables empty columns"
cols = set(flatten(L(self.dm.values()).filter().map(_non_empty_keys)))
candidates = self._map if self.verbose else {'docment': 'Details'}
return OrderedDict({k:v for k,v in candidates.items() if k in cols})
@property
def has_docment(self): return 'docment' in self._columns and self._row_list
@property
def has_return(self): return self.returns and bool(_non_empty_keys(self.dm.get('return', {})))
def _row(self, nm, props):
"unpack data for single row to correspond with column names."
return [nm] + [props[c] for c in self._columns]
@property
def _row_list(self):
"unpack data for all rows."
ordered_params = [(p, self.dm[p]) for p in self.params if p != 'self' and p in self.dm]
return L([self._row(nm, props) for nm,props in ordered_params])
@property
def _hdr_list(self): return [' '] + [_bold(l) for l in L(self._columns.values())]
@property
def hdr_str(self):
"The markdown string for the header portion of the table"
md = _list2row(self._hdr_list)
return md + '\n' + _list2row(['-' * len(l) for l in self._hdr_list])
@property
def params_str(self):
"The markdown string for the parameters portion of the table."
return '\n'.join(self._row_list.map(_list2row))
@property
def return_str(self):
"The markdown string for the returns portion of the table."
return _list2row(['**Returns**']+[_bold(_maybe_nm(self.dm['return'][c])) for c in self._columns])
def _repr_markdown_(self):
if not self.has_docment: return ''
_tbl = [self.hdr_str, self.params_str]
if self.has_return: _tbl.append(self.return_str)
return '\n'.join(_tbl)
def __eq__(self,other): return self.__str__() == str(other).strip()
def __str__(self): return self._repr_markdown_()
DocmentTbl
can render a markdown table showing docments
if appropriate. This is an example of how a docments
table will render for a function:
def _f(a, # description of param a
b=True, # description of param b
c:str=None
) -> int: ...
_dm = DocmentTbl(_f)
_dm
Type | Default | Details | |
---|---|---|---|
a | description of param a | ||
b | bool | True | description of param b |
c | str | None | |
Returns | int |
#|hide
_exp_res="""
| | **Type** | **Default** | **Details** |
| -- | -------- | ----------- | ----------- |
| a | | | description of param a |
| b | bool | True | description of param b |
| c | str | None | |
| **Returns** | **int** | | |
"""
test_eq(_dm, _exp_res)
If one column in the table has no information, for example because there are no default values, that column will not be shown. In the below example, the Default column, will not be shown. Additionally, if the return of the function is not annotated the Returns row will not be rendered:
def _f(a,
b, #param b
c #param c
): ...
_dm2 = DocmentTbl(_f)
_dm2
Details | |
---|---|
a | |
b | param b |
c | param c |
#|hide
_exp_res2 = """
| | **Details** |
| -- | ----------- |
| a | |
| b | param b |
| c | param c |
"""
test_eq(_dm2, _exp_res2)
DocmentTbl
also works on classes. By default, the __init__
will be rendered:
class _Test:
def __init__(self,
a, # description of param a
b=True, # description of param b
c:str=None):
...
def foo(self,
c:int, # description of param c
d=True, # description of param d
):
...
DocmentTbl(_Test)
Type | Default | Details | |
---|---|---|---|
a | description of param a | ||
b | bool | True | description of param b |
c | str | None |
You can also pass a method to be rendered as well:
DocmentTbl(_Test.foo)
Type | Default | Details | |
---|---|---|---|
c | int | description of param c | |
d | bool | True | description of param d |
#|hide
_exp_res3 = """
| | **Type** | **Default** | **Details** |
| -- | -------- | ----------- | ----------- |
| c | int | | description of param c |
| d | bool | True | description of param d |
"""
test_eq(DocmentTbl(_Test.foo), _exp_res3)
Render the signature as well as the docments
to show complete documentation for an object.
#|export
class ShowDocRenderer:
def __init__(self, sym, disp:bool=True, name:str|None=None, title_level:int|None=None):
"Show documentation for `sym`"
store_attr()
self.nm = name or qual_name(sym)
self.isfunc = inspect.isfunction(sym)
self.isprop = isinstance_str(sym, 'property')
if self.isprop: self.sig = None
else:
try: self.sig = signature_ex(sym, eval_str=True)
except (ValueError,TypeError): self.sig = None
self.docs = docstring(sym)
self.dm = DocmentTbl(sym)
#|export
def _fmt_sig(sig):
if sig is None: return ''
p = {k:v for k,v in sig.parameters.items()}
_params = [str(p[k]).replace(' ','') for k in p.keys() if k != 'self']
return "(" + ', '.join(_params) + ")"
def _wrap_sig(s):
"wrap a signature to appear on multiple lines if necessary."
pad = '> ' + ' ' * 5
indent = pad + ' ' * (s.find('(') + 1)
return fill(s, width=80, initial_indent=pad, subsequent_indent=indent)
#|hide
def _long_f(a_param, b_param=True, c_param:str='Some quite long value', d:int=2, e:bool=False):
"A docstring"
...
_res = "> (a_param, b_param=True, c_param:str='Somequitelongvalue', d:int=2,\n> e:bool=False)"
_sig = _fmt_sig(signature_ex(_long_f, eval_str=True))
test_eq(_wrap_sig(_sig), _res)
#|export
class BasicMarkdownRenderer(ShowDocRenderer):
def _repr_markdown_(self):
doc = '---\n\n'
if self.isfunc or self.isprop: doc += '#'
sig = _wrap_sig(f"{self.nm} {_fmt_sig(self.sig)}") if self.sig else ''
doc += f'### {self.nm}\n\n{sig}'
if self.docs: doc += f"\n\n{self.docs.splitlines()[0]}"
if self.dm.has_docment: doc += f"\n\n{self.dm}"
return doc
#|export
def show_doc(sym, disp=True, renderer=None, name:str|None=None, title_level:int|None=None):
if renderer is None: renderer = get_config().get('renderer', None)
if renderer is None: renderer=BasicMarkdownRenderer
elif isinstance(renderer,str):
p,m = renderer.rsplit('.', 1)
renderer = getattr(import_module(p), m)
if isinstance(sym, TypeDispatch): pass
else:return renderer(sym or show_doc, disp=disp, name=name, title_level=title_level)
You can use show_doc
to document apis of functions, classes or methods:
def f(x:int=1):
"func docstring"
...
show_doc(f)
:::{.callout-warning}
If you are using a version of python that is older than 3.10, type hints might be rendered as strings when running show_doc
. We recommend upgrading to python 3.10 locally if possible so you can preview docs without this artifact. We have set the version of python to be 3.10 .github/workflows/deploy.yaml
for this reason as well.
:::
def f(x:int=1 # the parameter x
) -> None: # this function doesn't return anything
"func docstring"
...
show_doc(f)
f (x:int=1)
func docstring
Type | Default | Details | |
---|---|---|---|
x | int | 1 | the parameter x |
Returns | None | this function doesn't return anything |
if you have numpy docstrings instead of docments
, show_doc
will attempt to parse and render those just like docments
:
def f(x=1):
"""
func docstring in the numpy style.
Parameters
----------
x : int
the parameter x
Returns
-------
None
this function doesn't return anything
"""
...
show_doc(f)
f (x=1)
func docstring in the numpy style.
Type | Default | Details | |
---|---|---|---|
x | int | 1 | the parameter x |
Returns | None | this function doesn't return anything |
:::{.callout-warning}
Numpy docstring formatting is very strict. If your docstrings do not strictly adhere to the numpy format, it will not be parsed properly and information about parameters and return values may not properly be rendered in the table below the signature. Where possible, we recommend using docments
to annonate your function instead.
:::
show_doc
on Classes¶show_doc works on Classes, too including when you use @patch
:
class Foo:
def __init__(self, d:str,e:int):
"This is the docstring for the __init__ method"
...
@property
def some_prop(self):
"This is a class property."
return 'foo property'
show_doc(Foo)
You can define methods for the class Foo
with @patch
which is convenient in allowing you to break up code for documentation in notebooks:
@patch
def a_method(self:Foo,
a:list, # param a
b:dict,c):
"This is a method"
...
_res = show_doc(Foo.a_method)
_res
Foo.a_method (a:list, b:dict, c)
This is a method
Type | Details | |
---|---|---|
a | list | param a |
b | dict | |
c |
#|hide
# signature and docment should show properly when using @patch
assert '(a:list, b:dict, c)' in _res._repr_markdown_()
assert 'param a' in _res._repr_markdown_()
Class properties also work with showdoc:
_res = show_doc(Foo.some_prop)
_res
#|hide
test_eq(_res._repr_markdown_(), '---\n\n#### Foo.some_prop\n\n\n\nThis is a class property.')
#|export
class BasicHtmlRenderer(ShowDocRenderer):
def _repr_html_(self):
doc = '<hr/>\n'
lvl = 4 if self.isfunc else 3
doc += f'<h{lvl}>{self.nm}</h{lvl}>\n<blockquote><code>{self.nm}{self.sig}</code></blockquote>'
if self.docs: doc += f"<p>{self.docs}</p>"
return doc
class F:
"class docstring"
def __init__(self, x:int=1): ...
@classmethod
def class_method(cls,
foo:str, # docment for parameter foo
bar:int):
"This is a class method."
pass
def regular_method(self,
baz:bool=True, # docment for parameter baz
):
"This is a regular method"
pass
show_doc(F, renderer=BasicHtmlRenderer)
F(x: 'int' = 1)
class docstring
_res = show_doc(F.class_method)
_res
F.class_method (foo:str, bar:int)
This is a class method.
Type | Details | |
---|---|---|
foo | str | docment for parameter foo |
bar | int |
#|hide
# There should be docments for a class method
assert 'docment for parameter foo' in _res._repr_markdown_(), 'No docment found for class method'
show_doc(F.regular_method)
F.regular_method (baz:bool=True)
This is a regular method
Type | Default | Details | |
---|---|---|---|
baz | bool | True | docment for parameter baz |
#|export
def showdoc_nm(tree):
"Get the fully qualified name for showdoc."
return ifnone(get_patch_name(tree), tree.name)
#|hide
import ast
code="""
@bar
@patch
@foo
def a_method(self:Foo, a:list,b:dict,c):
"This is a method"
...
"""
code2="""
@bar
@foo
def a_method(self:Foo, a:list,b:dict,c):
"This is a method"
...
"""
_tree = ast.parse(code).body[0]
test_eq(showdoc_nm(_tree), 'Foo.a_method')
_tree2 = ast.parse(code2).body[0]
test_eq(showdoc_nm(_tree2), 'a_method')
#|export
def colab_link(path):
"Get a link to the notebook at `path` on Colab"
from IPython.display import Markdown
cfg = get_config()
pre = 'https://colab.research.google.com/github/'
res = f'{pre}{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('index')
#|hide
e = enum.Enum('e', 'a b')
test_eq(show_doc(e)._repr_markdown_(), '---\n\n### e\n\n> e (value, names=None, module=None, qualname=None, type=None, start=1)\n\nAn enumeration.')
#|hide
# disabled due to py310 anno issue
# from typing import Sequence
# @dataclass
# class A:
# "Test dataclass"
# a:int = 2 # First
# b:Sequence[int] = (1,2,3) # Second
# def test(self,
# c:int = 1, # it's a test
# d:str = 'test' # it's a second test
# )->str: # it's a return string
# return d
# test_eq(show_doc(A)._repr_markdown_(), '---\n\n### A\n\n> A (a:int=2, b:Sequence[int]=(1,2,3))\n\nTest dataclass')
# test_eq(show_doc(A.test)._repr_markdown_(),
# "---\n\n#### A.test\n\n> A.test (c:int=1, d:str='test')\n\n| | **Type** | **Default** | **Details** |\n| -- | -------- | ----------- | ----------- |\n| c | int | 1 | it's a test |\n| d | str | test | it's a second test |\n| **Returns** | **str** | | **it's a return string** |")
#|hide
from fastcore.dispatch import typedispatch
@typedispatch
def _typ_test(
a:list, # A list
b:str, # A second integer
) -> float:
"Perform op"
return a.extend(b)
@typedispatch
def _typ_test(
a:str, # An integer
b:str # A str
) -> float:
"Perform op"
return str(a) + b
test_eq(show_doc(_typ_test), None) # show_doc ignores typedispatch at the moment
#|hide
#|eval: false
from nbdev.doclinks import nbdev_export
nbdev_export()