cattrs
cattrs copied to clipboard
Destructure and structure enum
I am trying to unstructure and structure an Enum like this:
from cattr import structure, unstructure
class CatBreed(Enum):
SIAMESE = "siamese"
MAINE_COON = "maine_coon"
SACRED_BIRMAN = "birman"
a = unstructure(CatBreed)
structure(a, CatBreed)
This gives me the following error:
ValueError: <enum 'CatBreed'> is not a valid CatBreed
What's the correct way to be able to structure and destructure an Enum?
Thanks
Hello, you're trying to unstructure the enum class instead of an enum member. Try unstructure(CatBreed.SIAMESE) instead.
Hi
So the context to the situation is like this:
@dataclass
class Something:
breed: Type[CatBreed]
The type isn't a field of CatBreed, it's CatBreed the class. This is why I need to structure and unstructure the actual class itself, not just a field.
Ah interesting. What would you expect the unstructured data to be? Just the name of the enum or an entire definition of the enum?
I was thinking the entire definition of enum in a dict, i.e:
{"SIAMESE": "siamese", "MAINE_COON": "maine_coon", "SACRED_BIRMAN": "birman"}
This isn't supported out of the box but it's not hard to do it yourself.
Since this is a more complex case, you'll need to use converter.register_unstructure_hook_func. You'll need two parts:
- a predicate function to detect a value is an enum class
- an unstructuring function, to turn the enum class into the output you want
For the first one, we can just check if the class is enum.EnumMeta (all enum classes are instances of this class): lambda t: t is EnumMeta.
For the second one, we can just iterate over the enum class: lambda enum: {v.name: v.value for v in enum}
Putting it all together:
from enum import Enum, EnumMeta
from cattrs import GenConverter
c = GenConverter()
class CatBreed(Enum):
SIAMESE = "siamese"
MAINE_COON = "maine_coon"
SACRED_BIRMAN = "birman"
c.register_unstructure_hook_func(
lambda t: t is EnumMeta, lambda enum: {v.name: v.value for v in enum}
)
>>> c.unstructure(CatBreed)
{'SIAMESE': 'siamese', 'MAINE_COON': 'maine_coon', 'SACRED_BIRMAN': 'birman'}
Awesome! perfect - thank you, that's exactly what I was looking for. Will close this issue now :)
Ah I was too quick to close - I found that the code you gave worked for the example you gave above. But when I use the example I gave further up it doesn't work:
from enum import Enum, EnumMeta
from typing import Type
from cattrs import GenConverter
from attr import define
class CatBreed(Enum):
SIAMESE = "siamese"
MAINE_COON = "maine_coon"
SACRED_BIRMAN = "birman"
@define
class Something:
breed: Type[CatBreed]
x = Something(CatBreed)
c = GenConverter()
c.register_unstructure_hook_func(
lambda t: t is EnumMeta, lambda enum: {v.name: v.value for v in enum}
)
print(c.unstructure(x))
This returns:
{'breed': <enum 'CatBreed'>}
Hi @andatt , I played with your example and I made it work like this:
from enum import Enum, EnumMeta
from typing import Type
from cattrs import GenConverter
from attr import define
class CatBreed(Enum):
SIAMESE = "siamese"
MAINE_COON = "maine_coon"
SACRED_BIRMAN = "birman"
@define
class Something:
breed: Type[CatBreed]
x = Something(CatBreed)
c = GenConverter()
c.register_unstructure_hook_func(
lambda x: x is Type[CatBreed],
lambda enum: {v.name: v.value for v in enum}
)
print(c.unstructure(x))
"""
{'breed': {'SIAMESE': 'siamese', 'MAINE_COON': 'maine_coon', 'SACRED_BIRMAN': 'birman'}}
"""
However, it seems really repetetive and error-prone, so I played around a bit and came with a following solution, using Converter.unstructure_hook_factory:
from enum import Enum, EnumMeta
from typing import Type, _GenericAlias #
from cattrs import GenConverter
from attr import define
class CatBreed(Enum):
SIAMESE = "siamese"
MAINE_COON = "maine_coon"
SACRED_BIRMAN = "birman"
@define
class Something:
breed: Type[CatBreed]
class Fruit(Enum):
APPLE = "apple"
BANANA = "banana"
@define
class Something2:
fruit: Type[Fruit]
def check_enum_type(type_):
"""
Check whether it is a `Type[...]` annotation and `...` is an Enum
"""
return isinstance(type_, _GenericAlias) and isinstance(type_.__args__[0], EnumMeta)
d = GenConverter()
d.register_unstructure_hook_factory(check_enum_type, lambda t: lambda enum: {v.name: v.value for v in enum})
d.unstructure(Something(CatBreed))
"""
Result:
{'breed': {'SIAMESE': 'siamese',
'MAINE_COON': 'maine_coon',
'SACRED_BIRMAN': 'birman'}}
"""
d.unstructure(Something2(Fruit))
"""
Result:
{'fruit': {'APPLE': 'apple', 'BANANA': 'banana'}}
"""
Do both approaches make sense?
Is this the way to solve this @Tinche ?
Regards, Libor
... to follow-up my comment https://github.com/python-attrs/cattrs/issues/235#issuecomment-1074456626 :
we do not need to use register_instructure_hook_factory at all actually, register_unstructure_func is all that is needed:
def check_enum_type(type_):
"""
Check whether it is a `Type[...]` annotation and `...` is an Enum
"""
return isinstance(type_, _GenericAlias) and isinstance(type_.__args__[0], EnumMeta)
d = GenConverter()
d.register_unstructure_hook_func(check_enum_type, lambda enum: {v.name: v.value for v in enum})
d.unstructure(Something(CatBreed))
"""
Result:
{'breed': {'SIAMESE': 'siamese',
'MAINE_COON': 'maine_coon',
'SACRED_BIRMAN': 'birman'}}
"""
d.unstructure(Something2(Fruit))
"""
Result:
{'fruit': {'APPLE': 'apple', 'BANANA': 'banana'}}
"""
Sorry for the unnecessarily complex code in the preceeding comment, now it should be able to de-serialize any Enum type given. :)
There are probably many ways to solve this, register_unstructure_hook_func is probably the simplest.
@andatt which version of Python are you using?
Hi @bibajz
Thanks for you detailed replies, sorry it's taken me some time to reply. The proposed solutions work in these simple examples however when I apply them to some more complex code I have I get a different error:
AttributeError: 'str' object has no attribute 'name'
I am suspicious that there is some other issue in my code (passing wrong type possibly) so I am searching for this issue before I can 100% confirm it's not an issue with your proposed solution. Will come back on this as soon as I can confirm.
@Tinche I am using python 3.9.9
Thanks both for your help, much appreciated.
Sorry it's taken me some time on this, it was quite difficult isolating the issue from my more complex code. It's turns out there are two different issues that's prevent the above approach from working for me. So if I take the solution above (which fixes the Types[] issue) then apply it to this code:
from enum import Enum, EnumMeta
from typing import _GenericAlias, Union
from cattrs import GenConverter
from attr import define
class CatBreed(Enum):
SIAMESE = "siamese"
MAINE_COON = "maine_coon"
SACRED_BIRMAN = "birman"
class Fruits(Enum):
APPLE = "apple"
BANANA = "banana"
class Basket(Enum):
cats = CatBreed
fruits = Fruits
@define
class Something:
cats_and_fruits: Union[Basket.cats.value, Basket.fruits.value]
def check_enum_type(type_):
"""
Check whether it is a `Type[...]` annotation and `...` is an Enum
"""
return isinstance(type_, _GenericAlias) and isinstance(type_.__args__[0], EnumMeta)
c = GenConverter()
c.register_unstructure_hook_func(
check_enum_type,
lambda enum: {v.name: v.value for v in enum}
)
print(c.unstructure(Something(CatBreed.SACRED_BIRMAN)))
This gives me:
TypeError: 'CatBreed' object is not iterable
Maybe my type annotations are wrong here? Not sure.
The other issue I found:
from enum import Enum, EnumMeta
from typing import _GenericAlias, Union
from cattrs import GenConverter
from attr import define
def make_siamese_cat(cat):
return cat
class CatBreed(Enum):
SIAMESE = make_siamese_cat
@define
class Something:
cats: CatBreed
def check_enum_type(type_):
"""
Check whether it is a `Type[...]` annotation and `...` is an Enum
"""
return isinstance(type_, _GenericAlias) and isinstance(type_.__args__[0], EnumMeta)
c = GenConverter()
c.register_unstructure_hook_func(
check_enum_type,
lambda enum: {v.name: v.value for v in enum}
)
print(c.unstructure(Something(CatBreed.SIAMESE)))
Returns:
AttributeError: 'function' object has no attribute 'value'
I think what I am going to do is try to refactor my code so I don't have these more complex structures with Types[SomeEnum] and functions as Enum values. Hopefully then the results will be more stable.
But in this case, you're unstructuring instances of enums, not the enum definitions itself. Here's a version with the hooks commented out, which works: https://replit.com/@TinTvrtkovic/cattrs235#main.py
Going to close this as stale, please open another issue if you need more help ;)