Coverage for src / ezplog / handlers / console.py: 74.72%
162 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:27 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:27 +0000
1# ///////////////////////////////////////////////////////////////
2# EZPL - Console Printer Handler
3# Project: ezpl
4# ///////////////////////////////////////////////////////////////
6"""
7Console printer handler for Ezpl logging framework.
9This module provides a console-based logging handler with advanced formatting,
10indentation management, and color support using Rich.
11"""
13from __future__ import annotations
15# ///////////////////////////////////////////////////////////////
16# IMPORTS
17# ///////////////////////////////////////////////////////////////
18# Standard library imports
19from collections.abc import Generator
20from contextlib import contextmanager
21from typing import Any
23# Third-party imports
24from rich.console import Console
25from rich.progress import Progress, SpinnerColumn, TextColumn
26from rich.text import Text
28# Local imports
29from ..core.exceptions import ValidationError
30from ..core.interfaces import IndentationManager, LoggingHandler
31from ..types.enums import LogLevel, Pattern, get_pattern_color
32from ..utils import safe_str_convert, sanitize_for_console
33from .wizard import RichWizard
35# ///////////////////////////////////////////////////////////////
36# CLASSES
37# ///////////////////////////////////////////////////////////////
40class EzPrinter(LoggingHandler, IndentationManager):
41 """
42 Console printer handler with advanced formatting and indentation support using Rich.
44 This handler provides console-based logging with:
45 - Color-coded log levels using Rich
46 - Indentation management
47 - Robust character handling (Rich handles special characters automatically)
48 - Context manager support
49 - Pattern-based logging (SUCCESS, ERROR, WARN, TIP, etc.)
50 - Access to RichWizard for advanced display features
51 """
53 _MAX_INDENT = 10 # Maximum indentation level
55 # ///////////////////////////////////////////////////////////////
56 # INIT
57 # ///////////////////////////////////////////////////////////////
59 def __init__(
60 self,
61 level: str = "INFO",
62 indent_step: int = 3,
63 indent_symbol: str = ">",
64 base_indent_symbol: str = "~",
65 ) -> None:
66 """
67 Initialize the console printer handler.
69 Args:
70 level: The desired logging level
71 indent_step: Number of spaces for each indentation level
72 indent_symbol: Symbol for indentation levels
73 base_indent_symbol: Symbol for the base indentation
75 Raises:
76 ValidationError: If the provided level is invalid
77 """
78 if not LogLevel.is_valid_level(level): 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 raise ValidationError(f"Invalid log level: {level}", "level", level)
81 self._level = level.upper()
82 self._level_manually_set = False
83 self._indent = 0
84 self._indent_step = indent_step
85 self._indent_symbol = indent_symbol
86 self._base_indent_symbol = base_indent_symbol
88 # Initialize Rich Console
89 self._console = Console()
90 self._level_numeric = LogLevel.get_no(self._level)
92 # Initialize Rich Wizard for advanced display features
93 self._wizard = RichWizard(self._console)
95 # ///////////////////////////////////////////////////////////////
96 # UTILS METHODS
97 # ///////////////////////////////////////////////////////////////
99 @property
100 def level(self) -> str:
101 """Return the current logging level."""
102 return self._level
104 @property
105 def level_manually_set(self) -> bool:
106 """Return whether level was set manually at runtime."""
107 return self._level_manually_set
109 @property
110 def indent_step(self) -> int:
111 """Return the configured indentation step."""
112 return self._indent_step
114 @property
115 def indent_symbol(self) -> str:
116 """Return the configured indentation symbol."""
117 return self._indent_symbol
119 @property
120 def base_indent_symbol(self) -> str:
121 """Return the configured base indentation symbol."""
122 return self._base_indent_symbol
124 def mark_level_as_configured(self) -> None:
125 """Mark the current level as coming from configuration (not manual set)."""
126 self._level_manually_set = False
128 def set_level(self, level: str) -> None:
129 """
130 Set the logging level.
132 Args:
133 level: The desired logging level
135 Raises:
136 ValidationError: If the provided level is invalid
137 """
138 if not LogLevel.is_valid_level(level):
139 raise ValidationError(f"Invalid log level: {level}", "level", level)
141 self._level = level.upper()
142 self._level_numeric = LogLevel.get_no(self._level)
143 self._level_manually_set = True
145 def log(self, level: str, message: Any) -> None:
146 """
147 Log a message with the specified level.
149 Args:
150 level: The log level
151 message: The message to log (any type, will be safely converted to string)
153 Raises:
154 ValidationError: If the level is invalid
155 """
156 if not LogLevel.is_valid_level(level):
157 raise ValidationError(f"Invalid log level: {level}", "level", level)
159 # Convert message to string robustly
160 message = safe_str_convert(message)
161 message = sanitize_for_console(message)
163 try:
164 level_numeric = LogLevel.get_no(level)
165 if level_numeric < self._level_numeric:
166 return # Level too low, skip
168 # Map log levels to patterns for consistent output
169 pattern_map = {
170 "DEBUG": Pattern.DEBUG,
171 "INFO": Pattern.INFO,
172 "SUCCESS": Pattern.SUCCESS,
173 "WARNING": Pattern.WARN,
174 "ERROR": Pattern.ERROR,
175 "CRITICAL": Pattern.ERROR, # Critical also uses ERROR pattern
176 }
177 pattern_enum = pattern_map.get(level.upper(), Pattern.INFO)
178 self.print_pattern(pattern_enum, message, level)
180 except Exception as e:
181 # Never raise an exception, just log the error safely
182 try:
183 self._console.print(
184 f"[bold red]LOGGING ERROR:[/bold red] {type(e).__name__}"
185 )
186 except Exception as e:
187 raise ValueError(f"Failed to print logging error: {e}") from e
189 # ///////////////////////////////////////////////////////////////
190 # LOGGING METHODS (API primaire)
191 # ///////////////////////////////////////////////////////////////
193 def info(self, message: Any) -> None:
194 """Log an informational message with pattern format."""
195 self.print_pattern(Pattern.INFO, message, "INFO")
197 def debug(self, message: Any) -> None:
198 """Log a debug message with pattern format."""
199 self.print_pattern(Pattern.DEBUG, message, "DEBUG")
201 def success(self, message: Any) -> None:
202 """Log a success message with pattern format."""
203 self.print_pattern(Pattern.SUCCESS, message, "INFO")
205 def warning(self, message: Any) -> None:
206 """Log a warning message with pattern format."""
207 self.print_pattern(Pattern.WARN, message, "WARNING")
209 def error(self, message: Any) -> None:
210 """Log an error message with pattern format."""
211 self.print_pattern(Pattern.ERROR, message, "ERROR")
213 def critical(self, message: Any) -> None:
214 """Log a critical message with pattern format."""
215 self.print_pattern(Pattern.ERROR, message, "CRITICAL")
217 # ------------------------------------------------
218 # ADDITIONAL PATTERN METHODS
219 # ------------------------------------------------
221 def tip(self, message: Any) -> None:
222 """Display a tip message with pattern format."""
223 self.print_pattern(Pattern.TIP, message, "INFO")
225 def system(self, message: Any) -> None:
226 """Display a system message with pattern format."""
227 self.print_pattern(Pattern.SYSTEM, message, "INFO")
229 def install(self, message: Any) -> None:
230 """Display an installation message with pattern format."""
231 self.print_pattern(Pattern.INSTALL, message, "INFO")
233 def detect(self, message: Any) -> None:
234 """Display a detection message with pattern format."""
235 self.print_pattern(Pattern.DETECT, message, "INFO")
237 def config(self, message: Any) -> None:
238 """Display a configuration message with pattern format."""
239 self.print_pattern(Pattern.CONFIG, message, "INFO")
241 def deps(self, message: Any) -> None:
242 """Display a dependencies message with pattern format."""
243 self.print_pattern(Pattern.DEPS, message, "INFO")
245 def print_pattern(
246 self, pattern: str | Pattern, message: Any, level: str = "INFO"
247 ) -> None:
248 """
249 Display a message with pattern format: • PATTERN :: message
251 Args:
252 pattern: Pattern name (string) or Pattern enum
253 message: Message to display
254 level: Log level for filtering (default: INFO)
255 """
256 try:
257 # Convert pattern to Pattern enum if string
258 if isinstance(pattern, str):
259 try:
260 pattern_enum = Pattern[pattern.upper()]
261 except KeyError:
262 # If pattern not found, use INFO as default
263 pattern_enum = Pattern.INFO
264 else:
265 pattern_enum = pattern
267 # Check if level should be displayed
268 level_numeric = LogLevel.get_no(level)
269 if level_numeric < self._level_numeric:
270 return # Level too low, don't display
272 # Convert message to string safely and sanitize for console
273 message = safe_str_convert(message)
274 message = sanitize_for_console(message)
276 # Get pattern color
277 pattern_color = get_pattern_color(pattern_enum)
278 pattern_name = pattern_enum.value
280 # Build text with pattern format: • PATTERN :: message
281 text = Text()
282 text.append("• ", style=pattern_color)
283 text.append(pattern_name.ljust(8), style=f"bold {pattern_color}")
284 text.append(":: ", style="dim white")
286 # Handle indentation - add it just before the message (after ":: ")
287 indent_str = self.get_indent()
288 if indent_str and indent_str != "~":
289 # Add indentation just before the message
290 text.append(indent_str, style="dim")
291 text.append(" ", style="dim")
293 # Add the message
294 text.append(str(message), style="white")
296 self._console.print(text)
298 except Exception as e:
299 # Robust error handling: never raise exception
300 try:
301 error_msg = f"[bold red]PATTERN ERROR:[/bold red] {type(e).__name__}"
302 self._console.print(error_msg)
303 except Exception as e:
304 raise ValueError(f"Failed to print pattern: {e}") from e
306 # ///////////////////////////////////////////////////////////////
307 # INDENTATION MANAGEMENT
308 # ///////////////////////////////////////////////////////////////
310 def get_indent(self) -> str:
311 """
312 Get the current indentation string.
314 Returns:
315 The current indentation string
316 """
317 try:
318 indent_spaces = " " * (self._indent * self._indent_step)
319 if self._indent > 0:
320 return f"{indent_spaces}{self._indent_symbol}"
321 else:
322 return self._base_indent_symbol
323 except Exception:
324 return "~" # Safe fallback
326 def add_indent(self) -> None:
327 """Increase the indentation level by one (with maximum limit)."""
328 self._indent = min(self._indent + 1, self._MAX_INDENT)
330 def del_indent(self) -> None:
331 """Decrease the indentation level by one, ensuring it doesn't go below zero."""
332 self._indent = max(0, self._indent - 1)
334 def reset_indent(self) -> None:
335 """Reset the indentation level to zero."""
336 self._indent = 0
338 @contextmanager
339 def manage_indent(self) -> Generator[None, None, None]:
340 """
341 Context manager for temporary indentation.
343 Yields:
344 None
345 """
346 try:
347 self.add_indent()
348 yield
349 finally:
350 self.del_indent()
352 # ///////////////////////////////////////////////////////////////
353 # WIZARD ACCESS
354 # ///////////////////////////////////////////////////////////////
356 @property
357 def wizard(self) -> RichWizard:
358 """
359 Get the Rich Wizard instance for advanced display features.
361 Returns:
362 RichWizard instance for panels, tables, JSON, etc.
364 Example:
365 >>> printer.wizard.success_panel("Success", "Operation completed")
366 >>> printer.wizard.status_table("Status", data)
367 >>> printer.wizard.dependency_table({"tool": "1.0.0"})
368 """
369 return self._wizard
371 # ///////////////////////////////////////////////////////////////
372 # ENHANCED METHODS (Rich features)
373 # ///////////////////////////////////////////////////////////////
375 def print_table(self, data: list[dict[str, Any]], title: str | None = None) -> None:
376 """
377 Display a table using Rich (delegates to RichWizard).
379 Args:
380 data: List of dictionaries representing table rows
381 title: Optional table title
382 """
383 self._wizard.table(data, title=title)
385 def print_panel(
386 self, content: str, title: str | None = None, style: str = "blue"
387 ) -> None:
388 """
389 Display a panel using Rich (delegates to RichWizard).
391 Args:
392 content: Panel content
393 title: Optional panel title
394 style: Panel style (Rich style string, used as border_style)
395 """
396 self._wizard.panel(content, title=title, border_style=style)
398 def print_progress(self, *args, **kwargs) -> None:
399 """
400 Display a progress bar using Rich.
402 Note: This is a placeholder. For full progress functionality,
403 users should use Rich's Progress context manager directly.
404 """
405 try:
406 with Progress(
407 SpinnerColumn(),
408 TextColumn("[progress.description]{task.description}"),
409 *args,
410 **kwargs,
411 ):
412 # Placeholder - users should use Rich.Progress context manager directly
413 pass
414 except Exception as e:
415 try:
416 self._console.print(f"[red]Progress error:[/red] {type(e).__name__}")
417 except Exception as e:
418 raise ValueError(f"Failed to print progress: {e}") from e
420 def print_json(
421 self,
422 data: str | dict | list,
423 title: str | None = None,
424 indent: int | None = None,
425 highlight: bool = True,
426 ) -> None:
427 """
428 Display JSON data in a formatted and syntax-highlighted way using Rich (delegates to RichWizard).
430 Args:
431 data: JSON data to display (dict, list, or JSON string)
432 title: Optional title for the JSON display
433 indent: Number of spaces for indentation (default: 2)
434 highlight: Whether to enable syntax highlighting (default: True)
436 Examples:
437 >>> printer.print_json({"name": "Alice", "age": 30})
438 >>> printer.print_json('{"key": "value"}', title="Config")
439 >>> printer.print_json([1, 2, 3], indent=4)
440 """
441 self._wizard.json(data, title=title, indent=indent, highlight=highlight)
443 # ///////////////////////////////////////////////////////////////
444 # REPRESENTATION METHODS
445 # ///////////////////////////////////////////////////////////////
447 def __str__(self) -> str:
448 """String representation of the console printer."""
449 return f"EzPrinter(level={self._level}, indent={self._indent})"
451 def __repr__(self) -> str:
452 """Detailed string representation of the console printer."""
453 return f"EzPrinter(level={self._level}, indent={self._indent}, indent_step={self._indent_step})"
456# ///////////////////////////////////////////////////////////////
457# PUBLIC API
458# ///////////////////////////////////////////////////////////////
460__all__ = [
461 "EzPrinter",
462]