edq.util.json
This file standardizes how we write and read JSON. Specifically, we try to be flexible when reading (using JSON5), and strict when writing (using vanilla JSON).
1""" 2This file standardizes how we write and read JSON. 3Specifically, we try to be flexible when reading (using JSON5), 4and strict when writing (using vanilla JSON). 5""" 6 7import enum 8import json 9import os 10import typing 11 12import json5 13 14import edq.util.dirent 15 16class DictConverter(): 17 """ 18 A base class for class that can represent (serialize) and reconstruct (deserialize) themselves as/from a dict. 19 The intention is that the dict can then be cleanly converted to/from JSON. 20 21 General (but inefficient) implementations of several core Python equality, comparison, and representation methods are provided. 22 """ 23 24 def to_dict(self) -> typing.Dict[str, typing.Any]: 25 """ 26 Return a dict that can be used to represent this object. 27 If the dict is passed to from_dict(), an identical object should be reconstructed. 28 29 A general (but inefficient) implementation is provided by default. 30 """ 31 32 return vars(self).copy() 33 34 @classmethod 35 # Note that `typing.Self` is returned, but that is introduced in Python 3.12. 36 def from_dict(cls, data: typing.Dict[str, typing.Any]) -> typing.Any: 37 """ 38 Return an instance of this subclass created using the given dict. 39 If the dict came from to_dict(), the returned object should be identical to the original. 40 41 A general (but inefficient) implementation is provided by default. 42 """ 43 44 return cls(**data) 45 46 def __eq__(self, other: object) -> bool: 47 """ 48 Check for equality. 49 50 This check uses to_dict() and compares the results. 51 This may not be complete or efficient depending on the child class. 52 """ 53 54 # Note the hard type check (done so we can keep this method general). 55 if (type(self) != type(other)): # pylint: disable=unidiomatic-typecheck 56 return False 57 58 return bool(self.to_dict() == other.to_dict()) # type: ignore[attr-defined] 59 60 def __lt__(self, other: 'DictConverter') -> bool: 61 return dumps(self) < dumps(other) 62 63 def __hash__(self) -> int: 64 return hash(dumps(self)) 65 66 def __str__(self) -> str: 67 return dumps(self) 68 69 def __repr__(self) -> str: 70 return dumps(self) 71 72def _custom_handle(value: typing.Any) -> typing.Union[typing.Dict[str, typing.Any], str]: 73 """ 74 Handle objects that are not JSON serializable by default, 75 e.g., calling vars() on an object. 76 """ 77 78 if (isinstance(value, DictConverter)): 79 return value.to_dict() 80 81 if (isinstance(value, enum.Enum)): 82 return str(value) 83 84 if (hasattr(value, '__dict__')): 85 return dict(vars(value)) 86 87 raise ValueError(f"Could not JSON serialize object: '{value}'.") 88 89def load(file_obj: typing.TextIO, strict: bool = False, **kwargs: typing.Any) -> typing.Any: 90 """ 91 Load a file object/handler as JSON. 92 If strict is set, then use standard Python JSON, 93 otherwise use JSON5. 94 """ 95 96 if (strict): 97 return json.load(file_obj, **kwargs) 98 99 return json5.load(file_obj, **kwargs) 100 101def loads(text: str, strict: bool = False, **kwargs: typing.Any) -> typing.Any: 102 """ 103 Load a string as JSON. 104 If strict is set, then use standard Python JSON, 105 otherwise use JSON5. 106 """ 107 108 if (strict): 109 return json.loads(text, **kwargs) 110 111 return json5.loads(text, **kwargs) 112 113def load_path( 114 path: str, 115 strict: bool = False, 116 encoding: str = edq.util.dirent.DEFAULT_ENCODING, 117 **kwargs: typing.Any) -> typing.Any: 118 """ 119 Load a file path as JSON. 120 If strict is set, then use standard Python JSON, 121 otherwise use JSON5. 122 """ 123 124 if (os.path.isdir(path)): 125 raise IsADirectoryError(f"Cannot open JSON file, expected a file but got a directory at '{path}'.") 126 127 try: 128 with open(path, 'r', encoding = encoding) as file: 129 return load(file, strict = strict, **kwargs) 130 except Exception as ex: 131 raise ValueError(f"Failed to read JSON file '{path}'.") from ex 132 133def loads_object(text: str, cls: typing.Type[DictConverter], **kwargs: typing.Any) -> DictConverter: 134 """ Load a JSON string into an object (which is a subclass of DictConverter). """ 135 136 data = loads(text, **kwargs) 137 if (not isinstance(data, dict)): 138 raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.") 139 140 return cls.from_dict(data) # type: ignore[no-any-return] 141 142def load_object_path(path: str, cls: typing.Type[DictConverter], **kwargs: typing.Any) -> DictConverter: 143 """ Load a JSON file into an object (which is a subclass of DictConverter). """ 144 145 data = load_path(path, **kwargs) 146 if (not isinstance(data, dict)): 147 raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.") 148 149 return cls.from_dict(data) # type: ignore[no-any-return] 150 151def dump( 152 data: typing.Any, 153 file_obj: typing.TextIO, 154 default: typing.Union[typing.Callable, None] = _custom_handle, 155 sort_keys: bool = True, 156 **kwargs: typing.Any) -> None: 157 """ Dump an object as a JSON file object. """ 158 159 json.dump(data, file_obj, default = default, sort_keys = sort_keys, **kwargs) 160 161def dumps( 162 data: typing.Any, 163 default: typing.Union[typing.Callable, None] = _custom_handle, 164 sort_keys: bool = True, 165 **kwargs: typing.Any) -> str: 166 """ Dump an object as a JSON string. """ 167 168 return json.dumps(data, default = default, sort_keys = sort_keys, **kwargs) 169 170def dump_path( 171 data: typing.Any, 172 path: str, 173 default: typing.Union[typing.Callable, None] = _custom_handle, 174 sort_keys: bool = True, 175 encoding: str = edq.util.dirent.DEFAULT_ENCODING, 176 **kwargs: typing.Any) -> None: 177 """ Dump an object as a JSON file. """ 178 179 with open(path, 'w', encoding = encoding) as file: 180 dump(data, file, default = default, sort_keys = sort_keys, **kwargs)
17class DictConverter(): 18 """ 19 A base class for class that can represent (serialize) and reconstruct (deserialize) themselves as/from a dict. 20 The intention is that the dict can then be cleanly converted to/from JSON. 21 22 General (but inefficient) implementations of several core Python equality, comparison, and representation methods are provided. 23 """ 24 25 def to_dict(self) -> typing.Dict[str, typing.Any]: 26 """ 27 Return a dict that can be used to represent this object. 28 If the dict is passed to from_dict(), an identical object should be reconstructed. 29 30 A general (but inefficient) implementation is provided by default. 31 """ 32 33 return vars(self).copy() 34 35 @classmethod 36 # Note that `typing.Self` is returned, but that is introduced in Python 3.12. 37 def from_dict(cls, data: typing.Dict[str, typing.Any]) -> typing.Any: 38 """ 39 Return an instance of this subclass created using the given dict. 40 If the dict came from to_dict(), the returned object should be identical to the original. 41 42 A general (but inefficient) implementation is provided by default. 43 """ 44 45 return cls(**data) 46 47 def __eq__(self, other: object) -> bool: 48 """ 49 Check for equality. 50 51 This check uses to_dict() and compares the results. 52 This may not be complete or efficient depending on the child class. 53 """ 54 55 # Note the hard type check (done so we can keep this method general). 56 if (type(self) != type(other)): # pylint: disable=unidiomatic-typecheck 57 return False 58 59 return bool(self.to_dict() == other.to_dict()) # type: ignore[attr-defined] 60 61 def __lt__(self, other: 'DictConverter') -> bool: 62 return dumps(self) < dumps(other) 63 64 def __hash__(self) -> int: 65 return hash(dumps(self)) 66 67 def __str__(self) -> str: 68 return dumps(self) 69 70 def __repr__(self) -> str: 71 return dumps(self)
A base class for class that can represent (serialize) and reconstruct (deserialize) themselves as/from a dict. The intention is that the dict can then be cleanly converted to/from JSON.
General (but inefficient) implementations of several core Python equality, comparison, and representation methods are provided.
25 def to_dict(self) -> typing.Dict[str, typing.Any]: 26 """ 27 Return a dict that can be used to represent this object. 28 If the dict is passed to from_dict(), an identical object should be reconstructed. 29 30 A general (but inefficient) implementation is provided by default. 31 """ 32 33 return vars(self).copy()
Return a dict that can be used to represent this object. If the dict is passed to from_dict(), an identical object should be reconstructed.
A general (but inefficient) implementation is provided by default.
35 @classmethod 36 # Note that `typing.Self` is returned, but that is introduced in Python 3.12. 37 def from_dict(cls, data: typing.Dict[str, typing.Any]) -> typing.Any: 38 """ 39 Return an instance of this subclass created using the given dict. 40 If the dict came from to_dict(), the returned object should be identical to the original. 41 42 A general (but inefficient) implementation is provided by default. 43 """ 44 45 return cls(**data)
Return an instance of this subclass created using the given dict. If the dict came from to_dict(), the returned object should be identical to the original.
A general (but inefficient) implementation is provided by default.
90def load(file_obj: typing.TextIO, strict: bool = False, **kwargs: typing.Any) -> typing.Any: 91 """ 92 Load a file object/handler as JSON. 93 If strict is set, then use standard Python JSON, 94 otherwise use JSON5. 95 """ 96 97 if (strict): 98 return json.load(file_obj, **kwargs) 99 100 return json5.load(file_obj, **kwargs)
Load a file object/handler as JSON. If strict is set, then use standard Python JSON, otherwise use JSON5.
102def loads(text: str, strict: bool = False, **kwargs: typing.Any) -> typing.Any: 103 """ 104 Load a string as JSON. 105 If strict is set, then use standard Python JSON, 106 otherwise use JSON5. 107 """ 108 109 if (strict): 110 return json.loads(text, **kwargs) 111 112 return json5.loads(text, **kwargs)
Load a string as JSON. If strict is set, then use standard Python JSON, otherwise use JSON5.
114def load_path( 115 path: str, 116 strict: bool = False, 117 encoding: str = edq.util.dirent.DEFAULT_ENCODING, 118 **kwargs: typing.Any) -> typing.Any: 119 """ 120 Load a file path as JSON. 121 If strict is set, then use standard Python JSON, 122 otherwise use JSON5. 123 """ 124 125 if (os.path.isdir(path)): 126 raise IsADirectoryError(f"Cannot open JSON file, expected a file but got a directory at '{path}'.") 127 128 try: 129 with open(path, 'r', encoding = encoding) as file: 130 return load(file, strict = strict, **kwargs) 131 except Exception as ex: 132 raise ValueError(f"Failed to read JSON file '{path}'.") from ex
Load a file path as JSON. If strict is set, then use standard Python JSON, otherwise use JSON5.
134def loads_object(text: str, cls: typing.Type[DictConverter], **kwargs: typing.Any) -> DictConverter: 135 """ Load a JSON string into an object (which is a subclass of DictConverter). """ 136 137 data = loads(text, **kwargs) 138 if (not isinstance(data, dict)): 139 raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.") 140 141 return cls.from_dict(data) # type: ignore[no-any-return]
Load a JSON string into an object (which is a subclass of DictConverter).
143def load_object_path(path: str, cls: typing.Type[DictConverter], **kwargs: typing.Any) -> DictConverter: 144 """ Load a JSON file into an object (which is a subclass of DictConverter). """ 145 146 data = load_path(path, **kwargs) 147 if (not isinstance(data, dict)): 148 raise ValueError(f"JSON to load into an object is not a dict, found '{type(data)}'.") 149 150 return cls.from_dict(data) # type: ignore[no-any-return]
Load a JSON file into an object (which is a subclass of DictConverter).
152def dump( 153 data: typing.Any, 154 file_obj: typing.TextIO, 155 default: typing.Union[typing.Callable, None] = _custom_handle, 156 sort_keys: bool = True, 157 **kwargs: typing.Any) -> None: 158 """ Dump an object as a JSON file object. """ 159 160 json.dump(data, file_obj, default = default, sort_keys = sort_keys, **kwargs)
Dump an object as a JSON file object.
162def dumps( 163 data: typing.Any, 164 default: typing.Union[typing.Callable, None] = _custom_handle, 165 sort_keys: bool = True, 166 **kwargs: typing.Any) -> str: 167 """ Dump an object as a JSON string. """ 168 169 return json.dumps(data, default = default, sort_keys = sort_keys, **kwargs)
Dump an object as a JSON string.
171def dump_path( 172 data: typing.Any, 173 path: str, 174 default: typing.Union[typing.Callable, None] = _custom_handle, 175 sort_keys: bool = True, 176 encoding: str = edq.util.dirent.DEFAULT_ENCODING, 177 **kwargs: typing.Any) -> None: 178 """ Dump an object as a JSON file. """ 179 180 with open(path, 'w', encoding = encoding) as file: 181 dump(data, file, default = default, sort_keys = sort_keys, **kwargs)
Dump an object as a JSON file.