Skip to content

Commit 0c226c2

Browse files
authored
Extract use_alias from converter by default (#660)
* Extract use_alias from converter by default * Add forgotten default change * Add another forgotten default change * Update changelog * Fix changelog * Drop unneeded argument * Add test * Add argument docstring * Avoid hypothesis usage * Replace {py:func} with {func}
1 parent 9894095 commit 0c226c2

4 files changed

Lines changed: 70 additions & 4 deletions

File tree

HISTORY.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ The third number is for emergencies when we need to start branches for older rel
1111

1212
Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).
1313

14+
## 25.2.0 (unreleased)
15+
16+
- Add a `use_alias` parameter to {class}`cattrs.Converter`.
17+
{func}`cattrs.gen.make_dict_unstructure_fn_from_attrs`, {func}`cattrs.gen.make_dict_unstructure_fn`,
18+
{func}`cattrs.gen.make_dict_structure_fn_from_attrs`, {func}`cattrs.gen.make_dict_structure_fn`
19+
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.
20+
If you're using these functions directly, the old behavior can be restored by passing in the desired value directly.
21+
([#596](https://github.com/python-attrs/cattrs/issues/596) [#660](https://github.com/python-attrs/cattrs/pull/660))
22+
1423
## 25.1.1 (2025-06-04)
1524

1625
- Fixed `AttributeError: no attribute '__parameters__'` while structuring attrs classes that inherit from parametrized generic aliases from `collections.abc`.

src/cattrs/converters.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,7 @@ class Converter(BaseConverter):
10341034
"forbid_extra_keys",
10351035
"omit_if_default",
10361036
"type_overrides",
1037+
"use_alias",
10371038
)
10381039

10391040
def __init__(
@@ -1050,6 +1051,7 @@ def __init__(
10501051
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
10511052
None, t
10521053
),
1054+
use_alias: bool = False,
10531055
):
10541056
"""
10551057
:param detailed_validation: Whether to use a slightly slower mode for detailed
@@ -1058,12 +1060,15 @@ def __init__(
10581060
registered unstructuring hooks match.
10591061
:param structure_fallback_factory: A hook factory to be called when no
10601062
registered structuring hooks match.
1063+
:param use_alias: Whether to use the field alias instead of the field name as
1064+
the un/structured dictionary key by default.
10611065
10621066
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
10631067
.. versionadded:: 23.2.0 *structure_fallback_factory*
10641068
.. versionchanged:: 24.2.0
10651069
The default `structure_fallback_factory` now raises errors for missing handlers
10661070
more eagerly, surfacing problems earlier.
1071+
.. versionadded:: 25.2.0 *use_alias*
10671072
"""
10681073
super().__init__(
10691074
dict_factory=dict_factory,
@@ -1076,6 +1081,7 @@ def __init__(
10761081
self.omit_if_default = omit_if_default
10771082
self.forbid_extra_keys = forbid_extra_keys
10781083
self.type_overrides = dict(type_overrides)
1084+
self.use_alias = use_alias
10791085

10801086
unstruct_collection_overrides = {
10811087
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
@@ -1299,6 +1305,7 @@ def gen_structure_attrs_fromdict(
12991305
_cattrs_forbid_extra_keys=self.forbid_extra_keys,
13001306
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
13011307
_cattrs_detailed_validation=self.detailed_validation,
1308+
_cattrs_use_alias=self.use_alias,
13021309
**attrib_overrides,
13031310
)
13041311

@@ -1377,6 +1384,7 @@ def copy(
13771384
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
13781385
prefer_attrib_converters: bool | None = None,
13791386
detailed_validation: bool | None = None,
1387+
use_alias: bool | None = None,
13801388
) -> Self:
13811389
"""Create a copy of the converter, keeping all existing custom hooks.
13821390
@@ -1416,6 +1424,7 @@ def copy(
14161424
if detailed_validation is not None
14171425
else self.detailed_validation
14181426
),
1427+
use_alias=(use_alias if use_alias is not None else self.use_alias),
14191428
)
14201429

14211430
self._unstructure_func.copy_to(

src/cattrs/gen/__init__.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs(
7474
typevar_map: dict[str, Any] = {},
7575
_cattrs_omit_if_default: bool = False,
7676
_cattrs_use_linecache: bool = True,
77-
_cattrs_use_alias: bool = False,
77+
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
7878
_cattrs_include_init_false: bool = False,
7979
**kwargs: AttributeOverride,
8080
) -> Callable[[T], dict[str, Any]]:
@@ -96,6 +96,9 @@ def make_dict_unstructure_fn_from_attrs(
9696
will be included.
9797
9898
.. versionadded:: 24.1.0
99+
.. versionchanged:: 25.2.0
100+
The `_cattrs_use_alias` parameter takes its value from the given converter
101+
by default.
99102
"""
100103

101104
fn_name = "unstructure_" + cl.__name__
@@ -104,6 +107,10 @@ def make_dict_unstructure_fn_from_attrs(
104107
invocation_lines = []
105108
internal_arg_parts = {}
106109

110+
if _cattrs_use_alias == "from_converter":
111+
# BaseConverter doesn't have it so we're careful.
112+
_cattrs_use_alias = getattr(converter, "use_alias", False)
113+
107114
for a in attrs:
108115
attr_name = a.name
109116
override = kwargs.get(attr_name, neutral)
@@ -217,7 +224,7 @@ def make_dict_unstructure_fn(
217224
converter: BaseConverter,
218225
_cattrs_omit_if_default: bool = False,
219226
_cattrs_use_linecache: bool = True,
220-
_cattrs_use_alias: bool = False,
227+
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
221228
_cattrs_include_init_false: bool = False,
222229
**kwargs: AttributeOverride,
223230
) -> Callable[[T], dict[str, Any]]:
@@ -237,11 +244,17 @@ def make_dict_unstructure_fn(
237244
238245
.. versionadded:: 23.2.0 *_cattrs_use_alias*
239246
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
247+
.. versionchanged:: 25.2.0
248+
The `_cattrs_use_alias` parameter takes its value from the given converter
249+
by default.
240250
"""
241251
origin = get_origin(cl)
242252
attrs = adapted_fields(origin or cl) # type: ignore
243253

244254
mapping = {}
255+
if _cattrs_use_alias == "from_converter":
256+
# BaseConverter doesn't have it so we're careful.
257+
_cattrs_use_alias = getattr(converter, "use_alias", False)
245258
if is_generic(cl):
246259
mapping = generate_mapping(cl, mapping)
247260

@@ -289,7 +302,7 @@ def make_dict_structure_fn_from_attrs(
289302
bool | Literal["from_converter"]
290303
) = "from_converter",
291304
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
292-
_cattrs_use_alias: bool = False,
305+
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
293306
_cattrs_include_init_false: bool = False,
294307
**kwargs: AttributeOverride,
295308
) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -315,6 +328,9 @@ def make_dict_structure_fn_from_attrs(
315328
will be included.
316329
317330
.. versionadded:: 24.1.0
331+
.. versionchanged:: 25.2.0
332+
The `_cattrs_use_alias` parameter takes its value from the given converter
333+
by default.
318334
"""
319335

320336
cl_name = cl.__name__
@@ -350,6 +366,9 @@ def make_dict_structure_fn_from_attrs(
350366
if _cattrs_forbid_extra_keys == "from_converter":
351367
# BaseConverter doesn't have it so we're careful.
352368
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
369+
if _cattrs_use_alias == "from_converter":
370+
# BaseConverter doesn't have it so we're careful.
371+
_cattrs_use_alias = getattr(converter, "use_alias", False)
353372
if _cattrs_detailed_validation == "from_converter":
354373
_cattrs_detailed_validation = converter.detailed_validation
355374
if _cattrs_prefer_attrib_converters == "from_converter":
@@ -682,7 +701,7 @@ def make_dict_structure_fn(
682701
bool | Literal["from_converter"]
683702
) = "from_converter",
684703
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
685-
_cattrs_use_alias: bool = False,
704+
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
686705
_cattrs_include_init_false: bool = False,
687706
**kwargs: AttributeOverride,
688707
) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -714,6 +733,9 @@ def make_dict_structure_fn(
714733
.. versionchanged:: 24.1.0
715734
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
716735
converter by default.
736+
.. versionchanged:: 25.2.0
737+
The `_cattrs_use_alias` parameter takes its value from the given converter
738+
by default.
717739
"""
718740

719741
mapping = {}

tests/test_converter.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,32 @@ def test_forbid_extra_keys(cls_and_vals):
146146
assert cve.value.exceptions[0].extra_fields == {bad_key}
147147

148148

149+
@pytest.mark.parametrize("use_alias", [True, False])
150+
def test_use_alias(use_alias):
151+
"""
152+
(Un)structuring with use_alias=True generates/uses aliased keys.
153+
"""
154+
155+
@define
156+
class C:
157+
a: int = field(default=0, alias="_alias")
158+
b: int = field(default=0)
159+
160+
inst = C()
161+
converter = Converter(use_alias=use_alias)
162+
unstructured = converter.unstructure(inst)
163+
for fld in fields(C):
164+
if use_alias:
165+
assert fld.alias in unstructured
166+
if fld.name != fld.alias:
167+
assert fld.name not in unstructured
168+
else:
169+
assert fld.name in unstructured
170+
if fld.name != fld.alias:
171+
assert fld.alias not in unstructured
172+
converter.structure(unstructured, C)
173+
174+
149175
@given(simple_typed_attrs(defaults=True))
150176
def test_forbid_extra_keys_defaults(attr_and_vals):
151177
"""

0 commit comments

Comments
 (0)