cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Destructure and structure enum

Open andatt opened this issue 3 years ago • 13 comments

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

andatt avatar Mar 20 '22 18:03 andatt

Hello, you're trying to unstructure the enum class instead of an enum member. Try unstructure(CatBreed.SIAMESE) instead.

Tinche avatar Mar 21 '22 01:03 Tinche

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.

andatt avatar Mar 21 '22 09:03 andatt

Ah interesting. What would you expect the unstructured data to be? Just the name of the enum or an entire definition of the enum?

Tinche avatar Mar 21 '22 11:03 Tinche

I was thinking the entire definition of enum in a dict, i.e:

{"SIAMESE": "siamese", "MAINE_COON": "maine_coon", "SACRED_BIRMAN": "birman"}

andatt avatar Mar 21 '22 11:03 andatt

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'}

Tinche avatar Mar 21 '22 13:03 Tinche

Awesome! perfect - thank you, that's exactly what I was looking for. Will close this issue now :)

andatt avatar Mar 21 '22 17:03 andatt

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'>}

andatt avatar Mar 21 '22 20:03 andatt

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

bibajz avatar Mar 21 '22 21:03 bibajz

... 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. :)

bibajz avatar Mar 21 '22 23:03 bibajz

There are probably many ways to solve this, register_unstructure_hook_func is probably the simplest.

@andatt which version of Python are you using?

Tinche avatar Mar 22 '22 12:03 Tinche

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.

andatt avatar Mar 23 '22 21:03 andatt

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.

andatt avatar Mar 27 '22 15:03 andatt

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

Tinche avatar Mar 27 '22 20:03 Tinche

Going to close this as stale, please open another issue if you need more help ;)

Tinche avatar Jan 03 '23 16:01 Tinche