Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
singlestore-labs
GitHub Repository: singlestore-labs/singlestoredb-python
Path: blob/main/singlestoredb/docstring/epydoc.py
469 views
1
"""Epyoc-style docstring parsing.
2
3
.. seealso:: http://epydoc.sourceforge.net/manual-fields.html
4
"""
5
import inspect
6
import re
7
import typing as T
8
9
from .common import Docstring
10
from .common import DocstringMeta
11
from .common import DocstringParam
12
from .common import DocstringRaises
13
from .common import DocstringReturns
14
from .common import DocstringStyle
15
from .common import ParseError
16
from .common import RenderingStyle
17
18
19
def _clean_str(string: str) -> T.Optional[str]:
20
string = string.strip()
21
if len(string) > 0:
22
return string
23
return None
24
25
26
def parse(text: T.Optional[str]) -> Docstring:
27
"""Parse the epydoc-style docstring into its components.
28
29
:returns: parsed docstring
30
"""
31
ret = Docstring(style=DocstringStyle.EPYDOC)
32
if not text:
33
return ret
34
35
text = inspect.cleandoc(text)
36
match = re.search('^@', text, flags=re.M)
37
if match:
38
desc_chunk = text[:match.start()]
39
meta_chunk = text[match.start():]
40
else:
41
desc_chunk = text
42
meta_chunk = ''
43
44
parts = desc_chunk.split('\n', 1)
45
ret.short_description = parts[0] or None
46
if len(parts) > 1:
47
long_desc_chunk = parts[1] or ''
48
ret.blank_after_short_description = long_desc_chunk.startswith('\n')
49
ret.blank_after_long_description = long_desc_chunk.endswith('\n\n')
50
ret.long_description = long_desc_chunk.strip() or None
51
52
param_pattern = re.compile(
53
r'(param|keyword|type)(\s+[_A-z][_A-z0-9]*\??):',
54
)
55
raise_pattern = re.compile(r'(raise)(\s+[_A-z][_A-z0-9]*\??)?:')
56
return_pattern = re.compile(r'(return|rtype|yield|ytype):')
57
meta_pattern = re.compile(
58
r'([_A-z][_A-z0-9]+)((\s+[_A-z][_A-z0-9]*\??)*):',
59
)
60
61
# tokenize
62
stream: T.List[T.Tuple[str, str, T.List[str], str]] = []
63
for match in re.finditer(
64
r'(^@.*?)(?=^@|\Z)', meta_chunk, flags=re.S | re.M,
65
):
66
chunk = match.group(0)
67
if not chunk:
68
continue
69
70
param_match = re.search(param_pattern, chunk)
71
raise_match = re.search(raise_pattern, chunk)
72
return_match = re.search(return_pattern, chunk)
73
meta_match = re.search(meta_pattern, chunk)
74
75
match = param_match or raise_match or return_match or meta_match
76
if not match:
77
raise ParseError(f'Error parsing meta information near "{chunk}".')
78
79
desc_chunk = chunk[match.end():]
80
if param_match:
81
base = 'param'
82
key = match.group(1)
83
args = [match.group(2).strip()]
84
elif raise_match:
85
base = 'raise'
86
key = match.group(1)
87
args = [] if match.group(2) is None else [match.group(2).strip()]
88
elif return_match:
89
base = 'return'
90
key = match.group(1)
91
args = []
92
else:
93
base = 'meta'
94
key = match.group(1)
95
token = _clean_str(match.group(2).strip())
96
args = [] if token is None else re.split(r'\s+', token)
97
98
# Make sure we didn't match some existing keyword in an incorrect
99
# way here:
100
if key in [
101
'param',
102
'keyword',
103
'type',
104
'return',
105
'rtype',
106
'yield',
107
'ytype',
108
]:
109
raise ParseError(
110
f'Error parsing meta information near "{chunk}".',
111
)
112
113
desc = desc_chunk.strip()
114
if '\n' in desc:
115
first_line, rest = desc.split('\n', 1)
116
desc = first_line + '\n' + inspect.cleandoc(rest)
117
stream.append((base, key, args, desc))
118
119
# Combine type_name, arg_name, and description information
120
params: T.Dict[str, T.Dict[str, T.Any]] = {}
121
for base, key, args, desc in stream:
122
if base not in ['param', 'return']:
123
continue # nothing to do
124
125
(arg_name,) = args or ('return',)
126
info = params.setdefault(arg_name, {})
127
info_key = 'type_name' if 'type' in key else 'description'
128
info[info_key] = desc
129
130
if base == 'return':
131
is_generator = key in {'ytype', 'yield'}
132
if info.setdefault('is_generator', is_generator) != is_generator:
133
raise ParseError(
134
f'Error parsing meta information for "{arg_name}".',
135
)
136
137
meta_item: T.Union[DocstringParam, DocstringReturns, DocstringRaises, DocstringMeta]
138
is_done: T.Dict[str, bool] = {}
139
for base, key, args, desc in stream:
140
if base == 'param' and not is_done.get(args[0], False):
141
(arg_name,) = args
142
info = params[arg_name]
143
type_name = info.get('type_name')
144
145
if type_name and type_name.endswith('?'):
146
is_optional = True
147
type_name = type_name[:-1]
148
else:
149
is_optional = False
150
151
match = re.match(r'.*defaults to (.+)', desc, flags=re.DOTALL)
152
default = match.group(1).rstrip('.') if match else None
153
154
meta_item = DocstringParam(
155
args=[key, arg_name],
156
description=info.get('description'),
157
arg_name=arg_name,
158
type_name=type_name,
159
is_optional=is_optional,
160
default=default,
161
)
162
is_done[arg_name] = True
163
elif base == 'return' and not is_done.get('return', False):
164
info = params['return']
165
meta_item = DocstringReturns(
166
args=[key],
167
description=info.get('description'),
168
type_name=info.get('type_name'),
169
is_generator=info.get('is_generator', False),
170
)
171
is_done['return'] = True
172
elif base == 'raise':
173
(type_name,) = args or (None,)
174
meta_item = DocstringRaises(
175
args=[key] + args,
176
description=desc,
177
type_name=type_name,
178
)
179
elif base == 'meta':
180
meta_item = DocstringMeta(
181
args=[key] + args,
182
description=desc,
183
)
184
else:
185
(key, *_) = args or ('return',)
186
assert is_done.get(key, False)
187
continue # don't append
188
189
ret.meta.append(meta_item)
190
191
return ret
192
193
194
def compose(
195
docstring: Docstring,
196
rendering_style: RenderingStyle = RenderingStyle.COMPACT,
197
indent: str = ' ',
198
) -> str:
199
"""Render a parsed docstring into docstring text.
200
201
:param docstring: parsed docstring representation
202
:param rendering_style: the style to render docstrings
203
:param indent: the characters used as indentation in the docstring string
204
:returns: docstring text
205
"""
206
207
def process_desc(desc: T.Optional[str], is_type: bool) -> str:
208
if not desc:
209
return ''
210
211
if rendering_style == RenderingStyle.EXPANDED or (
212
rendering_style == RenderingStyle.CLEAN and not is_type
213
):
214
(first, *rest) = desc.splitlines()
215
return '\n'.join(
216
['\n' + indent + first] + [indent + line for line in rest],
217
)
218
219
(first, *rest) = desc.splitlines()
220
return '\n'.join([' ' + first] + [indent + line for line in rest])
221
222
parts: T.List[str] = []
223
if docstring.short_description:
224
parts.append(docstring.short_description)
225
if docstring.blank_after_short_description:
226
parts.append('')
227
if docstring.long_description:
228
parts.append(docstring.long_description)
229
if docstring.blank_after_long_description:
230
parts.append('')
231
232
for meta in docstring.meta:
233
if isinstance(meta, DocstringParam):
234
if meta.type_name:
235
type_name = (
236
f'{meta.type_name}?'
237
if meta.is_optional
238
else meta.type_name
239
)
240
text = f'@type {meta.arg_name}:'
241
text += process_desc(type_name, True)
242
parts.append(text)
243
text = f'@param {meta.arg_name}:' + process_desc(
244
meta.description, False,
245
)
246
parts.append(text)
247
elif isinstance(meta, DocstringReturns):
248
(arg_key, type_key) = (
249
('yield', 'ytype')
250
if meta.is_generator
251
else ('return', 'rtype')
252
)
253
if meta.type_name:
254
text = f'@{type_key}:' + process_desc(meta.type_name, True)
255
parts.append(text)
256
if meta.description:
257
text = f'@{arg_key}:' + process_desc(meta.description, False)
258
parts.append(text)
259
elif isinstance(meta, DocstringRaises):
260
text = f'@raise {meta.type_name}:' if meta.type_name else '@raise:'
261
text += process_desc(meta.description, False)
262
parts.append(text)
263
else:
264
text = f'@{" ".join(meta.args)}:'
265
text += process_desc(meta.description, False)
266
parts.append(text)
267
return '\n'.join(parts)
268
269