diff --git a/CHANGES.rst b/CHANGES.rst index 1b862cf..52b9441 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +Version 2.1.0 +============= + +* Adding `.update` and `.set_default` functionality +* Adding `dir` support + Version 2.0.0 ============= diff --git a/README.rst b/README.rst index fab086c..4957916 100644 --- a/README.rst +++ b/README.rst @@ -294,21 +294,17 @@ config values into python types. It supports `list`, `bool`, `int` and `float`. Similar Libraries ----------------- -**Bunch** - -* Does not work recursively. - **EasyDict** * EasyDict not have a way to make sub items recursively back into a regular dictionary. * Adding new dicts to lists in the dictionary does not make them into EasyDicts. -* Both EasyDicts `str` and `repr` print a dictionary look alike, `Box` makes it clear in repr that it is a Box object. +* Both EasyDicts `str` and `repr` print a dictionary look alike, `Box` makes it clear in `repr` that it is a Box object. **addict** * Adding new dicts or lists does not make them into `addict.Dict` objects. * Is a default dictionary, as in it will never fail on lookup. -* Both EasyDicts `str` and `repr` print a dictionary look alike, `Box` makes it clear in repr that it is a Box object. +* Both `addict.Dict`'s `str` and `repr` print a dictionary look alike, `Box` makes it clear in `repr` that it is a Box object. License diff --git a/box.py b/box.py index 633d5f3..cc9b78a 100644 --- a/box.py +++ b/box.py @@ -6,6 +6,7 @@ Improved dictionary management. Inspired by javascript style referencing, as it's one of the few things they got right. """ +import string import sys import json @@ -28,7 +29,7 @@ __all__ = ['Box', 'ConfigBox', 'LightBox', 'BoxList'] __author__ = "Chris Griffith" -__version__ = "2.0.0" +__version__ = "2.1.0" class LightBox(dict): @@ -98,8 +99,52 @@ def __str__(self): return str(self.to_dict()) def __call__(self, *args, **kwargs): + """ Return keys as a tuple""" return tuple(sorted(self.keys())) + def __dir__(self): + builtins = ("True", "False", "None", "if", "elif", "else", "for", + "in", "not", "is", "def", "class", "return", "yield", + "except", "while", "raise") + allowed = string.ascii_letters + string.digits + "_" + + out = dir(dict) + ['to_dict', 'to_json'] + # Only show items accessible by dot notation + for key in self.keys(): + if (" " not in key and + key[0] not in string.digits and + key not in builtins): + for letter in key: + if letter not in allowed: + break + else: + out.append(key) + + if yaml_support: + out.append('to_yaml') + return out + + def update(self, item=None, **kwargs): + if not item: + item = kwargs + iter_over = item.items() if hasattr(item, 'items') else item + for k, v in iter_over: + if isinstance(v, dict): + v = Box(v) + if k in self and isinstance(self[k], dict): + self[k].update(v) + continue + self.__setattr__(k, v) + + def setdefault(self, item, default=None): + if item in self: + return self[item] + + if isinstance(default, dict): + default = Box(default) + self[item] = default + return default + def to_dict(self, in_dict=None): """ Turn the Box and sub Boxes back into a native @@ -233,6 +278,31 @@ def to_dict(self, in_dict=None): out_dict[k] = v return out_dict + def update(self, item=None, **kwargs): + if not item: + item = kwargs + iter_over = item.items() if hasattr(item, 'items') else item + for k, v in iter_over: + if isinstance(v, dict): + v = Box(v) + if k in self and isinstance(self[k], dict): + self[k].update(v) + continue + elif isinstance(v, list): + v = BoxList(v) + self.__setattr__(k, v) + + def setdefault(self, item, default=None): + if item in self: + return self[item] + + if isinstance(default, dict): + default = Box(default) + elif isinstance(default, list): + default = BoxList(default) + self[item] = default + return default + class BoxList(list): """ @@ -296,8 +366,8 @@ class ConfigBox(LightBox): """ - _protected_keys = dir({}) + ['to_dict', 'tree_view', - 'bool', 'int', 'float', 'list', 'getboolean', + _protected_keys = dir({}) + ['to_dict', 'bool', 'int', 'float', + 'list', 'getboolean', 'to_json', 'to_yaml', 'getfloat', 'getint'] def __getattr__(self, item): @@ -308,6 +378,11 @@ def __getattr__(self, item): except AttributeError: return super(ConfigBox, self).__getattr__(item.lower()) + def __dir__(self): + return super(ConfigBox, self).__dir__() + ['bool', 'int', 'float', + 'list', 'getboolean', + 'getfloat', 'getint'] + def bool(self, item, default=None): """ Return value of key as a boolean diff --git a/requirements-test.txt b/requirements-test.txt index 9f95f03..9250127 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,4 +2,4 @@ pytest coverage >= 3.6 tox pytest-cov -pyyaml +PyYAML diff --git a/test/test_box.py b/test/test_box.py index 3524b5d..1080ed4 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -280,3 +280,77 @@ def test_boxlist(self): assert isinstance(str(new_list), str) assert isinstance(new_list[1], BoxList) assert not isinstance(new_list.to_list(), BoxList) + + def test_dir(self): + test_dict = {'key1': 'value1', + 'not$allowed': 'fine_value', + "Key 2": {"Key 3": "Value 3", + "Key4": {"Key5": "Value5"}}} + a = Box(test_dict) + assert 'key1' in dir(a) + assert 'not$allowed' not in dir(a) + assert 'Key4' in a['Key 2'] + for item in ('to_yaml', 'to_dict', 'to_json'): + assert item in dir(a) + + b = ConfigBox(test_dict) + + for item in ('to_yaml', 'to_dict', 'to_json', 'int', 'list', 'float'): + assert item in dir(b) + + def test_update(self): + test_dict = {'key1': 'value1', + "Key 2": {"Key 3": "Value 3", + "Key4": {"Key5": "Value5"}}} + a = Box(test_dict) + a.update({'key1': {'new': 5}, 'Key 2': {"add_key": 6}, + 'lister': ['a']}) + a.update([('asdf', 'fdsa')]) + a.update(testkey=66) + + assert a.key1.new == 5 + assert a['Key 2'].add_key == 6 + assert "Key5" in a['Key 2'].Key4 + assert isinstance(a.key1, Box) + assert isinstance(a.lister, BoxList) + assert a.asdf == 'fdsa' + assert a.testkey == 66 + + b = LightBox(test_dict) + b.update([('asdf', 'fdsa')]) + b.update(testkey=66) + b.update({'key1': {'new': 5}, 'Key 2': {"add_key": 6}}) + + assert b.key1.new == 5 + assert b['Key 2'].add_key == 6 + assert "Key5" in b['Key 2'].Key4 + assert isinstance(b.key1, LightBox) + assert b.asdf == 'fdsa' + assert b.testkey == 66 + + def test_set_default(self): + test_dict = {'key1': 'value1', + "Key 2": {"Key 3": "Value 3", + "Key4": {"Key5": "Value5"}}} + a = Box(test_dict) + + new = a.setdefault("key3", {'item': 2}) + new_list = a.setdefault("lister", [{'gah': 7}]) + assert a.setdefault("key1", False) == 'value1' + + assert new == Box(item=2) + assert new_list == BoxList([{'gah': 7}]) + assert a.key3.item == 2 + assert a.lister[0].gah == 7 + + b = LightBox(test_dict) + + new = b.setdefault("key3", {'item': 2}) + new_list = b.setdefault("lister", [{'gah': 7}]) + + assert b.setdefault("key1", False) == 'value1' + assert new == Box(item=2) + assert new_list == [{'gah': 7}] + assert b.key3.item == 2 + assert b.lister[0]["gah"] == 7 + assert not isinstance(b.lister, BoxList)