TypedDict unstructuring skips value unions
salotz opened this issue · 4 comments
salotz commented
- cattrs version:
cattrs==23.2.3
attrs
- Python version: 3.11.8
- Operating System: Linux
Description
Special union hooks in TypeDict values are skipped. If you register a handler for a union in general it will not be run. See minimal repro below.
Repro
from typing import TypedDict
import attrs
from cattrs import Converter
from cattrs.strategies import configure_tagged_union
@attrs.define
class A:
val: int
@attrs.define
class B:
val: int
converter = Converter()
configure_tagged_union(A | B, converter)
assert converter.unstructure(
A(1),
A,
) == {"val" : 1}
assert converter.unstructure(
A(1),
A | B,
) == {
"_type" : "A",
"val" : 1,
}
class MyTaggedUnionDict(TypedDict):
options: A | B
# THIS SHOULD NOT PASS
assert converter.unstructure(
MyTaggedUnionDict(options=A(1)),
) == {
"options" : {
"val" : 1,
},
}
# THIS RAISES AssertionError
assert converter.unstructure(
MyTaggedUnionDict(options=A(1)),
) == {
"options" : {
"_type" : "A",
"val" : 1,
},
}, "Unstructured value has no type tag."
salotz commented
Any workaround would be appreciated! Not sure how to do it myself.
Tinche commented
Here's the thing: typed dicts are what's called a structural type.
When you type
MyTaggedUnionDict(options=A(1))
that's actually equivalent to
{
"options": A(1)
}
and cattrs can't tell this is supposed to be a MyTaggedUnionDict
and not an ordinary dictionary.
So you can explicitly tell it:
converter.unstructure(
MyTaggedUnionDict(options=A(1)),
unstructure_as=MyTaggedUnionDict
)
Tinche commented
Here's the fixed code:
from typing import TypedDict
import attrs
from cattrs import Converter
from cattrs.strategies import configure_tagged_union
@attrs.define
class A:
val: int
@attrs.define
class B:
val: int
converter = Converter()
configure_tagged_union(A | B, converter)
assert converter.unstructure(A(1), A) == {"val": 1}
assert converter.unstructure(A(1), A | B) == {"_type": "A", "val": 1}
class MyTaggedUnionDict(TypedDict):
options: A | B
# THIS SHOULD NOT PASS
assert converter.unstructure({"options": A(1)}, unstructure_as=MyTaggedUnionDict) != {
"options": {"val": 1}
}
# THIS RAISES AssertionError
assert converter.unstructure({"options": A(1)}, unstructure_as=MyTaggedUnionDict) == {
"options": {"_type": "A", "val": 1}
}, "Unstructured value has no type tag."
Closing unless you have other questions ;)