diff --git a/box/box.py b/box/box.py index 8be6738..cfcf4e9 100644 --- a/box/box.py +++ b/box/box.py @@ -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 """ @@ -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, @@ -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, } @@ -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, @@ -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, } @@ -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 @@ -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__"): @@ -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: @@ -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__"): @@ -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: diff --git a/box/converters.py b/box/converters.py index 80c1ced..89709da 100644 --- a/box/converters.py +++ b/box/converters.py @@ -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", diff --git a/test/test_box.py b/test/test_box.py index 1232f3c..cc6e82c 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -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