1

I'd like to create a dict to a dataclass which contains as a List of dataclass as attribute

Here is an example of what I'd like to achieve:

from typing import List
from dataclasses import dataclass


@dataclass
class level2:
    key21: int
    key22: int


@nested_dataclass
class level1:
    key1: int
    key2: List[level2]


data = {
    'key1': value1,
    'key2': [{
        'key21': value21,
        'key22': value22,
    }]
}

my_object = level1(**data)
print(my_object.key2[0].key21) #should print value21

Closest decorator I found was this one, but it does not work with Lists of dataclass: Creating nested dataclass objects in Python

def is_dataclass(obj):
    """Returns True if obj is a dataclass or an instance of a
    dataclass."""
    _FIELDS = '__dataclass_fields__'
    return hasattr(obj, _FIELDS)


def nested_dataclass(*args, **kwargs):

    def wrapper(cls):
        cls = dataclass(cls, **kwargs)
        original_init = cls.__init__

        def __init__(self, *args, **kwargs):
            for name, value in kwargs.items():
                field_type = cls.__annotations__.get(name, None)

                if is_dataclass(field_type) and isinstance(value, dict):
                     new_obj = field_type(**value)
                     kwargs[name] = new_obj

            original_init(self, *args, **kwargs)

        cls.__init__ = __init__
        return cls

    return wrapper(args[0]) if args else wrapper

How would you modify this decorator or create one that would do the job? (I've got zero experience in building decorator)

Any comment/code is very much appreciated. Thank you

Joseph
  • 209
  • 2
  • 11

3 Answers3

1

Alright so I changed the decorator a bit but it is very specific to the example provided here. The main issue was that your List[level2] field was not a dataclass. So to go around that I played around a bit and noticed that there was an args property that could tell you the nested type in the list. I've never work with dataclasses before (except with pydantic) so perhaps there is a better answer out there

def nested_dataclass(*args, **kwargs):

    def wrapper(cls):
        cls = dataclass(cls, **kwargs)
        original_init = cls.__init__

        def __init__(self, *args, **kwargs):
            for name, value in kwargs.items():
                field_type = cls.__annotations__.get(name, None)

                if hasattr(field_type, '__args__'):
                    inner_type = field_type.__args__[0]
                    if is_dataclass(inner_type):
                        new_obj = [inner_type(**dict_) for dict_ in value]
                        kwargs[name] = new_obj

            original_init(self, *args, **kwargs)

        cls.__init__ = __init__
        return cls

    return wrapper(args[0]) if args else wrapper


@dataclass
class level2:
    key21: int
    key22: int

@nested_dataclass
class level1:
    key1: int
    key2: List[level2]


data = {
    'key1': 1,
    'key2': [{
        'key21': 21,
        'key22': 22,
    },
    {
     'key21': 23,
     'key22': 24
     }]
}

my_object = level1(**data)
print(my_object.key2[0].key21) #should print 21
print(my_object.key2[1].key21) #should print 23

@nested_dataclass
class random:
    key1: int
    key2: List[int]

random_object = random(**{'key1': 1, 'key2': [1,2,3]})
print(random_object.key2) # prints [1,2,3]

Further nesting

@nested_dataclass
class level3:
    key3: List[level1]

level3(**{'key3': [data]})

Output:

level3(key3=[level1(key1=1, key2=[level2(key21=21, key22=22), level2(key21=23, key22=24)])])
Buckeye14Guy
  • 831
  • 6
  • 12
  • Just found a case where it does not work: dataclass class level3: key31: int nested_dataclass class level2: key21: level3 nested_dataclass class level1: key11: List[level2] data = { 'key11': [{ 'key21': { 'key31': 21, } }] } my_object = level1(**data) print(my_object) However, if nested_dataclass class level2: key21: List[level3] it works. I don't really understand why. Any thoughts? – Joseph Jul 22 '19 at 17:37
  • The solution I provided is very specific to the case as mentioned. Your new `level2` is not nested like the cases above where the nesting is only for a `List` of other data classes. You can modify the code I had to work for you want but to generalize it you will need far more code than that. So once again I suggest using `pydantic` – Buckeye14Guy Jul 22 '19 at 18:08
  • 1
    Thanks @Buckeye14Guy, will try to make it work and post the generalized solution – Joseph Jul 23 '19 at 09:40
1

pip install validated-dc

ValidatedDC: https://github.com/EvgeniyBurdin/validated_dc

from dataclasses import dataclass
from typing import List, Union

from validated_dc import ValidatedDC


@dataclass
class Level2(ValidatedDC):
    key21: int
    key22: int


@dataclass
class Level1(ValidatedDC):
    key1: int
    key2: List[Level2]


data = {
    'key1': 1,
    'key2': [{
        'key21': 21,
        'key22': 22,
    }]
}

my_object = Level1(**data)
assert my_object.key2[0].key21 == 21


# ----------------------------------

@dataclass
class Level1(ValidatedDC):
    key1: int
    key2: Union[Level2, List[Level2]]


my_object = Level1(**data)  # key2 - list
assert my_object.key2[0].key21 == 21

data['key2'] = {
    'key21': 21,
    'key22': 22,
}

my_object = Level1(**data)  # key2 - dict
assert my_object.key2.key21 == 21
NealWalters
  • 17,197
  • 42
  • 141
  • 251
Evgeniy_Burdin
  • 627
  • 5
  • 14
0

This does not provide how to change the decorator and if you do not wish to use any 3rd party packages then please disregard this answer. But I think that pydantic can do what you want. The only reason I am suggesting is because it will not allow you to mistakenly have key2 as a dictionary when it was declared to be a list.

from typing import List
from pydantic import BaseModel
class level2(BaseModel):
    key21: int
    key22: int

class level1(BaseModel):
    key1: int
    key2: List[level2]

data = {
    'key1': 1,
    'key2': [{
        'key21': 21,
        'key22': 22,
    }]
}

my_object = level1(**data)
print(my_object.key2[0].key21) # prints 21

If you actually wanted to have key21 directly accessible from key2 then

class level1(BaseModel):
    key1: int
    key2: level2 # Not a list

data = {
    'key1': 1,
    'key2': {
        'key21': 21,
        'key22': 22,
    }
}

my_object = level1(**data)
print(my_object.key2.key21) # prints 21

Again ignore this if your goal is successfully get the decorator to work. Otherwise installing pydantic won't hurt :)

Buckeye14Guy
  • 831
  • 6
  • 12