module.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. from abc import ABC, abstractmethod
  2. from pathlib import Path
  3. from typing import Any, Dict, Optional, Tuple, List
  4. import logging
  5. from typer import Typer, Option, Argument
  6. from rich.console import Console
  7. from rich.panel import Panel
  8. from rich.text import Text
  9. from rich.syntax import Syntax
  10. from rich.table import Table
  11. from io import StringIO
  12. from rich import box
  13. from .library import LibraryManager
  14. from .prompt import PromptHandler
  15. from .template import Template
  16. from .variables import VariableGroup, VariableManager
  17. from .config import ConfigManager
  18. from .processor import VariableProcessor
  19. logger = logging.getLogger('boilerplates')
  20. class Module(ABC):
  21. """
  22. Base Module for all CLI Commands.
  23. This class now uses VariableManager for centralized variable management,
  24. providing better organization and more advanced variable operations.
  25. """
  26. def __init__(self, name: str, description: str, files: list[str], vars: list[VariableGroup] = None):
  27. self.name = name
  28. self.description = description
  29. self.files = files
  30. # Initialize ConfigManager and VariableManager with it
  31. self.config = ConfigManager()
  32. self.vars = VariableManager(vars if vars is not None else [], self.config)
  33. self.app = Typer()
  34. self.libraries = LibraryManager() # Initialize library manager
  35. # Validate that required attributes are set
  36. if not self.name:
  37. raise ValueError("Module name must be set")
  38. if not self.description:
  39. raise ValueError("Module description must be set")
  40. if not isinstance(self.files, list) or len(self.files) == 0:
  41. raise ValueError("Module files must be a non-empty list")
  42. if not all(isinstance(var, VariableGroup) for var in (vars if vars is not None else [])):
  43. raise ValueError("Module vars must be a list of VariableGroup instances")
  44. def _validate_variables(self, variables: List[str]) -> Tuple[bool, List[str]]:
  45. """Validate if all template variables exist in the variable groups.
  46. Args:
  47. variables: List of variable names to validate
  48. Returns:
  49. Tuple of (success: bool, missing_variables: List[str])
  50. """
  51. missing_variables = [var for var in variables if not self.vars.has_variable(var)]
  52. success = len(missing_variables) == 0
  53. return success, missing_variables
  54. def _get_variable_defaults_for_template(self, template_vars: List[str]) -> Dict[str, Any]:
  55. """Get default values for variables used in a template.
  56. Args:
  57. template_vars: List of variable names used in the template
  58. Returns:
  59. Dictionary mapping variable names to their default values
  60. """
  61. defaults = {}
  62. for group in self.vars.variable_groups:
  63. for variable in group.vars:
  64. if variable.name in template_vars and variable.value is not None:
  65. defaults[variable.name] = variable.value
  66. return defaults
  67. def _get_groups_with_template_vars(self, template_vars: List[str]) -> List[VariableGroup]:
  68. """Get groups that contain at least one template variable.
  69. Args:
  70. template_vars: List of variable names used in the template
  71. Returns:
  72. List of VariableGroup objects that have variables used by the template
  73. """
  74. result = []
  75. for group in self.vars.variable_groups:
  76. if any(var.name in template_vars for var in group.vars):
  77. result.append(group)
  78. return result
  79. def _resolve_variable_defaults(self, template_vars: List[str], template_defaults: Dict[str, Any] = None) -> Dict[str, Any]:
  80. """Resolve variable default values with priority handling.
  81. Priority order:
  82. 1. Module variable defaults (low priority)
  83. 2. Template's built-in defaults (medium priority)
  84. 3. User config defaults (high priority)
  85. """
  86. if template_defaults is None:
  87. template_defaults = {}
  88. # Start with module defaults, then override with template and user config
  89. defaults = self._get_variable_defaults_for_template(template_vars)
  90. defaults.update(template_defaults)
  91. defaults.update({var: value for var, value in self.config.get_variable_defaults(self.name).items() if var in template_vars})
  92. return defaults
  93. def _filter_variables_for_template(self, template_vars: List[str]) -> Dict[str, Any]:
  94. """Filter the variable groups to only include variables needed by the template."""
  95. filtered_vars = {}
  96. template_vars_set = set(template_vars) # Convert to set for O(1) lookup
  97. for group in self._get_groups_with_template_vars(template_vars):
  98. # Get variables that match template vars and convert to dict format
  99. group_vars = {
  100. var.name: var.to_dict() for var in group.vars if var.name in template_vars_set
  101. }
  102. # Only include groups that have variables
  103. if group_vars:
  104. filtered_vars[group.name] = {
  105. 'description': group.description,
  106. 'enabled': group.enabled,
  107. 'prompt_to_set': getattr(group, 'prompt_to_set', ''),
  108. 'prompt_to_enable': getattr(group, 'prompt_to_enable', ''),
  109. 'vars': group_vars
  110. }
  111. return filtered_vars
  112. def list(self):
  113. """List all templates in the module."""
  114. logger.debug(f"Listing templates for module: {self.name}")
  115. templates = self.libraries.find(self.name, self.files, sorted=True)
  116. logger.debug(f"Found {len(templates)} templates")
  117. for template in templates:
  118. print(f"{template.id} ({template.name}, {template.directory})")
  119. return templates
  120. def show(self, id: str = Argument(..., metavar="template", help="The template to show details for")):
  121. """Show details about a template"""
  122. logger.debug(f"Showing details for template: {id} in module: {self.name}")
  123. template = self.libraries.find_by_id(module_name=self.name, files=self.files, template_id=id)
  124. if not template:
  125. logger.error(f"Template with ID '{id}' not found")
  126. print(f"Template with ID '{id}' not found.")
  127. return
  128. console = Console()
  129. # Build title with version if available
  130. version_suffix = f" v{template.version}" if template.version else ""
  131. title = f"[bold magenta]{template.name} ({template.id}{version_suffix})[/bold magenta]"
  132. # Print header
  133. console.print(title)
  134. console.print(f"[dim white]{template.description}[/dim white]")
  135. console.print()
  136. # Build and print metadata fields
  137. metadata = []
  138. if template.author:
  139. metadata.append(f"Author: [cyan]{template.author}[/cyan]")
  140. if template.date:
  141. metadata.append(f"Date: [cyan]{template.date}[/cyan]")
  142. if template.tags:
  143. metadata.append(f"Tags: [cyan]{', '.join(template.tags)}[/cyan]")
  144. # Find variable groups used by this template
  145. template_var_groups = [
  146. group.name for group in self._get_groups_with_template_vars(template.vars)
  147. ]
  148. if template_var_groups:
  149. metadata.append(f"Functions: [cyan]{', '.join(template_var_groups)}[/cyan]")
  150. # Print all metadata
  151. for item in metadata:
  152. console.print(item)
  153. # Template content
  154. if template.content:
  155. console.print(f"\n{template.content}")
  156. def generate(self, id: str = Argument(..., metavar="template", help="The template to generate from"), out: Optional[Path] = Option(None, "--out", "-o", help="Output file to save the generated template")):
  157. """Generate a new template with complex variable prompting logic"""
  158. # Find template by ID
  159. template = self.libraries.find_by_id(module_name=self.name, files=self.files, template_id=id)
  160. if not template:
  161. print(f"Template '{id}' not found.")
  162. return
  163. # Validate if the variables in the template are valid ones
  164. success, missing = self._validate_variables(template.vars)
  165. if not success:
  166. print(f"Template '{id}' has invalid variables: {missing}")
  167. return
  168. # Process variables using dedicated processor
  169. try:
  170. processor = VariableProcessor(self.vars, self.config, self.name)
  171. final_variable_values = processor.process_variables_for_template(template)
  172. logger.debug(f"Variable processing completed with {len(final_variable_values)} variables")
  173. except KeyboardInterrupt:
  174. print("\n[red]Template generation cancelled.[/red]")
  175. return
  176. except Exception as e:
  177. print(f"Error during variable prompting: {e}")
  178. return
  179. # Step 7: Generate template with final variable values
  180. try:
  181. generated_content = template.render(final_variable_values)
  182. except Exception as e:
  183. print(f"Error rendering template: {e}")
  184. return
  185. # Step 8: Output the generated content
  186. if out:
  187. try:
  188. out.parent.mkdir(parents=True, exist_ok=True)
  189. with open(out, 'w', encoding='utf-8') as f:
  190. f.write(generated_content)
  191. logger.info(f"Template generated and saved to {out}")
  192. print(f"✅ Template generated and saved to {out}")
  193. except Exception as e:
  194. logger.error(f"Error saving to file {out}: {e}")
  195. print(f"Error saving to file {out}: {e}")
  196. else:
  197. print("\n" + "="*60)
  198. print("📄 Generated Template Content:")
  199. print("="*60)
  200. print(generated_content)
  201. def register(self, app: Typer):
  202. self.app.command()(self.list)
  203. self.app.command()(self.show)
  204. self.app.command()(self.generate)
  205. app.add_typer(self.app, name=self.name, help=self.description, no_args_is_help=True)