Coverage for sources/ictr/standard/linearizers.py: 95%
123 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 12:43 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 12:43 +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''' Conversion of objects to lines of text. '''
24from . import __
25from . import core as _core
28class LinearizerConfiguration( __.immut.DataclassObject ):
29 ''' Behaviors for standard textual linearizer. '''
31 colorize: __.typx.Annotated[
32 bool, __.typx.Doc( ''' Attempt to colorize? ''' )
33 ] = True
34 columns_constraint: __.typx.Annotated[
35 _core.ColumnsConstraints,
36 __.ddoc.Doc(
37 ''' How to constrain text which exceeds maximum columns. ''' ),
38 ] = _core.ColumnsConstraints.Complect
39 columns_max: __.typx.Annotated[
40 __.typx.Optional[ int ],
41 __.ddoc.Doc(
42 ''' How many columns per line to assume if printer does not tell.
44 If ``None``, then infinite number of columns is assumed.
45 ''' ),
46 ] = None
47 exceptionscfg: __.typx.Annotated[
48 _core.ExceptionsConfiguration,
49 __.ddoc.Doc( ''' Configuration pertaining to exceptions. ''' ),
50 ] = __.dcls.field( default_factory = _core.ExceptionsConfiguration )
51 incision_boundary: __.typx.Annotated[
52 _core.IncisionBoundaries,
53 __.ddoc.Doc(
54 ''' Where to constrain text which exceeds maximum columns. ''' ),
55 ] = _core.IncisionBoundaries.Wordsplits
58class LinearizerState( __.immut.DataclassObject ):
59 ''' Data transfer object for linearizer state. '''
61 configuration: LinearizerConfiguration
62 control: __.TextualizationControl
63 colorize: __.typx.Annotated[ bool, __.ddoc.Doc( ''' Colorize? ''' ) ]
64 columns_constraint: __.typx.Annotated[
65 _core.ColumnsConstraints,
66 __.ddoc.Doc( ''' Effective columns constraint for lines. ''' ),
67 ] = _core.ColumnsConstraints.Exceed
68 columns_max: __.typx.Annotated[
69 __.Absential[ int ],
70 __.ddoc.Doc(
71 ''' Available line length (maximum columns) of target. ''' ),
72 ] = __.absent
74 @classmethod
75 def from_configuration(
76 cls,
77 configuration: LinearizerConfiguration,
78 control: __.TextualizationControl,
79 ) -> __.typx.Self:
80 colorize = __.ENRICH and control.colorize and configuration.colorize
81 columns_constraint = configuration.columns_constraint
82 columns_max = control.columns_max or configuration.columns_max
83 if columns_max is None:
84 columns_constraint = _core.ColumnsConstraints.Exceed
85 columns_max = __.absent
86 return cls(
87 configuration = configuration,
88 control = control,
89 colorize = colorize,
90 columns_constraint = columns_constraint,
91 columns_max = columns_max )
94class Linearizer( __.Linearizer ):
96 configuration: __.typx.Annotated[
97 LinearizerConfiguration,
98 __.ddoc.Doc( ''' Default behaviors for textual linearizer. ''' ),
99 ] = __.dcls.field( default_factory = LinearizerConfiguration )
101 def __call__(
102 self,
103 control: __.TextualizationControl,
104 entity: object,
105 columns_max: __.Absential[ int ] = __.absent,
106 ) -> tuple[ str, ... ]:
107 auxdata = LinearizerState.from_configuration(
108 configuration = self.configuration, control = control )
109 return linearize_omni( auxdata, entity, columns_max = columns_max )
112def linearize_exception_plain(
113 auxdata: LinearizerState,
114 exception: BaseException,
115 columns_max: __.Absential[ int ] = __.absent,
116) -> tuple[ str, ... ]:
117 ecfg = auxdata.configuration.exceptionscfg
118 tbe = __.tb.TracebackException.from_exception( exception )
119 lines = [ *ecfg.interpolate( exception ) ]
120 if ecfg.enable_stacktraces:
121 lines.extend(
122 linearize_stacktrace_plain( auxdata, tbe.stack, columns_max ) )
123 # TODO: Process '__cause__' and '__context__'.
124 # TODO: Process exception groups.
125 return tuple( lines )
128def linearize_exception_rich(
129 auxdata: LinearizerState,
130 exception: BaseException,
131 columns_max: __.Absential[ int ] = __.absent,
132) -> tuple[ str, ... ]:
133 # TODO: Ensure that exception groups are handled properly.
134 ecfg = auxdata.configuration.exceptionscfg
135 capture = __.io.StringIO( )
136 console = __.produce_rich_console( auxdata.control, capture, columns_max )
137 if not ecfg.enable_stacktraces:
138 console.print( exception )
139 return tuple( capture.getvalue( ).split( '\n' ) )
140 traceback = __.rich_traceback.Traceback.from_exception(
141 type( exception ), exception, exception.__traceback__ )
142 console.print( traceback )
143 return tuple( capture.getvalue( ).split( '\n' ) )
146def linearize_object_plain(
147 auxdata: LinearizerState,
148 entity: object,
149 columns_max: __.Absential[ int ] = __.absent,
150) -> tuple[ str, ... ]:
151 # TODO? Pass configurable indentation width.
152 text = (
153 __.pprint.saferepr( entity ) if __.is_absent( columns_max )
154 else __.pprint.pformat( entity, indent = 2, width = columns_max ) )
155 return tuple( text.split( '\n' ) )
158def linearize_object_rich(
159 auxdata: LinearizerState,
160 entity: object,
161 columns_max: __.Absential[ int ] = __.absent,
162) -> tuple[ str, ... ]:
163 capture = __.io.StringIO( )
164 console = __.produce_rich_console( auxdata.control, capture, columns_max )
165 console.print( entity )
166 return tuple( capture.getvalue( ).split( '\n' ) )
169def linearize_omni(
170 auxdata: LinearizerState,
171 entity: object,
172 columns_max: __.Absential[ int ] = __.absent,
173) -> tuple[ str, ... ]:
174 if auxdata.colorize:
175 return linearize_omni_rich( auxdata, entity, columns_max )
176 return linearize_omni_plain( auxdata, entity, columns_max )
179def linearize_omni_plain(
180 auxdata: LinearizerState,
181 entity: object,
182 columns_max: __.Absential[ int ] = __.absent,
183) -> tuple[ str, ... ]:
184 if isinstance( entity, str ):
185 return linearize_text_plain( auxdata, entity, columns_max )
186 if isinstance( entity, BaseException ):
187 return linearize_exception_plain( auxdata, entity, columns_max )
188 return linearize_object_plain( auxdata, entity, columns_max )
191def linearize_omni_rich(
192 auxdata: LinearizerState,
193 entity: object,
194 columns_max: __.Absential[ int ] = __.absent,
195) -> tuple[ str, ... ]:
196 if isinstance( entity, str ):
197 return linearize_text_rich( auxdata, entity, columns_max )
198 if isinstance( entity, BaseException ): 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true
199 return linearize_exception_rich( auxdata, entity, columns_max )
200 return linearize_object_rich( auxdata, entity, columns_max )
203def linearize_stacktrace_plain(
204 auxdata: LinearizerState,
205 stacktrace: __.tb.StackSummary,
206 columns_max: __.Absential[ int ] = __.absent,
207) -> tuple[ str, ... ]:
208 infinite_lines = __.is_absent( columns_max )
209 lines: list[ str ] = [ ]
210 for frame in stacktrace:
211 filename_part = f"File '{frame.filename}'"
212 lineno_part = f"line {frame.lineno}" if frame.lineno else ''
213 name_part = f"in {frame.name}"
214 parts = ( filename_part, lineno_part, name_part )
215 address = ', '.join( filter( None, parts ) )
216 address_size = len( address )
217 if infinite_lines or address_size <= columns_max:
218 lines.append( address )
219 else:
220 excess_size = address_size - columns_max - 2 # sans ', '
221 parts = ( lineno_part, name_part )
222 address = ', '.join( filter( None, parts ) )
223 address_size = len( address )
224 if excess_size <= address_size: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true
225 lines.append( filename_part )
226 else:
227 excess_size = columns_max - len( frame.filename ) + 4
228 filename = frame.filename[ excess_size : ]
229 # TODO? Drop middle rather than start.
230 lines.append( f"File '... {filename}'" )
231 lines.append( address )
232 if frame.line: 232 ↛ 210line 232 didn't jump to line 210 because the condition on line 232 was always true
233 line = frame.line.strip( )
234 # TODO? Apply Pygments to line.
235 lines_ = iter(
236 linearize_text_plain(
237 auxdata, line,
238 __.absent if infinite_lines else columns_max - 4 ) )
239 lines.append( " {}".format( next( lines_ ) ) )
240 lines.extend( f" {line_}" for line_ in lines_ )
241 return tuple( lines )
244def linearize_stacktrace_rich(
245 auxdata: LinearizerState,
246 stacktrace: __.tb.StackSummary,
247 columns_max: __.Absential[ int ] = __.absent,
248) -> tuple[ str, ... ]:
249 frames = [
250 __.rich_traceback.Frame(
251 frame.filename, frame.lineno or -1, frame.name, frame.line or '' )
252 for frame in stacktrace ]
253 stack = __.rich_traceback.Stack(
254 exc_type = 'Callstack', exc_value = 'Inspection', frames = frames )
255 trace = __.rich_traceback.Trace( stacks = [ stack ] )
256 traceback = __.rich_traceback.Traceback( trace = trace )
257 capture = __.io.StringIO( )
258 console = __.produce_rich_console( auxdata.control, capture, columns_max )
259 console.print( traceback )
260 # TODO? Remove exception lines.
261 return tuple( capture.getvalue( ).split( '\n' ) )
264def linearize_text_plain(
265 auxdata: LinearizerState,
266 text: str,
267 columns_max: __.Absential[ int ] = __.absent,
268) -> tuple[ str, ... ]:
269 text_no_ansi = __.remove_ansi_c1_sequences( text )
270 if __.is_absent( columns_max ):
271 return tuple( text_no_ansi.split( '\n' ) )
272 configuration = auxdata.configuration
273 incise_excesses = (
274 configuration.incision_boundary
275 is not _core.IncisionBoundaries.Nowhere )
276 incise_naturally = (
277 configuration.incision_boundary
278 is _core.IncisionBoundaries.Wordsplits )
279 # TODO? Account for wide characters.
280 return tuple( __.textwrap.wrap(
281 text_no_ansi,
282 break_long_words = incise_excesses,
283 break_on_hyphens = incise_naturally,
284 width = columns_max ) )
287def linearize_text_rich(
288 auxdata: LinearizerState,
289 text: str,
290 columns_max: __.Absential[ int ] = __.absent,
291) -> tuple[ str, ... ]:
292 configuration = auxdata.configuration
293 text_ = __.rich_text.Text.from_ansi( text )
294 infinite_lines = __.is_absent( columns_max )
295 incise = (
296 not infinite_lines
297 and configuration.incision_boundary
298 is not _core.IncisionBoundaries.Nowhere )
299 capture = __.io.StringIO( )
300 console = __.produce_rich_console( auxdata.control, capture, columns_max )
301 console.print(
302 text_,
303 overflow = 'ignore' if infinite_lines else 'fold',
304 no_wrap = not incise )
305 return tuple( capture.getvalue( ).split( '\n' ) )