Skip to content
Open
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
12 changes: 12 additions & 0 deletions dotmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,18 @@ def bannerStr(self):
s = '\n'.join(lines)
return s



class StaticDotMap(DotMap):
def __init__(self, *args, **kwargs):
# DotMap uses multiple inheritance so we can not just pass the parameter
# to `super(...).__init__()`
if kwargs.get('_dynamic', False):
raise ValueError('StaticDotMap does not support _dynamic=True')
kwargs['_dynamic'] = False
super().__init__(*args, **kwargs)


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

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

from dotmap import DotMap, StaticDotMap


class TestReadme(unittest.TestCase):
Expand Down Expand Up @@ -142,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 @@ -231,7 +234,6 @@ def capture(**kwargs):

class TestDeepCopy(unittest.TestCase):
def test(self):
import copy
original = DotMap()
original.a = 1
original.b = 3
Expand All @@ -246,7 +248,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 @@ -269,7 +270,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 @@ -384,6 +384,160 @@ 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):
# A caller cannot opt back into dynamic behaviour through the kwarg.
with self.assertRaises(ValueError):
StaticDotMap({'a': 1}, _dynamic=True)

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