Coverage for sources/ictr/standard/renderables.py: 40%

81 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''' Protocols and default implementations for renderable objects. ''' 

22 

23 

24import json as _json 

25 

26from . import __ 

27from . import linearizers as _linearizers 

28 

29 

30DictionaryRenderable = __.Renderable 

31DictionaryRenderableDataclass = __.RenderableDataclass 

32 

33 

34@__.typx.runtime_checkable 

35class JsonRenderable( 

36 __.Renderable, __.typx.Protocol, 

37 class_mutables = __.PROTOCOL_RTC_MUTABLES, 

38): 

39 ''' Objects which can be rendered as JSON. ''' 

40 

41 def render_as_json( 

42 self, 

43 auxdata: _linearizers.LinearizerState, /, *, 

44 compact: bool = False, 

45 indent: int = 2, 

46 ) -> str: 

47 ''' Returns JSON string representation. ''' 

48 dictionary = self.render_as_dictionary( ) 

49 return _render_as_json( 

50 dictionary, compact = compact, indent = indent ) 

51 

52 

53@__.typx.runtime_checkable 

54class JsonRenderableDataclass( 

55 JsonRenderable, __.RenderableDataclass, __.typx.Protocol, 

56 class_mutables = __.PROTOCOL_RTC_MUTABLES, 

57): 

58 ''' Dataclass objects which can be rendered as JSON. ''' 

59 

60 

61@__.typx.runtime_checkable 

62class MarkdownRenderable( 

63 __.Renderable, __.typx.Protocol, 

64 class_mutables = __.PROTOCOL_RTC_MUTABLES, 

65): 

66 ''' Objects which can be rendered as Markdown. ''' 

67 

68 def render_as_markdown( 

69 self, auxdata: _linearizers.LinearizerState, / 

70 ) -> str: 

71 ''' Returns Markdown string representation. ''' 

72 dictionary = self.render_as_dictionary( ) 

73 return _render_as_markdown( dictionary, auxdata ) 

74 

75 

76@__.typx.runtime_checkable 

77class MarkdownRenderableDataclass( 

78 MarkdownRenderable, __.RenderableDataclass, __.typx.Protocol, 

79 class_mutables = __.PROTOCOL_RTC_MUTABLES, 

80): 

81 ''' Dataclass objects which can be rendered as Markdown. ''' 

82 

83 

84def _dictionary_to_markdown_lines( 

85 dictionary: __.cabc.Mapping[ __.typx.Any, __.typx.Any ], 

86 auxdata: _linearizers.LinearizerState, 

87 level: int = 0, 

88) -> tuple[ str, ... ]: 

89 lines: list[ str ] = [ ] 

90 prefix = ( ' ' * level + '- ' ) if level else '' 

91 level += 1 

92 for name, value in dictionary.items( ): 

93 name_ = f"**{name}**" 

94 if value is None: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true

95 lines.append( f"{prefix}{name_}: (none)" ) 

96 elif isinstance( value, ( bool, int, float, str ) ): 96 ↛ 98line 96 didn't jump to line 98 because the condition on line 96 was always true

97 lines.append( f"{prefix}{name_}: {value}" ) 

98 elif isinstance( value, __.cabc.Container ) and not value: 

99 lines.append( f"{prefix}{name_}: (empty)" ) 

100 elif isinstance( value, __.cabc.Sequence ): 

101 lines.append( f"{prefix}{name_}:" ) 

102 lines.extend( _sequence_to_markdown_lines( 

103 __.typx.cast( __.cabc.Sequence[ __.typx.Any ], value ), 

104 auxdata, level = level ) ) 

105 elif isinstance( value, __.cabc.Mapping ): 

106 lines.append( f"{prefix}{name_}:" ) 

107 lines.extend( _dictionary_to_markdown_lines( 

108 __.typx.cast( 

109 __.cabc.Mapping[ __.typx.Any, __.typx.Any ], value ), 

110 auxdata, level ) ) 

111 else: lines.append( f"{prefix}{name_}: {value}" ) 

112 return tuple( lines ) 

113 

114 

115def _render_as_json( 

116 dictionary: dict[ str, __.typx.Any ], 

117 compact: bool = False, indent: int = 2, 

118) -> str: 

119 ''' Returns JSON string representation. ''' 

120 if compact: 

121 return _json.dumps( 

122 dictionary, ensure_ascii = False, separators = ( ',', ':' ) ) 

123 return _json.dumps( dictionary, ensure_ascii = False, indent = indent ) 

124 

125 

126def _render_as_markdown( 

127 dictionary: dict[ str, __.typx.Any ], 

128 auxdata: _linearizers.LinearizerState, 

129 level: int = 0, 

130) -> str: 

131 ''' Returns Markdown string representation. ''' 

132 lines = _dictionary_to_markdown_lines( dictionary, auxdata, level = level ) 

133 markdown = '\n'.join( lines ) 

134 if auxdata.colorize: 134 ↛ 137line 134 didn't jump to line 137 because the condition on line 134 was never true

135 # TODO? Parse colors from result. 

136 # markdown_c = __.rich_text.Text.from_ansi( markdown ) 

137 capture = __.io.StringIO( ) 

138 console = __.produce_rich_console( 

139 auxdata.control, capture, auxdata.columns_max ) 

140 console.print( __.rich_markdown.Markdown( markdown ) ) 

141 return capture.getvalue( ).rstrip( '\n' ) 

142 return markdown 

143 

144 

145def _sequence_to_markdown_lines( 

146 sequence: __.cabc.Sequence[ __.typx.Any ], 

147 auxdata: _linearizers.LinearizerState, 

148 level: int = 0, 

149) -> tuple[ str, ... ]: 

150 lines: list[ str ] = [ ] 

151 prefix = ' ' * level + '- ' 

152 level += 1 

153 for element in sequence: 

154 if element is None: 

155 lines.append( f"{prefix}(none)" ) 

156 elif isinstance( element, __.cabc.Container ) and not element: 

157 lines.append( f"{prefix}(empty)" ) 

158 elif isinstance( element, __.cabc.Sequence ): 

159 lines.append( prefix ) 

160 lines.extend( _sequence_to_markdown_lines( 

161 __.typx.cast( __.cabc.Sequence[ __.typx.Any ], element ), 

162 auxdata, level = level ) ) 

163 elif isinstance( element, __.cabc.Mapping ): 

164 lines.append( prefix ) 

165 lines.extend( _dictionary_to_markdown_lines( 

166 __.typx.cast( 

167 __.cabc.Mapping[ __.typx.Any, __.typx.Any ], element ), 

168 auxdata, level = level ) ) 

169 else: lines.append( f"{prefix}{element}" ) 

170 return tuple( lines ) 

171 

172 

173def _serialize_value( value: __.typx.Any ) -> __.typx.Any: 

174 ''' Recursively serializes a value for dictionary representation. ''' 

175 if isinstance( value, ( str, int, float, bool, __.types.NoneType ) ): 

176 return value 

177 if isinstance( value, __.cabc.Sequence ): 

178 return [ _serialize_value( e ) for e in value ] # pyright: ignore 

179 if isinstance( value, __.cabc.Mapping ): 

180 return { 

181 str( k ): _serialize_value( v ) # pyright: ignore 

182 for k, v in value.items( ) } # pyright: ignore 

183 if callable( getattr( value, 'render_as_dictionary', None ) ): 

184 return value.render_as_dictionary( ) 

185 if __.dcls.is_dataclass( value ) and not isinstance( value, type ): 

186 return __.render_as_dictionary( value ) 

187 return repr( value )