Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
singlestore-labs
GitHub Repository: singlestore-labs/singlestoredb-python
Path: blob/main/singlestoredb/docstring/rest.py
469 views
1
"""ReST-style docstring parsing."""
2
import inspect
3
import re
4
import typing as T
5
6
from .common import DEPRECATION_KEYWORDS
7
from .common import Docstring
8
from .common import DocstringDeprecated
9
from .common import DocstringMeta
10
from .common import DocstringParam
11
from .common import DocstringRaises
12
from .common import DocstringReturns
13
from .common import DocstringStyle
14
from .common import PARAM_KEYWORDS
15
from .common import ParseError
16
from .common import RAISES_KEYWORDS
17
from .common import RenderingStyle
18
from .common import RETURNS_KEYWORDS
19
from .common import YIELDS_KEYWORDS
20
21
22
def _build_meta(args: T.List[str], desc: str) -> DocstringMeta:
23
key = args[0]
24
25
if key in PARAM_KEYWORDS:
26
if len(args) == 3:
27
key, type_name, arg_name = args
28
if type_name.endswith('?'):
29
is_optional = True
30
type_name = type_name[:-1]
31
else:
32
is_optional = False
33
elif len(args) == 2:
34
key, arg_name = args
35
type_name = None
36
is_optional = None
37
else:
38
raise ParseError(
39
f'Expected one or two arguments for a {key} keyword.',
40
)
41
42
match = re.match(r'.*defaults to (.+)', desc, flags=re.DOTALL)
43
default = match.group(1).rstrip('.') if match else None
44
45
return DocstringParam(
46
args=args,
47
description=desc,
48
arg_name=arg_name,
49
type_name=type_name,
50
is_optional=is_optional,
51
default=default,
52
)
53
54
if key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:
55
if len(args) == 2:
56
type_name = args[1]
57
elif len(args) == 1:
58
type_name = None
59
else:
60
raise ParseError(
61
f'Expected one or no arguments for a {key} keyword.',
62
)
63
64
return DocstringReturns(
65
args=args,
66
description=desc,
67
type_name=type_name,
68
is_generator=key in YIELDS_KEYWORDS,
69
)
70
71
if key in DEPRECATION_KEYWORDS:
72
match = re.search(
73
r'^(?P<version>v?((?:\d+)(?:\.[0-9a-z\.]+))) (?P<desc>.+)',
74
desc,
75
flags=re.I,
76
)
77
return DocstringDeprecated(
78
args=args,
79
version=match.group('version') if match else None,
80
description=match.group('desc') if match else desc,
81
)
82
83
if key in RAISES_KEYWORDS:
84
if len(args) == 2:
85
type_name = args[1]
86
elif len(args) == 1:
87
type_name = None
88
else:
89
raise ParseError(
90
f'Expected one or no arguments for a {key} keyword.',
91
)
92
return DocstringRaises(
93
args=args, description=desc, type_name=type_name,
94
)
95
96
return DocstringMeta(args=args, description=desc)
97
98
99
def parse(text: T.Optional[str]) -> Docstring:
100
"""Parse the ReST-style docstring into its components.
101
102
:returns: parsed docstring
103
"""
104
ret = Docstring(style=DocstringStyle.REST)
105
if not text:
106
return ret
107
108
text = inspect.cleandoc(text)
109
match = re.search('^:', text, flags=re.M)
110
if match:
111
desc_chunk = text[:match.start()]
112
meta_chunk = text[match.start():]
113
else:
114
desc_chunk = text
115
meta_chunk = ''
116
117
parts = desc_chunk.split('\n', 1)
118
ret.short_description = parts[0] or None
119
if len(parts) > 1:
120
long_desc_chunk = parts[1] or ''
121
ret.blank_after_short_description = long_desc_chunk.startswith('\n')
122
ret.blank_after_long_description = long_desc_chunk.endswith('\n\n')
123
ret.long_description = long_desc_chunk.strip() or None
124
125
types = {}
126
rtypes = {}
127
for match in re.finditer(
128
r'(^:.*?)(?=^:|\Z)', meta_chunk, flags=re.S | re.M,
129
):
130
chunk = match.group(0)
131
if not chunk:
132
continue
133
try:
134
args_chunk, desc_chunk = chunk.lstrip(':').split(':', 1)
135
except ValueError as ex:
136
raise ParseError(
137
f'Error parsing meta information near "{chunk}".',
138
) from ex
139
args = args_chunk.split()
140
desc = desc_chunk.strip()
141
142
if '\n' in desc:
143
first_line, rest = desc.split('\n', 1)
144
desc = first_line + '\n' + inspect.cleandoc(rest)
145
146
# Add special handling for :type a: typename
147
if len(args) == 2 and args[0] == 'type':
148
types[args[1]] = desc
149
elif len(args) in [1, 2] and args[0] == 'rtype':
150
rtypes[None if len(args) == 1 else args[1]] = desc
151
else:
152
ret.meta.append(_build_meta(args, desc))
153
154
for meta in ret.meta:
155
if isinstance(meta, DocstringParam):
156
meta.type_name = meta.type_name or types.get(meta.arg_name)
157
elif isinstance(meta, DocstringReturns):
158
meta.type_name = meta.type_name or rtypes.get(meta.return_name)
159
160
if not any(isinstance(m, DocstringReturns) for m in ret.meta) and rtypes:
161
for return_name, type_name in rtypes.items():
162
ret.meta.append(
163
DocstringReturns(
164
args=[],
165
type_name=type_name,
166
description=None,
167
is_generator=False,
168
return_name=return_name,
169
),
170
)
171
172
return ret
173
174
175
def compose(
176
docstring: Docstring,
177
rendering_style: RenderingStyle = RenderingStyle.COMPACT,
178
indent: str = ' ',
179
) -> str:
180
"""Render a parsed docstring into docstring text.
181
182
:param docstring: parsed docstring representation
183
:param rendering_style: the style to render docstrings
184
:param indent: the characters used as indentation in the docstring string
185
:returns: docstring text
186
"""
187
188
def process_desc(desc: T.Optional[str]) -> str:
189
if not desc:
190
return ''
191
192
if rendering_style == RenderingStyle.CLEAN:
193
(first, *rest) = desc.splitlines()
194
return '\n'.join([' ' + first] + [indent + line for line in rest])
195
196
if rendering_style == RenderingStyle.EXPANDED:
197
(first, *rest) = desc.splitlines()
198
return '\n'.join(
199
['\n' + indent + first] + [indent + line for line in rest],
200
)
201
202
return ' ' + desc
203
204
parts: T.List[str] = []
205
if docstring.short_description:
206
parts.append(docstring.short_description)
207
if docstring.blank_after_short_description:
208
parts.append('')
209
if docstring.long_description:
210
parts.append(docstring.long_description)
211
if docstring.blank_after_long_description:
212
parts.append('')
213
214
for meta in docstring.meta:
215
if isinstance(meta, DocstringParam):
216
if meta.type_name:
217
type_text = (
218
f' {meta.type_name}? '
219
if meta.is_optional
220
else f' {meta.type_name} '
221
)
222
else:
223
type_text = ' '
224
if rendering_style == RenderingStyle.EXPANDED:
225
text = f':param {meta.arg_name}:'
226
text += process_desc(meta.description)
227
parts.append(text)
228
if type_text[:-1]:
229
parts.append(f':type {meta.arg_name}:{type_text[:-1]}')
230
else:
231
text = f':param{type_text}{meta.arg_name}:'
232
text += process_desc(meta.description)
233
parts.append(text)
234
elif isinstance(meta, DocstringReturns):
235
type_text = f' {meta.type_name}' if meta.type_name else ''
236
key = 'yields' if meta.is_generator else 'returns'
237
238
if rendering_style == RenderingStyle.EXPANDED:
239
if meta.description:
240
text = f':{key}:'
241
text += process_desc(meta.description)
242
parts.append(text)
243
if type_text:
244
parts.append(f':rtype:{type_text}')
245
else:
246
text = f':{key}{type_text}:'
247
text += process_desc(meta.description)
248
parts.append(text)
249
elif isinstance(meta, DocstringRaises):
250
type_text = f' {meta.type_name} ' if meta.type_name else ''
251
text = f':raises{type_text}:' + process_desc(meta.description)
252
parts.append(text)
253
else:
254
text = f':{" ".join(meta.args)}:' + process_desc(meta.description)
255
parts.append(text)
256
return '\n'.join(parts)
257
258