ClickUp Operator

"""Epyoc-style docstring parsing. .. seealso:: http://epydoc.sourceforge.net/manual-fields.html """ import inspect import re import typing as T from .common import ( Docstring, DocstringMeta, DocstringParam, DocstringRaises, DocstringReturns, DocstringStyle, ParseError, RenderingStyle, ) def _clean_str(string: str) -> T.Optional[str]: string = string.strip() if len(string) > 0: return string return None def parse(text: str) -> Docstring: """Parse the epydoc-style docstring into its components. :returns: parsed docstring """ ret = Docstring(style=DocstringStyle.EPYDOC) if not text: return ret text = inspect.cleandoc(text) match = re.search("^@", text, flags=re.M) if match: desc_chunk = text[: match.start()] meta_chunk = text[match.start() :] else: desc_chunk = text meta_chunk = "" parts = desc_chunk.split("\n", 1) ret.short_description = parts[0] or None if len(parts) > 1: long_desc_chunk = parts[1] or "" ret.blank_after_short_description = long_desc_chunk.startswith("\n") ret.blank_after_long_description = long_desc_chunk.endswith("\n\n") ret.long_description = long_desc_chunk.strip() or None param_pattern = re.compile( r"(param|keyword|type)(\s+[_A-z][_A-z0-9]*\??):" ) raise_pattern = re.compile(r"(raise)(\s+[_A-z][_A-z0-9]*\??)?:") return_pattern = re.compile(r"(return|rtype|yield|ytype):") meta_pattern = re.compile( r"([_A-z][_A-z0-9]+)((\s+[_A-z][_A-z0-9]*\??)*):" ) # tokenize stream: T.List[T.Tuple[str, str, T.List[str], str]] = [] for match in re.finditer( r"(^@.*?)(?=^@|\Z)", meta_chunk, flags=re.S | re.M ): chunk = match.group(0) if not chunk: continue param_match = re.search(param_pattern, chunk) raise_match = re.search(raise_pattern, chunk) return_match = re.search(return_pattern, chunk) meta_match = re.search(meta_pattern, chunk) match = param_match or raise_match or return_match or meta_match if not match: raise ParseError(f'Error parsing meta information near "{chunk}".') desc_chunk = chunk[match.end() :] if param_match: base = "param" key: str = match.group(1) args = [match.group(2).strip()] elif raise_match: base = "raise" key: str = match.group(1) args = [] if match.group(2) is None else [match.group(2).strip()] elif return_match: base = "return" key: str = match.group(1) args = [] else: base = "meta" key: str = match.group(1) token = _clean_str(match.group(2).strip()) args = [] if token is None else re.split(r"\s+", token) # Make sure we didn't match some existing keyword in an incorrect # way here: if key in [ "param", "keyword", "type", "return", "rtype", "yield", "ytype", ]: raise ParseError( f'Error parsing meta information near "{chunk}".' ) desc = desc_chunk.strip() if "\n" in desc: first_line, rest = desc.split("\n", 1) desc = first_line + "\n" + inspect.cleandoc(rest) stream.append((base, key, args, desc)) # Combine type_name, arg_name, and description information params: T.Dict[str, T.Dict[str, T.Any]] = {} for (base, key, args, desc) in stream: if base not in ["param", "return"]: continue # nothing to do (arg_name,) = args or ("return",) info = params.setdefault(arg_name, {}) info_key = "type_name" if "type" in key else "description" info[info_key] = desc if base == "return": is_generator = key in {"ytype", "yield"} if info.setdefault("is_generator", is_generator) != is_generator: raise ParseError( f'Error parsing meta information for "{arg_name}".' ) is_done: T.Dict[str, bool] = {} for (base, key, args, desc) in stream: if base == "param" and not is_done.get(args[0], False): (arg_name,) = args info = params[arg_name] type_name = info.get("type_name") if type_name and type_name.endswith("?"): is_optional = True type_name = type_name[:-1] else: is_optional = False match = re.match(r".*defaults to (.+)", desc, flags=re.DOTALL) default = match.group(1).rstrip(".") if match else None meta_item = DocstringParam( args=[key, arg_name], description=info.get("description"), arg_name=arg_name, type_name=type_name, is_optional=is_optional, default=default, ) is_done[arg_name] = True elif base == "return" and not is_done.get("return", False): info = params["return"] meta_item = DocstringReturns( args=[key], description=info.get("description"), type_name=info.get("type_name"), is_generator=info.get("is_generator", False), ) is_done["return"] = True elif base == "raise": (type_name,) = args or (None,) meta_item = DocstringRaises( args=[key] + args, description=desc, type_name=type_name, ) elif base == "meta": meta_item = DocstringMeta( args=[key] + args, description=desc, ) else: (key, *_) = args or ("return",) assert is_done.get(key, False) continue # don't append ret.meta.append(meta_item) return ret def compose( docstring: Docstring, rendering_style: RenderingStyle = RenderingStyle.COMPACT, indent: str = " ", ) -> str: """Render a parsed docstring into docstring text. :param docstring: parsed docstring representation :param rendering_style: the style to render docstrings :param indent: the characters used as indentation in the docstring string :returns: docstring text """ def process_desc(desc: T.Optional[str], is_type: bool) -> str: if not desc: return "" if rendering_style == RenderingStyle.EXPANDED or ( rendering_style == RenderingStyle.CLEAN and not is_type ): (first, *rest) = desc.splitlines() return "\n".join( ["\n" + indent + first] + [indent + line for line in rest] ) (first, *rest) = desc.splitlines() return "\n".join([" " + first] + [indent + line for line in rest]) parts: T.List[str] = [] if docstring.short_description: parts.append(docstring.short_description) if docstring.blank_after_short_description: parts.append("") if docstring.long_description: parts.append(docstring.long_description) if docstring.blank_after_long_description: parts.append("") for meta in docstring.meta: if isinstance(meta, DocstringParam): if meta.type_name: type_name = ( f"{meta.type_name}?" if meta.is_optional else meta.type_name ) text = f"@type {meta.arg_name}:" text += process_desc(type_name, True) parts.append(text) text = f"@param {meta.arg_name}:" + process_desc( meta.description, False ) parts.append(text) elif isinstance(meta, DocstringReturns): (arg_key, type_key) = ( ("yield", "ytype") if meta.is_generator else ("return", "rtype") ) if meta.type_name: text = f"@{type_key}:" + process_desc(meta.type_name, True) parts.append(text) if meta.description: text = f"@{arg_key}:" + process_desc(meta.description, False) parts.append(text) elif isinstance(meta, DocstringRaises): text = f"@raise {meta.type_name}:" if meta.type_name else "@raise:" text += process_desc(meta.description, False) parts.append(text) else: text = f'@{" ".join(meta.args)}:' text += process_desc(meta.description, False) parts.append(text) return "\n".join(parts)