-1

I'm wondering if what I am trying to do is even possible or if perhaps my approach to this is just completely wrong.

Let's say we've got 3 dataclasses:

@dataclass
class MyFirstClass:
    my_int: int = 1


@dataclass
class MySecondClass:
    first_class: MyFirstClass = field(default_factory=MyFirstClass)
    my_bool: bool = False


@dataclass
class MainClass:
    second_class: MySecondClass = field(default_factory=MySecondClass)
    my_str: str = 'Original String'

And now I want to update a value in any given class:

def test_this():
    # make copy of dataclass
    data_object = deepcopy(MainClass())

    # first print as is
    print(f'\n{asdict(data_object)}')

    # update first class
    data_object.second_class.first_class.my_int = 2

    # update second class
    data_object.second_class.my_bool = True

    # update main class
    data_object.my_str = 'Updated String'

    # print updated object
    print(f'\n{asdict(data_object)}')

Which would output this:

Original:
{'second_class': {'first_class': {'my_int': 1}, 'my_bool': False}, 'my_str': 'Original String'}

Updated:
{'second_class': {'first_class': {'my_int': 2}, 'my_bool': True}, 'my_str': 'Updated String'}

I really like the clean approach of doing something like this:

# make copy of dataclass
data_object = deepcopy(MainClass())

data_object.second_class.first_class.my_int = 2

as opposed to this:

data_object = replace(MainClass(), second_class=MySecondClass(first_class=MyFirstClass(my_int=2)))

And basically, I have tests that I want to run through with different data for each field of each dataclass and so I am wondering if there's some way to dynamically "get" the correct field to be updated?

I thought of initially looping through the fields of the object created, like this:

def test_this():
    # make copy of dataclass
    data_object = deepcopy(MainClass())

    # loop through dataclass
    for field in fields(data_object):
        print(f'\nField: {field}')
        print(f'\nField Name: {field.name}')
        print(f'Field Type: {field.type}')
        print(f'Field Value: {getattr(data_object, field.name)}\n')

Which outputs this:

Field: Field(name='second_class',type=<class 'tests.sample_test_four.MySecondClass'>,default=<dataclasses._MISSING_TYPE object at 0x106943c10>,default_factory=<class 'tests.sample_test_four.MySecondClass'>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD)

Field Name: second_class
Field Type: <class 'tests.sample_test_four.MySecondClass'>
Field Value: MySecondClass(first_class=MyFirstClass(my_int=1), my_bool=False)


Field: Field(name='my_str',type=<class 'str'>,default='Original String',default_factory=<dataclasses._MISSING_TYPE object at 0x106943c10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD)

Field Name: my_str
Field Type: <class 'str'>
Field Value: Original String

But looping through the MainClass object only shows second_class and my_str as available fields and not all the fields within the nested classes.

I thought then perhaps I could do a conversion of the dataclass into a dict and from there use a deep_update and end up with something like:

Using pydantic

def test_this():
    # create initial dict
    data_object = asdict(deepcopy(MainClass()))

    # first print as is
    print(f'\nOriginal:\n{data_object}')

    # update dict
    updated_dict = deep_update(data_object, {'my_int': 2})

    # print updated
    print(f'\nUpdated:\n{updated_dict}')

Or creating my own update function:

Update value of a nested dictionary of varying depth

def update(d, u):
    for k, v in u.items():
        if isinstance(v, collections.abc.Mapping):
            d[k] = update(d.get(k, {}), v)
        else:
            d[k] = v
    return d

Which both outputs this:

Original:
{'second_class': {'first_class': {'my_int': 1}, 'my_bool': False}, 'my_str': 'Original String'}

Updated:
{'second_class': {'first_class': {'my_int': 1}, 'my_bool': False}, 'my_str': 'Original String', 'my_int': 2}

The problem is that it didn't update the nested field, instead it added a new one in the outer-most dict

Also, with this approach, I fear that if any of the classes have identically named fields, then which one would be updated?

So I'm not sure if what I am trying to do is possible vs the effort involved in getting the desired outcome OR if I should approach it differently? Maybe use JSON files instead of dataclasses and do something from there?

Eitel Dagnin
  • 869
  • 3
  • 19
  • 46

1 Answers1

-1

I changed my approach and I am not using dataclasses to manage the data injection for this particular use case.

Since I was anyway immediately converting the dataclass into a JSON object, I figured I would just use JSON files for storing and injecting the data I need and update the dict accordingly.

Credit to: @Gustavo Alves Casqueiro for original answer

Here's the function:

def update(dictionary: dict[str, any], key: str, value: any, nested_dict_name: str = None) -> dict[str, any]:
    if not nested_dict_name:  # if current (outermost) dict should be updated
        if key in dictionary.keys():  # check if key exists in current dict
            dictionary[key] = value
            return dictionary
    else:  # if nested dict should be updated
        if nested_dict_name in dictionary.keys():  # check if dict is in next layer
            if isinstance(dictionary[nested_dict_name], dict):
                if key in dictionary[nested_dict_name].keys():  # check if key exists in current dict
                    dictionary[nested_dict_name][key] = value
                    return dictionary
            if isinstance(dictionary[nested_dict_name], list):
                list_index = random.choice(range(len(dictionary[nested_dict_name])))  # pick a random dict from the list

                if key in dictionary[nested_dict_name][list_index].keys():  # check if key exists in current dict
                    dictionary[nested_dict_name][list_index][key] = value
                    return dictionary

    dic_aux = []

    # this would only run IF the above if-statement was not able to identity and update a dict
    for val_aux in dictionary.values():
        if isinstance(val_aux, dict):
            dic_aux.append(val_aux)

    # call the update function again for recursion
    for i in dic_aux:
        return update(dictionary=i, key=key, value=value, nested_dict_name=nested_dict_name)

The full solution can be found here: Update value of a nested dictionary of varying depth

This is where I found MANY available options to do what I needed to do.

I honestly would have preferred using a lib that could do the heavy lifting for me, but I just couldn't find something that did what I needed.

E_net4 - Krabbe mit Hüten
  • 24,143
  • 12
  • 85
  • 121
Eitel Dagnin
  • 869
  • 3
  • 19
  • 46