From afae30267c0df2e48641e9323cce48d638f1f555 Mon Sep 17 00:00:00 2001 From: WU Leizhi Date: Sat, 3 Oct 2026 22:03:14 +0800 Subject: [PATCH] fix: resolve BoxList paths in dotted membership --- AUTHORS.rst | 1 + CHANGES.rst | 6 ++++++ box/box.py | 29 +++++++++++++++++++++++------ box/box_list.py | 31 ++++++++++++++++++++++++++++++- test/test_box.py | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 6b44851..6718a45 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -5,6 +5,7 @@ reporting, all input is greatly appreciated! Code contributions: +- WU Leizhi (MbappeWU) - Alexandre Decan (AlexandreDecan) - dhilipsiva (dhilipsiva) - MAA (FooBarQuaxx) diff --git a/CHANGES.rst b/CHANGES.rst index c4a347d..ac09402 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Changelog ========= +Unreleased +---------- + +* Fixing ``box_dots`` membership and explicit-default ``get`` for paths through + lists. + Version 7.4.1 ------------- diff --git a/box/box.py b/box/box.py index 8252643..8fdf8fc 100644 --- a/box/box.py +++ b/box/box.py @@ -138,6 +138,17 @@ def _get_box_config(): } +def _contains_path(value, path): + """Check a dotted/bracket path without invoking default creation.""" + if isinstance(value, Box): + return value._contains_dotted(path) + if isinstance(value, box.BoxList): + return value._contains_dotted(path) + if isinstance(value, Mapping): + return path in value + return False + + def _get_property_func(obj, key): """ Try to get property helper functions of given object and property name. @@ -410,17 +421,23 @@ def __contains__(self, item): return in_me if in_me: return True - if "." not in item: + return self._contains_dotted(item) + + def _contains_dotted(self, item): + """Return whether a path exists, without default-box side effects.""" + if not isinstance(item, str): + return False + if super().__contains__(item): + return True + if not self.__process_dotted_key(item): return False try: first_item, children = _parse_box_dots(self, item) except BoxError: return False - else: - if not super().__contains__(first_item): - return False - it = self[first_item] - return isinstance(it, Iterable) and children in it + if not super().__contains__(first_item): + return False + return _contains_path(super().__getitem__(first_item), children) def keys(self, dotted: bool = False): if not dotted: diff --git a/box/box_list.py b/box/box_list.py index 72609ff..3582ab1 100644 --- a/box/box_list.py +++ b/box/box_list.py @@ -6,7 +6,7 @@ import copy import re -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from os import PathLike from typing import Any @@ -159,6 +159,35 @@ def _dotted_helper(self) -> list[str]: keys.append(f"[{idx}]") return keys + def _contains_dotted(self, item) -> bool: + """Return whether a bracket path exists without default creation.""" + if not self.box_options.get("box_dots") or not isinstance(item, str) or not item.startswith("["): + return False + match = _list_pos_re.match(item) + if match is None: + return False + try: + pos = int(match.group()[1:-1]) + except ValueError: + return False + if pos >= len(self): + return False + remainder = item[len(match.group()) :] + if not remainder: + return True + if remainder[0] == ".": + remainder = remainder[1:] + elif remainder[0] != "[": + return False + child = self[pos] + if isinstance(child, box.Box): + return child._contains_dotted(remainder) + if isinstance(child, BoxList): + return child._contains_dotted(remainder) + if isinstance(child, Mapping): + return remainder in child + return False + def __repr__(self): return f"{self.__class__.__name__}({self.to_list()})" diff --git a/test/test_box.py b/test/test_box.py index e5c56e0..9dde57b 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -771,6 +771,39 @@ def test_contains(self): assert "a.b.c" in bx_dot assert "a.b.c.d" not in bx_dot + nested = Box({"a": [{"b": 1}]}, box_dots=True) + assert nested["a[0].b"] == 1 + assert "a[0]" in nested + assert "a[0].b" in nested + sentinel = object() + assert nested.get("a[0].b", sentinel) == 1 + assert nested.get("a[1].b", sentinel) is sentinel + + literal = Box({"a.b": 2}, box_dots=True, box_dots_exclude=r"^a\.b$") + assert literal["a.b"] == 2 + assert "a.b" in literal + + intact_mapping = Box({"a": {"b": 1}}, box_dots=True, box_intact_types=(dict,)) + assert "a.b" in intact_mapping + assert intact_mapping.get("a.b", sentinel) == 1 + + intact_list = Box({"a": [{"b": 1}]}, box_dots=True, box_intact_types=(list,)) + assert "a[0].b" not in intact_list + assert intact_list.get("a[0].b", sentinel) is sentinel + + oversized_index = "a[" + ("9" * 4301) + "].b" + assert oversized_index not in nested + assert nested.get(oversized_index, sentinel) is sentinel + + def test_dotted_membership_does_not_create_default_values(self): + nested = Box(default_box=True, box_dots=True) + nested["a[0].b"] = 1 + before = nested.to_dict() + sentinel = object() + assert "a[1].b" not in nested + assert nested.get("a[1].b", sentinel) is sentinel + assert nested.to_dict() == before + def test_get_default_box(self): bx = Box(default_box=True) assert bx.get("test", 4) == 4