konradhalas / dacite

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

Take @dataclass(init=False) into account in from_dict #134

Closed dsuch closed 3 years ago

dsuch commented 3 years ago

Good day,

I am using dacite 1.6.0.

practically all of my dataclasses use "init=False" in their definitions, e.g.:

@dataclass(init=False)
class MyClass:
    id: str
    timestamp: str
    event_type: int

This works nicely except that dacite.core.from_dict does not honour the "init=False" flag and invokes create_instance with fields in init_values alone.

Whether a field should become part of the init_values depends on the field's "init" attribute only. If it is True, the field is part of the initial ones, otherwise it is part of post init values.

Just to serve as a reminder, here is dacite.dataclasses.create_instance:

def create_instance(data_class: Type[T], init_values: Data, post_init_values: Data) -> T:
    instance = data_class(**init_values)
    for key, value in post_init_values.items():
        setattr(instance, key, value)
    return instance

If a dataclass uses init=False but individual fields do not, that results in an exception:

    instance = data_class(**init_values)
TypeError: object() takes no parameters

The exception makes sense because there is no __init__ defined for the class so it is object's __init__ that is called above. Hence the exception.

My point is that if there is a class-level init=False then the logic in from_dict should take it into account. That is, with that flag set to False, all of the fields should go to post_init_values. In this way, create_instance, will not attempt to invoke a missing __init__ method.

The implementation of it can be very simple. At the top of from_dict, add:

is_class_init_false = data_class.__dataclass_params__.init is False

Then, at the below, right before create_instance is called, add this (or a variation thereof if you prefer shorter and less explicit branches):

        if is_class_init_false:
            post_init_values[field.name] = value
        else:
            if field.init:
                init_values[field.name] = value
            else:
                post_init_values[field.name] = value

I will not have time to submit a PR. I am leaving the code here for someone else to do it.

Regards.

dsuch commented 3 years ago

Actually, let's skip this idea. Thank you.