|
16 | 16 | from .utils import * |
17 | 17 | from .meta import delegates |
18 | 18 | from . import docscrape |
| 19 | +from textwrap import fill |
19 | 20 | from inspect import isclass,getdoc |
20 | 21 |
|
21 | 22 | # %% auto 0 |
22 | 23 | __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'] |
24 | 25 |
|
25 | 26 | # %% ../nbs/04_docments.ipynb |
26 | 27 | def docstring(sym): |
@@ -247,3 +248,158 @@ def extract_docstrings(code): |
247 | 248 | extractor = _DocstringExtractor() |
248 | 249 | extractor.visit(ast.parse(code)) |
249 | 250 | 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