Path: blob/main/singlestoredb/docstring/epydoc.py
469 views
"""Epyoc-style docstring parsing.12.. seealso:: http://epydoc.sourceforge.net/manual-fields.html3"""4import inspect5import re6import typing as T78from .common import Docstring9from .common import DocstringMeta10from .common import DocstringParam11from .common import DocstringRaises12from .common import DocstringReturns13from .common import DocstringStyle14from .common import ParseError15from .common import RenderingStyle161718def _clean_str(string: str) -> T.Optional[str]:19string = string.strip()20if len(string) > 0:21return string22return None232425def parse(text: T.Optional[str]) -> Docstring:26"""Parse the epydoc-style docstring into its components.2728:returns: parsed docstring29"""30ret = Docstring(style=DocstringStyle.EPYDOC)31if not text:32return ret3334text = inspect.cleandoc(text)35match = re.search('^@', text, flags=re.M)36if match:37desc_chunk = text[:match.start()]38meta_chunk = text[match.start():]39else:40desc_chunk = text41meta_chunk = ''4243parts = desc_chunk.split('\n', 1)44ret.short_description = parts[0] or None45if len(parts) > 1:46long_desc_chunk = parts[1] or ''47ret.blank_after_short_description = long_desc_chunk.startswith('\n')48ret.blank_after_long_description = long_desc_chunk.endswith('\n\n')49ret.long_description = long_desc_chunk.strip() or None5051param_pattern = re.compile(52r'(param|keyword|type)(\s+[_A-z][_A-z0-9]*\??):',53)54raise_pattern = re.compile(r'(raise)(\s+[_A-z][_A-z0-9]*\??)?:')55return_pattern = re.compile(r'(return|rtype|yield|ytype):')56meta_pattern = re.compile(57r'([_A-z][_A-z0-9]+)((\s+[_A-z][_A-z0-9]*\??)*):',58)5960# tokenize61stream: T.List[T.Tuple[str, str, T.List[str], str]] = []62for match in re.finditer(63r'(^@.*?)(?=^@|\Z)', meta_chunk, flags=re.S | re.M,64):65chunk = match.group(0)66if not chunk:67continue6869param_match = re.search(param_pattern, chunk)70raise_match = re.search(raise_pattern, chunk)71return_match = re.search(return_pattern, chunk)72meta_match = re.search(meta_pattern, chunk)7374match = param_match or raise_match or return_match or meta_match75if not match:76raise ParseError(f'Error parsing meta information near "{chunk}".')7778desc_chunk = chunk[match.end():]79if param_match:80base = 'param'81key = match.group(1)82args = [match.group(2).strip()]83elif raise_match:84base = 'raise'85key = match.group(1)86args = [] if match.group(2) is None else [match.group(2).strip()]87elif return_match:88base = 'return'89key = match.group(1)90args = []91else:92base = 'meta'93key = match.group(1)94token = _clean_str(match.group(2).strip())95args = [] if token is None else re.split(r'\s+', token)9697# Make sure we didn't match some existing keyword in an incorrect98# way here:99if key in [100'param',101'keyword',102'type',103'return',104'rtype',105'yield',106'ytype',107]:108raise ParseError(109f'Error parsing meta information near "{chunk}".',110)111112desc = desc_chunk.strip()113if '\n' in desc:114first_line, rest = desc.split('\n', 1)115desc = first_line + '\n' + inspect.cleandoc(rest)116stream.append((base, key, args, desc))117118# Combine type_name, arg_name, and description information119params: T.Dict[str, T.Dict[str, T.Any]] = {}120for base, key, args, desc in stream:121if base not in ['param', 'return']:122continue # nothing to do123124(arg_name,) = args or ('return',)125info = params.setdefault(arg_name, {})126info_key = 'type_name' if 'type' in key else 'description'127info[info_key] = desc128129if base == 'return':130is_generator = key in {'ytype', 'yield'}131if info.setdefault('is_generator', is_generator) != is_generator:132raise ParseError(133f'Error parsing meta information for "{arg_name}".',134)135136meta_item: T.Union[DocstringParam, DocstringReturns, DocstringRaises, DocstringMeta]137is_done: T.Dict[str, bool] = {}138for base, key, args, desc in stream:139if base == 'param' and not is_done.get(args[0], False):140(arg_name,) = args141info = params[arg_name]142type_name = info.get('type_name')143144if type_name and type_name.endswith('?'):145is_optional = True146type_name = type_name[:-1]147else:148is_optional = False149150match = re.match(r'.*defaults to (.+)', desc, flags=re.DOTALL)151default = match.group(1).rstrip('.') if match else None152153meta_item = DocstringParam(154args=[key, arg_name],155description=info.get('description'),156arg_name=arg_name,157type_name=type_name,158is_optional=is_optional,159default=default,160)161is_done[arg_name] = True162elif base == 'return' and not is_done.get('return', False):163info = params['return']164meta_item = DocstringReturns(165args=[key],166description=info.get('description'),167type_name=info.get('type_name'),168is_generator=info.get('is_generator', False),169)170is_done['return'] = True171elif base == 'raise':172(type_name,) = args or (None,)173meta_item = DocstringRaises(174args=[key] + args,175description=desc,176type_name=type_name,177)178elif base == 'meta':179meta_item = DocstringMeta(180args=[key] + args,181description=desc,182)183else:184(key, *_) = args or ('return',)185assert is_done.get(key, False)186continue # don't append187188ret.meta.append(meta_item)189190return ret191192193def compose(194docstring: Docstring,195rendering_style: RenderingStyle = RenderingStyle.COMPACT,196indent: str = ' ',197) -> str:198"""Render a parsed docstring into docstring text.199200:param docstring: parsed docstring representation201:param rendering_style: the style to render docstrings202:param indent: the characters used as indentation in the docstring string203:returns: docstring text204"""205206def process_desc(desc: T.Optional[str], is_type: bool) -> str:207if not desc:208return ''209210if rendering_style == RenderingStyle.EXPANDED or (211rendering_style == RenderingStyle.CLEAN and not is_type212):213(first, *rest) = desc.splitlines()214return '\n'.join(215['\n' + indent + first] + [indent + line for line in rest],216)217218(first, *rest) = desc.splitlines()219return '\n'.join([' ' + first] + [indent + line for line in rest])220221parts: T.List[str] = []222if docstring.short_description:223parts.append(docstring.short_description)224if docstring.blank_after_short_description:225parts.append('')226if docstring.long_description:227parts.append(docstring.long_description)228if docstring.blank_after_long_description:229parts.append('')230231for meta in docstring.meta:232if isinstance(meta, DocstringParam):233if meta.type_name:234type_name = (235f'{meta.type_name}?'236if meta.is_optional237else meta.type_name238)239text = f'@type {meta.arg_name}:'240text += process_desc(type_name, True)241parts.append(text)242text = f'@param {meta.arg_name}:' + process_desc(243meta.description, False,244)245parts.append(text)246elif isinstance(meta, DocstringReturns):247(arg_key, type_key) = (248('yield', 'ytype')249if meta.is_generator250else ('return', 'rtype')251)252if meta.type_name:253text = f'@{type_key}:' + process_desc(meta.type_name, True)254parts.append(text)255if meta.description:256text = f'@{arg_key}:' + process_desc(meta.description, False)257parts.append(text)258elif isinstance(meta, DocstringRaises):259text = f'@raise {meta.type_name}:' if meta.type_name else '@raise:'260text += process_desc(meta.description, False)261parts.append(text)262else:263text = f'@{" ".join(meta.args)}:'264text += process_desc(meta.description, False)265parts.append(text)266return '\n'.join(parts)267268269