|
"""Utilities to allow inserting docstring fragments for common |
|
parameters into function and method docstrings.""" |
|
|
|
from collections.abc import Callable, Iterable, Mapping |
|
from typing import Protocol, TypeVar |
|
import sys |
|
|
|
__all__ = [ |
|
"docformat", |
|
"inherit_docstring_from", |
|
"indentcount_lines", |
|
"filldoc", |
|
"unindent_dict", |
|
"unindent_string", |
|
"extend_notes_in_docstring", |
|
"replace_notes_in_docstring", |
|
"doc_replace", |
|
] |
|
|
|
_F = TypeVar("_F", bound=Callable[..., object]) |
|
|
|
|
|
class Decorator(Protocol): |
|
"""A decorator of a function.""" |
|
|
|
def __call__(self, func: _F, /) -> _F: ... |
|
|
|
|
|
def docformat(docstring: str, docdict: Mapping[str, str] | None = None) -> str: |
|
"""Fill a function docstring from variables in dictionary. |
|
|
|
Adapt the indent of the inserted docs |
|
|
|
Parameters |
|
---------- |
|
docstring : str |
|
A docstring from a function, possibly with dict formatting strings. |
|
docdict : dict[str, str], optional |
|
A dictionary with keys that match the dict formatting strings |
|
and values that are docstring fragments to be inserted. The |
|
indentation of the inserted docstrings is set to match the |
|
minimum indentation of the ``docstring`` by adding this |
|
indentation to all lines of the inserted string, except the |
|
first. |
|
|
|
Returns |
|
------- |
|
docstring : str |
|
string with requested ``docdict`` strings inserted. |
|
|
|
Examples |
|
-------- |
|
>>> docformat(' Test string with %(value)s', {'value':'inserted value'}) |
|
' Test string with inserted value' |
|
>>> docstring = 'First line\\n Second line\\n %(value)s' |
|
>>> inserted_string = "indented\\nstring" |
|
>>> docdict = {'value': inserted_string} |
|
>>> docformat(docstring, docdict) |
|
'First line\\n Second line\\n indented\\n string' |
|
""" |
|
if not docstring: |
|
return docstring |
|
if docdict is None: |
|
docdict = {} |
|
if not docdict: |
|
return docstring |
|
lines = docstring.expandtabs().splitlines() |
|
|
|
if len(lines) < 2: |
|
icount = 0 |
|
else: |
|
icount = indentcount_lines(lines[1:]) |
|
indent = " " * icount |
|
|
|
indented = {} |
|
for name, dstr in docdict.items(): |
|
lines = dstr.expandtabs().splitlines() |
|
try: |
|
newlines = [lines[0]] |
|
for line in lines[1:]: |
|
newlines.append(indent + line) |
|
indented[name] = "\n".join(newlines) |
|
except IndexError: |
|
indented[name] = dstr |
|
return docstring % indented |
|
|
|
|
|
def inherit_docstring_from(cls: object) -> Decorator: |
|
"""This decorator modifies the decorated function's docstring by |
|
replacing occurrences of '%(super)s' with the docstring of the |
|
method of the same name from the class `cls`. |
|
|
|
If the decorated method has no docstring, it is simply given the |
|
docstring of `cls`s method. |
|
|
|
Parameters |
|
---------- |
|
cls : type or object |
|
A class with a method with the same name as the decorated method. |
|
The docstring of the method in this class replaces '%(super)s' in the |
|
docstring of the decorated method. |
|
|
|
Returns |
|
------- |
|
decfunc : function |
|
The decorator function that modifies the __doc__ attribute |
|
of its argument. |
|
|
|
Examples |
|
-------- |
|
In the following, the docstring for Bar.func created using the |
|
docstring of `Foo.func`. |
|
|
|
>>> class Foo: |
|
... def func(self): |
|
... '''Do something useful.''' |
|
... return |
|
... |
|
>>> class Bar(Foo): |
|
... @inherit_docstring_from(Foo) |
|
... def func(self): |
|
... '''%(super)s |
|
... Do it fast. |
|
... ''' |
|
... return |
|
... |
|
>>> b = Bar() |
|
>>> b.func.__doc__ |
|
'Do something useful.\n Do it fast.\n ' |
|
""" |
|
|
|
def _doc(func: _F) -> _F: |
|
cls_docstring = getattr(cls, func.__name__).__doc__ |
|
func_docstring = func.__doc__ |
|
if func_docstring is None: |
|
func.__doc__ = cls_docstring |
|
else: |
|
new_docstring = func_docstring % dict(super=cls_docstring) |
|
func.__doc__ = new_docstring |
|
return func |
|
|
|
return _doc |
|
|
|
|
|
def extend_notes_in_docstring(cls: object, notes: str) -> Decorator: |
|
"""This decorator replaces the decorated function's docstring |
|
with the docstring from corresponding method in `cls`. |
|
It extends the 'Notes' section of that docstring to include |
|
the given `notes`. |
|
|
|
Parameters |
|
---------- |
|
cls : type or object |
|
A class with a method with the same name as the decorated method. |
|
The docstring of the method in this class replaces the docstring of the |
|
decorated method. |
|
notes : str |
|
Additional notes to append to the 'Notes' section of the docstring. |
|
|
|
Returns |
|
------- |
|
decfunc : function |
|
The decorator function that modifies the __doc__ attribute |
|
of its argument. |
|
""" |
|
|
|
def _doc(func: _F) -> _F: |
|
cls_docstring = getattr(cls, func.__name__).__doc__ |
|
|
|
|
|
if cls_docstring is None: |
|
return func |
|
end_of_notes = cls_docstring.find(" References\n") |
|
if end_of_notes == -1: |
|
end_of_notes = cls_docstring.find(" Examples\n") |
|
if end_of_notes == -1: |
|
end_of_notes = len(cls_docstring) |
|
func.__doc__ = ( |
|
cls_docstring[:end_of_notes] + notes + cls_docstring[end_of_notes:] |
|
) |
|
return func |
|
|
|
return _doc |
|
|
|
|
|
def replace_notes_in_docstring(cls: object, notes: str) -> Decorator: |
|
"""This decorator replaces the decorated function's docstring |
|
with the docstring from corresponding method in `cls`. |
|
It replaces the 'Notes' section of that docstring with |
|
the given `notes`. |
|
|
|
Parameters |
|
---------- |
|
cls : type or object |
|
A class with a method with the same name as the decorated method. |
|
The docstring of the method in this class replaces the docstring of the |
|
decorated method. |
|
notes : str |
|
The notes to replace the existing 'Notes' section with. |
|
|
|
Returns |
|
------- |
|
decfunc : function |
|
The decorator function that modifies the __doc__ attribute |
|
of its argument. |
|
""" |
|
|
|
def _doc(func: _F) -> _F: |
|
cls_docstring = getattr(cls, func.__name__).__doc__ |
|
notes_header = " Notes\n -----\n" |
|
|
|
|
|
if cls_docstring is None: |
|
return func |
|
start_of_notes = cls_docstring.find(notes_header) |
|
end_of_notes = cls_docstring.find(" References\n") |
|
if end_of_notes == -1: |
|
end_of_notes = cls_docstring.find(" Examples\n") |
|
if end_of_notes == -1: |
|
end_of_notes = len(cls_docstring) |
|
func.__doc__ = ( |
|
cls_docstring[: start_of_notes + len(notes_header)] |
|
+ notes |
|
+ cls_docstring[end_of_notes:] |
|
) |
|
return func |
|
|
|
return _doc |
|
|
|
|
|
def indentcount_lines(lines: Iterable[str]) -> int: |
|
"""Minimum indent for all lines in line list |
|
|
|
Parameters |
|
---------- |
|
lines : Iterable[str] |
|
The lines to find the minimum indent of. |
|
|
|
Returns |
|
------- |
|
indent : int |
|
The minimum indent. |
|
|
|
|
|
Examples |
|
-------- |
|
>>> lines = [' one', ' two', ' three'] |
|
>>> indentcount_lines(lines) |
|
1 |
|
>>> lines = [] |
|
>>> indentcount_lines(lines) |
|
0 |
|
>>> lines = [' one'] |
|
>>> indentcount_lines(lines) |
|
1 |
|
>>> indentcount_lines([' ']) |
|
0 |
|
""" |
|
indentno = sys.maxsize |
|
for line in lines: |
|
stripped = line.lstrip() |
|
if stripped: |
|
indentno = min(indentno, len(line) - len(stripped)) |
|
if indentno == sys.maxsize: |
|
return 0 |
|
return indentno |
|
|
|
|
|
def filldoc(docdict: Mapping[str, str], unindent_params: bool = True) -> Decorator: |
|
"""Return docstring decorator using docdict variable dictionary. |
|
|
|
Parameters |
|
---------- |
|
docdict : dict[str, str] |
|
A dictionary containing name, docstring fragment pairs. |
|
unindent_params : bool, optional |
|
If True, strip common indentation from all parameters in docdict. |
|
Default is False. |
|
|
|
Returns |
|
------- |
|
decfunc : function |
|
The decorator function that applies dictionary to its |
|
argument's __doc__ attribute. |
|
""" |
|
if unindent_params: |
|
docdict = unindent_dict(docdict) |
|
|
|
def decorate(func: _F) -> _F: |
|
|
|
doc = func.__doc__ or "" |
|
func.__doc__ = docformat(doc, docdict) |
|
return func |
|
|
|
return decorate |
|
|
|
|
|
def unindent_dict(docdict: Mapping[str, str]) -> dict[str, str]: |
|
"""Unindent all strings in a docdict. |
|
|
|
Parameters |
|
---------- |
|
docdict : dict[str, str] |
|
A dictionary with string values to unindent. |
|
|
|
Returns |
|
------- |
|
docdict : dict[str, str] |
|
The `docdict` dictionary but each of its string values are unindented. |
|
""" |
|
can_dict: dict[str, str] = {} |
|
for name, dstr in docdict.items(): |
|
can_dict[name] = unindent_string(dstr) |
|
return can_dict |
|
|
|
|
|
def unindent_string(docstring: str) -> str: |
|
"""Set docstring to minimum indent for all lines, including first. |
|
|
|
Parameters |
|
---------- |
|
docstring : str |
|
The input docstring to unindent. |
|
|
|
Returns |
|
------- |
|
docstring : str |
|
The unindented docstring. |
|
|
|
Examples |
|
-------- |
|
>>> unindent_string(' two') |
|
'two' |
|
>>> unindent_string(' two\\n three') |
|
'two\\n three' |
|
""" |
|
lines = docstring.expandtabs().splitlines() |
|
icount = indentcount_lines(lines) |
|
if icount == 0: |
|
return docstring |
|
return "\n".join([line[icount:] for line in lines]) |
|
|
|
|
|
def doc_replace(obj: object, oldval: str, newval: str) -> Decorator: |
|
"""Decorator to take the docstring from obj, with oldval replaced by newval |
|
|
|
Equivalent to ``func.__doc__ = obj.__doc__.replace(oldval, newval)`` |
|
|
|
Parameters |
|
---------- |
|
obj : object |
|
A class or object whose docstring will be used as the basis for the |
|
replacement operation. |
|
oldval : str |
|
The string to search for in the docstring. |
|
newval : str |
|
The string to replace `oldval` with in the docstring. |
|
|
|
Returns |
|
------- |
|
decfunc : function |
|
A decorator function that replaces occurrences of `oldval` with `newval` |
|
in the docstring of the decorated function. |
|
""" |
|
|
|
doc = (obj.__doc__ or "").replace(oldval, newval) |
|
|
|
def inner(func: _F) -> _F: |
|
func.__doc__ = doc |
|
return func |
|
|
|
return inner |
|
|