Path: blob/main/singlestoredb/docstring/rest.py
469 views
"""ReST-style docstring parsing."""1import inspect2import re3import typing as T45from .common import DEPRECATION_KEYWORDS6from .common import Docstring7from .common import DocstringDeprecated8from .common import DocstringMeta9from .common import DocstringParam10from .common import DocstringRaises11from .common import DocstringReturns12from .common import DocstringStyle13from .common import PARAM_KEYWORDS14from .common import ParseError15from .common import RAISES_KEYWORDS16from .common import RenderingStyle17from .common import RETURNS_KEYWORDS18from .common import YIELDS_KEYWORDS192021def _build_meta(args: T.List[str], desc: str) -> DocstringMeta:22key = args[0]2324if key in PARAM_KEYWORDS:25if len(args) == 3:26key, type_name, arg_name = args27if type_name.endswith('?'):28is_optional = True29type_name = type_name[:-1]30else:31is_optional = False32elif len(args) == 2:33key, arg_name = args34type_name = None35is_optional = None36else:37raise ParseError(38f'Expected one or two arguments for a {key} keyword.',39)4041match = re.match(r'.*defaults to (.+)', desc, flags=re.DOTALL)42default = match.group(1).rstrip('.') if match else None4344return DocstringParam(45args=args,46description=desc,47arg_name=arg_name,48type_name=type_name,49is_optional=is_optional,50default=default,51)5253if key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:54if len(args) == 2:55type_name = args[1]56elif len(args) == 1:57type_name = None58else:59raise ParseError(60f'Expected one or no arguments for a {key} keyword.',61)6263return DocstringReturns(64args=args,65description=desc,66type_name=type_name,67is_generator=key in YIELDS_KEYWORDS,68)6970if key in DEPRECATION_KEYWORDS:71match = re.search(72r'^(?P<version>v?((?:\d+)(?:\.[0-9a-z\.]+))) (?P<desc>.+)',73desc,74flags=re.I,75)76return DocstringDeprecated(77args=args,78version=match.group('version') if match else None,79description=match.group('desc') if match else desc,80)8182if key in RAISES_KEYWORDS:83if len(args) == 2:84type_name = args[1]85elif len(args) == 1:86type_name = None87else:88raise ParseError(89f'Expected one or no arguments for a {key} keyword.',90)91return DocstringRaises(92args=args, description=desc, type_name=type_name,93)9495return DocstringMeta(args=args, description=desc)969798def parse(text: T.Optional[str]) -> Docstring:99"""Parse the ReST-style docstring into its components.100101:returns: parsed docstring102"""103ret = Docstring(style=DocstringStyle.REST)104if not text:105return ret106107text = inspect.cleandoc(text)108match = re.search('^:', text, flags=re.M)109if match:110desc_chunk = text[:match.start()]111meta_chunk = text[match.start():]112else:113desc_chunk = text114meta_chunk = ''115116parts = desc_chunk.split('\n', 1)117ret.short_description = parts[0] or None118if len(parts) > 1:119long_desc_chunk = parts[1] or ''120ret.blank_after_short_description = long_desc_chunk.startswith('\n')121ret.blank_after_long_description = long_desc_chunk.endswith('\n\n')122ret.long_description = long_desc_chunk.strip() or None123124types = {}125rtypes = {}126for match in re.finditer(127r'(^:.*?)(?=^:|\Z)', meta_chunk, flags=re.S | re.M,128):129chunk = match.group(0)130if not chunk:131continue132try:133args_chunk, desc_chunk = chunk.lstrip(':').split(':', 1)134except ValueError as ex:135raise ParseError(136f'Error parsing meta information near "{chunk}".',137) from ex138args = args_chunk.split()139desc = desc_chunk.strip()140141if '\n' in desc:142first_line, rest = desc.split('\n', 1)143desc = first_line + '\n' + inspect.cleandoc(rest)144145# Add special handling for :type a: typename146if len(args) == 2 and args[0] == 'type':147types[args[1]] = desc148elif len(args) in [1, 2] and args[0] == 'rtype':149rtypes[None if len(args) == 1 else args[1]] = desc150else:151ret.meta.append(_build_meta(args, desc))152153for meta in ret.meta:154if isinstance(meta, DocstringParam):155meta.type_name = meta.type_name or types.get(meta.arg_name)156elif isinstance(meta, DocstringReturns):157meta.type_name = meta.type_name or rtypes.get(meta.return_name)158159if not any(isinstance(m, DocstringReturns) for m in ret.meta) and rtypes:160for return_name, type_name in rtypes.items():161ret.meta.append(162DocstringReturns(163args=[],164type_name=type_name,165description=None,166is_generator=False,167return_name=return_name,168),169)170171return ret172173174def compose(175docstring: Docstring,176rendering_style: RenderingStyle = RenderingStyle.COMPACT,177indent: str = ' ',178) -> str:179"""Render a parsed docstring into docstring text.180181:param docstring: parsed docstring representation182:param rendering_style: the style to render docstrings183:param indent: the characters used as indentation in the docstring string184:returns: docstring text185"""186187def process_desc(desc: T.Optional[str]) -> str:188if not desc:189return ''190191if rendering_style == RenderingStyle.CLEAN:192(first, *rest) = desc.splitlines()193return '\n'.join([' ' + first] + [indent + line for line in rest])194195if rendering_style == RenderingStyle.EXPANDED:196(first, *rest) = desc.splitlines()197return '\n'.join(198['\n' + indent + first] + [indent + line for line in rest],199)200201return ' ' + desc202203parts: T.List[str] = []204if docstring.short_description:205parts.append(docstring.short_description)206if docstring.blank_after_short_description:207parts.append('')208if docstring.long_description:209parts.append(docstring.long_description)210if docstring.blank_after_long_description:211parts.append('')212213for meta in docstring.meta:214if isinstance(meta, DocstringParam):215if meta.type_name:216type_text = (217f' {meta.type_name}? '218if meta.is_optional219else f' {meta.type_name} '220)221else:222type_text = ' '223if rendering_style == RenderingStyle.EXPANDED:224text = f':param {meta.arg_name}:'225text += process_desc(meta.description)226parts.append(text)227if type_text[:-1]:228parts.append(f':type {meta.arg_name}:{type_text[:-1]}')229else:230text = f':param{type_text}{meta.arg_name}:'231text += process_desc(meta.description)232parts.append(text)233elif isinstance(meta, DocstringReturns):234type_text = f' {meta.type_name}' if meta.type_name else ''235key = 'yields' if meta.is_generator else 'returns'236237if rendering_style == RenderingStyle.EXPANDED:238if meta.description:239text = f':{key}:'240text += process_desc(meta.description)241parts.append(text)242if type_text:243parts.append(f':rtype:{type_text}')244else:245text = f':{key}{type_text}:'246text += process_desc(meta.description)247parts.append(text)248elif isinstance(meta, DocstringRaises):249type_text = f' {meta.type_name} ' if meta.type_name else ''250text = f':raises{type_text}:' + process_desc(meta.description)251parts.append(text)252else:253text = f':{" ".join(meta.args)}:' + process_desc(meta.description)254parts.append(text)255return '\n'.join(parts)256257258