fix(engine): actionable error when a Jinja field is missing/None/empty#633
fix(engine): actionable error when a Jinja field is missing/None/empty#633nabinchha wants to merge 1 commit into
Conversation
Empty-render and missing-attribute failures used to surface as the generic "User provided prompt generation template is invalid." either because `sanitize_user_exceptions` stripped the detail or because Jinja's raw `UndefinedError` leaked through. Both now raise a new `EmptyTemplateRenderError` carrying a row-level diagnostic that names the offending chain and includes copy-pasteable Jinja conditional and SkipConfig fix patterns. Closes #629.
PR #633 Review —
|
Greptile SummaryThis PR replaces two different sources of a generic, unactionable Jinja failure message with a new
|
| Filename | Overview |
|---|---|
| packages/data-designer-engine/src/data_designer/engine/processing/ginja/environment.py | Adds UndefinedError catch paths and diagnostic message builder; gate expression logic has an edge-case bug when the template's root variable is entirely absent from the row record. |
| packages/data-designer-engine/src/data_designer/engine/processing/ginja/ast.py | Adds ast_extract_access_chains and resolve_access_chain helpers; visitor logic correctly deduplicates prefix chains and handles dynamic subscripts. |
| packages/data-designer-engine/src/data_designer/engine/processing/ginja/exceptions.py | Adds EmptyTemplateRenderError as a UserTemplateError subclass; straightforward and correctly placed in the hierarchy. |
| packages/data-designer-engine/tests/engine/processing/ginja/test_environment.py | Comprehensive regression tests covering both bug paths; missing coverage for the root-variable-absent-from-record scenario that reveals the gate-expression bug. |
| packages/data-designer-engine/tests/engine/processing/ginja/test_ast.py | Parametrized coverage for all chain-extraction and resolution cases; thorough and correct. |
| packages/data-designer-engine/tests/engine/column_generators/generators/test_image.py | Updated match string from 'invalid' to 'empty' to reflect the new error message; correct and minimal change. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[safe_render / render_template called] --> B[template.render record]
B -->|Success| C[validate_rendered_text]
C -->|rendered_text != empty| D[Return rendered_text]
C -->|rendered_text == empty| E[_build_empty_render_message]
B -->|UndefinedError| E
B -->|SecurityError| F[UserTemplateError]
B -->|OverflowError| F
E --> G[Parse AST to ast_extract_access_chains]
G --> H[resolve_access_chain per chain]
H --> I{Any culprit?}
I -->|Yes| J[Build gate_accessors len prefix plus 1]
I -->|No| K[Generic fallback message]
J -->|root in record| L[Correct gate expression]
J -->|root NOT in record| M[gate goes one step too deep suggested fix raises UndefinedError]
L --> N[EmptyTemplateRenderError raised]
M --> N
N --> O[sanitize_user_exceptions bypasses it]
O --> P[Actionable message reaches user]
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
packages/data-designer-engine/src/data_designer/engine/processing/ginja/environment.py:736-737
**Gate expression is one step too deep when the root variable is absent from the record**
`resolve_access_chain` returns `(False, None, [])` both when the root name is missing from `record` (`name not in record`) and when the first accessor fails — both yield `prefix = []`. The gate computation `sample_accessors[:len(sample_prefix)+1]` therefore produces `sample_accessors[:1]` in either scenario. For `{{ person.address.street }}` with `record = {}`, that gives `gate_chain = "person.address"`, so the suggestion becomes `{{ person.address.street if person.address else 'N/A' }}`. But in Jinja2, `person` is `Undefined` when it is absent from the context, and `Undefined.__getattr__` raises `UndefinedError` immediately — the suggested template itself fails with the same kind of error the user is trying to fix.
The gate should fall back to the root name alone (`gate_accessors = []`) when `sample_name` is not present in `record`.
Reviews (1): Last reviewed commit: "fix(engine): actionable error when a Jin..." | Re-trigger Greptile
| gate_accessors = sample_accessors[: len(sample_prefix) + 1] | ||
| gate_chain = _format_access_chain(sample_name, gate_accessors) |
There was a problem hiding this comment.
Gate expression is one step too deep when the root variable is absent from the record
resolve_access_chain returns (False, None, []) both when the root name is missing from record (name not in record) and when the first accessor fails — both yield prefix = []. The gate computation sample_accessors[:len(sample_prefix)+1] therefore produces sample_accessors[:1] in either scenario. For {{ person.address.street }} with record = {}, that gives gate_chain = "person.address", so the suggestion becomes {{ person.address.street if person.address else 'N/A' }}. But in Jinja2, person is Undefined when it is absent from the context, and Undefined.__getattr__ raises UndefinedError immediately — the suggested template itself fails with the same kind of error the user is trying to fix.
The gate should fall back to the root name alone (gate_accessors = []) when sample_name is not present in record.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-engine/src/data_designer/engine/processing/ginja/environment.py
Line: 736-737
Comment:
**Gate expression is one step too deep when the root variable is absent from the record**
`resolve_access_chain` returns `(False, None, [])` both when the root name is missing from `record` (`name not in record`) and when the first accessor fails — both yield `prefix = []`. The gate computation `sample_accessors[:len(sample_prefix)+1]` therefore produces `sample_accessors[:1]` in either scenario. For `{{ person.address.street }}` with `record = {}`, that gives `gate_chain = "person.address"`, so the suggestion becomes `{{ person.address.street if person.address else 'N/A' }}`. But in Jinja2, `person` is `Undefined` when it is absent from the context, and `Undefined.__getattr__` raises `UndefinedError` immediately — the suggested template itself fails with the same kind of error the user is trying to fix.
The gate should fall back to the root name alone (`gate_accessors = []`) when `sample_name` is not present in `record`.
How can I resolve this? If you propose a fix, please make it concise.
Summary
Closes #629.
Empty-render and missing-attribute Jinja failures used to surface as the same unhelpful message:
Two distinct failure modes were collapsing into that generic string:
{{ person.preferred_english_name }}where the key is missing):_assert_rendered_text_not_emptyraised a descriptiveUserTemplateError, butsanitize_user_exceptionsthen replaced the message with the generic one.UndefinedError(e.g.{{ person.address.street }}whereaddressis missing): ajinja2.exceptions.UndefinedErrorleaked all the way up because the sanitizer only caughtUserTemplateError/TemplateSyntaxError.Both paths now raise a new
EmptyTemplateRenderError(UserTemplateError)subclass that the sanitizer is explicitly told to pass through. The exception carries an actionable, row-level message naming the offending chain plus copy-pasteable Jinja conditional and SkipConfig remediation patterns:The "gate" expression in the suggestions stops one accessor past the resolvable prefix so it stays safe to evaluate (avoiding a fresh
UndefinedErroron the suggested fix itself).What changed
packages/data-designer-engine/src/data_designer/engine/processing/ginja/:exceptions.py— newEmptyTemplateRenderErrorsubclass.ast.py— new public helpersast_extract_access_chainsandresolve_access_chain(plus anAccessChaintype alias).environment.py:_assert_rendered_text_not_empty/validate_rendered_textthread the template + record through and raise the new error with the per-row diagnostic.UserTemplateSandboxEnvironment.safe_rendercatchesUndefinedErrorand converts it to the sameEmptyTemplateRenderError.NativeJinjaSandboxEnvironment.render_templatemirrors the conversion for consistency.sanitize_user_exceptionsbypassesEmptyTemplateRenderError(same treatment asUserTemplateUnsupportedFiltersError).End-to-end propagation already prepends the column name via
_run_batch, so no other layers needed changes.Test plan
test_empty_render_raises_empty_template_render_error_with_culprit_chain,test_undefined_nested_attr_raises_empty_template_render_error_with_safe_gate).NativeJinjaSandboxEnvironmentalso convertsUndefinedError.ast_extract_access_chains(plain text, single name, nested attr, subscript, mixed, dynamic subscript skipped, loop/conditional context) andresolve_access_chain(resolved, partially-resolved, type-mismatch cases).match="invalid"tomatch="empty"to reflect the new (more informative) message.uv run pytest packages/data-designer-engine/tests/engine/processing/ginja/— 95 passed.uv run ruff format+uv run ruff check— clean.