260 lines
8.1 KiB
Python
260 lines
8.1 KiB
Python
"""ReST-style docstring parsing."""
|
|
|
|
import inspect
|
|
import re
|
|
import typing as T
|
|
|
|
from .common import (
|
|
DEPRECATION_KEYWORDS,
|
|
PARAM_KEYWORDS,
|
|
RAISES_KEYWORDS,
|
|
RETURNS_KEYWORDS,
|
|
YIELDS_KEYWORDS,
|
|
Docstring,
|
|
DocstringDeprecated,
|
|
DocstringMeta,
|
|
DocstringParam,
|
|
DocstringRaises,
|
|
DocstringReturns,
|
|
DocstringStyle,
|
|
ParseError,
|
|
RenderingStyle,
|
|
)
|
|
|
|
|
|
def _build_meta(args: T.List[str], desc: str) -> DocstringMeta:
|
|
key = args[0]
|
|
|
|
if key in PARAM_KEYWORDS:
|
|
if len(args) == 3:
|
|
key, type_name, arg_name = args
|
|
if type_name.endswith("?"):
|
|
is_optional = True
|
|
type_name = type_name[:-1]
|
|
else:
|
|
is_optional = False
|
|
elif len(args) == 2:
|
|
key, arg_name = args
|
|
type_name = None
|
|
is_optional = None
|
|
else:
|
|
raise ParseError(
|
|
f"Expected one or two arguments for a {key} keyword."
|
|
)
|
|
|
|
match = re.match(r".*defaults to (.+)", desc, flags=re.DOTALL)
|
|
default = match.group(1).rstrip(".") if match else None
|
|
|
|
return DocstringParam(
|
|
args=args,
|
|
description=desc,
|
|
arg_name=arg_name,
|
|
type_name=type_name,
|
|
is_optional=is_optional,
|
|
default=default,
|
|
)
|
|
|
|
if key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:
|
|
if len(args) == 2:
|
|
type_name = args[1]
|
|
elif len(args) == 1:
|
|
type_name = None
|
|
else:
|
|
raise ParseError(
|
|
f"Expected one or no arguments for a {key} keyword."
|
|
)
|
|
|
|
return DocstringReturns(
|
|
args=args,
|
|
description=desc,
|
|
type_name=type_name,
|
|
is_generator=key in YIELDS_KEYWORDS,
|
|
)
|
|
|
|
if key in DEPRECATION_KEYWORDS:
|
|
match = re.search(
|
|
r"^(?P<version>v?((?:\d+)(?:\.[0-9a-z\.]+))) (?P<desc>.+)",
|
|
desc,
|
|
flags=re.I,
|
|
)
|
|
return DocstringDeprecated(
|
|
args=args,
|
|
version=match.group("version") if match else None,
|
|
description=match.group("desc") if match else desc,
|
|
)
|
|
|
|
if key in RAISES_KEYWORDS:
|
|
if len(args) == 2:
|
|
type_name = args[1]
|
|
elif len(args) == 1:
|
|
type_name = None
|
|
else:
|
|
raise ParseError(
|
|
f"Expected one or no arguments for a {key} keyword."
|
|
)
|
|
return DocstringRaises(
|
|
args=args, description=desc, type_name=type_name
|
|
)
|
|
|
|
return DocstringMeta(args=args, description=desc)
|
|
|
|
|
|
def parse(text: str) -> Docstring:
|
|
"""Parse the ReST-style docstring into its components.
|
|
|
|
:returns: parsed docstring
|
|
"""
|
|
ret = Docstring(style=DocstringStyle.REST)
|
|
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
|
|
|
|
types = {}
|
|
rtypes = {}
|
|
for match in re.finditer(
|
|
r"(^:.*?)(?=^:|\Z)", meta_chunk, flags=re.S | re.M
|
|
):
|
|
chunk = match.group(0)
|
|
if not chunk:
|
|
continue
|
|
try:
|
|
args_chunk, desc_chunk = chunk.lstrip(":").split(":", 1)
|
|
except ValueError as ex:
|
|
raise ParseError(
|
|
f'Error parsing meta information near "{chunk}".'
|
|
) from ex
|
|
args = args_chunk.split()
|
|
desc = desc_chunk.strip()
|
|
|
|
if "\n" in desc:
|
|
first_line, rest = desc.split("\n", 1)
|
|
desc = first_line + "\n" + inspect.cleandoc(rest)
|
|
|
|
# Add special handling for :type a: typename
|
|
if len(args) == 2 and args[0] == "type":
|
|
types[args[1]] = desc
|
|
elif len(args) in [1, 2] and args[0] == "rtype":
|
|
rtypes[None if len(args) == 1 else args[1]] = desc
|
|
else:
|
|
ret.meta.append(_build_meta(args, desc))
|
|
|
|
for meta in ret.meta:
|
|
if isinstance(meta, DocstringParam):
|
|
meta.type_name = meta.type_name or types.get(meta.arg_name)
|
|
elif isinstance(meta, DocstringReturns):
|
|
meta.type_name = meta.type_name or rtypes.get(meta.return_name)
|
|
|
|
if not any(isinstance(m, DocstringReturns) for m in ret.meta) and rtypes:
|
|
for return_name, type_name in rtypes.items():
|
|
ret.meta.append(
|
|
DocstringReturns(
|
|
args=[],
|
|
type_name=type_name,
|
|
description=None,
|
|
is_generator=False,
|
|
return_name=return_name,
|
|
)
|
|
)
|
|
|
|
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]) -> str:
|
|
if not desc:
|
|
return ""
|
|
|
|
if rendering_style == RenderingStyle.CLEAN:
|
|
(first, *rest) = desc.splitlines()
|
|
return "\n".join([" " + first] + [indent + line for line in rest])
|
|
|
|
if rendering_style == RenderingStyle.EXPANDED:
|
|
(first, *rest) = desc.splitlines()
|
|
return "\n".join(
|
|
["\n" + indent + first] + [indent + line for line in rest]
|
|
)
|
|
|
|
return " " + desc
|
|
|
|
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_text = (
|
|
f" {meta.type_name}? "
|
|
if meta.is_optional
|
|
else f" {meta.type_name} "
|
|
)
|
|
else:
|
|
type_text = " "
|
|
if rendering_style == RenderingStyle.EXPANDED:
|
|
text = f":param {meta.arg_name}:"
|
|
text += process_desc(meta.description)
|
|
parts.append(text)
|
|
if type_text[:-1]:
|
|
parts.append(f":type {meta.arg_name}:{type_text[:-1]}")
|
|
else:
|
|
text = f":param{type_text}{meta.arg_name}:"
|
|
text += process_desc(meta.description)
|
|
parts.append(text)
|
|
elif isinstance(meta, DocstringReturns):
|
|
type_text = f" {meta.type_name}" if meta.type_name else ""
|
|
key = "yields" if meta.is_generator else "returns"
|
|
|
|
if rendering_style == RenderingStyle.EXPANDED:
|
|
if meta.description:
|
|
text = f":{key}:"
|
|
text += process_desc(meta.description)
|
|
parts.append(text)
|
|
if type_text:
|
|
parts.append(f":rtype:{type_text}")
|
|
else:
|
|
text = f":{key}{type_text}:"
|
|
text += process_desc(meta.description)
|
|
parts.append(text)
|
|
elif isinstance(meta, DocstringRaises):
|
|
type_text = f" {meta.type_name} " if meta.type_name else ""
|
|
text = f":raises{type_text}:" + process_desc(meta.description)
|
|
parts.append(text)
|
|
else:
|
|
text = f':{" ".join(meta.args)}:' + process_desc(meta.description)
|
|
parts.append(text)
|
|
return "\n".join(parts)
|