diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py index 62e3a37a..41d3c6e3 100644 --- a/src/cattrs/gen/__init__.py +++ b/src/cattrs/gen/__init__.py @@ -4,7 +4,7 @@ from collections.abc import Callable, Iterable, Mapping from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar -from attrs import NOTHING, Attribute, Factory +from attrs import NOTHING, Attribute, Converter, Factory from typing_extensions import NoDefault from .._compat import ( @@ -177,16 +177,27 @@ def make_dict_unstructure_fn_from_attrs( if isinstance(d, Factory): globs[def_name] = d.factory internal_arg_parts[def_name] = d.factory - if d.takes_self: - lines.append(f" if instance.{attr_name} != {def_name}(instance):") - else: - lines.append(f" if instance.{attr_name} != {def_name}():") - lines.append(f" res['{kn}'] = {invoke}") + def_str = f"{def_name}(instance)" if d.takes_self else f"{def_name}()" else: globs[def_name] = d internal_arg_parts[def_name] = d - lines.append(f" if instance.{attr_name} != {def_name}:") - lines.append(f" res['{kn}'] = {invoke}") + def_str = def_name + + c = a.converter + if c is not None: + conv_name = f"__c_conv_{attr_name}" + globs[conv_name] = c + internal_arg_parts[conv_name] = c + if isinstance(c, Converter): + field_name = f"__c_field_{attr_name}" + globs[field_name] = a + internal_arg_parts[field_name] = a + def_str = f"{conv_name}({def_str}, instance, {field_name})" + else: + def_str = f"{conv_name}({def_str})" + + lines.append(f" if instance.{attr_name} != {def_str}:") + lines.append(f" res['{kn}'] = {invoke}") else: # No default or no override. diff --git a/tests/test_converter.py b/tests/test_converter.py index 3c5cbe25..d2b7f6a1 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -16,6 +16,7 @@ Union, ) +import attrs import pytest from attrs import Factory, define, field, fields, has, make_class from hypothesis import assume, given @@ -339,6 +340,31 @@ class C: assert inst == converter.structure(unstructured, C) +@given(simple_typed_classes(defaults="always", allow_nan=False)) +def test_omit_default_with_attrs_converter_roundtrip(cl_and_vals): + """ + Omit default with attrs' converter on the converter works. + """ + converter = Converter(omit_if_default=True) + cl, vals, kwargs = cl_and_vals + + @define + class C: + a1: int = field(default="1", converter=int) + a2: int = field(default="1", converter=attrs.Converter(int)) + c: cl = Factory(lambda: cl(*vals, **kwargs)) + + inst = C() + unstructured = converter.unstructure(inst) + assert unstructured == {} + assert inst == converter.structure(unstructured, C) + + inst = C(0, 0) + unstructured = converter.unstructure(inst) + assert unstructured == {"a1": 0, "a2": 0} + assert inst == converter.structure(unstructured, C) + + def test_dict_roundtrip_with_alias(): """ A class with an aliased attribute can be unstructured and structured.