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
« 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 -*-
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#============================================================================#
21''' Standard compositor and renderers. '''
24from . import __
25from . import core as _core
27from .linearizers import linearize_omni as _linearize_omni
30class Compositor( __.Compositor ):
31 ''' Standard compositor. '''
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.
42 Factory takes control object and record as arguments.
43 Returns introduction string.
44 ''' ),
45 ] = f"{__.package_name}| "
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 ) )
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 ) )
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 )
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 )
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 )
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 )
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 )