helpers.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. """Helper methods for module variable application and template generation."""
  2. from __future__ import annotations
  3. import logging
  4. from pathlib import Path
  5. from typing import Any
  6. import click
  7. import yaml
  8. from typer import Exit
  9. from ..display import DisplayManager
  10. from ..input import PromptHandler
  11. logger = logging.getLogger(__name__)
  12. def parse_var_inputs(var_options: list[str], extra_args: list[str]) -> dict[str, Any]:
  13. """Parse variable inputs from --var options and extra args with type conversion.
  14. Supports formats:
  15. --var KEY=VALUE
  16. --var KEY VALUE
  17. Values are automatically converted to appropriate types:
  18. - 'true', 'yes', '1' → True
  19. - 'false', 'no', '0' → False
  20. - Numeric strings → int or float
  21. - Everything else → string
  22. Args:
  23. var_options: List of variable options from CLI
  24. extra_args: Additional arguments that may contain values
  25. Returns:
  26. Dictionary of parsed variables with converted types
  27. """
  28. variables = {}
  29. # Parse --var KEY=VALUE format
  30. for var_option in var_options:
  31. if "=" in var_option:
  32. key, value = var_option.split("=", 1)
  33. variables[key] = _convert_string_to_type(value)
  34. # --var KEY VALUE format - value should be in extra_args
  35. elif extra_args:
  36. value = extra_args.pop(0)
  37. variables[var_option] = _convert_string_to_type(value)
  38. else:
  39. logger.warning(f"No value provided for variable '{var_option}'")
  40. return variables
  41. def _convert_string_to_type(value: str) -> Any:
  42. """Convert string value to appropriate Python type.
  43. Args:
  44. value: String value to convert
  45. Returns:
  46. Converted value (bool, int, float, or str)
  47. """
  48. # Boolean conversion
  49. if value.lower() in ("true", "yes", "1"):
  50. return True
  51. if value.lower() in ("false", "no", "0"):
  52. return False
  53. # Integer conversion
  54. try:
  55. return int(value)
  56. except ValueError:
  57. pass
  58. # Float conversion
  59. try:
  60. return float(value)
  61. except ValueError:
  62. pass
  63. # Return as string
  64. return value
  65. def load_var_file(var_file_path: str) -> dict:
  66. """Load variables from a YAML file.
  67. Args:
  68. var_file_path: Path to the YAML file containing variables
  69. Returns:
  70. Dictionary of variable names to values (flat structure)
  71. Raises:
  72. FileNotFoundError: If the var file doesn't exist
  73. ValueError: If the file is not valid YAML or has invalid structure
  74. """
  75. var_path = Path(var_file_path).expanduser().resolve()
  76. if not var_path.exists():
  77. raise FileNotFoundError(f"Variable file not found: {var_file_path}")
  78. if not var_path.is_file():
  79. raise ValueError(f"Variable file path is not a file: {var_file_path}")
  80. try:
  81. with var_path.open(encoding="utf-8") as f:
  82. content = yaml.safe_load(f)
  83. except yaml.YAMLError as e:
  84. raise ValueError(f"Invalid YAML in variable file: {e}") from e
  85. except OSError as e:
  86. raise ValueError(f"Error reading variable file: {e}") from e
  87. if not isinstance(content, dict):
  88. raise ValueError(f"Variable file must contain a YAML dictionary, got {type(content).__name__}")
  89. logger.info(f"Loaded {len(content)} variables from file: {var_path.name}")
  90. logger.debug(f"Variables from file: {', '.join(content.keys())}")
  91. return content
  92. def apply_variable_defaults(template, config_manager, module_name: str) -> None:
  93. """Apply config defaults to template variables.
  94. Args:
  95. template: Template instance with variables to configure
  96. config_manager: ConfigManager instance
  97. module_name: Name of the module
  98. """
  99. if not template.variables:
  100. return
  101. config_defaults = config_manager.get_defaults(module_name)
  102. if config_defaults:
  103. logger.info(f"Loading config defaults for module '{module_name}'")
  104. successful = template.variables.apply_defaults(config_defaults, "config")
  105. if successful:
  106. logger.debug(f"Applied config defaults for: {', '.join(successful)}")
  107. def apply_var_file(template, var_file_path: str | None, display: DisplayManager) -> None:
  108. """Apply variables from a YAML file to template.
  109. Args:
  110. template: Template instance to apply variables to
  111. var_file_path: Path to the YAML file containing variables
  112. display: DisplayManager for error messages
  113. Raises:
  114. Exit: If the file cannot be loaded or contains invalid data
  115. """
  116. if not var_file_path or not template.variables:
  117. return
  118. try:
  119. var_file_vars = load_var_file(var_file_path)
  120. if var_file_vars:
  121. # Get list of valid variable names from template
  122. valid_vars = set()
  123. for section in template.variables.get_sections().values():
  124. valid_vars.update(section.variables.keys())
  125. # Warn about unknown variables
  126. unknown_vars = set(var_file_vars.keys()) - valid_vars
  127. if unknown_vars:
  128. for var_name in sorted(unknown_vars):
  129. logger.warning(f"Variable '{var_name}' from var-file does not exist in template '{template.id}'")
  130. successful = template.variables.apply_defaults(var_file_vars, "var-file")
  131. if successful:
  132. logger.debug(f"Applied var-file overrides for: {', '.join(successful)}")
  133. except (FileNotFoundError, ValueError) as e:
  134. display.error(
  135. f"Failed to load variable file: {e}",
  136. context="variable file loading",
  137. )
  138. raise Exit(code=1) from e
  139. def apply_cli_overrides(template, var: list[str] | None, ctx=None) -> None:
  140. """Apply CLI variable overrides to template.
  141. Args:
  142. template: Template instance to apply overrides to
  143. var: List of variable override strings from --var flags
  144. ctx: Context object containing extra args (optional, will get current context if None)
  145. """
  146. if not template.variables:
  147. return
  148. # Get context if not provided (compatible with all Typer versions)
  149. if ctx is None:
  150. try:
  151. ctx = click.get_current_context()
  152. except RuntimeError:
  153. ctx = None
  154. extra_args = list(ctx.args) if ctx and hasattr(ctx, "args") else []
  155. cli_overrides = parse_var_inputs(var or [], extra_args)
  156. if cli_overrides:
  157. logger.info(f"Received {len(cli_overrides)} variable overrides from CLI")
  158. successful_overrides = template.variables.apply_defaults(cli_overrides, "cli")
  159. if successful_overrides:
  160. logger.debug(f"Applied CLI overrides for: {', '.join(successful_overrides)}")
  161. def collect_variable_values(template, interactive: bool) -> dict[str, Any]:
  162. """Collect variable values from user prompts and template defaults.
  163. Args:
  164. template: Template instance with variables
  165. interactive: Whether to prompt user for values interactively
  166. Returns:
  167. Dictionary of variable names to values
  168. """
  169. variable_values = {}
  170. # Collect values interactively if enabled
  171. if interactive and template.variables:
  172. prompt_handler = PromptHandler()
  173. collected_values = prompt_handler.collect_variables(template.variables)
  174. if collected_values:
  175. variable_values.update(collected_values)
  176. logger.info(f"Collected {len(collected_values)} variable values from user input")
  177. # Add satisfied variable values (respects dependencies and toggles)
  178. if template.variables:
  179. variable_values.update(template.variables.get_satisfied_values())
  180. return variable_values