From 29f6adbfa00e036c2502cd7c983c1e62c1ebf31c Mon Sep 17 00:00:00 2001 From: Adrian Sosic Date: Tue, 24 Jun 2025 08:13:35 +0200 Subject: [PATCH 01/10] Extract use_alias from converter by default --- src/cattrs/converters.py | 7 +++++++ src/cattrs/gen/__init__.py | 16 ++++++++++++++-- src/cattrs/gen/typeddicts.py | 7 +++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 9a54476b..84e463fe 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -1034,6 +1034,7 @@ class Converter(BaseConverter): "forbid_extra_keys", "omit_if_default", "type_overrides", + "use_alias", ) def __init__( @@ -1050,6 +1051,7 @@ def __init__( structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error( None, t ), + use_alias: bool = False, ): """ :param detailed_validation: Whether to use a slightly slower mode for detailed @@ -1064,6 +1066,7 @@ def __init__( .. versionchanged:: 24.2.0 The default `structure_fallback_factory` now raises errors for missing handlers more eagerly, surfacing problems earlier. + .. versionadded:: 25.2.0 *use_alias* """ super().__init__( dict_factory=dict_factory, @@ -1076,6 +1079,7 @@ def __init__( self.omit_if_default = omit_if_default self.forbid_extra_keys = forbid_extra_keys self.type_overrides = dict(type_overrides) + self.use_alias = use_alias unstruct_collection_overrides = { get_origin(k) or k: v for k, v in unstruct_collection_overrides.items() @@ -1299,6 +1303,7 @@ def gen_structure_attrs_fromdict( _cattrs_forbid_extra_keys=self.forbid_extra_keys, _cattrs_prefer_attrib_converters=self._prefer_attrib_converters, _cattrs_detailed_validation=self.detailed_validation, + _cattrs_use_alias=self.use_alias, **attrib_overrides, ) @@ -1377,6 +1382,7 @@ def copy( unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None, prefer_attrib_converters: bool | None = None, detailed_validation: bool | None = None, + use_alias: bool | None = None, ) -> Self: """Create a copy of the converter, keeping all existing custom hooks. @@ -1416,6 +1422,7 @@ def copy( if detailed_validation is not None else self.detailed_validation ), + use_alias=(use_alias if use_alias is not None else self.use_alias), ) self._unstructure_func.copy_to( diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py index 3afa3b9b..fe30b367 100644 --- a/src/cattrs/gen/__init__.py +++ b/src/cattrs/gen/__init__.py @@ -217,7 +217,7 @@ def make_dict_unstructure_fn( converter: BaseConverter, _cattrs_omit_if_default: bool = False, _cattrs_use_linecache: bool = True, - _cattrs_use_alias: bool = False, + _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter", _cattrs_include_init_false: bool = False, **kwargs: AttributeOverride, ) -> Callable[[T], dict[str, Any]]: @@ -237,11 +237,17 @@ def make_dict_unstructure_fn( .. versionadded:: 23.2.0 *_cattrs_use_alias* .. versionadded:: 23.2.0 *_cattrs_include_init_false* + .. versionchanged:: 25.2.0 + The `_cattrs_use_alias` parameter takes its value from the given converter + by default. """ origin = get_origin(cl) attrs = adapted_fields(origin or cl) # type: ignore mapping = {} + if _cattrs_use_alias == "from_converter": + # BaseConverter doesn't have it so we're careful. + _cattrs_use_alias = getattr(converter, "use_alias", False) if is_generic(cl): mapping = generate_mapping(cl, mapping) @@ -289,7 +295,7 @@ def make_dict_structure_fn_from_attrs( bool | Literal["from_converter"] ) = "from_converter", _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter", - _cattrs_use_alias: bool = False, + _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter", _cattrs_include_init_false: bool = False, **kwargs: AttributeOverride, ) -> SimpleStructureHook[Mapping[str, Any], T]: @@ -315,6 +321,9 @@ def make_dict_structure_fn_from_attrs( will be included. .. versionadded:: 24.1.0 + .. versionchanged:: 25.2.0 + The `_cattrs_use_alias` parameter takes its value from the given converter + by default. """ cl_name = cl.__name__ @@ -350,6 +359,9 @@ def make_dict_structure_fn_from_attrs( if _cattrs_forbid_extra_keys == "from_converter": # BaseConverter doesn't have it so we're careful. _cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False) + if _cattrs_use_alias == "from_converter": + # BaseConverter doesn't have it so we're careful. + _cattrs_use_alias = getattr(converter, "use_alias", False) if _cattrs_detailed_validation == "from_converter": _cattrs_detailed_validation = converter.detailed_validation if _cattrs_prefer_attrib_converters == "from_converter": diff --git a/src/cattrs/gen/typeddicts.py b/src/cattrs/gen/typeddicts.py index bca38a54..eebbc79e 100644 --- a/src/cattrs/gen/typeddicts.py +++ b/src/cattrs/gen/typeddicts.py @@ -234,6 +234,7 @@ def make_dict_structure_fn( _cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter", _cattrs_use_linecache: bool = True, _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter", + _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter", **kwargs: AttributeOverride, ) -> Callable[[dict, Any], Any]: """Generate a specialized dict structuring function for typed dicts. @@ -252,6 +253,9 @@ def make_dict_structure_fn( .. versionchanged:: 23.2.0 The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters take their values from the given converter by default. + .. versionchanged:: 25.2.0 + The `_cattrs_use_alias` parameter takes its value from the given converter + by default. """ mapping = {} @@ -300,6 +304,9 @@ def make_dict_structure_fn( if _cattrs_forbid_extra_keys == "from_converter": # BaseConverter doesn't have it so we're careful. _cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False) + if _cattrs_use_alias == "from_converter": + # BaseConverter doesn't have it so we're careful. + _cattrs_use_alias = getattr(converter, "use_alias", False) if _cattrs_detailed_validation == "from_converter": _cattrs_detailed_validation = converter.detailed_validation From f7e1dce3f5c2a588477d2e9ebbbb3174cf3f49f2 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 24 Jun 2025 10:22:31 +0200 Subject: [PATCH 02/10] Add forgotten default change --- src/cattrs/gen/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py index fe30b367..6951e11e 100644 --- a/src/cattrs/gen/__init__.py +++ b/src/cattrs/gen/__init__.py @@ -694,7 +694,7 @@ def make_dict_structure_fn( bool | Literal["from_converter"] ) = "from_converter", _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter", - _cattrs_use_alias: bool = False, + _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter", _cattrs_include_init_false: bool = False, **kwargs: AttributeOverride, ) -> SimpleStructureHook[Mapping[str, Any], T]: @@ -726,6 +726,9 @@ def make_dict_structure_fn( .. versionchanged:: 24.1.0 The `_cattrs_prefer_attrib_converters` parameter takes its value from the given converter by default. + .. versionchanged:: 25.2.0 + The `_cattrs_use_alias` parameter takes its value from the given converter + by default. """ mapping = {} From 3387f3cdd8ba96ef921f454f8e32af777610aaff Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 24 Jun 2025 12:48:16 +0200 Subject: [PATCH 03/10] Add another forgotten default change --- src/cattrs/gen/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py index 6951e11e..62e3a37a 100644 --- a/src/cattrs/gen/__init__.py +++ b/src/cattrs/gen/__init__.py @@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs( typevar_map: dict[str, Any] = {}, _cattrs_omit_if_default: bool = False, _cattrs_use_linecache: bool = True, - _cattrs_use_alias: bool = False, + _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter", _cattrs_include_init_false: bool = False, **kwargs: AttributeOverride, ) -> Callable[[T], dict[str, Any]]: @@ -96,6 +96,9 @@ def make_dict_unstructure_fn_from_attrs( will be included. .. versionadded:: 24.1.0 + .. versionchanged:: 25.2.0 + The `_cattrs_use_alias` parameter takes its value from the given converter + by default. """ fn_name = "unstructure_" + cl.__name__ @@ -104,6 +107,10 @@ def make_dict_unstructure_fn_from_attrs( invocation_lines = [] internal_arg_parts = {} + if _cattrs_use_alias == "from_converter": + # BaseConverter doesn't have it so we're careful. + _cattrs_use_alias = getattr(converter, "use_alias", False) + for a in attrs: attr_name = a.name override = kwargs.get(attr_name, neutral) From 40cf4f4cb4da93e8438946e4b4cbae0f6be682d0 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 24 Jun 2025 12:51:50 +0200 Subject: [PATCH 04/10] Update changelog --- HISTORY.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 562d111e..e35621cf 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,15 @@ The third number is for emergencies when we need to start branches for older rel Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md). +## 25.2.0 (2025-06-24) + +- **Potentially breaking**: {class}`cattrs.Converter` now accepts a `use_alias` parameters. + {py:func}`cattrs.gen.make_dict_unstructure_fn_from_attrs`, {py:func}`cattrs.gen.make_dict_unstructure_fn`, + {py:func}`cattrs.gen.make_dict_structure_fn_from_attrs`, {py:func}`cattrs.gen.make_dict_structure_fn` + and {py:func}`cattrs.gen.typeddicts.make_dict_structure_fn` will use the value for the `use_alias` parameter from the given converter by default now. + If you're using these functions directly, the old behavior can be restored by passing in the desired value directly. + ([#596](https://github.com/python-attrs/cattrs/issues/596) [#660](https://github.com/python-attrs/cattrs/pull/660)) + ## 25.1.1 (2025-06-04) - Fixed `AttributeError: no attribute '__parameters__'` while structuring attrs classes that inherit from parametrized generic aliases from `collections.abc`. From 33a84668031b150510e9f4a1453e6a42ff1df526 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 25 Jun 2025 17:12:28 +0200 Subject: [PATCH 05/10] Fix changelog --- HISTORY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index e35621cf..29b50616 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,9 +11,9 @@ The third number is for emergencies when we need to start branches for older rel Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md). -## 25.2.0 (2025-06-24) +## 25.2.0 (unreleased) -- **Potentially breaking**: {class}`cattrs.Converter` now accepts a `use_alias` parameters. +- Add a `use_alias` parameter to {class}`cattrs.Converter`. {py:func}`cattrs.gen.make_dict_unstructure_fn_from_attrs`, {py:func}`cattrs.gen.make_dict_unstructure_fn`, {py:func}`cattrs.gen.make_dict_structure_fn_from_attrs`, {py:func}`cattrs.gen.make_dict_structure_fn` and {py:func}`cattrs.gen.typeddicts.make_dict_structure_fn` will use the value for the `use_alias` parameter from the given converter by default now. From 1ba74f17b1de6113c271e25db9c81f253a36214d Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 25 Jun 2025 17:19:18 +0200 Subject: [PATCH 06/10] Drop unneeded argument --- src/cattrs/gen/typeddicts.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/cattrs/gen/typeddicts.py b/src/cattrs/gen/typeddicts.py index eebbc79e..bca38a54 100644 --- a/src/cattrs/gen/typeddicts.py +++ b/src/cattrs/gen/typeddicts.py @@ -234,7 +234,6 @@ def make_dict_structure_fn( _cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter", _cattrs_use_linecache: bool = True, _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter", - _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter", **kwargs: AttributeOverride, ) -> Callable[[dict, Any], Any]: """Generate a specialized dict structuring function for typed dicts. @@ -253,9 +252,6 @@ def make_dict_structure_fn( .. versionchanged:: 23.2.0 The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters take their values from the given converter by default. - .. versionchanged:: 25.2.0 - The `_cattrs_use_alias` parameter takes its value from the given converter - by default. """ mapping = {} @@ -304,9 +300,6 @@ def make_dict_structure_fn( if _cattrs_forbid_extra_keys == "from_converter": # BaseConverter doesn't have it so we're careful. _cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False) - if _cattrs_use_alias == "from_converter": - # BaseConverter doesn't have it so we're careful. - _cattrs_use_alias = getattr(converter, "use_alias", False) if _cattrs_detailed_validation == "from_converter": _cattrs_detailed_validation = converter.detailed_validation From 2cc251fabd973ef0b30d42a8b514252d5159c838 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 25 Jun 2025 20:19:40 +0200 Subject: [PATCH 07/10] Add test --- tests/test_converter.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_converter.py b/tests/test_converter.py index b071ae28..adc8da88 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -146,6 +146,25 @@ def test_forbid_extra_keys(cls_and_vals): assert cve.value.exceptions[0].extra_fields == {bad_key} +@given(cls_and_vals=simple_typed_classes()) +@pytest.mark.parametrize("use_alias", [True, False]) +def test_use_alias(cls_and_vals, use_alias): + """ + (Un)structuring with use_alias=True generates/uses aliased keys. + """ + converter = Converter(use_alias=use_alias) + cl, vals, kwargs = cls_and_vals + flds = fields(cl) + inst = cl(*vals, **kwargs) + unstructured = converter.unstructure(inst) + for fld in flds: + if use_alias: + assert fld.alias in unstructured + else: + assert fld.name in unstructured + converter.structure(unstructured, cl) + + @given(simple_typed_attrs(defaults=True)) def test_forbid_extra_keys_defaults(attr_and_vals): """ From 79311174d9004ab09dd395fc7df868ea6ad49685 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 25 Jun 2025 20:32:13 +0200 Subject: [PATCH 08/10] Add argument docstring --- src/cattrs/converters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 84e463fe..aa895552 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -1060,6 +1060,8 @@ def __init__( registered unstructuring hooks match. :param structure_fallback_factory: A hook factory to be called when no registered structuring hooks match. + :param use_alias: Whether to use the field alias instead of the field name as + the un/structured dictionary key by default. .. versionadded:: 23.2.0 *unstructure_fallback_factory* .. versionadded:: 23.2.0 *structure_fallback_factory* From c18e66bc8a2f8adca65167f9895237d2c941fb58 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 25 Jun 2025 20:47:02 +0200 Subject: [PATCH 09/10] Avoid hypothesis usage --- tests/test_converter.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index adc8da88..9f91dc53 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -146,23 +146,30 @@ def test_forbid_extra_keys(cls_and_vals): assert cve.value.exceptions[0].extra_fields == {bad_key} -@given(cls_and_vals=simple_typed_classes()) @pytest.mark.parametrize("use_alias", [True, False]) -def test_use_alias(cls_and_vals, use_alias): +def test_use_alias(use_alias): """ (Un)structuring with use_alias=True generates/uses aliased keys. """ + + @define + class C: + a: int = field(default=0, alias="_alias") + b: int = field(default=0) + + inst = C() converter = Converter(use_alias=use_alias) - cl, vals, kwargs = cls_and_vals - flds = fields(cl) - inst = cl(*vals, **kwargs) unstructured = converter.unstructure(inst) - for fld in flds: + for fld in fields(C): if use_alias: assert fld.alias in unstructured + if fld.name != fld.alias: + assert fld.name not in unstructured else: assert fld.name in unstructured - converter.structure(unstructured, cl) + if fld.name != fld.alias: + assert fld.alias not in unstructured + converter.structure(unstructured, C) @given(simple_typed_attrs(defaults=True)) From 0c732796ff4d21c8abb694a9f0db272b53f9fe93 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 25 Jun 2025 22:52:44 +0200 Subject: [PATCH 10/10] Replace {py:func} with {func} --- HISTORY.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 29b50616..2c56c85d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,9 +14,9 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ## 25.2.0 (unreleased) - Add a `use_alias` parameter to {class}`cattrs.Converter`. - {py:func}`cattrs.gen.make_dict_unstructure_fn_from_attrs`, {py:func}`cattrs.gen.make_dict_unstructure_fn`, - {py:func}`cattrs.gen.make_dict_structure_fn_from_attrs`, {py:func}`cattrs.gen.make_dict_structure_fn` - and {py:func}`cattrs.gen.typeddicts.make_dict_structure_fn` will use the value for the `use_alias` parameter from the given converter by default now. + {func}`cattrs.gen.make_dict_unstructure_fn_from_attrs`, {func}`cattrs.gen.make_dict_unstructure_fn`, + {func}`cattrs.gen.make_dict_structure_fn_from_attrs`, {func}`cattrs.gen.make_dict_structure_fn` + and {func}`cattrs.gen.typeddicts.make_dict_structure_fn` will use the value for the `use_alias` parameter from the given converter by default now. If you're using these functions directly, the old behavior can be restored by passing in the desired value directly. ([#596](https://github.com/python-attrs/cattrs/issues/596) [#660](https://github.com/python-attrs/cattrs/pull/660))