1+ import ast
2+ import re
3+ from typing import List , Tuple
4+ from collections import OrderedDict
5+
6+ # Define some regex to help with parsing the docstrings.
7+ _PARAM_LINE_RE = re .compile (r"^\s*:param\s+([A-Za-z_]\w*)\s*:\s*(.*)$" )
8+ _RET_LINE_RE = re .compile (r"^\s*:returns?\s*:\s*(.*)$" )
9+ _STOP_BLOCK_RE = re .compile (r"^\s*:(param|type|returns?|rtype)\b" )
10+
11+
12+ class Docstring :
13+
14+ def create_from_overloads (
15+ self , overload_fns : List [ast .FunctionDef ], class_name : str ,
16+ method_name : str ) -> str :
17+ intro_blocks : List [List [str ]] = []
18+ seen_intro_keys = set ()
19+ params_union : "OrderedDict[str, str]" = OrderedDict ()
20+ returns_set : List [str ] = []
21+
22+ # Iterate through each overload.
23+ for fn in overload_fns :
24+ # Get the docstring of the overload.
25+ doc = (ast .get_docstring (fn ) or "" ).strip ()
26+ # If there is no docstring in the overload, just continue.
27+ if not doc :
28+ continue
29+ # Decompose the docstring into its 3 parts.
30+ intro , param_lines , return_lines = self ._split_doc (doc )
31+ # If there is an introduction in the docstring...
32+ if intro :
33+ # Make the introduction look nice.
34+ key = self ._normalize_block (intro )
35+ # Only save unique introduction text blocks.
36+ if key and key not in seen_intro_keys :
37+ seen_intro_keys .add (key )
38+ intro_blocks .append (intro )
39+ # Iterate through each parameter description in the
40+ # docstring.
41+ for pl in param_lines :
42+ m = _PARAM_LINE_RE .match (pl )
43+ if not m :
44+ continue
45+ # Get the parameter name and description.
46+ name , desc = m .group (1 ), m .group (2 ).strip ()
47+ if name not in params_union :
48+ params_union [name ] = desc
49+ elif not params_union [name ] and desc :
50+ params_union [name ] = desc
51+ # Iterate through each return description in the docstring.
52+ for rl in return_lines :
53+ m = _RET_LINE_RE .match (rl )
54+ if not m :
55+ continue
56+ desc = m .group (1 ).strip ()
57+ if desc and desc not in returns_set :
58+ returns_set .append (desc )
59+ # Create an empty list to store the lines we'll write.
60+ out_lines : List [str ] = []
61+ # If there was only one unique description we found, use it.
62+ if len (intro_blocks ) == 1 :
63+ out_lines .extend (intro_blocks [0 ])
64+ # Otherwise, just list all of the unique descriptions.
65+ elif len (intro_blocks ) > 1 :
66+ out_lines .append ("Signature descriptions:" )
67+ out_lines .append ("" )
68+ for i , block in enumerate (intro_blocks ):
69+ first = True
70+ for ln in block :
71+ if first :
72+ out_lines .append (f"- { ln } " )
73+ first = False
74+ else :
75+ out_lines .append (f" { ln } " )
76+ if i < len (intro_blocks ) - 1 :
77+ out_lines .append ("" )
78+
79+ # Write the parameter descriptions.
80+ if params_union :
81+ if out_lines :
82+ out_lines .append ("" )
83+ for name , desc in params_union .items ():
84+ out_lines .append (f":param { name } : { desc } " )
85+
86+ # Write the return description.
87+ if returns_set :
88+ # If there was only one, use it.
89+ if len (returns_set ) == 1 :
90+ if out_lines :
91+ out_lines .append ("" )
92+ out_lines .append (f":returns: { returns_set [0 ]} " )
93+ # Otherwise, list out all the cases.
94+ else :
95+ combined = "; " .join (f"Case { i + 1 } : [{ desc } ]" for i , desc in enumerate (returns_set ))
96+ if out_lines :
97+ out_lines .append ("" )
98+ out_lines .append (f":returns: Depends on the signature used. { combined } " )
99+
100+ return "\n " .join (out_lines ).rstrip ()
101+
102+ def _split_doc (self , doc : str ) -> Tuple [List [str ], List [str ], List [str ]]:
103+ # Split the docstring into lines.
104+ lines = [ln .rstrip () for ln in doc .splitlines ()]
105+ # Create empty lists to store the intro, param descriptions, and
106+ # return description.
107+ intro : List [str ] = []
108+ param_lines : List [str ] = []
109+ return_lines : List [str ] = []
110+ in_intro = True
111+ # Iterate through each line in the docstring.
112+ for line in lines :
113+ # Collect the intro description in the docstring.
114+ if in_intro :
115+ if not line .strip ():
116+ in_intro = False
117+ continue
118+ if _STOP_BLOCK_RE .match (line ):
119+ in_intro = False
120+ else :
121+ intro .append (line )
122+ continue
123+ # Collect the :param: lines in the doctring.
124+ if _PARAM_LINE_RE .match (line ):
125+ param_lines .append (line .strip ())
126+ continue
127+ # Collect the :return: line in the docstring.
128+ if _RET_LINE_RE .match (line ):
129+ return_lines .append (line .strip ())
130+ return intro , param_lines , return_lines
131+
132+ def _normalize_block (self , lines : List [str ]) -> str :
133+ text = "\n " .join (lines ).strip ()
134+ return "\n " .join (" " .join (l .split ()) for l in text .splitlines ()).strip ()
0 commit comments