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