Fix rendered expression display for dotted Jinja variables (#SKY-7937) (#4684)

This commit is contained in:
Celal Zamanoglu
2026-02-11 01:47:12 +03:00
committed by GitHub
parent 0c879492e4
commit 76411b6022

View File

@@ -4957,6 +4957,59 @@ def _is_pure_jinja_expression(expression: str) -> bool:
return True
def _resolve_nested_path(value: Any, path: str) -> Any:
"""
Resolve a dotted/bracket access path on a nested value.
Examples:
_resolve_nested_path({"a": {"b": 1}}, ".a.b") -> 1
_resolve_nested_path([{"x": 2}], "[0].x") -> 2
Args:
value: The root value to traverse
path: The access path (e.g., ".field1.field2[0].field3")
Returns:
The resolved leaf value
Raises:
LookupError: If the path cannot be resolved
"""
segments = re.findall(r"\.([a-zA-Z_]\w*)|\[(\d+)\]", path)
current = value
for dot_key, bracket_idx in segments:
if dot_key:
if isinstance(current, dict):
if dot_key not in current:
raise LookupError(f"Key {dot_key!r} not found")
current = current[dot_key]
else:
raise LookupError(f"Cannot access .{dot_key} on {type(current).__name__}")
elif bracket_idx:
idx = int(bracket_idx)
if isinstance(current, (list, tuple)):
if idx >= len(current):
raise LookupError(f"Index [{idx}] out of range")
current = current[idx]
else:
raise LookupError(f"Cannot index [{idx}] on {type(current).__name__}")
return current
_JINJA_DISPLAY_FILTERS: dict[str, Callable[[Any], Any]] = {
"lower": lambda v: str(v).lower(),
"upper": lambda v: str(v).upper(),
"trim": lambda v: str(v).strip(),
"title": lambda v: str(v).title(),
"capitalize": lambda v: str(v).capitalize(),
"int": lambda v: int(v),
"float": lambda v: float(v),
"string": lambda v: str(v),
"length": lambda v: len(v),
"abs": lambda v: abs(v),
}
def _render_jinja_expression_for_display(
expression: str,
context_values: dict[str, Any],
@@ -4969,6 +5022,13 @@ def _render_jinja_expression_for_display(
without actually evaluating the expression. For example:
- Input: "{{ base_date == date_1 }}" with context {"base_date": "01-25-2026", "date_1": "01-25-2026"}
- Output: '"01-25-2026" == "01-25-2026"'
- Input: "{{ output.extracted_information.field != None }}" with nested dict context
- Output: '"some_value" != None'
- Input: "{{ output.status|lower == 'active' }}" with context {"output": {"status": "Active"}}
- Output: '"active" == \'active\''
Known Jinja filters (lower, upper, trim, etc.) are applied to the resolved value.
Unknown filters are left as-is in the output.
Returns the original expression if it's not a pure Jinja expression or if rendering fails.
"""
@@ -4980,15 +5040,48 @@ def _render_jinja_expression_for_display(
inner_expr = expression.strip()[2:-2].strip()
display_expr = inner_expr
# Substitute variable names with their values using word boundary regex
# This ensures we only match whole variable names, not substrings
# e.g., "date" won't match inside "validate_date" or "date_1"
# Substitute variable references (including dotted/bracket access paths and filters)
# with their values.
# Match var_name optionally followed by .field or [index] segments,
# then optionally followed by a |filter_name.
# Sort by key length (longest first) to avoid partial matches.
for var_name in sorted(context_values.keys(), key=len, reverse=True):
pattern = r"\b" + re.escape(var_name) + r"\b"
var_value = context_values[var_name]
# Quote string values for clarity
replacement = f'"{var_value}"' if isinstance(var_value, str) else str(var_value)
display_expr = re.sub(pattern, replacement, display_expr)
pattern = r"\b" + re.escape(var_name) + r"((?:\.[a-zA-Z_]\w*|\[\d+\])*)(\|[a-zA-Z_]\w*)?"
def _replacer(match: re.Match, _var_name: str = var_name) -> str:
access_path = match.group(1) # the dotted/bracket part after var_name
filter_expr = match.group(2) # e.g., "|lower" or None
var_value = context_values[_var_name]
if access_path:
try:
var_value = _resolve_nested_path(var_value, access_path)
except LookupError:
# Path couldn't be resolved — return original text unchanged
return match.group(0)
if filter_expr:
filter_name = filter_expr[1:] # strip the leading |
filter_fn = _JINJA_DISPLAY_FILTERS.get(filter_name)
if filter_fn is not None:
try:
var_value = filter_fn(var_value)
except Exception:
# Filter application failed — show value with filter text
if isinstance(var_value, str):
return f'"{var_value}"{filter_expr}'
return f"{var_value}{filter_expr}"
else:
# Unknown filter — show value with filter text preserved
if isinstance(var_value, str):
return f'"{var_value}"{filter_expr}'
return f"{var_value}{filter_expr}"
if isinstance(var_value, str):
return f'"{var_value}"'
return str(var_value)
display_expr = re.sub(pattern, _replacer, display_expr)
return display_expr
except Exception as exc: