Coverage for pymend\docstring_parser\util.py: 97%
41 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-20 19:09 +0200
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-20 19:09 +0200
1"""Utility functions for working with docstrings."""
3from collections import ChainMap
4from collections.abc import Iterable
5from inspect import Signature
6from itertools import chain
7from typing import Callable
9from .common import (
10 DocstringMeta,
11 DocstringParam,
12 DocstringStyle,
13 RenderingStyle,
14)
15from .parser import compose, parse
17_Func = Callable[..., object]
20def combine_docstrings(
21 *others: _Func,
22 exclude: Iterable[type[DocstringMeta]] = (),
23 style: DocstringStyle = DocstringStyle.AUTO,
24 rendering_style: RenderingStyle = RenderingStyle.COMPACT,
25) -> _Func:
26 """Combine docstrings of multiple functions.
28 Parses the docstrings from `others`,
29 programmatically combines them with the parsed docstring of the decorated
30 function, and replaces the docstring of the decorated function with the
31 composed result. Only parameters that are part of the decorated functions
32 signature are included in the combined docstring. When multiple sources for
33 a parameter or docstring metadata exists then the decorator will first
34 default to the wrapped function's value (when available) and otherwise use
35 the rightmost definition from ``others``.
37 Parameters
38 ----------
39 *others : _Func
40 callables from which to parse docstrings.
41 exclude : Iterable[type[DocstringMeta]]
42 an iterable of ``DocstringMeta`` subclasses to exclude when
43 combining docstrings. (Default value = ())
44 style : DocstringStyle
45 Style that the docstrings are currently in. Default will infer style.
46 (Default value = DocstringStyle.AUTO)
47 rendering_style : RenderingStyle
48 Rendering style to use. (Default value = RenderingStyle.COMPACT)
50 Returns
51 -------
52 _Func
53 the decorated function with a modified docstring.
55 Examples
56 --------
57 >>> def fun1(a, b, c, d):
58 ... '''short_description: fun1
59 ...
60 ... :param a: fun1
61 ... :param b: fun1
62 ... :return: fun1
63 ... '''
64 >>> def fun2(b, c, d, e):
65 ... '''short_description: fun2
66 ...
67 ... long_description: fun2
68 ...
69 ... :param b: fun2
70 ... :param c: fun2
71 ... :param e: fun2
72 ... '''
73 >>> @combine_docstrings(fun1, fun2)
74 >>> def decorated(a, b, c, d, e, f):
75 ... '''
76 ... :param e: decorated
77 ... :param f: decorated
78 ... '''
79 >>> print(decorated.__doc__)
80 short_description: fun2
81 <BLANKLINE>
82 long_description: fun2
83 <BLANKLINE>
84 :param a: fun1
85 :param b: fun1
86 :param c: fun2
87 :param e: fun2
88 :param f: decorated
89 :returns: fun1
90 >>> @combine_docstrings(fun1, fun2, exclude=[DocstringReturns])
91 >>> def decorated(a, b, c, d, e, f): pass
92 >>> print(decorated.__doc__)
93 short_description: fun2
94 <BLANKLINE>
95 long_description: fun2
96 <BLANKLINE>
97 :param a: fun1
98 :param b: fun1
99 :param c: fun2
100 :param e: fun2
101 """
103 def wrapper(func: _Func) -> _Func:
104 """Wrap the function.
106 Parameters
107 ----------
108 func : _Func
109 Function to wrap.
111 Returns
112 -------
113 _Func
114 Wrapped function
115 """
116 sig = Signature.from_callable(func)
118 comb_doc = parse(func.__doc__ or "")
119 docs = [parse(other.__doc__ or "") for other in others] + [comb_doc]
120 params = dict(
121 ChainMap(*({param.arg_name: param for param in doc.params} for doc in docs))
122 )
124 for doc in reversed(docs): 124 ↛ 131line 124 didn't jump to line 131, because the loop on line 124 didn't complete
125 if not doc.short_description:
126 continue
127 comb_doc.short_description = doc.short_description
128 comb_doc.blank_after_short_description = doc.blank_after_short_description
129 break
131 for doc in reversed(docs): 131 ↛ 138line 131 didn't jump to line 138, because the loop on line 131 didn't complete
132 if not doc.long_description:
133 continue
134 comb_doc.long_description = doc.long_description
135 comb_doc.blank_after_long_description = doc.blank_after_long_description
136 break
138 combined: dict[type[DocstringMeta], list[DocstringMeta]] = {}
139 for doc in docs:
140 metas: dict[type[DocstringMeta], list[DocstringMeta]] = {}
141 for meta in doc.meta:
142 meta_type = type(meta)
143 if meta_type in exclude:
144 continue
145 metas.setdefault(meta_type, []).append(meta)
146 for meta_type, meta in metas.items():
147 combined[meta_type] = meta
149 combined[DocstringParam] = [
150 params[name] for name in sig.parameters if name in params
151 ]
152 comb_doc.meta = list(chain(*combined.values()))
153 func.__doc__ = compose(comb_doc, style=style, rendering_style=rendering_style)
154 return func
156 return wrapper