def test_is_namedtuple_fake1(): """ Tries to make a fake namedtuple by adding `_field_types` to a `tuple`. Note: for some odd reason the following test fails in 3.7.1, so this test is only executed in Python 3.7.4+. """ C = namedtuple("C", ["name", "value"]) version_info = sys.version_info if version_info[0:3] >= (3, 7, 4): C._field_types = {"name": str, "value": int} assert is_namedtuple(C, failure_callback=failure_callback)
def test_is_namedtuple_fake6(): """ Tries to make a fake `typing.NamedTuple` by adding `_fields`, `_field_types` and `_field_defaults` to a `tuple` and adding the actual fields to the class. Unfortunately, the fields don't get added to `dir` for a `tuple`. """ class A(NamedTuple): # pylint:disable=all name: str = "a" value: int = 0 assert is_namedtuple(A, failure_callback=failure_callback) class FakeA(tuple): name: str value: int _fields = ("name", "value") _field_types = {"name": str, "value": int} _field_defaults = {"name": "a", "value": 0} assert not is_namedtuple(FakeA, failure_callback=failure_callback)
def test_is_namedtuple_basic(): """ Tests that `typing.NamedTuple` are identified correctly, while classes, `tuple` and `namedtuple` are not. """ class A(NamedTuple): # pylint:disable=all name: List[str] value: Union[int, float] assert is_namedtuple(A, failure_callback=failure_callback) a = A(["hi"], 1.1) assert is_instance(a, A, failure_callback=failure_callback) a = A(0, 1.1) # type:ignore assert not is_instance(a, A, failure_callback=failure_callback) a = A(["hi"], 1.1) class B: # pylint:disable=all name: List[str] def __init__(self, name: List[str]): self.name = name class Btuple(tuple): # pylint:disable=all name: List[str] def __init__(self, name: List[str]): self.name = name assert not is_namedtuple(0, failure_callback=failure_callback) assert not is_namedtuple(B, failure_callback=failure_callback) assert not is_namedtuple(Btuple, failure_callback=failure_callback) b = B(["hi"]) assert not is_instance(b, A, failure_callback=failure_callback) C = namedtuple("C", ["name", "value"]) assert not is_namedtuple(C, failure_callback=failure_callback)
def _from_json_obj_iterator(obj, element_t, cast_decimal=True): if is_namedtuple(element_t): fields = getattr(element_t, "_fields") field_types = getattr(element_t, "_field_types") field_defaults = getattr(element_t, "_field_defaults") return (_from_json_obj_namedtuple(el, element_t, fields, field_types, field_defaults, cast_decimal=cast_decimal) for el in obj) return (from_json_obj(el, element_t, cast_decimal=cast_decimal) for el in obj)
def to_json_obj(obj: Any, t: Any) -> Any: """ Converts an json encodable type to a json standard type. """ # pylint:disable=invalid-name,too-many-return-statements,too-many-branches if not is_json_encodable(t): raise TypeError("Type %s is not json-encodable." % str(t)) if not is_instance(obj, t): raise TypeError("Object %s is not of type %s" % (str(obj), str(t))) if t in JSON_BASE_TYPES: return obj if t in (None, type(None), ...): return None if is_namedtuple(t): field_types = getattr(t, "_field_types") json_dict = OrderedDict() # type:ignore for field in field_types: json_dict[field] = to_json_obj(getattr(obj, field), field_types[field]) return json_dict if hasattr(t, "__origin__") and hasattr(t, "__args__"): # generics if t.__origin__ is Union: for s in t.__args__: if is_instance(obj, s): return to_json_obj(obj, s) raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover if t.__origin__ is Literal: return obj if t.__origin__ in (list, set, frozenset, deque): return [to_json_obj(x, t.__args__[0]) for x in obj] if t.__origin__ is tuple: if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return return [to_json_obj(x, t.__args__[0]) for x in obj] else: return [ to_json_obj(x, t.__args__[i]) for i, x in enumerate(obj) ] if t.__origin__ in (dict, Mapping): return { field: to_json_obj(obj[field], t.__args__[1]) for field in obj } if t.__origin__ is OrderedDict: new_ordered_dict = OrderedDict() # type:ignore for field in obj: new_ordered_dict[field] = to_json_obj(obj[field], t.__args__[1]) return new_ordered_dict raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover
def from_json_obj(obj: Any, t: Type, cast_decimal: bool = True) -> Any: """ Decodes a JSON object `obj` into an instance of a typecheckable type `t`. This method raises `TypeError` if type `t` is not JSON encodable according to `typing_json.encoding.is_json_encodable`. This method also raises `TypeError` if `obj` is not a valid JSON encoding for an instance of type `t`. Currently, this method acts as follows on an JSON object `obj` and a JSON-encodable type `t`: - if `t` is one of the JSON basic types `bool`, `float`, `str`, `NoneType`, `obj` must be an instance of the type an is returned unchanged; - if `t` is of the JSON basic type `int` and the `cast_decimal` parameter is set to `False`, `obj` must be an instance of `int` and is returned unchanged; - it `t` is of the JSON basic type `int` and the `cast_decimal` parameter is set to `True` (its default value), `obj` can be either an instance of `int`, in which case it is returned unchanged, or an instance of `decimal.Decimal` encoding an integer, in which case `int(obj)` is returned; - if `t` is of the JSON basic type `float` and the `cast_decimal` parameter is set to `False`, `obj` must be an instance of `int` or `float`, and `float(obj)` is returned; - it `t` is of the JSON basic type `float` and the `cast_decimal` parameter is set to `True` (its default value), `obj` can be either an instance of `int`, `float` or `decimal.Decimal`, in which case `float(obj)` is returned; - if `t` is `None`, used as an alias for `NoneType`, `obj` must be `None` and is returned unchanged; - if `t` is `decimal.Decimal` and the `cast_decimal` parameter is set to `False`, `obj` must be either a `decimal.Decimal`, an `int` or a `str` encoding a valid decimal, in which case `decimal.Decimal(obj)` is returned; v- if `t` is `decimal.Decimal` and the `cast_decimal` parameter is set to `True`, `obj` must be either a `decimal.Decimal`, an `int`, a `float` or a `str` encoding a valid decimal, in which case `decimal.Decimal(obj)` is returned; - if `t` is an enumeration, `obj` must be a key in the dictionary `t.__members__` of names for the enumeration constants, in which case `t.__members__[obj]` is returned; - if `t` is a namedtuple (according to `typing_json.typechecking.is_namedtuple`), see below; - if `t` is a namedtuple (according to `typing_json.typechecking.is_typed_dict`), see below; - if `t` is `typing.Union` or `typing.Optional`, try to decoded `obj` using the generic type arguments one after the other, until a suitable one is found; - if `t` is `typing_extensions.Literal`, check that `obj` is one of the literals and return it unaltered; - if `t` is `typing.List`, check that `obj` is a list and return a list with recursively JSON-decoded elements of `obj` in it; - if `t` is `typing.Tuple`, check that `obj` is a list and return a tuple with recursively JSON-decoded elements of `obj` in it; - if `t` is `typing.Deque`, check that `obj` is a list and return a deque with recursively JSON-decoded elements of `obj` in it; - if `t` is `typing.Set`, check that `obj` is a list and return a set with recursively JSON-decoded elements of `obj` in it; - if `t` is `typing.FrozenSet`, check that `obj` is a list and return a frozenset with recursively JSON-decoded elements of `obj` in it; - if `t` is `typing.Dict` or `typing.Mapping`, check that `obj` is a dict and return a dict with recursively JSON-decoded keys and values from `obj` (first parsing the keys from strings in all those cases where `typing_json.encoding.to_json_obj` would have stringified them); - if `t` is `typing.OrderedDict`, check that `obj` is a `collections.OrderedDict` and return a `collections.OrderedDict` with recursively JSON-decoded keys and values from `obj` (first parsing the keys from strings in all those cases where `typing_json.encoding.to_json_obj` would have stringified them); If `t` is a namedtuple (according to `typing_json.typechecking.is_namedtuple`), `obj` must be a dictionary (not necessarily ordered, although namedtuple are JSON-encoded as such). The keys for the dictionary must form a subset of all field names for the namedtuple `t`, including at least all names of fields without default value. An instance of `t` is then constructed (and returned) by assigning to fields having names in the dictionary the JSON decoding of the corresponding values in the dictionary, and to all other fields the default values specified by `t`. As an exception to the above rule, decoding of namedtuples is allowed from lists of values, in the same order as the namedtuple fields they are to be assigned to. Missing values are allowed at the end and are filled with default field values. No excess values are allowed. This is to support the default `json` library behaviour on namedtuples, encoded as lists of field values. If `t` is a typed dict (according to `typing_json.typechecking.is_typed_dict`), `obj` must be a dictionary (not necessarily ordered). The keys for the dictionary must form a subset of all keys for the typed dict `t`; if `t` is total, then all keys must be presend. An instance of `t` is then constructed (and returned) by assigning to keys having names in the dictionary the JSON decoding of the corresponding values in the dictionary. (Version 0.1.3) """ # pylint: disable = too-many-branches, too-many-statements, too-many-return-statements trace: List[str] = [] def failure_callback(message: str) -> None: trace.append(message) if not is_json_encodable(t, failure_callback=failure_callback): # Argument `t` must be JSON encodable. raise TypeError("Type %s is not json-encodable. Trace:\n%s" % (str(t), "\n".join(trace))) if t in JSON_BASE_TYPES: # JSON basic types are returned unaltered, with the exception of casting `Decimal` to `int`/`float` if `cast_decimal` is `True`. if t == int and cast_decimal and isinstance( obj, Decimal) and obj == obj.to_integral_value(): return int(obj) if t == float and cast_decimal and isinstance(obj, Decimal): return float(obj) if t == float and isinstance( obj, int) and obj is not True and obj is not False: return float(obj) if not is_instance(obj, t, cast_decimal=cast_decimal): raise TypeError("Object %s is not of json basic type t=%s." % (short_str(obj), str(t))) return obj if t in (None, type(None)): # The only value of `NoneType` is `None`, which is returned unaltered. if obj is not None: raise TypeError("Object %s is not None (t=%s)." % (short_str(obj), str(t))) return None if t == Decimal: # Instances of `decimal.Decimal` are decoded from `int` or `string`, as well as from `float` if `cast_decimal` is `True` try: if isinstance( obj, (int, str, Decimal)) and obj is not True and obj is not False: return Decimal(obj) if cast_decimal and isinstance( obj, float) and obj is not True and obj is not False: return Decimal(obj) except InvalidOperation: ... raise TypeError("Object %s is not decimal.Decimal (t=%s)." % (short_str(obj), str(t))) if isinstance(t, EnumMeta): # For enumerations, use the `t.__members__` dictionary to convert the string name into an enumeration value. if not isinstance(obj, str): raise TypeError("Object %s is not a string (t=%s)." % (short_str(obj), str(t))) if obj not in t.__members__: # type: ignore raise TypeError( "Object %s is not the string of a value of the enum (t=%s)." % (short_str(obj), str(t))) return t.__members__[obj] # type: ignore # pylint:disable=protected-access if is_namedtuple(t): fields = getattr(t, "_fields") field_types = getattr(t, "_field_types") field_defaults = getattr(t, "_field_defaults") return _from_json_obj_namedtuple(obj, t, fields, field_types, field_defaults, cast_decimal=cast_decimal) if is_typed_dict(t): # Typed dicts are encoded as ordered dictionaries, with their fields as keys and the JSON-encoded field values as corresponding values. field_types = getattr(t, "__annotations__") total = getattr(t, "__total__") if not isinstance(obj, (dict, OrderedDict)): raise TypeError("Object %s is not dict or OrderedDict (t=%s)." % (short_str(obj), str(t))) converted_dict = dict() # type:ignore for field, field_type in field_types.items(): if total and field not in obj: raise TypeError( "Key %s missing from object %s (typed dict is total, t=%s)" % (field, short_str(obj), str(t))) if field in obj: converted_dict[field] = from_json_obj( obj[field], field_type, cast_decimal=cast_decimal) for field in obj: if field not in field_types: raise TypeError( "Extra field %s found when decoding object. (t=%s)." % (field, str(t))) return converted_dict if hasattr(t, "__origin__") and hasattr(t, "__args__"): # `typing` generics if t.__origin__ is Union: # For `typing.Union` (and `typing.Optional`), attempt to decode the value using the generic type arguments in sequence for s in t.__args__: try: return_val = from_json_obj(obj, s, cast_decimal=cast_decimal) # assert is_instance(return_val, t, cast_decimal=cast_decimal) return return_val except TypeError: continue raise TypeError( "Object %s is not convertible to any of the types in %s." % (short_str(obj), str(t))) if t.__origin__ is Literal: # for `typing_extensions.Literal`, check that the object is an instance of `t` and then return it unaltered trace = [] if not is_instance(obj, t, failure_callback=failure_callback, cast_decimal=cast_decimal): raise TypeError("Object %s is not allowed (t=%s). Trace:\n%s" % (short_str(obj), str(t), "\n".join(trace))) return obj if t.__origin__ is list: # for `typing.List`, expect a list and return a list with recursively JSON-decoded elements if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (short_str(obj), str(t))) return_val = list( _from_json_obj_iterator(obj, t.__args__[0], cast_decimal=cast_decimal)) # assert is_instance(return_val, t, cast_decimal=cast_decimal) return return_val if t.__origin__ is deque: # for `typing.Deque`, expect a list and return a deque with recursively JSON-decoded elements if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (short_str(obj), str(t))) return_val = deque( _from_json_obj_iterator(obj, t.__args__[0], cast_decimal=cast_decimal)) # assert is_instance(return_val, t, cast_decimal=cast_decimal) return return_val if t.__origin__ is set: # for `typing.Set`, expect a list and return a set with recursively JSON-decoded elements if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (short_str(obj), str(t))) return_val = set( _from_json_obj_iterator(obj, t.__args__[0], cast_decimal=cast_decimal)) # assert is_instance(return_val, t, cast_decimal=cast_decimal) return return_val if t.__origin__ is frozenset: # for `typing.FrozenSet`, expect a list and return a frozenset with recursively JSON-decoded elements if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (short_str(obj), str(t))) return_val = frozenset( _from_json_obj_iterator(obj, t.__args__[0], cast_decimal=cast_decimal)) # assert is_instance(return_val, t, cast_decimal=cast_decimal) return return_val if t.__origin__ is tuple: # for `typing.Tuple`, expect a list and return a tuple with recursively JSON-decoded elements if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (short_str(obj), str(t))) if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return return_val = tuple( _from_json_obj_iterator(obj, t.__args__[0], cast_decimal=cast_decimal)) # assert is_instance(return_val, t, cast_decimal=cast_decimal) return return_val else: if len(obj) != len(t.__args__): raise TypeError("List %s is of incorrect length (t=%s)." % (short_str(obj), str(t))) return_val = tuple( from_json_obj(x, t.__args__[i], cast_decimal=cast_decimal) for i, x in enumerate(obj)) # assert is_instance(return_val, t, cast_decimal=cast_decimal) return return_val if t.__origin__ in (dict, Mapping): # for `typing.Dict` and `typing.Mapping`, expect a dict and return a dict with recursively JSON-decoded values and keys (parsing keys from strings in all those cases where they would have been stringified) if not isinstance(obj, (dict, OrderedDict)): raise TypeError( "Object %s is not dict or OrderedDict (t=%s)." % (short_str(obj), str(t))) converted_dict = dict() # type:ignore for field in obj: if t.__args__[0] in JSON_BASE_TYPES: if not is_instance( field, t.__args__[0], cast_decimal=cast_decimal): raise TypeError( "Object key %s is not of json basic type %s (t=%s)." % (field, str(t.__args__[0]), str(t))) converted_field = field elif isinstance(t.__args__[0], EnumMeta) or hasattr( t.__args__[0], "__origin__") and t.__args__[0].__origin__ is Literal: converted_field = from_json_obj(field, t.__args__[0], cast_decimal=cast_decimal) else: converted_field = from_json_obj(json.loads(field), t.__args__[0], cast_decimal=cast_decimal) converted_dict[converted_field] = from_json_obj( obj[field], t.__args__[1], cast_decimal=cast_decimal) # assert is_instance(converted_dict, t, cast_decimal=cast_decimal) return converted_dict if t.__origin__ is OrderedDict: # for `typing.OrderedDict`, expect a `collections.OrderedDict` and return an ordered dict with recursively JSON-decoded values and keys (parsing keys from strings in all those cases where they would have been stringified) if not isinstance(obj, OrderedDict): raise TypeError("Object %s is not OrderedDict (t=%s)." % (short_str(obj), str(t))) converted_dict = OrderedDict() # type:ignore for field in obj: if t.__args__[0] in JSON_BASE_TYPES: if not isinstance(field, t.__args__[0]): raise TypeError( "Object key %s not of json basic type %s (t=%s)." % (field, str(t.__args__[0]), str(t))) converted_field = field elif isinstance(t.__args__[0], EnumMeta) or hasattr( t.__args__[0], "__origin__") and t.__args__[0].__origin__ is Literal: converted_field = from_json_obj(field, t.__args__[0], cast_decimal=cast_decimal) else: converted_field = from_json_obj(json.loads(field), t.__args__[0], cast_decimal=cast_decimal) converted_dict[converted_field] = from_json_obj( obj[field], t.__args__[1], cast_decimal=cast_decimal) # assert is_instance(converted_dict, t, cast_decimal=cast_decimal) return converted_dict raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover
def test_is_namedtuple(): class A(NamedTuple): # pylint:disable=all name: List[str] value: Union[int, float] assert is_namedtuple(A) a = A(["hi"], 1.1) assert is_instance(a, A) a = A(0, 1.1) # type:ignore assert not is_instance(a, A) class B: # pylint:disable=all name: List[str] def __init__(self, name: List[str]): self.name = name assert not is_namedtuple(B) b = B(["hi"]) assert not is_instance(b, A) C = namedtuple("C", ["name", "value"]) assert not is_namedtuple(C) # for some odd reason the following test fails in 3.7.1 version_info = sys.version_info if version_info[0:3] >= (3, 7, 4): C._field_types = {"name": str, "value": int} assert is_namedtuple(C) class D1(tuple): _fields = [] assert not is_namedtuple(D1) class D2(tuple): _fields = ("name", 0) assert not is_namedtuple(D2) class D3(tuple): _fields = ("name", "value") _field_types = [] assert not is_namedtuple(D3) class D4(tuple): _fields = ("name", "value") _field_types = {"name": str, "val": int} assert not is_namedtuple(D4) class D5(tuple): _fields = ("name", "value") _field_types = {"name": str, "value": int, "val": int} assert not is_namedtuple(D5) class D6(tuple): _fields = ("name", "value") _field_types = {"name": str, "value": B} assert not is_namedtuple(D6) class D7(tuple): _fields = ("name", "value") _field_types = {"name": str, "value": int} _field_defaults = [] assert not is_namedtuple(D7) class D8(tuple): _fields = ("name", "value") _field_types = {"name": str, "value": int} _field_defaults = {"val": 0} assert not is_namedtuple(D8) class D9(tuple): _fields = ("name", "value") _field_types = {"name": str, "value": int} _field_defaults = {"value": "bye"} assert not is_namedtuple(D9)
def is_json_encodable( t: Type, failure_callback: Optional[Callable[[str], None]] = None) -> bool: """ Checks whether a type `t` can be encoded into JSON (or decoded from JSON) using the `typing_json` library. The optional parameter `failure_callback` can be used to collect a detailed trace of the reasons behind this method returning `False` on a given type `t`. Currently, a type `t` is JSON encodable according to this method if it is typecheckable according to `typing_json.typechecking.is_typecheckable` and it satisfies one of the following conditions: - if `t` is one of the JSON basic types `bool`, `int`, `float`, `str`, `NoneType`; - if `t` is a `decimal.Decimal`; - if `t` is `None` (used as an alias for `NoneType`); - if `t` is an enum (i.e. `isinstance(t, EnumMeta)`); - if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable; - if `t` is a typed dictionary according to `typing_json.typechecking.is_typed_dict` and all its values are JSON encodable; - if `t` is one of `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque`, `typing.Optional` or a variadic `typing.Tuple` and its generic type argument is JSON encodable; - if `t` is a `typing.Union` or a fixed-length `typing.Tuple` and all of its generic type arguments are JSON encodable; - if `t` is a `typing.Dict`, `typing.OrderedDict` or `typing.Mapping`, its generic key type is keyable (according to `typing_json.typechecking.is_keyable`) and both its generic key and value types are JSON encodable; - if `t` is a `typing_extensions.Literal` and all of its literal arguments are of JSON basic type. (Version 0.1.3) """ # pylint: disable = too-many-return-statements, too-many-branches if not is_typecheckable(t, failure_callback=failure_callback): # only typecheckable types are encodable return _not_json_encodable("Type %s is not typecheckable." % str(t), failure_callback=failure_callback) if t in JSON_BASE_TYPES: # JSON basic types are encodable return True if t is Decimal: # `decimal.Decimal` is encodable return True if t is None: # `None` canbe used as an alias for class `NoneType` return True if isinstance(t, EnumMeta): # enums are encodable return True if is_namedtuple(t): field_types = getattr(t, "_field_types") if all( is_json_encodable(field_types[field], failure_callback=failure_callback) for field in field_types): # namedtuples are encodable if all their fields are of encodable types return True return _not_json_encodable( "Not all fields of namedtuple %s are json-encodable." % str(t), failure_callback=failure_callback) if is_typed_dict(t): field_types = getattr(t, "__annotations__") if all( is_json_encodable(field_types[field], failure_callback=failure_callback) for field in field_types): # typed dicts are encodable if all their fields are of encodable types return True return _not_json_encodable( "Not all fields of typed dict %s are json-encodable." % str(t), failure_callback=failure_callback) if hasattr(t, "__origin__") and hasattr(t, "__args__"): # `typing` generics if t.__origin__ in (list, set, frozenset, deque, Optional): if is_json_encodable(t.__args__[0], failure_callback=failure_callback): # `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque` and `typing.Optional` are encodable if their generic type argument is encodable return True return _not_json_encodable( "Type of elements in %s is not json-encodable." % str(t), failure_callback=failure_callback) if t.__origin__ is tuple: # `typing.Tuple` if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return if is_json_encodable(t.__args__[0], failure_callback=failure_callback): # variadic `typing.Tuple` are encodable if their generic type argument is encodable return True return _not_json_encodable( "Type of elements in %s is not json-encodable." % str(t), failure_callback=failure_callback) else: if all( is_json_encodable(s, failure_callback=failure_callback) for s in t.__args__): # fixed-length `typing.Tuple` are encodable if all their generic type arguments are encodable return True return _not_json_encodable( "Type of some element in %s is not json-encodable." % str(t), failure_callback=failure_callback) if t.__origin__ is Union: if all( is_json_encodable(s, failure_callback=failure_callback) for s in t.__args__): # `typing.Union` are encodable if all their generic type arguments are encodable return True return _not_json_encodable( "Some type in %s is not json-encodable." % str(t), failure_callback=failure_callback) if t.__origin__ in (dict, OrderedDict, Mapping): # `typing.Dict`, `typing.OrderedDict` and `typing.Mapping` are encodable if their generic key and value types are encodable and their key type is keyable if not is_keyable(t.__args__[0], failure_callback=failure_callback): return _not_json_encodable( "Type of keys in %s is not keyable." % str(t), failure_callback=failure_callback) if not is_json_encodable(t.__args__[0], failure_callback=failure_callback): return _not_json_encodable( "Type of keys in %s is not json-encodable." % str(t), failure_callback=failure_callback) if not is_json_encodable(t.__args__[1], failure_callback=failure_callback): return _not_json_encodable( "Type of values in %s is not json-encodable." % str(t), failure_callback=failure_callback) return True if t.__origin__ is Literal: # `typing_extensions.Literal` are encodable as long as their literals are JSON basic types, which is always the case if they are typecheckable. return True return False
def to_json_obj(obj: Any, t: Type, use_decimal: bool = False, typecheck: bool = True, namedtuples_as_lists=False) -> Any: """ Encodes an instance `obj` of typecheckable type `t` into a JSON object. The optional `use_decimal` parameter can be used to specify that instances of `decimal.Decimal` can be used in the output: if `False`, they are converted to strings. This method raises `TypeError` if type `t` is not typecheckable according to `typing_json.typechecking.is_typecheckable`. This method raises `TypeError` if `obj` is not of type `t` according to `typing_json.typechecking.is_instance`. Currently, this method acts as follows on an instance `obj` of type `t`: - if `t` is one of the JSON basic types `bool`, `int`, `float`, `str`, `NoneType`, the instance `obj` is returned unchanged; - if `t` is `decimal.Decimal` and `use_decimal` is `False` (default), `str(obj)` is returned; - if `t` is `decimal.Decimal` and `use_decimal` is `True`, `obj` is returned unchanged; - if `t` is `None` (used as an alias for `NoneType`), `None` is returned; - if `t` is an enum (i.e. `isinstance(t, EnumMeta)`), the enum value name `obj._name_` is returned; - if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable and `namedtuples_as_lists` is `False`, this method is called recursively on all field values and then an ordered dictionary is returned with the field names as names and the JSON-encoded field values as corresponding values; - if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable and `namedtuples_as_lists` is `True`, this method is called recursively on all field values and then a list is returned with the JSON-encoded field values appearing in the same order as the namedtuple fields (which are not explicitly encoded); - if `t` is a typed dict according to `typing_json.typechecking.is_typed_dict` and all its values are JSON encodable, then a dictionary is returned with the same keys as `obj` and JSON-encoded values using the types specified by `t`. - if `t` is `typing.Union`, the generic type arguments in the union are tried one after the other until a `u` is found such that `is_instance(obj, u)`, then `obj` is JSON-encoded using `u` as its type. - if `t` is a `typing_extensions.Literal`, `obj` is returned unchanged; - if `t` is one of `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque` or `typing.Tuple`, a list is returned containing the elements of the original collection, recursively JSON-encoded; - if `t` is a `typing.Dict` or `typing.Mapping`, a dictionary (`dict`) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below); - if `t` is `typing.OrderedDict`, an ordered dictionary (`collections.OrderedDict`) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below). In the case of dictionaries, it is not necessarily the case keys will be compatible with the JSON specification in their JSON-encoded form. When encoding dictionaries, the keys used in the encoding follow the following criteria: - if the key type is a JOSN basic type, `decimal.Decimal` or an enumeration type, the JSON encoding of the keys is used; - otherwise, the stringified version of the JSON encoding (using `json.dumps`) is used; Literals can only be of JSON basic type. An optional parameter `typecheck` (default: `True`) can be used to skip the check that `t` be JSON encodable and that `obj` be an instance of `t`. The parameter `typecheck` is set to `False` in all recursive calls (i.e. typechecking is only done once). (Version 0.1.3) """ # pylint:disable=invalid-name,too-many-return-statements,too-many-branches if typecheck: trace: List[str] = [] def failure_callback(message: str) -> None: trace.append(message) if not is_json_encodable(t, failure_callback=failure_callback): # Argument `t` must be JSON encodable. raise TypeError("Type %s is not json-encodable. Trace:\n%s" % (str(t), "\n".join(trace))) trace = [] if not is_instance(obj, t, failure_callback=failure_callback): # Argument `obj` must be an instance of argument `t`. raise TypeError("Object %s is not of type %s. Trace:\n%s" % (short_str(obj), str(t), "\n".join(trace))) if t in JSON_BASE_TYPES: # JSON basic types are returned unchanged. return obj if t is Decimal: # If `use_decimal` is `True`, `obj` is returned unchanged: if use_decimal: return obj # If `use_decimal` is `False` (default), instances of `decimal.Decimal` are encoded as strings. return str(obj) if t in (None, type(None)): # `None` can be used as an alias for `NoneType`. return None if isinstance(t, EnumMeta): # Enum values are encoded by their name. return obj._name_ # pylint:disable=protected-access if is_namedtuple(t): # Namedtuples are encoded as ordered dictionaries, with their fields as keys and the JSON-encoded field values as corresponding values. field_types = getattr(t, "_field_types") return _to_json_obj_namedtuple( obj, field_types, use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) if is_typed_dict(t): # Typed dicts are encoded as ordered dictionaries, with their fields as keys and the JSON-encoded field values as corresponding values. field_types = getattr(t, "__annotations__") # return _to_json_obj_namedtuple(obj, field_types, use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) # A `dict`is used for `typing.Dict` and `typing.Mapping`. return { field: to_json_obj(obj[field], field_type, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field, field_type in field_types.items() } if hasattr(t, "__origin__") and hasattr(t, "__args__"): # Generics from the `typing` module. if t.__origin__ is Union: # values in a `typing.Union` are JSON-encoded using the first type in the union that the object is found to be an instance of. for s in t.__args__: if is_instance(obj, s): return to_json_obj( obj, s, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover if t.__origin__ is Literal: # `typing_extensions.Literal` are returned unchanged return obj if t.__origin__ in (list, set, frozenset, deque): # `typing.List`, `typing.Set`, `typing.FrozenSet` and `typing.Deque` are turned into lists, with their elements recursively JSON-encoded return _to_json_obj_homogeneous_collection( obj, t.__args__[0], use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) if t.__origin__ is tuple: # `typing.Tuple` are turned into lists, with their elements recursively JSON-encoded if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return return _to_json_obj_homogeneous_collection( obj, t.__args__[0], use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) else: return [ to_json_obj(x, t.__args__[i], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for i, x in enumerate(obj) ] if t.__origin__ in (dict, OrderedDict, Mapping): # `typing.Dict` and `typing.Mapping` are turned into dictionaries and `typing.OrderedDict` are turned into ordered dictionaries. # The values are recursively JSON-encoded. Keys require special handling. fields = [field for field in obj ] # pylint: disable = unnecessary-comprehension if t.__args__[0] in JSON_BASE_TYPES + ( Decimal, None, ): # Keys of JSON basic types, `decimal.Decimal` and `None` are recursively JSON-encoded. # encoded_fields = [field for field in fields] # pylint: disable = unnecessary-comprehension encoded_fields = [ to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields ] elif (hasattr(t.__args__[0], "__origin__") and t.__args__[0].__origin__ is Literal): # Keys of `typing_extensions.Literal` types are recursively JSON-encoded. # encoded_fields = [field for field in fields] # pylint: disable = unnecessary-comprehension encoded_fields = [ to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields ] elif isinstance(t.__args__[0], EnumMeta): # Keys of enumeration types are recursively JSON-encoded. encoded_fields = [ to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields ] else: # Keys of any other type are recursively JSON-encoded and then JSON dumped to strings. encoded_fields = [ json.dumps( to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists)) for field in fields ] if t.__origin__ in (dict, Mapping): # A `dict`is used for `typing.Dict` and `typing.Mapping`. return { encoded_fields[i]: to_json_obj(obj[field], t.__args__[1], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for i, field in enumerate(fields) } if t.__origin__ is OrderedDict: # A `collections.OrderedDict` is used for `typing.OrderedDict`. new_ordered_dict = OrderedDict() # type:ignore for i, field in enumerate(fields): new_ordered_dict[encoded_fields[i]] = to_json_obj( obj[field], t.__args__[1], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) return new_ordered_dict raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover
def from_json_obj(obj: Any, t: Any) -> Any: """ Converts an object of json standard type to json encodable type. """ # pylint:disable=invalid-name,too-many-branches,too-many-statements,too-many-return-statements if not is_json_encodable(t): raise TypeError("Type %s is not json-encodable." % str(t)) if t in JSON_BASE_TYPES: if not isinstance(obj, t): raise TypeError("Object %s is not %s." % (str(obj), str(t))) return obj if t in (None, type(None)): if obj is not None: raise TypeError("Object %s is not null (t=%s)." % (str(obj), str(t))) return None if t is ...: if obj is not None: raise TypeError("Object %s is not null (t=%s)." % (str(obj), str(t))) return ... if is_namedtuple(t): if not isinstance(obj, (dict, OrderedDict, list)): raise TypeError( "Object %s is not (ordered) dictionary or list (t=%s)." % (str(obj), str(t))) # pylint:disable=line-too-long fields = getattr(t, "_fields") field_types = getattr(t, "_field_types") field_defaults = getattr(t, "_field_defaults") if isinstance(obj, list): if len(fields) != len(obj): raise TypeError( "Object %s does not provide the right number of values for a namedtuple." ) return_val = t(*tuple( from_json_obj( obj[i] if i < len(obj) else field_defaults[field], field_types[field]) for i, field in enumerate(fields))) # pylint:disable=line-too-long assert is_instance(return_val, t) return return_val converted_dict: OrderedDict() = {} # type:ignore if set(obj.keys()).union(set(field_defaults.keys())) != set( field_types.keys()): key_diff = set(obj.keys()).union(set(field_defaults.keys())) - set( field_types.keys()) if key_diff: raise TypeError( "Object %s does not have the required keys: t=%s, extra keys %s." % (str(obj), str(t), str(key_diff))) # pylint:disable=line-too-long key_diff = set(field_types.keys()) - set(obj.keys()).union( set(field_defaults.keys())) raise TypeError( "Object %s does not have the required keys: t=%s, missing keys %s." % (str(obj), str(t), str(key_diff))) # pylint:disable=line-too-long for field in fields: field_type = field_types[field] if not field in obj: converted_dict[field] = field_defaults[field] else: converted_dict[field] = from_json_obj(obj[field], field_type) return_val = t(**converted_dict) assert is_instance(return_val, t) return return_val if hasattr(t, "__origin__") and hasattr(t, "__args__"): # generics if t.__origin__ is Union: for s in t.__args__: try: return_val = from_json_obj(obj, s) assert is_instance(return_val, t) return return_val except TypeError: continue raise TypeError("Object %s is not convertible to any of %s." % (str(obj), str(t))) if t.__origin__ is Literal: if not is_instance(obj, t): raise TypeError("Object %s is not allowed (t=%s)." % (str(obj), str(t))) return obj if t.__origin__ is list: if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (str(obj), str(t))) return_val = list(from_json_obj(x, t.__args__[0]) for x in obj) assert is_instance(return_val, t) return return_val if t.__origin__ is deque: if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (str(obj), str(t))) return_val = deque(from_json_obj(x, t.__args__[0]) for x in obj) assert is_instance(return_val, t) return return_val if t.__origin__ is set: if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (str(obj), str(t))) return_val = set(from_json_obj(x, t.__args__[0]) for x in obj) assert is_instance(return_val, t) return return_val if t.__origin__ is frozenset: if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (str(obj), str(t))) return_val = frozenset( from_json_obj(x, t.__args__[0]) for x in obj) assert is_instance(return_val, t) return return_val if t.__origin__ is tuple: if not isinstance(obj, list): raise TypeError("Object %s is not list (t=%s)." % (str(obj), str(t))) if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return return_val = tuple( from_json_obj(x, t.__args__[0]) for x in obj) assert is_instance(return_val, t) return return_val else: if len(obj) != len(t.__args__): raise TypeError("List %s is of incorrect length (t=%s)." % (str(obj), str(t))) return_val = tuple( from_json_obj(x, t.__args__[i]) for i, x in enumerate(obj)) assert is_instance(return_val, t) return return_val if t.__origin__ in (dict, Mapping): if not isinstance(obj, (dict, OrderedDict)): raise TypeError( "Object %s is not dict or OrderedDict (t=%s)." % (str(obj), str(t))) converted_dict = dict() # type:ignore for field in obj: if not isinstance(field, str): raise TypeError("Object key %s is string (t=%s)." % (field, str(t))) converted_dict[field] = from_json_obj(obj[field], t.__args__[1]) assert is_instance(converted_dict, t) return converted_dict if t.__origin__ is OrderedDict: if not isinstance(obj, OrderedDict): raise TypeError( "Object %s is not dict or OrderedDict (t=%s)." % (str(obj), str(t))) converted_dict = OrderedDict() # type:ignore for field in obj: if not isinstance(field, str): raise TypeError("Object key %s is string (t=%s)." % (field, str(t))) converted_dict[field] = from_json_obj(obj[field], t.__args__[1]) assert is_instance(converted_dict, t) return converted_dict raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover