refactor: Simplify to dotted notation only, remove dict variable handling

- Removed all dict variable handling code
- Simplified TemplateVariable dataclass (no more is_dict, dict_keys)
- Simplified template parser to only handle dotted notation
- Removed _prompt_dict_variable from prompt handler
- Updated nginx template to use underscores instead of dots/dicts

This greatly simplifies the system but requires templates to use
underscores (service_port_http) instead of dots or dict access
to avoid Jinja2's attribute resolution confusion.
This commit is contained in:
xcad
2025-09-08 09:51:21 +02:00
parent 238ad6b0b0
commit b014b23f13
4 changed files with 22 additions and 151 deletions
+5 -54
View File
@@ -118,11 +118,7 @@ class SimplifiedPromptHandler:
console.print(f"\n[bold cyan]{display_name} - Required Configuration[/bold cyan]")
for var_name in required:
var = self.variables[var_name]
if var.is_dict:
console.print(f"\n[cyan]{var.name}[/cyan]")
self.values[var_name] = self._prompt_dict_variable(var)
else:
self.values[var_name] = self._prompt_variable(var, required=True)
self.values[var_name] = self._prompt_variable(var, required=True)
# Process optional variables
if optional:
@@ -135,49 +131,9 @@ class SimplifiedPromptHandler:
if Confirm.ask("Do you want to change any values?", default=False):
for var_name in optional:
var = self.variables[var_name]
if var.is_dict:
console.print(f"\n[cyan]{var.name}[/cyan]")
self.values[var_name] = self._prompt_dict_variable(
var, current_values=self.values.get(var_name, {})
)
else:
self.values[var_name] = self._prompt_variable(
var, current_value=self.values[var_name]
)
def _prompt_dict_variable(self, var: TemplateVariable, current_values: Dict[str, Any] = None) -> Dict[str, Any]:
"""Prompt for a dict variable with dynamic keys."""
result = {}
current_values = current_values or {}
for key in var.dict_keys:
# Use current value if available, otherwise check for default
current_value = current_values.get(key)
if current_value is None and isinstance(var.default, dict):
current_value = var.default.get(key)
prompt_msg = f"Enter {var.name}['{key}']"
if current_value is not None:
prompt_msg += f" [dim]({current_value})[/dim]"
else:
prompt_msg += " [red](Required)[/red]"
# Infer type from current value or default
if isinstance(current_value, int):
while True:
try:
result[key] = IntPrompt.ask(prompt_msg, default=current_value)
break
except ValueError:
console.print("[red]Please enter a valid integer[/red]")
elif isinstance(current_value, bool):
result[key] = Confirm.ask(prompt_msg, default=current_value)
else:
value = Prompt.ask(prompt_msg, default=str(current_value) if current_value else None)
result[key] = value if value else current_value
return result
self.values[var_name] = self._prompt_variable(
var, current_value=self.values[var_name]
)
def _show_variables(self, var_names: List[str]):
"""Display current variable values."""
@@ -185,12 +141,7 @@ class SimplifiedPromptHandler:
var = self.variables[var_name]
value = self.values.get(var_name, var.default)
if value is not None:
# Special formatting for dict values - show each key separately
if isinstance(value, dict):
for key, val in value.items():
console.print(f" {var.display_name}['{key}']: [dim]{val}[/dim]")
else:
console.print(f" {var.display_name}: [dim]{value}[/dim]")
console.print(f" {var.display_name}: [dim]{value}[/dim]")
def _prompt_variable(
self,
+9 -78
View File
@@ -36,7 +36,6 @@ class Template:
# Template variable analysis results
vars: Set[str] = field(default_factory=set, init=False)
var_defaults: Dict[str, Any] = field(default_factory=dict, init=False)
var_dict_keys: Dict[str, List[str]] = field(default_factory=dict, init=False) # Track dict access patterns
variables: Dict[str, TemplateVariable] = field(default_factory=dict, init=False) # Analyzed variables
def __post_init__(self):
@@ -52,10 +51,10 @@ class Template:
self.size = self.file_path.stat().st_size if self.file_path.exists() else 0
# Parse template variables
self.vars, self.var_defaults, self.var_dict_keys = self._parse_template_variables(self.content)
self.vars, self.var_defaults = self._parse_template_variables(self.content)
# Analyze variables to create TemplateVariable objects
self.variables = analyze_template_variables(
self.vars, self.var_defaults, self.var_dict_keys, self.content
self.vars, self.var_defaults, self.content
)
@staticmethod
@@ -96,17 +95,15 @@ class Template:
post = frontmatter.load(f)
return post.metadata, post.content
def _parse_template_variables(self, template_content: str) -> Tuple[Set[str], Dict[str, Any], Dict[str, List[str]]]:
def _parse_template_variables(self, template_content: str) -> Tuple[Set[str], Dict[str, Any]]:
"""Parse Jinja2 template to extract variables and their defaults.
Handles multiple patterns:
Handles:
- Simple variables: service_name
- Dotted notation: traefik.host
- Dict notation: service_port['http']
- Nested patterns: nginx_dashboard.port['dashboard']
- Dotted notation: traefik.host, service_port.http
Returns:
Tuple of (all_variable_names, variable_defaults, dict_access_patterns)
Tuple of (all_variable_names, variable_defaults)
"""
try:
env = self._create_jinja_env()
@@ -115,42 +112,7 @@ class Template:
# Start with variables found by Jinja2's meta utility
all_variables = meta.find_undeclared_variables(ast)
# Track dict access patterns
dict_keys = {} # var_name -> list of keys accessed
# Handle all Getitem nodes (dict access)
for node in ast.find_all(nodes.Getitem):
if isinstance(node.arg, nodes.Const) and isinstance(node.arg.value, str):
key = node.arg.value
current = node.node
# Handle nested patterns like nginx_dashboard.port['dashboard']
if isinstance(current, nodes.Getattr):
# Build the full dotted name
parts = [current.attr]
base = current.node
while isinstance(base, nodes.Getattr):
parts.insert(0, base.attr)
base = base.node
if isinstance(base, nodes.Name):
parts.insert(0, base.name)
var_name = '.'.join(parts)
# This is a dotted variable with dict access
all_variables.add(var_name)
if var_name not in dict_keys:
dict_keys[var_name] = []
if key not in dict_keys[var_name]:
dict_keys[var_name].append(key)
# Handle simple dict access like service_port['http']
elif isinstance(current, nodes.Name):
var_name = current.name
if var_name not in dict_keys:
dict_keys[var_name] = []
if key not in dict_keys[var_name]:
dict_keys[var_name].append(key)
# Handle dotted notation variables (like traefik.host, swarm.replicas)
# Handle dotted notation variables (like traefik.host, service_port.http)
for node in ast.find_all(nodes.Getattr):
current = node.node
# Build the full dotted name
@@ -171,37 +133,6 @@ class Template:
if isinstance(node.node, nodes.Name):
defaults[node.node.name] = node.args[0].value
# Handle dict access defaults
elif isinstance(node.node, nodes.Getitem):
if isinstance(node.node.arg, nodes.Const):
key = node.node.arg.value
# Handle nested pattern defaults: {{ nginx_dashboard.port['dashboard'] | default(8081) }}
if isinstance(node.node.node, nodes.Getattr):
# Build the full dotted name
parts = []
current = node.node.node
while isinstance(current, nodes.Getattr):
parts.insert(0, current.attr)
current = current.node
if isinstance(current, nodes.Name):
parts.insert(0, current.name)
var_name = '.'.join(parts)
if var_name not in defaults:
defaults[var_name] = {}
if not isinstance(defaults[var_name], dict):
defaults[var_name] = {}
defaults[var_name][key] = node.args[0].value
# Handle simple dict defaults: {{ var['key'] | default(value) }}
elif isinstance(node.node.node, nodes.Name):
var_name = node.node.node.name
if var_name not in defaults:
defaults[var_name] = {}
if not isinstance(defaults[var_name], dict):
defaults[var_name] = {}
defaults[var_name][key] = node.args[0].value
# Handle dotted variable defaults: {{ traefik.host | default('example.com') }}
elif isinstance(node.node, nodes.Getattr):
# Build the full dotted name
@@ -215,10 +146,10 @@ class Template:
var_name = '.'.join(parts)
defaults[var_name] = node.args[0].value
return all_variables, defaults, dict_keys
return all_variables, defaults
except Exception as e:
logging.getLogger('boilerplates').debug(f"Error parsing template variables: {e}")
return set(), {}, {}
return set(), {}
def validate(self) -> List[str]:
"""Validate template integrity.
+4 -15
View File
@@ -8,8 +8,7 @@ class TemplateVariable:
Represents a variable found in a template with all its properties:
- Simple variables: service_name, container_name
- Dotted variables: traefik.host, network.name
- Dict variables: service_port['http'], nginx_dashboard.port['dashboard']
- Dotted variables: traefik.host, network.name, service_port.http
- Enabler variables: Variables used in {% if var %} conditions
"""
name: str
@@ -18,8 +17,6 @@ class TemplateVariable:
# Variable characteristics
is_enabler: bool = False # Used in {% if %} conditions
is_dict: bool = False # Has dict access patterns like var['key']
dict_keys: List[str] = field(default_factory=list) # Keys accessed if is_dict
# Grouping info (extracted from dotted notation)
group: Optional[str] = None # e.g., 'traefik' for 'traefik.host'
@@ -41,7 +38,6 @@ class TemplateVariable:
def analyze_template_variables(
vars_used: Set[str],
var_defaults: Dict[str, Any],
var_dict_keys: Dict[str, List[str]],
template_content: str
) -> Dict[str, TemplateVariable]:
"""Analyze template variables and create TemplateVariable objects.
@@ -49,7 +45,6 @@ def analyze_template_variables(
Args:
vars_used: Set of all variable names used in template
var_defaults: Dict of variable defaults from template
var_dict_keys: Dict of variables with dict access patterns
template_content: The raw template content for additional analysis
Returns:
@@ -69,22 +64,16 @@ def analyze_template_variables(
# Detect if it's an enabler
var.is_enabler = var_name in enablers
# Detect if it's a dict variable
if var_name in var_dict_keys:
var.is_dict = True
var.dict_keys = var_dict_keys[var_name]
# Dict variables might have dict defaults
if isinstance(var.default, dict):
var.type = "dict"
# Infer type from default value
if var.default is not None and var.type == "string":
if var.default is not None:
if isinstance(var.default, bool):
var.type = "boolean"
elif isinstance(var.default, int):
var.type = "integer"
elif isinstance(var.default, float):
var.type = "float"
else:
var.type = "string"
# If it's an enabler without a default, assume boolean
if var.is_enabler and var.default is None:
+4 -4
View File
@@ -29,12 +29,12 @@ services:
- traefik.http.routers.{{ container_name }}.service={{ container_name }}
{% endif %}
{% endif %}
{% if not traefik and service_port %}
{% if not traefik %}
ports:
- "{{ service_port['http'] | default(8080) }}:80"
- "{{ service_port['https'] | default(8443) }}:443"
- "{{ service_port_http | default(8080) }}:80"
- "{{ service_port_https | default(8443) }}:443"
{% if nginx_dashboard %}
- "{{ nginx_dashboard.port['dashboard'] | default(8081) }}:8080"
- "{{ nginx_dashboard_port_dashboard | default(8081) }}:8080"
{% endif %}
{% endif %}
# volumes: