Skip to content

Commit

Permalink
* Adding support for sliceable boxes (thanks to Dias)
Browse files Browse the repository at this point in the history
* Changing #215 support ruamel.yaml new syntax (thanks to Ivan Pepelnjak)
  • Loading branch information
cdgriffith committed Jan 10, 2022
1 parent b3ba1fb commit 0979451
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ release.bat
coverage/
# don't upload cython files
box/*.c
.pytest_cache/
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ repos:
hooks:
- id: cythonize-check
name: Cythonize
entry: bash -c 'python setup.py build_ext --inplace'
entry: python setup.py build_ext --inplace
language: system
types: [python]
pass_filenames: false
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ Version 6.0.0
-------------

* Adding Cython support to greatly speed up normal Box operations on supported systems
* Adding support for sliceable boxes (thanks to Dias)
* 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
* Fixing internal `_safe_key` logic to be twice as fast

Expand Down
7 changes: 7 additions & 0 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,13 @@ def __getitem__(self, item, _ignore_default=False):
if self._box_config["default_box"] and not _ignore_default:
return self.__get_default(item)
raise BoxKeyError(str(err)) from _exception_cause(err)
except TypeError as err:
if isinstance(item, slice):
new_box = self._box_config["box_class"](**self.__box_config())
for x in list(super().keys())[item.start : item.stop : item.step]:
new_box[x] = self[x]
return new_box
raise BoxTypeError(str(err)) from _exception_cause(err)

def __getattr__(self, item):
try:
Expand Down
67 changes: 54 additions & 13 deletions box/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,24 @@

from box.exceptions import BoxError

yaml_available = True
pyyaml_available = True
ruamel_available = True
toml_available = True
msgpack_available = True

try:
import ruamel.yaml as yaml
from ruamel.yaml import version_info, YAML
except ImportError:
try:
import yaml # type: ignore
except ImportError:
yaml = None # type: ignore
yaml_available = False
ruamel_available = False
else:
if version_info[1] < 17:
ruamel_available = False

try:
import yaml
except ImportError:
pyyaml_available = False

try:
import toml
except ImportError:
Expand All @@ -36,6 +42,8 @@
msgpack = None # type: ignore
msgpack_available = False

yaml_available = pyyaml_available or ruamel_available

BOX_PARAMETERS = (
"default_box",
"default_box_attr",
Expand Down Expand Up @@ -111,31 +119,64 @@ def _to_yaml(
default_flow_style: bool = False,
encoding: str = "utf-8",
errors: str = "strict",
ruamel_typ: str = "rt",
**yaml_kwargs,
):
if filename:
_exists(filename, create=True)
with open(filename, "w", encoding=encoding, errors=errors) as f:
yaml.dump(obj, stream=f, default_flow_style=default_flow_style, **yaml_kwargs)
if ruamel_available:
yaml_dumper = YAML(typ=ruamel_typ)
yaml_dumper.default_flow_style = default_flow_style
return yaml_dumper.dump(obj, stream=f, **yaml_kwargs)
elif pyyaml_available:
return yaml.dump(obj, stream=f, default_flow_style=default_flow_style, **yaml_kwargs)
else:
raise BoxError("No YAML Parser available, please install ruamel.yaml>0.17 or PyYAML")

else:
return yaml.dump(obj, default_flow_style=default_flow_style, **yaml_kwargs)
if ruamel_available:
yaml_dumper = YAML(typ=ruamel_typ)
yaml_dumper.default_flow_style = default_flow_style
with StringIO() as string_stream:
yaml_dumper.dump(obj, stream=string_stream, **yaml_kwargs)
return string_stream.getvalue()
elif pyyaml_available:
return yaml.dump(obj, default_flow_style=default_flow_style, **yaml_kwargs)
else:
raise BoxError("No YAML Parser available, please install ruamel.yaml>0.15 or PyYAML")


def _from_yaml(
yaml_string: str = None,
filename: Union[str, PathLike] = None,
encoding: str = "utf-8",
errors: str = "strict",
ruamel_typ: str = "rt",
**kwargs,
):
if "Loader" not in kwargs:
kwargs["Loader"] = yaml.SafeLoader
if filename:
_exists(filename)
with open(filename, "r", encoding=encoding, errors=errors) as f:
data = yaml.load(f, **kwargs)
if ruamel_available:
yaml_loader = YAML(typ=ruamel_typ)
data = yaml_loader.load(stream=f)
elif pyyaml_available:
if "Loader" not in kwargs:
kwargs["Loader"] = yaml.SafeLoader
data = yaml.load(f, **kwargs)
else:
raise BoxError("No YAML Parser available, please install ruamel.yaml>0.15 or PyYAML")
elif yaml_string:
data = yaml.load(yaml_string, **kwargs)
if ruamel_available:
yaml_loader = YAML(typ=ruamel_typ)
data = yaml_loader.load(stream=yaml_string)
elif pyyaml_available:
if "Loader" not in kwargs:
kwargs["Loader"] = yaml.SafeLoader
data = yaml.load(yaml_string, **kwargs)
else:
raise BoxError("No YAML Parser available, please install ruamel.yaml>0.17 or PyYAML")
else:
raise BoxError("from_yaml requires a string or filename")
return data
Expand Down
2 changes: 2 additions & 0 deletions box/converters.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ def _to_yaml(
default_flow_style: bool = False,
encoding: str = "utf-8",
errors: str = "strict",
ruamel_typ: str = "rt",
**yaml_kwargs,
) -> Any: ...
def _from_yaml(
yaml_string: str = None,
filename: Union[str, PathLike] = None,
encoding: str = "utf-8",
errors: str = "strict",
ruamel_typ: str = "rt",
**kwargs,
) -> Any: ...
def _to_toml(obj, filename: Union[str, PathLike] = None, encoding: str = "utf-8", errors: str = "strict") -> Any: ...
Expand Down
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ pytest>=5.4.1
pytest-cov>=2.8.1
ruamel.yaml>=0.16,<0.17
toml>=0.10.2
types-PyYAML>=6.0.3
types-toml>=0.1.3
wheel>=0.34.2
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
msgpack>=1.0.0
ruamel.yaml>=0.16.10,<0.17
ruamel.yaml>=0.15
toml>=0.10.2
21 changes: 17 additions & 4 deletions test/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import shutil
from multiprocessing import Queue
from pathlib import Path
from io import StringIO
from test.common import (
data_json_file,
data_yaml_file,
Expand All @@ -23,7 +24,7 @@
)

import pytest
import ruamel.yaml as yaml
from ruamel.yaml import YAML

from box import Box, BoxError, BoxKeyError, BoxList, ConfigBox, SBox, box
from box.box import _get_dot_paths # type: ignore
Expand Down Expand Up @@ -208,13 +209,17 @@ def test_to_json_basic(self):

def test_to_yaml_basic(self):
a = Box(test_dict)
assert yaml.load(a.to_yaml(), Loader=yaml.SafeLoader) == test_dict
yaml = YAML(typ="safe")
print("HERE")
print(a.to_yaml())
assert yaml.load(a.to_yaml()) == test_dict

def test_to_yaml_file(self):
a = Box(test_dict)
a.to_yaml(tmp_yaml_file)
with open(tmp_yaml_file) as f:
data = yaml.load(f, Loader=yaml.SafeLoader)
yaml = YAML(typ="safe")
data = yaml.load(f)
assert data == test_dict

def test_dir(self):
Expand Down Expand Up @@ -347,7 +352,11 @@ def test_from_json(self):
assert bx.key1 == "value1"

def test_from_yaml(self):
bx = Box.from_yaml(yaml.dump(test_dict), conversion_box=False, default_box=True)
yaml = YAML(typ="safe")
with StringIO() as sio:
yaml.dump(test_dict, sio)
data = sio.getvalue()
bx = Box.from_yaml(data, conversion_box=False, default_box=True)
assert isinstance(bx, Box)
assert bx.key1 == "value1"
assert bx.Key_2 == Box()
Expand Down Expand Up @@ -1281,3 +1290,7 @@ def test_setdefault_dots_default(self):
assert box.d.e["f.g"] == True
assert isinstance(box["e.f"], BoxList)
assert box.e.f[1] == 2

def test_box_slice(self):
data = Box(qwe=123, asd=234, q=1)
assert data[:-1] == Box(qwe=123, asd=234)

0 comments on commit 0979451

Please sign in to comment.