konradhalas / dacite

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

Support for Literal? #80

Closed jbcpollak closed 4 years ago

jbcpollak commented 4 years ago

I'd like to be able to deserialize from_dict() a Union of overlapping types based on the Literal values. Is that possible?

    @dataclass
    class AType:
        # changing type to str doesn't help
        type: Literal['A'] = 'A'

    @dataclass
    class BType:
        type: Literal['B'] = 'B'

    @dataclass
    class AorB:
        u: Union[AType, BType]

        obj = dacite.from_dict(
            data_class=AorB,
            data=loaded_dict,
            config=dacite.Config(strict=False)
       )
no1xsyzy commented 4 years ago

I would like to give a more realistic using:

@dataclass
class Circle:
    type: Literal['circle'] = 'circle'
    radius: float
    @property
    def area(self):
        return math.pi*self.radius*self.radius

@dataclass
class Semicircle:
    type: Literal['circle'] = 'semicircle'
    radius: float
    @property
    def area(self):
        return math.pi*self.radius*self.radius/2

This would be way clearer than:

@dataclass
class Circle_or_semicircle:
    type: str
    radius: float
    @property
    def area(self):
        if self.type == 'circle':
            return math.pi*self.radius*self.radius
        elif self.type == 'semicircle':
            return math.pi*self.radius*self.radius/2
        else:
            raise Error("neither circle nor semicircle")

It would be clearer in three ways:

  1. You would know if it was a circle or semicircle with isinstance(), which is better for semantics;
  2. The code of area for different class was separated;
  3. If the type was neither 'circle' nor 'semicircle', think about one day the server started to create arcs {"type": "arc", "radius": 0, "angle": 30}, the error would be raised when dacite.from_dict, at which time the error had been very clear.
konradhalas commented 4 years ago

@jbcpollak thank you for reporting this issue.

Yes, it will be possible. I've just added Literal support in https://github.com/konradhalas/dacite/commit/ce5d4039b888e5361f93dda3e194bab47e39ae5f and it should work with Union as you described.

I will release new version soon.

konradhalas commented 4 years ago

@jbcpollak Deployed to PyPI 🎉

jbcpollak commented 4 years ago

Fantastic, TY!

jbcpollak commented 4 years ago

Hi @konradhalas - we just tried this and it turns out this doesn't work with Python 3.7.6. We aren't sure if we can upgrade to 3.8 or not (we are going to look into it), but I wanted to mention it.

I guess this is because in 3.7 Literal is in typing_extensions and in 3.8 its been moved to typing. I'm not sure if there is a good way around that.

konradhalas commented 4 years ago

Hi @jbcpollak - argh, you have right, I'm using typing from 3.8. I would check if I can do something reasonable on my side, but TBH I don't want to add dependencies to dacite.

jbcpollak commented 4 years ago

Yeah - I think you'd have to add code to check for the Python version then programmatically choose which library to import from.

We can't upgrade to 3.8 yet, a library we depend on doesn't support it.

Gideon-Felt commented 4 years ago

Guess I'm stuck with not being able to use it... I'm on 3.7. GCP Cloud Functions are still in beta for 3.8 so I have to use 3.7.6 for production. I'm using dacite with typing_extensions.

Gideon-Felt commented 4 years ago

haha, the docs dont have it out of beta at least maybe I can get the project updated now. either way, it would be nice for 3.7 devs if @jbcpollak's suggestion about programmatic version checking was available.

jbcpollak commented 4 years ago

@Gideon-Felt in the meantime you can use this in your requirements.txt file:

git+https://github.com/Pickle-Robot/dacite/@master#egg=dacite

We are still stuck on 3.7.6 as well and this works for us.