Coverage for sources/ictr/standard/introducers.py: 86%
84 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''' Standard introducer with support for decorations and styles. '''
24from . import __
25from . import core as _core
28class IntroducerConfiguration( __.immut.DataclassObject ):
29 ''' Behaviors and format for text from standard introducer. '''
31 auxiliaries: __.typx.Annotated[
32 _core.Auxiliaries,
33 __.typx.Doc( ''' Auxiliaries for interpolation. ''' ),
34 ] = __.dcls.field( default_factory = _core.Auxiliaries )
35 colorize: __.typx.Annotated[
36 bool, __.typx.Doc( ''' Attempt to colorize? ''' )
37 ] = True
38 label_as: __.typx.Annotated[
39 _core.LabelPresentations,
40 __.ddoc.Doc(
41 ''' How to present prefix label.
43 ``Words``: As words like ``TRACE0`` or ``ERROR``.
44 ``Emoji``: As emoji like ``🔎`` or ``❌``.
46 For both emoji and words: ``Emoji | Words``.
47 ''' )
48 ] = _core.LabelPresentations.Words
49 styles: __.typx.Annotated[
50 _core.InterpolantsStylesRegistry,
51 __.ddoc.Doc(
52 ''' Mapping of interpolant names to style objects. ''' ),
53 ] = __.dcls.field( default_factory = _core.InterpolantsStylesRegistry )
54 template: __.typx.Annotated[
55 str,
56 __.ddoc.Doc(
57 ''' String format for prefix.
59 The following interpolants are supported:
60 ``flavor``: Decorated flavor.
61 ``address``: Address of invoker.
62 ``timestamp``: Current timestamp, formatted as string.
63 ``process_id``: ID of current process according to OS kernel.
64 ``thread_id``: ID of current thread.
65 ``thread_name``: Name of current thread.
66 ''' ),
67 ] = "{flavor}| " # "{timestamp} [{module_qname}] {flavor}| "
68 ts_format: __.typx.Annotated[
69 str,
70 __.ddoc.Doc(
71 ''' String format for prefix timestamp.
73 Used by :py:func:`time.strftime` or equivalent.
74 ''' ),
75 ] = '%Y-%m-%d %H:%M:%S.%f'
78INTRODUCER_CONFIGURATION_DEFAULT = IntroducerConfiguration( )
81class IntroducerState( __.immut.DataclassObject ):
82 ''' Data transfer object for introducer state. '''
84 configuration: IntroducerConfiguration
85 control: __.TextualizationControl
86 colorize: __.typx.Annotated[ bool, __.ddoc.Doc( ''' Colorize? ''' ) ]
87 columns_max: __.typx.Annotated[
88 __.Absential[ int ],
89 __.ddoc.Doc(
90 ''' Available line length (maximum columns) of target. ''' ),
91 ] = __.absent
93 @classmethod
94 def from_configuration(
95 cls,
96 configuration: IntroducerConfiguration,
97 control: __.TextualizationControl,
98 columns_max: __.Absential[ int ] = __.absent,
99 ) -> __.typx.Self:
100 colorize = __.ENRICH and control.colorize and configuration.colorize
101 return cls(
102 configuration = configuration,
103 control = control,
104 colorize = colorize,
105 columns_max = columns_max )
108class Introducer( __.Introducer ):
109 ''' Standard introducer. '''
111 configuration: __.typx.Annotated[
112 IntroducerConfiguration,
113 __.ddoc.Doc(
114 ''' Default behaviors and format for introductory text. ''' ),
115 ] = __.dcls.field( default_factory = IntroducerConfiguration )
117 def __call__(
118 self,
119 control: __.TextualizationControl,
120 record: __.Record,
121 columns_max: __.Absential[ int ] = __.absent,
122 ) -> str:
123 configuration = self.configuration
124 auxdata = IntroducerState.from_configuration(
125 configuration = configuration,
126 control = control,
127 columns_max = columns_max )
128 if isinstance( record.flavor, int ):
129 return _render_trace_label( auxdata, record )
130 return _render_nominal_label( auxdata, record )
133def _render_nominal_label(
134 auxdata: IntroducerState, record: __.Record
135) -> str:
136 configuration = auxdata.configuration
137 styles = dict( configuration.styles )
138 flavor = record.flavor
139 if isinstance( flavor, int ): 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 raise __.FlavorMisclassification( flavor, expectation = 'string' )
141 name = __.flavor_aliases_standard.get( flavor, flavor )
142 spec = __.flavor_specifications_standard[ name ]
143 label = ''
144 if configuration.label_as & _core.LabelPresentations.Emoji:
145 if configuration.label_as & _core.LabelPresentations.Words: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 label = f"{spec.emoji} {spec.label}"
147 else: label = f"{spec.emoji}"
148 elif configuration.label_as & _core.LabelPresentations.Words: 148 ↛ 150line 148 didn't jump to line 150 because the condition on line 148 was always true
149 label = f"{spec.label}"
150 if auxdata.colorize:
151 styles[ 'flavor' ] = _core.Style( fgcolor = spec.color )
152 return _render_common( auxdata, record, styles, label )
155def _render_trace_label(
156 auxdata: IntroducerState, record: __.Record
157) -> str:
158 # TODO? Option to render indentation guides.
159 configuration = auxdata.configuration
160 styles = dict( configuration.styles )
161 flavor = record.flavor
162 if not isinstance( flavor, int ): 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true
163 raise __.FlavorMisclassification( flavor, expectation = 'int' )
164 level = flavor
165 label = ''
166 if configuration.label_as & _core.LabelPresentations.Emoji: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 if configuration.label_as & _core.LabelPresentations.Words:
168 label = f"🔎 TRACE{level}"
169 else: label = '🔎'
170 elif configuration.label_as & _core.LabelPresentations.Words: 170 ↛ 172line 170 didn't jump to line 172 because the condition on line 170 was always true
171 label = f"TRACE{level}"
172 if auxdata.colorize and level < len( _trace_color_names ): 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true
173 styles[ 'flavor' ] = (
174 _core.Style( fgcolor = _trace_color_names[ level ] ) )
175 return _render_common( auxdata, record, styles, label )
178def _render_common(
179 auxdata: IntroducerState,
180 record: __.Record,
181 styles: __.cabc.Mapping[ str, _core.Style ],
182 label: str
183) -> str:
184 # TODO? Performance optimization: Only compute and interpolate PID, thread,
185 # and timestamp, if capabilities set permits.
186 configuration = auxdata.configuration
187 auxiliaries = configuration.auxiliaries
188 thread = auxiliaries.thread_discoverer( )
189 interpolants: dict[ str, str ] = {
190 'flavor': label,
191 'address': record.address,
192 'timestamp': auxiliaries.time_formatter( configuration.ts_format ),
193 'process_id': str( auxiliaries.pid_discoverer( ) ),
194 'thread_id': str( thread.ident ),
195 'thread_name': thread.name,
196 }
197 if auxdata.colorize:
198 _stylize_interpolants( auxdata, interpolants, styles )
199 return configuration.template.format( **interpolants )
202def _stylize_interpolants(
203 auxdata: IntroducerState,
204 interpolants: dict[ str, str ],
205 styles: __.cabc.Mapping[ str, _core.Style ],
206) -> None:
207 style_default = styles.get( 'flavor' )
208 interpolants_: dict[ str, str ] = { }
209 for iname, ivalue in interpolants.items( ):
210 style = styles.get( iname, style_default )
211 if not style: continue # pragma: no branch
212 capture = __.io.StringIO( )
213 console = __.produce_rich_console(
214 auxdata.control, capture, auxdata.columns_max )
215 style_ = __.rich_style.Style( color = style.fgcolor )
216 console.print( ivalue, end = '', highlight = False, style = style_ )
217 interpolants_[ iname ] = capture.getvalue( )
218 interpolants.update( interpolants_ )
221_trace_color_names: tuple[ str, ... ] = (
222 'grey85', 'grey82', 'grey78', 'grey74', 'grey70',
223 'grey66', 'grey62', 'grey58', 'grey54', 'grey50' )
225_trace_prefix_styles: tuple[ _core.Style, ... ] = tuple(
226 _core.Style( fgcolor = name ) for name in _trace_color_names )