diff --git a/HISTORY.md b/HISTORY.md index cafbeec2..f81acc7a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,6 +13,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ## NEXT (UNRELEASED) +- Fix heterogeneous tuples and `NamedTuple`s with a member type containing a quote in its `repr`, like `tuple[Literal["a"], int]`, crashing structuring code generation with `SyntaxError`; the index note is now embedded with `repr`. + ([#777](https://github.com/python-attrs/cattrs/pull/777)) - Fix {func}`transform_error ` listing the extra keys of a `ForbiddenExtraKeysError` in set iteration order, which made the message differ between runs; the keys are now sorted, like the error's own `__str__` already sorts them. ([#776](https://github.com/python-attrs/cattrs/pull/776)) diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py index d5e1772a..43e1559f 100644 --- a/src/cattrs/gen/__init__.py +++ b/src/cattrs/gen/__init__.py @@ -892,6 +892,7 @@ def make_hetero_tuple_structure_fn( invocation = f"{struct_handler_name}(o[{ix}])" else: invocation = f"{struct_handler_name}(o[{ix}], {type_name})" + note = f"Structuring {cl} @ index {ix}" lines.extend( [ f" if len(o) > {ix}:", @@ -900,7 +901,7 @@ def make_hetero_tuple_structure_fn( " except Exception as e:", ( f" e.__notes__ = [*getattr(e, '__notes__', []), " - f"__c_ivn('Structuring {cl} @ index {ix}', {ix}, {type_name})]" + f"__c_ivn({note!r}, {ix}, {type_name})]" ), " errors.append(e)", ] diff --git a/tests/test_tuples.py b/tests/test_tuples.py index 7ea69072..fcd57306 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,6 +1,6 @@ """Tests for tuples of all kinds.""" -from typing import List, NamedTuple, Tuple +from typing import Annotated, List, Literal, NamedTuple, Tuple from attrs import Factory, define from pytest import raises @@ -32,6 +32,33 @@ def test_structuring_invalid_tuples(converter: BaseConverter): converter.structure(["1", 2, "c"], tuple[int, int, int]) +def test_type_names_with_quotes(): + """Types with quote characters in their reprs should work. + + The index note is baked into the generated source, and it used to be + interpolated inside single quotes, so `Literal["a"]` produced invalid + source and raised `SyntaxError` before anything was structured. + """ + + class NT(NamedTuple): + a: Literal["a"] + b: int + + c = Converter() + + assert c.structure(["a", 1], Tuple[Literal["a"], int]) == ("a", 1) + assert c.structure([["a"], 1], Tuple[List[Literal["a"]], int]) == (["a"], 1) + assert c.structure([1, 2], Tuple[Annotated[int, "it's"], int]) == (1, 2) + assert c.structure(["a", 1], NT) == NT("a", 1) + + with raises(IterableValidationError) as exc_info: + c.structure(["b", 1], Tuple[Literal["a"], int]) + + assert exc_info.value.exceptions[0].__notes__ == [ + f"Structuring {Tuple[Literal['a'], int]} @ index 0" + ] + + def test_simple_hetero_tuples(genconverter: Converter): """Simple heterogenous tuples work.