Skip to content

Commit

Permalink
* Adding #195 box_from_string function (thanks to Marcelo Huerta)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdgriffith committed Oct 29, 2022
1 parent 1b8d9dd commit ddc939e
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ repos:
hooks:
- id: mypy
types: [python]
additional_dependencies: [ruamel.yaml,tomli,tomli-w,msgpack,types-PyYAML]
additional_dependencies: [ruamel.yaml,toml,types-toml,tomli,tomli-w,msgpack,types-PyYAML]
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Version 6.1.0
-------------

* Adding Python 3.11 support
* Adding #195 box_from_string function (thanks to Marcelo Huerta)
* Changing the deprecated ``toml`` package with modern ``tomllib``, ``tomli`` and ``tomli-w`` usage
* Changing the tests requiring `toml` if it is not available (thanks to Michał Górny)
* Fixing mypy __ior__ type (thanks to Jacob Hayes)
Expand Down
2 changes: 1 addition & 1 deletion box/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from box.box_list import BoxList
from box.config_box import ConfigBox
from box.exceptions import BoxError, BoxKeyError
from box.from_file import box_from_file
from box.from_file import box_from_file, box_from_string
from box.shorthand_box import SBox
import box.converters

Expand Down
1 change: 1 addition & 0 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ def to_toml(self, filename: Union[str, PathLike] = None, encoding: str = "utf-8"
return _to_toml(self.to_dict(), filename=filename, encoding=encoding, errors=errors)

else:

def to_toml(self, filename: Union[str, PathLike] = None, encoding: str = "utf-8", errors: str = "strict"):
raise BoxError('toml is unavailable on this system, please install the "tomli-w" package')

Expand Down
45 changes: 29 additions & 16 deletions box/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
toml_write_library: Optional[Any] = None
toml_decode_error: Optional[Callable] = None


class BoxTomlDecodeError(BoxError):
"""Toml Error"""
"""Toml Decode Error"""


try:
import toml
Expand All @@ -46,7 +48,9 @@ class BoxTomlDecodeError(BoxError):
toml_write_library = toml
toml_decode_error = toml.TomlDecodeError

class BoxTomlDecodeError(BoxError, toml.TomlDecodeError): pass
class BoxTomlDecodeError(BoxError, toml.TomlDecodeError): # type: ignore
"""Toml Decode Error"""


try:
import tomllib
Expand All @@ -55,7 +59,10 @@ class BoxTomlDecodeError(BoxError, toml.TomlDecodeError): pass
else:
toml_read_library = tomllib
toml_decode_error = tomllib.TomlDecodeError
class BoxTomlDecodeError(BoxError, tomllib.TomlDecodeError): pass

class BoxTomlDecodeError(BoxError, tomllib.TomlDecodeError): # type: ignore
"""Toml Decode Error"""


try:
import tomli
Expand All @@ -64,7 +71,10 @@ class BoxTomlDecodeError(BoxError, tomllib.TomlDecodeError): pass
else:
toml_read_library = tomli
toml_decode_error = tomli.TOMLDecodeError
class BoxTomlDecodeError(BoxError, tomli.TOMLDecodeError): pass

class BoxTomlDecodeError(BoxError, tomli.TOMLDecodeError): # type: ignore
"""Toml Decode Error"""


try:
import tomli_w
Expand Down Expand Up @@ -236,38 +246,41 @@ def _from_yaml(
def _to_toml(obj, filename: Union[str, PathLike] = None, encoding: str = "utf-8", errors: str = "strict"):
if filename:
_exists(filename, create=True)
if toml_write_library.__name__ == 'toml':
if toml_write_library.__name__ == "toml": # type: ignore
with open(filename, "w", encoding=encoding, errors=errors) as f:
try:
toml_write_library.dump(obj, f)
except toml_decode_error as err:
toml_write_library.dump(obj, f) # type: ignore
except toml_decode_error as err: # type: ignore
raise BoxTomlDecodeError(err) from err
else:
with open(filename, "wb") as f:
try:
toml_write_library.dump(obj, f)
except toml_decode_error as err:
toml_write_library.dump(obj, f) # type: ignore
except toml_decode_error as err: # type: ignore
raise BoxTomlDecodeError(err) from err
else:
try:
return toml_write_library.dumps(obj)
except toml_decode_error as err:
return toml_write_library.dumps(obj) # type: ignore
except toml_decode_error as err: # type: ignore
raise BoxTomlDecodeError(err) from err


def _from_toml(
toml_string: str = None, filename: Union[str, PathLike] = None, encoding: str = "utf-8", errors: str = "strict",
toml_string: str = None,
filename: Union[str, PathLike] = None,
encoding: str = "utf-8",
errors: str = "strict",
):
if filename:
_exists(filename)
if toml_read_library.__name__ == 'toml':
if toml_read_library.__name__ == "toml": # type: ignore
with open(filename, "r", encoding=encoding, errors=errors) as f:
data = toml_read_library.load(f)
data = toml_read_library.load(f) # type: ignore
else:
with open(filename, "rb") as f:
data = toml_read_library.load(f)
data = toml_read_library.load(f) # type: ignore
elif toml_string:
data = toml_read_library.loads(toml_string)
data = toml_read_library.loads(toml_string) # type: ignore
else:
raise BoxError("from_toml requires a string or filename")
return data
Expand Down
3 changes: 2 additions & 1 deletion box/converters.pyi
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from box.exceptions import BoxError as BoxError
from os import PathLike as PathLike
from typing import Any, Union, Optional, Dict
from typing import Any, Union, Optional, Dict, Callable

yaml_available: bool
toml_available: bool
msgpack_available: bool
BOX_PARAMETERS: Any
toml_read_library: Optional[Any]
toml_write_library: Optional[Any]
toml_decode_error: Optional[Callable]

def _exists(filename: Union[str, PathLike], create: bool = False) -> Any: ...
def _to_json(
Expand Down
48 changes: 37 additions & 11 deletions box/from_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from box.box import Box
from box.box_list import BoxList
from box.converters import msgpack_available, toml_read_library, yaml_available
from box.converters import msgpack_available, toml_read_library, yaml_available, toml_decode_error
from box.exceptions import BoxError

try:
Expand All @@ -19,21 +19,13 @@
except ImportError:
YAMLError = False # type: ignore

if sys.version_info >= (3, 11):
from tomllib import TOMLDecodeError # type: ignore
else:
try:
from tomli import TOMLDecodeError # type: ignore
except ImportError:
TOMLDecodeError = False # type: ignore

try:
from msgpack import UnpackException # type: ignore
except ImportError:
UnpackException = False # type: ignore


__all__ = ["box_from_file"]
__all__ = ["box_from_file", "box_from_string"]


def _to_json(file, encoding, errors, **kwargs):
Expand Down Expand Up @@ -67,7 +59,7 @@ def _to_toml(file, encoding, errors, **kwargs):
raise BoxError(f'File "{file}" is toml but no package is available to open it. Please install "tomli"')
try:
return Box.from_toml(filename=file, encoding=encoding, errors=errors, **kwargs)
except TOMLDecodeError:
except toml_decode_error:
raise BoxError("File is not TOML as expected")


Expand Down Expand Up @@ -117,3 +109,37 @@ def box_from_file(
if file_type.lower() in converters:
return converters[file_type.lower()](file, encoding, errors, **kwargs) # type: ignore
raise BoxError(f'"{file_type}" is an unknown type. Please use either csv, toml, msgpack, yaml or json')


def box_from_string(content: str, string_type: str = "json") -> Union[Box, BoxList]:
"""
Parse the provided string into a Box or BoxList object as appropriate.
:param content: String to parse
:param string_type: manually specify file type: json, toml or yaml
:return: Box or BoxList
"""

if string_type == "json":
try:
return Box.from_json(json_string=content)
except JSONDecodeError:
raise BoxError("File is not JSON as expected")
except BoxError:
return BoxList.from_json(json_string=content)
elif string_type == "toml":
try:
return Box.from_toml(toml_string=content)
except JSONDecodeError:
raise BoxError("File is not JSON as expected")
except BoxError:
return BoxList.from_toml(toml_string=content)
elif string_type == "yaml":
try:
return Box.from_yaml(yaml_string=content)
except JSONDecodeError:
raise BoxError("File is not JSON as expected")
except BoxError:
return BoxList.from_yaml(yaml_string=content)
else:
raise BoxError(f"Unsupported string_string of {string_type}")
1 change: 1 addition & 0 deletions box/from_file.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ from typing import Any, Union
def box_from_file(
file: Union[str, PathLike], file_type: str = ..., encoding: str = ..., errors: str = ..., **kwargs: Any
) -> Union[Box, BoxList]: ...
def box_from_string(content: str, string_type: str = ...) -> Union[Box, BoxList]: ...
12 changes: 11 additions & 1 deletion test/test_from_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest

from box import Box, BoxError, BoxList, box_from_file
from box import Box, BoxError, BoxList, box_from_file, box_from_string


class TestFromFile:
Expand Down Expand Up @@ -37,3 +37,13 @@ def test_bad_file(self):
box_from_file(Path(test_root, "data", "bad_file.txt"))
with pytest.raises(BoxError):
box_from_file("does not exist")

def test_from_string_all(self):
with open(Path(test_root, "data", "json_file.json"), "r") as f:
box_from_string(f.read())

with open(Path(test_root, "data", "toml_file.tml"), "r") as f:
box_from_string(f.read(), string_type="toml")

with open(Path(test_root, "data", "yaml_file.yaml"), "r") as f:
box_from_string(f.read(), string_type="yaml")

0 comments on commit ddc939e

Please sign in to comment.