The value of enums should be unstructured and structured too
I'm running into an interesting issue where the value of an enum is a structured value which isn't being properly unstructured and that's causing issues with serialization.
Here's a simple example:
import attrs
import cattrs
import enum
class E(enum.Enum):
A = 0
B = 1
class EE(enum.Enum):
A0 = (E.A, 0)
A1 = (E.A, 1)
B0 = (E.B, 0)
B1 = (E.B, 1)
@attrs.define
class C:
e: E
ee: EE
c = C(e=E.B, ee=EE.A1)
print(cattrs.unstructure(c))
The e attribute of the C class is properly unstructured to a 1 value and the ee attribute is unstructured to the tuple (E.A, 1), but the E.A in that resulting tuple is not unstructured, which causes, for instance, JSON serialization to fail.
I can see where to fix this while unstructuring. Changing converters.py _unstructure_enum to:
def _unstructure_enum(self, obj: Enum) -> Any:
"""Convert an enum to its value."""
return self._unstructure_func.dispatch(obj.value.__class__)(obj.value)
Fixes the unstructuring, but I'm not sure how to fix the structuring.
Hm, tricky. You're right in that enum members can contain values of arbitrary complexity. Judging by the amount of issues I've seen about them, this seems to be pretty rare in practice.
For unstructuring, your fix would work but it would slow down primitive-value enums. We would need a separate code path for it, with simpler enums taking the faster path. This probably means we need predicates and hook factories for simple and complex enums, instead of the current singledispatch-based hook for unstructuring.
Structuring is harder since it's tricky to actually detect what the underlying type is. Currently we just let the enum itself do the structuring/validation; in your example that would be equivalent to just E(val) and then just deal with what happens. Obviously that won't work for more complex cases, like your EE enum.
For example, your EE enum would have an underlying value type of tuple[E, int], so then structuring would be equivalent to EE(converter.structure(val, tuple[E, int])). One approach would be to iterate over all members and try to reconstruct this type, recursively.
If you're interested in working on this I'd be happy to support.
Yeah, this is absolutely an edge case and not something that most users of enums would run into, but I've ended up building this kind of Enum multiple times in the past and it would be nice to have it be supported.
I've started to go a bit deeper down this rabbit hole and have some ideas. The hard part would be to find the types of the values. I know that type checker have ways of inferring types but I'm not sure if cattrs does that or how to go about it. Looking at the Python typing documentation, though, shows a standardized way to specify types of Enum values. Giving a type hint for the _value_ member.
class EE(enum.Enum):
_value_: tuple[E, int]
A0 = (E.A, 0)
# etc.
This seems like it would be a good solution for where to find that information for cattrs and fit into the way that attrs and dataobjects like to define things.
We would need a separate code path for it, with simpler enums taking the faster path. This probably means we need predicates and hook factories for simple and complex enums, instead of the current singledispatch-based hook for unstructuring.
I'm not totally sure what this would entail, I haven't dug too deep into the guts of cattrs, just deep enough to figure out where this issue could be solved. I'll see what I can figure out and toss up a PR then you can tell me if I've missed something or if there's a better way to implement it.
Giving a type hint for the value member.
Ooh, interesting! I didn't even know about this. This probably solves our issue then - we can just check for this field. We probably don't even need to go to hook factories; a field check should be fast enough.