exceptions.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. """Custom exception classes for the boilerplates CLI.
  2. This module defines specific exception types for better error handling
  3. and diagnostics throughout the application.
  4. """
  5. from __future__ import annotations
  6. from dataclasses import dataclass, field
  7. class BoilerplatesError(Exception):
  8. """Base exception for all boilerplates CLI errors."""
  9. pass
  10. class ConfigError(BoilerplatesError):
  11. """Raised when configuration operations fail."""
  12. pass
  13. class ConfigValidationError(ConfigError):
  14. """Raised when configuration validation fails."""
  15. pass
  16. class TemplateError(BoilerplatesError):
  17. """Base exception for template-related errors."""
  18. pass
  19. class TemplateNotFoundError(TemplateError):
  20. """Raised when a template cannot be found."""
  21. def __init__(self, template_id: str, module_name: str | None = None):
  22. self.template_id = template_id
  23. self.module_name = module_name
  24. msg = f"Template '{template_id}' not found"
  25. if module_name:
  26. msg += f" in module '{module_name}'"
  27. super().__init__(msg)
  28. class TemplateDraftError(TemplateError):
  29. """Raised when attempting to use a draft template."""
  30. def __init__(self, template_id: str, module_name: str | None = None):
  31. self.template_id = template_id
  32. self.module_name = module_name
  33. module_suffix = f" in module '{module_name}'" if module_name else ""
  34. msg = (
  35. f"Template '{template_id}' is in draft mode and not yet available for use{module_suffix}.\n"
  36. "Draft templates are work-in-progress and cannot be generated yet.\n"
  37. "To get updates when published, run 'boilerplates repo update' to sync your library."
  38. )
  39. super().__init__(msg)
  40. class DuplicateTemplateError(TemplateError):
  41. """Raised when duplicate template IDs are found within the same library."""
  42. def __init__(self, template_id: str, library_name: str):
  43. self.template_id = template_id
  44. self.library_name = library_name
  45. super().__init__(
  46. f"Duplicate template ID '{template_id}' found in library '{library_name}'. "
  47. f"Each template within a library must have a unique ID."
  48. )
  49. class TemplateLoadError(TemplateError):
  50. """Raised when a template fails to load."""
  51. pass
  52. class TemplateSyntaxError(TemplateError):
  53. """Raised when a Jinja2 template has syntax errors."""
  54. def __init__(self, template_id: str, errors: list[str]):
  55. self.template_id = template_id
  56. self.errors = errors
  57. msg = f"Jinja2 syntax errors in template '{template_id}':\n" + "\n".join(errors)
  58. super().__init__(msg)
  59. class TemplateValidationError(TemplateError):
  60. """Raised when template validation fails."""
  61. pass
  62. class IncompatibleSchemaVersionError(TemplateError):
  63. """Raised when a template uses a schema version not supported by the module."""
  64. def __init__(
  65. self,
  66. template_id: str,
  67. template_schema: str,
  68. module_schema: str,
  69. module_name: str,
  70. ):
  71. self.template_id = template_id
  72. self.template_schema = template_schema
  73. self.module_schema = module_schema
  74. self.module_name = module_name
  75. msg = (
  76. f"Template '{template_id}' uses schema version {template_schema}, "
  77. f"but module '{module_name}' only supports up to version {module_schema}.\n\n"
  78. f"This template requires features not available in your current CLI version.\n"
  79. f"Please upgrade the boilerplates CLI.\n\n"
  80. f"Run: pip install --upgrade boilerplates"
  81. )
  82. super().__init__(msg)
  83. @dataclass
  84. class RenderErrorContext:
  85. """Context information for template rendering errors."""
  86. file_path: str | None = None
  87. line_number: int | None = None
  88. column: int | None = None
  89. context_lines: list[str] = field(default_factory=list)
  90. variable_context: dict[str, str] = field(default_factory=dict)
  91. suggestions: list[str] = field(default_factory=list)
  92. original_error: Exception | None = None
  93. class TemplateRenderError(TemplateError):
  94. """Raised when template rendering fails."""
  95. def __init__(self, message: str, context: RenderErrorContext | None = None):
  96. self.context = context or RenderErrorContext()
  97. # Expose context fields as instance attributes for backward compatibility
  98. self.file_path = self.context.file_path
  99. self.line_number = self.context.line_number
  100. self.column = self.context.column
  101. self.context_lines = self.context.context_lines
  102. self.variable_context = self.context.variable_context
  103. self.suggestions = self.context.suggestions
  104. self.original_error = self.context.original_error
  105. # Build enhanced error message
  106. parts = [message]
  107. if self.context.file_path:
  108. location = f"File: {self.context.file_path}"
  109. if self.context.line_number:
  110. location += f", Line: {self.context.line_number}"
  111. if self.context.column:
  112. location += f", Column: {self.context.column}"
  113. parts.append(location)
  114. super().__init__("\n".join(parts))
  115. class VariableError(BoilerplatesError):
  116. """Base exception for variable-related errors."""
  117. pass
  118. class VariableValidationError(VariableError):
  119. """Raised when variable validation fails."""
  120. def __init__(self, variable_name: str, message: str):
  121. self.variable_name = variable_name
  122. msg = f"Validation error for variable '{variable_name}': {message}"
  123. super().__init__(msg)
  124. class VariableTypeError(VariableError):
  125. """Raised when a variable has an incorrect type."""
  126. def __init__(self, variable_name: str, expected_type: str, actual_type: str):
  127. self.variable_name = variable_name
  128. self.expected_type = expected_type
  129. self.actual_type = actual_type
  130. msg = f"Type error for variable '{variable_name}': expected {expected_type}, got {actual_type}"
  131. super().__init__(msg)
  132. class LibraryError(BoilerplatesError):
  133. """Raised when library operations fail."""
  134. pass
  135. class ModuleError(BoilerplatesError):
  136. """Raised when module operations fail."""
  137. pass
  138. class ModuleNotFoundError(ModuleError):
  139. """Raised when a module cannot be found."""
  140. def __init__(self, module_name: str):
  141. self.module_name = module_name
  142. msg = f"Module '{module_name}' not found"
  143. super().__init__(msg)
  144. class ModuleLoadError(ModuleError):
  145. """Raised when a module fails to load."""
  146. pass
  147. class SchemaError(BoilerplatesError):
  148. """Raised when schema operations fail."""
  149. def __init__(self, message: str, details: str | None = None):
  150. self.details = details
  151. msg = message
  152. if details:
  153. msg += f" ({details})"
  154. super().__init__(msg)
  155. class FileOperationError(BoilerplatesError):
  156. """Raised when file operations fail."""
  157. pass
  158. class RenderError(BoilerplatesError):
  159. """Raised when rendering operations fail."""
  160. pass
  161. class YAMLParseError(BoilerplatesError):
  162. """Raised when YAML parsing fails."""
  163. def __init__(self, file_path: str, original_error: Exception):
  164. self.file_path = file_path
  165. self.original_error = original_error
  166. msg = f"Failed to parse YAML file '{file_path}': {original_error}"
  167. super().__init__(msg)