Skip to content
Open
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
20 changes: 14 additions & 6 deletions src/cattrs/strategies/_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from .. import BaseConverter
from .._compat import get_newtype_base, is_literal, is_subclass, is_union_type
from ..typealiases import is_type_alias
from ..typealiases import get_type_alias_base, is_type_alias

__all__ = [
"configure_tagged_union",
Expand Down Expand Up @@ -174,7 +174,11 @@ def configure_union_passthrough(
.. versionchanged:: 25.2.0
Introduced the `accept_ints_as_floats` parameter.
"""
args = set(union.__args__)

def get_passthrough_base(type: Any) -> Any:
return get_type_alias_base(type) if is_type_alias(type) else type

args = {get_passthrough_base(type) for type in union.__args__}

def make_structure_native_union(exact_type: Any) -> Callable:
# `exact_type` is likely to be a subset of the entire configured union (`args`).
Expand All @@ -194,9 +198,10 @@ def make_structure_native_union(exact_type: Any) -> Callable:
}

non_literal_classes = {
get_newtype_base(t) or t
get_newtype_base(t) or get_passthrough_base(t)
for t in exact_type.__args__
if not is_literal(t) and ((get_newtype_base(t) or t) in args)
if not is_literal(t)
and ((get_newtype_base(t) or get_passthrough_base(t)) in args)
}

# We augment the set of allowed classes with any configured subclasses of
Expand All @@ -211,7 +216,8 @@ def make_structure_native_union(exact_type: Any) -> Callable:
spillover = {
a
for a in exact_type.__args__
if (get_newtype_base(a) or a) not in non_literal_classes
if (get_newtype_base(a) or get_passthrough_base(a))
not in non_literal_classes
and not is_literal(a)
}

Expand Down Expand Up @@ -273,7 +279,9 @@ def contains_native_union(exact_type: Any) -> bool:
for lit_arg in t.__args__
}
non_literal_types = {
get_newtype_base(t) or t for t in type_args if not is_literal(t)
get_newtype_base(t) or get_passthrough_base(t)
for t in type_args
if not is_literal(t)
}

return (literal_classes | non_literal_types) & args
Expand Down
21 changes: 21 additions & 0 deletions tests/strategies/test_union_passthrough_695.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from dataclasses import dataclass

from cattrs import BaseConverter
from cattrs.strategies import configure_union_passthrough


@dataclass
class DataClass:
field: str


def test_type_alias_union_member(converter: BaseConverter) -> None:
"""Native union passthrough handles PEP 695 aliases in the exact union."""
type NewScalar = str

configure_union_passthrough(str | int, converter)

assert converter.structure("value", NewScalar | DataClass) == "value"
assert converter.structure({"field": "value"}, NewScalar | DataClass) == DataClass(
"value"
)