Coverage for sources / ictr / standard / core.py: 100%

91 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''' Core data structures and utilities. ''' 

22 

23 

24from . import __ 

25 

26 

27class Auxiliaries( __.immut.DataclassObject ): 

28 ''' Auxiliary functions used by textualizers and interpolation. 

29 

30 Typically used by unit tests to inject mock dependencies, 

31 but can also be used to deeply customize output. 

32 ''' 

33 

34 pid_discoverer: __.typx.Annotated[ 

35 __.typx.Callable[ [ ], int ], 

36 __.ddoc.Doc( ''' Returns ID of current process. ''' ), 

37 ] = __.os.getpid 

38 thread_discoverer: __.typx.Annotated[ 

39 __.typx.Callable[ [ ], __.threads.Thread ], 

40 __.ddoc.Doc( ''' Returns current thread. ''' ), 

41 ] = __.threads.current_thread 

42 time_formatter: __.typx.Annotated[ 

43 __.typx.Callable[ [ str ], str ], 

44 __.ddoc.Doc( ''' Returns current time in specified format. ''' ), 

45 ] = lambda fmt: __.Datetime.now( __.Timezone.utc ).strftime( fmt ) 

46 

47 

48class ColumnsConstraints( __.enum.Enum ): 

49 ''' How to constrain text which exceeds maximum columns. ''' 

50 

51 Complect = __.enum.auto( ) # fold/wrap 

52 Exceed = __.enum.auto( ) # overflow 

53 # Truncate = __.enum.auto( ) # chop/cut 

54 

55 

56class IncisionBoundaries( __.enum.Enum ): 

57 ''' Where to constrain text which exceeds maximum columns. ''' 

58 

59 Nowhere = __.enum.auto( ) 

60 Whitespace = __.enum.auto( ) # horizontal spaces and tabs 

61 Wordsplits = __.enum.auto( ) # hyphens + whitespace 

62 Anywhere = __.enum.auto( ) 

63 

64 

65class Style( __.immut.DataclassObject ): 

66 ''' Style for text. Corresponds to terminal attributes. ''' 

67 

68 bgcolor: __.typx.Optional[ str ] = None 

69 fgcolor: __.typx.Optional[ str ] = None 

70 # TODO: Int flag enum for bold, blink, etc... 

71 

72 

73InterpolantsStylesRegistry: __.typx.TypeAlias = ( 

74 __.accret.Dictionary[ str, Style ] ) 

75 

76 

77class LabelPresentations( __.enum.IntFlag ): 

78 ''' How introduction labels should be presented. ''' 

79 

80 Nothing = 0 

81 Words = __.enum.auto( ) 

82 Emoji = __.enum.auto( ) 

83 

84 

85class ExceptionsConfiguration( __.immut.DataclassObject ): 

86 ''' Configuration pertaining to exceptions. ''' 

87 

88 discoverer: __.typx.Annotated[ 

89 __.typx.Callable[ [ ], __.ExceptionInfo ], 

90 __.ddoc.Doc( ''' Returns information on current exception. ''' ), 

91 ] = __.sys.exc_info 

92 enable_discovery: __.typx.Annotated[ 

93 bool, __.ddoc.Doc( ''' Discover active exception? ''' ) 

94 ] = False 

95 enable_stacktraces: __.typx.Annotated[ 

96 bool, __.ddoc.Doc( ''' Render tracebacks? ''' ) 

97 ] = False 

98 recursive_stacktraces: __.typx.Annotated[ 

99 bool, __.ddoc.Doc( 

100 ''' Render traceback for each exception group member? ''' ), 

101 ] = False 

102 template: __.typx.Annotated[ 

103 str, __.ddoc.Doc( ''' Template for exception message. ''' ) 

104 ] = '[{name}] {message}' 

105 

106 def discover( self ) -> __.typx.Optional[ BaseException ]: 

107 ''' Discovers active exception. ''' 

108 return self.discoverer( )[ 1 ] if self.enable_discovery else None 

109 

110 def interpolate( self, exception: BaseException ) -> tuple[ str, ... ]: 

111 ''' Interpolates exception attributes into message template. ''' 

112 eclass = type( exception ) 

113 name = eclass.__name__ 

114 qname = eclass.__qualname__ 

115 mname = eclass.__module__ 

116 interpolants = dict( 

117 name = name, qname = qname, mname = mname, 

118 message = str( exception ) ) 

119 interpolants[ 'fqname' ] = f"{mname}.{qname}" 

120 return tuple( self.template.format( **interpolants ).split( '\n' ) ) 

121 

122 

123class IntroducerConfiguration( __.immut.DataclassObject ): 

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

125 

126 auxiliaries: __.typx.Annotated[ 

127 Auxiliaries, __.typx.Doc( ''' Auxiliaries for interpolation. ''' ) 

128 ] = __.dcls.field( default_factory = Auxiliaries ) 

129 colorize: __.typx.Annotated[ 

130 bool, __.typx.Doc( ''' Attempt to colorize? ''' ) 

131 ] = True 

132 label_as: __.typx.Annotated[ 

133 LabelPresentations, 

134 __.ddoc.Doc( 

135 ''' How to present prefix label. 

136 

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

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

139 

140 For both emoji and words: ``Emoji | Words``. 

141 ''' ) 

142 ] = LabelPresentations.Words 

143 styles: __.typx.Annotated[ 

144 InterpolantsStylesRegistry, 

145 __.ddoc.Doc( 

146 ''' Mapping of interpolant names to style objects. ''' ), 

147 ] = __.dcls.field( default_factory = InterpolantsStylesRegistry ) 

148 template: __.typx.Annotated[ 

149 str, 

150 __.ddoc.Doc( 

151 ''' String format for prefix. 

152 

153 The following interpolants are supported: 

154 ``flavor``: Decorated flavor. 

155 ``address``: Address of invoker. 

156 ``timestamp``: Current timestamp, formatted as string. 

157 ``process_id``: ID of current process according to OS kernel. 

158 ``thread_id``: ID of current thread. 

159 ``thread_name``: Name of current thread. 

160 ''' ), 

161 ] = "{flavor}| " # "{timestamp} [{module_qname}] {flavor}| " 

162 ts_format: __.typx.Annotated[ 

163 str, 

164 __.ddoc.Doc( 

165 ''' String format for prefix timestamp. 

166 

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

168 ''' ), 

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

170 

171 

172class IntroducerState( __.immut.DataclassObject ): 

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

174 

175 configuration: IntroducerConfiguration 

176 control: __.TextualizationControl 

177 colorize: __.typx.Annotated[ bool, __.ddoc.Doc( ''' Colorize? ''' ) ] 

178 columns_max: __.typx.Annotated[ 

179 __.Absential[ int ], 

180 __.ddoc.Doc( 

181 ''' Available line length (maximum columns) of target. ''' ), 

182 ] = __.absent 

183 

184 @classmethod 

185 def from_configuration( 

186 cls, 

187 configuration: IntroducerConfiguration, 

188 control: __.TextualizationControl, 

189 columns_max: __.Absential[ int ] = __.absent, 

190 ) -> __.typx.Self: 

191 colorize = __.ENRICH and control.colorize and configuration.colorize 

192 return cls( 

193 configuration = configuration, 

194 control = control, 

195 colorize = colorize, 

196 columns_max = columns_max ) 

197 

198 

199class LinearizerConfiguration( __.immut.DataclassObject ): 

200 ''' Behaviors for standard textual linearizer. ''' 

201 

202 colorize: __.typx.Annotated[ 

203 bool, __.typx.Doc( ''' Attempt to colorize? ''' ) 

204 ] = True 

205 columns_constraint: __.typx.Annotated[ 

206 ColumnsConstraints, 

207 __.ddoc.Doc( 

208 ''' How to constrain text which exceeds maximum columns. ''' ), 

209 ] = ColumnsConstraints.Complect 

210 columns_max: __.typx.Annotated[ 

211 __.typx.Optional[ int ], 

212 __.ddoc.Doc( 

213 ''' How many columns per line to assume if printer does not tell. 

214 

215 If ``None``, then infinite number of columns is assumed. 

216 ''' ), 

217 ] = None 

218 exceptionscfg: __.typx.Annotated[ 

219 ExceptionsConfiguration, 

220 __.ddoc.Doc( ''' Configuration pertaining to exceptions. ''' ), 

221 ] = __.dcls.field( default_factory = ExceptionsConfiguration ) 

222 incision_boundary: __.typx.Annotated[ 

223 IncisionBoundaries, 

224 __.ddoc.Doc( 

225 ''' Where to constrain text which exceeds maximum columns. ''' ), 

226 ] = IncisionBoundaries.Wordsplits 

227 

228 

229class LinearizerState( __.immut.DataclassObject ): 

230 ''' Data transfer object for linearizer state. ''' 

231 

232 configuration: LinearizerConfiguration 

233 control: __.TextualizationControl 

234 colorize: __.typx.Annotated[ bool, __.ddoc.Doc( ''' Colorize? ''' ) ] 

235 columns_constraint: __.typx.Annotated[ 

236 ColumnsConstraints, 

237 __.ddoc.Doc( ''' Effective columns constraint for lines. ''' ), 

238 ] = ColumnsConstraints.Exceed 

239 columns_max: __.typx.Annotated[ 

240 __.Absential[ int ], 

241 __.ddoc.Doc( 

242 ''' Available line length (maximum columns) of target. ''' ), 

243 ] = __.absent 

244 

245 @classmethod 

246 def from_configuration( 

247 cls, 

248 configuration: LinearizerConfiguration, 

249 control: __.TextualizationControl, 

250 ) -> __.typx.Self: 

251 colorize = __.ENRICH and control.colorize and configuration.colorize 

252 columns_constraint = configuration.columns_constraint 

253 columns_max = control.columns_max or configuration.columns_max 

254 if columns_max is None: 

255 columns_constraint = ColumnsConstraints.Exceed 

256 columns_max = __.absent 

257 return cls( 

258 configuration = configuration, 

259 control = control, 

260 colorize = colorize, 

261 columns_constraint = columns_constraint, 

262 columns_max = columns_max ) 

263 

264 

265class CompositorConfiguration( __.immut.DataclassObject ): 

266 ''' Behaviors and format for text from standard compositor. ''' 

267 

268 detail_prefix_initial: __.typx.Annotated[ 

269 str, __.ddoc.Doc( ''' Initial prefix for message detail. ''' ) 

270 ] = '' 

271 detail_prefix_subsequent: __.typx.Annotated[ 

272 __.typx.Optional[ str ], 

273 __.ddoc.Doc( 

274 ''' Subsequent prefix for message detail. 

275 

276 If ``None``, then automatic padding is calculated based on the 

277 visual width of the initial prefix for message detail. 

278 ''' ), 

279 ] = None 

280 # TODO? 'details_maximum' 

281 details_separator: __.typx.Annotated[ 

282 str, __.ddoc.Doc( ''' Separator between details. ''' ) 

283 ] = '\n\n' 

284 line_prefix_initial: __.typx.Annotated[ 

285 str, __.ddoc.Doc( ''' Prefix before first line. ''' ) 

286 ] = '' 

287 line_prefix_subsequent: __.typx.Annotated[ 

288 __.typx.Optional[ str ], 

289 __.ddoc.Doc( 

290 ''' Prefix before each line after the first. 

291 

292 If ``None``, then automatic padding is calculated based on the 

293 visual width of the initial line prefix. 

294 ''' ), 

295 ] = None 

296 linearizercfg: __.typx.Annotated[ 

297 LinearizerConfiguration, 

298 __.ddoc.Doc( 

299 ''' Text linearization and pretty-formatting behaviors. ''' ), 

300 ] = __.dcls.field( default_factory = LinearizerConfiguration ) 

301 summary_incision_ratio: __.typx.Annotated[ 

302 float, 

303 __.ddoc.Doc( 

304 ''' Ratio of introduction width to full width at which to split. 

305 

306 If ratio is met or exceeded, then introduction and summary are 

307 split onto consecutive lines. 

308 ''' ), 

309 ] = 0.3 

310 

311 

312class CompositorState( __.immut.DataclassObject ): 

313 ''' Data transfer object for textualizer state. ''' 

314 

315 configuration: CompositorConfiguration 

316 linearizer: LinearizerState 

317 

318 @classmethod 

319 def from_configuration( 

320 cls, 

321 configuration: CompositorConfiguration, 

322 control: __.TextualizationControl, 

323 ) -> __.typx.Self: 

324 linearizer = LinearizerState.from_configuration( 

325 configuration = configuration.linearizercfg, control = control ) 

326 return cls( configuration = configuration, linearizer = linearizer ) 

327 

328 

329COMPOSITOR_CONFIGURATION_DEFAULT = CompositorConfiguration( ) 

330INTRODUCER_CONFIGURATION_DEFAULT = IntroducerConfiguration( )