Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON synchronized Dictionary in Python : dealing with temporary references

I am writing a class that acts as a dictionary but saves its content to a json file each time a modification is made to ensure a synchronous state.

However I stumbled upon a special case that breaks my synchronization : when appending a value to a list inside the dictionary. Since this uses __getitem__, how can I make sure that if the item returned is modified, I save it to the JSON file ?

Here is a fully functional code snippet (Python 3.9.2) to illustrate what I mean

import json


class SyncDictJSON(dict):
    __instances: dict = {}

    @classmethod
    def create(cls, filepath: str, **kwargs):
        if filepath not in SyncDictJSON.__instances:
            SyncDictJSON.__instances[filepath] = cls(filepath, **kwargs)
        return SyncDictJSON.__instances[filepath]

    def __init__(self, filepath: str, **kwargs):
        super().__init__(**kwargs)
        self.filepath = filepath
        self.update(SyncDictJSON.read_data_from_filename(self.filepath))

    def __getitem__(self, item):
        print(f"getitem {item}")
        return super(SyncDictJSON, self).__getitem__(item)

    def __setitem__(self, key, value):
        print(f"set item {key},{value}")
        super().__setitem__(key, value)
        SyncDictJSON.write_data_to_filename(self, self.filepath)

    def __delitem__(self, key):
        super().__delitem__(key)
        SyncDictJSON.write_data_to_filename(self, self.filepath)

    @staticmethod
    def write_data_to_filename(data, filepath: str):
        with open(filepath, "w", encoding="utf-8") as file:
            json.dump(data, file, indent=2, ensure_ascii=False)

    @staticmethod
    def read_data_from_filename(filename: str):
        with open(filename, "r", encoding="utf-8") as file:
            return json.load(file)

    @classmethod
    def from_file(cls, filepath):
        return cls(filepath)


if __name__ == '__main__':
    with open("testing.json", "w") as file:
        file.write("{}")
    dico = SyncDictJSON.create("testing.json")

    dico["a_list"] = []
    dico["a_list"].append(5)

    print(dico)  # {'a_list': [5]} but testing.json will be empty

like image 650
Nathan Marotte Avatar asked Nov 07 '22 02:11

Nathan Marotte


1 Answers

The SyncDictJSON object will record changes to the dictionary. However, once the list for a_list is created, appending to it will not change the dictionary; the dictionary will contain a reference to a list [5], which is still the same reference as when that list was empty.

To record appending to the list, too, you could use the following code, which wraps the class list into a similar wrapper. It relies on a reference to the synchronized dictionary being passed down to the list, so it will require a little extra space. If you're planning to delete list items and/or insert them, you would need to overwrite those methods in SyncList, too.

import json

class SyncList(list):
    def __init__(self, container, *args, **kwargs):
        print('list init', *args, **kwargs)
        self._container = container
        super().__init__(*args, **kwargs)

    def append(self, x):
        print('list.append', self, x)
        super().append(x)
        self._container.write_data_to_filename(
            self._container, self._container.filepath)


class SyncDictJSON(dict):
    __instances: dict = {}

    @classmethod
    def create(cls, filepath: str, **kwargs):
        if filepath not in SyncDictJSON.__instances:
            SyncDictJSON.__instances[filepath] = cls(filepath, **kwargs)
        return SyncDictJSON.__instances[filepath]

    def __init__(self, filepath: str, **kwargs):
        super().__init__(**kwargs)
        self.filepath = filepath
        self.update(SyncDictJSON.read_data_from_filename(self.filepath))

    def __getitem__(self, item):
        print(f"getitem {item}")
        return super(SyncDictJSON, self).__getitem__(item)

    def __setitem__(self, key, value):
        print(f"set item {key},{value}")
        super().__setitem__(key, value)
        SyncDictJSON.write_data_to_filename(self, self.filepath)

    def __delitem__(self, key):
        super().__delitem__(key)
        SyncDictJSON.write_data_to_filename(self, self.filepath)

    @staticmethod
    def write_data_to_filename(data, filepath: str):
        with open(filepath, "w", encoding="utf-8") as file:
            json.dump(data, file, indent=2, ensure_ascii=False)

    @staticmethod
    def read_data_from_filename(filename: str):
        with open(filename, "r", encoding="utf-8") as file:
            return json.load(file)

    @classmethod
    def from_file(cls, filepath):
        return cls(filepath)


if __name__ == '__main__':
    with open("testing.json", "w") as file:
        file.write("{}")
    dico = SyncDictJSON.create("testing.json")
    dico["a_list"] = SyncList(dico, [])
    dico["a_list"].append(5)
    print(dico)  # {'a_list': [5]}; testing.json will have the same
# list init []
# set item a_list,[]
# getitem a_list
# list.append [] 5
# {'a_list': [5]}

like image 183
VirtualScooter Avatar answered Nov 11 '22 06:11

VirtualScooter