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
« 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 -*-
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#============================================================================#
21''' Protocols and default implementations for renderable objects. '''
24import json as _json
26from . import __
27from . import linearizers as _linearizers
30DictionaryRenderable = __.Renderable
31DictionaryRenderableDataclass = __.RenderableDataclass
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. '''
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 )
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. '''
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. '''
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 )
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. '''
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 )
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 )
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
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 )
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 )