omdev 0.0.0.dev312__py3-none-any.whl → 0.0.0.dev314__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,329 @@
1
+ """Google-style docstring parsing."""
2
+ import collections
3
+ import enum
4
+ import inspect
5
+ import re
6
+ import typing as ta
7
+
8
+ from .common import EXAMPLES_KEYWORDS
9
+ from .common import PARAM_KEYWORDS
10
+ from .common import RAISES_KEYWORDS
11
+ from .common import RETURNS_KEYWORDS
12
+ from .common import YIELDS_KEYWORDS
13
+ from .common import Docstring
14
+ from .common import DocstringExample
15
+ from .common import DocstringMeta
16
+ from .common import DocstringParam
17
+ from .common import DocstringRaises
18
+ from .common import DocstringReturns
19
+ from .common import DocstringStyle
20
+ from .common import ParseError
21
+
22
+
23
+ ##
24
+
25
+
26
+ class SectionType(enum.IntEnum):
27
+ """Types of sections."""
28
+
29
+ SINGULAR = 0
30
+ """For sections like examples."""
31
+
32
+ MULTIPLE = 1
33
+ """For sections like params."""
34
+
35
+ SINGULAR_OR_MULTIPLE = 2
36
+ """For sections like returns or yields."""
37
+
38
+
39
+ class Section(ta.NamedTuple):
40
+ """A docstring section."""
41
+
42
+ title: str
43
+ key: str
44
+ type: SectionType
45
+
46
+
47
+ _GOOGLE_TYPED_ARG_PAT = re.compile(r'\s*(.+?)\s*\(\s*(.*[^\s]+)\s*\)')
48
+ _GOOGLE_ARG_DESC_PAT = re.compile(r'.*\. Defaults to (.+)\.')
49
+ _MULTIPLE_PATTERN = re.compile(r'(\s*[^:\s]+:)|([^:]*\]:.*)')
50
+
51
+ DEFAULT_SECTIONS = [
52
+ Section('Arguments', 'param', SectionType.MULTIPLE),
53
+ Section('Args', 'param', SectionType.MULTIPLE),
54
+ Section('Parameters', 'param', SectionType.MULTIPLE),
55
+ Section('Params', 'param', SectionType.MULTIPLE),
56
+ Section('Raises', 'raises', SectionType.MULTIPLE),
57
+ Section('Exceptions', 'raises', SectionType.MULTIPLE),
58
+ Section('Except', 'raises', SectionType.MULTIPLE),
59
+ Section('Attributes', 'attribute', SectionType.MULTIPLE),
60
+ Section('Example', 'examples', SectionType.SINGULAR),
61
+ Section('Examples', 'examples', SectionType.SINGULAR),
62
+ Section('Returns', 'returns', SectionType.SINGULAR_OR_MULTIPLE),
63
+ Section('Yields', 'yields', SectionType.SINGULAR_OR_MULTIPLE),
64
+ ]
65
+
66
+
67
+ class GoogleParser:
68
+ """Parser for Google-style docstrings."""
69
+
70
+ def __init__(
71
+ self,
72
+ sections: list[Section] | None = None,
73
+ title_colon: bool = True,
74
+ ) -> None:
75
+ """
76
+ Setup sections.
77
+
78
+ :param sections: Recognized sections or None to defaults.
79
+ :param title_colon: require colon after section title.
80
+ """
81
+
82
+ super().__init__()
83
+
84
+ if not sections:
85
+ sections = DEFAULT_SECTIONS
86
+ self.sections = {s.title: s for s in sections}
87
+ self.title_colon = title_colon
88
+ self._setup()
89
+
90
+ def _setup(self) -> None:
91
+ if self.title_colon:
92
+ colon = ':'
93
+ else:
94
+ colon = ''
95
+
96
+ self.titles_re = re.compile(
97
+ '^('
98
+ + '|'.join(f'({t})' for t in self.sections)
99
+ + ')'
100
+ + colon
101
+ + '[ \t\r\f\v]*$',
102
+ flags=re.MULTILINE,
103
+ )
104
+
105
+ def _build_meta(self, text: str, title: str) -> DocstringMeta:
106
+ """
107
+ Build docstring element.
108
+
109
+ :param text: docstring element text
110
+ :param title: title of section containing element
111
+ :return:
112
+ """
113
+
114
+ section = self.sections[title]
115
+
116
+ if (
117
+ (
118
+ section.type == SectionType.SINGULAR_OR_MULTIPLE
119
+ and not _MULTIPLE_PATTERN.match(text)
120
+ ) or
121
+ section.type == SectionType.SINGULAR
122
+ ):
123
+ return self._build_single_meta(section, text)
124
+
125
+ if ':' not in text:
126
+ raise ParseError(f'Expected a colon in {text!r}.')
127
+
128
+ # Split spec and description
129
+ before, desc = text.split(':', 1)
130
+ if desc:
131
+ desc = desc[1:] if desc[0] == ' ' else desc
132
+ if '\n' in desc:
133
+ first_line, rest = desc.split('\n', 1)
134
+ desc = first_line + '\n' + inspect.cleandoc(rest)
135
+ desc = desc.strip('\n')
136
+
137
+ return self._build_multi_meta(section, before, desc)
138
+
139
+ @staticmethod
140
+ def _build_single_meta(section: Section, desc: str) -> DocstringMeta:
141
+ if section.key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:
142
+ return DocstringReturns(
143
+ args=[section.key],
144
+ description=desc,
145
+ type_name=None,
146
+ is_generator=section.key in YIELDS_KEYWORDS,
147
+ )
148
+
149
+ if section.key in RAISES_KEYWORDS:
150
+ return DocstringRaises(
151
+ args=[section.key],
152
+ description=desc,
153
+ type_name=None,
154
+ )
155
+
156
+ if section.key in EXAMPLES_KEYWORDS:
157
+ return DocstringExample(
158
+ args=[section.key],
159
+ snippet=None,
160
+ description=desc,
161
+ )
162
+
163
+ if section.key in PARAM_KEYWORDS:
164
+ raise ParseError('Expected paramenter name.')
165
+
166
+ return DocstringMeta(args=[section.key], description=desc)
167
+
168
+ @staticmethod
169
+ def _build_multi_meta(
170
+ section: Section,
171
+ before: str,
172
+ desc: str,
173
+ ) -> DocstringMeta:
174
+ if section.key in PARAM_KEYWORDS:
175
+ match = _GOOGLE_TYPED_ARG_PAT.match(before)
176
+ if match:
177
+ arg_name, type_name = match.group(1, 2)
178
+ if type_name.endswith(', optional'):
179
+ is_optional = True
180
+ type_name = type_name[:-10]
181
+ elif type_name.endswith('?'):
182
+ is_optional = True
183
+ type_name = type_name[:-1]
184
+ else:
185
+ is_optional = False
186
+ else:
187
+ arg_name, type_name = before, None
188
+ is_optional = None
189
+
190
+ match = _GOOGLE_ARG_DESC_PAT.match(desc)
191
+ default = match.group(1) if match else None
192
+
193
+ return DocstringParam(
194
+ args=[section.key, before],
195
+ description=desc,
196
+ arg_name=arg_name,
197
+ type_name=type_name,
198
+ is_optional=is_optional,
199
+ default=default,
200
+ )
201
+ if section.key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:
202
+ return DocstringReturns(
203
+ args=[section.key, before],
204
+ description=desc,
205
+ type_name=before,
206
+ is_generator=section.key in YIELDS_KEYWORDS,
207
+ )
208
+ if section.key in RAISES_KEYWORDS:
209
+ return DocstringRaises(
210
+ args=[section.key, before], description=desc, type_name=before,
211
+ )
212
+ return DocstringMeta(args=[section.key, before], description=desc)
213
+
214
+ def add_section(self, section: Section) -> None:
215
+ """
216
+ Add or replace a section.
217
+
218
+ :param section: The new section.
219
+ """
220
+
221
+ self.sections[section.title] = section
222
+ self._setup()
223
+
224
+ def parse(self, text: str) -> Docstring:
225
+ """
226
+ Parse the Google-style docstring into its components.
227
+
228
+ :returns: parsed docstring
229
+ """
230
+
231
+ ret = Docstring(style=DocstringStyle.GOOGLE)
232
+ if not text:
233
+ return ret
234
+
235
+ # Clean according to PEP-0257
236
+ text = inspect.cleandoc(text)
237
+
238
+ # Find first title and split on its position
239
+ match = self.titles_re.search(text)
240
+ if match:
241
+ desc_chunk = text[: match.start()]
242
+ meta_chunk = text[match.start():]
243
+ else:
244
+ desc_chunk = text
245
+ meta_chunk = ''
246
+
247
+ # Break description into short and long parts
248
+ parts = desc_chunk.split('\n', 1)
249
+ ret.short_description = parts[0] or None
250
+ if len(parts) > 1:
251
+ long_desc_chunk = parts[1] or ''
252
+ ret.blank_after_short_description = long_desc_chunk.startswith('\n')
253
+ ret.blank_after_long_description = long_desc_chunk.endswith('\n\n')
254
+ ret.long_description = long_desc_chunk.strip() or None
255
+
256
+ # Split by sections determined by titles
257
+ matches = list(self.titles_re.finditer(meta_chunk))
258
+ if not matches:
259
+ return ret
260
+
261
+ splits = []
262
+ for j in range(len(matches) - 1):
263
+ splits.append((matches[j].end(), matches[j + 1].start()))
264
+
265
+ splits.append((matches[-1].end(), len(meta_chunk)))
266
+
267
+ chunks = collections.OrderedDict() # type: ta.MutableMapping[str,str]
268
+ for j, (start, end) in enumerate(splits):
269
+ title = matches[j].group(1)
270
+ if title not in self.sections:
271
+ continue
272
+
273
+ # Clear Any Unknown Meta
274
+ # Ref: https://github.com/rr-/docstring_parser/issues/29
275
+ meta_details = meta_chunk[start:end]
276
+ unknown_meta = re.search(r'\n\S', meta_details)
277
+ if unknown_meta is not None:
278
+ meta_details = meta_details[: unknown_meta.start()]
279
+
280
+ chunks[title] = meta_details.strip('\n')
281
+
282
+ if not chunks:
283
+ return ret
284
+
285
+ # Add elements from each chunk
286
+ for title, chunk in chunks.items():
287
+ # Determine indent
288
+ indent_match = re.search(r'^\s*', chunk)
289
+ if not indent_match:
290
+ raise ParseError(f'Can\'t infer indent from "{chunk}"')
291
+
292
+ indent = indent_match.group()
293
+
294
+ # Check for singular elements
295
+ if self.sections[title].type in [
296
+ SectionType.SINGULAR,
297
+ SectionType.SINGULAR_OR_MULTIPLE,
298
+ ]:
299
+ part = inspect.cleandoc(chunk)
300
+ ret.meta.append(self._build_meta(part, title))
301
+ continue
302
+
303
+ # Split based on lines which have exactly that indent
304
+ _re = '^' + indent + r'(?=\S)'
305
+
306
+ c_matches = list(re.finditer(_re, chunk, flags=re.MULTILINE))
307
+ if not c_matches:
308
+ raise ParseError(f'No specification for "{title}": "{chunk}"')
309
+
310
+ c_splits = []
311
+ for j in range(len(c_matches) - 1):
312
+ c_splits.append((c_matches[j].end(), c_matches[j + 1].start()))
313
+ c_splits.append((c_matches[-1].end(), len(chunk)))
314
+
315
+ for start, end in c_splits:
316
+ part = chunk[start:end].strip('\n')
317
+ ret.meta.append(self._build_meta(part, title))
318
+
319
+ return ret
320
+
321
+
322
+ def parse(text: str) -> Docstring:
323
+ """
324
+ Parse the Google-style docstring into its components.
325
+
326
+ :returns: parsed docstring
327
+ """
328
+
329
+ return GoogleParser().parse(text)