Я пытаюсь программно принять («отразить») строки документации в стиле Google. Я использую sphinx.ext.napoleon
, так как, похоже, не многие инструменты делают это. Я следую этому примеру с помощью функции ниже:
from sphinx.ext.napoleon import Config, GoogleDocstring
def foo(arg: int | None = 5) -> None:
"""Stub summary.
Args:
arg(int): Optional integer defaulted to 5.
"""
docstring = GoogleDocstring(foo.__doc__)
print(docstring)
Однако мое использование не преобразует печатный вывод автоматически в стиль reST, как это делает пример Sphinx.
Итак, это подводит меня к моему вопросу. Как можно программно получить сводку, расширенное описание, имена аргументов и описания аргументов из строки документации Google Style? В идеале они преобразуются в какую-то структуру данных (например, dict
или dataclass
).
Вместо этого вы можете попробовать использовать встроенный модуль inspect
для получения строки документации, например:
import inspect
docstring = GoogleDocstring(inspect.getdoc(foo))
print(docstring)
Это будет напечатано в следующем формате:
Stub summary.
:param arg: Optional integer defaulted to 5.
:type arg: int
Кажется, разница между inspect.getdoc(foo)
и foo.__doc__
заключается в отступах:
print(foo.__doc__)
Stub summary.
Args:
arg(int): Optional integer defaulted to 5.
print(inspect.getdoc(foo))
Stub summary.
Args:
arg(int): Optional integer defaulted to 5.
Чтобы использовать атрибут __doc__
, вы можете применить функцию prepare_docstring
, например:
from sphinx.util.docstrings import prepare_docstring
docstring = GoogleDocstring(prepare_docstring(foo.__doc__))
print(docstring)
Затем вы можете либо написать свой собственный парсер, либо использовать сторонние библиотеки, такие как doctrans , docstring_parser и т. д. Для примера и простоты я взял решение ниже из источника doctrans. Поскольку он поддерживает больше, чем требуется, а также я не хотел устанавливать и загрязнять систему, поэтому я просто использовал код напрямую:
import re
import sys
PARAM_OR_RETURNS_REGEX = re.compile(":(?:param|returns?)")
RETURNS_REGEX = re.compile(":returns?: (?P<doc>.*)", re.DOTALL)
PARAM_REGEX = re.compile(
r":param (?P<name>[\*\w]+): (?P<doc>.*?)"
r"(?:(?=:param)|(?=:return)|(?=:raises)|\Z)",
re.DOTALL,
)
def trim(docstring):
"""Trim function from PEP-257."""
if not docstring:
return ""
# Convert tabs to spaces (following the normal Python rules)
# and split into a list of lines:
lines = docstring.expandtabs().splitlines()
# Determine minimum indentation (first line doesn't count):
indent = sys.maxsize
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < sys.maxsize:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Strip off trailing and leading blank lines:
while trimmed and not trimmed[-1]:
trimmed.pop()
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Current code/unittests expects a line return at
# end of multiline docstrings
# workaround expected behavior from unittests
if "\n" in docstring:
trimmed.append("")
# Return a single string:
return "\n".join(trimmed)
def reindent(string):
return "\n".join(line.strip() for line in string.strip().split("\n"))
def doc_to_type_doc(name, doc):
doc = trim(doc).splitlines()
docs, typ = [], []
for line in doc:
if line.startswith(":type"):
line = line[len(":type ") :]
colon_at = line.find(":")
found_name = line[:colon_at]
assert name == found_name, f"{name!r} != {found_name!r}"
line = line[colon_at + 2 :]
typ.append(
line[3:-3] if line.startswith("```") and line.endswith("```") else line
)
elif len(typ):
typ.append(line)
else:
docs.append(line)
return dict(doc = "\n".join(docs), **{"typ": "\n".join(typ)} if len(typ) else {})
def parse_docstring(docstring):
"""Parse the docstring into its components.
:returns: a dictionary of form
{
'short_description': ...,
'long_description': ...,
'params': [{'name': ..., 'doc': ..., 'typ': ...}, ...],
"returns': {'name': ..., 'typ': ...}
}
"""
short_description = long_description = returns = ""
params = []
if docstring:
docstring = trim(docstring.lstrip("\n"))
lines = docstring.split("\n", 1)
short_description = lines[0]
if len(lines) > 1:
long_description = lines[1].strip()
params_returns_desc = None
match = PARAM_OR_RETURNS_REGEX.search(long_description)
if match:
long_desc_end = match.start()
params_returns_desc = long_description[long_desc_end:].strip()
long_description = long_description[:long_desc_end].rstrip()
if params_returns_desc:
params = [
dict(name=name, **doc_to_type_doc(name, doc))
for name, doc in PARAM_REGEX.findall(params_returns_desc)
]
match = RETURNS_REGEX.search(params_returns_desc)
if match:
returns = reindent(match.group("doc"))
if returns:
r_dict = {"name": ""}
for idx, char in enumerate(returns):
if char == ":":
r_dict["typ"] = returns[idx + len(":rtype:") :].strip()
if r_dict["typ"].startswith("```") and r_dict[
"typ"
].endswith("```"):
r_dict["typ"] = r_dict["typ"][3:-3]
break
r_dict["name"] += char
r_dict["name"] = r_dict["name"].rstrip()
returns = r_dict
return {
"short_description": short_description,
"long_description": long_description,
"params": params,
"returns": returns,
}
parse_docstring("\n".join(docstring.lines()))