├── .gitignore ├── README ├── ddict_t.py └── ddict.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Description: 2 | ------------ 3 | 4 | DotDict is an extension of python dictionary which uses and operates 5 | with dot notations. For example, DotDict.get('a.b.c') or simply 6 | DotDict['a.b.c']. It extends set/get operations to set/assign new 7 | values, as well as provide get_keys/get_values/delete APIs. It also 8 | smart enough to work with complex dict structures. Here is a few 9 | examples: 10 | 11 | from ddict import DotDict 12 | row = {'a':{'b':1, 'c':[1,2]}} 13 | rec = DotDict(row) 14 | print rec['a.c'] 15 | [1,2] 16 | rec['x.y.z'] = 1 17 | print rec 18 | {'a':{'b':1, 'c':[1,2]}, 'x': {'y': {'z': 1}} 19 | 20 | For a complete list of examples, see ddict_t.py unit test module. 21 | 22 | -------------------------------------------------------------------------------- /ddict_t.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #pylint: disable-msg=C0301,C0103 3 | 4 | """ 5 | Unit test for DotDict module 6 | """ 7 | 8 | import os 9 | import sys 10 | import unittest 11 | from ddict import DotDict 12 | 13 | class Test_DotDict(unittest.TestCase): 14 | """ 15 | A test class for the DotDict module 16 | """ 17 | def setUp(self): 18 | """ 19 | set up DAS core module 20 | """ 21 | self.rec1 = {'a':{'b':1, 'c':[1,2]}} 22 | self.rec2 = {'a':[{'b':1, 'c':[1,2]}, {'b':10, 'c':[10,20]}]} 23 | 24 | def test_get_keys(self): 25 | """test get_keys method""" 26 | rec = DotDict(self.rec1) 27 | expect = ['a', 'a.b', 'a.c'] 28 | expect.sort() 29 | result = rec.get_keys() 30 | result.sort() 31 | self.assertEqual(expect, result) 32 | 33 | rec = DotDict(self.rec2) 34 | result = rec.get_keys() 35 | result.sort() 36 | self.assertEqual(expect, result) 37 | 38 | def test_get(self): 39 | """test get method""" 40 | rec = DotDict(self.rec1) 41 | expect = [1,2] 42 | result = rec.get('a.c') 43 | self.assertEqual(expect, result) 44 | self.assertEqual(expect, rec['a.c']) 45 | 46 | def test_get_values(self): 47 | """test get_values method""" 48 | rec = DotDict(self.rec2) 49 | expect = [1, 2, 10, 20] 50 | result = [o for o in rec.get_values('a.c')] 51 | self.assertEqual(expect, result) 52 | 53 | def test_set(self): 54 | """test set method""" 55 | rec = DotDict(self.rec1) 56 | rec['a.d'] = 111 57 | self.assertEqual(rec['a.d'], 111) 58 | self.assertRaises(Exception, rec.set, ('a.c.d', 1)) 59 | 60 | def test_delete(self): 61 | """test delete method""" 62 | rec = DotDict(self.rec1) 63 | rec.delete('a.c') 64 | expect = {'a':{'b':1}} 65 | self.assertEqual(expect, rec) 66 | 67 | def test_DotDict(self): 68 | """Test DotDict class""" 69 | res = {u'zip' : {u'code':u'14850'}} 70 | mdict = DotDict(res) 71 | mdict['zip.code'] = 14850 72 | expect = {u'zip' : {u'code':14850}} 73 | self.assertEqual(expect, mdict) 74 | 75 | res = {'a':{'b':{'c':10}, 'd':10}} 76 | mdict = DotDict(res) 77 | mdict['x.y.z'] = 10 78 | expect = {'a':{'b':{'c':10}, 'd':10}, 'x':{'y':{'z':10}}} 79 | self.assertEqual(expect, mdict) 80 | 81 | mdict['a.b.k.m'] = 10 82 | expect = {'a':{'b':{'c':10, 'k':{'m':10}}, 'd':10}, 'x':{'y':{'z':10}}} 83 | self.assertEqual(expect, mdict) 84 | expect = 10 85 | result = mdict.get('a.b.k.m') 86 | self.assertEqual(expect, result) 87 | 88 | res = {'a':{'b':{'c':10}, 'd':[{'x':1}, {'x':2}]}} 89 | mdict = DotDict(res) 90 | expect = 1 91 | result = mdict.get('a.d.x') 92 | self.assertEqual(expect, result) 93 | expect = None 94 | result = mdict.get('a.M.Z') 95 | self.assertEqual(expect, result) 96 | 97 | res = {'a': {'b': {'c':1, 'd':2}}} 98 | mdict = DotDict(res) 99 | expect = {'a': {'b': {'c':1}}} 100 | mdict.delete('a.b.d') 101 | self.assertEqual(expect, mdict) 102 | 103 | def test_DotDict_list(self): 104 | """Test DotDict class""" 105 | res = {'a':[{'b':1, 'c':1}, {'c':1}]} 106 | mdict = DotDict(res) 107 | expect = 1 108 | result = mdict.get('a.b') 109 | self.assertEqual(expect, result) 110 | 111 | res = {'a':[{'c':1}, {'b':1, 'c':1}]} 112 | mdict = DotDict(res) 113 | expect = 1 114 | result = mdict.get('a.b') 115 | self.assertEqual(expect, result) 116 | 117 | def test_DotDict_values(self): 118 | """Test DotDict get_values method""" 119 | res = {'a':[{'b':1, 'c':1}, {'c':2}]} 120 | mdict = DotDict(res) 121 | expect = [1] 122 | result = [r for r in mdict.get_values('a.b')] 123 | self.assertEqual(expect, result) 124 | 125 | expect = [1,2] 126 | result = [r for r in mdict.get_values('a.c')] 127 | self.assertEqual(expect, result) 128 | 129 | res = {'a':[{'b': [{'c':2}, {'c':3}]}, {'b': [{'c':4}, {'c':5}]}]} 130 | mdict = DotDict(res) 131 | expect = [2,3,4,5] 132 | result = [r for r in mdict.get_values('a.b.c')] 133 | self.assertEqual(expect, result) 134 | 135 | def test_DotDict_keys(self): 136 | """Test DotDict get_keys method""" 137 | res = {'a':[{'b':1, 'c':1}, {'c':2}]} 138 | mdict = DotDict(res) 139 | expect = ['a.b', 'a.c'] 140 | result = [r for r in mdict.get_keys('a')] 141 | self.assertEqual(set(expect), set(result)) 142 | 143 | res = {'a':[{'b': [{'c':2}, {'c':{'d':1}}]}, 144 | {'b': [{'c':4}, {'c':5}]}]} 145 | mdict = DotDict(res) 146 | expect = ['a.b', 'a.b.c', 'a.b.c.d'] 147 | result = [r for r in mdict.get_keys('a')] 148 | self.assertEqual(set(expect), set(result)) 149 | # 150 | # main 151 | # 152 | if __name__ == '__main__': 153 | unittest.main() 154 | 155 | -------------------------------------------------------------------------------- /ddict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: ISO-8859-1 -*- 3 | """ 4 | File: DotDict.py 5 | Author: Valentin Kuznetsov 6 | 7 | Description: 8 | ------------ 9 | 10 | DotDict is an extension of python dictionary which uses and operates 11 | with dot notations. For example, DotDict.get('a.b.c') or simply 12 | DotDict['a.b.c']. It extends set/get operations to set/assign new 13 | values, as well as provide get_keys/get_values/delete APIs. It also 14 | smart enough to work with complex dict structures. Here is a few 15 | examples: 16 | 17 | .. doctest:: 18 | 19 | from ddict import DotDict 20 | row = {'a':{'b':1, 'c':[1,2]}} 21 | rec = DotDict(row) 22 | print rec['a.c'] 23 | [1,2] 24 | rec['x.y.z'] = 1 25 | print rec 26 | {'a':{'b':1, 'c':[1,2]}, 'x': {'y': {'z': 1}} 27 | 28 | For a complete list of examples, see DotDict_t.py unit test module. 29 | """ 30 | 31 | from types import GeneratorType 32 | 33 | def isdictinstance(obj): 34 | """ 35 | Return if provided object is type of dict or instance of DotDict class 36 | """ 37 | return isinstance(obj, dict) or isinstance(obj, DotDict) 38 | 39 | def convert_dot_notation(key, val): 40 | """ 41 | Take provided key/value pair and convert it into dict if it 42 | is required. 43 | """ 44 | split_list = key.split('.') 45 | if len(split_list) == 1: # no dot notation found 46 | return key, val 47 | split_list.reverse() 48 | newval = val 49 | item = None 50 | for item in split_list: 51 | if item == split_list[-1]: 52 | return item, newval 53 | newval = {item:newval} 54 | return item, newval 55 | 56 | def yield_obj(rdict, ckey): 57 | """ 58 | Helper function for DotDict class. For a given dict and compound key, 59 | e.g. a.b.c, extract and yield next key and its object(s). 60 | """ 61 | keys = ckey.split('.') 62 | key = keys[0] 63 | if len(keys) > 1: 64 | next_key = '.'.join(keys[1:]) 65 | else: 66 | next_key = None 67 | if rdict.has_key(key): 68 | obj = rdict[key] 69 | if isinstance(obj, list) or isinstance(obj, GeneratorType): 70 | for item in obj: 71 | yield next_key, item 72 | else: 73 | yield next_key, obj 74 | 75 | def helper_loop(combo, vals): 76 | "Helper function" 77 | if isinstance(vals, dict): 78 | for kkk in DotDict(vals).get_keys(): 79 | yield '%s.%s' % (combo, kkk) 80 | elif isinstance(vals, list): 81 | for item in vals: 82 | if isinstance(item, dict): 83 | for kkk in DotDict(item).get_keys(): 84 | yield '%s.%s' % (combo, kkk) 85 | 86 | class DotDict(dict): 87 | """ 88 | Access python dictionaries via dot notations, original idea taken from 89 | http://parand.com/say/index.php/2008/10/24/python-dot-notation-dictionary-access/ 90 | Class has been extended with helper method to use compound keys, e.g. a.b.c. 91 | All extended method follow standard python dictionary syntax. 92 | """ 93 | def __init__(self, idict): 94 | super(DotDict, self).__init__(idict) 95 | 96 | __setattr__ = dict.__setitem__ 97 | __delattr__ = dict.__delitem__ 98 | 99 | def __getattr__(self, key): 100 | """Overwriten __getattr__ method""" 101 | obj = super(DotDict, self).get(key, None) 102 | if isinstance(obj, dict): 103 | return DotDict(obj) 104 | return obj 105 | 106 | def __getitem__(self, key): 107 | """Overwriten __getitem__ method""" 108 | return self.get(key) 109 | 110 | def __setitem__(self, key, val): 111 | """Overwriten __setitem__ method""" 112 | self._set(key, val) 113 | 114 | def _set(self, ikey, value): 115 | """ 116 | Set value for provided compound key. 117 | """ 118 | obj = self 119 | keys = ikey.split('.') 120 | for idx in range(0, len(keys)): 121 | key = keys[idx] 122 | if not obj.has_key(key): 123 | ckey = '.'.join(keys[idx:]) 124 | nkey, nval = convert_dot_notation(ckey, value) 125 | if isinstance(obj, DotDict): 126 | super(DotDict, obj).__setitem__(nkey, nval) 127 | else: 128 | obj.__setitem__(nkey, nval) 129 | return 130 | if key != keys[-1]: 131 | try: 132 | obj = super(DotDict, obj).__getitem__(key) 133 | except: 134 | try: 135 | obj = obj[key] 136 | except: 137 | raise 138 | if not isinstance(obj, dict): 139 | msg = 'Cannot assign new value, internal obj is not dict' 140 | raise Exception(msg) 141 | if isinstance(obj, DotDict): 142 | super(DotDict, obj).__setitem__(key, value) 143 | else: 144 | obj.__setitem__(key, value) 145 | 146 | def _get_keys(self, ckey): 147 | """Helper generator which yields all keys for a starting ckey""" 148 | if self.has_key(ckey): 149 | doc = self[ckey] 150 | else: 151 | doc = [o for o in self.get_values(ckey)] 152 | if isinstance(doc, dict): 153 | for key in doc.keys(): 154 | if ckey.rfind('%s.' % key) == -1: 155 | combo = '%s.%s' % (ckey, key) 156 | yield combo 157 | vals = [v for v in self.get_values(combo)] 158 | for kkk in helper_loop(combo, vals): 159 | yield kkk 160 | else: 161 | yield ckey 162 | elif isinstance(doc, list): 163 | for item in doc: 164 | if isinstance(item, dict): 165 | for key in item.keys(): 166 | if ckey.rfind('%s.' % key) == -1: 167 | combo = '%s.%s' % (ckey, key) 168 | yield combo 169 | vals = [v for v in self.get_values(combo)] 170 | for kkk in helper_loop(combo, vals): 171 | yield kkk 172 | elif isinstance(item, list): 173 | for elem in item: 174 | if isinstance(elem, dict): 175 | for kkk in elem.keys(): 176 | yield '%s.%s' % (ckey, kkk) 177 | else: 178 | yield ckey 179 | else: # basic type, so we reach the end 180 | yield ckey 181 | else: # basic type, so we reach the end 182 | yield ckey 183 | 184 | ### public methods 185 | def delete(self, ckey): 186 | """ 187 | Delete provided compound key from DotDict 188 | """ 189 | obj = self 190 | keys = ckey.split('.') 191 | for key in keys: 192 | if key == keys[-1]: 193 | del obj[key] 194 | break 195 | if isinstance(obj, DotDict): 196 | obj = super(DotDict, obj).__getitem__(key) 197 | else: 198 | obj = obj.__getitem__(key) 199 | 200 | def get(self, ckey, default=None): 201 | """ 202 | Get value for provided compound key. In a case of 203 | accessed value of a list type returns its first element. 204 | """ 205 | obj = default 206 | keys = ckey.split('.') 207 | first = keys[0] 208 | if self.has_key(first): 209 | obj = super(DotDict, self).__getitem__(first) 210 | if first == ckey: 211 | if isinstance(obj, dict): 212 | return DotDict(obj) 213 | else: 214 | return obj 215 | if isdictinstance(obj): 216 | return DotDict(obj).get('.'.join(keys[1:])) 217 | elif isinstance(obj, list): 218 | for elem in obj: 219 | if isdictinstance(elem): 220 | newobj = elem.get('.'.join(keys[1:])) 221 | if newobj: 222 | if isinstance(newobj, dict): 223 | return DotDict(newobj) 224 | return newobj 225 | return obj 226 | 227 | def get_values(self, ckey): 228 | """ 229 | Generator which yields values for any compound key. It works 230 | up to three level deep in DotDict structure. 231 | """ 232 | for next_key, item in yield_obj(self, ckey): 233 | if isdictinstance(item): 234 | for final, elem in yield_obj(item, next_key): 235 | if isdictinstance(elem) and elem.has_key(final): 236 | yield elem[final] 237 | else: 238 | yield elem 239 | elif isinstance(item, list) or isinstance(item, GeneratorType): 240 | for final, elem in item: 241 | for last, att in yield_obj(elem, final): 242 | if isdictinstance(att) and att.has_key(last): 243 | yield att[last] 244 | else: 245 | yield att 246 | 247 | def get_keys(self, ckey=None): 248 | """Return all keys for a starting ckey""" 249 | if ckey: 250 | keys = self._get_keys(ckey) 251 | else: 252 | keys = self.keys() 253 | for key in self.keys(): 254 | keys += [k for k in self._get_keys(key)] 255 | return list(set(keys)) 256 | --------------------------------------------------------------------------------