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

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 introducer with support for decorations and styles. ''' 

22 

23 

24from . import __ 

25from . import core as _core 

26 

27 

28class IntroducerConfiguration( __.immut.DataclassObject ): 

29 ''' Behaviors and format for text from standard introducer. ''' 

30 

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. 

42 

43 ``Words``: As words like ``TRACE0`` or ``ERROR``. 

44 ``Emoji``: As emoji like ``🔎`` or ``❌``. 

45 

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. 

58 

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. 

72 

73 Used by :py:func:`time.strftime` or equivalent. 

74 ''' ), 

75 ] = '%Y-%m-%d %H:%M:%S.%f' 

76 

77 

78INTRODUCER_CONFIGURATION_DEFAULT = IntroducerConfiguration( ) 

79 

80 

81class IntroducerState( __.immut.DataclassObject ): 

82 ''' Data transfer object for introducer state. ''' 

83 

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 

92 

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 ) 

106 

107 

108class Introducer( __.Introducer ): 

109 ''' Standard introducer. ''' 

110 

111 configuration: __.typx.Annotated[ 

112 IntroducerConfiguration, 

113 __.ddoc.Doc( 

114 ''' Default behaviors and format for introductory text. ''' ), 

115 ] = __.dcls.field( default_factory = IntroducerConfiguration ) 

116 

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 ) 

131 

132 

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 ) 

153 

154 

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 ) 

176 

177 

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 ) 

200 

201 

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_ ) 

219 

220 

221_trace_color_names: tuple[ str, ... ] = ( 

222 'grey85', 'grey82', 'grey78', 'grey74', 'grey70', 

223 'grey66', 'grey62', 'grey58', 'grey54', 'grey50' ) 

224 

225_trace_prefix_styles: tuple[ _core.Style, ... ] = tuple( 

226 _core.Style( fgcolor = name ) for name in _trace_color_names )