Skip to content

Commit

Permalink
Version 5.4.1 (#206)
Browse files Browse the repository at this point in the history
* Fixing setdefault behavior with box_dots (thanks to Ivan Pepelnjak)

Co-authored-by: Ivan Pepelnjak <ip@ipspace.net>
  • Loading branch information
cdgriffith and ipspace authored Aug 22, 2021
1 parent 51042b8 commit 4f9b3a6
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 10 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Code contributions:
- Varun Madiath (vamega)
- Jacob Hayes (JacobHayes)
- Dominic (Yobmod)
- Ivan Pepelnjak (ipspace)

Suggestions and bug reporting:

Expand Down
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

Version 5.4.1
-------------

* Fixing #205 setdefault behavior with box_dots (thanks to Ivan Pepelnjak)

Version 5.4.0
-------------

Expand Down
2 changes: 1 addition & 1 deletion box/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-

__author__ = "Chris Griffith"
__version__ = "5.4.0"
__version__ = "5.4.1"

from box.box import Box
from box.box_list import BoxList
Expand Down
43 changes: 34 additions & 9 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ def _parse_box_dots(bx, item, setting=False):
raise BoxError("Could not split box dots properly")


def _get_dot_paths(bx, current=""):
"""A generator of all the end node keys in a box in box_dots format"""

def handle_dicts(sub_bx, paths=""):
for key, value in sub_bx.items():
yield f"{paths}.{key}" if paths else key
if isinstance(value, dict):
yield from handle_dicts(value, f"{paths}.{key}" if paths else key)
elif isinstance(value, list):
yield from handle_lists(value, f"{paths}.{key}" if paths else key)

def handle_lists(bx_list, paths=""):
for i, value in enumerate(bx_list):
yield f"{paths}[{i}]"
if isinstance(value, list):
yield from handle_lists(value, f"{paths}[{i}]")
if isinstance(value, dict):
yield from handle_dicts(value, f"{paths}[{i}]")

yield from handle_dicts(bx, current)


def _get_box_config():
return {
# Internal use only
Expand Down Expand Up @@ -692,17 +714,20 @@ def convert_and_set(k, v):
convert_and_set(key, kwargs[key])

def setdefault(self, item, default=None):
# Have to use a try except instead of "item in self" as box_dots may not be in iterable
try:
return self[item]
except KeyError:
if isinstance(default, dict):
default = self._box_config["box_class"](default, **self.__box_config())
if isinstance(default, list):
default = box.BoxList(default, **self.__box_config())
self[item] = default
if item in self:
return self[item]

if self._box_config["box_dots"]:
if item in _get_dot_paths(self):
return self[item]

if isinstance(default, dict):
default = self._box_config["box_class"](default, **self.__box_config())
if isinstance(default, list):
default = box.BoxList(default, **self.__box_config())
self[item] = default
return self[item]

def _safe_attr(self, attr):
"""Convert a key into something that is accessible as an attribute"""
allowed = string.ascii_letters + string.digits + "_"
Expand Down
62 changes: 62 additions & 0 deletions test/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import ruamel.yaml as yaml

from box import Box, BoxError, BoxKeyError, BoxList, ConfigBox, SBox, box
from box.box import _get_dot_paths # type: ignore


def mp_queue_test(q):
Expand Down Expand Up @@ -312,6 +313,24 @@ def test_set_default_box_dots(self):
a.setdefault("x.y", 20)
assert a["x.y"] == 10

a["lists"] = [[[{"test": "here"}], {1, 2}], (4, 5)]
assert list(_get_dot_paths(a)) == [
"x",
"x.y",
"lists",
"lists[0]",
"lists[0][0]",
"lists[0][0][0]",
"lists[0][0][0].test",
"lists[0][1]",
"lists[1]",
]

t = Box({"a": 1}, default_box=True, box_dots=True, default_box_none_transform=False)
assert t.setdefault("b", [1, 2]) == [1, 2]
assert t == Box(a=1, b=[1, 2])
assert t.setdefault("c", [{"d": 2}]) == BoxList([{"d": 2}])

def test_from_json_file(self):
bx = Box.from_json(filename=data_json_file)
assert isinstance(bx, Box)
Expand Down Expand Up @@ -1219,3 +1238,46 @@ def test_box_from_empty_yaml(self):

out2 = BoxList.from_yaml("---")
assert out2 == BoxList()

def test_setdefault_simple(self):
box = Box({"a": 1})
box.setdefault("b", 2)
box.setdefault("c", "test")
box.setdefault("d", {"e": True})
box.setdefault("f", [1, 2])

assert box["b"] == 2
assert box["c"] == "test"
assert isinstance(box["d"], Box)
assert box["d"]["e"] == True
assert isinstance(box["f"], BoxList)
assert box["f"][1] == 2

def test_setdefault_dots(self):
box = Box({"a": 1}, box_dots=True)
box.setdefault("b", 2)
box.c = {"d": 3}
box.setdefault("c.e", "test")
box.setdefault("d", {"e": True})
box.setdefault("f", [1, 2])

assert box.b == 2
assert box.c.e == "test"
assert isinstance(box["d"], Box)
assert box.d.e == True
assert isinstance(box["f"], BoxList)
assert box.f[1] == 2

def test_setdefault_dots_default(self):
box = Box({"a": 1}, box_dots=True, default_box=True)
box.b.c.d.setdefault("e", 2)
box.c.setdefault("e", "test")
box.d.e.setdefault("f", {"g": True})
box.e.setdefault("f", [1, 2])

assert box["b.c.d"].e == 2
assert box.c.e == "test"
assert isinstance(box["d.e.f"], Box)
assert box.d.e["f.g"] == True
assert isinstance(box["e.f"], BoxList)
assert box.e.f[1] == 2

0 comments on commit 4f9b3a6

Please sign in to comment.