Created
October 8, 2019 14:51
-
-
Save Sidneys1/a19c45af2c5154724f5c3d15eb3966b5 to your computer and use it in GitHub Desktop.
Python JsonObject
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Helper for json-based objects""" | |
from inspect import isclass | |
from typing import get_type_hints, Union, TypeVar, Type | |
# cSpell:ignore isclass | |
def patch_target(target, patch): | |
if not isinstance(patch, dict): | |
return target | |
if not isinstance(target, dict): | |
target = {} | |
for key, value in patch.items(): | |
if value is not None: | |
target[key] = patch_target(target.get(key), value) | |
elif key in target: | |
del target[key] | |
return target | |
TJsonObject = TypeVar('TJsonObject', bound='JsonObject') | |
class JsonObject: | |
'''Helper abstract class to convert dicts to classes and back''' | |
__annotations__ = {} | |
__READONLY__ = [] | |
''' | |
A list of attributes that are treated as read-only by `patch()` | |
''' | |
def __init__(self, **kwargs): | |
'''Set values from kwargs''' | |
type_map = get_type_hints(self) | |
for key in kwargs: | |
if key[0] == "_" or not hasattr(self, key) or callable(getattr(self, key)): | |
raise KeyError(key) | |
value = JsonObject._coerce(type_map.get(key, None), kwargs[key]) | |
setattr(self, key, value) | |
def to_json(self) -> dict: | |
'''Creates a dict from this instance''' | |
ret = {} | |
for key in [x for x in dir(self) if x[0] != '_' and not callable(getattr(self, x))]: | |
value = getattr(self, key) | |
# if value is not None: | |
if isinstance(value, JsonObject): | |
value = value.to_json() | |
elif isinstance(value, list): | |
value = [x.to_json() if isinstance(x, JsonObject) else x for x in value] | |
elif isinstance(value, dict): | |
value = JsonObject._walk_dict(value) | |
ret[key] = value | |
return ret | |
def patch(self, patch: dict): | |
for key in patch: | |
if not hasattr(self, key): | |
raise KeyError('key "{}" does not exist'.format(key)) | |
if key in self.__READONLY__: | |
raise KeyError('key "{}" is read-only'.format(key)) | |
attr = getattr(self, key) | |
if isinstance(attr, JsonObject): | |
setattr(self, key, attr.patch(patch[key])) | |
elif isinstance(attr, dict): | |
setattr(self, key, patch_target(attr, patch[key])) | |
else: | |
setattr(self, key, patch[key]) | |
return self | |
@staticmethod | |
def _walk_dict(value: dict) -> dict: | |
keys = list(value.keys()) | |
for key in keys: | |
new_key = key | |
new_value = value.pop(key) | |
if isinstance(key, JsonObject): | |
new_key = key.to_json() | |
if isinstance(new_value, JsonObject): | |
new_value = new_value.to_json() | |
elif isinstance(new_value, list): | |
new_value = [x.to_json() if isinstance(x, JsonObject) else x for x in new_value] | |
elif isinstance(new_value, dict): | |
new_value = JsonObject._walk_dict(new_value) | |
value[new_key] = new_value | |
return value | |
@staticmethod | |
def _coerce(arg_type, value): | |
if arg_type is None or value is None: | |
return value | |
if isclass(arg_type) and issubclass(arg_type, JsonObject): | |
return arg_type.from_json(value) | |
if getattr(arg_type, '__origin__', None) is Union: | |
specials = getattr(arg_type, '__args__', None) | |
if specials is not None and specials[1] == type(None): | |
# This is an Optional[], which is shorthand for Union[T, NoneType] | |
return JsonObject._coerce(specials[0], value) | |
raise NotImplementedError("JsonObject doesn't resolve unions yet") | |
typing_name = getattr(arg_type, '_name', None) | |
if typing_name == 'List': | |
special = getattr(arg_type, '__args__', [None])[0] | |
if special is not None: | |
return [JsonObject._coerce(special, x) for x in value] | |
elif typing_name == 'Dict': | |
special_k, special_v = getattr(arg_type, '__args__', (None, None)) | |
value = { | |
JsonObject._coerce(special_k, k): JsonObject._coerce(special_v, v) | |
for k, v in value.items() | |
} | |
return value | |
@classmethod | |
def from_json(cls: Type[TJsonObject], json_dict: dict) -> TJsonObject: | |
''' | |
Create an instance of `cls` using values in `json_dict`. | |
Positional parameters become required, while everything else is passed as kwargs | |
''' | |
defaults_count = len(cls.__init__.__defaults__) if cls.__init__.__defaults__ else 0 | |
type_map = get_type_hints(cls.__init__) | |
args = [ | |
json_dict.pop(x) for x | |
in cls.__init__.__code__.co_varnames[1:cls.__init__.__code__.co_argcount - defaults_count] | |
] | |
for position in range(1, cls.__init__.__code__.co_argcount - defaults_count): | |
args[position - 1] = JsonObject._coerce(type_map.get(cls.__init__.__code__.co_varnames[position], None), | |
args[position - 1]) | |
return cls(*args, **json_dict) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment