"""Utility classes and methods for docstrings."""
from typing import Any, Literal, Callable, Self
from functools import wraps, partial
import re
[docs]
class NumpyDocString:
"""Numpy docstring parser.
Attributes
----------
sections
Sections dictionary.
"""
[docs]
def __init__(
self,
docstring_or_sections: str | dict[str, Any],
/
) -> None:
"""Initialization method.
Parameters
----------
docstring_or_sections
Raw docstring text or sections dictionary.
"""
if not docstring_or_sections or isinstance(docstring_or_sections, str):
docstring_or_sections = self.parse_sections(docstring_or_sections)
self.sections = docstring_or_sections
@property
def text(self) -> str:
docstring = []
for section in self.sections.values():
header = section.get("header")
content = section.get("content")
lines = []
if header:
lines.append(header)
if content:
lines.append(content)
docstring.append("\n".join(lines))
docstring = "\n".join(docstring).strip()
return docstring
[docs]
@staticmethod
def parse_sections(text: str) -> dict[str, str]:
"""Parse NumpyDoc style docstring sections.
Parameters
----------
text
Docstring text.
Returns
-------
dict
Mapping from section names to their
raw content.
"""
dct = {}
if not text:
return dct
rx_sep = re.compile(r"(?<=\s)\n(?=\s)")
rx_div = re.compile(r"^\s*-+\s*$")
sections = rx_sep.split(text)
dct["Header"] = { "content": sections[0] }
dct["Description"] = { "content": "" }
for section in sections[1:]:
lines = section.split("\n")
if len(lines) >= 2 and rx_div.search(lines[1]):
title = lines[0].strip()
head = "\n".join(lines[:2])
content = "\n".join(lines[2:])
dct[title] = {
"header": head,
"content": content
}
else:
content = "\n".join(lines)
dct["Description"]["content"] += content
return dct
[docs]
def merge(
self,
other: Self,
*,
__default: Literal["append", "replace"] = "append",
**kwds: Any
) -> Self:
"""Merge with ``other`` docstring.
Parameters
----------
__default
Default merging behavior. If ``"append"`` then section
content from ``other`` is appended to that of ``self``.
If ``"replace"`` then replaces it.
**kwds
Keyword arguments can be used to set different
merging policies (``"append"`` or ``"merge"``)
other sections. The policy for the ``"header"``
section is by default set to ``"replace"``,
so it has to be overriden here change this.
Returns
-------
doc
New :class:`NumpyDocString` object.
"""
kwds = { "Header": "replace", **kwds }
sections = { k: v.copy() for k, v in self.sections.items() }
for name, section in other.sections.items():
section = section.copy()
if name in sections:
action = kwds.get(name.title(), __default)
if action == "append":
sections[name]["content"] += section["content"]
elif action == "replace":
sections[name]["content"] = \
section["content"].rstrip()+"\n"
else:
raise ValueError(f"incorrect merge policy '{action}'")
else:
sections[name] = section
return self.__class__(sections)
def _inherit_docstring(
typ: type,
spec: dict[str, Any] | None = None,
*,
default: Literal["replace", "append"] = "append",
stop_at: type = object
) -> Callable:
"""Inherit docstring from parent.
See :func:`inherit_docstrings` for details.
"""
def merge_docs(obj, parent):
# pylint: disable=attribute-defined-outside-init
odoc = NumpyDocString(obj.__doc__)
pdoc = NumpyDocString(parent.__doc__)
return pdoc.merge(odoc, __default=default, **spec).text
spec = spec or {}
if not isinstance(typ, type):
raise TypeError("only types can inherit docstrings")
mro = []
for c in typ.mro()[1:]:
if c is stop_at:
break
mro.append(c)
if not mro:
return typ
parent = mro[0]
typ.__doc__ = merge_docs(typ, parent)
for name, obj in typ.__dict__.items():
if not isinstance(obj, Callable):
continue
parent_obj = getattr(parent, name, None)
if name in parent.__abstractmethods__ and parent_obj:
obj.__doc__ = merge_docs(obj, parent_obj)
return typ
[docs]
def inherit_docstring(*args: Any, **kwds: Any) -> Callable:
"""Decorator for inheriting and mergin docstrings from
parent classes.
Parameters
----------
obj
Class or a method.
which
Whether to inherit from the direct parent class
or from the first abstract base class in MRO.
spec
Dictionary mapping section names to either
``"append"`` or ``"replace"``, which sets
merge policies for different sections.
default
Default mergin policy.
"""
@wraps(_inherit_docstring)
def decorator(obj, *args, **kwds) -> Callable:
return _inherit_docstring(obj, *args, **kwds)
if not args or not isinstance(args[0], Callable):
return partial(decorator, *args, **kwds)
return decorator(*args)