Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (unreleased)

- Add a `use_alias` parameter to {class}`cattrs.Converter`.
{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))

## 25.1.1 (2025-06-04)

- Fixed `AttributeError: no attribute '__parameters__'` while structuring attrs classes that inherit from parametrized generic aliases from `collections.abc`.
Expand Down
9 changes: 9 additions & 0 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,7 @@ class Converter(BaseConverter):
"forbid_extra_keys",
"omit_if_default",
"type_overrides",
"use_alias",
)

def __init__(
Expand All @@ -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
Expand All @@ -1058,12 +1060,15 @@ 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*
.. 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*
Comment thread
Tinche marked this conversation as resolved.
"""
super().__init__(
dict_factory=dict_factory,
Expand All @@ -1076,6 +1081,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()
Expand Down Expand Up @@ -1299,6 +1305,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,
)

Expand Down Expand Up @@ -1377,6 +1384,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.

Expand Down Expand Up @@ -1416,6 +1424,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(
Expand Down
30 changes: 26 additions & 4 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand All @@ -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__
Expand All @@ -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)
Expand Down Expand Up @@ -217,7 +224,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]]:
Expand All @@ -237,11 +244,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
Comment thread
Tinche marked this conversation as resolved.
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)

Expand Down Expand Up @@ -289,7 +302,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]:
Expand All @@ -315,6 +328,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__
Expand Down Expand Up @@ -350,6 +366,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":
Expand Down Expand Up @@ -682,7 +701,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]:
Expand Down Expand Up @@ -714,6 +733,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 = {}
Expand Down
26 changes: 26 additions & 0 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,32 @@ def test_forbid_extra_keys(cls_and_vals):
assert cve.value.exceptions[0].extra_fields == {bad_key}


@pytest.mark.parametrize("use_alias", [True, False])
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)
unstructured = converter.unstructure(inst)
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
if fld.name != fld.alias:
assert fld.alias not in unstructured
converter.structure(unstructured, C)


@given(simple_typed_attrs(defaults=True))
def test_forbid_extra_keys_defaults(attr_and_vals):
"""
Expand Down