helpers.py 6.7 KB

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