Bump pydantic-handlebars to 0.2.1 and drop escape-coupling shims#1963
Merged
dmontagu merged 1 commit intoMay 25, 2026
Conversation
eecfd4a to
3295ede
Compare
`pydantic-handlebars 0.2.1` matches the Handlebars.js spec for runs of
backslashes preceding an open delimiter: N backslashes contributes
`N // 2` literal `\` characters, with parity deciding whether the
mustache renders (even) or stays literal (odd). That made the logfire
`_HAS_REFERENCE` regex (`(?<!\\)@\{` — "any preceding backslash means
escaped") actively wrong for even-backslash cases like `\\@{x}@`: we
returned `has_references == False`, took the short-circuit path, and
ended up with literal `\@{x}@` in output instead of `\X`.
Fix is also a cleanup. The renderer is now the single source of truth
for escape semantics, so the regex gate can degrade to a plain
substring check (`'@{' in v`):
- `has_references` is now `_HAS_OPEN_DELIM in serialized_value`. No
more escape detection in the regex layer. Variable-width lookbehind
would have been required to do this correctly in `re`, and is
unnecessary now.
- `_render_value` no longer does the manual `\@{` → `@{` substitution
when "no refs" — strings with no `@{` at all pass through unchanged;
strings with `@{` (escaped or not) route through the renderer, which
handles parity-correct unescape.
- `expand_references` drops the early-return-with-`_unescape_serialized`
branch. Even when no real refs resolve, `_render_value` still walks
the structure so escape-only strings get parity-correct handling.
- `_unescape_serialized` is unused and deleted.
## Tests
- Updated two pre-existing tests that asserted the *old* (buggy by spec)
behaviour:
- `test_keyword_block_references_are_ignored` now expects
`@{#if this}@yES@{/if}@` to render `yes` (real Handlebars `if` block
against the empty context, which is truthy). `composed == []` still
asserts that no variable lookups are performed.
- `test_referenced_escaped_reference_is_preserved` (renamed in spirit)
now expects `\@{not_a_ref}@` to render as `@{not_a_ref}@` — the
backslash is the escape marker per the spec.
- New `test_backslash_run_parity_under_composition` covers the
parity table (1, 2, 3, 4 backslashes) end-to-end through
`Variable.get()` so the previously-buggy even-backslash cases are
pinned.
447 variable / composition / template / push tests pass.
3295ede to
7e856d7
Compare
221bda9
into
feature/variable-composition-tier1-fixes
14 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1962. Bumps the dep and cleans up the now-stale escape shims that #1962 left a coupling comment about.
Why now
pydantic-handlebars 0.2.1 ships spec-compliant escape semantics: a run of N backslashes preceding an open delimiter contributes
N // 2literal\characters, with parity deciding whether the mustache renders (even) or stays literal (odd).That makes the logfire-side
_HAS_REFERENCEregex ((?<!\\)@\{— "any preceding backslash means escaped") actively wrong for even-backslash cases. With 0.2.1:\@{x}@@{x}@@{x}@✓\\@{x}@\@{x}@\X\\\@{x}@\\@{x}@\@{x}@✓\\\\@{x}@\\\@{x}@\\XThe regex returned
has_references == Falsefor any\@{prefix, so even-backslash cases short-circuited the composition path and emerged unrendered. The fix routes everything containing@{through the renderer, which now handles parity correctly.What changed
pyproject.toml:pydantic-handlebars>=0.2.0→>=0.2.1in both the[variables]extra and the dev group.composition.pycleanup:has_referencesdrops the regex; becomes'@{' in serialized_value. Doing parity-correct detection in pure regex would need a variable-width lookbehind we can't write portably inreon 3.10/3.11; with the renderer as the source of truth it isn't needed._render_valueno longer does the manual\@{→@{substitution for "no refs" strings. Strings with no@{pass through unchanged; strings with@{route through the renderer.expand_referencesdrops the special-case early return that called_unescape_serializedwhen no real refs resolved._render_valuehandles escape-only strings on its own walk._unescape_serializedis unused and deleted._HAS_REFERENCEregex deleted (with the coupling-note comment).Tests
Two pre-existing tests asserted the old (buggy by spec) behaviour and have been updated to assert the correct behaviour:
test_keyword_block_references_are_ignored—@{#if this}@yes@{/if}@now rendersyes(real Handlebarsifblock against the empty context, which is truthy). Thecomposed == []assertion still pins the property thatthisandifaren't resolved as variable lookups.test_referenced_escaped_reference_is_preserved—\@{not_a_ref}@now renders as@{not_a_ref}@(escape consumed per spec).New test
test_backslash_run_parity_under_compositionpins the parity table (N = 1, 2, 3, 4) end-to-end throughVariable.get()so the previously-buggy even-backslash cases stay correct.447 variable / composition / template / push tests pass. Pyright + ruff format + check clean.
Merge ordering
Targets
feature/variable-composition-tier1-fixes(#1962's branch). Once #1962 merges → #1954 → main, this rebases onto main.