Skip to content
Closed
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ reporting, all input is greatly appreciated!

Code contributions:

- WU Leizhi (MbappeWU)
- Alexandre Decan (AlexandreDecan)
- dhilipsiva (dhilipsiva)
- MAA (FooBarQuaxx)
Expand Down
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
=========

Unreleased
----------

* Fixing ``box_dots`` membership and explicit-default ``get`` for paths through
lists.

Version 7.4.1
-------------

Expand Down
29 changes: 23 additions & 6 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
31 changes: 30 additions & 1 deletion box/box_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()})"

Expand Down
33 changes: 33 additions & 0 deletions test/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down