status_display.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. from __future__ import annotations
  2. import logging
  3. from pathlib import Path
  4. from typing import TYPE_CHECKING
  5. from rich.console import Console
  6. from rich.panel import Panel
  7. from rich.prompt import Confirm
  8. from rich.syntax import Syntax
  9. if TYPE_CHECKING:
  10. from . import DisplayManager
  11. from ..exceptions import TemplateRenderError
  12. logger = logging.getLogger(__name__)
  13. console = Console()
  14. console_err = Console(stderr=True)
  15. class StatusDisplayManager:
  16. """Handles status messages and error display.
  17. This manager is responsible for displaying success, error, warning,
  18. and informational messages with consistent formatting.
  19. """
  20. def __init__(self, parent: "DisplayManager"):
  21. """Initialize StatusDisplayManager.
  22. Args:
  23. parent: Reference to parent DisplayManager for accessing shared resources
  24. """
  25. self.parent = parent
  26. def display_message(
  27. self, level: str, message: str, context: str | None = None
  28. ) -> None:
  29. """Display a message with consistent formatting.
  30. Args:
  31. level: Message level (error, warning, success, info)
  32. message: The message to display
  33. context: Optional context information
  34. """
  35. from . import IconManager
  36. # Errors and warnings always go to stderr, even in quiet mode
  37. # Success and info respect quiet mode and go to stdout
  38. if level in ("error", "warning"):
  39. output_console = console_err
  40. should_print = True
  41. else:
  42. output_console = console
  43. should_print = not self.parent.quiet
  44. if not should_print:
  45. return
  46. settings = self.parent.settings
  47. icon = IconManager.get_status_icon(level)
  48. colors = {
  49. "error": settings.COLOR_ERROR,
  50. "warning": settings.COLOR_WARNING,
  51. "success": settings.COLOR_SUCCESS,
  52. "info": settings.COLOR_INFO,
  53. }
  54. color = colors.get(level, "white")
  55. # Format message based on context
  56. if context:
  57. text = (
  58. f"{level.capitalize()} in {context}: {message}"
  59. if level == "error" or level == "warning"
  60. else f"{context}: {message}"
  61. )
  62. else:
  63. text = (
  64. f"{level.capitalize()}: {message}"
  65. if level == "error" or level == "warning"
  66. else message
  67. )
  68. output_console.print(f"[{color}]{icon} {text}[/{color}]")
  69. # Log appropriately
  70. log_message = f"{context}: {message}" if context else message
  71. log_methods = {
  72. "error": logger.error,
  73. "warning": logger.warning,
  74. "success": logger.info,
  75. "info": logger.info,
  76. }
  77. log_methods.get(level, logger.info)(log_message)
  78. def display_error(self, message: str, context: str | None = None) -> None:
  79. """Display an error message.
  80. Args:
  81. message: Error message
  82. context: Optional context
  83. """
  84. self.display_message("error", message, context)
  85. def display_warning(self, message: str, context: str | None = None) -> None:
  86. """Display a warning message.
  87. Args:
  88. message: Warning message
  89. context: Optional context
  90. """
  91. self.display_message("warning", message, context)
  92. def display_success(self, message: str, context: str | None = None) -> None:
  93. """Display a success message.
  94. Args:
  95. message: Success message
  96. context: Optional context
  97. """
  98. self.display_message("success", message, context)
  99. def display_info(self, message: str, context: str | None = None) -> None:
  100. """Display an informational message.
  101. Args:
  102. message: Info message
  103. context: Optional context
  104. """
  105. self.display_message("info", message, context)
  106. def display_validation_error(self, message: str) -> None:
  107. """Display a validation error message.
  108. Args:
  109. message: Validation error message
  110. """
  111. self.display_message("error", message)
  112. def display_version_incompatibility(
  113. self, template_id: str, required_version: str, current_version: str
  114. ) -> None:
  115. """Display a version incompatibility error with upgrade instructions.
  116. Args:
  117. template_id: ID of the incompatible template
  118. required_version: Minimum CLI version required by template
  119. current_version: Current CLI version
  120. """
  121. from . import IconManager
  122. console_err.print()
  123. console_err.print(
  124. f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]"
  125. )
  126. console_err.print()
  127. console_err.print(
  128. f"Template '[cyan]{template_id}[/cyan]' requires CLI version [green]{required_version}[/green] or higher."
  129. )
  130. console_err.print(f"Current CLI version: [yellow]{current_version}[/yellow]")
  131. console_err.print()
  132. console_err.print("[bold]Upgrade Instructions:[/bold]")
  133. console_err.print(
  134. f" {IconManager.UI_ARROW_RIGHT} Run: [cyan]pip install --upgrade boilerplates[/cyan]"
  135. )
  136. console_err.print(
  137. f" {IconManager.UI_ARROW_RIGHT} Or install specific version: [cyan]pip install boilerplates=={required_version}[/cyan]"
  138. )
  139. console_err.print()
  140. logger.error(
  141. f"Template '{template_id}' requires CLI version {required_version}, "
  142. f"current version is {current_version}"
  143. )
  144. def display_skipped(self, message: str, reason: str | None = None) -> None:
  145. """Display a skipped/disabled message.
  146. Args:
  147. message: The main message to display
  148. reason: Optional reason why it was skipped
  149. """
  150. from . import IconManager
  151. icon = IconManager.get_status_icon("skipped")
  152. if reason:
  153. console.print(f"\n[dim]{icon} {message} (skipped - {reason})[/dim]")
  154. else:
  155. console.print(f"\n[dim]{icon} {message} (skipped)[/dim]")
  156. def display_warning_with_confirmation(
  157. self, message: str, details: list[str] | None = None, default: bool = False
  158. ) -> bool:
  159. """Display a warning message with optional details and get confirmation.
  160. Args:
  161. message: Warning message to display
  162. details: Optional list of detail lines to show
  163. default: Default value for confirmation
  164. Returns:
  165. True if user confirms, False otherwise
  166. """
  167. from . import IconManager
  168. icon = IconManager.get_status_icon("warning")
  169. console.print(f"\n[yellow]{icon} {message}[/yellow]")
  170. if details:
  171. for detail in details:
  172. console.print(f"[yellow] {detail}[/yellow]")
  173. return Confirm.ask("Continue?", default=default)
  174. def display_template_render_error(
  175. self, error: "TemplateRenderError", context: str | None = None
  176. ) -> None:
  177. """Display a detailed template rendering error with context and suggestions.
  178. Args:
  179. error: TemplateRenderError exception with detailed error information
  180. context: Optional context information (e.g., template ID)
  181. """
  182. from . import IconManager
  183. # Always display errors to stderr
  184. icon = IconManager.get_status_icon("error")
  185. if context:
  186. console_err.print(
  187. f"\n[red bold]{icon} Template Rendering Error[/red bold] [dim]({context})[/dim]"
  188. )
  189. else:
  190. console_err.print(
  191. f"\n[red bold]{icon} Template Rendering Error[/red bold]"
  192. )
  193. console_err.print()
  194. # Display error message
  195. if error.file_path:
  196. console_err.print(
  197. f"[red]Error in file:[/red] [cyan]{error.file_path}[/cyan]"
  198. )
  199. if error.line_number:
  200. location = f"Line {error.line_number}"
  201. if error.column:
  202. location += f", Column {error.column}"
  203. console_err.print(f"[red]Location:[/red] {location}")
  204. console_err.print(
  205. f"[red]Message:[/red] {str(error.original_error) if error.original_error else str(error)}"
  206. )
  207. console_err.print()
  208. # Display code context if available
  209. if error.context_lines:
  210. console_err.print("[bold cyan]Code Context:[/bold cyan]")
  211. # Build the context text
  212. context_text = "\n".join(error.context_lines)
  213. # Display in a panel with syntax highlighting if possible
  214. file_ext = Path(error.file_path).suffix if error.file_path else ""
  215. if file_ext == ".j2":
  216. # Remove .j2 to get base extension for syntax highlighting
  217. base_name = Path(error.file_path).stem
  218. base_ext = Path(base_name).suffix
  219. lexer = "jinja2" if not base_ext else None
  220. else:
  221. lexer = None
  222. try:
  223. if lexer:
  224. syntax = Syntax(
  225. context_text, lexer, line_numbers=False, theme="monokai"
  226. )
  227. console_err.print(
  228. Panel(syntax, border_style="red", padding=(1, 2))
  229. )
  230. else:
  231. console_err.print(
  232. Panel(context_text, border_style="red", padding=(1, 2))
  233. )
  234. except Exception:
  235. # Fallback to plain panel if syntax highlighting fails
  236. console_err.print(
  237. Panel(context_text, border_style="red", padding=(1, 2))
  238. )
  239. console_err.print()
  240. # Display suggestions if available
  241. if error.suggestions:
  242. console_err.print("[bold yellow]Suggestions:[/bold yellow]")
  243. for i, suggestion in enumerate(error.suggestions, 1):
  244. bullet = IconManager.UI_BULLET
  245. console_err.print(f" [yellow]{bullet}[/yellow] {suggestion}")
  246. console_err.print()
  247. # Display variable context in debug mode
  248. if error.variable_context:
  249. console_err.print("[bold blue]Available Variables (Debug):[/bold blue]")
  250. var_list = ", ".join(sorted(error.variable_context.keys()))
  251. console_err.print(f"[dim]{var_list}[/dim]")
  252. console_err.print()