module.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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 PromptHandler
  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. # Variable groups
  56. if template.vars:
  57. groups = self.variables.get_variables_for_template(template.vars)
  58. if groups:
  59. console.print(f"Functions: [cyan]{', '.join(groups.keys())}[/cyan]")
  60. # Content
  61. if template.content:
  62. console.print(f"\n{template.content}")
  63. def _get_template(self, template_id: str, raise_on_missing: bool = False):
  64. """Get template by ID with unified error handling."""
  65. template = self.libraries.find_by_id(self.name, self.files, template_id)
  66. if not template:
  67. logger.error(f"Template '{template_id}' not found")
  68. if raise_on_missing:
  69. raise TemplateNotFoundError(template_id, self.name)
  70. console.print(f"[red]Template '{template_id}' not found[/red]")
  71. return template
  72. def generate(
  73. self,
  74. id: str = Argument(..., help="Template ID"),
  75. out: Optional[Path] = Option(None, "--out", "-o")
  76. ):
  77. """Generate from template."""
  78. logger.debug(f"Generating template: {id}")
  79. template = self._get_template(id, raise_on_missing=True)
  80. # Validate template (will raise TemplateValidationError if validation fails)
  81. self._validate_template(template, id)
  82. # Process variables and render
  83. values = self._process_variables(template)
  84. try:
  85. content = template.render(values)
  86. except Exception as e:
  87. logger.error(f"Failed to render template: {e}")
  88. raise
  89. # Output result
  90. if out:
  91. out.parent.mkdir(parents=True, exist_ok=True)
  92. out.write_text(content)
  93. console.print(f"[green]✅ Generated to {out}[/green]")
  94. else:
  95. console.print(f"\n{'='*60}\nGenerated Content:\n{'='*60}")
  96. console.print(content)
  97. def _validate_template(self, template, template_id: str) -> None:
  98. """Validate template and raise error if validation fails."""
  99. errors = template.validate(set(self.variables.variables.keys()))
  100. if errors:
  101. logger.error(f"Template '{template_id}' validation failed")
  102. raise TemplateValidationError(template_id, errors)
  103. def _process_variables(self, template) -> Dict[str, Any]:
  104. """Process template variables with prompting."""
  105. grouped_vars = self.variables.get_variables_for_template(list(template.vars))
  106. if not grouped_vars:
  107. return {}
  108. # Collect all defaults
  109. defaults = {
  110. var.name: var.default
  111. for group_vars in grouped_vars.values()
  112. for var in group_vars
  113. if var.default is not None
  114. }
  115. defaults.update(template.var_defaults) # Template defaults override
  116. # Use rich output if enabled
  117. if not get_config().use_rich_output:
  118. # Simple fallback - just prompt for missing values
  119. values = defaults.copy()
  120. for group_vars in grouped_vars.values():
  121. for var in group_vars:
  122. if var.name not in values:
  123. desc = f" ({var.description})" if var.description else ""
  124. values[var.name] = input(f"Enter {var.name}{desc}: ")
  125. return values
  126. # Format for PromptHandler
  127. formatted_groups = {}
  128. for group_name, variables in grouped_vars.items():
  129. group_info = self.variables.groups.get(group_name, {})
  130. formatted_groups[group_name] = {
  131. 'display_name': group_info.get('display_name', group_name.title()),
  132. 'description': group_info.get('description', ''),
  133. 'icon': group_info.get('icon', ''),
  134. 'vars': {},
  135. 'enabler': self.variables.group_enablers.get(group_name, '')
  136. }
  137. # Add usage patterns to each variable config
  138. for var in variables:
  139. var_config = var.to_prompt_config()
  140. # Add usage patterns if this variable is used in the template
  141. if var.name in template.var_usage:
  142. var_config['usage_patterns'] = template.var_usage[var.name]
  143. formatted_groups[group_name]['vars'][var.name] = var_config
  144. return PromptHandler(formatted_groups, defaults)()
  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)