From 858b7c541ccecc8b5bc03236ffda794064e5a868 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:58:19 +0300 Subject: [PATCH] Honor explicit pop defaults without creating missing values --- AUTHORS.rst | 2 ++ CHANGES.rst | 5 +++++ box/box.py | 10 +++++++--- box/box_list.py | 7 +++++-- test/test_box.py | 39 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 6b44851..0e7873e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -98,3 +98,5 @@ Suggestions and bug reporting: - d00m514y3r - Sébastien Weber (seb5g) - Ward Loos (wrdls) + +- Huanyi Xie (xiehuanyi) diff --git a/CHANGES.rst b/CHANGES.rst index c4a347d..a1acab8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Changelog ========= +Unreleased +---------- + +* Honor explicit ``pop`` defaults without creating missing default-box values. + Version 7.4.1 ------------- diff --git a/box/box.py b/box/box.py index 8252643..6481aa1 100644 --- a/box/box.py +++ b/box/box.py @@ -5,6 +5,7 @@ """ Improved dictionary access through dot notation with additional tools. """ + from __future__ import annotations import copy @@ -621,8 +622,11 @@ def __getitem__(self, item, _ignore_default=False): return self.__get_default(item) raise BoxKeyError(str(item)) from _exception_cause(err) if first_item in self.keys(): - if hasattr(self[first_item], "__getitem__"): - return self[first_item][children] + child = self[first_item] + if hasattr(child, "__getitem__"): + if _ignore_default and isinstance(child, (Box, box.BoxList)): + return child.__getitem__(children, _ignore_default=True) + return child[children] if self._box_config["camel_killer_box"] and isinstance(item, str): converted = _camel_killer(item) if converted in self.keys(): @@ -758,7 +762,7 @@ def pop(self, key, *args): if len(args) != 1: raise BoxError('pop() takes only one optional argument "default"') try: - item = self[key] + item = self.__getitem__(key, _ignore_default=True) except KeyError: return args[0] else: diff --git a/box/box_list.py b/box/box_list.py index 72609ff..97e4b06 100644 --- a/box/box_list.py +++ b/box/box_list.py @@ -65,13 +65,16 @@ def frozen(*args, **kwargs): for method in ["append", "extend", "insert", "pop", "remove", "reverse", "sort"]: self.__setattr__(method, frozen) - def __getitem__(self, item): + def __getitem__(self, item, _ignore_default=False): if self.box_options.get("box_dots") and isinstance(item, str) and item.startswith("["): list_pos = _list_pos_re.search(item) value = super().__getitem__(int(list_pos.groups()[0])) if len(list_pos.group()) == len(item): return value - return value.__getitem__(item[len(list_pos.group()) :].lstrip(".")) + children = item[len(list_pos.group()) :].lstrip(".") + if _ignore_default and isinstance(value, (box.Box, BoxList)): + return value.__getitem__(children, _ignore_default=True) + return value.__getitem__(children) if isinstance(item, tuple): result = self for idx in item: diff --git a/test/test_box.py b/test/test_box.py index e5c56e0..d7bb822 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -1506,3 +1506,42 @@ def func(box_instance, key): my_box = DDBox(default_box_attr=func) assert my_box.a == {"bi": "{}", "key": "a"} + + +@pytest.mark.parametrize("create_on_get", [True, False]) +@pytest.mark.parametrize("default", [None, 0, [], {"fallback": True}]) +def test_pop_explicit_default_without_autovivification(create_on_get, default): + calls = [] + + def factory(): + calls.append(None) + return "generated" + + value = Box(default_box=True, default_box_attr=factory, default_box_create_on_get=create_on_get) + assert value.pop("missing", default) is default + assert value == {} + assert calls == [] + value.present = 1 + assert value.pop("present", default) == 1 + assert value == {} + + +def test_pop_explicit_default_dotted_path(): + value = Box({"nested": {}}, default_box=True, box_dots=True) + default = object() + assert value.pop("nested.missing", default) is default + assert value.to_dict() == {"nested": {}} + + +def test_pop_explicit_default_camel_case_key(): + value = Box({"actualKey": 1}, default_box=True, camel_killer_box=True) + assert value.pop("actualKey", None) == 1 + assert value == {} + + +@pytest.mark.parametrize("path", ["items[0].missing", "items[1][0].missing"]) +def test_pop_explicit_default_dotted_list_path(path): + value = Box({"items": [{}, [{}]]}, default_box=True, box_dots=True) + default = object() + assert value.pop(path, default) is default + assert value.to_dict() == {"items": [{}, [{}]]}