Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ m = DotMap()
m.people.steve.age = 31
```

You can provide a default value for missing attributes when you do not want automatic hierarchy creation for absent keys

```python
m = DotMap({'city': 'abc', 'CountryCode': 101}, _default='')
print(m.zipCode)
# ''
```

And key initialization

```python
Expand Down
58 changes: 49 additions & 9 deletions dotmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ def here(item=None):

__all__ = ['DotMap']

_default_sentinel = object()

class DotMap(MutableMapping, OrderedDict):
def __init__(self, *args, **kwargs):
self._map = OrderedDict()
self._dynamic = kwargs.pop('_dynamic', True)
default = kwargs.pop('_default', _default_sentinel)
self._default = None if default is _default_sentinel else default
self._default_provided = default is not _default_sentinel
Comment thread
sachinsachdeva marked this conversation as resolved.
Outdated
if self._default_provided and not self._dynamic:
raise ValueError('cannot provide _default when _dynamic is False')
self._prevent_method_masking = kwargs.pop('_prevent_method_masking', False)

_key_convert_hook = kwargs.pop('_key_convert_hook', None)
Expand Down Expand Up @@ -46,7 +53,15 @@ def __init__(self, *args, **kwargs):
v = trackedIDs[idv]
else:
trackedIDs[idv] = v
v = self.__class__(v, _dynamic=self._dynamic, _prevent_method_masking = self._prevent_method_masking, _key_convert_hook =_key_convert_hook, _trackedIDs = trackedIDs)
child_kwargs = {
'_dynamic': self._dynamic,
'_prevent_method_masking': self._prevent_method_masking,
'_key_convert_hook': _key_convert_hook,
'_trackedIDs': trackedIDs
}
if self._default_provided:
child_kwargs['_default'] = self._default
v = self.__class__(v, **child_kwargs)
if type(v) is list:
l = []
for i in v:
Expand All @@ -57,7 +72,14 @@ def __init__(self, *args, **kwargs):
n = trackedIDs[idi]
else:
trackedIDs[idi] = i
n = self.__class__(i, _dynamic=self._dynamic, _key_convert_hook =_key_convert_hook, _prevent_method_masking = self._prevent_method_masking)
child_kwargs = {
'_dynamic': self._dynamic,
'_key_convert_hook': _key_convert_hook,
'_prevent_method_masking': self._prevent_method_masking
}
if self._default_provided:
child_kwargs['_default'] = self._default
n = self.__class__(i, **child_kwargs)
l.append(n)
v = l
self._map[k] = v
Expand Down Expand Up @@ -90,13 +112,20 @@ def next(self):
def __setitem__(self, k, v):
self._map[k] = v
def __getitem__(self, k):
if k not in self._map and self._dynamic and k != '_ipython_canary_method_should_not_exist_':
# automatically extend to new DotMap
self[k] = self.__class__()
if k not in self._map:
if self._default_provided:
return self._default
if self._dynamic and k != '_ipython_canary_method_should_not_exist_':
# automatically extend to new DotMap
self[k] = self.__class__()
return self._map[k]

def __setattr__(self, k, v):
if k in {'_map','_dynamic', '_ipython_canary_method_should_not_exist_', '_prevent_method_masking'}:
if k in {
'_map', '_dynamic', '_default', '_default_provided',
'_ipython_canary_method_should_not_exist_',
'_prevent_method_masking'
}:
super(DotMap, self).__setattr__(k,v)
elif self._prevent_method_masking and k in reserved_keys:
raise KeyError('"{}" is reserved'.format(k))
Expand All @@ -107,7 +136,10 @@ def __getattr__(self, k):
if k.startswith('__') and k.endswith('__'):
raise AttributeError(k)

if k in {'_map','_dynamic','_ipython_canary_method_should_not_exist_'}:
if k in {
'_map', '_dynamic', '_default', '_default_provided',
'_ipython_canary_method_should_not_exist_'
}:
return super(DotMap, self).__getattr__(k)

try:
Expand Down Expand Up @@ -243,7 +275,10 @@ def __len__(self):
def clear(self):
self._map.clear()
def copy(self):
return self.__class__(self)
kwargs = {}
if self._default_provided:
kwargs['_default'] = self._default
return self.__class__(self, **kwargs)
def __copy__(self):
return self.copy()
def __deepcopy__(self, memo=None):
Expand Down Expand Up @@ -280,7 +315,12 @@ def fromkeys(cls, seq, value=None):
d._map = OrderedDict.fromkeys(seq, value)
return d
def __getstate__(self): return self.__dict__
def __setstate__(self, d): self.__dict__.update(d)
def __setstate__(self, d):
self.__dict__.update(d)
if '_default' not in self.__dict__:
self._default = None
if '_default_provided' not in self.__dict__:
self._default_provided = False
# bannerStr
def _getListStr(self,items):
out = '['
Expand Down
38 changes: 38 additions & 0 deletions dotmap/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,44 @@ def assignNonDynamicKeyWithInit():
self.assertRaises(KeyError, assignNonDynamicKeyWithInit)


class TestDefault(unittest.TestCase):
def test_missing_attribute_returns_default(self):
address = {'city': 'abc', 'country': 'XY', 'CountryCode': 101}
Comment thread
sachinsachdeva marked this conversation as resolved.
m = DotMap(address, _default='')

self.assertEqual(m.city, 'abc')
self.assertEqual(m.CountryCode, 101)
self.assertNotIn('zipCode', m)
self.assertEqual(m.zipCode, '')

def test_nested_maps_inherit_default(self):
m = DotMap({'address': {'city': 'abc'}}, _default='')

self.assertEqual(m.address.city, 'abc')
self.assertEqual(m.address.zipCode, '')

def test_default_with_dynamic_false_raises(self):
self.assertRaises(ValueError, lambda: DotMap(_default='', _dynamic=False))
Comment thread
sachinsachdeva marked this conversation as resolved.
Outdated

def test_copy_preserves_default(self):
m = DotMap({'city': 'abc'}, _default='')
c = m.copy()

self.assertEqual(c.city, 'abc')
self.assertNotIn('zipCode', c)
self.assertEqual(c.zipCode, '')

def test_deepcopy_preserves_default(self):
import copy
Comment thread
sachinsachdeva marked this conversation as resolved.
Outdated
m = DotMap({'address': {'city': 'abc'}}, _default='')
c = copy.deepcopy(m)

self.assertEqual(c.address.city, 'abc')
self.assertNotIn('zipCode', c)
self.assertEqual(c.zipCode, '')
self.assertEqual(c.address.zipCode, '')


class TestRecursive(unittest.TestCase):
def test(self):
m = DotMap()
Expand Down