# Copyright (c) 2021 Christopher Prohm
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
import dataclasses
import typing
def fromdict(d, cls):
type_origin = typing.get_origin(cls)
type_args = typing.get_args(cls)
if dataclasses.is_dataclass(cls):
return fromdict_dataclass(d, cls)
elif type_origin is dict:
return fromdict_dict(d, type_args)
elif type_origin is typing.Union:
return fromdict_union(d, type_args)
elif type_origin is tuple:
return fromdict_tuple(d, type_args)
elif isinstance(cls, type) and hasattr(cls, "_fromdict"):
return cls._fromdict(d)
elif isinstance(cls, type):
return d
else:
raise NotImplementedError(f"Unsupported type {cls}")
def fromdict_dataclass(d, cls):
assert isinstance(d, dict)
data = {}
for field in dataclasses.fields(cls):
try:
data[field.name] = fromdict(d[field.name], field.type)
except Exception as cause:
raise TypeError(f"Could not convert {field.name}: {cause}") from cause
return cls(**data)
def fromdict_union(d, type_args):
none_type = type(None)
has_none = any(t is none_type for t in type_args)
not_none_types = [t for t in type_args if t is not none_type]
if d is None and has_none:
return None
if not not_none_types:
raise TypeError("No not None types in union with no None arg")
causes = []
for t in not_none_types:
try:
return fromdict(d, t)
except Exception as e:
causes.append((t, e))
else:
raise UnionError(*causes)
class UnionError(Exception):
def __str__(self):
res = ["Error during fromdict for Union type. No matching type found:"]
for t, err in self.args:
res.append(f"Conversion for {t} failed with {err}")
return "\n".join(res)
def fromdict_dict(d, type_args):
assert len(type_args) == 2
assert isinstance(d, dict)
key_type, val_type = type_args
return {fromdict(k, key_type): fromdict(v, val_type) for (k, v) in d.items()}
def fromdict_tuple(d, type_args):
assert len(type_args) == 1
assert isinstance(d, (tuple, list))
(item_type,) = type_args
return tuple(fromdict(item, item_type) for item in d)