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

72 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-03-31 02:59 +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 ''' Produces truck and installs it into builtins with alias. 

89 

90 Replaces an existing truck, preserving its module configurations. 

91 

92 Library developers should call :py:func:`__.register_module` instead. 

93 ''' 

94 truck = produce_truck( 

95 flavors = flavors, 

96 active_flavors = active_flavors, 

97 trace_levels = trace_levels, 

98 mode = mode, 

99 stderr = stderr ) 

100 return truck.install( alias = alias ) 

101 

102 

103@_validate_arguments 

104def produce_console_formatter( 

105 console: _Console, 

106 # pylint: disable=unused-argument 

107 control: __.FormatterControl, 

108 mname: str, 

109 flavor: int | str, 

110 # pylint: enable=unused-argument 

111) -> __.Formatter: 

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

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

114 

115 

116@_validate_arguments 

117def produce_console_printer( 

118 console: _Console, 

119 # pylint: disable=unused-argument 

120 mname: str, 

121 flavor: __.Flavor, 

122 # pylint: enable=unused-argument 

123) -> __.Printer: 

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

125 

126 .. note:: 

127 

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

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

130 formatters. 

131 ''' 

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

133 

134 

135@_validate_arguments 

136def produce_pretty_formatter( 

137 # pylint: disable=unused-argument 

138 control: __.FormatterControl, 

139 mname: str, 

140 flavor: int | str, 

141 # pylint: enable=unused-argument 

142) -> __.Formatter: 

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

144 return _pretty_repr 

145 

146 

147@_validate_arguments 

148def produce_simple_printer( 

149 target: __.io.TextIOBase, 

150 # pylint: disable=unused-argument 

151 mname: str, 

152 flavor: __.Flavor, 

153 # pylint: enable=unused-argument 

154) -> __.Printer: 

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

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

157 

158 

159@_validate_arguments 

160def produce_truck( 

161 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

162 active_flavors: __.ProduceTruckActiveFlavorsArgument = __.absent, 

163 trace_levels: __.ProduceTruckTraceLevelsArgument = __.absent, 

164 mode: ProduceTruckModeArgument = Modes.Formatter, 

165 stderr: ProduceTruckStderrArgument = True, 

166) -> __.Truck: 

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

168 match mode: 

169 case Modes.Formatter: factory = _produce_formatter_truck 

170 case Modes.Printer: factory = _produce_printer_truck 

171 return factory( 

172 flavors = flavors, 

173 active_flavors = active_flavors, 

174 trace_levels = trace_levels, 

175 stderr = stderr ) 

176 

177 

178@_validate_arguments 

179def register_module( 

180 name: __.RegisterModuleNameArgument = __.absent, 

181 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

182 include_context: __.RegisterModuleIncludeContextArgument = __.absent, 

183 prefix_emitter: __.RegisterModulePrefixEmitterArgument = __.absent, 

184) -> None: 

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

186 

187 Intended for library developers to configure debugging flavors without 

188 overriding anything set by the application or other libraries. 

189 ''' 

190 __.register_module( 

191 name = name, 

192 flavors = flavors, 

193 formatter_factory = produce_pretty_formatter, 

194 include_context = include_context, 

195 prefix_emitter = prefix_emitter ) 

196 

197 

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

199 with console.capture( ) as capture: 

200 console.print( value ) 

201 return capture.get( ) 

202 

203 

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

205 with _windows_replace_ansi_sgr( ): 

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

207 console.print( text ) 

208 

209 

210def _produce_formatter_truck( 

211 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

212 active_flavors: __.ProduceTruckActiveFlavorsArgument = __.absent, 

213 trace_levels: __.ProduceTruckTraceLevelsArgument = __.absent, 

214 stderr: ProduceTruckStderrArgument = True, 

215) -> __.Truck: 

216 console = _Console( stderr = stderr ) 

217 gc_nomargs = { } 

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

219 generalcfg = __.VehicleConfiguration( 

220 formatter_factory = __.funct.partial( 

221 produce_console_formatter, console ), 

222 **gc_nomargs ) # pyright: ignore 

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

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

225 raise ConsoleTextIoInvalidity( target ) 

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

227 active_flavors = active_flavors, 

228 generalcfg = generalcfg, 

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

230 trace_levels = trace_levels ) 

231 return __.produce_truck( **nomargs ) 

232 

233 

234def _produce_printer_truck( 

235 flavors: __.ProduceTruckFlavorsArgument = __.absent, 

236 active_flavors: __.ProduceTruckActiveFlavorsArgument = __.absent, 

237 trace_levels: __.ProduceTruckTraceLevelsArgument = __.absent, 

238 stderr: ProduceTruckStderrArgument = True, 

239) -> __.Truck: 

240 console = _Console( stderr = stderr ) 

241 gc_nomargs = { } 

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

243 generalcfg = __.VehicleConfiguration( 

244 formatter_factory = produce_pretty_formatter, 

245 **gc_nomargs ) # pyright: ignore 

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

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

248 raise ConsoleTextIoInvalidity( target ) 

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

250 active_flavors = active_flavors, 

251 generalcfg = generalcfg, 

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

253 trace_levels = trace_levels ) 

254 return __.produce_truck( **nomargs ) 

255 

256 

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

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

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

260# return _icecream.DEFAULT_PREFIX 

261 

262 

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

264 with _windows_replace_ansi_sgr( ): 

265 print( text, file = target ) 

266 

267 

268@__.ctxl.contextmanager 

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

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

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

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

273 # Otherwise, rendering on terminal occurs normally. 

274 _colorama.init( ) 

275 yield 

276 _colorama.deinit( )