Skip to content

Commit

Permalink
* Adding #164 default_box_create_on_get toggle to disable setting box…
Browse files Browse the repository at this point in the history
… variable on get request (thanks to ipcoder)

* Changing #123 box_dots to convert ints and bytes as needed (thanks to Marcelo Huerta)
* Changing #192 box_dots and box_default to split and create sub dictionaries as needed for insertion (thanks to Rexbard)
  • Loading branch information
cdgriffith committed Feb 14, 2022
1 parent 28fa27a commit 87f3733
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
package-checks:
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Version 6.0.0
* Adding #161 support for access box dots with `get` and checking with `in` (thanks to scott-createplay)
* Adding #183 support for all allowed character sets (thanks to Giulio Malventi)
* Adding #196 support for sliceable boxes (thanks to Dias)
* Adding #164 default_box_create_on_get toggle to disable setting box variable on get request (thanks to ipcoder)
* Changing #123 box_dots to convert ints and bytes as needed (thanks to Marcelo Huerta)
* Changing #192 box_dots and box_default to split and create sub dictionaries as needed for insertion (thanks to Rexbard)
* Changing #208 __repr__ to produce `eval`-able text (thanks to Jeff Robbins)
* Changing #215 support ruamel.yaml new syntax (thanks to Ivan Pepelnjak)
* Changing `update` and `merge_update` to not use a keyword that could cause issues in rare circumstances
Expand Down
2 changes: 1 addition & 1 deletion box/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-

__author__ = "Chris Griffith"
__version__ = "6.0.0rc4"
__version__ = "6.0.0rc5"

from box.box import Box
from box.box_list import BoxList
Expand Down
81 changes: 61 additions & 20 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import warnings
from keyword import iskeyword
from os import PathLike
from typing import Any, Dict, Generator, List, Tuple, Union
from typing import Any, Dict, Generator, List, Tuple, Union, Type

try:
from typing import Callable, Iterable, Mapping
Expand Down Expand Up @@ -161,6 +161,7 @@ def __new__(
default_box: bool = False,
default_box_attr: Any = NO_DEFAULT,
default_box_none_transform: bool = True,
default_box_create_on_get: bool = True,
frozen_box: bool = False,
camel_killer_box: bool = False,
conversion_box: bool = True,
Expand All @@ -170,7 +171,7 @@ def __new__(
box_intact_types: Union[Tuple, List] = (),
box_recast: Dict = None,
box_dots: bool = False,
box_class: Union[Dict, "Box"] = None,
box_class: Union[Dict, Type["Box"]] = None,
**kwargs: Any,
):
"""
Expand All @@ -184,6 +185,7 @@ def __new__(
"default_box": default_box,
"default_box_attr": cls.__class__ if default_box_attr is NO_DEFAULT else default_box_attr,
"default_box_none_transform": default_box_none_transform,
"default_box_create_on_get": default_box_create_on_get,
"conversion_box": conversion_box,
"box_safe_prefix": box_safe_prefix,
"frozen_box": frozen_box,
Expand All @@ -204,6 +206,7 @@ def __init__(
default_box: bool = False,
default_box_attr: Any = NO_DEFAULT,
default_box_none_transform: bool = True,
default_box_create_on_get: bool = True,
frozen_box: bool = False,
camel_killer_box: bool = False,
conversion_box: bool = True,
Expand All @@ -213,7 +216,7 @@ def __init__(
box_intact_types: Union[Tuple, List] = (),
box_recast: Dict = None,
box_dots: bool = False,
box_class: Union[Dict, "Box"] = None,
box_class: Union[Dict, Type["Box"]] = None,
**kwargs: Any,
):
super().__init__()
Expand All @@ -223,6 +226,7 @@ def __init__(
"default_box": default_box,
"default_box_attr": self.__class__ if default_box_attr is NO_DEFAULT else default_box_attr,
"default_box_none_transform": default_box_none_transform,
"default_box_create_on_get": default_box_create_on_get,
"conversion_box": conversion_box,
"box_safe_prefix": box_safe_prefix,
"frozen_box": frozen_box,
Expand Down Expand Up @@ -346,12 +350,25 @@ def __dir__(self):

return list(items)

def _box_dots_item(self, item):
if isinstance(item, str):
return item
if isinstance(item, (bytes, bytearray)):
return item.decode(encoding="utf-8", errors="ignore")
if isinstance(item, int):
return str(item)

def __contains__(self, item):
in_me = super().__contains__(item)
if not self._box_config["box_dots"] or not isinstance(item, str):
if not self._box_config["box_dots"]:
return in_me
if in_me:
return True
if not isinstance(item, (bytearray, bytes, str, int)):
return in_me
item = self._box_dots_item(item)
if item in [self._box_dots_item(key) for key in self.keys()]:
return True
if "." not in item:
return False
try:
Expand Down Expand Up @@ -443,8 +460,9 @@ def __get_default(self, item, attr=False):
value = default_value.copy()
else:
value = default_value
if not attr or not (item.startswith("_") and item.endswith("_")):
super().__setitem__(item, value)
if self._box_config["default_box_create_on_get"]:
if not attr or not (item.startswith("_") and item.endswith("_")):
super().__setitem__(item, value)
return value

def __box_config(self) -> Dict:
Expand Down Expand Up @@ -499,16 +517,18 @@ def __getitem__(self, item, _ignore_default=False):
if item == "_box_config":
cause = _exception_cause(err)
raise BoxKeyError("_box_config should only exist as an attribute and is never defaulted") from cause
if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item):
try:
first_item, children = _parse_box_dots(self, item)
except BoxError:
if self._box_config["default_box"] and not _ignore_default:
return self.__get_default(item)
raise
if first_item in self.keys():
if hasattr(self[first_item], "__getitem__"):
return self[first_item][children]
if self._box_config["box_dots"]:
box_dots_items = self._box_dots_item(item)
if isinstance(box_dots_items, str) and ("." in box_dots_items or "[" in box_dots_items):
try:
first_item, children = _parse_box_dots(self, box_dots_items)
except BoxError:
if self._box_config["default_box"] and not _ignore_default:
return self.__get_default(box_dots_items)
raise
if first_item in self.keys():
if hasattr(self[first_item], "__getitem__"):
return self[first_item][children]
if self._box_config["camel_killer_box"] and isinstance(item, str):
converted = _camel_killer(item)
if converted in self.keys():
Expand Down Expand Up @@ -549,11 +569,32 @@ 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):
first_item, children = _parse_box_dots(self, key, setting=True)
if first_item in self.keys():
if hasattr(self[first_item], "__setitem__"):
if self._box_config["box_dots"]:
box_dots_items = self._box_dots_item(key)
if isinstance(box_dots_items, str) and ("." in box_dots_items or "[" in box_dots_items):
first_item, children = _parse_box_dots(self, box_dots_items, setting=True)

is_first_numeric = first_item.isnumeric()

if isinstance(key, (bytes, bytearray)):
first_item = first_item.encode(encoding="utf-8", errors="ignore")
children = children.encode(encoding="utf-8", errors="ignore")

if is_first_numeric and int(first_item) in self.keys():
if hasattr(self[int(first_item)], "__setitem__"):
return self[int(first_item)].__setitem__(children, value)

if first_item in self.keys():
if hasattr(self[first_item], "__setitem__"):
return self[first_item].__setitem__(children, value)

if self._box_config["default_box"]:
if is_first_numeric:
first_item = int(first_item)

self[first_item] = self._box_config["box_class"](**self.__box_config())
return self[first_item].__setitem__(children, value)

value = self.__recast(key, value)
if key not in self.keys() and self._box_config["camel_killer_box"]:
if self._box_config["camel_killer_box"] and isinstance(key, str):
Expand Down
2 changes: 2 additions & 0 deletions box/box.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Box(dict):
default_box: bool = ...,
default_box_attr: Any = ...,
default_box_none_transform: bool = ...,
default_box_create_on_get: bool = ...,
frozen_box: bool = ...,
camel_killer_box: bool = ...,
conversion_box: bool = ...,
Expand All @@ -27,6 +28,7 @@ class Box(dict):
default_box: bool = ...,
default_box_attr: Any = ...,
default_box_none_transform: bool = ...,
default_box_create_on_get: bool = ...,
frozen_box: bool = ...,
camel_killer_box: bool = ...,
conversion_box: bool = ...,
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
Expand Down
38 changes: 36 additions & 2 deletions test/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,9 +1230,9 @@ def test_default_dots(self):
a["a.a.a.."]
assert a == {"a.a.a": {"": {"": {}}}}
a["b.b"] = 3
assert a == {"a.a.a": {"": {"": {}}}, "b.b": 3}
assert a == {"a.a.a": {"": {"": {}}}, "b": {"b": 3}}
a.b.b = 4
assert a == {"a.a.a": {"": {"": {}}}, "b.b": 3, "b": {"b": 4}}
assert a == {"a.a.a": {"": {"": {}}}, "b": {"b": 4}}
assert a["non.existent.key"] == {}

def test_merge_list_options(self):
Expand Down Expand Up @@ -1329,3 +1329,37 @@ def test_box_greek(self):
a.σeq = 1
a.µeq = 2
assert a == Box({"σeq": 1, "μeq": 2})

def test_box_dots_numbered_keys(self):
box = Box(a={2: {"c": "done"}}, box_dots=True)

assert "a.2" in box
assert box.a == Box({2: {"c": "done"}})

def test_box_dots_defaults(self):
box = Box(a={2: {"c": "done"}}, box_dots=True)
with pytest.raises(BoxKeyError):
box.d.e = 5

box["d.e"] = 5
assert "d.e" in list(box.keys()) # Do this because box.__contains__ looks for box_dot stuff

box2 = Box(box_dots=True, default_box=True)
box2[b"4.7.8.e"] = 5
assert box2 == Box({4: {7: {8: {b"e": 5}}}})

def test_box_default_not_create_on_get(self):
box = Box(default_box=True)

assert box.a.b.c == Box()

assert box == Box(a=Box(b=Box(c=Box())))
assert "c" in box.a.b

box2 = Box(default_box=True, default_box_create_on_get=False)

assert box2.a.b.c == Box()

assert "c" not in box2.a.b

assert box2 == Box()

0 comments on commit 87f3733

Please sign in to comment.