Skip to content

Commit 443379f

Browse files
committed
union passthrough: add accept_ints_as_floats
1 parent f5d0594 commit 443379f

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

src/cattrs/strategies/_unions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ def structure_tagged_union(
139139
converter.register_structure_hook(union, structure_tagged_union)
140140

141141

142-
def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
142+
def configure_union_passthrough(
143+
union: Any, converter: BaseConverter, accept_ints_as_floats: bool = True
144+
) -> None:
143145
"""
144146
Configure the converter to support validating and passing through unions of the
145147
provided types and their subsets.
@@ -205,6 +207,16 @@ def make_structure_native_union(exact_type: Any) -> Callable:
205207
and not is_literal(a)
206208
}
207209

210+
# By default, when floats are part of the union, accept ints too.
211+
if (
212+
accept_ints_as_floats
213+
and int in args
214+
and float in args
215+
and float in non_literal_classes
216+
and int not in non_literal_classes
217+
):
218+
non_literal_classes.add(int)
219+
208220
if spillover:
209221
spillover_type = (
210222
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))

tests/strategies/test_native_unions.py renamed to tests/strategies/test_union_passthrough.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pytest
1010
from attrs import define
1111

12-
from cattrs import BaseConverter
12+
from cattrs import BaseConverter, ClassValidationError, Converter
1313
from cattrs.strategies import configure_union_passthrough
1414

1515

@@ -109,3 +109,35 @@ class B:
109109

110110
with pytest.raises(TypeError):
111111
converter.structure((), union)
112+
113+
114+
def test_int_is_float(converter: BaseConverter) -> None:
115+
"""By default, ints can also be accepted when floats are.
116+
117+
When the strategy gets initialized with both ints and floats,
118+
unions that only contain floats also accept ints by default.
119+
"""
120+
121+
configure_union_passthrough(Union[int, float, str, None], converter)
122+
123+
assert converter.structure(1, Union[float, str, None]) == 1
124+
assert isinstance(converter.structure(1, Union[float, str, None]), int)
125+
126+
127+
def test_int_is_not_float(converter: BaseConverter) -> None:
128+
"""Ints can be configured to be separate."""
129+
130+
@define
131+
class A:
132+
a: int
133+
134+
configure_union_passthrough(
135+
Union[int, float], converter, accept_ints_as_floats=False
136+
)
137+
138+
with pytest.raises(
139+
ClassValidationError
140+
if isinstance(converter, Converter) and converter.detailed_validation
141+
else TypeError
142+
):
143+
converter.structure(1, Union[float, A])

0 commit comments

Comments
 (0)