structure saas with tools

This commit is contained in:
Davidson Gomes
2025-04-25 15:30:54 -03:00
commit 1aef473937
16434 changed files with 6584257 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
"""Parse docstrings as per Sphinx notation."""
from .common import (
Docstring,
DocstringDeprecated,
DocstringMeta,
DocstringParam,
DocstringRaises,
DocstringReturns,
DocstringStyle,
ParseError,
RenderingStyle,
)
from .parser import compose, parse, parse_from_object
from .util import combine_docstrings
Style = DocstringStyle # backwards compatibility
__all__ = [
"parse",
"parse_from_object",
"combine_docstrings",
"compose",
"ParseError",
"Docstring",
"DocstringMeta",
"DocstringParam",
"DocstringRaises",
"DocstringReturns",
"DocstringDeprecated",
"DocstringStyle",
"RenderingStyle",
"Style",
]

View File

@@ -0,0 +1,138 @@
"""Attribute docstrings parsing.
.. seealso:: https://peps.python.org/pep-0257/#what-is-a-docstring
"""
import ast
import inspect
import sys
import textwrap
import typing as T
from types import ModuleType
from .common import Docstring, DocstringParam
ast_constant_attr = {ast.Constant: "value"}
if sys.version_info[:2] <= (3, 7):
ast_constant_attr.update(
{
ast.NameConstant: "value",
ast.Num: "n",
ast.Str: "s",
}
)
def ast_get_constant_value(node: ast.AST) -> T.Any:
"""Return the constant's value if the given node is a constant."""
return getattr(node, ast_constant_attr[node.__class__])
def ast_unparse(node: ast.AST) -> T.Optional[str]:
"""Convert the AST node to source code as a string."""
if hasattr(ast, "unparse"):
return ast.unparse(node)
# Support simple cases in Python < 3.9
if isinstance(node, (ast.Str, ast.Num, ast.NameConstant, ast.Constant)):
return str(ast_get_constant_value(node))
if isinstance(node, ast.Name):
return node.id
return None
def ast_is_literal_str(node: ast.AST) -> bool:
"""Return True if the given node is a literal string."""
return (
isinstance(node, ast.Expr)
and isinstance(node.value, (ast.Constant, ast.Str))
and isinstance(ast_get_constant_value(node.value), str)
)
def ast_get_attribute(
node: ast.AST,
) -> T.Optional[T.Tuple[str, T.Optional[str], T.Optional[str]]]:
"""Return name, type and default if the given node is an attribute."""
if isinstance(node, (ast.Assign, ast.AnnAssign)):
target = (
node.targets[0] if isinstance(node, ast.Assign) else node.target
)
if isinstance(target, ast.Name):
type_str = None
if isinstance(node, ast.AnnAssign):
type_str = ast_unparse(node.annotation)
default = None
if node.value:
default = ast_unparse(node.value)
return target.id, type_str, default
return None
class AttributeDocstrings(ast.NodeVisitor):
"""An ast.NodeVisitor that collects attribute docstrings."""
attr_docs = None
prev_attr = None
def visit(self, node):
if self.prev_attr and ast_is_literal_str(node):
attr_name, attr_type, attr_default = self.prev_attr
self.attr_docs[attr_name] = (
ast_get_constant_value(node.value),
attr_type,
attr_default,
)
self.prev_attr = ast_get_attribute(node)
if isinstance(node, (ast.ClassDef, ast.Module)):
self.generic_visit(node)
def get_attr_docs(
self, component: T.Any
) -> T.Dict[str, T.Tuple[str, T.Optional[str], T.Optional[str]]]:
"""Get attribute docstrings from the given component.
:param component: component to process (class or module)
:returns: for each attribute docstring, a tuple with (description,
type, default)
"""
self.attr_docs = {}
self.prev_attr = None
try:
source = textwrap.dedent(inspect.getsource(component))
except OSError:
pass
else:
tree = ast.parse(source)
if inspect.ismodule(component):
self.visit(tree)
elif isinstance(tree, ast.Module) and isinstance(
tree.body[0], ast.ClassDef
):
self.visit(tree.body[0])
return self.attr_docs
def add_attribute_docstrings(
obj: T.Union[type, ModuleType], docstring: Docstring
) -> None:
"""Add attribute docstrings found in the object's source code.
:param obj: object from which to parse attribute docstrings
:param docstring: Docstring object where found attributes are added
:returns: list with names of added attributes
"""
params = set(p.arg_name for p in docstring.params)
for arg_name, (description, type_name, default) in (
AttributeDocstrings().get_attr_docs(obj).items()
):
if arg_name not in params:
param = DocstringParam(
args=["attribute", arg_name],
description=description,
arg_name=arg_name,
type_name=type_name,
is_optional=default is not None,
default=default,
)
docstring.meta.append(param)

View File

@@ -0,0 +1,228 @@
"""Common methods for parsing."""
import enum
import typing as T
PARAM_KEYWORDS = {
"param",
"parameter",
"arg",
"argument",
"attribute",
"key",
"keyword",
}
RAISES_KEYWORDS = {"raises", "raise", "except", "exception"}
DEPRECATION_KEYWORDS = {"deprecation", "deprecated"}
RETURNS_KEYWORDS = {"return", "returns"}
YIELDS_KEYWORDS = {"yield", "yields"}
EXAMPLES_KEYWORDS = {"example", "examples"}
class ParseError(RuntimeError):
"""Base class for all parsing related errors."""
class DocstringStyle(enum.Enum):
"""Docstring style."""
REST = 1
GOOGLE = 2
NUMPYDOC = 3
EPYDOC = 4
AUTO = 255
class RenderingStyle(enum.Enum):
"""Rendering style when unparsing parsed docstrings."""
COMPACT = 1
CLEAN = 2
EXPANDED = 3
class DocstringMeta:
"""Docstring meta information.
Symbolizes lines in form of
:param arg: description
:raises ValueError: if something happens
"""
def __init__(
self, args: T.List[str], description: T.Optional[str]
) -> None:
"""Initialize self.
:param args: list of arguments. The exact content of this variable is
dependent on the kind of docstring; it's used to distinguish
between custom docstring meta information items.
:param description: associated docstring description.
"""
self.args = args
self.description = description
class DocstringParam(DocstringMeta):
"""DocstringMeta symbolizing :param metadata."""
def __init__(
self,
args: T.List[str],
description: T.Optional[str],
arg_name: str,
type_name: T.Optional[str],
is_optional: T.Optional[bool],
default: T.Optional[str],
) -> None:
"""Initialize self."""
super().__init__(args, description)
self.arg_name = arg_name
self.type_name = type_name
self.is_optional = is_optional
self.default = default
class DocstringReturns(DocstringMeta):
"""DocstringMeta symbolizing :returns or :yields metadata."""
def __init__(
self,
args: T.List[str],
description: T.Optional[str],
type_name: T.Optional[str],
is_generator: bool,
return_name: T.Optional[str] = None,
) -> None:
"""Initialize self."""
super().__init__(args, description)
self.type_name = type_name
self.is_generator = is_generator
self.return_name = return_name
class DocstringRaises(DocstringMeta):
"""DocstringMeta symbolizing :raises metadata."""
def __init__(
self,
args: T.List[str],
description: T.Optional[str],
type_name: T.Optional[str],
) -> None:
"""Initialize self."""
super().__init__(args, description)
self.type_name = type_name
self.description = description
class DocstringDeprecated(DocstringMeta):
"""DocstringMeta symbolizing deprecation metadata."""
def __init__(
self,
args: T.List[str],
description: T.Optional[str],
version: T.Optional[str],
) -> None:
"""Initialize self."""
super().__init__(args, description)
self.version = version
self.description = description
class DocstringExample(DocstringMeta):
"""DocstringMeta symbolizing example metadata."""
def __init__(
self,
args: T.List[str],
snippet: T.Optional[str],
description: T.Optional[str],
) -> None:
"""Initialize self."""
super().__init__(args, description)
self.snippet = snippet
self.description = description
class Docstring:
"""Docstring object representation."""
def __init__(
self,
style=None, # type: T.Optional[DocstringStyle]
) -> None:
"""Initialize self."""
self.short_description = None # type: T.Optional[str]
self.long_description = None # type: T.Optional[str]
self.blank_after_short_description = False
self.blank_after_long_description = False
self.meta = [] # type: T.List[DocstringMeta]
self.style = style # type: T.Optional[DocstringStyle]
@property
def description(self) -> T.Optional[str]:
"""Return the full description of the function
Returns None if the docstring did not include any description
"""
ret = []
if self.short_description:
ret.append(self.short_description)
if self.blank_after_short_description:
ret.append("")
if self.long_description:
ret.append(self.long_description)
if not ret:
return None
return "\n".join(ret)
@property
def params(self) -> T.List[DocstringParam]:
"""Return a list of information on function params."""
return [item for item in self.meta if isinstance(item, DocstringParam)]
@property
def raises(self) -> T.List[DocstringRaises]:
"""Return a list of information on the exceptions that the function
may raise.
"""
return [
item for item in self.meta if isinstance(item, DocstringRaises)
]
@property
def returns(self) -> T.Optional[DocstringReturns]:
"""Return a single information on function return.
Takes the first return information.
"""
for item in self.meta:
if isinstance(item, DocstringReturns):
return item
return None
@property
def many_returns(self) -> T.List[DocstringReturns]:
"""Return a list of information on function return."""
return [
item for item in self.meta if isinstance(item, DocstringReturns)
]
@property
def deprecation(self) -> T.Optional[DocstringDeprecated]:
"""Return a single information on function deprecation notes."""
for item in self.meta:
if isinstance(item, DocstringDeprecated):
return item
return None
@property
def examples(self) -> T.List[DocstringExample]:
"""Return a list of information on function examples."""
return [
item for item in self.meta if isinstance(item, DocstringExample)
]

View File

@@ -0,0 +1,268 @@
"""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)

View File

@@ -0,0 +1,408 @@
"""Google-style docstring parsing."""
import inspect
import re
import typing as T
from collections import OrderedDict, namedtuple
from enum import IntEnum
from .common import (
EXAMPLES_KEYWORDS,
PARAM_KEYWORDS,
RAISES_KEYWORDS,
RETURNS_KEYWORDS,
YIELDS_KEYWORDS,
Docstring,
DocstringExample,
DocstringMeta,
DocstringParam,
DocstringRaises,
DocstringReturns,
DocstringStyle,
ParseError,
RenderingStyle,
)
class SectionType(IntEnum):
"""Types of sections."""
SINGULAR = 0
"""For sections like examples."""
MULTIPLE = 1
"""For sections like params."""
SINGULAR_OR_MULTIPLE = 2
"""For sections like returns or yields."""
class Section(namedtuple("SectionBase", "title key type")):
"""A docstring section."""
GOOGLE_TYPED_ARG_REGEX = re.compile(r"\s*(.+?)\s*\(\s*(.*[^\s]+)\s*\)")
GOOGLE_ARG_DESC_REGEX = re.compile(r".*\. Defaults to (.+)\.")
MULTIPLE_PATTERN = re.compile(r"(\s*[^:\s]+:)|([^:]*\]:.*)")
DEFAULT_SECTIONS = [
Section("Arguments", "param", SectionType.MULTIPLE),
Section("Args", "param", SectionType.MULTIPLE),
Section("Parameters", "param", SectionType.MULTIPLE),
Section("Params", "param", SectionType.MULTIPLE),
Section("Raises", "raises", SectionType.MULTIPLE),
Section("Exceptions", "raises", SectionType.MULTIPLE),
Section("Except", "raises", SectionType.MULTIPLE),
Section("Attributes", "attribute", SectionType.MULTIPLE),
Section("Example", "examples", SectionType.SINGULAR),
Section("Examples", "examples", SectionType.SINGULAR),
Section("Returns", "returns", SectionType.SINGULAR_OR_MULTIPLE),
Section("Yields", "yields", SectionType.SINGULAR_OR_MULTIPLE),
]
class GoogleParser:
"""Parser for Google-style docstrings."""
def __init__(
self, sections: T.Optional[T.List[Section]] = None, title_colon=True
):
"""Setup sections.
:param sections: Recognized sections or None to defaults.
:param title_colon: require colon after section title.
"""
if not sections:
sections = DEFAULT_SECTIONS
self.sections = {s.title: s for s in sections}
self.title_colon = title_colon
self._setup()
def _setup(self):
if self.title_colon:
colon = ":"
else:
colon = ""
self.titles_re = re.compile(
"^("
+ "|".join(f"({t})" for t in self.sections)
+ ")"
+ colon
+ "[ \t\r\f\v]*$",
flags=re.M,
)
def _build_meta(self, text: str, title: str) -> DocstringMeta:
"""Build docstring element.
:param text: docstring element text
:param title: title of section containing element
:return:
"""
section = self.sections[title]
if (
section.type == SectionType.SINGULAR_OR_MULTIPLE
and not MULTIPLE_PATTERN.match(text)
) or section.type == SectionType.SINGULAR:
return self._build_single_meta(section, text)
if ":" not in text:
raise ParseError(f"Expected a colon in {text!r}.")
# Split spec and description
before, desc = text.split(":", 1)
if desc:
desc = desc[1:] if desc[0] == " " else desc
if "\n" in desc:
first_line, rest = desc.split("\n", 1)
desc = first_line + "\n" + inspect.cleandoc(rest)
desc = desc.strip("\n")
return self._build_multi_meta(section, before, desc)
@staticmethod
def _build_single_meta(section: Section, desc: str) -> DocstringMeta:
if section.key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:
return DocstringReturns(
args=[section.key],
description=desc,
type_name=None,
is_generator=section.key in YIELDS_KEYWORDS,
)
if section.key in RAISES_KEYWORDS:
return DocstringRaises(
args=[section.key], description=desc, type_name=None
)
if section.key in EXAMPLES_KEYWORDS:
return DocstringExample(
args=[section.key], snippet=None, description=desc
)
if section.key in PARAM_KEYWORDS:
raise ParseError("Expected paramenter name.")
return DocstringMeta(args=[section.key], description=desc)
@staticmethod
def _build_multi_meta(
section: Section, before: str, desc: str
) -> DocstringMeta:
if section.key in PARAM_KEYWORDS:
match = GOOGLE_TYPED_ARG_REGEX.match(before)
if match:
arg_name, type_name = match.group(1, 2)
if type_name.endswith(", optional"):
is_optional = True
type_name = type_name[:-10]
elif type_name.endswith("?"):
is_optional = True
type_name = type_name[:-1]
else:
is_optional = False
else:
arg_name, type_name = before, None
is_optional = None
match = GOOGLE_ARG_DESC_REGEX.match(desc)
default = match.group(1) if match else None
return DocstringParam(
args=[section.key, before],
description=desc,
arg_name=arg_name,
type_name=type_name,
is_optional=is_optional,
default=default,
)
if section.key in RETURNS_KEYWORDS | YIELDS_KEYWORDS:
return DocstringReturns(
args=[section.key, before],
description=desc,
type_name=before,
is_generator=section.key in YIELDS_KEYWORDS,
)
if section.key in RAISES_KEYWORDS:
return DocstringRaises(
args=[section.key, before], description=desc, type_name=before
)
return DocstringMeta(args=[section.key, before], description=desc)
def add_section(self, section: Section):
"""Add or replace a section.
:param section: The new section.
"""
self.sections[section.title] = section
self._setup()
def parse(self, text: str) -> Docstring:
"""Parse the Google-style docstring into its components.
:returns: parsed docstring
"""
ret = Docstring(style=DocstringStyle.GOOGLE)
if not text:
return ret
# Clean according to PEP-0257
text = inspect.cleandoc(text)
# Find first title and split on its position
match = self.titles_re.search(text)
if match:
desc_chunk = text[: match.start()]
meta_chunk = text[match.start() :]
else:
desc_chunk = text
meta_chunk = ""
# Break description into short and long parts
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
# Split by sections determined by titles
matches = list(self.titles_re.finditer(meta_chunk))
if not matches:
return ret
splits = []
for j in range(len(matches) - 1):
splits.append((matches[j].end(), matches[j + 1].start()))
splits.append((matches[-1].end(), len(meta_chunk)))
chunks = OrderedDict() # type: T.Mapping[str,str]
for j, (start, end) in enumerate(splits):
title = matches[j].group(1)
if title not in self.sections:
continue
# Clear Any Unknown Meta
# Ref: https://github.com/rr-/docstring_parser/issues/29
meta_details = meta_chunk[start:end]
unknown_meta = re.search(r"\n\S", meta_details)
if unknown_meta is not None:
meta_details = meta_details[: unknown_meta.start()]
chunks[title] = meta_details.strip("\n")
if not chunks:
return ret
# Add elements from each chunk
for title, chunk in chunks.items():
# Determine indent
indent_match = re.search(r"^\s*", chunk)
if not indent_match:
raise ParseError(f'Can\'t infer indent from "{chunk}"')
indent = indent_match.group()
# Check for singular elements
if self.sections[title].type in [
SectionType.SINGULAR,
SectionType.SINGULAR_OR_MULTIPLE,
]:
part = inspect.cleandoc(chunk)
ret.meta.append(self._build_meta(part, title))
continue
# Split based on lines which have exactly that indent
_re = "^" + indent + r"(?=\S)"
c_matches = list(re.finditer(_re, chunk, flags=re.M))
if not c_matches:
raise ParseError(f'No specification for "{title}": "{chunk}"')
c_splits = []
for j in range(len(c_matches) - 1):
c_splits.append((c_matches[j].end(), c_matches[j + 1].start()))
c_splits.append((c_matches[-1].end(), len(chunk)))
for j, (start, end) in enumerate(c_splits):
part = chunk[start:end].strip("\n")
ret.meta.append(self._build_meta(part, title))
return ret
def parse(text: str) -> Docstring:
"""Parse the Google-style docstring into its components.
:returns: parsed docstring
"""
return GoogleParser().parse(text)
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_one(
one: T.Union[DocstringParam, DocstringReturns, DocstringRaises]
):
head = ""
if isinstance(one, DocstringParam):
head += one.arg_name or ""
elif isinstance(one, DocstringReturns):
head += one.return_name or ""
if isinstance(one, DocstringParam) and one.is_optional:
optional = (
"?"
if rendering_style == RenderingStyle.COMPACT
else ", optional"
)
else:
optional = ""
if one.type_name and head:
head += f" ({one.type_name}{optional}):"
elif one.type_name:
head += f"{one.type_name}{optional}:"
else:
head += ":"
head = indent + head
if one.description and rendering_style == RenderingStyle.EXPANDED:
body = f"\n{indent}{indent}".join(
[head] + one.description.splitlines()
)
parts.append(body)
elif one.description:
(first, *rest) = one.description.splitlines()
body = f"\n{indent}{indent}".join([head + " " + first] + rest)
parts.append(body)
else:
parts.append(head)
def process_sect(name: str, args: T.List[T.Any]):
if args:
parts.append(name)
for arg in args:
process_one(arg)
parts.append("")
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("")
process_sect(
"Args:", [p for p in docstring.params or [] if p.args[0] == "param"]
)
process_sect(
"Attributes:",
[p for p in docstring.params or [] if p.args[0] == "attribute"],
)
process_sect(
"Returns:",
[p for p in docstring.many_returns or [] if not p.is_generator],
)
process_sect(
"Yields:", [p for p in docstring.many_returns or [] if p.is_generator]
)
process_sect("Raises:", docstring.raises or [])
if docstring.returns and not docstring.many_returns:
ret = docstring.returns
parts.append("Yields:" if ret else "Returns:")
parts.append("-" * len(parts[-1]))
process_one(ret)
for meta in docstring.meta:
if isinstance(
meta, (DocstringParam, DocstringReturns, DocstringRaises)
):
continue # Already handled
parts.append(meta.args[0].replace("_", "").title() + ":")
if meta.description:
lines = [indent + l for l in meta.description.splitlines()]
parts.append("\n".join(lines))
parts.append("")
while parts and not parts[-1]:
parts.pop()
return "\n".join(parts)

View File

@@ -0,0 +1,532 @@
"""Numpydoc-style docstring parsing.
:see: https://numpydoc.readthedocs.io/en/latest/format.html
"""
import inspect
import itertools
import re
import typing as T
from textwrap import dedent
from .common import (
Docstring,
DocstringDeprecated,
DocstringExample,
DocstringMeta,
DocstringParam,
DocstringRaises,
DocstringReturns,
DocstringStyle,
RenderingStyle,
)
def _pairwise(iterable: T.Iterable, end=None) -> T.Iterable:
left, right = itertools.tee(iterable)
next(right, None)
return itertools.zip_longest(left, right, fillvalue=end)
def _clean_str(string: str) -> T.Optional[str]:
string = string.strip()
if len(string) > 0:
return string
return None
KV_REGEX = re.compile(r"^[^\s].*$", flags=re.M)
PARAM_KEY_REGEX = re.compile(r"^(?P<name>.*?)(?:\s*:\s*(?P<type>.*?))?$")
PARAM_OPTIONAL_REGEX = re.compile(r"(?P<type>.*?)(?:, optional|\(optional\))$")
# numpydoc format has no formal grammar for this,
# but we can make some educated guesses...
PARAM_DEFAULT_REGEX = re.compile(
r"(?<!\S)[Dd]efault(?: is | = |: |s to |)\s*(?P<value>[\w\-\.]*\w)"
)
RETURN_KEY_REGEX = re.compile(r"^(?:(?P<name>.*?)\s*:\s*)?(?P<type>.*?)$")
class Section:
"""Numpydoc section parser.
:param title: section title. For most sections, this is a heading like
"Parameters" which appears on its own line, underlined by
en-dashes ('-') on the following line.
:param key: meta key string. In the parsed ``DocstringMeta`` instance this
will be the first element of the ``args`` attribute list.
"""
def __init__(self, title: str, key: str) -> None:
self.title = title
self.key = key
@property
def title_pattern(self) -> str:
"""Regular expression pattern matching this section's header.
This pattern will match this instance's ``title`` attribute in
an anonymous group.
"""
dashes = "-" * len(self.title)
return rf"^({self.title})\s*?\n{dashes}\s*$"
def parse(self, text: str) -> T.Iterable[DocstringMeta]:
"""Parse ``DocstringMeta`` objects from the body of this section.
:param text: section body text. Should be cleaned with
``inspect.cleandoc`` before parsing.
"""
yield DocstringMeta([self.key], description=_clean_str(text))
class _KVSection(Section):
"""Base parser for numpydoc sections with key-value syntax.
E.g. sections that look like this:
key
value
key2 : type
values can also span...
... multiple lines
"""
def _parse_item(self, key: str, value: str) -> DocstringMeta:
pass
def parse(self, text: str) -> T.Iterable[DocstringMeta]:
for match, next_match in _pairwise(KV_REGEX.finditer(text)):
start = match.end()
end = next_match.start() if next_match is not None else None
value = text[start:end]
yield self._parse_item(
key=match.group(), value=inspect.cleandoc(value)
)
class _SphinxSection(Section):
"""Base parser for numpydoc sections with sphinx-style syntax.
E.g. sections that look like this:
.. title:: something
possibly over multiple lines
"""
@property
def title_pattern(self) -> str:
return rf"^\.\.\s*({self.title})\s*::"
class ParamSection(_KVSection):
"""Parser for numpydoc parameter sections.
E.g. any section that looks like this:
arg_name
arg_description
arg_2 : type, optional
descriptions can also span...
... multiple lines
"""
def _parse_item(self, key: str, value: str) -> DocstringParam:
match = PARAM_KEY_REGEX.match(key)
arg_name = type_name = is_optional = None
if match is not None:
arg_name = match.group("name")
type_name = match.group("type")
if type_name is not None:
optional_match = PARAM_OPTIONAL_REGEX.match(type_name)
if optional_match is not None:
type_name = optional_match.group("type")
is_optional = True
else:
is_optional = False
default = None
if len(value) > 0:
default_match = PARAM_DEFAULT_REGEX.search(value)
if default_match is not None:
default = default_match.group("value")
return DocstringParam(
args=[self.key, arg_name],
description=_clean_str(value),
arg_name=arg_name,
type_name=type_name,
is_optional=is_optional,
default=default,
)
class RaisesSection(_KVSection):
"""Parser for numpydoc raises sections.
E.g. any section that looks like this:
ValueError
A description of what might raise ValueError
"""
def _parse_item(self, key: str, value: str) -> DocstringRaises:
return DocstringRaises(
args=[self.key, key],
description=_clean_str(value),
type_name=key if len(key) > 0 else None,
)
class ReturnsSection(_KVSection):
"""Parser for numpydoc returns sections.
E.g. any section that looks like this:
return_name : type
A description of this returned value
another_type
Return names are optional, types are required
"""
is_generator = False
def _parse_item(self, key: str, value: str) -> DocstringReturns:
match = RETURN_KEY_REGEX.match(key)
if match is not None:
return_name = match.group("name")
type_name = match.group("type")
else:
return_name = None
type_name = None
return DocstringReturns(
args=[self.key],
description=_clean_str(value),
type_name=type_name,
is_generator=self.is_generator,
return_name=return_name,
)
class YieldsSection(ReturnsSection):
"""Parser for numpydoc generator "yields" sections."""
is_generator = True
class DeprecationSection(_SphinxSection):
"""Parser for numpydoc "deprecation warning" sections."""
def parse(self, text: str) -> T.Iterable[DocstringDeprecated]:
version, desc, *_ = text.split(sep="\n", maxsplit=1) + [None, None]
if desc is not None:
desc = _clean_str(inspect.cleandoc(desc))
yield DocstringDeprecated(
args=[self.key], description=desc, version=_clean_str(version)
)
class ExamplesSection(Section):
"""Parser for numpydoc examples sections.
E.g. any section that looks like this:
>>> import numpy.matlib
>>> np.matlib.empty((2, 2)) # filled with random data
matrix([[ 6.76425276e-320, 9.79033856e-307], # random
[ 7.39337286e-309, 3.22135945e-309]])
>>> np.matlib.empty((2, 2), dtype=int)
matrix([[ 6600475, 0], # random
[ 6586976, 22740995]])
"""
def parse(self, text: str) -> T.Iterable[DocstringMeta]:
"""Parse ``DocstringExample`` objects from the body of this section.
:param text: section body text. Should be cleaned with
``inspect.cleandoc`` before parsing.
"""
lines = dedent(text).strip().splitlines()
while lines:
snippet_lines = []
description_lines = []
while lines:
if not lines[0].startswith(">>>"):
break
snippet_lines.append(lines.pop(0))
while lines:
if lines[0].startswith(">>>"):
break
description_lines.append(lines.pop(0))
yield DocstringExample(
[self.key],
snippet="\n".join(snippet_lines) if snippet_lines else None,
description="\n".join(description_lines),
)
DEFAULT_SECTIONS = [
ParamSection("Parameters", "param"),
ParamSection("Params", "param"),
ParamSection("Arguments", "param"),
ParamSection("Args", "param"),
ParamSection("Other Parameters", "other_param"),
ParamSection("Other Params", "other_param"),
ParamSection("Other Arguments", "other_param"),
ParamSection("Other Args", "other_param"),
ParamSection("Receives", "receives"),
ParamSection("Receive", "receives"),
RaisesSection("Raises", "raises"),
RaisesSection("Raise", "raises"),
RaisesSection("Warns", "warns"),
RaisesSection("Warn", "warns"),
ParamSection("Attributes", "attribute"),
ParamSection("Attribute", "attribute"),
ReturnsSection("Returns", "returns"),
ReturnsSection("Return", "returns"),
YieldsSection("Yields", "yields"),
YieldsSection("Yield", "yields"),
ExamplesSection("Examples", "examples"),
ExamplesSection("Example", "examples"),
Section("Warnings", "warnings"),
Section("Warning", "warnings"),
Section("See Also", "see_also"),
Section("Related", "see_also"),
Section("Notes", "notes"),
Section("Note", "notes"),
Section("References", "references"),
Section("Reference", "references"),
DeprecationSection("deprecated", "deprecation"),
]
class NumpydocParser:
"""Parser for numpydoc-style docstrings."""
def __init__(self, sections: T.Optional[T.Dict[str, Section]] = None):
"""Setup sections.
:param sections: Recognized sections or None to defaults.
"""
sections = sections or DEFAULT_SECTIONS
self.sections = {s.title: s for s in sections}
self._setup()
def _setup(self):
self.titles_re = re.compile(
r"|".join(s.title_pattern for s in self.sections.values()),
flags=re.M,
)
def add_section(self, section: Section):
"""Add or replace a section.
:param section: The new section.
"""
self.sections[section.title] = section
self._setup()
def parse(self, text: str) -> Docstring:
"""Parse the numpy-style docstring into its components.
:returns: parsed docstring
"""
ret = Docstring(style=DocstringStyle.NUMPYDOC)
if not text:
return ret
# Clean according to PEP-0257
text = inspect.cleandoc(text)
# Find first title and split on its position
match = self.titles_re.search(text)
if match:
desc_chunk = text[: match.start()]
meta_chunk = text[match.start() :]
else:
desc_chunk = text
meta_chunk = ""
# Break description into short and long parts
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
for match, nextmatch in _pairwise(self.titles_re.finditer(meta_chunk)):
title = next(g for g in match.groups() if g is not None)
factory = self.sections[title]
# section chunk starts after the header,
# ends at the start of the next header
start = match.end()
end = nextmatch.start() if nextmatch is not None else None
ret.meta.extend(factory.parse(meta_chunk[start:end]))
return ret
def parse(text: str) -> Docstring:
"""Parse the numpy-style docstring into its components.
:returns: parsed docstring
"""
return NumpydocParser().parse(text)
def compose(
# pylint: disable=W0613
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_one(
one: T.Union[DocstringParam, DocstringReturns, DocstringRaises]
):
if isinstance(one, DocstringParam):
head = one.arg_name
elif isinstance(one, DocstringReturns):
head = one.return_name
else:
head = None
if one.type_name and head:
head += f" : {one.type_name}"
elif one.type_name:
head = one.type_name
elif not head:
head = ""
if isinstance(one, DocstringParam) and one.is_optional:
head += ", optional"
if one.description:
body = f"\n{indent}".join([head] + one.description.splitlines())
parts.append(body)
else:
parts.append(head)
def process_sect(name: str, args: T.List[T.Any]):
if args:
parts.append("")
parts.append(name)
parts.append("-" * len(parts[-1]))
for arg in args:
process_one(arg)
parts: T.List[str] = []
if docstring.short_description:
parts.append(docstring.short_description)
if docstring.blank_after_short_description:
parts.append("")
if docstring.deprecation:
first = ".. deprecated::"
if docstring.deprecation.version:
first += f" {docstring.deprecation.version}"
if docstring.deprecation.description:
rest = docstring.deprecation.description.splitlines()
else:
rest = []
sep = f"\n{indent}"
parts.append(sep.join([first] + rest))
if docstring.long_description:
parts.append(docstring.long_description)
if docstring.blank_after_long_description:
parts.append("")
process_sect(
"Parameters",
[item for item in docstring.params or [] if item.args[0] == "param"],
)
process_sect(
"Attributes",
[
item
for item in docstring.params or []
if item.args[0] == "attribute"
],
)
process_sect(
"Returns",
[
item
for item in docstring.many_returns or []
if not item.is_generator
],
)
process_sect(
"Yields",
[item for item in docstring.many_returns or [] if item.is_generator],
)
if docstring.returns and not docstring.many_returns:
ret = docstring.returns
parts.append("Yields" if ret else "Returns")
parts.append("-" * len(parts[-1]))
process_one(ret)
process_sect(
"Receives",
[
item
for item in docstring.params or []
if item.args[0] == "receives"
],
)
process_sect(
"Other Parameters",
[
item
for item in docstring.params or []
if item.args[0] == "other_param"
],
)
process_sect(
"Raises",
[item for item in docstring.raises or [] if item.args[0] == "raises"],
)
process_sect(
"Warns",
[item for item in docstring.raises or [] if item.args[0] == "warns"],
)
for meta in docstring.meta:
if isinstance(
meta,
(
DocstringDeprecated,
DocstringParam,
DocstringReturns,
DocstringRaises,
),
):
continue # Already handled
parts.append("")
parts.append(meta.args[0].replace("_", "").title())
parts.append("-" * len(meta.args[0]))
if meta.description:
parts.append(meta.description)
return "\n".join(parts)

View File

@@ -0,0 +1,98 @@
"""The main parsing routine."""
import inspect
import typing as T
from docstring_parser import epydoc, google, numpydoc, rest
from docstring_parser.attrdoc import add_attribute_docstrings
from docstring_parser.common import (
Docstring,
DocstringStyle,
ParseError,
RenderingStyle,
)
_STYLE_MAP = {
DocstringStyle.REST: rest,
DocstringStyle.GOOGLE: google,
DocstringStyle.NUMPYDOC: numpydoc,
DocstringStyle.EPYDOC: epydoc,
}
def parse(text: str, style: DocstringStyle = DocstringStyle.AUTO) -> Docstring:
"""Parse the docstring into its components.
:param text: docstring text to parse
:param style: docstring style
:returns: parsed docstring representation
"""
if style != DocstringStyle.AUTO:
return _STYLE_MAP[style].parse(text)
exc: T.Optional[Exception] = None
rets = []
for module in _STYLE_MAP.values():
try:
ret = module.parse(text)
except ParseError as ex:
exc = ex
else:
rets.append(ret)
if not rets:
raise exc
return sorted(rets, key=lambda d: len(d.meta), reverse=True)[0]
def parse_from_object(
obj: T.Any,
style: DocstringStyle = DocstringStyle.AUTO,
) -> Docstring:
"""Parse the object's docstring(s) into its components.
The object can be anything that has a ``__doc__`` attribute. In contrast to
the ``parse`` function, ``parse_from_object`` is able to parse attribute
docstrings which are defined in the source code instead of ``__doc__``.
Currently only attribute docstrings defined at class and module levels are
supported. Attribute docstrings defined in ``__init__`` methods are not
supported.
When given a class, only the attribute docstrings of that class are parsed,
not its inherited classes. This is a design decision. Separate calls to
this function should be performed to get attribute docstrings of parent
classes.
:param obj: object from which to parse the docstring(s)
:param style: docstring style
:returns: parsed docstring representation
"""
docstring = parse(obj.__doc__, style=style)
if inspect.isclass(obj) or inspect.ismodule(obj):
add_attribute_docstrings(obj, docstring)
return docstring
def compose(
docstring: Docstring,
style: DocstringStyle = DocstringStyle.AUTO,
rendering_style: RenderingStyle = RenderingStyle.COMPACT,
indent: str = " ",
) -> str:
"""Render a parsed docstring into docstring text.
:param docstring: parsed docstring representation
:param style: docstring style to render
:param indent: the characters used as indentation in the docstring string
:returns: docstring text
"""
module = _STYLE_MAP[
docstring.style if style == DocstringStyle.AUTO else style
]
return module.compose(
docstring, rendering_style=rendering_style, indent=indent
)

View File

@@ -0,0 +1 @@
# Marker file for PEP 561.

View File

@@ -0,0 +1,259 @@
"""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)

View File

@@ -0,0 +1 @@
"""Tests for docstring parser."""

View File

@@ -0,0 +1,22 @@
"""Private pydoctor customization code in order to exclude the package
docstring_parser.tests from the API documentation. Based on Twisted code.
"""
# pylint: disable=invalid-name
try:
from pydoctor.model import Documentable, PrivacyClass, System
except ImportError:
pass
else:
class HidesTestsPydoctorSystem(System):
"""A PyDoctor "system" used to generate the docs."""
def privacyClass(self, documentable: Documentable) -> PrivacyClass:
"""Report the privacy level for an object. Hide the module
'docstring_parser.tests'.
"""
if documentable.fullName().startswith("docstring_parser.tests"):
return PrivacyClass.HIDDEN
return super().privacyClass(documentable)

View File

@@ -0,0 +1,723 @@
"""Tests for epydoc-style docstring routines."""
import typing as T
import pytest
from docstring_parser.common import ParseError, RenderingStyle
from docstring_parser.epydoc import compose, parse
@pytest.mark.parametrize(
"source, expected",
[
("", None),
("\n", None),
("Short description", "Short description"),
("\nShort description\n", "Short description"),
("\n Short description\n", "Short description"),
],
)
def test_short_description(source: str, expected: str) -> None:
"""Test parsing short description."""
docstring = parse(source)
assert docstring.short_description == expected
assert docstring.long_description is None
assert not docstring.meta
@pytest.mark.parametrize(
"source, expected_short_desc, expected_long_desc, expected_blank",
[
(
"Short description\n\nLong description",
"Short description",
"Long description",
True,
),
(
"""
Short description
Long description
""",
"Short description",
"Long description",
True,
),
(
"""
Short description
Long description
Second line
""",
"Short description",
"Long description\nSecond line",
True,
),
(
"Short description\nLong description",
"Short description",
"Long description",
False,
),
(
"""
Short description
Long description
""",
"Short description",
"Long description",
False,
),
(
"\nShort description\nLong description\n",
"Short description",
"Long description",
False,
),
(
"""
Short description
Long description
Second line
""",
"Short description",
"Long description\nSecond line",
False,
),
],
)
def test_long_description(
source: str,
expected_short_desc: str,
expected_long_desc: str,
expected_blank: bool,
) -> None:
"""Test parsing long description."""
docstring = parse(source)
assert docstring.short_description == expected_short_desc
assert docstring.long_description == expected_long_desc
assert docstring.blank_after_short_description == expected_blank
assert not docstring.meta
@pytest.mark.parametrize(
"source, expected_short_desc, expected_long_desc, "
"expected_blank_short_desc, expected_blank_long_desc",
[
(
"""
Short description
@meta: asd
""",
"Short description",
None,
False,
False,
),
(
"""
Short description
Long description
@meta: asd
""",
"Short description",
"Long description",
False,
False,
),
(
"""
Short description
First line
Second line
@meta: asd
""",
"Short description",
"First line\n Second line",
False,
False,
),
(
"""
Short description
First line
Second line
@meta: asd
""",
"Short description",
"First line\n Second line",
True,
False,
),
(
"""
Short description
First line
Second line
@meta: asd
""",
"Short description",
"First line\n Second line",
True,
True,
),
(
"""
@meta: asd
""",
None,
None,
False,
False,
),
],
)
def test_meta_newlines(
source: str,
expected_short_desc: T.Optional[str],
expected_long_desc: T.Optional[str],
expected_blank_short_desc: bool,
expected_blank_long_desc: bool,
) -> None:
"""Test parsing newlines around description sections."""
docstring = parse(source)
assert docstring.short_description == expected_short_desc
assert docstring.long_description == expected_long_desc
assert docstring.blank_after_short_description == expected_blank_short_desc
assert docstring.blank_after_long_description == expected_blank_long_desc
assert len(docstring.meta) == 1
def test_meta_with_multiline_description() -> None:
"""Test parsing multiline meta documentation."""
docstring = parse(
"""
Short description
@meta: asd
1
2
3
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["meta"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
def test_multiple_meta() -> None:
"""Test parsing multiple meta."""
docstring = parse(
"""
Short description
@meta1: asd
1
2
3
@meta2: herp
@meta3: derp
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 3
assert docstring.meta[0].args == ["meta1"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[1].args == ["meta2"]
assert docstring.meta[1].description == "herp"
assert docstring.meta[2].args == ["meta3"]
assert docstring.meta[2].description == "derp"
def test_meta_with_args() -> None:
"""Test parsing meta with additional arguments."""
docstring = parse(
"""
Short description
@meta ene due rabe: asd
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["meta", "ene", "due", "rabe"]
assert docstring.meta[0].description == "asd"
def test_params() -> None:
"""Test parsing params."""
docstring = parse("Short description")
assert len(docstring.params) == 0
docstring = parse(
"""
Short description
@param name: description 1
@param priority: description 2
@type priority: int
@param sender: description 3
@type sender: str?
@param message: description 4, defaults to 'hello'
@type message: str?
@param multiline: long description 5,
defaults to 'bye'
@type multiline: str?
"""
)
assert len(docstring.params) == 5
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "description 1"
assert docstring.params[0].default is None
assert not docstring.params[0].is_optional
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
assert not docstring.params[1].is_optional
assert docstring.params[1].default is None
assert docstring.params[2].arg_name == "sender"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description == "description 3"
assert docstring.params[2].is_optional
assert docstring.params[2].default is None
assert docstring.params[3].arg_name == "message"
assert docstring.params[3].type_name == "str"
assert (
docstring.params[3].description == "description 4, defaults to 'hello'"
)
assert docstring.params[3].is_optional
assert docstring.params[3].default == "'hello'"
assert docstring.params[4].arg_name == "multiline"
assert docstring.params[4].type_name == "str"
assert (
docstring.params[4].description
== "long description 5,\ndefaults to 'bye'"
)
assert docstring.params[4].is_optional
assert docstring.params[4].default == "'bye'"
def test_returns() -> None:
"""Test parsing returns."""
docstring = parse(
"""
Short description
"""
)
assert docstring.returns is None
docstring = parse(
"""
Short description
@return: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name is None
assert docstring.returns.description == "description"
assert not docstring.returns.is_generator
docstring = parse(
"""
Short description
@return: description
@rtype: int
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == "description"
assert not docstring.returns.is_generator
def test_yields() -> None:
"""Test parsing yields."""
docstring = parse(
"""
Short description
"""
)
assert docstring.returns is None
docstring = parse(
"""
Short description
@yield: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name is None
assert docstring.returns.description == "description"
assert docstring.returns.is_generator
docstring = parse(
"""
Short description
@yield: description
@ytype: int
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == "description"
assert docstring.returns.is_generator
def test_raises() -> None:
"""Test parsing raises."""
docstring = parse(
"""
Short description
"""
)
assert len(docstring.raises) == 0
docstring = parse(
"""
Short description
@raise: description
"""
)
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name is None
assert docstring.raises[0].description == "description"
docstring = parse(
"""
Short description
@raise ValueError: description
"""
)
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name == "ValueError"
assert docstring.raises[0].description == "description"
def test_broken_meta() -> None:
"""Test parsing broken meta."""
with pytest.raises(ParseError):
parse("@")
with pytest.raises(ParseError):
parse("@param herp derp")
with pytest.raises(ParseError):
parse("@param: invalid")
with pytest.raises(ParseError):
parse("@param with too many args: desc")
# these should not raise any errors
parse("@sthstrange: desc")
@pytest.mark.parametrize(
"source, expected",
[
("", ""),
("\n", ""),
("Short description", "Short description"),
("\nShort description\n", "Short description"),
("\n Short description\n", "Short description"),
(
"Short description\n\nLong description",
"Short description\n\nLong description",
),
(
"""
Short description
Long description
""",
"Short description\n\nLong description",
),
(
"""
Short description
Long description
Second line
""",
"Short description\n\nLong description\nSecond line",
),
(
"Short description\nLong description",
"Short description\nLong description",
),
(
"""
Short description
Long description
""",
"Short description\nLong description",
),
(
"\nShort description\nLong description\n",
"Short description\nLong description",
),
(
"""
Short description
Long description
Second line
""",
"Short description\nLong description\nSecond line",
),
(
"""
Short description
@meta: asd
""",
"Short description\n@meta: asd",
),
(
"""
Short description
Long description
@meta: asd
""",
"Short description\nLong description\n@meta: asd",
),
(
"""
Short description
First line
Second line
@meta: asd
""",
"Short description\nFirst line\n Second line\n@meta: asd",
),
(
"""
Short description
First line
Second line
@meta: asd
""",
"Short description\n"
"\n"
"First line\n"
" Second line\n"
"@meta: asd",
),
(
"""
Short description
First line
Second line
@meta: asd
""",
"Short description\n"
"\n"
"First line\n"
" Second line\n"
"\n"
"@meta: asd",
),
(
"""
@meta: asd
""",
"@meta: asd",
),
(
"""
Short description
@meta: asd
1
2
3
""",
"Short description\n"
"\n"
"@meta: asd\n"
" 1\n"
" 2\n"
" 3",
),
(
"""
Short description
@meta1: asd
1
2
3
@meta2: herp
@meta3: derp
""",
"Short description\n"
"\n@meta1: asd\n"
" 1\n"
" 2\n"
" 3\n@meta2: herp\n"
"@meta3: derp",
),
(
"""
Short description
@meta ene due rabe: asd
""",
"Short description\n\n@meta ene due rabe: asd",
),
(
"""
Short description
@param name: description 1
@param priority: description 2
@type priority: int
@param sender: description 3
@type sender: str?
@type message: str?
@param message: description 4, defaults to 'hello'
@type multiline: str?
@param multiline: long description 5,
defaults to 'bye'
""",
"Short description\n"
"\n"
"@param name: description 1\n"
"@type priority: int\n"
"@param priority: description 2\n"
"@type sender: str?\n"
"@param sender: description 3\n"
"@type message: str?\n"
"@param message: description 4, defaults to 'hello'\n"
"@type multiline: str?\n"
"@param multiline: long description 5,\n"
" defaults to 'bye'",
),
(
"""
Short description
@raise: description
""",
"Short description\n@raise: description",
),
(
"""
Short description
@raise ValueError: description
""",
"Short description\n@raise ValueError: description",
),
],
)
def test_compose(source: str, expected: str) -> None:
"""Test compose in default mode."""
assert compose(parse(source)) == expected
@pytest.mark.parametrize(
"source, expected",
[
(
"""
Short description
@param name: description 1
@param priority: description 2
@type priority: int
@param sender: description 3
@type sender: str?
@type message: str?
@param message: description 4, defaults to 'hello'
@type multiline: str?
@param multiline: long description 5,
defaults to 'bye'
""",
"Short description\n"
"\n"
"@param name:\n"
" description 1\n"
"@type priority: int\n"
"@param priority:\n"
" description 2\n"
"@type sender: str?\n"
"@param sender:\n"
" description 3\n"
"@type message: str?\n"
"@param message:\n"
" description 4, defaults to 'hello'\n"
"@type multiline: str?\n"
"@param multiline:\n"
" long description 5,\n"
" defaults to 'bye'",
),
],
)
def test_compose_clean(source: str, expected: str) -> None:
"""Test compose in clean mode."""
assert (
compose(parse(source), rendering_style=RenderingStyle.CLEAN)
== expected
)
@pytest.mark.parametrize(
"source, expected",
[
(
"""
Short description
@param name: description 1
@param priority: description 2
@type priority: int
@param sender: description 3
@type sender: str?
@type message: str?
@param message: description 4, defaults to 'hello'
@type multiline: str?
@param multiline: long description 5,
defaults to 'bye'
""",
"Short description\n"
"\n"
"@param name:\n"
" description 1\n"
"@type priority:\n"
" int\n"
"@param priority:\n"
" description 2\n"
"@type sender:\n"
" str?\n"
"@param sender:\n"
" description 3\n"
"@type message:\n"
" str?\n"
"@param message:\n"
" description 4, defaults to 'hello'\n"
"@type multiline:\n"
" str?\n"
"@param multiline:\n"
" long description 5,\n"
" defaults to 'bye'",
),
],
)
def test_compose_expanded(source: str, expected: str) -> None:
"""Test compose in expanded mode."""
assert (
compose(parse(source), rendering_style=RenderingStyle.EXPANDED)
== expected
)
def test_short_rtype() -> None:
"""Test abbreviated docstring with only return type information."""
string = "Short description.\n\n@rtype: float"
docstring = parse(string)
assert compose(docstring) == string

View File

@@ -0,0 +1,977 @@
"""Tests for Google-style docstring routines."""
import typing as T
import pytest
from docstring_parser.common import ParseError, RenderingStyle
from docstring_parser.google import (
GoogleParser,
Section,
SectionType,
compose,
parse,
)
def test_google_parser_unknown_section() -> None:
"""Test parsing an unknown section with default GoogleParser
configuration.
"""
parser = GoogleParser()
docstring = parser.parse(
"""
Unknown:
spam: a
"""
)
assert docstring.short_description == "Unknown:"
assert docstring.long_description == "spam: a"
assert len(docstring.meta) == 0
def test_google_parser_custom_sections() -> None:
"""Test parsing an unknown section with custom GoogleParser
configuration.
"""
parser = GoogleParser(
[
Section("DESCRIPTION", "desc", SectionType.SINGULAR),
Section("ARGUMENTS", "param", SectionType.MULTIPLE),
Section("ATTRIBUTES", "attribute", SectionType.MULTIPLE),
Section("EXAMPLES", "examples", SectionType.SINGULAR),
],
title_colon=False,
)
docstring = parser.parse(
"""
DESCRIPTION
This is the description.
ARGUMENTS
arg1: first arg
arg2: second arg
ATTRIBUTES
attr1: first attribute
attr2: second attribute
EXAMPLES
Many examples
More examples
"""
)
assert docstring.short_description is None
assert docstring.long_description is None
assert len(docstring.meta) == 6
assert docstring.meta[0].args == ["desc"]
assert docstring.meta[0].description == "This is the description."
assert docstring.meta[1].args == ["param", "arg1"]
assert docstring.meta[1].description == "first arg"
assert docstring.meta[2].args == ["param", "arg2"]
assert docstring.meta[2].description == "second arg"
assert docstring.meta[3].args == ["attribute", "attr1"]
assert docstring.meta[3].description == "first attribute"
assert docstring.meta[4].args == ["attribute", "attr2"]
assert docstring.meta[4].description == "second attribute"
assert docstring.meta[5].args == ["examples"]
assert docstring.meta[5].description == "Many examples\nMore examples"
def test_google_parser_custom_sections_after() -> None:
"""Test parsing an unknown section with custom GoogleParser configuration
that was set at a runtime.
"""
parser = GoogleParser(title_colon=False)
parser.add_section(Section("Note", "note", SectionType.SINGULAR))
docstring = parser.parse(
"""
short description
Note:
a note
"""
)
assert docstring.short_description == "short description"
assert docstring.long_description == "Note:\n a note"
docstring = parser.parse(
"""
short description
Note a note
"""
)
assert docstring.short_description == "short description"
assert docstring.long_description == "Note a note"
docstring = parser.parse(
"""
short description
Note
a note
"""
)
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["note"]
assert docstring.meta[0].description == "a note"
@pytest.mark.parametrize(
"source, expected",
[
("", None),
("\n", None),
("Short description", "Short description"),
("\nShort description\n", "Short description"),
("\n Short description\n", "Short description"),
],
)
def test_short_description(source: str, expected: str) -> None:
"""Test parsing short description."""
docstring = parse(source)
assert docstring.short_description == expected
assert docstring.long_description is None
assert not docstring.meta
@pytest.mark.parametrize(
"source, expected_short_desc, expected_long_desc, expected_blank",
[
(
"Short description\n\nLong description",
"Short description",
"Long description",
True,
),
(
"""
Short description
Long description
""",
"Short description",
"Long description",
True,
),
(
"""
Short description
Long description
Second line
""",
"Short description",
"Long description\nSecond line",
True,
),
(
"Short description\nLong description",
"Short description",
"Long description",
False,
),
(
"""
Short description
Long description
""",
"Short description",
"Long description",
False,
),
(
"\nShort description\nLong description\n",
"Short description",
"Long description",
False,
),
(
"""
Short description
Long description
Second line
""",
"Short description",
"Long description\nSecond line",
False,
),
],
)
def test_long_description(
source: str,
expected_short_desc: str,
expected_long_desc: str,
expected_blank: bool,
) -> None:
"""Test parsing long description."""
docstring = parse(source)
assert docstring.short_description == expected_short_desc
assert docstring.long_description == expected_long_desc
assert docstring.blank_after_short_description == expected_blank
assert not docstring.meta
@pytest.mark.parametrize(
"source, expected_short_desc, expected_long_desc, "
"expected_blank_short_desc, expected_blank_long_desc",
[
(
"""
Short description
Args:
asd:
""",
"Short description",
None,
False,
False,
),
(
"""
Short description
Long description
Args:
asd:
""",
"Short description",
"Long description",
False,
False,
),
(
"""
Short description
First line
Second line
Args:
asd:
""",
"Short description",
"First line\n Second line",
False,
False,
),
(
"""
Short description
First line
Second line
Args:
asd:
""",
"Short description",
"First line\n Second line",
True,
False,
),
(
"""
Short description
First line
Second line
Args:
asd:
""",
"Short description",
"First line\n Second line",
True,
True,
),
(
"""
Args:
asd:
""",
None,
None,
False,
False,
),
],
)
def test_meta_newlines(
source: str,
expected_short_desc: T.Optional[str],
expected_long_desc: T.Optional[str],
expected_blank_short_desc: bool,
expected_blank_long_desc: bool,
) -> None:
"""Test parsing newlines around description sections."""
docstring = parse(source)
assert docstring.short_description == expected_short_desc
assert docstring.long_description == expected_long_desc
assert docstring.blank_after_short_description == expected_blank_short_desc
assert docstring.blank_after_long_description == expected_blank_long_desc
assert len(docstring.meta) == 1
def test_meta_with_multiline_description() -> None:
"""Test parsing multiline meta documentation."""
docstring = parse(
"""
Short description
Args:
spam: asd
1
2
3
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["param", "spam"]
assert docstring.meta[0].arg_name == "spam"
assert docstring.meta[0].description == "asd\n1\n 2\n3"
def test_default_args() -> None:
"""Test parsing default arguments."""
docstring = parse(
"""A sample function
A function the demonstrates docstrings
Args:
arg1 (int): The firsty arg
arg2 (str): The second arg
arg3 (float, optional): The third arg. Defaults to 1.0.
arg4 (Optional[Dict[str, Any]], optional): The last arg. Defaults to None.
arg5 (str, optional): The fifth arg. Defaults to DEFAULT_ARG5.
Returns:
Mapping[str, Any]: The args packed in a mapping
"""
)
assert docstring is not None
assert len(docstring.params) == 5
arg4 = docstring.params[3]
assert arg4.arg_name == "arg4"
assert arg4.is_optional
assert arg4.type_name == "Optional[Dict[str, Any]]"
assert arg4.default == "None"
assert arg4.description == "The last arg. Defaults to None."
def test_multiple_meta() -> None:
"""Test parsing multiple meta."""
docstring = parse(
"""
Short description
Args:
spam: asd
1
2
3
Raises:
bla: herp
yay: derp
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 3
assert docstring.meta[0].args == ["param", "spam"]
assert docstring.meta[0].arg_name == "spam"
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[1].args == ["raises", "bla"]
assert docstring.meta[1].type_name == "bla"
assert docstring.meta[1].description == "herp"
assert docstring.meta[2].args == ["raises", "yay"]
assert docstring.meta[2].type_name == "yay"
assert docstring.meta[2].description == "derp"
def test_params() -> None:
"""Test parsing params."""
docstring = parse("Short description")
assert len(docstring.params) == 0
docstring = parse(
"""
Short description
Args:
name: description 1
priority (int): description 2
sender (str?): description 3
ratio (Optional[float], optional): description 4
"""
)
assert len(docstring.params) == 4
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "description 1"
assert not docstring.params[0].is_optional
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
assert not docstring.params[1].is_optional
assert docstring.params[2].arg_name == "sender"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description == "description 3"
assert docstring.params[2].is_optional
assert docstring.params[3].arg_name == "ratio"
assert docstring.params[3].type_name == "Optional[float]"
assert docstring.params[3].description == "description 4"
assert docstring.params[3].is_optional
docstring = parse(
"""
Short description
Args:
name: description 1
with multi-line text
priority (int): description 2
"""
)
assert len(docstring.params) == 2
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == (
"description 1\nwith multi-line text"
)
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
def test_attributes() -> None:
"""Test parsing attributes."""
docstring = parse("Short description")
assert len(docstring.params) == 0
docstring = parse(
"""
Short description
Attributes:
name: description 1
priority (int): description 2
sender (str?): description 3
ratio (Optional[float], optional): description 4
"""
)
assert len(docstring.params) == 4
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "description 1"
assert not docstring.params[0].is_optional
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
assert not docstring.params[1].is_optional
assert docstring.params[2].arg_name == "sender"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description == "description 3"
assert docstring.params[2].is_optional
assert docstring.params[3].arg_name == "ratio"
assert docstring.params[3].type_name == "Optional[float]"
assert docstring.params[3].description == "description 4"
assert docstring.params[3].is_optional
docstring = parse(
"""
Short description
Attributes:
name: description 1
with multi-line text
priority (int): description 2
"""
)
assert len(docstring.params) == 2
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == (
"description 1\nwith multi-line text"
)
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
def test_returns() -> None:
"""Test parsing returns."""
docstring = parse(
"""
Short description
"""
)
assert docstring.returns is None
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 0
docstring = parse(
"""
Short description
Returns:
description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name is None
assert docstring.returns.description == "description"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
docstring = parse(
"""
Short description
Returns:
description with: a colon!
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name is None
assert docstring.returns.description == "description with: a colon!"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
docstring = parse(
"""
Short description
Returns:
int: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == "description"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
docstring = parse(
"""
Returns:
Optional[Mapping[str, List[int]]]: A description: with a colon
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "Optional[Mapping[str, List[int]]]"
assert docstring.returns.description == "A description: with a colon"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
docstring = parse(
"""
Short description
Yields:
int: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == "description"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
docstring = parse(
"""
Short description
Returns:
int: description
with much text
even some spacing
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == (
"description\nwith much text\n\neven some spacing"
)
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
def test_raises() -> None:
"""Test parsing raises."""
docstring = parse(
"""
Short description
"""
)
assert len(docstring.raises) == 0
docstring = parse(
"""
Short description
Raises:
ValueError: description
"""
)
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name == "ValueError"
assert docstring.raises[0].description == "description"
def test_examples() -> None:
"""Test parsing examples."""
docstring = parse(
"""
Short description
Example:
example: 1
Examples:
long example
more here
"""
)
assert len(docstring.examples) == 2
assert docstring.examples[0].description == "example: 1"
assert docstring.examples[1].description == "long example\n\nmore here"
def test_broken_meta() -> None:
"""Test parsing broken meta."""
with pytest.raises(ParseError):
parse("Args:")
with pytest.raises(ParseError):
parse("Args:\n herp derp")
def test_unknown_meta() -> None:
"""Test parsing unknown meta."""
docstring = parse(
"""Short desc
Unknown 0:
title0: content0
Args:
arg0: desc0
arg1: desc1
Unknown1:
title1: content1
Unknown2:
title2: content2
"""
)
assert docstring.params[0].arg_name == "arg0"
assert docstring.params[0].description == "desc0"
assert docstring.params[1].arg_name == "arg1"
assert docstring.params[1].description == "desc1"
def test_broken_arguments() -> None:
"""Test parsing broken arguments."""
with pytest.raises(ParseError):
parse(
"""This is a test
Args:
param - poorly formatted
"""
)
def test_empty_example() -> None:
"""Test parsing empty examples section."""
docstring = parse(
"""Short description
Example:
Raises:
IOError: some error
"""
)
assert len(docstring.examples) == 1
assert docstring.examples[0].args == ["examples"]
assert docstring.examples[0].description == ""
@pytest.mark.parametrize(
"source, expected",
[
("", ""),
("\n", ""),
("Short description", "Short description"),
("\nShort description\n", "Short description"),
("\n Short description\n", "Short description"),
(
"Short description\n\nLong description",
"Short description\n\nLong description",
),
(
"""
Short description
Long description
""",
"Short description\n\nLong description",
),
(
"""
Short description
Long description
Second line
""",
"Short description\n\nLong description\nSecond line",
),
(
"Short description\nLong description",
"Short description\nLong description",
),
(
"""
Short description
Long description
""",
"Short description\nLong description",
),
(
"\nShort description\nLong description\n",
"Short description\nLong description",
),
(
"""
Short description
Long description
Second line
""",
"Short description\nLong description\nSecond line",
),
(
"""
Short description
Meta:
asd
""",
"Short description\nMeta:\n asd",
),
(
"""
Short description
Long description
Meta:
asd
""",
"Short description\nLong description\nMeta:\n asd",
),
(
"""
Short description
First line
Second line
Meta:
asd
""",
"Short description\n"
"First line\n"
" Second line\n"
"Meta:\n"
" asd",
),
(
"""
Short description
First line
Second line
Meta:
asd
""",
"Short description\n"
"\n"
"First line\n"
" Second line\n"
"Meta:\n"
" asd",
),
(
"""
Short description
First line
Second line
Meta:
asd
""",
"Short description\n"
"\n"
"First line\n"
" Second line\n"
"\n"
"Meta:\n"
" asd",
),
(
"""
Short description
Meta:
asd
1
2
3
""",
"Short description\n"
"\n"
"Meta:\n"
" asd\n"
" 1\n"
" 2\n"
" 3",
),
(
"""
Short description
Meta1:
asd
1
2
3
Meta2:
herp
Meta3:
derp
""",
"Short description\n"
"\n"
"Meta1:\n"
" asd\n"
" 1\n"
" 2\n"
" 3\n"
"Meta2:\n"
" herp\n"
"Meta3:\n"
" derp",
),
(
"""
Short description
Args:
name: description 1
priority (int): description 2
sender (str, optional): description 3
message (str, optional): description 4, defaults to 'hello'
multiline (str?):
long description 5,
defaults to 'bye'
""",
"Short description\n"
"\n"
"Args:\n"
" name: description 1\n"
" priority (int): description 2\n"
" sender (str?): description 3\n"
" message (str?): description 4, defaults to 'hello'\n"
" multiline (str?): long description 5,\n"
" defaults to 'bye'",
),
(
"""
Short description
Raises:
ValueError: description
""",
"Short description\nRaises:\n ValueError: description",
),
],
)
def test_compose(source: str, expected: str) -> None:
"""Test compose in default mode."""
assert compose(parse(source)) == expected
@pytest.mark.parametrize(
"source, expected",
[
(
"""
Short description
Args:
name: description 1
priority (int): description 2
sender (str, optional): description 3
message (str, optional): description 4, defaults to 'hello'
multiline (str?):
long description 5,
defaults to 'bye'
""",
"Short description\n"
"\n"
"Args:\n"
" name: description 1\n"
" priority (int): description 2\n"
" sender (str, optional): description 3\n"
" message (str, optional): description 4, defaults to 'hello'\n"
" multiline (str, optional): long description 5,\n"
" defaults to 'bye'",
),
],
)
def test_compose_clean(source: str, expected: str) -> None:
"""Test compose in clean mode."""
assert (
compose(parse(source), rendering_style=RenderingStyle.CLEAN)
== expected
)
@pytest.mark.parametrize(
"source, expected",
[
(
"""
Short description
Args:
name: description 1
priority (int): description 2
sender (str, optional): description 3
message (str, optional): description 4, defaults to 'hello'
multiline (str?):
long description 5,
defaults to 'bye'
""",
"Short description\n"
"\n"
"Args:\n"
" name:\n"
" description 1\n"
" priority (int):\n"
" description 2\n"
" sender (str, optional):\n"
" description 3\n"
" message (str, optional):\n"
" description 4, defaults to 'hello'\n"
" multiline (str, optional):\n"
" long description 5,\n"
" defaults to 'bye'",
),
],
)
def test_compose_expanded(source: str, expected: str) -> None:
"""Test compose in expanded mode."""
assert (
compose(parse(source), rendering_style=RenderingStyle.EXPANDED)
== expected
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
"""Tests for parse_from_object function and attribute docstrings."""
from unittest.mock import patch
from docstring_parser import parse_from_object
module_attr: int = 1
"""Description for module_attr"""
def test_from_module_attribute_docstrings() -> None:
"""Test the parse of attribute docstrings from a module."""
from . import test_parse_from_object # pylint: disable=C0415,W0406
docstring = parse_from_object(test_parse_from_object)
assert "parse_from_object" in docstring.short_description
assert len(docstring.params) == 1
assert docstring.params[0].arg_name == "module_attr"
assert docstring.params[0].type_name == "int"
assert docstring.params[0].description == "Description for module_attr"
def test_from_class_attribute_docstrings() -> None:
"""Test the parse of attribute docstrings from a class."""
class StandardCase:
"""Short description
Long description
"""
attr_one: str
"""Description for attr_one"""
attr_two: bool = False
"""Description for attr_two"""
docstring = parse_from_object(StandardCase)
assert docstring.short_description == "Short description"
assert docstring.long_description == "Long description"
assert docstring.description == "Short description\nLong description"
assert len(docstring.params) == 2
assert docstring.params[0].arg_name == "attr_one"
assert docstring.params[0].type_name == "str"
assert docstring.params[0].description == "Description for attr_one"
assert docstring.params[1].arg_name == "attr_two"
assert docstring.params[1].type_name == "bool"
assert docstring.params[1].description == "Description for attr_two"
def test_from_class_attribute_docstrings_without_type() -> None:
"""Test the parse of untyped attribute docstrings."""
class WithoutType: # pylint: disable=missing-class-docstring
attr_one = "value"
"""Description for attr_one"""
docstring = parse_from_object(WithoutType)
assert docstring.short_description is None
assert docstring.long_description is None
assert docstring.description is None
assert len(docstring.params) == 1
assert docstring.params[0].arg_name == "attr_one"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "Description for attr_one"
def test_from_class_without_source() -> None:
"""Test the parse of class when source is unavailable."""
class WithoutSource:
"""Short description"""
attr_one: str
"""Description for attr_one"""
with patch(
"inspect.getsource", side_effect=OSError("could not get source code")
):
docstring = parse_from_object(WithoutSource)
assert docstring.short_description == "Short description"
assert docstring.long_description is None
assert docstring.description == "Short description"
assert len(docstring.params) == 0
def test_from_function() -> None:
"""Test the parse of a function docstring."""
def a_function(param1: str, param2: int = 2):
"""Short description
Args:
param1: Description for param1
param2: Description for param2
"""
return f"{param1} {param2}"
docstring = parse_from_object(a_function)
assert docstring.short_description == "Short description"
assert docstring.description == "Short description"
assert len(docstring.params) == 2
assert docstring.params[0].arg_name == "param1"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "Description for param1"
assert docstring.params[1].arg_name == "param2"
assert docstring.params[1].type_name is None
assert docstring.params[1].description == "Description for param2"

View File

@@ -0,0 +1,222 @@
"""Tests for generic docstring routines."""
import pytest
from docstring_parser.common import DocstringStyle, ParseError
from docstring_parser.parser import parse
def test_rest() -> None:
"""Test ReST-style parser autodetection."""
docstring = parse(
"""
Short description
Long description
Causing people to indent:
A lot sometimes
:param spam: spam desc
:param int bla: bla desc
:param str yay:
:raises ValueError: exc desc
:returns tuple: ret desc
"""
)
assert docstring.style == DocstringStyle.REST
assert docstring.short_description == "Short description"
assert docstring.long_description == (
"Long description\n\n"
"Causing people to indent:\n\n"
" A lot sometimes"
)
assert docstring.description == (
"Short description\n\n"
"Long description\n\n"
"Causing people to indent:\n\n"
" A lot sometimes"
)
assert len(docstring.params) == 3
assert docstring.params[0].arg_name == "spam"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "spam desc"
assert docstring.params[1].arg_name == "bla"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "bla desc"
assert docstring.params[2].arg_name == "yay"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description == ""
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name == "ValueError"
assert docstring.raises[0].description == "exc desc"
assert docstring.returns is not None
assert docstring.returns.type_name == "tuple"
assert docstring.returns.description == "ret desc"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
def test_google() -> None:
"""Test Google-style parser autodetection."""
docstring = parse(
"""Short description
Long description
Causing people to indent:
A lot sometimes
Args:
spam: spam desc
bla (int): bla desc
yay (str):
Raises:
ValueError: exc desc
Returns:
tuple: ret desc
"""
)
assert docstring.style == DocstringStyle.GOOGLE
assert docstring.short_description == "Short description"
assert docstring.long_description == (
"Long description\n\n"
"Causing people to indent:\n\n"
" A lot sometimes"
)
assert docstring.description == (
"Short description\n\n"
"Long description\n\n"
"Causing people to indent:\n\n"
" A lot sometimes"
)
assert len(docstring.params) == 3
assert docstring.params[0].arg_name == "spam"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "spam desc"
assert docstring.params[1].arg_name == "bla"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "bla desc"
assert docstring.params[2].arg_name == "yay"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description == ""
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name == "ValueError"
assert docstring.raises[0].description == "exc desc"
assert docstring.returns is not None
assert docstring.returns.type_name == "tuple"
assert docstring.returns.description == "ret desc"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
def test_numpydoc() -> None:
"""Test numpydoc-style parser autodetection."""
docstring = parse(
"""Short description
Long description
Causing people to indent:
A lot sometimes
Parameters
----------
spam
spam desc
bla : int
bla desc
yay : str
Raises
------
ValueError
exc desc
Other Parameters
----------------
this_guy : int, optional
you know him
Returns
-------
tuple
ret desc
See Also
--------
multiple lines...
something else?
Warnings
--------
multiple lines...
none of this is real!
"""
)
assert docstring.style == DocstringStyle.NUMPYDOC
assert docstring.short_description == "Short description"
assert docstring.long_description == (
"Long description\n\n"
"Causing people to indent:\n\n"
" A lot sometimes"
)
assert docstring.description == (
"Short description\n\n"
"Long description\n\n"
"Causing people to indent:\n\n"
" A lot sometimes"
)
assert len(docstring.params) == 4
assert docstring.params[0].arg_name == "spam"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "spam desc"
assert docstring.params[1].arg_name == "bla"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "bla desc"
assert docstring.params[2].arg_name == "yay"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description is None
assert docstring.params[3].arg_name == "this_guy"
assert docstring.params[3].type_name == "int"
assert docstring.params[3].is_optional
assert docstring.params[3].description == "you know him"
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name == "ValueError"
assert docstring.raises[0].description == "exc desc"
assert docstring.returns is not None
assert docstring.returns.type_name == "tuple"
assert docstring.returns.description == "ret desc"
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
def test_autodetection_error_detection() -> None:
"""Test autodection for the case where one of the parsers throws an error
and another one succeeds.
"""
source = """
Does something useless
:param 3 + 3 a: a param
"""
with pytest.raises(ParseError):
# assert that one of the parsers does raise
parse(source, DocstringStyle.REST)
# assert that autodetection still works
docstring = parse(source)
assert docstring
assert docstring.style == DocstringStyle.GOOGLE

View File

@@ -0,0 +1,541 @@
"""Tests for ReST-style docstring routines."""
import typing as T
import pytest
from docstring_parser.common import ParseError, RenderingStyle
from docstring_parser.rest import compose, parse
@pytest.mark.parametrize(
"source, expected",
[
("", None),
("\n", None),
("Short description", "Short description"),
("\nShort description\n", "Short description"),
("\n Short description\n", "Short description"),
],
)
def test_short_description(source: str, expected: str) -> None:
"""Test parsing short description."""
docstring = parse(source)
assert docstring.short_description == expected
assert docstring.description == expected
assert docstring.long_description is None
assert not docstring.meta
@pytest.mark.parametrize(
"source, expected_short_desc, expected_long_desc, expected_blank",
[
(
"Short description\n\nLong description",
"Short description",
"Long description",
True,
),
(
"""
Short description
Long description
""",
"Short description",
"Long description",
True,
),
(
"""
Short description
Long description
Second line
""",
"Short description",
"Long description\nSecond line",
True,
),
(
"Short description\nLong description",
"Short description",
"Long description",
False,
),
(
"""
Short description
Long description
""",
"Short description",
"Long description",
False,
),
(
"\nShort description\nLong description\n",
"Short description",
"Long description",
False,
),
(
"""
Short description
Long description
Second line
""",
"Short description",
"Long description\nSecond line",
False,
),
],
)
def test_long_description(
source: str,
expected_short_desc: str,
expected_long_desc: str,
expected_blank: bool,
) -> None:
"""Test parsing long description."""
docstring = parse(source)
assert docstring.short_description == expected_short_desc
assert docstring.long_description == expected_long_desc
assert docstring.blank_after_short_description == expected_blank
assert not docstring.meta
@pytest.mark.parametrize(
"source, expected_short_desc, expected_long_desc, "
"expected_blank_short_desc, expected_blank_long_desc, "
"expected_full_desc",
[
(
"""
Short description
:meta: asd
""",
"Short description",
None,
False,
False,
"Short description",
),
(
"""
Short description
Long description
:meta: asd
""",
"Short description",
"Long description",
False,
False,
"Short description\nLong description",
),
(
"""
Short description
First line
Second line
:meta: asd
""",
"Short description",
"First line\n Second line",
False,
False,
"Short description\nFirst line\n Second line",
),
(
"""
Short description
First line
Second line
:meta: asd
""",
"Short description",
"First line\n Second line",
True,
False,
"Short description\n\nFirst line\n Second line",
),
(
"""
Short description
First line
Second line
:meta: asd
""",
"Short description",
"First line\n Second line",
True,
True,
"Short description\n\nFirst line\n Second line",
),
(
"""
:meta: asd
""",
None,
None,
False,
False,
None,
),
],
)
def test_meta_newlines(
source: str,
expected_short_desc: T.Optional[str],
expected_long_desc: T.Optional[str],
expected_blank_short_desc: bool,
expected_blank_long_desc: bool,
expected_full_desc: T.Optional[str],
) -> None:
"""Test parsing newlines around description sections."""
docstring = parse(source)
assert docstring.short_description == expected_short_desc
assert docstring.long_description == expected_long_desc
assert docstring.blank_after_short_description == expected_blank_short_desc
assert docstring.blank_after_long_description == expected_blank_long_desc
assert docstring.description == expected_full_desc
assert len(docstring.meta) == 1
def test_meta_with_multiline_description() -> None:
"""Test parsing multiline meta documentation."""
docstring = parse(
"""
Short description
:meta: asd
1
2
3
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["meta"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
def test_multiple_meta() -> None:
"""Test parsing multiple meta."""
docstring = parse(
"""
Short description
:meta1: asd
1
2
3
:meta2: herp
:meta3: derp
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 3
assert docstring.meta[0].args == ["meta1"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[1].args == ["meta2"]
assert docstring.meta[1].description == "herp"
assert docstring.meta[2].args == ["meta3"]
assert docstring.meta[2].description == "derp"
def test_meta_with_args() -> None:
"""Test parsing meta with additional arguments."""
docstring = parse(
"""
Short description
:meta ene due rabe: asd
"""
)
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["meta", "ene", "due", "rabe"]
assert docstring.meta[0].description == "asd"
def test_params() -> None:
"""Test parsing params."""
docstring = parse("Short description")
assert len(docstring.params) == 0
docstring = parse(
"""
Short description
:param name: description 1
:param int priority: description 2
:param str? sender: description 3
:param str? message: description 4, defaults to 'hello'
:param str? multiline: long description 5,
defaults to 'bye'
"""
)
assert len(docstring.params) == 5
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "description 1"
assert docstring.params[0].default is None
assert not docstring.params[0].is_optional
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
assert not docstring.params[1].is_optional
assert docstring.params[1].default is None
assert docstring.params[2].arg_name == "sender"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description == "description 3"
assert docstring.params[2].is_optional
assert docstring.params[2].default is None
assert docstring.params[3].arg_name == "message"
assert docstring.params[3].type_name == "str"
assert (
docstring.params[3].description == "description 4, defaults to 'hello'"
)
assert docstring.params[3].is_optional
assert docstring.params[3].default == "'hello'"
assert docstring.params[4].arg_name == "multiline"
assert docstring.params[4].type_name == "str"
assert (
docstring.params[4].description
== "long description 5,\ndefaults to 'bye'"
)
assert docstring.params[4].is_optional
assert docstring.params[4].default == "'bye'"
docstring = parse(
"""
Short description
:param a: description a
:type a: int
:param int b: description b
"""
)
assert len(docstring.params) == 2
assert docstring.params[0].arg_name == "a"
assert docstring.params[0].type_name == "int"
assert docstring.params[0].description == "description a"
assert docstring.params[0].default is None
assert not docstring.params[0].is_optional
def test_returns() -> None:
"""Test parsing returns."""
docstring = parse(
"""
Short description
"""
)
assert docstring.returns is None
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 0
docstring = parse(
"""
Short description
:returns: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name is None
assert docstring.returns.description == "description"
assert not docstring.returns.is_generator
assert docstring.many_returns == [docstring.returns]
docstring = parse(
"""
Short description
:returns int: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == "description"
assert not docstring.returns.is_generator
assert docstring.many_returns == [docstring.returns]
docstring = parse(
"""
Short description
:returns: description
:rtype: int
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == "description"
assert not docstring.returns.is_generator
assert docstring.many_returns == [docstring.returns]
def test_yields() -> None:
"""Test parsing yields."""
docstring = parse(
"""
Short description
"""
)
assert docstring.returns is None
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 0
docstring = parse(
"""
Short description
:yields: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name is None
assert docstring.returns.description == "description"
assert docstring.returns.is_generator
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
docstring = parse(
"""
Short description
:yields int: description
"""
)
assert docstring.returns is not None
assert docstring.returns.type_name == "int"
assert docstring.returns.description == "description"
assert docstring.returns.is_generator
assert docstring.many_returns is not None
assert len(docstring.many_returns) == 1
assert docstring.many_returns[0] == docstring.returns
def test_raises() -> None:
"""Test parsing raises."""
docstring = parse(
"""
Short description
"""
)
assert len(docstring.raises) == 0
docstring = parse(
"""
Short description
:raises: description
"""
)
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name is None
assert docstring.raises[0].description == "description"
docstring = parse(
"""
Short description
:raises ValueError: description
"""
)
assert len(docstring.raises) == 1
assert docstring.raises[0].type_name == "ValueError"
assert docstring.raises[0].description == "description"
def test_broken_meta() -> None:
"""Test parsing broken meta."""
with pytest.raises(ParseError):
parse(":")
with pytest.raises(ParseError):
parse(":param herp derp")
with pytest.raises(ParseError):
parse(":param: invalid")
with pytest.raises(ParseError):
parse(":param with too many args: desc")
# these should not raise any errors
parse(":sthstrange: desc")
def test_deprecation() -> None:
"""Test parsing deprecation notes."""
docstring = parse(":deprecation: 1.1.0 this function will be removed")
assert docstring.deprecation is not None
assert docstring.deprecation.version == "1.1.0"
assert docstring.deprecation.description == "this function will be removed"
docstring = parse(":deprecation: this function will be removed")
assert docstring.deprecation is not None
assert docstring.deprecation.version is None
assert docstring.deprecation.description == "this function will be removed"
@pytest.mark.parametrize(
"rendering_style, expected",
[
(
RenderingStyle.COMPACT,
"Short description.\n"
"\n"
"Long description.\n"
"\n"
":param int foo: a description\n"
":param int bar: another description\n"
":returns float: a return",
),
(
RenderingStyle.CLEAN,
"Short description.\n"
"\n"
"Long description.\n"
"\n"
":param int foo: a description\n"
":param int bar: another description\n"
":returns float: a return",
),
(
RenderingStyle.EXPANDED,
"Short description.\n"
"\n"
"Long description.\n"
"\n"
":param foo:\n"
" a description\n"
":type foo: int\n"
":param bar:\n"
" another description\n"
":type bar: int\n"
":returns:\n"
" a return\n"
":rtype: float",
),
],
)
def test_compose(rendering_style: RenderingStyle, expected: str) -> None:
"""Test compose"""
docstring = parse(
"""
Short description.
Long description.
:param int foo: a description
:param int bar: another description
:return float: a return
"""
)
assert compose(docstring, rendering_style=rendering_style) == expected
def test_short_rtype() -> None:
"""Test abbreviated docstring with only return type information."""
string = "Short description.\n\n:rtype: float"
docstring = parse(string)
rendering_style = RenderingStyle.EXPANDED
assert compose(docstring, rendering_style=rendering_style) == string

View File

@@ -0,0 +1,64 @@
"""Test for utility functions."""
from docstring_parser.common import DocstringReturns
from docstring_parser.util import combine_docstrings
def test_combine_docstrings() -> None:
"""Test combine_docstrings wrapper."""
def fun1(arg_a, arg_b, arg_c, arg_d):
"""short_description: fun1
:param arg_a: fun1
:param arg_b: fun1
:return: fun1
"""
assert arg_a and arg_b and arg_c and arg_d
def fun2(arg_b, arg_c, arg_d, arg_e):
"""short_description: fun2
long_description: fun2
:param arg_b: fun2
:param arg_c: fun2
:param arg_e: fun2
"""
assert arg_b and arg_c and arg_d and arg_e
@combine_docstrings(fun1, fun2)
def decorated1(arg_a, arg_b, arg_c, arg_d, arg_e, arg_f):
"""
:param arg_e: decorated
:param arg_f: decorated
"""
assert arg_a and arg_b and arg_c and arg_d and arg_e and arg_f
assert decorated1.__doc__ == (
"short_description: fun2\n"
"\n"
"long_description: fun2\n"
"\n"
":param arg_a: fun1\n"
":param arg_b: fun1\n"
":param arg_c: fun2\n"
":param arg_e: fun2\n"
":param arg_f: decorated\n"
":returns: fun1"
)
@combine_docstrings(fun1, fun2, exclude=[DocstringReturns])
def decorated2(arg_a, arg_b, arg_c, arg_d, arg_e, arg_f):
assert arg_a and arg_b and arg_c and arg_d and arg_e and arg_f
assert decorated2.__doc__ == (
"short_description: fun2\n"
"\n"
"long_description: fun2\n"
"\n"
":param arg_a: fun1\n"
":param arg_b: fun1\n"
":param arg_c: fun2\n"
":param arg_e: fun2"
)

View File

@@ -0,0 +1,144 @@
"""Utility functions for working with docstrings."""
import typing as T
from collections import ChainMap
from inspect import Signature
from itertools import chain
from .common import (
DocstringMeta,
DocstringParam,
DocstringReturns,
DocstringStyle,
RenderingStyle,
)
from .parser import compose, parse
_Func = T.Callable[..., T.Any]
assert DocstringReturns # used in docstring
def combine_docstrings(
*others: _Func,
exclude: T.Iterable[T.Type[DocstringMeta]] = (),
style: DocstringStyle = DocstringStyle.AUTO,
rendering_style: RenderingStyle = RenderingStyle.COMPACT,
) -> _Func:
"""A function decorator that parses the docstrings from `others`,
programmatically combines them with the parsed docstring of the decorated
function, and replaces the docstring of the decorated function with the
composed result. Only parameters that are part of the decorated functions
signature are included in the combined docstring. When multiple sources for
a parameter or docstring metadata exists then the decorator will first
default to the wrapped function's value (when available) and otherwise use
the rightmost definition from ``others``.
The following example illustrates its usage:
>>> def fun1(a, b, c, d):
... '''short_description: fun1
...
... :param a: fun1
... :param b: fun1
... :return: fun1
... '''
>>> def fun2(b, c, d, e):
... '''short_description: fun2
...
... long_description: fun2
...
... :param b: fun2
... :param c: fun2
... :param e: fun2
... '''
>>> @combine_docstrings(fun1, fun2)
>>> def decorated(a, b, c, d, e, f):
... '''
... :param e: decorated
... :param f: decorated
... '''
>>> print(decorated.__doc__)
short_description: fun2
<BLANKLINE>
long_description: fun2
<BLANKLINE>
:param a: fun1
:param b: fun1
:param c: fun2
:param e: fun2
:param f: decorated
:returns: fun1
>>> @combine_docstrings(fun1, fun2, exclude=[DocstringReturns])
>>> def decorated(a, b, c, d, e, f): pass
>>> print(decorated.__doc__)
short_description: fun2
<BLANKLINE>
long_description: fun2
<BLANKLINE>
:param a: fun1
:param b: fun1
:param c: fun2
:param e: fun2
:param others: callables from which to parse docstrings.
:param exclude: an iterable of ``DocstringMeta`` subclasses to exclude when
combining docstrings.
:param style: style composed docstring. The default will infer the style
from the decorated function.
:param rendering_style: The rendering style used to compose a docstring.
:return: the decorated function with a modified docstring.
"""
def wrapper(func: _Func) -> _Func:
sig = Signature.from_callable(func)
comb_doc = parse(func.__doc__ or "")
docs = [parse(other.__doc__ or "") for other in others] + [comb_doc]
params = dict(
ChainMap(
*(
{param.arg_name: param for param in doc.params}
for doc in docs
)
)
)
for doc in reversed(docs):
if not doc.short_description:
continue
comb_doc.short_description = doc.short_description
comb_doc.blank_after_short_description = (
doc.blank_after_short_description
)
break
for doc in reversed(docs):
if not doc.long_description:
continue
comb_doc.long_description = doc.long_description
comb_doc.blank_after_long_description = (
doc.blank_after_long_description
)
break
combined = {}
for doc in docs:
metas = {}
for meta in doc.meta:
meta_type = type(meta)
if meta_type in exclude:
continue
metas.setdefault(meta_type, []).append(meta)
for (meta_type, meta) in metas.items():
combined[meta_type] = meta
combined[DocstringParam] = [
params[name] for name in sig.parameters if name in params
]
comb_doc.meta = list(chain(*combined.values()))
func.__doc__ = compose(
comb_doc, style=style, rendering_style=rendering_style
)
return func
return wrapper