Skip to content

Commit 29e832c

Browse files
committed
fixes #703
1 parent 7b63121 commit 29e832c

File tree

3 files changed

+787
-2
lines changed

3 files changed

+787
-2
lines changed

fastcore/_modidx.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,31 @@
225225
'fastcore.basics.wrap_class': ('basics.html#wrap_class', 'fastcore/basics.py'),
226226
'fastcore.basics.zip_cycle': ('basics.html#zip_cycle', 'fastcore/basics.py')},
227227
'fastcore.dispatch': {},
228-
'fastcore.docments': { 'fastcore.docments._DocstringExtractor': ('docments.html#_docstringextractor', 'fastcore/docments.py'),
228+
'fastcore.docments': { 'fastcore.docments.DocmentTbl': ('docments.html#docmenttbl', 'fastcore/docments.py'),
229+
'fastcore.docments.DocmentTbl.__eq__': ('docments.html#docmenttbl.__eq__', 'fastcore/docments.py'),
230+
'fastcore.docments.DocmentTbl.__init__': ('docments.html#docmenttbl.__init__', 'fastcore/docments.py'),
231+
'fastcore.docments.DocmentTbl._columns': ('docments.html#docmenttbl._columns', 'fastcore/docments.py'),
232+
'fastcore.docments.DocmentTbl._hdr_list': ('docments.html#docmenttbl._hdr_list', 'fastcore/docments.py'),
233+
'fastcore.docments.DocmentTbl._repr_markdown_': ( 'docments.html#docmenttbl._repr_markdown_',
234+
'fastcore/docments.py'),
235+
'fastcore.docments.DocmentTbl._row': ('docments.html#docmenttbl._row', 'fastcore/docments.py'),
236+
'fastcore.docments.DocmentTbl._row_list': ('docments.html#docmenttbl._row_list', 'fastcore/docments.py'),
237+
'fastcore.docments.DocmentTbl.has_docment': ( 'docments.html#docmenttbl.has_docment',
238+
'fastcore/docments.py'),
239+
'fastcore.docments.DocmentTbl.has_return': ( 'docments.html#docmenttbl.has_return',
240+
'fastcore/docments.py'),
241+
'fastcore.docments.DocmentTbl.hdr_str': ('docments.html#docmenttbl.hdr_str', 'fastcore/docments.py'),
242+
'fastcore.docments.DocmentTbl.params_str': ( 'docments.html#docmenttbl.params_str',
243+
'fastcore/docments.py'),
244+
'fastcore.docments.DocmentTbl.return_str': ( 'docments.html#docmenttbl.return_str',
245+
'fastcore/docments.py'),
246+
'fastcore.docments.MarkdownRenderer': ('docments.html#markdownrenderer', 'fastcore/docments.py'),
247+
'fastcore.docments.MarkdownRenderer._repr_markdown_': ( 'docments.html#markdownrenderer._repr_markdown_',
248+
'fastcore/docments.py'),
249+
'fastcore.docments.ShowDocRenderer': ('docments.html#showdocrenderer', 'fastcore/docments.py'),
250+
'fastcore.docments.ShowDocRenderer.__init__': ( 'docments.html#showdocrenderer.__init__',
251+
'fastcore/docments.py'),
252+
'fastcore.docments._DocstringExtractor': ('docments.html#_docstringextractor', 'fastcore/docments.py'),
229253
'fastcore.docments._DocstringExtractor.__init__': ( 'docments.html#_docstringextractor.__init__',
230254
'fastcore/docments.py'),
231255
'fastcore.docments._DocstringExtractor.visit_ClassDef': ( 'docments.html#_docstringextractor.visit_classdef',
@@ -234,17 +258,31 @@
234258
'fastcore/docments.py'),
235259
'fastcore.docments._DocstringExtractor.visit_Module': ( 'docments.html#_docstringextractor.visit_module',
236260
'fastcore/docments.py'),
261+
'fastcore.docments._bold': ('docments.html#_bold', 'fastcore/docments.py'),
237262
'fastcore.docments._clean_comment': ('docments.html#_clean_comment', 'fastcore/docments.py'),
238263
'fastcore.docments._docments': ('docments.html#_docments', 'fastcore/docments.py'),
264+
'fastcore.docments._docstring': ('docments.html#_docstring', 'fastcore/docments.py'),
265+
'fastcore.docments._escape_markdown': ('docments.html#_escape_markdown', 'fastcore/docments.py'),
266+
'fastcore.docments._ext_link': ('docments.html#_ext_link', 'fastcore/docments.py'),
267+
'fastcore.docments._f_name': ('docments.html#_f_name', 'fastcore/docments.py'),
268+
'fastcore.docments._fmt_anno': ('docments.html#_fmt_anno', 'fastcore/docments.py'),
269+
'fastcore.docments._fmt_sig': ('docments.html#_fmt_sig', 'fastcore/docments.py'),
270+
'fastcore.docments._fullname': ('docments.html#_fullname', 'fastcore/docments.py'),
239271
'fastcore.docments._get_comment': ('docments.html#_get_comment', 'fastcore/docments.py'),
240272
'fastcore.docments._get_full': ('docments.html#_get_full', 'fastcore/docments.py'),
241273
'fastcore.docments._get_params': ('docments.html#_get_params', 'fastcore/docments.py'),
242274
'fastcore.docments._get_property_name': ('docments.html#_get_property_name', 'fastcore/docments.py'),
275+
'fastcore.docments._ital_first': ('docments.html#_ital_first', 'fastcore/docments.py'),
276+
'fastcore.docments._list2row': ('docments.html#_list2row', 'fastcore/docments.py'),
277+
'fastcore.docments._maybe_nm': ('docments.html#_maybe_nm', 'fastcore/docments.py'),
243278
'fastcore.docments._merge_doc': ('docments.html#_merge_doc', 'fastcore/docments.py'),
244279
'fastcore.docments._merge_docs': ('docments.html#_merge_docs', 'fastcore/docments.py'),
280+
'fastcore.docments._non_empty_keys': ('docments.html#_non_empty_keys', 'fastcore/docments.py'),
245281
'fastcore.docments._param_locs': ('docments.html#_param_locs', 'fastcore/docments.py'),
246282
'fastcore.docments._parses': ('docments.html#_parses', 'fastcore/docments.py'),
283+
'fastcore.docments._show_param': ('docments.html#_show_param', 'fastcore/docments.py'),
247284
'fastcore.docments._tokens': ('docments.html#_tokens', 'fastcore/docments.py'),
285+
'fastcore.docments._wrap_sig': ('docments.html#_wrap_sig', 'fastcore/docments.py'),
248286
'fastcore.docments.docments': ('docments.html#docments', 'fastcore/docments.py'),
249287
'fastcore.docments.docstring': ('docments.html#docstring', 'fastcore/docments.py'),
250288
'fastcore.docments.extract_docstrings': ('docments.html#extract_docstrings', 'fastcore/docments.py'),

fastcore/docments.py

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
from .utils import *
1717
from .meta import delegates
1818
from . import docscrape
19+
from textwrap import fill
1920
from inspect import isclass,getdoc
2021

2122
# %% auto 0
2223
__all__ = ['empty', 'docstring', 'parse_docstring', 'isdataclass', 'get_dataclass_source', 'get_source', 'get_name', 'qual_name',
23-
'docments', 'sig2str', 'extract_docstrings']
24+
'docments', 'sig2str', 'extract_docstrings', 'DocmentTbl', 'ShowDocRenderer', 'MarkdownRenderer']
2425

2526
# %% ../nbs/04_docments.ipynb
2627
def docstring(sym):
@@ -247,3 +248,158 @@ def extract_docstrings(code):
247248
extractor = _DocstringExtractor()
248249
extractor.visit(ast.parse(code))
249250
return extractor.docstrings
251+
252+
# %% ../nbs/04_docments.ipynb
253+
def _non_empty_keys(d:dict): return L([k for k,v in d.items() if v != inspect._empty])
254+
def _bold(s): return f'**{s}**' if s.strip() else s
255+
256+
# %% ../nbs/04_docments.ipynb
257+
def _escape_markdown(s):
258+
for c in '|^': s = re.sub(rf'\\?\{c}', rf'\{c}', s)
259+
return s.replace('\n', '<br>')
260+
261+
# %% ../nbs/04_docments.ipynb
262+
def _maybe_nm(o):
263+
if (o == inspect._empty): return ''
264+
else: return o.__name__ if hasattr(o, '__name__') else _escape_markdown(str(o))
265+
266+
# %% ../nbs/04_docments.ipynb
267+
def _list2row(l:list): return '| '+' | '.join([_maybe_nm(o) for o in l]) + ' |'
268+
269+
# %% ../nbs/04_docments.ipynb
270+
class DocmentTbl:
271+
# this is the column order we want these items to appear
272+
_map = {'anno':'Type', 'default':'Default', 'docment':'Details'}
273+
274+
def __init__(self, obj, verbose=True, returns=True):
275+
"Compute the docment table string"
276+
self.verbose = verbose
277+
self.returns = False if isdataclass(obj) else returns
278+
try: self.params = L(signature_ex(obj, eval_str=True).parameters.keys())
279+
except (ValueError,TypeError): self.params=[]
280+
try: _dm = docments(obj, full=True, returns=returns)
281+
except: _dm = {}
282+
if 'self' in _dm: del _dm['self']
283+
for d in _dm.values(): d['docment'] = ifnone(d['docment'], inspect._empty)
284+
self.dm = _dm
285+
286+
@property
287+
def _columns(self):
288+
"Compute the set of fields that have at least one non-empty value so we don't show tables empty columns"
289+
cols = set(flatten(L(self.dm.values()).filter().map(_non_empty_keys)))
290+
candidates = self._map if self.verbose else {'docment': 'Details'}
291+
return {k:v for k,v in candidates.items() if k in cols}
292+
293+
@property
294+
def has_docment(self): return 'docment' in self._columns and self._row_list
295+
296+
@property
297+
def has_return(self): return self.returns and bool(_non_empty_keys(self.dm.get('return', {})))
298+
299+
def _row(self, nm, props):
300+
"unpack data for single row to correspond with column names."
301+
return [nm] + [props[c] for c in self._columns]
302+
303+
@property
304+
def _row_list(self):
305+
"unpack data for all rows."
306+
ordered_params = [(p, self.dm[p]) for p in self.params if p != 'self' and p in self.dm]
307+
return L([self._row(nm, props) for nm,props in ordered_params])
308+
309+
@property
310+
def _hdr_list(self): return [' '] + [_bold(l) for l in L(self._columns.values())]
311+
312+
@property
313+
def hdr_str(self):
314+
"The markdown string for the header portion of the table"
315+
md = _list2row(self._hdr_list)
316+
return md + '\n' + _list2row(['-' * len(l) for l in self._hdr_list])
317+
318+
@property
319+
def params_str(self):
320+
"The markdown string for the parameters portion of the table."
321+
return '\n'.join(self._row_list.map(_list2row))
322+
323+
@property
324+
def return_str(self):
325+
"The markdown string for the returns portion of the table."
326+
return _list2row(['**Returns**']+[_bold(_maybe_nm(self.dm['return'][c])) for c in self._columns])
327+
328+
def _repr_markdown_(self):
329+
if not self.has_docment: return ''
330+
_tbl = [self.hdr_str, self.params_str]
331+
if self.has_return: _tbl.append(self.return_str)
332+
return '\n'.join(_tbl)
333+
334+
def __eq__(self,other): return self.__str__() == str(other).strip()
335+
336+
__str__ = _repr_markdown_
337+
__repr__ = basic_repr()
338+
339+
# %% ../nbs/04_docments.ipynb
340+
def _docstring(sym):
341+
npdoc = parse_docstring(sym)
342+
return '\n\n'.join([npdoc['Summary'], npdoc['Extended']]).strip()
343+
344+
# %% ../nbs/04_docments.ipynb
345+
def _fullname(o):
346+
module,name = getattr(o, "__module__", None),qual_name(o)
347+
return name if module is None or module in ('__main__','builtins') else module + '.' + name
348+
349+
class ShowDocRenderer:
350+
def __init__(self, sym, name:str|None=None, title_level:int=3):
351+
"Show documentation for `sym`"
352+
sym = getattr(sym, '__wrapped__', sym)
353+
sym = getattr(sym, 'fget', None) or getattr(sym, 'fset', None) or sym
354+
store_attr()
355+
self.nm = name or qual_name(sym)
356+
self.isfunc = inspect.isfunction(sym)
357+
try: self.sig = signature_ex(sym, eval_str=True)
358+
except (ValueError,TypeError): self.sig = None
359+
self.docs = _docstring(sym)
360+
self.dm = DocmentTbl(sym)
361+
self.fn = _fullname(sym)
362+
363+
__repr__ = basic_repr()
364+
365+
# %% ../nbs/04_docments.ipynb
366+
def _f_name(o): return f'<function {o.__name__}>' if isinstance(o, FunctionType) else None
367+
def _fmt_anno(o): return inspect.formatannotation(o).strip("'").replace(' ','')
368+
369+
def _show_param(param):
370+
"Like `Parameter.__str__` except removes: quotes in annos, spaces, ids in reprs"
371+
kind,res,anno,default = param.kind,param._name,param._annotation,param._default
372+
kind = '*' if kind==inspect._VAR_POSITIONAL else '**' if kind==inspect._VAR_KEYWORD else ''
373+
res = kind+res
374+
if anno is not inspect._empty: res += f':{_f_name(anno) or _fmt_anno(anno)}'
375+
if default is not inspect._empty: res += f'={_f_name(default) or repr(default)}'
376+
return res
377+
378+
# %% ../nbs/04_docments.ipynb
379+
def _fmt_sig(sig):
380+
if sig is None: return ''
381+
p = {k:v for k,v in sig.parameters.items()}
382+
_params = [_show_param(p[k]) for k in p.keys() if k != 'self']
383+
return "(" + ', '.join(_params) + ")"
384+
385+
def _wrap_sig(s):
386+
"wrap a signature to appear on multiple lines if necessary."
387+
pad = '> ' + ' ' * 5
388+
indent = pad + ' ' * (s.find('(') + 1)
389+
return fill(s, width=80, initial_indent=pad, subsequent_indent=indent)
390+
391+
def _ital_first(s:str):
392+
"Surround first line with * for markdown italics, preserving leading spaces"
393+
return re.sub(r'^(\s*)(.+)', r'\1*\2*', s, count=1)
394+
395+
# %% ../nbs/04_docments.ipynb
396+
def _ext_link(url, txt, xtra=""): return f'[{txt}]({url}){{target="_blank" {xtra}}}'
397+
398+
class MarkdownRenderer(ShowDocRenderer):
399+
"Markdown renderer for `show_doc`"
400+
def _repr_markdown_(self):
401+
doc = _wrap_sig(f"{self.nm} {_fmt_sig(self.sig)}") if self.sig else ''
402+
if self.docs: doc += f"\n\n{_ital_first(self.docs)}"
403+
if self.dm.has_docment: doc += f"\n\n{self.dm}"
404+
return doc
405+
__repr__=__str__=_repr_markdown_

0 commit comments

Comments
 (0)