"""Common methods for parsing."""
import enum
from collections import UserDict
from dataclasses import dataclass
from typing import Optional, TypeVar, Union
from typing_extensions import TypeAlias
PARAM_KEYWORDS = {
"param",
"parameter",
"arg",
"argument",
"attribute",
"generics",
"key",
"keyword",
}
# These could be made into frozen sets
# Then one could create a dictionary
# {KEYWORD: FUNCTION_THAT_HANDLES_THAT_KEYWORD_SECTION} # noqa: ERA001
RAISES_KEYWORDS = {"raises", "raise", "except", "exception"}
DEPRECATION_KEYWORDS = {"deprecation", "deprecated"}
RETURNS_KEYWORDS = {"return", "returns"}
YIELDS_KEYWORDS = {"yield", "yields"}
EXAMPLES_KEYWORDS = {"example", "examples"}
[docs]
def clean_str(string: str) -> Optional[str]:
"""Strip a string and return None if it is now empty.
Parameters
----------
string : str
String to clean
Returns
-------
Optional[str]
None of the stripped string is empty. Otherwise the stripped string.
"""
string = string.strip()
return string if string != "" else None
[docs]
class ParseError(RuntimeError):
"""Base class for all parsing related errors."""
[docs]
class DocstringStyle(enum.Enum):
"""Docstring style."""
REST = 1
GOOGLE = 2
NUMPYDOC = 3
EPYDOC = 4
AUTO = 255
[docs]
class RenderingStyle(enum.Enum):
"""Rendering style when unparsing parsed docstrings."""
COMPACT = 1
CLEAN = 2
EXPANDED = 3
[docs]
@dataclass
class DocstringParam(DocstringMeta):
"""DocstringMeta symbolizing :param metadata."""
arg_name: str
type_name: Optional[str]
is_optional: Optional[bool]
default: Optional[str]
[docs]
@dataclass
class DocstringReturns(DocstringMeta):
"""DocstringMeta symbolizing :returns metadata."""
type_name: Optional[str]
is_generator: bool
return_name: Optional[str] = None
[docs]
@dataclass
class DocstringYields(DocstringMeta):
"""DocstringMeta symbolizing :yields metadata."""
type_name: Optional[str]
is_generator: bool
yield_name: Optional[str] = None
[docs]
@dataclass
class DocstringRaises(DocstringMeta):
"""DocstringMeta symbolizing :raises metadata."""
type_name: Optional[str]
MainSections: TypeAlias = Union[
DocstringParam, DocstringRaises, DocstringReturns, DocstringYields
]
[docs]
@dataclass
class DocstringDeprecated(DocstringMeta):
"""DocstringMeta symbolizing deprecation metadata."""
version: Optional[str]
[docs]
@dataclass
class DocstringExample(DocstringMeta):
"""DocstringMeta symbolizing example metadata."""
snippet: Optional[str]
K = TypeVar("K")
V = TypeVar("V")
[docs]
class KeyReturnDict(UserDict[K, V]):
"""Custom dict that returns the key back in missing case."""
def __missing__(self, key: K) -> K:
"""Return the key.
Parameters
----------
key : K
key to look for.
Returns
-------
K
Returns the key directly.
"""
return key
[docs]
class Docstring:
"""Docstring object representation."""
def __init__(
self,
style: Optional[DocstringStyle] = None,
section_titles: Optional[KeyReturnDict[str, str]] = None,
) -> None:
"""Initialize self.
Parameters
----------
style : Optional[DocstringStyle]
Style that this docstring was formatted in. (Default value = None)
"""
self.short_description: Optional[str] = None
self.long_description: Optional[str] = None
self.blank_after_short_description: bool = False
self.blank_after_long_description: bool = False
self.meta: list[DocstringMeta] = []
self.style: Optional[DocstringStyle] = style
self.section_titles: KeyReturnDict[str, str] = section_titles or KeyReturnDict()
def __bool__(self) -> bool:
"""Return True if the docstring has any content.
Returns
-------
bool
True if the docstring has any content.
"""
return any(
(
self.short_description,
self.long_description,
self.meta,
)
)
@property
def params(self) -> list[DocstringParam]:
"""Return a list of information on function params.
Returns
-------
list[DocstringParam]
list of information on function params
"""
return [item for item in self.meta if isinstance(item, DocstringParam)]
@property
def raises(self) -> list[DocstringRaises]:
"""Return a list of the exceptions that the function may raise.
Returns
-------
list[DocstringRaises]
list of the exceptions that the function may raise.
"""
return [item for item in self.meta if isinstance(item, DocstringRaises)]
@property
def returns(self) -> Optional[DocstringReturns]:
"""Return a single information on function return.
Takes the first return information.
Returns
-------
Optional[DocstringReturns]
Single information on function return.
"""
return next(
(item for item in self.meta if isinstance(item, DocstringReturns)),
None,
)
@property
def many_returns(self) -> list[DocstringReturns]:
"""Return a list of information on function return.
Returns
-------
list[DocstringReturns]
list of information on function return.
"""
return [item for item in self.meta if isinstance(item, DocstringReturns)]
@property
def yields(self) -> Optional[DocstringYields]:
"""Return information on function yield.
Takes the first generator information.
Returns
-------
Optional[DocstringYields]
Single information on function yield.
"""
return next(
(
item
for item in self.meta
if isinstance(item, DocstringYields) and item.is_generator
),
None,
)
@property
def many_yields(self) -> list[DocstringYields]:
"""Return a list of information on function yields.
Returns
-------
list[DocstringYields]
list of information on function yields.
"""
return [item for item in self.meta if isinstance(item, DocstringYields)]
@property
def deprecation(self) -> Optional[DocstringDeprecated]:
"""Return a single information on function deprecation notes.
Returns
-------
Optional[DocstringDeprecated]
single information on function deprecation notes.
"""
return next(
(item for item in self.meta if isinstance(item, DocstringDeprecated)),
None,
)
@property
def examples(self) -> list[DocstringExample]:
"""Return a list of information on function examples.
Returns
-------
list[DocstringExample]
list of information on function examples.
"""
return [item for item in self.meta if isinstance(item, DocstringExample)]
[docs]
def split_description(docstring: Docstring, desc_chunk: str) -> None:
"""Break description into short and long parts.
Parameters
----------
docstring : Docstring
Docstring to fill with description information.
desc_chunk : str
Chunk of the raw docstring representing the description.
"""
parts = desc_chunk.split("\n", 1)
docstring.short_description = parts[0] or None
if len(parts) > 1:
long_desc_chunk = parts[1] or ""
docstring.blank_after_short_description = long_desc_chunk.startswith("\n")
docstring.blank_after_long_description = long_desc_chunk.endswith("\n\n")
docstring.long_description = long_desc_chunk.strip() or None
[docs]
def append_description(docstring: Docstring, parts: list[str]) -> None:
"""Append the docstrings description to the output stream.
Parameters
----------
docstring : Docstring
Docstring whose information should be added.
parts : list[str]
List of strings representing the output of compose().
Descriptions should be added to this.
"""
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("")