makinacorpus / easydict

Access dict values as attributes (works recursively)
GNU Lesser General Public License v3.0
293 stars 48 forks source link

Cannot use the dict() function to convert an easydict object to a regular dictionary #48

Open WaterLoran opened 4 months ago

WaterLoran commented 4 months ago
    good = {
        'node': {
            'type': 'activity',
            'id': 'nG6CFOqb4dQq',
            'diagramId': '4wwTVGk0mPhGtfDI',
            'renderKey': 'activity',
            'x': 180,
            'y': 200,
            'width': 100,
            'height': 60,
            'angle': 0,
            'attrs': {
                'body': {
                    'fill': 'none',
                    'stroke': 'none',
                    'refWidth': '100%',
                    'refHeight': '100%'
                },
                'fo': {
                    'refWidth': '100%',
                    'refHeight': '100%'
                },
                'label': {
                    'fontSize': 14,
                    'fill': '#333',
                    'refX': '50%',
                    'refY': '50%',
                    'textAnchor': 'middle',
                    'textVerticalAnchor': 'middle'
                },
                'shapeText': {
                    'verticalAlign': 'middle',
                    'textAlign': 'center',
                    'fill': '#000000',
                    'fontFamily': '宋体',
                    'fontSize': 12,
                    'lineHeight': 1.2,
                    'text': '活动',
                    'fontWeight': 'normal'
                },
                'shapeStyle': {
                    'strokeWidth': 1,
                    'fill': {
                        'startColor': '#89BCFF',
                        'endColor': '#C0DCFF',
                        'direction': 'to bottom'
                    },
                    'stroke': '#000000',
                    'strokeDasharray': '',
                    '3dEffect': False
                }
            },
            'extraProps': {},
            'ports': {
                'items': [{
                    'group': 'top',
                    'id': 'rrbr7WGHuRHq'
                }, {
                    'group': 'right',
                    'id': 'WCfPQPY3qVVs'
                }, {
                    'group': 'bottom',
                    'id': 'XxGbrm8p3ywp'
                }, {
                    'group': 'left',
                    'id': 'iVxkmHyURPG9'
                }],
                'groups': {
                    'top': {
                        'position': {
                            'name': 'top'
                        },
                        'zIndex': 10
                    },
                    'right': {
                        'position': {
                            'name': 'right'
                        },
                        'zIndex': 10
                    },
                    'bottom': {
                        'position': {
                            'name': 'bottom'
                        },
                        'zIndex': 10
                    },
                    'left': {
                        'position': {
                            'name': 'left'
                        },
                        'zIndex': 10
                    }
                }
            },
            'data': {
                'number': None,
                'desc': None,
                'category': None,
                'executionRole': {
                    'roleId': '2XfVV5q3IPik',
                    'roleText': '角色'
                },
                'keyActivity': None,
                'input': [{
                    'id': 3056,
                    'name': 'file_a.docx',
                    'nameEn': None,
                    'number': None,
                    'parentId': 3055,
                    'sortId': 0,
                    'type': 'STD_FILE',
                    'path': '3054-3055-3056-',
                    'namePath': '架构_batch_upload_files_process_001/流程图_batch_upload_files_process_001/file_a.docx',
                    'parentName': None,
                    'approvalStatus': None,
                    'isModified': None,
                    'hasChildren': None,
                    'isPublished': False,
                    'recordStatus': 0,
                    'level': 2,
                    'viewSort': '016100000000',
                    'isPublic': 0,
                    'extraProps': {
                        'relatedFileId': 3055
                    },
                    'processId': None,
                    'supportRiskDirId': None,
                    'institutionId': None,
                    'modifiedTime': None,
                    'supportFileDirId': 3055,
                    'deptId': None,
                    'pubTime': None
                }, {
                    'id': 3057,
                    'name': 'file_b.docx',
                    'nameEn': None,
                    'number': None,
                    'parentId': 3055,
                    'sortId': 1,
                    'type': 'STD_FILE',
                    'path': '3054-3055-3057-',
                    'namePath': '架构_batch_upload_files_process_001/流程图_batch_upload_files_process_001/file_b.docx',
                    'parentName': None,
                    'approvalStatus': None,
                    'isModified': None,
                    'hasChildren': None,
                    'isPublished': False,
                    'recordStatus': 0,
                    'level': 2,
                    'viewSort': '016100000001',
                    'isPublic': 0,
                    'extraProps': {
                        'relatedFileId': 3055
                    },
                    'processId': None,
                    'supportRiskDirId': None,
                    'institutionId': None,
                    'modifiedTime': None,
                    'supportFileDirId': 3055,
                    'deptId': None,
                    'pubTime': None
                }, {
                    'id': 3058,
                    'name': 'file_c.docx',
                    'nameEn': None,
                    'number': None,
                    'parentId': 3055,
                    'sortId': 2,
                    'type': 'STD_FILE',
                    'path': '3054-3055-3058-',
                    'namePath': '架构_batch_upload_files_process_001/流程图_batch_upload_files_process_001/file_c.docx',
                    'parentName': None,
                    'approvalStatus': None,
                    'isModified': None,
                    'hasChildren': None,
                    'isPublished': False,
                    'recordStatus': 0,
                    'level': 2,
                    'viewSort': '016100000002',
                    'isPublic': 0,
                    'extraProps': {
                        'relatedFileId': 3055
                    },
                    'processId': None,
                    'supportRiskDirId': None,
                    'institutionId': None,
                    'modifiedTime': None,
                    'supportFileDirId': 3055,
                    'deptId': None,
                    'pubTime': None
                }],
                'output': None,
                'sopList': [],
                'controlPoints': [],
                'standards': [],
                'indicators': [],
                'itSystems': [],
                'processingTimeLimit': None
            },
            'zIndex': 5
        }
    }
    print("kwargs: " + json.dumps(good))
    from easydict import EasyDict
    a = EasyDict(good)
    a = dict(a)
    print("kwargs: " + json.dumps(a))

############################################# I think it will be a problem that needs to be solved because it is not possible to revert back to a regular dict, which may result in certain scenarios where other third-party libraries cannot be used. Because of the code design, sometimes I may need to first convert it to a regular dictionary, process it, and then revert back

WaterLoran commented 4 months ago

If we use this easydict related data to send the function to the send function of the request function, it may also cause problems

WaterLoran commented 4 months ago

Alternatively, I would expect to provide a method for this class, such as XX. change_to_dict(), and then help me switch back to a regular dictionary

WaterLoran commented 4 months ago

image 大概是处理不了这种列表类型的

WaterLoran commented 4 months ago

I can't help it, I was too eager to use it, so I wrote the method for rebuilding the dictionary myself.

Can you also let me join this project so that I can provide the relevant code

The code is as follows

def rebuild_dict(self):
    def iter_node(data):
        if isinstance(data, self.__class__):
            new_dict = {}
            for key, value in data.__dict__.items():
                if key == "rebuild_dict":
                    pass  # 类内置函数, 不做递归处理
                else:
                    if isinstance(value, self.__class__):
                        new_dict[key] = iter_node(value)
                    elif isinstance(value, dict):
                        new_dict[key] = iter_node(value)
                    elif isinstance(value, list):  # 列表里面为列表的情况:
                        new_dict[key] = iter_node(value)
                    else:
                        new_dict[key] = value
            return new_dict
        elif isinstance(data, dict):
            new_dict = {}
            for key, value in data.items():
                if key == "rebuild_dict":
                    pass
                else:
                    if isinstance(value, self.__class__):
                        new_dict[key] = iter_node(value)
                    elif isinstance(value, dict):  # 列表里面为字典的情况
                        new_dict[key] = iter_node(value)
                    elif isinstance(value, list):  # 列表里面为列表的情况:
                        new_dict[key] = iter_node(value)
                    else:
                        new_dict[key] = value
            return new_dict
        elif isinstance(data, list):
            new_list = []
            for item in data:
                if isinstance(item, self.__class__):
                    new_list.append(iter_node(item))
                elif isinstance(item, dict):  # 列表里面为字典的情况
                    new_list.append(iter_node(item))
                elif isinstance(item, list):  # 列表里面为列表的情况:
                    new_list.append(iter_node(item))
                else:
                    new_list.append(item)
            return new_list
        else:
            raise
    return iter_node(self)
WaterLoran commented 4 months ago

class EasyDict(dict): """ Get attributes

>>> d = EasyDict({'foo':3})
>>> d['foo']
3
>>> d.foo
3
>>> d.bar
Traceback (most recent call last):
...
AttributeError: 'EasyDict' object has no attribute 'bar'

Works recursively

>>> d = EasyDict({'foo':3, 'bar':{'x':1, 'y':2}})
>>> isinstance(d.bar, dict)
True
>>> d.bar.x
1

Bullet-proof

>>> EasyDict({})
{}
>>> EasyDict(d={})
{}
>>> EasyDict(None)
{}
>>> d = {'a': 1}
>>> EasyDict(**d)
{'a': 1}
>>> EasyDict((('a', 1), ('b', 2)))
{'a': 1, 'b': 2}

Set attributes

>>> d = EasyDict()
>>> d.foo = 3
>>> d.foo
3
>>> d.bar = {'prop': 'value'}
>>> d.bar.prop
'value'
>>> d
{'foo': 3, 'bar': {'prop': 'value'}}
>>> d.bar.prop = 'newer'
>>> d.bar.prop
'newer'

Values extraction

>>> d = EasyDict({'foo':0, 'bar':[{'x':1, 'y':2}, {'x':3, 'y':4}]})
>>> isinstance(d.bar, list)
True
>>> from operator import attrgetter
>>> list(map(attrgetter('x'), d.bar))
[1, 3]
>>> list(map(attrgetter('y'), d.bar))
[2, 4]
>>> d = EasyDict()
>>> list(d.keys())
[]
>>> d = EasyDict(foo=3, bar=dict(x=1, y=2))
>>> d.foo
3
>>> d.bar.x
1

Still like a dict though

>>> o = EasyDict({'clean':True})
>>> list(o.items())
[('clean', True)]

And like a class

>>> class Flower(EasyDict):
...     power = 1
...
>>> f = Flower()
>>> f.power
1
>>> f = Flower({'height': 12})
>>> f.height
12
>>> f['power']
1
>>> sorted(f.keys())
['height', 'power']

update and pop items
>>> d = EasyDict(a=1, b='2')
>>> e = EasyDict(c=3.0, a=9.0)
>>> d.update(e)
>>> d.c
3.0
>>> d['c']
3.0
>>> d.get('c')
3.0
>>> d.update(a=4, b=4)
>>> d.b
4
>>> d.pop('a')
4
>>> d.a
Traceback (most recent call last):
...
AttributeError: 'EasyDict' object has no attribute 'a'
"""

def __init__(self, d=None, **kwargs):
    if d is None:
        d = {}
    else:
        d = dict(d)
    if kwargs:
        d.update(**kwargs)
    for k, v in d.items():
        setattr(self, k, v)
    # Class attributes
    for k in self.__class__.__dict__.keys():
        if not (k.startswith('__') and k.endswith('__')) and not k in ('update', 'pop', 'rebuild_dict'):
            setattr(self, k, getattr(self, k))

def __setattr__(self, name, value):
    if isinstance(value, (list, tuple)):
        value = [self.__class__(x)
                 if isinstance(x, dict) else x for x in value]
    elif isinstance(value, dict) and not isinstance(value, self.__class__):
        value = self.__class__(value)
    super(EasyDict, self).__setattr__(name, value)
    super(EasyDict, self).__setitem__(name, value)

__setitem__ = __setattr__

def update(self, e=None, **f):
    d = e or dict()
    d.update(f)
    for k in d:
        setattr(self, k, d[k])

def pop(self, k, d=None):
    delattr(self, k)
    return super(EasyDict, self).pop(k, d)

def rebuild_dict(self):
    def iter_node(data):
        if isinstance(data, self.__class__):
            new_dict = {}
            for key, value in data.__dict__.items():
                if isinstance(value, self.__class__):
                    new_dict[key] = iter_node(value)
                elif isinstance(value, dict):
                    new_dict[key] = iter_node(value)
                elif isinstance(value, list):  # 列表里面为列表的情况:
                    new_dict[key] = iter_node(value)
                else:
                    new_dict[key] = value
            return new_dict
        elif isinstance(data, dict):
            new_dict = {}
            for key, value in data.items():
                if isinstance(value, self.__class__):
                    new_dict[key] = iter_node(value)
                elif isinstance(value, dict):  # 列表里面为字典的情况
                    new_dict[key] = iter_node(value)
                elif isinstance(value, list):  # 列表里面为列表的情况:
                    new_dict[key] = iter_node(value)
                else:
                    new_dict[key] = value
            return new_dict
        elif isinstance(data, list):
            new_list = []
            for item in data:
                if isinstance(item, self.__class__):
                    new_list.append(iter_node(item))
                elif isinstance(item, dict):  # 列表里面为字典的情况
                    new_list.append(iter_node(item))
                elif isinstance(item, list):  # 列表里面为列表的情况:
                    new_list.append(iter_node(item))
                else:
                    new_list.append(item)
            return new_list
        else:
            raise

    return iter_node(self)

if name == "main": import doctest

doctest.testmod()
finemap commented 3 months ago

You can use deepcopy to recursively revert EasyDict to built-in dict

deepcopy is able to use dict to rebuild EasyDict instance recursively no matter where and how deep it exists in the instance to be copied, including in tuples, lists, dicts, and of course in EasyDicts.

first override __reduce__ method to tell deepcopy to use dict to build a deepcopy.

def __reduce__(self):
    return dict, (self.__dict__,)

and you can now

ezdict = EasyDict(items=[EasyDict(items='something')])

from copy import deepcopy
ezdict = deepcopy(ezdict)

and get a copied instance with every EasyDict node reverted to dict.

In your implementation, you define a new instance method rebuild_dict(), this will lead to side effect that a key named rebuild_dict is to be added in __dict__ of EasyDIct instances and contaminate the original content in EasyDict instances and may cause infinite recursion when binding this method to instance.

To avoid such side effect, you may handle it in a magic method way

def __reduce__(self):
    return dict, (self.__dict__,)

def __asdict__(self, recurse=True):
    if recurse:
        from copy import deepcopy
        return deepcopy(self.__dict__)
    else:
        return dict(self.__dict__)

and use it like:

dct = ezdict.__asdict__()

for type reversion.

Note: overriding __reduce__ will change pickle behavior.

If pickle compatible feature should be kept, an implementation could be:

def __asdict__(self, recurse=True):
    if recurse:
        _r = self.__class__.__reduce__
        self.__class__.__reduce__ = lambda obj: (dict, (obj.__dict__,),)
        from copy import deepcopy
        _c = deepcopy(self.__dict__)
        self.__class__.__reduce__ = _r
        return _c
    else:
        return dict(self.__dict__)
WaterLoran commented 3 months ago

It looks very good code. Thank you very much for your patient guidance. I will read this code a few more times

WaterLoran commented 3 months ago

I feel very happy being answered on GitHub. Thank you

WaterLoran commented 3 months ago

I just discovered a problem, which is that if the value of a key in this data object is an io_buffer, an error will occur, The error is as follows: err:cannot pickle '_io.BufferedReader' object,uri:csvc-file/files/chunk-upload @finemap

WaterLoran commented 3 months ago

Emmm, the solution is probably to make a targeted judgment. You can't use dict directly. Let me see how to change it.