konradhalas / dacite

Simple creation of data classes from dictionaries.
MIT License
1.76k stars 106 forks source link

Feature proposal: cast values in Literals #109

Open antonagestam opened 4 years ago

antonagestam commented 4 years ago

I ran into a case where I'm modeling API responses like this, simplified:

@dataclass
class Contact:
    contactType: Literal["Member", "Contact"]

@dataclass
class Member(Contact):
    contactType: Literal["Member"]

dacite enables me to make sure that objects returned from a certain endpoint are Members and not any Contact, by looking at the contactType field. Now, I'm finding myself also wanting to use the "Member" and "Contact" strings in a few other places and so it would be nice to introduce an enum for them. Literals support enum values, so these would be valid models as well:

class ContactType(enum.Enum):
    member = "Member"
    contact = "Contact"

@dataclass
class Contact:
    contactType: ContactType

@dataclass
class Member(Contact):
    contactType: Literal[ContactType.member]

However, dacite doesn't currently recognize that the literal contains an enum value and won't try to cast string values to their enum equivalents. I realize that this might be a pretty complicated thing to ask for, but it would be nice to see this implemented. If you think this is a behavior that makes sense I'd be willing to give a shot at implementing this.

To clarify, the feature would be to try and apply matching classes from cast=[...] in the config.

konradhalas commented 3 years ago

Hi @antonagestam - it sounds like a reasonable feature. I will be more than happy to receive PR :)

antonagestam commented 3 years ago

@konradhalas Awesome, unfortunately it's not likely that I'll have time to spend on this anytime soon, just in case anyone else wants to work on this in the meanwhile.

konradhalas commented 3 years ago

@antonagestam sure, I will add it to my roadmap :)

miknet commented 1 year ago

Hello,

I'd also like to see this feature in some near future. In v1.7.0. this could be potentially implemented with the following change:

index 6971f11..e2f5a2b 100644
--- a/dacite/types.py
+++ b/dacite/types.py
@@ -1,4 +1,5 @@
 from dataclasses import InitVar
+from enum import Enum
 from typing import (
     Type,
     Any,
@@ -12,6 +13,7 @@ from typing import (
     List,
     Tuple,
     cast as typing_cast,
+    get_args,
 )

 T = TypeVar("T", bound=Any)
@@ -49,6 +51,11 @@ def transform_value(
             )
         item_cls = extract_generic(target_type, defaults=(Any,))[0]
         return collection_cls(transform_value(type_hooks, cast, item_cls, item) for item in value)
+    if is_literal(target_type):
+        for literal_arg in get_args(target_type):
+            for cast_type in cast:
+                if isinstance(literal_arg, Enum) and isinstance(literal_arg, cast_type):
+                    value = cast_type(value)
     return value

If you think it's fine, I can submit a PR with few tests.

emosenkis commented 1 year ago

I had to implement this as type hooks. It would be great if this could be supported natively and @miknet's proposal looks pretty good.

OffByOnee commented 1 year ago

Has there been any progress on this feature?

OffByOnee commented 1 year ago

For anyone else who needs this in the meantime, here's an example of how to do it with type_hooks like @emosenkis mentioned.

from dataclasses import dataclass
import enum
from typing import Literal

import dacite

class ContactType(enum.Enum):
    member = "Member"
    contact = "Contact"

@dataclass
class Contact:
    contactType: ContactType

LiteralContactMember = Literal[ContactType.member]
@dataclass
class Member(Contact):
    contactType: LiteralContactMember

def transform_literal(v):
    if ContactType(v) != ContactType.member:
        raise ValueError(f'Invalid ContactType "{v}" supplied for {LiteralContactMember}')

    return v

config = dacite.Config(
    check_types=False,
    type_hooks={LiteralContactMember: transform_literal},
)