Coverage for src/ui/table_formatter.py: 73%

82 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-06-05 23:16 -0700

1""" 

2Table formatting utilities for TUI display. 

3 

4This module provides standardized table formatting functions using Rich library 

5to ensure consistent display of tabular data throughout the application. 

6""" 

7 

8from typing import Any, Dict, List, Union 

9 

10from rich.console import Console 

11from rich.table import Table 

12from rich.text import Text 

13from rich import box 

14 

15from src.ui.theme import TABLE_TITLE_STYLE, TABLE_BORDER_STYLE, HEADER_STYLE 

16 

17 

18def create_table( 

19 title: str = None, 

20 headers: List[str] = None, 

21 show_header: bool = True, 

22 show_lines: bool = True, 

23 box_style: str = "ROUNDED", 

24 padding: Union[int, tuple] = (0, 1), 

25 title_style: str = TABLE_TITLE_STYLE, 

26 header_style: str = HEADER_STYLE, 

27 border_style: str = TABLE_BORDER_STYLE, 

28 expand: bool = False, 

29) -> Table: 

30 """ 

31 Create a Rich Table with consistent styling. 

32 

33 Args: 

34 title: Optional title for the table 

35 headers: List of column headers 

36 show_header: Whether to display headers 

37 show_lines: Whether to show lines between rows 

38 box_style: Box style for the table (e.g., "ROUNDED", "MINIMAL", etc.) 

39 padding: Cell padding as (vertical, horizontal) or single value 

40 title_style: Style for the table title 

41 header_style: Style for column headers 

42 border_style: Style for table borders 

43 expand: Whether the table should expand to fill available width. 

44 Defaults to False so the table width matches its content. 

45 

46 Returns: 

47 A configured Rich Table object 

48 """ 

49 try: 

50 # Convert box_style to uppercase and get the corresponding box style 

51 box_style = box_style.upper() 

52 box_instance = getattr(box, box_style, box.ROUNDED) 

53 

54 table = Table( 

55 title=title, 

56 show_header=show_header, 

57 show_lines=show_lines, 

58 box=box_instance, 

59 padding=padding, 

60 title_style=title_style, 

61 border_style=border_style, 

62 expand=expand, 

63 ) 

64 

65 # Add headers if provided 

66 if headers and show_header: 

67 for header in headers: 

68 table.add_column(header, style=header_style) 

69 

70 return table 

71 except Exception as e: 

72 # Log error and fall back to default styling 

73 import logging 

74 

75 logging.error(f"Error creating table: {e}") 

76 

77 # Create a table with default styling as fallback 

78 table = Table( 

79 title=title, 

80 show_header=show_header, 

81 show_lines=show_lines, 

82 box=box.ROUNDED, 

83 padding=padding, 

84 title_style=title_style, 

85 border_style=border_style, 

86 expand=expand, 

87 ) 

88 

89 # Add headers if provided 

90 if headers and show_header: 

91 for header in headers: 

92 table.add_column(header, style=header_style) 

93 

94 return table 

95 

96 

97def format_cell(value: Any, style: Any = None, none_display: str = "N/A") -> Text: 

98 """ 

99 Format a cell value with consistent styling. 

100 

101 Args: 

102 value: The value to format 

103 style: Optional Rich style to apply (string or function) 

104 none_display: What to display for None values 

105 

106 Returns: 

107 A Rich Text object with formatted content 

108 """ 

109 # Handle None values 

110 if value is None: 

111 return Text(none_display, style="dim italic") 

112 

113 # Convert to string if not already 

114 if not isinstance(value, str): 

115 value = str(value) 

116 

117 # If style is a function, call it with the value 

118 applied_style = style 

119 if callable(style): 

120 try: 

121 applied_style = style(value) 

122 except Exception as e: 

123 import logging 

124 

125 logging.error(f"Error applying style function: {e}") 

126 applied_style = None 

127 

128 return Text(value, style=applied_style) 

129 

130 

131def add_row_with_styles( 

132 table: Table, 

133 row_data: List[Any], 

134 styles: List[str] = None, 

135) -> None: 

136 """ 

137 Add a row to a table with optional styling per cell. 

138 

139 Args: 

140 table: The Rich Table to add the row to 

141 row_data: List of cell values 

142 styles: Optional list of styles to apply to each cell 

143 """ 

144 # Initialize defaults if not provided 

145 if styles is None: 

146 styles = [None] * len(row_data) 

147 

148 # Format each cell and add to table 

149 formatted_cells = [ 

150 format_cell(data, style=style) for data, style in zip(row_data, styles) 

151 ] 

152 

153 table.add_row(*formatted_cells) 

154 

155 

156def add_rows_from_data( 

157 table: Table, 

158 data: List[Dict[str, Any]], 

159 columns: List[str], 

160 style_map: Dict[str, Any] = None, 

161) -> None: 

162 """ 

163 Add multiple rows from a list of dictionaries. 

164 

165 Args: 

166 table: The Rich Table to add rows to 

167 data: List of dictionaries containing row data 

168 columns: List of column keys to extract from each dictionary 

169 style_map: Optional dictionary mapping column names to styles or style functions 

170 Style functions can take the value, or both value and row 

171 """ 

172 if style_map is None: 

173 style_map = {} 

174 

175 for item in data: 

176 row_data = [] 

177 row_styles = [] 

178 

179 for col in columns: 

180 # Get the value for this column 

181 value = item.get(col) 

182 row_data.append(value) 

183 

184 # Get the style for this column 

185 style_func_or_value = style_map.get(col) 

186 

187 # If the style is callable, it might expect the row as additional context 

188 if callable(style_func_or_value): 

189 try: 

190 # First try calling with both value and row 

191 import inspect 

192 

193 sig = inspect.signature(style_func_or_value) 

194 if len(sig.parameters) > 1: 

195 # Function accepts multiple parameters, pass value and row 

196 style = style_func_or_value(value, item) 

197 else: 

198 # Function accepts only one parameter, just pass value 

199 style = style_func_or_value(value) 

200 except Exception: 

201 # Fall back to just passing the value 

202 try: 

203 style = style_func_or_value(value) 

204 except Exception: 

205 style = None 

206 else: 

207 # Not a function, just use the style value directly 

208 style = style_func_or_value 

209 

210 row_styles.append(style) 

211 

212 add_row_with_styles(table, row_data, row_styles) 

213 

214 

215def display_table( 

216 console: Console, 

217 data: List[Dict[str, Any]], 

218 columns: List[str], 

219 headers: List[str] = None, 

220 title: str = None, 

221 style_map: Dict[str, Any] = None, 

222 title_style: str = TABLE_TITLE_STYLE, 

223 header_style: str = HEADER_STYLE, 

224 border_style: str = TABLE_BORDER_STYLE, 

225 show_header: bool = True, 

226 show_lines: bool = True, 

227 box_style: str = "ROUNDED", 

228 padding: Union[int, tuple] = (0, 1), 

229 expand: bool = False, 

230) -> None: 

231 """ 

232 Create, populate and display a table in one operation. 

233 

234 This is the main function to use for displaying tabular data in the TUI. 

235 It handles various formatting options including styling, truncation, 

236 and customizable table appearance. 

237 

238 Args: 

239 console: Rich Console to display the table on 

240 data: List of dictionaries containing row data 

241 columns: List of column keys to extract from each dictionary 

242 headers: Column headers (defaults to columns if not provided) 

243 title: Optional table title 

244 style_map: Optional dictionary mapping column names to styles or style functions 

245 Style functions should accept a value and return a style string 

246 title_style: Style for the table title 

247 header_style: Style for the column headers 

248 border_style: Style for table borders 

249 show_header: Whether to show the header row 

250 show_lines: Whether to show lines between rows 

251 box_style: Box style for the table (e.g., "ROUNDED") 

252 padding: Cell padding as int or tuple 

253 expand: Whether the table should expand to fill available width. Defaults 

254 to False so the table width matches its content. 

255 """ 

256 try: 

257 # Use columns as headers if not provided 

258 if headers is None: 

259 headers = columns 

260 

261 # Create the table 

262 table = create_table( 

263 title=title, 

264 headers=headers, 

265 title_style=title_style, 

266 header_style=header_style, 

267 border_style=border_style, 

268 show_header=show_header, 

269 show_lines=show_lines, 

270 box_style=box_style, 

271 padding=padding, 

272 expand=expand, 

273 ) 

274 

275 # Handle empty data case 

276 if not data: 

277 console.print(table) 

278 return 

279 

280 # Add rows 

281 add_rows_from_data(table, data, columns, style_map) 

282 

283 # Display the table 

284 console.print(table) 

285 

286 except Exception as e: 

287 # Log the error and fall back to a simpler display method 

288 import logging 

289 

290 logging.error(f"Error displaying table: {e}") 

291 

292 # Print a simple error message and the raw data as fallback 

293 console.print(f"[red]Error displaying formatted table: {e}[/red]") 

294 console.print(f"Raw data: {data[:5]}" + ("..." if len(data) > 5 else ""))