diff --git a/CHANGELOG.md b/CHANGELOG.md index bdbf18fd4369d888ea0607552d08891212c1a88a..5715081c5c3ee7ec8b495bc53b7d7772e272ddc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,5 @@ # v2.14.23 + +* added `write_json_to_file` +* added `read_json_from_file` +* added `write_json_to_file_if_different` diff --git a/docs/json.rst b/docs/json.rst index 904496bb66475b8568f0d8f9a7c53d453277fd40..3ef1e4858bc32a7c52bbd905ddf4ffc6102b2e82 100644 --- a/docs/json.rst +++ b/docs/json.rst @@ -21,3 +21,11 @@ This will serialize unknown objects in the following way. First, **__dict__** will be extracted out of this object. The dictionary will be constructed in such a way, that for each key in this **__dict__**, it's value's **repr** will be assigned. + +.. autofunction:: satella.json.read_json_from_file + +.. autofunction:: satella.json.write_json_to_file + +.. autofunction:: satella.json.write_json_to_file_if_different + + diff --git a/satella/__init__.py b/satella/__init__.py index 8bce66746645d2755a6ed9293f4935839c96a536..702d0b12c8b6dbd385e2b9a9af9777a7f59d52b0 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.14.23_a1' +__version__ = '2.14.23_a2' diff --git a/satella/json.py b/satella/json.py index 77efdbdd1c18874cc6b4ccb7e2b779f31ddbfbcd..b8704306c55c3ee44e4613f22c7df8496d9eb3d1 100644 --- a/satella/json.py +++ b/satella/json.py @@ -1,8 +1,13 @@ import json import typing as tp from abc import ABCMeta, abstractmethod +try: + import ujson +except ImportError: + pass -__all__ = ['JSONEncoder', 'JSONAble', 'json_encode'] +__all__ = ['JSONEncoder', 'JSONAble', 'json_encode', 'read_json_from_file', + 'write_json_to_file', 'write_json_to_file_if_different'] Jsonable = tp.TypeVar('Jsonable', list, dict, str, int, float, None) @@ -39,3 +44,61 @@ def json_encode(x: tp.Any) -> str: :param x: object to convert """ return JSONEncoder().encode(x) + + +def write_json_to_file(path: str, value: JSONAble) -> None: + """ + Write out a JSON to a file as UTF-8 encoded plain text. + + :param path: path to the file + :param value: JSON-able content + """ + if isinstance(value, JSONAble): + value = value.to_json() + with open(path, 'w') as f_out: + try: + ujson.dump(value, f_out) + except NameError: + json.dump(value, f_out) + + +def write_json_to_file_if_different(path: str, value: JSONAble) -> bool: + """ + Read JSON from a file. Write out a JSON to a file if it's value is different, + as UTF-8 encoded plain text. + + :param path: path to the file + :param value: JSON-able content + :return: whether the write actually happened + """ + if isinstance(value, JSONAble): + value = value.to_json() + try: + val = read_json_from_file(path) + if val != value: + write_json_to_file(path, value) + return True + return False + except (OSError, ValueError): + write_json_to_file(path, value) + return True + + +def read_json_from_file(path: str) -> JSONAble: + """ + Load a JSON from a provided file, as UTF-8 encoded plain text. + + :param path: path to the file + :return: JSON content + :raises ValueError: the file contained an invalid JSON + :raises OSError: the file was not readable or did not exist + """ + try: + with open(path, 'r') as f_in: + return ujson.load(f_in) + except NameError: + with open(path, 'r') as f_in: + try: + return json.load(f_in) + except json.decoder.JSONDecodeError as e: + raise ValueError(str(e)) diff --git a/setup.py b/setup.py index 729eeac1c4e8084a887043a2019d2ef670c5881c..657a930f18aa974434c602bb20e55ad27d1047d8 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup(keywords=['ha', 'high availability', 'scalable', 'scalability', 'server', 'HTTPJSONSource': ['requests'], 'YAMLSource': ['pyyaml'], 'TOMLSource': ['toml'], - 'FasterJSONSource': ['ujson'], + 'FasterJSON': ['ujson'], 'cassandra': ['cassandra-driver'], 'opentracing': ['opentracing'] } diff --git a/tests/test_json.py b/tests/test_json.py index f1f120c0ac080a207c35cf8ecb50a0a27ce9e818..1966d3c5f33e56be3bb45cfc6a8615f60b6f2e56 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -2,11 +2,25 @@ import json import typing as tp import unittest -from satella.json import JSONAble, json_encode +from satella.json import JSONAble, json_encode, read_json_from_file, write_json_to_file, \ +write_json_to_file_if_different class TestJson(unittest.TestCase): + def test_write_json_to_file_if_different(self): + d = {'test': 4} + self.assertTrue(write_json_to_file_if_different('test2.json', d)) + self.assertFalse(write_json_to_file_if_different('test2.json', d)) + d = {'test': 5} + self.assertTrue(write_json_to_file_if_different('test2.json', d)) + self.assertFalse(write_json_to_file_if_different('test2.json', d)) + + def test_load_json_from_file(self): + d = {'test': 2} + write_json_to_file('test.json', d) + self.assertEqual(read_json_from_file('test.json'), d) + def test_jsonable_objects(self): class MyClass(JSONAble): def to_json(self) -> tp.Union[list, dict, str, int, float, None]: