From ddc939e039054ff13aa2caa5696ade288d1a7c00 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 29 Oct 2022 16:26:28 -0500 Subject: [PATCH] * Adding #195 box_from_string function (thanks to Marcelo Huerta) --- .pre-commit-config.yaml | 2 +- CHANGES.rst | 1 + box/__init__.py | 2 +- box/box.py | 1 + box/converters.py | 45 ++++++++++++++++++++++++-------------- box/converters.pyi | 3 ++- box/from_file.py | 48 +++++++++++++++++++++++++++++++---------- box/from_file.pyi | 1 + test/test_from_file.py | 12 ++++++++++- 9 files changed, 84 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index baad9fd..6270614 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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] diff --git a/CHANGES.rst b/CHANGES.rst index e900482..cf5081a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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) diff --git a/box/__init__.py b/box/__init__.py index fd4fb3b..8a44dd1 100644 --- a/box/__init__.py +++ b/box/__init__.py @@ -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 diff --git a/box/box.py b/box/box.py index b6bb150..06b8a60 100644 --- a/box/box.py +++ b/box/box.py @@ -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') diff --git a/box/converters.py b/box/converters.py index 6d1c7cb..60c0926 100644 --- a/box/converters.py +++ b/box/converters.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/box/converters.pyi b/box/converters.pyi index d379dfb..f18d1cf 100644 --- a/box/converters.pyi +++ b/box/converters.pyi @@ -1,6 +1,6 @@ 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 @@ -8,6 +8,7 @@ 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( diff --git a/box/from_file.py b/box/from_file.py index 3af9516..976fe13 100644 --- a/box/from_file.py +++ b/box/from_file.py @@ -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: @@ -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): @@ -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") @@ -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}") diff --git a/box/from_file.pyi b/box/from_file.pyi index 0dc30f5..00657eb 100644 --- a/box/from_file.pyi +++ b/box/from_file.pyi @@ -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]: ... diff --git a/test/test_from_file.py b/test/test_from_file.py index 90b8076..7361e84 100644 --- a/test/test_from_file.py +++ b/test/test_from_file.py @@ -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: @@ -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")