Skip to content

Add 'box_dots_exclude' parameter to keep certain keys with dots from being broken down #297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
24 changes: 15 additions & 9 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class Box(dict):
:param box_intact_types: tuple of types to ignore converting
:param box_recast: cast certain keys to a specified type
:param box_dots: access nested Boxes by period separated keys in string
:param box_dots_exclude: optional regular expression for dotted keys to exclude
:param box_class: change what type of class sub-boxes will be created as
:param box_namespace: the namespace this (possibly nested) Box lives within
"""
Expand Down Expand Up @@ -204,6 +205,7 @@ def __new__(
box_intact_types: Union[Tuple, List] = (),
box_recast: Optional[Dict] = None,
box_dots: bool = False,
box_dots_exclude: str = None,
box_class: Optional[Union[Dict, Type["Box"]]] = None,
box_namespace: Union[Tuple[str, ...], Literal[False]] = (),
**kwargs: Any,
Expand All @@ -229,6 +231,7 @@ def __new__(
"box_intact_types": tuple(box_intact_types),
"box_recast": box_recast,
"box_dots": box_dots,
"box_dots_exclude": re.compile(box_dots_exclude) if box_dots_exclude else None,
"box_class": box_class if box_class is not None else Box,
"box_namespace": box_namespace,
}
Expand All @@ -251,6 +254,7 @@ def __init__(
box_intact_types: Union[Tuple, List] = (),
box_recast: Optional[Dict] = None,
box_dots: bool = False,
box_dots_exclude: str = None,
box_class: Optional[Union[Dict, Type["Box"]]] = None,
box_namespace: Union[Tuple[str, ...], Literal[False]] = (),
**kwargs: Any,
Expand All @@ -272,6 +276,7 @@ def __init__(
"box_intact_types": tuple(box_intact_types),
"box_recast": box_recast,
"box_dots": box_dots,
"box_dots_exclude": re.compile(box_dots_exclude) if box_dots_exclude else None,
"box_class": box_class if box_class is not None else self.__class__,
"box_namespace": box_namespace,
}
Expand Down Expand Up @@ -489,6 +494,12 @@ def __setstate__(self, state):
self._box_config = state["_box_config"]
self.__dict__.update(state)

def __process_dotted_key(self,item):
if self._box_config["box_dots"] and isinstance(item, str):
return ("[" in item) or ("." in item and not (self._box_config["box_dots_exclude"]
and self._box_config["box_dots_exclude"].match(item)))
return False

def __get_default(self, item, attr=False):
if item in ("getdoc", "shape") and _is_ipython():
return None
Expand Down Expand Up @@ -526,7 +537,7 @@ def __get_default(self, item, attr=False):
value = default_value
if self._box_config["default_box_create_on_get"]:
if not attr or not (item.startswith("_") and item.endswith("_")):
if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item):
if self.__process_dotted_key(item):
first_item, children = _parse_box_dots(self, item, setting=True)
if first_item in self.keys():
if hasattr(self[first_item], "__setitem__"):
Expand Down Expand Up @@ -602,7 +613,7 @@ def __getitem__(self, item, _ignore_default=False):
for x in list(super().keys())[item.start : item.stop : item.step]:
new_box[x] = self[x]
return new_box
if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item):
if self.__process_dotted_key(item):
try:
first_item, children = _parse_box_dots(self, item)
except BoxError:
Expand Down Expand Up @@ -652,7 +663,7 @@ def __getattr__(self, item):
def __setitem__(self, key, value):
if key != "_box_config" and self._box_config["frozen_box"] and self._box_config["__created"]:
raise BoxError("Box is frozen")
if self._box_config["box_dots"] and isinstance(key, str) and ("." in key or "[" in key):
if self.__process_dotted_key(key):
first_item, children = _parse_box_dots(self, key, setting=True)
if first_item in self.keys():
if hasattr(self[first_item], "__setitem__"):
Expand Down Expand Up @@ -696,12 +707,7 @@ def __setattr__(self, key, value):
def __delitem__(self, key):
if self._box_config["frozen_box"]:
raise BoxError("Box is frozen")
if (
key not in self.keys()
and self._box_config["box_dots"]
and isinstance(key, str)
and ("." in key or "[" in key)
):
if key not in self.keys() and self.__process_dotted_key(key):
try:
first_item, children = _parse_box_dots(self, key)
except BoxError:
Expand Down
1 change: 1 addition & 0 deletions box/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class BoxTomlDecodeError(BoxError, tomli.TOMLDecodeError): # type: ignore
"box_duplicates",
"box_intact_types",
"box_dots",
"box_dots_exclude",
"box_recast",
"box_class",
"box_namespace",
Expand Down
7 changes: 7 additions & 0 deletions test/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,13 @@ def test_dots(self):
with pytest.raises(BoxKeyError):
del b["a.b"]

def test_dots_exclusion(self):
bx = Box.from_yaml(yaml_string="0.0.0.1: True",default_box=True,default_box_none_transform=False,box_dots=True,
box_dots_exclude=r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+')
assert bx["0.0.0.1"] == True
with pytest.raises(BoxKeyError):
del bx["0"]

def test_unicode(self):
bx = Box()
bx["\U0001f631"] = 4
Expand Down
Loading