Skip to content

Commit b25a258

Browse files
authored
Newtypes fix (#381)
* Newtypes fix * Rework test
1 parent 470b038 commit b25a258

File tree

4 files changed

+61
-3
lines changed

4 files changed

+61
-3
lines changed

HISTORY.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- _cattrs_ is now linted with [Ruff](https://beta.ruff.rs/docs/).
77
- Fix TypedDicts with periods in their field names.
88
([#376](https://github.com/python-attrs/cattrs/issues/376) [#377](https://github.com/python-attrs/cattrs/pull/377))
9+
- Optimize and improve unstructuring of `Optional` (unions of one type and `None`).
10+
([#380](https://github.com/python-attrs/cattrs/issues/380) [#381](https://github.com/python-attrs/cattrs/pull/381))
911
- Fix `format_exception` and `transform_error` type annotations.
1012

1113
## 23.1.2 (2023-06-02)

src/cattrs/converters.py

+15
Original file line numberDiff line numberDiff line change
@@ -881,13 +881,17 @@ def __init__(
881881
is_frozenset,
882882
lambda cl: self.gen_unstructure_iterable(cl, unstructure_to=frozenset),
883883
)
884+
self.register_unstructure_hook_factory(
885+
is_optional, self.gen_unstructure_optional
886+
)
884887
self.register_unstructure_hook_factory(
885888
is_typeddict, self.gen_unstructure_typeddict
886889
)
887890
self.register_unstructure_hook_factory(
888891
lambda t: get_newtype_base(t) is not None,
889892
lambda t: self._unstructure_func.dispatch(get_newtype_base(t)),
890893
)
894+
891895
self.register_structure_hook_factory(is_annotated, self.gen_structure_annotated)
892896
self.register_structure_hook_factory(is_mapping, self.gen_structure_mapping)
893897
self.register_structure_hook_factory(is_counter, self.gen_structure_counter)
@@ -938,6 +942,17 @@ def gen_unstructure_attrs_fromdict(
938942
cl, self, _cattrs_omit_if_default=self.omit_if_default, **attrib_overrides
939943
)
940944

945+
def gen_unstructure_optional(self, cl: Type[T]) -> Callable[[T], Any]:
946+
"""Generate an unstructuring hook for optional types."""
947+
union_params = cl.__args__
948+
other = union_params[0] if union_params[1] is NoneType else union_params[1]
949+
handler = self._unstructure_func.dispatch(other)
950+
951+
def unstructure_optional(val, _handler=handler):
952+
return None if val is None else _handler(val)
953+
954+
return unstructure_optional
955+
941956
def gen_structure_typeddict(self, cl: Any) -> Callable[[Dict], Dict]:
942957
"""Generate a TypedDict structure function.
943958

tests/test_newtypes.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
import pytest
55

6-
from cattrs import BaseConverter
6+
from cattrs import Converter
77

88
PositiveIntNewType = NewType("PositiveIntNewType", int)
99
BigPositiveIntNewType = NewType("BigPositiveIntNewType", PositiveIntNewType)
1010

1111

12-
def test_newtype_structure_hooks(genconverter: BaseConverter):
12+
def test_newtype_structure_hooks(genconverter: Converter):
1313
"""NewTypes should work with `register_structure_hook`."""
1414

1515
assert genconverter.structure("0", int) == 0
@@ -39,7 +39,7 @@ def test_newtype_structure_hooks(genconverter: BaseConverter):
3939
assert genconverter.structure("51", BigPositiveIntNewType) == 51
4040

4141

42-
def test_newtype_unstructure_hooks(genconverter: BaseConverter):
42+
def test_newtype_unstructure_hooks(genconverter: Converter):
4343
"""NewTypes should work with `register_unstructure_hook`."""
4444

4545
assert genconverter.unstructure(0, int) == 0

tests/test_optionals.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import NewType, Optional
2+
3+
import pytest
4+
from attrs import define
5+
6+
from cattrs._compat import is_py310_plus
7+
8+
9+
def test_newtype_optionals(genconverter):
10+
"""Newtype optionals should work."""
11+
Foo = NewType("Foo", str)
12+
13+
genconverter.register_unstructure_hook(Foo, lambda v: v.replace("foo", "bar"))
14+
15+
@define
16+
class ModelWithFoo:
17+
total_foo: Foo
18+
maybe_foo: Optional[Foo]
19+
20+
assert genconverter.unstructure(ModelWithFoo(Foo("foo"), Foo("is it a foo?"))) == {
21+
"total_foo": "bar",
22+
"maybe_foo": "is it a bar?",
23+
}
24+
25+
26+
@pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax")
27+
def test_newtype_modern_optionals(genconverter):
28+
"""Newtype optionals should work."""
29+
Foo = NewType("Foo", str)
30+
31+
genconverter.register_unstructure_hook(Foo, lambda v: v.replace("foo", "bar"))
32+
33+
@define
34+
class ModelWithFoo:
35+
total_foo: Foo
36+
maybe_foo: Foo | None
37+
38+
assert genconverter.unstructure(ModelWithFoo(Foo("foo"), Foo("is it a foo?"))) == {
39+
"total_foo": "bar",
40+
"maybe_foo": "is it a bar?",
41+
}

0 commit comments

Comments
 (0)