status_display.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. from .icon_manager import IconManager
  10. if TYPE_CHECKING:
  11. from ..exceptions import TemplateRenderError
  12. from . import DisplayManager
  13. logger = logging.getLogger(__name__)
  14. console_err = Console(stderr=True) # Keep for error output
  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. # Errors and warnings always go to stderr, even in quiet mode
  36. # Success and info respect quiet mode and go to stdout
  37. use_stderr = level in ("error", "warning")
  38. should_print = use_stderr or not self.parent.quiet
  39. if not should_print:
  40. return
  41. settings = self.parent.settings
  42. icon = IconManager.get_status_icon(level)
  43. colors = {
  44. "error": settings.COLOR_ERROR,
  45. "warning": settings.COLOR_WARNING,
  46. "success": settings.COLOR_SUCCESS,
  47. "info": settings.COLOR_INFO,
  48. }
  49. color = colors.get(level, "white")
  50. # Format message based on context
  51. if context:
  52. text = (
  53. f"{level.capitalize()} in {context}: {message}"
  54. if level in {"error", "warning"}
  55. else f"{context}: {message}"
  56. )
  57. else:
  58. text = (
  59. f"{level.capitalize()}: {message}"
  60. if level in {"error", "warning"}
  61. else message
  62. )
  63. formatted_text = f"[{color}]{icon} {text}[/{color}]"
  64. if use_stderr:
  65. console_err.print(formatted_text)
  66. else:
  67. self.parent.text(formatted_text)
  68. # Log appropriately
  69. log_message = f"{context}: {message}" if context else message
  70. log_methods = {
  71. "error": logger.error,
  72. "warning": logger.warning,
  73. "success": logger.info,
  74. "info": logger.info,
  75. }
  76. log_methods.get(level, logger.info)(log_message)
  77. def display_error(self, message: str, context: str | None = None) -> None:
  78. """Display an error message.
  79. Args:
  80. message: Error message
  81. context: Optional context
  82. """
  83. self.display_message("error", message, context)
  84. def display_warning(self, message: str, context: str | None = None) -> None:
  85. """Display a warning message.
  86. Args:
  87. message: Warning message
  88. context: Optional context
  89. """
  90. self.display_message("warning", message, context)
  91. def display_success(self, message: str, context: str | None = None) -> None:
  92. """Display a success message.
  93. Args:
  94. message: Success message
  95. context: Optional context
  96. """
  97. self.display_message("success", message, context)
  98. def display_info(self, message: str, context: str | None = None) -> None:
  99. """Display an informational message.
  100. Args:
  101. message: Info message
  102. context: Optional context
  103. """
  104. self.display_message("info", message, context)
  105. def display_validation_error(self, message: str) -> None:
  106. """Display a validation error message.
  107. Args:
  108. message: Validation error message
  109. """
  110. self.display_message("error", message)
  111. def display_version_incompatibility(
  112. self, template_id: str, required_version: str, current_version: str
  113. ) -> None:
  114. """Display a version incompatibility error with upgrade instructions.
  115. Args:
  116. template_id: ID of the incompatible template
  117. required_version: Minimum CLI version required by template
  118. current_version: Current CLI version
  119. """
  120. console_err.print()
  121. console_err.print(
  122. f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]"
  123. )
  124. console_err.print()
  125. console_err.print(
  126. f"Template '[cyan]{template_id}[/cyan]' requires CLI version [green]{required_version}[/green] or higher."
  127. )
  128. console_err.print(f"Current CLI version: [yellow]{current_version}[/yellow]")
  129. console_err.print()
  130. console_err.print("[bold]Upgrade Instructions:[/bold]")
  131. console_err.print(
  132. f" {IconManager.UI_ARROW_RIGHT} Run: [cyan]pip install --upgrade boilerplates[/cyan]"
  133. )
  134. console_err.print(
  135. f" {IconManager.UI_ARROW_RIGHT} Or install specific version: [cyan]pip install boilerplates=={required_version}[/cyan]"
  136. )
  137. console_err.print()
  138. logger.error(
  139. f"Template '{template_id}' requires CLI version {required_version}, "
  140. f"current version is {current_version}"
  141. )
  142. def display_skipped(self, message: str, reason: str | None = None) -> None:
  143. """Display a skipped/disabled message.
  144. Args:
  145. message: The main message to display
  146. reason: Optional reason why it was skipped
  147. """
  148. icon = IconManager.get_status_icon("skipped")
  149. if reason:
  150. self.parent.text(f"\n{icon} {message} (skipped - {reason})", style="dim")
  151. else:
  152. self.parent.text(f"\n{icon} {message} (skipped)", style="dim")
  153. def display_warning_with_confirmation(
  154. self, message: str, details: list[str] | None = None, default: bool = False
  155. ) -> bool:
  156. """Display a warning message with optional details and get confirmation.
  157. Args:
  158. message: Warning message to display
  159. details: Optional list of detail lines to show
  160. default: Default value for confirmation
  161. Returns:
  162. True if user confirms, False otherwise
  163. """
  164. icon = IconManager.get_status_icon("warning")
  165. self.parent.text(f"\n{icon} {message}", style="yellow")
  166. if details:
  167. for detail in details:
  168. self.parent.text(f" {detail}", style="yellow")
  169. return Confirm.ask("Continue?", default=default)
  170. def _display_error_header(self, icon: str, context: str | None) -> None:
  171. """Display error header with optional context."""
  172. if context:
  173. console_err.print(
  174. f"\n[red bold]{icon} Template Rendering Error[/red bold] [dim]({context})[/dim]"
  175. )
  176. else:
  177. console_err.print(f"\n[red bold]{icon} Template Rendering Error[/red bold]")
  178. console_err.print()
  179. def _display_error_location(self, error: TemplateRenderError) -> None:
  180. """Display error file path and location."""
  181. if not error.file_path:
  182. return
  183. console_err.print(f"[red]Error in file:[/red] [cyan]{error.file_path}[/cyan]")
  184. if error.line_number:
  185. location = f"Line {error.line_number}"
  186. if error.column:
  187. location += f", Column {error.column}"
  188. console_err.print(f"[red]Location:[/red] {location}")
  189. def _display_code_context(self, error: TemplateRenderError) -> None:
  190. """Display code context with syntax highlighting."""
  191. if not error.context_lines:
  192. return
  193. console_err.print("[bold cyan]Code Context:[/bold cyan]")
  194. context_text = "\n".join(error.context_lines)
  195. # Determine lexer for syntax highlighting
  196. lexer = self._get_lexer_for_file(error.file_path)
  197. # Try to display with syntax highlighting, fallback to plain on error
  198. try:
  199. self._display_syntax_panel(context_text, lexer)
  200. except Exception:
  201. console_err.print(Panel(context_text, border_style="red", padding=(1, 2)))
  202. console_err.print()
  203. def _get_lexer_for_file(self, file_path: str | None) -> str | None:
  204. """Determine lexer based on file extension."""
  205. if not file_path:
  206. return None
  207. file_ext = Path(file_path).suffix
  208. if file_ext == ".j2":
  209. base_name = Path(file_path).stem
  210. base_ext = Path(base_name).suffix
  211. return "jinja2" if not base_ext else None
  212. return None
  213. def _display_syntax_panel(self, text: str, lexer: str | None) -> None:
  214. """Display text in a panel with optional syntax highlighting."""
  215. if lexer:
  216. syntax = Syntax(text, lexer, line_numbers=False, theme="monokai")
  217. console_err.print(Panel(syntax, border_style="red", padding=(1, 2)))
  218. else:
  219. console_err.print(Panel(text, border_style="red", padding=(1, 2)))
  220. def display_template_render_error(
  221. self, error: TemplateRenderError, context: str | None = None
  222. ) -> None:
  223. """Display a detailed template rendering error with context and suggestions.
  224. Args:
  225. error: TemplateRenderError exception with detailed error information
  226. context: Optional context information (e.g., template ID)
  227. """
  228. # Display error header
  229. icon = IconManager.get_status_icon("error")
  230. self._display_error_header(icon, context)
  231. # Display error location
  232. self._display_error_location(error)
  233. # Display error message
  234. console_err.print(
  235. f"[red]Message:[/red] {str(error.original_error) if error.original_error else str(error)}"
  236. )
  237. console_err.print()
  238. # Display code context
  239. self._display_code_context(error)
  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()