def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102 if "signature" not in self.context: self.context["signature"] = getattr(self.context["obj"], "signature", None) if "annotation" not in self.context: self.context["annotation"] = getattr(self.context["obj"], "type", empty) if "attributes" not in self.context: self.context["attributes"] = {} sections = [] current_section = [] in_code_block = False lines = docstring.split("\n") i = 0 while i < len(lines): line_lower = lines[i].lower() if in_code_block: if line_lower.lstrip(" ").startswith("```"): in_code_block = False current_section.append(lines[i]) elif line_lower in SECTIONS_TITLES: if current_section: if any(current_section): sections.append(Section(Section.Type.MARKDOWN, "\n".join(current_section))) current_section = [] section_reader = self.section_reader[SECTIONS_TITLES[line_lower]] section, i = section_reader(lines, i + 1) if section: sections.append(section) elif line_lower.lstrip(" ").startswith("```"): in_code_block = True current_section.append(lines[i]) else: if self.replace_admonitions and not in_code_block and i + 1 < len(lines): match = RE_GOOGLE_STYLE_ADMONITION.match(lines[i]) if match: groups = match.groupdict() indent = groups["indent"] if lines[i + 1].startswith(indent + " " * 4): lines[i] = f"{indent}!!! {groups['type'].lower()}" if groups["title"]: lines[i] += f' "{groups["title"]}"' current_section.append(lines[i]) i += 1 if current_section: sections.append(Section(Section.Type.MARKDOWN, "\n".join(current_section))) return sections
def read_return_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]: """ Parse an "returns" section. Arguments: lines: The return block lines. start_index: The line number to start at. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ text, i = self.read_block(lines, start_index) if self.context["signature"]: annotation = self.context["signature"].return_annotation else: annotation = self.context["annotation"] if annotation is empty: if not text: self.error("No return type annotation") else: try: type_, text = text.split(":", 1) except ValueError: self.error("No type in return description") else: annotation = type_.lstrip() text = text.lstrip() if annotation is empty and not text: self.error(f"Empty return section at line {start_index}") return None, i return Section(Section.Type.RETURN, AnnotatedObject(annotation, text)), i
def read_return_section( self, docstring_obj: Docstring, ) -> Optional[Section]: """ Parse a "returns" section. Arguments: docstring_obj: Docstring object parsed by docstring_parser. Returns: A tuple containing a `Section` (or `None`). """ return_obj = docstring_obj.returns if docstring_obj.returns else [] text = return_obj.description if return_obj else "" if self.context["signature"]: annotation = self.context["signature"].return_annotation else: annotation = self.context["annotation"] if annotation is empty: if text: annotation = return_obj.type_name or empty text = return_obj.description elif return_obj and annotation is empty: self.error("No return type annotation") if return_obj and not text: self.error("Empty return description") if not return_obj or annotation is empty or not text: return None return Section(Section.Type.RETURN, AnnotatedObject(annotation, text))
def read_exceptions_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]: """ Parse an "exceptions" section. Arguments: lines: The exceptions block lines. start_index: The line number to start at. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ exceptions = [] block, i = self.read_block_items(lines, start_index) for exception_line in block: try: annotation, description = exception_line.split(": ", 1) except ValueError: self.error(f"Failed to get 'exception: description' pair from '{exception_line}'") else: exceptions.append(AnnotatedObject(annotation, description.lstrip(" "))) if exceptions: return Section(Section.Type.EXCEPTIONS, exceptions), i self.error(f"Empty exceptions section at line {start_index}") return None, i
def read_attributes_section( self, docstring: str, docstring_obj: Docstring, ) -> Optional[Section]: """ Parse an "attributes" section. Arguments: docstring_obj: Docstring object parsed by docstring_parser. Returns: A tuple containing a `Section` (or `None`). """ attributes = [] docstring_attributes = [ p for p in docstring_obj.params if p.args[0] == "attribute" ] for attr in docstring_attributes: attributes.append( Attribute( name=attr.arg_name, annotation=attr.type_name, description=attr.description, )) if attributes: return Section(Section.Type.ATTRIBUTES, attributes) if re.search("Attributes\n", docstring): self.error("Empty attributes section") return None
def read_exceptions_section( self, docstring: str, docstring_obj: Docstring, ) -> Optional[Section]: """ Parse an "exceptions" section. Arguments: docstring_obj: Docstring object parsed by docstring_parser. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ exceptions = [] except_obj = docstring_obj.raises for exception in except_obj: exceptions.append( AnnotatedObject(exception.type_name, exception.description)) if exceptions: return Section(Section.Type.EXCEPTIONS, exceptions) if re.search("Raises\n", docstring): self.error("Empty exceptions section") return None
def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]: """ Parse an "examples" section. Arguments: lines: The examples block lines. start_index: The line number to start at. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ text, i = self.read_block(lines, start_index) sub_sections = [] in_code_example = False in_code_block = False current_text: List[str] = [] current_example: List[str] = [] for line in text.split("\n"): if is_empty_line(line): if in_code_example: if current_example: sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example))) current_example = [] in_code_example = False else: current_text.append(line) elif in_code_example: current_example.append(line) elif line.startswith("```"): in_code_block = not in_code_block current_text.append(line) elif in_code_block: current_text.append(line) elif line.startswith(">>>"): if current_text: sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text))) current_text = [] in_code_example = True current_example.append(line) else: current_text.append(line) if current_text: sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text))) elif current_example: sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example))) if sub_sections: return Section(Section.Type.EXAMPLES, sub_sections), i self.error(f"Empty examples section at line {start_index}") return None, i
def read_parameters_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]: """ Parse a "parameters" section. Arguments: lines: The parameters block lines. start_index: The line number to start at. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ parameters = [] type_: Any block, i = self.read_block_items(lines, start_index) for param_line in block: try: name_with_type, description = param_line.split(":", 1) except ValueError: self.error(f"Failed to get 'name: description' pair from '{param_line}'") continue description = description.lstrip() if " " in name_with_type: name, type_ = name_with_type.split(" ", 1) type_ = type_.strip("()") if type_.endswith(", optional"): type_ = type_[:-10] else: name = name_with_type type_ = empty default = empty annotation = type_ kind = None try: signature_param = self.context["signature"].parameters[name.lstrip("*")] # type: ignore except (AttributeError, KeyError): self.error(f"No type annotation for parameter '{name}'") else: if signature_param.annotation is not empty: annotation = signature_param.annotation if signature_param.default is not empty: default = signature_param.default kind = signature_param.kind parameters.append( Parameter(name=name, annotation=annotation, description=description, default=default, kind=kind) ) if parameters: return Section(Section.Type.PARAMETERS, parameters), i self.error(f"Empty parameters section at line {start_index}") return None, i
def _parsed_values_to_sections(self) -> List[Section]: markdown_text = "\n".join( _strip_blank_lines(self._parsed_values.description)) result = [Section(Section.Type.MARKDOWN, markdown_text)] if self._parsed_values.parameters: param_values = list(self._parsed_values.parameters.values()) result.append(Section(Section.Type.PARAMETERS, param_values)) if self._parsed_values.attributes: attribute_values = list(self._parsed_values.attributes.values()) result.append(Section(Section.Type.ATTRIBUTES, attribute_values)) if self._parsed_values.return_value is not None: result.append( Section(Section.Type.RETURN, self._parsed_values.return_value)) if self._parsed_values.exceptions: result.append( Section(Section.Type.EXCEPTIONS, self._parsed_values.exceptions)) return result
def read_parameters_section( self, docstring: str, docstring_obj: Docstring, ) -> Optional[Section]: """ Parse a "parameters" section. Arguments: lines: The parameters block lines. start_index: The line number to start at. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ parameters = [] docstring_params = [ p for p in docstring_obj.params if p.args[0] == "param" ] for param in docstring_params: name = param.arg_name kind = None type_name = param.type_name default = param.default or empty try: signature_param = self.context["signature"].parameters[ name.lstrip("*")] # type: ignore except (AttributeError, KeyError): self.error(f"No type annotation for parameter '{name}'") else: if signature_param.annotation is not empty: type_name = signature_param.annotation if signature_param.default is not empty: default = signature_param.default kind = signature_param.kind parameters.append( Parameter( name=param.arg_name, annotation=type_name, description=param.description, default=default, kind=kind, )) if parameters: return Section(Section.Type.PARAMETERS, parameters) if re.search("Parameters\n", docstring): self.error("Empty parameter section") return None
def read_attributes_section( self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]: """ Parse an "attributes" section. Arguments: lines: The parameters block lines. start_index: The line number to start at. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ attributes = [] block, i = self.read_block_items(lines, start_index) for attr_line in block: try: name_with_type, description = attr_line.split(":", 1) except ValueError: self.error( f"Failed to get 'name: description' pair from '{attr_line}'" ) continue description = description.lstrip() if " " in name_with_type: name, annotation = name_with_type.split(" ", 1) annotation = annotation.strip("()") if annotation.endswith(", optional"): annotation = annotation[:-10] else: name = name_with_type annotation = self.context["attributes"].get(name, {}).get( "annotation", empty) attributes.append( Attribute(name=name, annotation=annotation, description=description)) if attributes: return Section(Section.Type.ATTRIBUTES, attributes), i self.error(f"Empty attributes section at line {start_index}") return None, i
def read_keyword_arguments_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]: """ Parse a "keyword arguments" section. Arguments: lines: The parameters block lines. start_index: The line number to start at. Returns: A tuple containing a `Section` (or `None`) and the index at which to continue parsing. """ parameters, i = self._parse_parameters_section(lines, start_index) if parameters: return Section(Section.Type.KEYWORD_ARGS, parameters), i self.error(f"Empty keyword arguments section at line {start_index}") return None, i
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102 if "signature" not in self.context: self.context["signature"] = getattr(self.context["obj"], "signature", None) if "annotation" not in self.context: self.context["annotation"] = getattr(self.context["obj"], "type", empty) if "attributes" not in self.context: self.context["attributes"] = {} docstring_obj = parse(docstring) description_all = ( none_str_cast(docstring_obj.short_description) + "\n\n" + none_str_cast(docstring_obj.long_description)).strip() sections = [Section(Section.Type.MARKDOWN, description_all) ] if description_all else [] sections_other = [ reader(docstring_obj) # type: ignore if sec == Section.Type.RETURN else reader( docstring, docstring_obj) # type: ignore for (sec, reader) in self.section_reader.items() ] sections.extend([sec for sec in sections_other if sec]) return sections
def read_examples_section( self, docstring: str, docstring_obj: Docstring, ) -> Optional[Section]: """ Parse an "examples" section. Arguments: docstring_obj: Docstring object parsed by docstring_parser. Returns: A tuple containing a `Section` (or `None`). """ text = next( (meta.description for meta in docstring_obj.meta if isinstance(meta, DocstringMeta) and meta.args[0] == "examples" ), "", ) sub_sections = [] in_code_example = False in_code_block = False current_text: List[str] = [] current_example: List[str] = [] if text: for line in text.split("\n"): if is_empty_line(line): if in_code_example: if current_example: sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example))) current_example = [] in_code_example = False else: current_text.append(line) elif in_code_example: current_example.append(line) elif line.startswith("```"): in_code_block = not in_code_block current_text.append(line) elif in_code_block: current_text.append(line) elif line.startswith(">>>"): if current_text: sub_sections.append( (Section.Type.MARKDOWN, "\n".join(current_text))) current_text = [] in_code_example = True current_example.append(line) else: current_text.append(line) if current_text: sub_sections.append( (Section.Type.MARKDOWN, "\n".join(current_text))) elif current_example: sub_sections.append( (Section.Type.EXAMPLES, "\n".join(current_example))) if sub_sections: return Section(Section.Type.EXAMPLES, sub_sections) if re.search("Examples\n", docstring): self.error("Empty examples section") return None
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102 sections = [] current_section = [] in_code_block = False lines = docstring.split("\n") i = 0 while i < len(lines): line_lower = lines[i].lower() if in_code_block: if line_lower.lstrip(" ").startswith("```"): in_code_block = False current_section.append(lines[i]) elif line_lower in TITLES_PARAMETERS: if current_section: if any(current_section): sections.append( Section(Section.Type.MARKDOWN, "\n".join(current_section))) current_section = [] section, i = self.read_parameters_section(lines, i + 1) if section: sections.append(section) elif line_lower in TITLES_EXCEPTIONS: if current_section: if any(current_section): sections.append( Section(Section.Type.MARKDOWN, "\n".join(current_section))) current_section = [] section, i = self.read_exceptions_section(lines, i + 1) if section: sections.append(section) elif line_lower in TITLES_RETURN: if current_section: if any(current_section): sections.append( Section(Section.Type.MARKDOWN, "\n".join(current_section))) current_section = [] section, i = self.read_return_section(lines, i + 1) if section: sections.append(section) elif line_lower.lstrip(" ").startswith("```"): in_code_block = True current_section.append(lines[i]) else: if self.replace_admonitions and not in_code_block and i + 1 < len( lines): match = RE_GOOGLE_STYLE_ADMONITION.match(lines[i]) if match: groups = match.groupdict() indent = groups["indent"] if lines[i + 1].startswith(indent + " " * 4): lines[i] = f"{indent}!!! {groups['type'].lower()}" if groups["title"]: lines[i] += f' "{groups["title"]}"' current_section.append(lines[i]) i += 1 if current_section: sections.append( Section(Section.Type.MARKDOWN, "\n".join(current_section))) return sections