module.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. from abc import ABC
  2. from pathlib import Path
  3. from typing import List, Optional, Dict, Any
  4. import logging
  5. from typer import Typer, Option, Argument
  6. from rich.console import Console
  7. from .config import get_config
  8. from .exceptions import TemplateNotFoundError, TemplateValidationError
  9. from .library import LibraryManager
  10. from .prompt import SimplifiedPromptHandler
  11. from .variables import VariableRegistry
  12. logger = logging.getLogger('boilerplates')
  13. console = Console() # Single shared console instance
  14. class Module(ABC):
  15. """Streamlined base module with minimal redundancy."""
  16. # Required class attributes for subclasses
  17. name = None
  18. description = None
  19. files = None
  20. def __init__(self):
  21. if not all([self.name, self.description, self.files]):
  22. raise ValueError(
  23. f"Module {self.__class__.__name__} must define name, description, and files"
  24. )
  25. self.libraries = LibraryManager()
  26. self.variables = VariableRegistry()
  27. # Allow subclasses to initialize variables if they override this
  28. if hasattr(self, '_init_variables'):
  29. self._init_variables()
  30. def list(self):
  31. """List all templates."""
  32. templates = self.libraries.find(self.name, self.files, sorted=True)
  33. for template in templates:
  34. console.print(f"[cyan]{template.id}[/cyan] - {template.name}")
  35. return templates
  36. def show(self, id: str = Argument(..., help="Template ID")):
  37. """Show template details."""
  38. logger.debug(f"Showing template: {id}")
  39. template = self._get_template(id)
  40. if not template:
  41. return
  42. # Header
  43. version = f" v{template.version}" if template.version else ""
  44. console.print(f"[bold magenta]{template.name} ({template.id}{version})[/bold magenta]")
  45. console.print(f"[dim white]{template.description}[/dim white]\n")
  46. # Metadata (only print if exists)
  47. metadata = [
  48. ("Author", template.author),
  49. ("Date", template.date),
  50. ("Tags", ', '.join(template.tags) if template.tags else None)
  51. ]
  52. for label, value in metadata:
  53. if value:
  54. console.print(f"{label}: [cyan]{value}[/cyan]")
  55. # Variables
  56. if template.vars:
  57. console.print(f"Variables: [cyan]{', '.join(sorted(template.vars))}[/cyan]")
  58. # Content
  59. if template.content:
  60. console.print(f"\n{template.content}")
  61. def _get_template(self, template_id: str, raise_on_missing: bool = False):
  62. """Get template by ID with unified error handling."""
  63. template = self.libraries.find_by_id(self.name, self.files, template_id)
  64. if not template:
  65. logger.error(f"Template '{template_id}' not found")
  66. if raise_on_missing:
  67. raise TemplateNotFoundError(template_id, self.name)
  68. console.print(f"[red]Template '{template_id}' not found[/red]")
  69. return template
  70. def generate(
  71. self,
  72. id: str = Argument(..., help="Template ID"),
  73. out: Optional[Path] = Option(None, "--out", "-o")
  74. ):
  75. """Generate from template."""
  76. logger.debug(f"Generating template: {id}")
  77. template = self._get_template(id, raise_on_missing=True)
  78. # Validate template (will raise TemplateValidationError if validation fails)
  79. self._validate_template(template, id)
  80. # Process variables and render
  81. values = self._process_variables(template)
  82. try:
  83. content = template.render(values)
  84. except Exception as e:
  85. logger.error(f"Failed to render template: {e}")
  86. raise
  87. # Output result
  88. if out:
  89. out.parent.mkdir(parents=True, exist_ok=True)
  90. out.write_text(content)
  91. console.print(f"[green]✅ Generated to {out}[/green]")
  92. else:
  93. console.print(f"\n{'='*60}\nGenerated Content:\n{'='*60}")
  94. console.print(content)
  95. def _validate_template(self, template, template_id: str) -> None:
  96. """Validate template and raise error if validation fails."""
  97. # Get registered variables for validation
  98. registered_vars = set(self.variables.variables.keys())
  99. # Validate will raise TemplateValidationError for critical errors (syntax)
  100. # and return a list of warnings for non-critical issues
  101. warnings = template.validate(registered_vars)
  102. # If there are non-critical warnings, log them but don't fail
  103. if warnings:
  104. logger.warning(f"Template '{template_id}' has validation warnings: {warnings}")
  105. # Optionally, you could still raise an error for strict validation:
  106. # raise TemplateValidationError(template_id, warnings)
  107. def _process_variables(self, template) -> Dict[str, Any]:
  108. """Process template variables with prompting."""
  109. # Get variables used in template that are registered
  110. template_vars = self.variables.get_variables_for_template(list(template.vars))
  111. if not template_vars:
  112. return {}
  113. # Collect all defaults from variables and template
  114. defaults = {}
  115. for var_name, var in template_vars.items():
  116. if var.default is not None:
  117. defaults[var_name] = var.default
  118. # Handle dict variable defaults specially
  119. # Auto-detect dict type from template usage
  120. for var_name, var in template_vars.items():
  121. # If template uses dict access, treat it as dict type regardless of registration
  122. if var_name in template.var_dict_keys:
  123. # This is a dict variable with dynamic keys
  124. # Get defaults for each key from template
  125. if var_name in template.var_defaults and isinstance(template.var_defaults[var_name], dict):
  126. if var_name not in defaults:
  127. defaults[var_name] = {}
  128. defaults[var_name].update(template.var_defaults[var_name])
  129. # Also add template defaults for regular variables
  130. for k, v in template.var_defaults.items():
  131. if not isinstance(v, dict): # Skip dict defaults, already handled
  132. defaults[k] = v
  133. # Use rich output if enabled
  134. if not get_config().use_rich_output:
  135. # Simple fallback - just prompt for missing values
  136. values = defaults.copy()
  137. for var_name, var in template_vars.items():
  138. if var_name not in values:
  139. desc = f" ({var.description})" if var.description else ""
  140. values[var_name] = input(f"Enter {var_name}{desc}: ")
  141. return values
  142. # Pass dict keys info to prompt handler
  143. # Use the new simplified prompt handler with dict support
  144. return SimplifiedPromptHandler(template_vars, defaults, template.var_dict_keys)()
  145. def register_cli(self, app: Typer):
  146. """Register module commands with the main app."""
  147. module_app = Typer()
  148. module_app.command()(self.list)
  149. module_app.command()(self.show)
  150. module_app.command()(self.generate)
  151. app.add_typer(module_app, name=self.name, help=self.description)