Coverage for pymend\docstring_parser\util.py: 97%

1"""Utility functions for working with docstrings."""

2

3 from collections import ChainMap

4 from collections.abc import Iterable

5 from inspect import Signature

6 from itertools import chain

7 from typing import Callable

8

9 from .common import (

10 DocstringMeta,

11 DocstringParam,

12 DocstringStyle,

13 RenderingStyle,

14)

15 from .parser import compose, parse

16

17 _Func = Callable[..., object]

18

19

20 def 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.

27

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``.

36

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)

49

50 Returns

51 -------

52 _Func

53 the decorated function with a modified docstring.

54

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 """

102

103 def wrapper(func: _Func) -> _Func:

104 """Wrap the function.

105

106 Parameters

107 ----------

108 func : _Func

109 Function to wrap.

110

111 Returns

112 -------

113 _Func

114 Wrapped function

115 """

116 sig = Signature.from_callable(func)

117

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 )

123

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

130

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

137

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

148

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

155

156 return wrapper