omdev 0.0.0.dev313__py3-none-any.whl → 0.0.0.dev315__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,403 @@
1
+ """
2
+ Numpydoc-style docstring parsing.
3
+
4
+ :see: https://numpydoc.readthedocs.io/en/latest/format.html
5
+ """
6
+ import abc
7
+ import inspect
8
+ import itertools
9
+ import re
10
+ import textwrap
11
+ import typing as ta
12
+
13
+ from .common import Docstring
14
+ from .common import DocstringDeprecated
15
+ from .common import DocstringExample
16
+ from .common import DocstringMeta
17
+ from .common import DocstringParam
18
+ from .common import DocstringRaises
19
+ from .common import DocstringReturns
20
+ from .common import DocstringStyle
21
+ from .common import ParseError
22
+
23
+
24
+ ##
25
+
26
+
27
+ def _pairwise(iterable: ta.Iterable, end=None) -> ta.Iterable:
28
+ left, right = itertools.tee(iterable)
29
+ next(right, None)
30
+ return itertools.zip_longest(left, right, fillvalue=end)
31
+
32
+
33
+ def _clean_str(string: str) -> str | None:
34
+ string = string.strip()
35
+ if len(string) > 0:
36
+ return string
37
+ return None
38
+
39
+
40
+ KV_PAT = re.compile(r'^[^\s].*$', flags=re.MULTILINE)
41
+ PARAM_KEY_PAT = re.compile(r'^(?P<name>.*?)(?:\s*:\s*(?P<type>.*?))?$')
42
+ PARAM_OPTIONAL_PAT = re.compile(r'(?P<type>.*?)(?:, optional|\(optional\))$')
43
+
44
+ # numpydoc format has no formal grammar for this, but we can make some educated guesses...
45
+ PARAM_DEFAULT_PAT = re.compile(r'(?<!\S)[Dd]efault(?: is | = |: |s to |)\s*(?P<value>[\w\-\.]*\w)')
46
+
47
+ RETURN_KEY_PAT = re.compile(r'^(?:(?P<name>.*?)\s*:\s*)?(?P<type>.*?)$')
48
+
49
+
50
+ class Section:
51
+ """
52
+ Numpydoc section parser.
53
+
54
+ :param title: section title. For most sections, this is a heading like "Parameters" which appears on its own line,
55
+ underlined by en-dashes ('-') on the following line.
56
+ :param key: meta key string. In the parsed ``DocstringMeta`` instance this will be the first element of the ``args``
57
+ attribute list.
58
+ """
59
+
60
+ def __init__(self, title: str, key: str) -> None:
61
+ super().__init__()
62
+
63
+ self.title = title
64
+ self.key = key
65
+
66
+ @property
67
+ def title_pattern(self) -> str:
68
+ """
69
+ Regular expression pattern matching this section's header.
70
+
71
+ This pattern will match this instance's ``title`` attribute in an anonymous group.
72
+ """
73
+
74
+ dashes = '-' * len(self.title)
75
+ return rf'^({self.title})\s*?\n{dashes}\s*$'
76
+
77
+ def parse(self, text: str) -> ta.Iterable[DocstringMeta]:
78
+ """
79
+ Parse ``DocstringMeta`` objects from the body of this section.
80
+
81
+ :param text: section body text. Should be cleaned with ``inspect.cleandoc`` before parsing.
82
+ """
83
+
84
+ yield DocstringMeta([self.key], description=_clean_str(text))
85
+
86
+
87
+ class _KVSection(Section, abc.ABC):
88
+ """
89
+ Base parser for numpydoc sections with key-value syntax.
90
+
91
+ E.g. sections that look like this:
92
+ key
93
+ value
94
+ key2 : type
95
+ values can also span...
96
+ ... multiple lines
97
+ """
98
+
99
+ @abc.abstractmethod
100
+ def _parse_item(self, key: str, value: str) -> DocstringMeta:
101
+ raise NotImplementedError
102
+
103
+ def parse(self, text: str) -> ta.Iterable[DocstringMeta]:
104
+ for match, next_match in _pairwise(KV_PAT.finditer(text)):
105
+ start = match.end()
106
+ end = next_match.start() if next_match is not None else None
107
+ value = text[start:end]
108
+ yield self._parse_item(
109
+ key=match.group(),
110
+ value=inspect.cleandoc(value),
111
+ )
112
+
113
+
114
+ class _SphinxSection(Section):
115
+ """
116
+ Base parser for numpydoc sections with sphinx-style syntax.
117
+
118
+ E.g. sections that look like this:
119
+ .. title:: something
120
+ possibly over multiple lines
121
+ """
122
+
123
+ @property
124
+ def title_pattern(self) -> str:
125
+ return rf'^\.\.\s*({self.title})\s*::'
126
+
127
+
128
+ class ParamSection(_KVSection):
129
+ """
130
+ Parser for numpydoc parameter sections.
131
+
132
+ E.g. any section that looks like this:
133
+ arg_name
134
+ arg_description
135
+ arg_2 : type, optional
136
+ descriptions can also span...
137
+ ... multiple lines
138
+ """
139
+
140
+ def _parse_item(self, key: str, value: str) -> DocstringParam:
141
+ match = PARAM_KEY_PAT.match(key)
142
+ arg_name = type_name = is_optional = None
143
+ if match is not None:
144
+ arg_name = match.group('name')
145
+ type_name = match.group('type')
146
+ if type_name is not None:
147
+ optional_match = PARAM_OPTIONAL_PAT.match(type_name)
148
+ if optional_match is not None:
149
+ type_name = optional_match.group('type')
150
+ is_optional = True
151
+ else:
152
+ is_optional = False
153
+
154
+ default = None
155
+ if len(value) > 0:
156
+ default_match = PARAM_DEFAULT_PAT.search(value)
157
+ if default_match is not None:
158
+ default = default_match.group('value')
159
+
160
+ if arg_name is None:
161
+ raise ParseError
162
+
163
+ return DocstringParam(
164
+ args=[self.key, arg_name],
165
+ description=_clean_str(value),
166
+ arg_name=arg_name,
167
+ type_name=type_name,
168
+ is_optional=is_optional,
169
+ default=default,
170
+ )
171
+
172
+
173
+ class RaisesSection(_KVSection):
174
+ """
175
+ Parser for numpydoc raises sections.
176
+
177
+ E.g. any section that looks like this:
178
+ ValueError
179
+ A description of what might raise ValueError
180
+ """
181
+
182
+ def _parse_item(self, key: str, value: str) -> DocstringRaises:
183
+ return DocstringRaises(
184
+ args=[self.key, key],
185
+ description=_clean_str(value),
186
+ type_name=key if len(key) > 0 else None,
187
+ )
188
+
189
+
190
+ class ReturnsSection(_KVSection):
191
+ """
192
+ Parser for numpydoc returns sections.
193
+
194
+ E.g. any section that looks like this:
195
+ return_name : type
196
+ A description of this returned value
197
+ another_type
198
+ Return names are optional, types are required
199
+ """
200
+
201
+ is_generator = False
202
+
203
+ def _parse_item(self, key: str, value: str) -> DocstringReturns:
204
+ match = RETURN_KEY_PAT.match(key)
205
+ if match is not None:
206
+ return_name = match.group('name')
207
+ type_name = match.group('type')
208
+ else:
209
+ return_name = None
210
+ type_name = None
211
+
212
+ return DocstringReturns(
213
+ args=[self.key],
214
+ description=_clean_str(value),
215
+ type_name=type_name,
216
+ is_generator=self.is_generator,
217
+ return_name=return_name,
218
+ )
219
+
220
+
221
+ class YieldsSection(ReturnsSection):
222
+ """Parser for numpydoc generator "yields" sections."""
223
+
224
+ is_generator = True
225
+
226
+
227
+ class DeprecationSection(_SphinxSection):
228
+ """Parser for numpydoc "deprecation warning" sections."""
229
+
230
+ def parse(self, text: str) -> ta.Iterable[DocstringDeprecated]:
231
+ version, desc, *_ = [*text.split(sep='\n', maxsplit=1), None, None]
232
+
233
+ if desc is not None:
234
+ desc = _clean_str(inspect.cleandoc(desc)) # type: ignore[unreachable]
235
+
236
+ yield DocstringDeprecated(
237
+ args=[self.key],
238
+ description=desc,
239
+ version=_clean_str(version),
240
+ )
241
+
242
+
243
+ class ExamplesSection(Section):
244
+ """
245
+ Parser for numpydoc examples sections.
246
+
247
+ E.g. any section that looks like this:
248
+ >>> import numpy.matlib
249
+ >>> np.matlib.empty((2, 2)) # filled with random data
250
+ matrix([[ 6.76425276e-320, 9.79033856e-307], # random
251
+ [ 7.39337286e-309, 3.22135945e-309]])
252
+ >>> np.matlib.empty((2, 2), dtype=int)
253
+ matrix([[ 6600475, 0], # random
254
+ [ 6586976, 22740995]])
255
+ """
256
+
257
+ def parse(self, text: str) -> ta.Iterable[DocstringMeta]:
258
+ """
259
+ Parse ``DocstringExample`` objects from the body of this section.
260
+
261
+ :param text: section body text. Should be cleaned with ``inspect.cleandoc`` before parsing.
262
+ """
263
+
264
+ lines = textwrap.dedent(text).strip().splitlines()
265
+ while lines:
266
+ snippet_lines = []
267
+ description_lines = []
268
+ while lines:
269
+ if not lines[0].startswith('>>>'):
270
+ break
271
+ snippet_lines.append(lines.pop(0))
272
+ while lines:
273
+ if lines[0].startswith('>>>'):
274
+ break
275
+ description_lines.append(lines.pop(0))
276
+ yield DocstringExample(
277
+ [self.key],
278
+ snippet='\n'.join(snippet_lines) if snippet_lines else None,
279
+ description='\n'.join(description_lines),
280
+ )
281
+
282
+
283
+ DEFAULT_SECTIONS = [
284
+ ParamSection('Parameters', 'param'),
285
+ ParamSection('Params', 'param'),
286
+ ParamSection('Arguments', 'param'),
287
+ ParamSection('Args', 'param'),
288
+ ParamSection('Other Parameters', 'other_param'),
289
+ ParamSection('Other Params', 'other_param'),
290
+ ParamSection('Other Arguments', 'other_param'),
291
+ ParamSection('Other Args', 'other_param'),
292
+ ParamSection('Receives', 'receives'),
293
+ ParamSection('Receive', 'receives'),
294
+ RaisesSection('Raises', 'raises'),
295
+ RaisesSection('Raise', 'raises'),
296
+ RaisesSection('Warns', 'warns'),
297
+ RaisesSection('Warn', 'warns'),
298
+ ParamSection('Attributes', 'attribute'),
299
+ ParamSection('Attribute', 'attribute'),
300
+ ReturnsSection('Returns', 'returns'),
301
+ ReturnsSection('Return', 'returns'),
302
+ YieldsSection('Yields', 'yields'),
303
+ YieldsSection('Yield', 'yields'),
304
+ ExamplesSection('Examples', 'examples'),
305
+ ExamplesSection('Example', 'examples'),
306
+ Section('Warnings', 'warnings'),
307
+ Section('Warning', 'warnings'),
308
+ Section('See Also', 'see_also'),
309
+ Section('Related', 'see_also'),
310
+ Section('Notes', 'notes'),
311
+ Section('Note', 'notes'),
312
+ Section('References', 'references'),
313
+ Section('Reference', 'references'),
314
+ DeprecationSection('deprecated', 'deprecation'),
315
+ ]
316
+
317
+
318
+ class NumpydocParser:
319
+ """Parser for numpydoc-style docstrings."""
320
+
321
+ def __init__(self, sections: ta.Iterable[Section] | None = None) -> None:
322
+ """
323
+ Setup sections.
324
+
325
+ :param sections: Recognized sections or None to defaults.
326
+ """
327
+
328
+ super().__init__()
329
+
330
+ sections = sections or DEFAULT_SECTIONS
331
+ self.sections = {s.title: s for s in sections}
332
+ self._setup()
333
+
334
+ def _setup(self):
335
+ self.titles_re = re.compile(
336
+ r'|'.join(s.title_pattern for s in self.sections.values()),
337
+ flags=re.MULTILINE,
338
+ )
339
+
340
+ def add_section(self, section: Section) -> None:
341
+ """
342
+ Add or replace a section.
343
+
344
+ :param section: The new section.
345
+ """
346
+
347
+ self.sections[section.title] = section
348
+ self._setup()
349
+
350
+ def parse(self, text: str) -> Docstring:
351
+ """
352
+ Parse the numpy-style docstring into its components.
353
+
354
+ :returns: parsed docstring
355
+ """
356
+
357
+ ret = Docstring(style=DocstringStyle.NUMPYDOC)
358
+ if not text:
359
+ return ret
360
+
361
+ # Clean according to PEP-0257
362
+ text = inspect.cleandoc(text)
363
+
364
+ # Find first title and split on its position
365
+ match = self.titles_re.search(text)
366
+ if match:
367
+ desc_chunk = text[: match.start()]
368
+ meta_chunk = text[match.start():]
369
+ else:
370
+ desc_chunk = text
371
+ meta_chunk = ''
372
+
373
+ # Break description into short and long parts
374
+ parts = desc_chunk.split('\n', 1)
375
+ ret.short_description = parts[0] or None
376
+ if len(parts) > 1:
377
+ long_desc_chunk = parts[1] or ''
378
+ ret.blank_after_short_description = long_desc_chunk.startswith('\n')
379
+ ret.blank_after_long_description = long_desc_chunk.endswith('\n\n')
380
+ ret.long_description = long_desc_chunk.strip() or None
381
+
382
+ for match, nextmatch in _pairwise(self.titles_re.finditer(meta_chunk)):
383
+ if match is None:
384
+ raise ParseError
385
+ title = next(g for g in match.groups() if g is not None)
386
+ factory = self.sections[title]
387
+
388
+ # section chunk starts after the header, ends at the start of the next header
389
+ start = match.end()
390
+ end = nextmatch.start() if nextmatch is not None else None
391
+ ret.meta.extend(factory.parse(meta_chunk[start:end]))
392
+
393
+ return ret
394
+
395
+
396
+ def parse(text: str) -> Docstring:
397
+ """
398
+ Parse the numpy-style docstring into its components.
399
+
400
+ :returns: parsed docstring
401
+ """
402
+
403
+ return NumpydocParser().parse(text)
@@ -0,0 +1,84 @@
1
+ """The main parsing routine."""
2
+ import inspect
3
+ import typing as ta
4
+
5
+ from . import epydoc
6
+ from . import google
7
+ from . import numpydoc
8
+ from . import rest
9
+ from .attrdoc import add_attribute_docstrings
10
+ from .common import Docstring
11
+ from .common import DocstringStyle
12
+ from .common import ParseError
13
+
14
+
15
+ ##
16
+
17
+
18
+ _STYLE_MAP = {
19
+ DocstringStyle.REST: rest,
20
+ DocstringStyle.GOOGLE: google,
21
+ DocstringStyle.NUMPYDOC: numpydoc,
22
+ DocstringStyle.EPYDOC: epydoc,
23
+ }
24
+
25
+
26
+ def parse(
27
+ text: str,
28
+ style: DocstringStyle = DocstringStyle.AUTO,
29
+ ) -> Docstring:
30
+ """
31
+ Parse the docstring into its components.
32
+
33
+ :param text: docstring text to parse
34
+ :param style: docstring style
35
+ :returns: parsed docstring representation
36
+ """
37
+
38
+ if style != DocstringStyle.AUTO:
39
+ return _STYLE_MAP[style].parse(text)
40
+
41
+ exc: Exception | type[Exception] = ParseError
42
+ rets = []
43
+ for module in _STYLE_MAP.values():
44
+ try:
45
+ ret = module.parse(text)
46
+ except ParseError as ex:
47
+ exc = ex
48
+ else:
49
+ rets.append(ret)
50
+
51
+ if not rets:
52
+ raise exc
53
+
54
+ return sorted(rets, key=lambda d: len(d.meta), reverse=True)[0]
55
+
56
+
57
+ def parse_from_object(
58
+ obj: ta.Any,
59
+ style: DocstringStyle = DocstringStyle.AUTO,
60
+ ) -> Docstring:
61
+ """
62
+ Parse the object's docstring(s) into its components.
63
+
64
+ The object can be anything that has a ``__doc__`` attribute. In contrast to the ``parse`` function,
65
+ ``parse_from_object`` is able to parse attribute docstrings which are defined in the source code instead of
66
+ ``__doc__``.
67
+
68
+ Currently only attribute docstrings defined at class and module levels are supported. Attribute docstrings defined
69
+ in ``__init__`` methods are not supported.
70
+
71
+ When given a class, only the attribute docstrings of that class are parsed, not its inherited classes. This is a
72
+ design decision. Separate calls to this function should be performed to get attribute docstrings of parent classes.
73
+
74
+ :param obj: object from which to parse the docstring(s)
75
+ :param style: docstring style
76
+ :returns: parsed docstring representation
77
+ """
78
+
79
+ docstring = parse(obj.__doc__, style=style)
80
+
81
+ if inspect.isclass(obj) or inspect.ismodule(obj):
82
+ add_attribute_docstrings(obj, docstring)
83
+
84
+ return docstring
@@ -0,0 +1,176 @@
1
+ """ReST-style docstring parsing."""
2
+ import inspect
3
+ import re
4
+
5
+ from .common import DEPRECATION_KEYWORDS
6
+ from .common import PARAM_KEYWORDS
7
+ from .common import RAISES_KEYWORDS
8
+ from .common import RETURNS_KEYWORDS
9
+ from .common import YIELDS_KEYWORDS
10
+ from .common import Docstring
11
+ from .common import DocstringDeprecated
12
+ from .common import DocstringMeta
13
+ from .common import DocstringParam
14
+ from .common import DocstringRaises
15
+ from .common import DocstringReturns
16
+ from .common import DocstringStyle
17
+ from .common import ParseError
18
+
19
+
20
+ ##
21
+
22
+
23
+ def _build_meta(args: list[str], desc: str) -> DocstringMeta:
24
+ key = args[0]
25
+
26
+ if key in PARAM_KEYWORDS:
27
+ if len(args) == 3:
28
+ key, type_name, arg_name = args
29
+ if type_name.endswith('?'):
30
+ is_optional = True
31
+ type_name = type_name[:-1]
32
+ else:
33
+ is_optional = False
34
+
35
+ elif len(args) == 2:
36
+ key, arg_name = args
37
+ type_name = None
38
+ is_optional = None
39
+
40
+ else:
41
+ raise ParseError(f'Expected one or two arguments for a {key} keyword.')
42
+
43
+ match = re.match(r'.*defaults to (.+)', desc, flags=re.DOTALL)
44
+ default = match.group(1).rstrip('.') if match else None
45
+
46
+ return DocstringParam(
47
+ args=args,
48
+ description=desc,
49
+ arg_name=arg_name,
50
+ type_name=type_name,
51
+ is_optional=is_optional,
52
+ default=default,
53
+ )
54
+
55
+ if key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:
56
+ if len(args) == 2:
57
+ type_name = args[1]
58
+ elif len(args) == 1:
59
+ type_name = None
60
+ else:
61
+ raise ParseError(f'Expected one or no arguments for a {key} keyword.')
62
+
63
+ return DocstringReturns(
64
+ args=args,
65
+ description=desc,
66
+ type_name=type_name,
67
+ is_generator=key in YIELDS_KEYWORDS,
68
+ )
69
+
70
+ if key in DEPRECATION_KEYWORDS:
71
+ match = re.search(
72
+ r'^(?P<version>v?((?:\d+)(?:\.[0-9a-z\.]+))) (?P<desc>.+)',
73
+ desc,
74
+ flags=re.IGNORECASE,
75
+ )
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(f'Expected one or no arguments for a {key} keyword.')
90
+
91
+ return DocstringRaises(
92
+ args=args,
93
+ description=desc,
94
+ type_name=type_name,
95
+ )
96
+
97
+ return DocstringMeta(args=args, description=desc)
98
+
99
+
100
+ def parse(text: str) -> Docstring:
101
+ """
102
+ Parse the ReST-style docstring into its components.
103
+
104
+ :returns: parsed docstring
105
+ """
106
+
107
+ ret = Docstring(style=DocstringStyle.REST)
108
+ if not text:
109
+ return ret
110
+
111
+ text = inspect.cleandoc(text)
112
+ match = re.search('^:', text, flags=re.MULTILINE)
113
+ if match:
114
+ desc_chunk = text[: match.start()]
115
+ meta_chunk = text[match.start():]
116
+ else:
117
+ desc_chunk = text
118
+ meta_chunk = ''
119
+
120
+ parts = desc_chunk.split('\n', 1)
121
+ ret.short_description = parts[0] or None
122
+ if len(parts) > 1:
123
+ long_desc_chunk = parts[1] or ''
124
+ ret.blank_after_short_description = long_desc_chunk.startswith('\n')
125
+ ret.blank_after_long_description = long_desc_chunk.endswith('\n\n')
126
+ ret.long_description = long_desc_chunk.strip() or None
127
+
128
+ types = {}
129
+ rtypes = {}
130
+ for match in re.finditer(r'(^:.*?)(?=^:|\Z)', meta_chunk, flags=re.DOTALL | re.MULTILINE):
131
+ chunk = match.group(0)
132
+ if not chunk:
133
+ continue
134
+
135
+ try:
136
+ args_chunk, desc_chunk = chunk.lstrip(':').split(':', 1)
137
+ except ValueError as ex:
138
+ raise ParseError(f'Error parsing meta information near "{chunk}".') from ex
139
+
140
+ args = args_chunk.split()
141
+ desc = desc_chunk.strip()
142
+
143
+ if '\n' in desc:
144
+ first_line, rest = desc.split('\n', 1)
145
+ desc = first_line + '\n' + inspect.cleandoc(rest)
146
+
147
+ # Add special handling for :type a: typename
148
+ if len(args) == 2 and args[0] == 'type':
149
+ types[args[1]] = desc
150
+
151
+ elif len(args) in [1, 2] and args[0] == 'rtype':
152
+ rtypes[None if len(args) == 1 else args[1]] = desc
153
+
154
+ else:
155
+ ret.meta.append(_build_meta(args, desc))
156
+
157
+ for meta in ret.meta:
158
+ if isinstance(meta, DocstringParam):
159
+ meta.type_name = meta.type_name or types.get(meta.arg_name)
160
+
161
+ elif isinstance(meta, DocstringReturns):
162
+ meta.type_name = meta.type_name or rtypes.get(meta.return_name)
163
+
164
+ if not any(isinstance(m, DocstringReturns) for m in ret.meta) and rtypes:
165
+ for return_name, type_name in rtypes.items():
166
+ ret.meta.append(
167
+ DocstringReturns(
168
+ args=[],
169
+ type_name=type_name,
170
+ description=None,
171
+ is_generator=False,
172
+ return_name=return_name,
173
+ ),
174
+ )
175
+
176
+ return ret
@@ -205,14 +205,21 @@ class MagicStyle:
205
205
 
206
206
  PY_MAGIC_STYLE = MagicStyle(
207
207
  name='py',
208
- exts=frozenset(['py']),
208
+ exts=frozenset([
209
+ 'py',
210
+ ]),
209
211
  line_prefix='# ',
210
212
  )
211
213
 
212
214
 
213
215
  C_MAGIC_STYLE = MagicStyle(
214
216
  name='c',
215
- exts=frozenset(['c', 'cc', 'cpp', 'cu']),
217
+ exts=frozenset([
218
+ 'c',
219
+ 'cc',
220
+ 'cpp',
221
+ 'cu',
222
+ ]),
216
223
  line_prefix='// ',
217
224
  block_prefix_suffix=('/* ', '*/'),
218
225
  )
omdev/tools/antlr/gen.py CHANGED
@@ -133,6 +133,7 @@ class GenPy:
133
133
  '# type: ignore\n',
134
134
  '# ruff: noqa\n',
135
135
  '# flake8: noqa\n',
136
+ '# @omlish-generated\n',
136
137
  ]
137
138
 
138
139
  for l in in_lines: