Skip to content

Instantly share code, notes, and snippets.

@markhu
Last active August 15, 2023 11:54
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save markhu/fbbab71359af00e527d0 to your computer and use it in GitHub Desktop.
Save markhu/fbbab71359af00e527d0 to your computer and use it in GitHub Desktop.
edict: load JSON into Python object but access with .dot notation
class edict(dict): # Similar to bunch, but less, and JSON-centric
# based on class dotdict(dict): # from http://stackoverflow.com/questions/224026/dot-notation-for-dictionary-keys
__setattr__= dict.__setitem__ # TBD: support assignment of nested dicts by overriding this?
__delattr__= dict.__delitem__
def __init__(self, data):
if type(data) in ( unicode, str ):
data = json.loads( data)
for name, value in data.iteritems():
setattr(self, name, self._wrap(value))
def __getattr__(self, attr):
return self.get(attr, None)
def _wrap(self, value): # from class Struct by XEye '11 http://stackoverflow.com/questions/1305532/convert-python-dict-to-object
if isinstance(value, (tuple, list, set, frozenset)):
return type(value)([self._wrap(v) for v in value]) # recursion!
else:
if isinstance(value, dict):
return edict(value) # is there a relative way to get class name?
else:
return value
# optional handy member function(s): pretty-printer, ... left as an exercise for the reader...
# def subset(self, only_keys=[], exclude_keys=[], as='json', indent=2, separators=(',',':')):
# EOF
@markhu
Copy link
Author

markhu commented Feb 17, 2016

Sample usage:

import edict

ed=edict.edict({"a":"b","k":"v","obj":{"sk":"sv"}})

print ed.obj.sk  # >> 'sv'

ed.subb=edict.edict({"more": ed.obj})  # populate from self as an example

print ed.subb.more  # >> {'sk': 'sv'}

@pallavrustogi
Copy link

Getting an error: TypeError: descriptor 'setitem' for 'dict' objects doesn't apply to 'edict' object

@caffeinatedMike
Copy link

@markhu If I may, I'd like to suggest an improved version that allows the usage of getattr for nested values that mimics that of a completely flattened json object.

from functools import reduce
import json

class edict(dict):
    
    __setattr__= dict.__setitem__
    __delattr__= dict.__delitem__

    def __init__(self, data):
        if isinstance(data, str):
            data = json.loads(data)
    
        for name, value in data.items():
            setattr(self, name, self._wrap(value))

    def __getattr__(self, attr):
        def _traverse(obj, attr):
            if self._is_indexable(obj):
                try:
                    return obj[int(attr)]
                except:
                    return None
            elif isinstance(obj, dict):
                return obj.get(attr, None)
            else:
                return attr
        if '.' in attr:
            return reduce(_traverse, attr.split('.'), self)
        return self.get(attr, None)

    def _wrap(self, value):
        if self._is_indexable(value):
            # (!) recursive (!)
            return type(value)([self._wrap(v) for v in value])
        elif isinstance(value, dict):
            return edict(value)
        else:
            return value
    
    @staticmethod
    def _is_indexable(obj):
        return isinstance(obj, (tuple, list, set, frozenset))
                
test_dict = {
    "dimensions": {
        "length": "112",
        "width": "103",
        "height": "42"
    },
    "meta_data": [
        {
            "id": 11089769,
            "key": "imported_gallery_files",
            "value": [
                "https://abc.com/wp-content/uploads/2019/09/unnamed-3.jpg",
                "https://abc.com/wp-content/uploads/2019/09/unnamed-2.jpg",
                "https://abc.com/wp-content/uploads/2019/09/unnamed-4.jpg"
            ]
        }
    ]
}
dotted_dict = edict(test_dict)
print(dotted_dict.dimensions.length) # => '112'
print(getattr(dotted_dict, 'dimensions.length')) # => '112'
print(dotted_dict.meta_data[0].key) # => 'imported_gallery_files'
print(getattr(dotted_dict, 'meta_data.0.key')) # => 'imported_gallery_files'
print(dotted_dict.meta_data[0].value) # => ['link1','link2','link2']
print(getattr(dotted_dict, 'meta_data.0.value')) # => ['link1','link2','link3']
print(dotted_dict.meta_data[0].value[2]) # => 'link3'
print(getattr(dotted_dict, 'meta_data.0.value.2')) # => 'link3'

@simkimsia
Copy link

👍 🙌 to you @caffeinatedMike

@vv3d0x
Copy link

vv3d0x commented Aug 15, 2023

@caffeinatedMike This code snippet looks good, but what if you want to add ability if value is None: return default_value
I am new in python and do not know other solution, my modification:

class JsonOptions(dict):
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

    def __init__(self, payload):
        super().__init__()

        if isinstance(payload, str):
            payload = json.loads(payload)

        for name, value in payload.items():
            setattr(self, name, self._wrap(value))

    def __getattr__(self, attr: str):
        return self.attr(attr)

    def _wrap(self, value):
        if self._is_indexable(value):
            # (!) recursive (!)
            return type(value)([self._wrap(v) for v in value])
        elif isinstance(value, dict):
            return JsonOptions(value)
        else:
            return value

    def attr(self, attr: str, default=None):
        def _traverse(o, name):
            if o is None:
                return None

            if self._is_indexable(o):
                try:
                    return o[int(name)]
                except (IndexError, ValueError):
                    return None
            elif isinstance(o, dict):
                return o.get(name, None)
            else:
                return None

        if '.' in attr:
            value = reduce(_traverse, attr.split('.'), self)
        else:
            value = self.get(attr, None)

        return default if value is None else value

    @staticmethod
    def _is_indexable(o):
        return isinstance(o, (tuple, list, set, frozenset))

P.S. is there a more nice solution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment