Skip to content

Commit 1ef8c79

Browse files
authored
Add 'box_dots_exclude' parameter to keep certain keys with dots from being broken down (#297)
1 parent a6b71cb commit 1ef8c79

File tree

3 files changed

+23
-9
lines changed

3 files changed

+23
-9
lines changed

box/box.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ class Box(dict):
171171
:param box_intact_types: tuple of types to ignore converting
172172
:param box_recast: cast certain keys to a specified type
173173
:param box_dots: access nested Boxes by period separated keys in string
174+
:param box_dots_exclude: optional regular expression for dotted keys to exclude
174175
:param box_class: change what type of class sub-boxes will be created as
175176
:param box_namespace: the namespace this (possibly nested) Box lives within
176177
"""
@@ -204,6 +205,7 @@ def __new__(
204205
box_intact_types: Union[Tuple, List] = (),
205206
box_recast: Optional[Dict] = None,
206207
box_dots: bool = False,
208+
box_dots_exclude: str = None,
207209
box_class: Optional[Union[Dict, Type["Box"]]] = None,
208210
box_namespace: Union[Tuple[str, ...], Literal[False]] = (),
209211
**kwargs: Any,
@@ -229,6 +231,7 @@ def __new__(
229231
"box_intact_types": tuple(box_intact_types),
230232
"box_recast": box_recast,
231233
"box_dots": box_dots,
234+
"box_dots_exclude": re.compile(box_dots_exclude) if box_dots_exclude else None,
232235
"box_class": box_class if box_class is not None else Box,
233236
"box_namespace": box_namespace,
234237
}
@@ -251,6 +254,7 @@ def __init__(
251254
box_intact_types: Union[Tuple, List] = (),
252255
box_recast: Optional[Dict] = None,
253256
box_dots: bool = False,
257+
box_dots_exclude: str = None,
254258
box_class: Optional[Union[Dict, Type["Box"]]] = None,
255259
box_namespace: Union[Tuple[str, ...], Literal[False]] = (),
256260
**kwargs: Any,
@@ -272,6 +276,7 @@ def __init__(
272276
"box_intact_types": tuple(box_intact_types),
273277
"box_recast": box_recast,
274278
"box_dots": box_dots,
279+
"box_dots_exclude": re.compile(box_dots_exclude) if box_dots_exclude else None,
275280
"box_class": box_class if box_class is not None else self.__class__,
276281
"box_namespace": box_namespace,
277282
}
@@ -489,6 +494,12 @@ def __setstate__(self, state):
489494
self._box_config = state["_box_config"]
490495
self.__dict__.update(state)
491496

497+
def __process_dotted_key(self,item):
498+
if self._box_config["box_dots"] and isinstance(item, str):
499+
return ("[" in item) or ("." in item and not (self._box_config["box_dots_exclude"]
500+
and self._box_config["box_dots_exclude"].match(item)))
501+
return False
502+
492503
def __get_default(self, item, attr=False):
493504
if item in ("getdoc", "shape") and _is_ipython():
494505
return None
@@ -526,7 +537,7 @@ def __get_default(self, item, attr=False):
526537
value = default_value
527538
if self._box_config["default_box_create_on_get"]:
528539
if not attr or not (item.startswith("_") and item.endswith("_")):
529-
if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item):
540+
if self.__process_dotted_key(item):
530541
first_item, children = _parse_box_dots(self, item, setting=True)
531542
if first_item in self.keys():
532543
if hasattr(self[first_item], "__setitem__"):
@@ -602,7 +613,7 @@ def __getitem__(self, item, _ignore_default=False):
602613
for x in list(super().keys())[item.start : item.stop : item.step]:
603614
new_box[x] = self[x]
604615
return new_box
605-
if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item):
616+
if self.__process_dotted_key(item):
606617
try:
607618
first_item, children = _parse_box_dots(self, item)
608619
except BoxError:
@@ -652,7 +663,7 @@ def __getattr__(self, item):
652663
def __setitem__(self, key, value):
653664
if key != "_box_config" and self._box_config["frozen_box"] and self._box_config["__created"]:
654665
raise BoxError("Box is frozen")
655-
if self._box_config["box_dots"] and isinstance(key, str) and ("." in key or "[" in key):
666+
if self.__process_dotted_key(key):
656667
first_item, children = _parse_box_dots(self, key, setting=True)
657668
if first_item in self.keys():
658669
if hasattr(self[first_item], "__setitem__"):
@@ -696,12 +707,7 @@ def __setattr__(self, key, value):
696707
def __delitem__(self, key):
697708
if self._box_config["frozen_box"]:
698709
raise BoxError("Box is frozen")
699-
if (
700-
key not in self.keys()
701-
and self._box_config["box_dots"]
702-
and isinstance(key, str)
703-
and ("." in key or "[" in key)
704-
):
710+
if key not in self.keys() and self.__process_dotted_key(key):
705711
try:
706712
first_item, children = _parse_box_dots(self, key)
707713
except BoxError:

box/converters.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class BoxTomlDecodeError(BoxError, tomli.TOMLDecodeError): # type: ignore
119119
"box_duplicates",
120120
"box_intact_types",
121121
"box_dots",
122+
"box_dots_exclude",
122123
"box_recast",
123124
"box_class",
124125
"box_namespace",

test/test_box.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,13 @@ def test_dots(self):
944944
with pytest.raises(BoxKeyError):
945945
del b["a.b"]
946946

947+
def test_dots_exclusion(self):
948+
bx = Box.from_yaml(yaml_string="0.0.0.1: True",default_box=True,default_box_none_transform=False,box_dots=True,
949+
box_dots_exclude=r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+')
950+
assert bx["0.0.0.1"] == True
951+
with pytest.raises(BoxKeyError):
952+
del bx["0"]
953+
947954
def test_unicode(self):
948955
bx = Box()
949956
bx["\U0001f631"] = 4

0 commit comments

Comments
 (0)