185 lines
4.8 KiB
Python
185 lines
4.8 KiB
Python
import re
|
|
from textwrap import dedent
|
|
from typing import List
|
|
|
|
from .types import Converter
|
|
|
|
|
|
# All possible sections in Google style docstrings
|
|
SECTION_HEADERS: List[str] = [
|
|
"Args",
|
|
"Returns",
|
|
"Raises",
|
|
"Yields",
|
|
"Example",
|
|
"Examples",
|
|
"Attributes",
|
|
"Note",
|
|
"Todo",
|
|
]
|
|
|
|
# These sections will not be parsed as a list of arguments/return values/etc
|
|
PLAIN_TEXT_SECTIONS: List[str] = [
|
|
"Examples",
|
|
"Example",
|
|
"Note",
|
|
"Todo",
|
|
]
|
|
|
|
ESCAPE_RULES = {
|
|
# Avoid Markdown in magic methods or filenames like __init__.py
|
|
r"__(?P<text>\S+)__": r"\_\_\g<text>\_\_",
|
|
}
|
|
|
|
|
|
class Section:
|
|
def __init__(self, name: str, content: str) -> None:
|
|
self.name = name
|
|
self.content = ""
|
|
|
|
self._parse(content)
|
|
|
|
def _parse(self, content: str) -> None:
|
|
content = content.rstrip("\n")
|
|
|
|
if self.name in PLAIN_TEXT_SECTIONS:
|
|
self.content = dedent(content)
|
|
return
|
|
|
|
parts = []
|
|
cur_part = []
|
|
|
|
for line in content.split("\n"):
|
|
line = line.replace(" ", "", 1)
|
|
|
|
if line.startswith(" "):
|
|
# Continuation from a multiline description
|
|
cur_part.append(line)
|
|
continue
|
|
|
|
if cur_part:
|
|
# Leaving multiline description
|
|
parts.append(cur_part)
|
|
cur_part = [line]
|
|
else:
|
|
# Entering new description part
|
|
cur_part.append(line)
|
|
|
|
# Last part
|
|
parts.append(cur_part)
|
|
|
|
# Format section
|
|
for part in parts:
|
|
indentation = ""
|
|
skip_first = False
|
|
|
|
if ":" in part[0]:
|
|
spl = part[0].split(":")
|
|
|
|
arg = spl[0]
|
|
description = ":".join(spl[1:]).lstrip()
|
|
indentation = (len(arg) + 6) * " "
|
|
|
|
if description:
|
|
self.content += "- `{}`: {}\n".format(arg, description)
|
|
else:
|
|
skip_first = True
|
|
self.content += "- `{}`: ".format(arg)
|
|
else:
|
|
self.content += "- {}\n".format(part[0])
|
|
|
|
for n, line in enumerate(part[1:]):
|
|
if skip_first and n == 0:
|
|
# This ensures that indented args get moved to the
|
|
# previous line
|
|
self.content += "{}\n".format(line.lstrip())
|
|
continue
|
|
|
|
self.content += "{}{}\n".format(indentation, line.lstrip())
|
|
|
|
self.content = self.content.rstrip("\n")
|
|
|
|
def as_markdown(self) -> str:
|
|
return "#### {}\n\n{}\n\n".format(self.name, self.content)
|
|
|
|
|
|
class GoogleDocstring:
|
|
def __init__(self, docstring: str) -> None:
|
|
self.sections: List[Section] = []
|
|
self.description: str = ""
|
|
|
|
self._parse(docstring)
|
|
|
|
def _parse(self, docstring: str) -> None:
|
|
self.sections = []
|
|
self.description = ""
|
|
|
|
buf = ""
|
|
cur_section = ""
|
|
|
|
for line in docstring.split("\n"):
|
|
if is_section(line):
|
|
# Entering new section
|
|
if cur_section:
|
|
# Leaving previous section, save it and reset buffer
|
|
self.sections.append(Section(cur_section, buf))
|
|
buf = ""
|
|
|
|
# Remember currently parsed section
|
|
cur_section = line.rstrip(":")
|
|
continue
|
|
|
|
# Parse section content
|
|
if cur_section:
|
|
buf += line + "\n"
|
|
else:
|
|
# Before setting cur_section, we're parsing the function description
|
|
self.description += line + "\n"
|
|
|
|
# Last section
|
|
self.sections.append(Section(cur_section, buf))
|
|
|
|
def as_markdown(self) -> str:
|
|
text = self.description
|
|
|
|
for section in self.sections:
|
|
text += section.as_markdown()
|
|
|
|
return text.rstrip("\n") + "\n" # Only keep one last newline
|
|
|
|
|
|
def is_section(line: str) -> bool:
|
|
for section in SECTION_HEADERS:
|
|
if re.search(r"{}:".format(section), line):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def looks_like_google(value: str) -> bool:
|
|
for section in SECTION_HEADERS:
|
|
if re.search(r"{}:\n".format(section), value):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def google_to_markdown(text: str, extract_signature: bool = True) -> str:
|
|
# Escape parts we don't want to render
|
|
for pattern, replacement in ESCAPE_RULES.items():
|
|
text = re.sub(pattern, replacement, text)
|
|
|
|
docstring = GoogleDocstring(text)
|
|
|
|
return docstring.as_markdown()
|
|
|
|
|
|
class GoogleConverter(Converter):
|
|
|
|
priority = 75
|
|
|
|
def can_convert(self, docstring):
|
|
return looks_like_google(docstring)
|
|
|
|
def convert(self, docstring):
|
|
return google_to_markdown(docstring)
|