Coverage for sources / ictr / standard / compositors.py: 93%

99 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-12 01:33 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Standard compositor and renderers. ''' 

22 

23 

24from . import __ 

25from . import core as _core 

26 

27from .linearizers import linearize_omni as _linearize_omni 

28 

29 

30class Compositor( __.Compositor ): 

31 ''' Standard compositor. ''' 

32 

33 configuration: __.typx.Annotated[ 

34 _core.CompositorConfiguration, 

35 __.ddoc.Doc( ''' Default behaviors and format for text. ''' ), 

36 ] = __.dcls.field( default_factory = _core.CompositorConfiguration ) 

37 introducer: __.typx.Annotated[ 

38 __.IntroducerUnion, 

39 __.ddoc.Doc( 

40 ''' String or factory which produces introduction string. 

41 

42 Factory takes control object and record as arguments. 

43 Returns introduction string. 

44 ''' ), 

45 ] = f"{__.package_name}| " 

46 

47 def __call__( 

48 self, control: __.TextualizationControl, record: __.Record 

49 ) -> str: 

50 configuration = self.configuration 

51 ecfg = configuration.linearizercfg.exceptionscfg 

52 auxdata = _core.CompositorState.from_configuration( 

53 configuration = configuration, control = control ) 

54 content = record.content 

55 introducer = self.introducer 

56 introduction = ( 

57 introducer if isinstance( introducer, str ) 

58 else introducer( 

59 control, record, auxdata.linearizer.columns_max ) ) 

60 if isinstance( content, __.MessageContent ): 60 ↛ 71line 60 didn't jump to line 71 because the condition on line 60 was always true

61 summary_ = content.summary 

62 exception = ( 

63 None if isinstance( summary_, BaseException ) 

64 else ecfg.discover( ) ) 

65 summary = _render_summary( auxdata, introduction, summary_ ) 

66 details = tuple( 

67 _render_detail( auxdata, detail ) 

68 for detail in filter( None, ( exception, *content.details ) ) ) 

69 return configuration.details_separator.join( ( 

70 summary, *details ) ) 

71 raise __.ContentMisclassification( type( content ) ) 

72 

73 

74def _calculate_ccount_max( 

75 initial: str, subsequent: __.typx.Optional[ str ] 

76) -> int: 

77 i_ccount = __.count_columns_visual( initial ) 

78 if subsequent is None: return i_ccount 

79 return max( i_ccount, __.count_columns_visual( subsequent ) ) 

80 

81 

82def _render_detail( auxdata: _core.CompositorState, detail: object ) -> str: 

83 configuration = auxdata.configuration 

84 columns_max = auxdata.linearizer.columns_max 

85 detail_prefix_i = configuration.detail_prefix_initial 

86 detail_prefix_i_ccount = __.count_columns_visual( detail_prefix_i ) 

87 detail_prefix_s = configuration.detail_prefix_subsequent 

88 if detail_prefix_s is None: 

89 detail_prefix_s = ' ' * detail_prefix_i_ccount 

90 detail_prefix_ccount = _calculate_ccount_max( 

91 detail_prefix_i, detail_prefix_s ) 

92 line_prefix_ccount = _calculate_ccount_max( 

93 configuration.line_prefix_initial, 

94 configuration.line_prefix_subsequent ) 

95 prefix_ccount = line_prefix_ccount + detail_prefix_ccount 

96 match auxdata.linearizer.columns_constraint: 

97 case _core.ColumnsConstraints.Complect: 

98 remainder_ccount = ( 

99 __.absent if __.is_absent( columns_max ) 

100 else columns_max - prefix_ccount ) 

101 case _core.ColumnsConstraints.Exceed: 101 ↛ 103line 101 didn't jump to line 103 because the pattern on line 101 always matched

102 remainder_ccount = __.absent 

103 lines = iter( _linearize_omni( 

104 auxdata.linearizer, detail, remainder_ccount ) ) 

105 lines_final: list[ str ] = [ ] 

106 line_i = next( lines ) 

107 _update_lines_collection( 

108 configuration, lines_final, 

109 f"{detail_prefix_i}{line_i}", 

110 tuple( f"{detail_prefix_s}{line}" for line in lines ) ) 

111 return '\n'.join( lines_final ) 

112 

113 

114def _render_summary( 

115 auxdata: _core.CompositorState, introduction: str, summary: object 

116) -> str: 

117 match auxdata.linearizer.columns_constraint: 

118 case _core.ColumnsConstraints.Complect: 

119 return _complect_render_summary( auxdata, introduction, summary ) 

120 case _core.ColumnsConstraints.Exceed: 120 ↛ exitline 120 didn't return from function '_render_summary' because the pattern on line 120 always matched

121 return _exceed_render_summary( auxdata, introduction, summary ) 

122 

123 

124def _complect_render_summary( 

125 auxdata: _core.CompositorState, introduction: str, summary: object 

126) -> str: 

127 configuration = auxdata.configuration 

128 columns_max = auxdata.linearizer.columns_max 

129 line_prefix_i = configuration.line_prefix_initial 

130 intro_ccount = __.count_columns_visual( introduction ) 

131 prefix_ccount = _calculate_ccount_max( 

132 line_prefix_i, configuration.line_prefix_subsequent ) 

133 remainder_ccount = ( 

134 __.absent if __.is_absent( columns_max ) 

135 else columns_max - prefix_ccount ) 

136 lines_final: list[ str ] = [ ] 

137 lines = _linearize_omni( auxdata.linearizer, summary, remainder_ccount ) 

138 match len( lines ): 

139 case 0: raise __.SummaryLinearizationFailure( ) 139 ↛ exitline 139 didn't except from function '_complect_render_summary' because the raise on line 139 wasn't executed

140 case 1: 

141 content = lines[ 0 ] 

142 incision_point = 0 

143 if not __.is_absent( columns_max ): 143 ↛ 146line 143 didn't jump to line 146 because the condition on line 143 was always true

144 incision_point = ( 

145 configuration.summary_incision_ratio * columns_max ) 

146 isolate_introduction = incision_point <= intro_ccount 

147 if not isolate_introduction: 

148 candidate = f"{introduction} {content}" 

149 candidate_ccount = ( 

150 prefix_ccount + intro_ccount 

151 + __.count_columns_visual( content ) + 1 ) 

152 if candidate_ccount <= columns_max: 152 ↛ 155line 152 didn't jump to line 155 because the condition on line 152 was always true

153 lines_final.append( f"{line_prefix_i}{candidate}" ) 

154 else: 

155 _update_lines_collection( 

156 configuration, lines_final, introduction, lines ) 

157 else: 

158 _update_lines_collection( 

159 configuration, lines_final, introduction, lines ) 

160 case _: 

161 _update_lines_collection( 

162 configuration, lines_final, introduction, lines ) 

163 return '\n'.join( lines_final ) 

164 

165 

166def _exceed_render_summary( 

167 auxdata: _core.CompositorState, introduction: str, summary: object 

168) -> str: 

169 configuration = auxdata.configuration 

170 line_prefix_i = configuration.line_prefix_initial 

171 lines_final: list[ str ] = [ ] 

172 lines = _linearize_omni( auxdata.linearizer, summary ) 

173 match len( lines ): 

174 case 0: raise __.SummaryLinearizationFailure( ) 174 ↛ exitline 174 didn't except from function '_exceed_render_summary' because the raise on line 174 wasn't executed

175 case 1: 

176 content = lines[ 0 ] 

177 lines_final.append( f"{line_prefix_i}{introduction} {content}" ) 

178 case _: 

179 _update_lines_collection( 

180 configuration, lines_final, introduction, lines ) 

181 return '\n'.join( lines_final ) 

182 

183 

184def _update_lines_collection( 

185 configuration: _core.CompositorConfiguration, 

186 collector: list[ str ], 

187 line_initial: str, 

188 lines_subsequent: __.typx.Optional[ tuple[ str, ... ] ] = None, 

189) -> None: 

190 line_prefix_i = configuration.line_prefix_initial 

191 line_prefix_s = configuration.line_prefix_subsequent 

192 if line_prefix_s is None: 

193 line_prefix_s = ' ' * __.count_columns_visual( line_prefix_i ) 

194 collector.append( f"{line_prefix_i}{line_initial}" ) 

195 if lines_subsequent: 

196 collector.extend( 

197 f"{line_prefix_s}{line}" for line in lines_subsequent )