Skip to content

Commit 85c5a7f

Browse files
committed
fixes #405
1 parent 0dac97a commit 85c5a7f

File tree

4 files changed

+336
-63
lines changed

4 files changed

+336
-63
lines changed

fastcore/_nbdev.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@
244244
"gather_attrs": "05_transform.ipynb",
245245
"gather_attr_names": "05_transform.ipynb",
246246
"Pipeline": "05_transform.ipynb",
247+
"docstring": "06_docments.ipynb",
248+
"parse_docstring": "06_docments.ipynb",
247249
"empty": "06_docments.ipynb",
248250
"docments": "06_docments.ipynb",
249251
"test_sig": "07_meta.ipynb",

fastcore/docments.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,36 @@
44
from __future__ import annotations
55

66

7-
__all__ = ['empty', 'docments']
7+
__all__ = ['docstring', 'parse_docstring', 'empty', 'docments']
88

99
# Cell
1010
#nbdev_comment from __future__ import annotations
11+
12+
import re
1113
from tokenize import tokenize,COMMENT
1214
from ast import parse,FunctionDef
1315
from io import BytesIO
1416
from textwrap import dedent
1517
from types import SimpleNamespace
1618
from inspect import getsource,isfunction,isclass,signature,Parameter
17-
from .basics import *
19+
from .utils import *
1820

19-
import re
21+
from fastcore import docscrape
22+
from inspect import isclass
23+
24+
# Cell
25+
def docstring(sym):
26+
"Get docstring for `sym` for functions ad classes"
27+
if isinstance(sym, str): return sym
28+
res = getattr(sym, "__doc__", None)
29+
if not res and isclass(sym): res = nested_attr(sym, "__init__.__doc__")
30+
return res or ""
31+
32+
# Cell
33+
def parse_docstring(sym):
34+
"Parse a numpy-style docstring in `sym`"
35+
docs = docstring(sym)
36+
return AttrDict(**docscrape.NumpyDocString(docstring(sym)))
2037

2138
# Cell
2239
def _parses(s):
@@ -36,7 +53,7 @@ def _clean_comment(s):
3653
def _param_locs(s, returns=True):
3754
"`dict` of parameter line numbers to names"
3855
body = _parses(s).body
39-
if len(body)!=1or not isinstance(body[0], FunctionDef): return None
56+
if len(body)!=1 or not isinstance(body[0], FunctionDef): return None
4057
defn = body[0]
4158
res = {arg.lineno:arg.arg for arg in defn.args.args}
4259
if returns and defn.returns: res[defn.returns.lineno] = 'return'
@@ -59,21 +76,36 @@ def _get_full(anno, name, default, docs):
5976
if anno==empty and default!=empty: anno = type(default)
6077
return AttrDict(docment=docs.get(name), anno=anno, default=default)
6178

79+
# Cell
80+
def _merge_doc(dm, npdoc):
81+
if not npdoc: return dm
82+
if not dm.anno or dm.anno==empty: dm.anno = npdoc.type
83+
if not dm.docment: dm.docment = '\n'.join(npdoc.desc)
84+
return dm
85+
86+
def _merge_docs(dms, npdocs):
87+
npparams = npdocs['Parameters']
88+
params = {nm:_merge_doc(dm,npparams.get(nm,None)) for nm,dm in dms.items()}
89+
if 'return' in dms: params['return'] = _merge_doc(dms['return'], npdocs['Returns'])
90+
return params
91+
6292
# Cell
6393
def docments(s, full=False, returns=True, eval_str=False):
6494
"`dict` of parameter names to 'docment-style' comments in function or string `s`"
95+
nps = parse_docstring(s)
6596
if isclass(s): s = s.__init__ # Constructor for a class
6697
comments = {o.start[0]:_clean_comment(o.string) for o in _tokens(s) if o.type==COMMENT}
6798
parms = _param_locs(s, returns=returns)
6899
docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()}
69-
if not full: return AttrDict(docs)
70100

71101
if isinstance(s,str): s = eval(s)
72102
sig = signature(s)
73103
res = {arg:_get_full(p.annotation, p.name, p.default, docs) for arg,p in sig.parameters.items()}
74104
if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs)
105+
res = _merge_docs(res, nps)
75106
if eval_str:
76107
hints = type_hints(s)
77108
for k,v in res.items():
78109
if k in hints: v['anno'] = hints.get(k)
110+
if not full: res = {k:v['docment'] for k,v in res.items()}
79111
return AttrDict(res)

fastcore/docscrape.py

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
contributors may be used to endorse or promote products derived
2222
from this software without specific prior written permission. """
2323

24+
import textwrap, re, copy
25+
from warnings import warn
26+
from collections import namedtuple
27+
from collections.abc import Mapping
28+
2429
__all__ = ['Parameter', 'NumpyDocString', 'dedent_lines']
2530

2631
Parameter = namedtuple('Parameter', ['name', 'type', 'desc'])
@@ -89,14 +94,17 @@ def __str__(self):
8994

9095
class NumpyDocString(Mapping):
9196
"""Parses a numpydoc string to an abstract representation """
92-
sections = { 'Signature': '', 'Summary': [''], 'Extended': [], 'Parameters': [], 'Returns': [], 'Yields': [], 'Raises': [] }
97+
sections = { 'Summary': [''], 'Extended': [], 'Parameters': [], 'Returns': [] }
9398

9499
def __init__(self, docstring, config=None):
95100
docstring = textwrap.dedent(docstring).split('\n')
96101
self._doc = Reader(docstring)
97102
self._parsed_data = copy.deepcopy(self.sections)
98103
self._parse()
99-
if 'Parameters' in self: self['Parameters'] = {o.name:o for o in self['Parameters']}
104+
self['Parameters'] = {o.name:o for o in self['Parameters']}
105+
if self['Returns']: self['Returns'] = self['Returns'][0]
106+
self['Summary'] = dedent_lines(self['Summary'], split=False)
107+
self['Extended'] = dedent_lines(self['Extended'], split=False)
100108

101109
def __iter__(self): return iter(self._parsed_data)
102110
def __len__(self): return len(self._parsed_data)
@@ -171,7 +179,6 @@ def _parse_summary(self):
171179
summary_str = " ".join([s.strip() for s in summary]).strip()
172180
compiled = re.compile(r'^([\w., ]+=)?\s*[\w\.]+\(.*\)$')
173181
if compiled.match(summary_str):
174-
self['Signature'] = summary_str
175182
if not self._is_at_section(): continue
176183
break
177184

@@ -216,16 +223,12 @@ def _obj(self):
216223

217224
def _error_location(self, msg, error=True):
218225
if self._obj is not None:
219-
# we know where the docs came from:
220-
try: filename = inspect.getsourcefile(self._obj)
221-
except TypeError: filename = None
222226
# Make UserWarning more descriptive via object introspection.
223227
# Skip if introspection fails
224228
name = getattr(self._obj, '__name__', None)
225229
if name is None:
226230
name = getattr(getattr(self._obj, '__class__', None), '__name__', None)
227231
if name is not None: msg += f" in the docstring of {name}"
228-
msg += f" in {filename}." if filename else ""
229232
if error: raise ValueError(msg)
230233
else: warn(msg)
231234

@@ -234,10 +237,6 @@ def _error_location(self, msg, error=True):
234237
def _str_header(self, name, symbol='-'): return [name, len(name)*symbol]
235238
def _str_indent(self, doc, indent=4): return [' '*indent + line for line in doc]
236239

237-
def _str_signature(self):
238-
if self['Signature']: return [self['Signature'].replace('*', r'\*')] + ['']
239-
return ['']
240-
241240
def _str_summary(self):
242241
if self['Summary']: return self['Summary'] + ['']
243242
return []
@@ -259,18 +258,10 @@ def _str_param_list(self, name):
259258
out += ['']
260259
return out
261260

262-
def __str__(self, func_role=''):
263-
out = []
264-
out += self._str_signature()
265-
out += self._str_summary()
266-
out += self._str_extended_summary()
267-
for param_list in ('Parameters', 'Returns', 'Yields', 'Receives', 'Other Parameters', 'Raises', 'Warns'):
268-
out += self._str_param_list(param_list)
269-
for param_list in ('Attributes', 'Methods'): out += self._str_param_list(param_list)
270-
return '\n'.join(out)
271-
272261

273-
def dedent_lines(lines):
262+
def dedent_lines(lines, split=True):
274263
"""Deindent a list of lines maximally"""
275-
return textwrap.dedent("\n".join(lines)).split("\n")
264+
res = textwrap.dedent("\n".join(lines))
265+
if split: res = res.split("\n")
266+
return res
276267

0 commit comments

Comments
 (0)