diff --git a/AUTHORS.rst b/AUTHORS.rst index 6b44851..6b60bcc 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -37,6 +37,7 @@ Code contributions: - Jesper Schlegel (jesperschlegel) - J vanBemmel (jbemmel) - m-janicki +- Arindam Sikder (arindamsikder) Suggestions and bug reporting: diff --git a/CHANGES.rst b/CHANGES.rst index c4a347d..2371794 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Changelog ========= +Unreleased +---------- + +* Raise a clear error for bracket paths without a numeric list index instead of AttributeError (#265). + Version 7.4.1 ------------- diff --git a/box/box_list.py b/box/box_list.py index 72609ff..2876d95 100644 --- a/box/box_list.py +++ b/box/box_list.py @@ -68,6 +68,8 @@ def frozen(*args, **kwargs): def __getitem__(self, item): if self.box_options.get("box_dots") and isinstance(item, str) and item.startswith("["): list_pos = _list_pos_re.search(item) + if list_pos is None: + raise BoxTypeError(f"Invalid list index: {item!r}") value = super().__getitem__(int(list_pos.groups()[0])) if len(list_pos.group()) == len(item): return value @@ -87,6 +89,8 @@ def __delitem__(self, key): raise BoxError("BoxList is frozen") if self.box_options.get("box_dots") and isinstance(key, str) and key.startswith("["): list_pos = _list_pos_re.search(key) + if list_pos is None: + raise BoxTypeError(f"Invalid list index: {key!r}") pos = int(list_pos.groups()[0]) if len(list_pos.group()) == len(key): return super().__delitem__(pos) @@ -99,6 +103,8 @@ def __setitem__(self, key, value): raise BoxError("BoxList is frozen") if self.box_options.get("box_dots") and isinstance(key, str) and key.startswith("["): list_pos = _list_pos_re.search(key) + if list_pos is None: + raise BoxTypeError(f"Invalid list index: {key!r}") pos = int(list_pos.groups()[0]) if pos >= len(self) and self.box_options.get("default_box"): self.extend([None] * (pos - len(self) + 1)) diff --git a/test/test_box.py b/test/test_box.py index e5c56e0..2ec44ae 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -349,6 +349,15 @@ def test_from_json_file(self): assert isinstance(bx, Box) assert bx.widget.window.height == 500 + def test_from_yaml_invalid_bracket_key(self): + key = r"^[^_^\d][\w]*$" + yaml_string = f"'{key}': {{}}" + with pytest.raises(BoxError) as error: + Box.from_yaml(yaml_string, default_box=True, box_dots=True) + assert "Invalid list index" in str(error.value) + assert repr(key[1:]) in str(error.value) + assert Box.from_yaml(yaml_string, default_box=True, box_dots=False).to_dict() == {key: {}} + def test_from_yaml_file(self): bx = Box.from_yaml(filename=data_yaml_file) assert isinstance(bx, Box) diff --git a/test/test_box_list.py b/test/test_box_list.py index 9cd90e0..5010007 100644 --- a/test/test_box_list.py +++ b/test/test_box_list.py @@ -222,6 +222,61 @@ def test_box_list_dots(self): for key in keys: db[key] + @pytest.mark.parametrize("key", ["[x]", "[]", "[", "[1x]"]) + @pytest.mark.parametrize("default_box", [False, True]) + def test_box_list_dots_invalid_get(self, key, default_box): + data = BoxList([{"value": 1}], box_dots=True, default_box=default_box) + with pytest.raises(BoxError) as error: + data[key] + assert "Invalid list index" in str(error.value) + assert repr(key) in str(error.value) + assert data.to_list() == [{"value": 1}] + + @pytest.mark.parametrize("key", ["[x]", "[]", "[", "[1x]"]) + @pytest.mark.parametrize("default_box", [False, True]) + def test_box_list_dots_invalid_set(self, key, default_box): + data = BoxList([{"value": 1}], box_dots=True, default_box=default_box) + with pytest.raises(BoxError) as error: + data[key] = 2 + assert "Invalid list index" in str(error.value) + assert repr(key) in str(error.value) + assert data.to_list() == [{"value": 1}] + + @pytest.mark.parametrize("key", ["[x]", "[]", "[", "[1x]"]) + @pytest.mark.parametrize("default_box", [False, True]) + def test_box_list_dots_invalid_delete(self, key, default_box): + data = BoxList([{"value": 1}], box_dots=True, default_box=default_box) + with pytest.raises(BoxError) as error: + del data[key] + assert "Invalid list index" in str(error.value) + assert repr(key) in str(error.value) + assert data.to_list() == [{"value": 1}] + + @pytest.mark.parametrize("method, args", [("__getitem__", ()), ("__setitem__", (2,)), ("__delitem__", ())]) + def test_box_list_dots_invalid_nested_index(self, method, args): + data = BoxList([[1]], box_dots=True) + with pytest.raises(BoxError) as error: + getattr(data, method)("[0][x]", *args) + assert "Invalid list index: '[x]'" in str(error.value) + assert data.to_list() == [[1]] + + def test_box_list_dots_frozen_invalid_index(self): + data = BoxList([1], box_dots=True, frozen_box=True) + with pytest.raises(BoxError, match="Invalid list index"): + data["[x]"] + with pytest.raises(BoxError, match="BoxList is frozen"): + data["[x]"] = 2 + with pytest.raises(BoxError, match="BoxList is frozen"): + del data["[x]"] + assert data.to_list() == [1] + + @pytest.mark.parametrize("method, args", [("__getitem__", ()), ("__setitem__", (2,)), ("__delitem__", ())]) + def test_box_list_invalid_index_without_dots(self, method, args): + data = BoxList([1], box_dots=False) + with pytest.raises(TypeError, match="list indices must be integers or slices"): + getattr(data, method)("[x]", *args) + assert data.to_list() == [1] + def test_box_list_default_dots(self): box_1 = Box(default_box=True, box_dots=True) box_1["a[0]"] = 42