Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ print(m.name)
# Joe Smith Jr
```

It also has fast, automatic hierarchy (which can be deactivated by initializing with `DotMap(_dynamic=False)`)
It also has fast, automatic hierarchy (which can be deactivated by initializing with `DotMap(_dynamic=False)` or by using `StaticDotMap`)

```python
m = DotMap()
Expand Down
34 changes: 19 additions & 15 deletions dotmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ def here(item=None):
out += '({})'.format(item)
print(out)

__all__ = ['DotMap']
__all__ = ['DotMap', 'StaticDotMap']

class DotMap(MutableMapping, OrderedDict):
def __init__(self, *args, **kwargs):
self._map = OrderedDict()
self._dynamic = kwargs.pop('_dynamic', True)
default_dynamic = getattr(type(self), '_dynamic', True)
self._dynamic = kwargs.pop('_dynamic', default_dynamic)
if default_dynamic is False and self._dynamic:
raise ValueError(f'can not set `_dynamic={self._dynamic!r}` for {self.__class__.__name__}')
self._default_factory = kwargs.pop('_default_factory', None)
if self._default_factory is not None and not callable(self._default_factory):
raise TypeError('_default_factory must be callable')
Expand All @@ -40,6 +43,14 @@ def __init__(self, *args, **kwargs):
elif isinstance(d, Iterable):
src = d

child_kwargs = {
'_dynamic': self._dynamic,
'_default_factory': self._default_factory,
'_prevent_method_masking': self._prevent_method_masking,
'_key_convert_hook': _key_convert_hook,
'_trackedIDs': trackedIDs
}

for k,v in src:
if self._prevent_method_masking and k in reserved_keys:
raise KeyError('"{}" is reserved'.format(k))
Expand All @@ -51,13 +62,6 @@ def __init__(self, *args, **kwargs):
v = trackedIDs[idv]
else:
trackedIDs[idv] = v
child_kwargs = {
'_dynamic': self._dynamic,
'_default_factory': self._default_factory,
'_prevent_method_masking': self._prevent_method_masking,
'_key_convert_hook': _key_convert_hook,
'_trackedIDs': trackedIDs
}
v = self.__class__(v, **child_kwargs)
if type(v) is list:
l = []
Expand All @@ -69,12 +73,6 @@ def __init__(self, *args, **kwargs):
n = trackedIDs[idi]
else:
trackedIDs[idi] = i
child_kwargs = {
'_dynamic': self._dynamic,
'_default_factory': self._default_factory,
'_key_convert_hook': _key_convert_hook,
'_prevent_method_masking': self._prevent_method_masking
}
n = self.__class__(i, **child_kwargs)
l.append(n)
v = l
Expand Down Expand Up @@ -387,6 +385,12 @@ def bannerStr(self):
s = '\n'.join(lines)
return s



class StaticDotMap(DotMap):
_dynamic = False


reserved_keys = {i for i in dir(DotMap) if not i.startswith('__') and not i.endswith('__')}

if __name__ == '__main__':
Expand Down
180 changes: 174 additions & 6 deletions dotmap/test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import unittest
import copy
from dotmap import DotMap
import pickle
import unittest
from collections import OrderedDict

from dotmap import DotMap, StaticDotMap


class TestReadme(unittest.TestCase):
Expand Down Expand Up @@ -143,7 +146,6 @@ def setUp(self):
}

def test(self):
import pickle
pm = DotMap(self.d)
s = pickle.dumps(pm)
m = pickle.loads(s)
Expand Down Expand Up @@ -192,6 +194,12 @@ def assignNonDynamicKeyWithInit():
nonDynamicWithInit['no'].creation
self.assertRaises(KeyError, assignNonDynamicKeyWithInit)

def test_none_means_non_dynamic(self):
m = DotMap({'a': 1}, _dynamic=None)
self.assertEqual(m.a, 1)
with self.assertRaises(AttributeError):
m.missing


class TestDefault(unittest.TestCase):
def test_missing_attribute_returns_default(self):
Expand Down Expand Up @@ -296,7 +304,6 @@ def capture(**kwargs):

class TestDeepCopy(unittest.TestCase):
def test(self):
import copy
original = DotMap()
original.a = 1
original.b = 3
Expand All @@ -311,7 +318,6 @@ def test(self):
self.assertNotEqual(original, deepCopy)

def test_order_preserved(self):
import copy
original = DotMap()
original.a = 1
original.b = 2
Expand All @@ -334,7 +340,6 @@ def test(self):

class TestOrderedDictInit(unittest.TestCase):
def test(self):
from collections import OrderedDict
o = OrderedDict([('a', 1), ('b', 2), ('c', [OrderedDict([('d', 3)])])])
m = DotMap(o)
self.assertIsInstance(m, DotMap)
Expand Down Expand Up @@ -412,6 +417,13 @@ def badAddition():
self.assertRaises(TypeError, badAddition)


class TestMethodMasking(unittest.TestCase):
def test_dynamic_key_is_not_reserved(self):
m = DotMap({'a': 1, '_dynamic': 2}, _prevent_method_masking=True)
self.assertEqual(m.a, 1)
self.assertEqual(m['_dynamic'], 2)


# Test classes for SubclassTestCase below

# class that overrides __getitem__
Expand Down Expand Up @@ -449,6 +461,162 @@ def test_subclass_with_property(self):
self.assertIsInstance(p.my_prop.second, PropertyDotMap)
self.assertEqual(p.my_prop.second.third, 456)


class TestStaticDotMap(unittest.TestCase):
def test_is_dotmap_subclass(self):
m = StaticDotMap()
self.assertIsInstance(m, StaticDotMap)
self.assertIsInstance(m, DotMap)

def test_empty_init(self):
m = StaticDotMap()
self.assertEqual(len(m), 0)
self.assertTrue(m.empty())
with self.assertRaises(AttributeError):
m.missing

def test_kwargs_init(self):
m = StaticDotMap(a=1, b=2)
self.assertEqual(m.a, 1)
self.assertEqual(m.b, 2)
with self.assertRaises(AttributeError):
m.missing

def test_dict_init(self):
d = {'a': 1, 'b': 2, 'sub': {'c': 3, 'd': 4}}
m = StaticDotMap(d)
self.assertEqual(m.a, 1)
self.assertEqual(m.b, 2)
self.assertEqual(m.sub.c, 3)
self.assertEqual(m.sub.d, 4)

def test_missing_attribute_raises(self):
m = StaticDotMap(a=1)
with self.assertRaises(AttributeError):
m.missing

def test_missing_item_raises(self):
m = StaticDotMap(a=1)
with self.assertRaises(KeyError):
m['missing']

def test_nested_missing_attribute_raises(self):
m = StaticDotMap({'sub': {'a': 1}})
with self.assertRaises(AttributeError):
m.sub.missing

def test_dynamic_true_kwarg_raises(self):
# StaticDotMap accepts an explicit false value but not dynamic mode.
with self.assertRaisesRegex(ValueError, '^can not set `_dynamic=True` for StaticDotMap$'):
StaticDotMap({'a': 1}, _dynamic=True)
m = StaticDotMap({'a': 1}, _dynamic=False)
self.assertFalse(m._dynamic)

def test_nested_dicts_become_static(self):
m = StaticDotMap({'a': 1, 'sub': {'b': 2, 'deep': {'c': 3}}})
self.assertIsInstance(m.sub, StaticDotMap)
self.assertIsInstance(m.sub.deep, StaticDotMap)
with self.assertRaises(AttributeError):
m.sub.missing
with self.assertRaises(AttributeError):
m.sub.deep.missing

def test_list_of_dicts_become_static(self):
m = StaticDotMap({
'children': [{'name': 'a'}, {'name': 'b'}],
})
self.assertEqual(len(m.children), 2)
for child in m.children:
self.assertIsInstance(child, StaticDotMap)
with self.assertRaises(AttributeError):
child.missing

def test_assignment_to_existing_or_new_key_works(self):
# Static only disables auto-creation on read; assignment is still allowed.
m = StaticDotMap({'a': 1})
m.a = 10
self.assertEqual(m.a, 10)
m.b = 2
self.assertEqual(m.b, 2)
m['c'] = 3
self.assertEqual(m['c'], 3)

def test_dict_protocol(self):
m = StaticDotMap({'a': 1, 'b': 2})
self.assertEqual(set(m.keys()), {'a', 'b'})
self.assertEqual(set(m.values()), {1, 2})
self.assertEqual(dict(m.items()), {'a': 1, 'b': 2})
self.assertEqual(len(m), 2)
self.assertIn('a', m)
self.assertNotIn('missing', m)
self.assertEqual(m.get('a'), 1)
self.assertEqual(m.get('missing', 'default'), 'default')

def test_toDict_returns_plain_dict(self):
m = StaticDotMap({'a': 1, 'sub': {'b': 2}})
d = m.toDict()
self.assertIsInstance(d, dict)
self.assertNotIsInstance(d, DotMap)
self.assertIsInstance(d['sub'], dict)
self.assertNotIsInstance(d['sub'], DotMap)
self.assertEqual(d, {'a': 1, 'sub': {'b': 2}})

def test_copy_preserves_type_and_static(self):
m = StaticDotMap({'a': 1, 'sub': {'b': 2}})
c = m.copy()
self.assertIsInstance(c, StaticDotMap)
self.assertIsInstance(c.sub, StaticDotMap)
self.assertEqual(c.a, 1)
self.assertEqual(c.sub.b, 2)
with self.assertRaises(AttributeError):
c.missing
with self.assertRaises(AttributeError):
c.sub.missing

def test_deepcopy_preserves_type_and_static(self):
m = StaticDotMap({'a': 1, 'sub': {'b': 2}})
c = copy.deepcopy(m)
self.assertIsInstance(c, StaticDotMap)
self.assertIsInstance(c.sub, StaticDotMap)
with self.assertRaises(AttributeError):
c.missing
with self.assertRaises(AttributeError):
c.sub.missing

def test_pickle_preserves_type_and_static(self):
m = StaticDotMap({'a': 1, 'sub': {'b': 2}})
restored = pickle.loads(pickle.dumps(m))
self.assertIsInstance(restored, StaticDotMap)
self.assertIsInstance(restored.sub, StaticDotMap)
self.assertEqual(restored.a, 1)
self.assertEqual(restored.sub.b, 2)
with self.assertRaises(AttributeError):
restored.missing
with self.assertRaises(AttributeError):
restored.sub.missing

def test_init_from_dotmap(self):
source = DotMap({'a': 1, 'sub': {'b': 2}})
m = StaticDotMap(source)
self.assertEqual(m.a, 1)
self.assertEqual(m.sub.b, 2)
with self.assertRaises(AttributeError):
m.missing

def test_equality_with_dict_and_dotmap(self):
m = StaticDotMap({'a': 1, 'b': 2})
self.assertEqual(m, {'a': 1, 'b': 2})
self.assertEqual(m, DotMap({'a': 1, 'b': 2}))
self.assertEqual(m, StaticDotMap({'a': 1, 'b': 2}))

def test_prevent_method_masking_still_works(self):
# The flag introduced for DotMap should pass through to StaticDotMap.
with self.assertRaises(KeyError):
StaticDotMap(a=1, get='mango', _prevent_method_masking=True)
with self.assertRaises(KeyError):
StaticDotMap({'a': 1, 'get': 'mango'}, _prevent_method_masking=True)


class TestKeyConvertHook(unittest.TestCase):
def fix_illegal_key(self, key):
return key.replace(".", "_").replace("-","_")
Expand Down