Coverage for sources/ictruck/recipes/rich.py: 100%

73 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-03-21 22:29 +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''' Recipes for Rich formatters and printers. 

22 

23 .. note:: 

24 

25 To use this module, you must have the ``rich`` package installed. 

26''' 

27 

28 

29from __future__ import annotations 

30 

31import colorama as _colorama 

32 

33from rich.console import Console as _Console 

34from rich.pretty import pretty_repr as _pretty_repr 

35 

36from . import __ 

37 

38 

39_validate_arguments = ( 

40 __.validate_arguments( 

41 globalvars = globals( ), 

42 errorclass = __.exceptions.ArgumentClassInvalidity ) ) 

43 

44 

45class ConsoleTextIoInvalidity( __.exceptions.Omnierror, TypeError ): 

46 ''' Text stream invalid for use with Rich console. ''' 

47 

48 def __init__( self, stream: __.typx.Any ): 

49 super( ).__init__( f"Invalid stream for Rich console: {stream!r}" ) 

50 

51 

52class Modes( __.enum.Enum ): 

53 ''' Operation modes for Rich truck. ''' 

54 Formatter = 'formatter' 

55 Printer = 'printer' 

56 

57 

58ProduceTruckModeArgument: __.typx.TypeAlias = __.typx.Annotated[ 

59 Modes, 

60 __.typx.Doc( 

61 ''' Operation mode. 

62 

63 ``Formatter`` uses Rich to highlight and pretty text prior to 

64 printing (output). Text from non-Rich formatters will be printed 

65 as-is. Safer, but slightly more boring option. 

66 ``Printer`` uses Rich to highlight and pretty text while printing 

67 (output). Text from non-Rich formatters will be potentially be 

68 highlighted and prettied. If the text already contains ANSI SGR 

69 sequences (e.g., terminal colorization), then it might be 

70 reprocessed by the printer, causing visual artifacts. Less safe, 

71 but more vibrant option. 

72 ''' ), 

73] 

74ProduceTruckStderrArgument: __.typx.TypeAlias = __.typx.Annotated[ 

75 bool, __.typx.Doc( ''' Output to standard diagnostic stream? ''' ) 

76] 

77 

78 

79@_validate_arguments 

80def install( # pylint: disable=too-many-arguments 

81 alias: __.InstallAliasArgument = __.builtins_alias_default, 

82 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

83 active_flavors: __.ProduceTruckActiveFlavorsArgument = __.absent, 

84 trace_levels: __.ProduceTruckTraceLevelsArgument = __.absent, 

85 mode: ProduceTruckModeArgument = Modes.Formatter, 

86 stderr: ProduceTruckStderrArgument = True, 

87) -> __.Truck: 

88 ''' Installs configured truck into builtins. 

89 

90 Application developers should call this early before importing 

91 library packages which may also use the builtin truck. 

92 ''' 

93 truck = produce_truck( 

94 flavors = flavors, 

95 active_flavors = active_flavors, 

96 trace_levels = trace_levels, 

97 mode = mode, 

98 stderr = stderr ) 

99 __.install_builtin_safely( 

100 alias, truck, __.exceptions.AttributeNondisplacement ) 

101 return truck 

102 

103 

104@_validate_arguments 

105def produce_console_formatter( 

106 console: _Console, 

107 # pylint: disable=unused-argument 

108 control: __.FormatterControl, 

109 mname: str, 

110 flavor: int | str, 

111 # pylint: enable=unused-argument 

112) -> __.Formatter: 

113 ''' Produces formatter which uses Rich highlighter and prettier. ''' 

114 return __.funct.partial( _console_format, console ) 

115 

116 

117@_validate_arguments 

118def produce_console_printer( 

119 console: _Console, 

120 # pylint: disable=unused-argument 

121 mname: str, 

122 flavor: __.Flavor, 

123 # pylint: enable=unused-argument 

124) -> __.Printer: 

125 ''' Produces a printer that uses Rich console printing. 

126 

127 .. note:: 

128 

129 May reprocess ANSI SGR codes or markup from formatters, potentially 

130 causing visual artifacts. Be careful to use this only with "safe" 

131 formatters. 

132 ''' 

133 return __.funct.partial( _console_print, console ) 

134 

135 

136@_validate_arguments 

137def produce_pretty_formatter( 

138 # pylint: disable=unused-argument 

139 control: __.FormatterControl, 

140 mname: str, 

141 flavor: int | str, 

142 # pylint: enable=unused-argument 

143) -> __.Formatter: 

144 ''' Produces formatter which uses Rich prettier. ''' 

145 return _pretty_repr 

146 

147 

148@_validate_arguments 

149def produce_simple_printer( 

150 target: __.io.TextIOBase, 

151 # pylint: disable=unused-argument 

152 mname: str, 

153 flavor: __.Flavor, 

154 # pylint: enable=unused-argument 

155) -> __.Printer: 

156 ''' Produces printer which uses standard Python 'print'. ''' 

157 return __.funct.partial( _simple_print, target = target ) 

158 

159 

160@_validate_arguments 

161def produce_truck( 

162 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

163 active_flavors: __.ProduceTruckActiveFlavorsArgument = __.absent, 

164 trace_levels: __.ProduceTruckTraceLevelsArgument = __.absent, 

165 mode: ProduceTruckModeArgument = Modes.Formatter, 

166 stderr: ProduceTruckStderrArgument = True, 

167) -> __.Truck: 

168 ''' Produces icecream truck which integrates with Rich. ''' 

169 match mode: 

170 case Modes.Formatter: factory = _produce_formatter_truck 

171 case Modes.Printer: factory = _produce_printer_truck 

172 return factory( 

173 flavors = flavors, 

174 active_flavors = active_flavors, 

175 trace_levels = trace_levels, 

176 stderr = stderr ) 

177 

178 

179@_validate_arguments 

180def register_module( 

181 name: __.RegisterModuleNameArgument = __.absent, 

182 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

183 include_context: __.RegisterModuleIncludeContextArgument = __.absent, 

184 prefix_emitter: __.RegisterModulePrefixEmitterArgument = __.absent, 

185) -> None: 

186 ''' Registers module with Rich prettier to format arguments. 

187 

188 Intended for library developers to configure debugging flavors without 

189 overriding anything set by the application or other libraries. 

190 ''' 

191 __.register_module( 

192 name = name, 

193 flavors = flavors, 

194 formatter_factory = produce_pretty_formatter, 

195 include_context = include_context, 

196 prefix_emitter = prefix_emitter ) 

197 

198 

199def _console_format( console: _Console, value: __.typx.Any ) -> str: 

200 with console.capture( ) as capture: 

201 console.print( value ) 

202 return capture.get( ) 

203 

204 

205def _console_print( console: _Console, text: str ) -> None: 

206 with _windows_replace_ansi_sgr( ): 

207 # console.print( text, markup = False ) 

208 console.print( text ) 

209 

210 

211def _produce_formatter_truck( 

212 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

213 active_flavors: __.ProduceTruckActiveFlavorsArgument = __.absent, 

214 trace_levels: __.ProduceTruckTraceLevelsArgument = __.absent, 

215 stderr: ProduceTruckStderrArgument = True, 

216) -> __.Truck: 

217 console = _Console( stderr = stderr ) 

218 gc_nomargs = { } 

219 if not __.is_absent( flavors ): gc_nomargs[ 'flavors' ] = flavors 

220 generalcfg = __.VehicleConfiguration( 

221 formatter_factory = __.funct.partial( 

222 produce_console_formatter, console ), 

223 **gc_nomargs ) # pyright: ignore 

224 target = __.sys.stderr if stderr else __.sys.stdout 

225 if not isinstance( target, __.io.TextIOBase ): 

226 raise ConsoleTextIoInvalidity( target ) 

227 nomargs: dict[ str, __.typx.Any ] = dict( 

228 active_flavors = active_flavors, 

229 generalcfg = generalcfg, 

230 printer_factory = __.funct.partial( produce_simple_printer, target ), 

231 trace_levels = trace_levels ) 

232 return __.produce_truck( **nomargs ) 

233 

234 

235def _produce_printer_truck( 

236 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

237 active_flavors: __.ProduceTruckActiveFlavorsArgument = __.absent, 

238 trace_levels: __.ProduceTruckTraceLevelsArgument = __.absent, 

239 stderr: ProduceTruckStderrArgument = True, 

240) -> __.Truck: 

241 console = _Console( stderr = stderr ) 

242 gc_nomargs = { } 

243 if not __.is_absent( flavors ): gc_nomargs[ 'flavors' ] = flavors 

244 generalcfg = __.VehicleConfiguration( 

245 formatter_factory = produce_pretty_formatter, 

246 **gc_nomargs ) # pyright: ignore 

247 target = __.sys.stderr if stderr else __.sys.stdout 

248 if not isinstance( target, __.io.TextIOBase ): # pragma: no cover 

249 raise ConsoleTextIoInvalidity( target ) 

250 nomargs: dict[ str, __.typx.Any ] = dict( 

251 active_flavors = active_flavors, 

252 generalcfg = generalcfg, 

253 printer_factory = __.funct.partial( produce_console_printer, console ), 

254 trace_levels = trace_levels ) 

255 return __.produce_truck( **nomargs ) 

256 

257 

258# def _produce_prefix( console: _Console, mname: str, flavor: _Flavor ) -> str: 

259# # TODO: Detect if terminal supports 256 colors or true color. 

260# # Make spectrum of hues for trace depths, if so. 

261# return _icecream.DEFAULT_PREFIX 

262 

263 

264def _simple_print( text: str, target: __.io.TextIOBase ) -> None: 

265 with _windows_replace_ansi_sgr( ): 

266 print( text, file = target ) 

267 

268 

269@__.ctxl.contextmanager 

270def _windows_replace_ansi_sgr( ) -> __.typx.Generator[ None, None, None ]: 

271 # Note: Copied from the 'icecream' sources. 

272 # Converts ANSI SGR sequences to Windows API calls on older 

273 # command terminals which do not have proper ANSI SGR support. 

274 # Otherwise, rendering on terminal occurs normally. 

275 _colorama.init( ) 

276 yield 

277 _colorama.deinit( )