dacite icon indicating copy to clipboard operation
dacite copied to clipboard

Feature proposal: cast values in Literals

Open antonagestam opened this issue 4 years ago • 7 comments

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.

antonagestam avatar Jul 08 '20 07:07 antonagestam

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

konradhalas avatar Aug 24 '21 14:08 konradhalas

@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.

antonagestam avatar Aug 24 '21 14:08 antonagestam

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

konradhalas avatar Aug 24 '21 15:08 konradhalas

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.

miknet avatar Jan 04 '23 10:01 miknet

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.

emosenkis avatar Feb 28 '23 18:02 emosenkis

Has there been any progress on this feature?

OffByOnee avatar Sep 19 '23 16:09 OffByOnee

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},
)

OffByOnee avatar Sep 19 '23 17:09 OffByOnee