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

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

2 

3from collections import ChainMap 

4from collections.abc import Iterable 

5from inspect import Signature 

6from itertools import chain 

7from typing import Callable 

8 

9from .common import ( 

10 DocstringMeta, 

11 DocstringParam, 

12 DocstringStyle, 

13 RenderingStyle, 

14) 

15from .parser import compose, parse 

16 

17_Func = Callable[..., object] 

18 

19 

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. 

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