Escape the index note when structuring heterogeneous tuples - #777
Merged
Tinche merged 1 commit intoSep 14, 2026
Merged
Conversation
`make_hetero_tuple_structure_fn` builds the per-index note by interpolating
`str(cl)` into a single-quoted string literal in the generated source, at
`src/cattrs/gen/__init__.py` line 903 on main:
`f"__c_ivn('Structuring {cl} @ index {ix}', {ix}, {type_name})]"`. When a type
argument has a quote in its `repr`, such as `Literal["a"]`, the quote closes
the literal early and compiling the hook raises `SyntaxError` before any data
is looked at. This only affects the generated path, so `Converter()` fails
where `BaseConverter()` and `Converter(detailed_validation=False)` succeed.
`NamedTuple`s structure through the same factory, so they crash the same way.
This is the same class of bug as python-attrs#769 and python-attrs#771, and the fix is the same shape
already used in `src/cattrs/gen/typeddicts.py` line 394: build the note text in
Python and embed it with `repr`. The note text is byte-identical to before, so
nothing that reads `IterableValidationNote` changes.
The test covers a quote from `Literal`, the same quote nested inside a `List`,
an `Annotated` whose metadata repr contains both quote characters, and a
`NamedTuple` field, then triggers a real validation failure and checks the note
reads exactly as it does for a tuple whose arguments have no quotes. The
expected note is built from the same type expression so the assertion does not
depend on how a given Python version renders typing objects.
Member
|
Ugh :/ Nice find. I suppose this should be released as 26.2.1 sooner rather than later. |
Member
|
Thanks! |
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.
On the default
Converter, structuring a heterogeneous tuple whose type arguments have a quote in theirreprraisesSyntaxErrorbefore any data is looked at.Converter().structure(["a", 1], tuple[Literal["a"], int])fails, whileBaseConverter()andConverter(detailed_validation=False)handle the same call fine, so whether it works depends only on which converter you reach for.NamedTuples go through the same factory, so aNamedTuplewith aLiteralfield fails too, which is the more likely way to hit this.The cause is in
make_hetero_tuple_structure_fn,src/cattrs/gen/__init__.pyline 903 on main, which interpolatesstr(cl)into a single-quoted string literal in the generated source:f"__c_ivn('Structuring {cl} @ index {ix}', {ix}, {type_name})]".str(tuple[Literal["a"], int])istuple[typing.Literal['a'], int], so the emitted line isand compiling the hook fails on it.
This is a regression in 26.2.0. Before #737 heterogeneous tuples went through
_structure_tuple, which builds the note at runtime;BaseConverterstill registers that path, which is why it is unaffected. It is also the same class of bug as #769 and #771, andsrc/cattrs/gen/typeddicts.pyline 394 already uses exactly the fix applied here.Reproducer:
On main, Python 3.11.15:
The wording of the
SyntaxErrorand the generated module name differ on other versions. On this branch all six lines print the structured value.The fix builds the note text in Python and embeds it with
repr, one new line and one changed line inmake_hetero_tuple_structure_fn. The note text is byte-identical to what was emitted before, so nothing that readsIterableValidationNotechanges, and no new branch is introduced. The test covers a quote fromLiteral, the same quote nested inside aList, anAnnotatedwhose metadata repr contains both quote characters, and aNamedTuplefield, then triggers a genuine validation failure and checks the note reads exactly as it does for a tuple whose arguments have no quotes:tuple[int, int]produces the same note on main and on the branch. It reuses the name oftest_type_names_with_quotesintests/test_gen_dict.py, which covers the same class of bug in the dict path. It builds its ownConverter()instead of using the file'sgenconverterfixture because the bug is specific to the detailed validation generated path, and it builds the expected note from the same type expression so the assertion does not depend on how a given Python version renders typing objects.Verification, all on the branch unless stated otherwise:
tests/test_tuples.pywith only thesrcchange reverted gives 1 failed, 16 passed, the failure being theSyntaxErrorabove raised while compiling the generated hook. With the change in place the file is 17 passed.The full suite on 3.11.15 is 867 passed, 15 xfailed.
tests/preconfandtests/test_preconf.pywere excluded because the optional serialization libraries are not installed in the environment I ran in; the change touches no preconf code.ruff check src/ tests benchgives "All checks passed!" andruff format --check src tests docs/conf.pygives "115 files already formatted", with ruff 0.15.6, the version pinned inuv.lock.