Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Escape rename keys when generating (un)structuring code - #771

Merged
Tinche merged 3 commits into
python-attrs:mainfrom
winklemad:fix/escape-rename-keys-in-codegen
Sep 1, 2026
Merged

Tinche merged 3 commits into
python-attrs:mainfrom
winklemad:fix/escape-rename-keys-in-codegen

Conversation

@winklemad

Copy link
Copy Markdown
Contributor

Summary

A field's rename target is a dictionary key, so it can legitimately contain any character. But the generated (un)structuring code interpolates it into the source inside single quotes:

res['{kn}'] = ...
instance.x = o['{kn}']
if '{kn}' in o:

So a rename to a key containing a quote produces invalid source and blows up when it's compiled:

from attrs import define
from cattrs import Converter
from cattrs.gen import make_dict_unstructure_fn, override

@define
class A:
    b: int

c = Converter()
c.register_unstructure_hook(A, make_dict_unstructure_fn(A, c, b=override(rename="it's")))
c.unstructure(A(1))
# SyntaxError: unterminated string literal (<cattrs generated unstructure __main__.A>)

Fix

Interpolate the key with repr ({kn!r}) instead of hand-written single quotes, so any key string is embedded as a valid, correctly-escaped literal. This only affects the rename key (kn); attrs already rejects aliases that aren't valid identifiers, so the alias-based keys can't hit this.

After the change, it's, keys with double quotes, and keys that look like code all round-trip as plain string keys.

Tests

Added test_renaming_to_key_with_special_characters (structure + unstructure round-trip for a rename containing a quote and a couple of other awkward keys). It raises SyntaxError on main and passes here. Full test_gen_dict.py / test_gen.py stay green (63 passed).

The generated (un)structuring code interpolated a field's rename target into
the source inside single quotes (o['{kn}'], res['{kn}'], etc). A rename key is
an arbitrary dict key, so one containing a quote (for example "it's") produced
invalid source and raised SyntaxError. Interpolate the key with repr ({kn!r})
so any key string is embedded safely.

@Tinche Tinche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, left a comment. Thanks

Comment thread tests/test_gen_dict.py Outdated
@codspeed

codspeed Bot commented Aug 31, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 64 untouched benchmarks


Comparing winklemad:fix/escape-rename-keys-in-codegen (9ff7d7a) with main (f2e42f3)

Open in CodSpeed

@winklemad

Copy link
Copy Markdown
Contributor Author

Done, parametrized in df3e28f's follow-up — thanks!

@Tinche

Tinche commented Sep 1, 2026

Copy link
Copy Markdown
Member

Great, thanks!

@Tinche
Tinche merged commit 893351d into python-attrs:main Sep 1, 2026
14 checks passed
Tinche pushed a commit that referenced this pull request Sep 14, 2026
`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 #769 and #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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants