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
« prev ^ index » next coverage.py v7.8.0, created at 2025-06-05 23:16 -0700
1"""
2Table formatting utilities for TUI display.
4This module provides standardized table formatting functions using Rich library
5to ensure consistent display of tabular data throughout the application.
6"""
8from typing import Any, Dict, List, Union
10from rich.console import Console
11from rich.table import Table
12from rich.text import Text
13from rich import box
15from src.ui.theme import TABLE_TITLE_STYLE, TABLE_BORDER_STYLE, HEADER_STYLE
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.
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.
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)
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 )
65 # Add headers if provided
66 if headers and show_header:
67 for header in headers:
68 table.add_column(header, style=header_style)
70 return table
71 except Exception as e:
72 # Log error and fall back to default styling
73 import logging
75 logging.error(f"Error creating table: {e}")
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 )
89 # Add headers if provided
90 if headers and show_header:
91 for header in headers:
92 table.add_column(header, style=header_style)
94 return table
97def format_cell(value: Any, style: Any = None, none_display: str = "N/A") -> Text:
98 """
99 Format a cell value with consistent styling.
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
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")
113 # Convert to string if not already
114 if not isinstance(value, str):
115 value = str(value)
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
125 logging.error(f"Error applying style function: {e}")
126 applied_style = None
128 return Text(value, style=applied_style)
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.
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)
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 ]
153 table.add_row(*formatted_cells)
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.
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 = {}
175 for item in data:
176 row_data = []
177 row_styles = []
179 for col in columns:
180 # Get the value for this column
181 value = item.get(col)
182 row_data.append(value)
184 # Get the style for this column
185 style_func_or_value = style_map.get(col)
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
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
210 row_styles.append(style)
212 add_row_with_styles(table, row_data, row_styles)
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.
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.
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
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 )
275 # Handle empty data case
276 if not data:
277 console.print(table)
278 return
280 # Add rows
281 add_rows_from_data(table, data, columns, style_map)
283 # Display the table
284 console.print(table)
286 except Exception as e:
287 # Log the error and fall back to a simpler display method
288 import logging
290 logging.error(f"Error displaying table: {e}")
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 ""))