py.lib

Yohn Y. 2022-08-18 Parent:84b54a8a6d4c Child:f1a05e880961

35:ab4cf9f4f10a Browse Files

* Исправлена работа со сложными аннотациями в классах данных

dataclass_utils.py

     1.1 --- a/dataclass_utils.py	Sun Aug 14 12:19:08 2022 +0300
     1.2 +++ b/dataclass_utils.py	Thu Aug 18 23:57:48 2022 +0300
     1.3 @@ -1,7 +1,7 @@
     1.4  # coding: utf-8
     1.5  
     1.6 -from dataclasses import fields, dataclass, is_dataclass
     1.7 -from typing import Union, Dict, Any
     1.8 +from dataclasses import fields, is_dataclass, asdict
     1.9 +from typing import Union, Dict, Any, Iterable
    1.10  
    1.11  
    1.12  def _dict_has(obj: Dict[str, Any], key: str) -> bool:
    1.13 @@ -20,50 +20,225 @@
    1.14      return getattr(obj, key)
    1.15  
    1.16  
    1.17 -def difficult_type_recognizer(type_obj, value):
    1.18 +class TypeDescriber:
    1.19      """\
    1.20 -    Магия борющаяся с костылями type hinting
    1.21 +    Реализует паттерн "адаптер" поверх типов, для упрощения приведения значений типов к объявленным формам
    1.22      """
    1.23 -    if type(type_obj).__name__ == '_GenericAlias':
    1.24 -        if (
    1.25 -                hasattr(type_obj, '__args__')
    1.26 -                and type(type_obj.__args__) == tuple
    1.27 -                and type_obj.__args__
    1.28 -        ):
    1.29 -            if type_obj.__name__ == 'List':
    1.30 -                return list(map(type_obj.__args__[0], value))
    1.31 +    def __init__(self, name, cast, like, is_complex=False):
    1.32 +        self.__name__ = name
    1.33 +        self.cast = cast
    1.34 +        self.like = like
    1.35 +        self.is_complex = is_complex
    1.36 +
    1.37 +    def __instancecheck__(self, instance):
    1.38 +        if self.like is None:
    1.39 +            return False
    1.40  
    1.41 -            elif type_obj.__name__ == 'Tuple':
    1.42 -                return tuple(map(type_obj.__args__[0], value))
    1.43 +        else:
    1.44 +            return isinstance(instance, self.like)
    1.45  
    1.46 -            else:
    1.47 -                raise ValueError('Неизвестный тип')
    1.48 +    def __repr__(self):
    1.49 +        return f'<TypeDescriber({self.__name__}, {self.like})>'
    1.50 +
    1.51 +    def __call__(self, val):
    1.52 +        if val is None:
    1.53 +            return None
    1.54  
    1.55          else:
    1.56 -            ValueError('Неизвестный тип')
    1.57 +           return self.cast(val)
    1.58 +
    1.59 +
    1.60 +def cast_iterator(t: Union[type, TypeDescriber], lst: Iterable):
    1.61 +    """\
    1.62 +    Обрабатывает последовательности единого типа.
    1.63 +    """
    1.64 +    for i in lst:
    1.65 +        if isinstance(i, t):
    1.66 +            yield i
    1.67 +
    1.68 +        try:
    1.69 +            yield t(i)
    1.70 +
    1.71 +        except (TypeError, ValueError) as e:
    1.72 +            raise ValueError(f'Не удалось привести значение к нужному типу: тип={t.__name__}; знач={i}')
    1.73 +
    1.74 +
    1.75 +def multi_item_tuple(tt, val):
    1.76 +    """\
    1.77 +    Обрабатывает кортежи, состоящие из нескольких значений типов (кортежи элементов разных типов)
    1.78 +    :param tt: Последовательность составляющих кортеж типов
    1.79 +    :param val: итерируемый объект, хранящий значения в указанном порядке.
    1.80 +    """
    1.81 +    val = list(val)
    1.82 +    t_len = len(tt)
    1.83 +    if t_len == 0:
    1.84 +        raise ValueError('При вызове процедуры конвертации котежей, не были указаны типы')
    1.85 +
    1.86 +    if len(val) != t_len:
    1.87 +        raise ValueError(f'Значение не содержит положенных {t_len} элементов: {len(val)} - {val}')
    1.88 +
    1.89 +    res = []
    1.90 +
    1.91 +    for i in range(t_len):
    1.92 +        if isinstance(val[i], tt[i]):
    1.93 +            yield val[i]
    1.94 +
    1.95 +        try:
    1.96 +            res.append(tt[i](val[i]))
    1.97 +
    1.98 +        except (TypeError, ValueError) as e:
    1.99 +            raise ValueError(f'Не удалось привести значение к нужному типу: тип={tt[i].__name__}; знач={val[i]}')
   1.100 +
   1.101 +    return tuple(res)
   1.102 +
   1.103  
   1.104 -    elif type(type_obj).__name__ == '_UnionGenericAlias':
   1.105 -        if type_obj.__name__ not in ('Union', 'Optional'):
   1.106 -            raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {type_obj.__name__}')
   1.107 +def union_processor(tt, val):
   1.108 +    """\
   1.109 +    Пытается привести значение к одному из указанных типов
   1.110 +    :param tt: список возможных типов
   1.111 +    :param val: приводимое значение
   1.112 +    """
   1.113 +    if val is None:
   1.114 +        return val
   1.115 +
   1.116 +    res = None
   1.117 +    ex = []
   1.118 +
   1.119 +    if len(tt) == 0:
   1.120 +        raise ValueError('Не указан ни один тип в составном типе Union')
   1.121 +
   1.122 +    for t in tt:
   1.123 +        try:
   1.124 +            res = t(val)
   1.125 +            break
   1.126 +
   1.127 +        except (TypeError, ValueError) as e:
   1.128 +            ex.append(f'{t}: {e}')
   1.129 +
   1.130 +    if res is None:
   1.131 +        raise ValueError('Не удалось привести значение не к одному из типов:\n' + '\n'.join(ex))
   1.132 +
   1.133 +    else:
   1.134 +        return res
   1.135 +
   1.136 +
   1.137 +def dict_processor(tt, val):
   1.138 +    if len(tt) != 2:
   1.139 +        raise ValueError(f'Попытка воссоздать словарь со странным количеством аргументов типа: {tt}')
   1.140 +
   1.141 +    try:
   1.142 +        _d = dict(val)
   1.143 +
   1.144 +    except (TypeError, ValueError) as e:
   1.145 +        raise ValueError(f'Не удалось воссоздать словарь из представленного значения: {e}')
   1.146 +
   1.147 +    _p = []
   1.148 +
   1.149 +    for k, v in _d.items():
   1.150 +        try:
   1.151 +            _p.append((tt[0](k), tt[1](v)))
   1.152 +
   1.153 +        except (TypeError, ValueError) as e:
   1.154 +            raise ValueError(f'Не удалось привести значения элемента словаря к требуемому типу: '
   1.155 +                             f'key="{k}" value="{v}"')
   1.156  
   1.157 -        if not (
   1.158 -            hasattr(type_obj, '__args__')
   1.159 -            and type(type_obj.__args__) == tuple
   1.160 -            and type_obj.__args__
   1.161 -        ):
   1.162 -            raise TypeError(f'Не ясно как работать с типом не вижу аргументов: {type_obj.__name__}')
   1.163 +    return dict(_p)
   1.164 +
   1.165 +
   1.166 +def get_type_describer(t) -> TypeDescriber:
   1.167 +    if type(t).__name__ == '_GenericAlias':
   1.168 +        try:
   1.169 +            _args = t.__args__
   1.170 +
   1.171 +        except AttributeError:
   1.172 +            raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного '
   1.173 +                            f'типа "_GenericAlias": {t}')
   1.174 +
   1.175 +        if t.__name__ == 'List':
   1.176 +            try:
   1.177 +                _t = _args[0]
   1.178 +
   1.179 +            except IndexError:
   1.180 +                raise ValueError(f'Тип {t} не содержит в себе типа своих элементов')
   1.181 +
   1.182 +            return TypeDescriber(
   1.183 +                name=f'{t.__name__}[{_t.__name__}]',
   1.184 +                cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)),
   1.185 +                like=list,
   1.186 +                is_complex=True
   1.187 +            )
   1.188 +
   1.189 +        elif t.__name__ == 'Tuple':
   1.190 +            if not _args:
   1.191 +                raise ValueError(f'Тип {t} не содержит в себе пояснений по составу хранящихся в нём типов')
   1.192 +
   1.193 +            if len(_args) == 1:
   1.194 +                _t = _args[0]
   1.195 +
   1.196 +                return TypeDescriber(
   1.197 +                    name=f'Tuple[{_t.__name__}]',
   1.198 +                    cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)),
   1.199 +                    like=tuple,
   1.200 +                    is_complex=True
   1.201 +                )
   1.202 +
   1.203 +            else:
   1.204 +                _name = ', '.join(map(lambda x: x.__name__, _args))
   1.205 +                _cast_args = tuple(get_type_describer(i) for i in _args)
   1.206  
   1.207 -        for _t in type_obj.__args__:
   1.208 -            if _t.__name__ == 'NoneType':
   1.209 -                continue
   1.210 +                return TypeDescriber(
   1.211 +                    name=f'Tuple[{_name}]',
   1.212 +                    cast=lambda x: multi_item_tuple(_cast_args, x),
   1.213 +                    like=tuple,
   1.214 +                    is_complex=True
   1.215 +                )
   1.216 +
   1.217 +        elif t.__name__ == 'Dict':
   1.218 +            if len(_args) != 2:
   1.219 +                raise ValueError(f'Неожиданное количество значений типа в составном типе Dict: '
   1.220 +                                 f'{len(_args)} values=({_args})')
   1.221 +
   1.222 +            _name = ', '.join(map(lambda x: x.__name__, _args))
   1.223 +
   1.224 +            return TypeDescriber(
   1.225 +                name=f'Dict[{_name}]',
   1.226 +                cast=lambda x: dict_processor(_args, x),
   1.227 +                like=dict,
   1.228 +                is_complex=True
   1.229 +            )
   1.230 +
   1.231 +        else:
   1.232 +            raise ValueError(f'Неизвестный представитель типа "_GenericAlias": {t.__name__}')
   1.233  
   1.234 -            try:
   1.235 -                return _t(value)
   1.236 +    elif type(t).__name__ == '_UnionGenericAlias':
   1.237 +        if t.__name__ not in ('Union', 'Optional'):
   1.238 +            raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {t.__name__}')
   1.239 +
   1.240 +        try:
   1.241 +            _args = t.__args__
   1.242 +
   1.243 +        except AttributeError:
   1.244 +            raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного '
   1.245 +                            f'типа "_UnionGenericAlias": {t}')
   1.246 +
   1.247 +        if len(_args) == 0:
   1.248 +            raise ValueError('Не указан ни один тип в конструкции Union')
   1.249  
   1.250 -            except (TypeError, ValueError):
   1.251 -                continue
   1.252 +        _cast_args = tuple(map(get_type_describer, _args))
   1.253 +        _args_name = ', '.join(map(lambda x: x.__name__, _args))
   1.254  
   1.255 -        raise ValueError('Не удалось привести значение к типу')
   1.256 +        return TypeDescriber(
   1.257 +            name=f'Union[{_args_name}]',
   1.258 +            cast=lambda x: union_processor(_cast_args, x),
   1.259 +            like=None
   1.260 +        )
   1.261 +
   1.262 +    else:
   1.263 +        return TypeDescriber(
   1.264 +            name=t.__name__,
   1.265 +            cast=t,
   1.266 +            like=t
   1.267 +        )
   1.268  
   1.269  
   1.270  def dataobj_extract(obj: Union[object, Dict[str, Any]], dataclass_type: type) -> object:
   1.271 @@ -88,18 +263,15 @@
   1.272      for fld in fields(dataclass_type):
   1.273          if _has(obj, fld.name):
   1.274              val = _get(obj, fld.name)
   1.275 -            if val is not None and not isinstance(val, fld.type):
   1.276 +            typedesc = get_type_describer(fld.type)
   1.277 +            if val is not None and not isinstance(val, typedesc):
   1.278                  try:
   1.279 -                    val = fld.type(val)
   1.280 +                    val = typedesc(val)
   1.281  
   1.282                  except (ValueError, TypeError) as e:
   1.283 -                    try:
   1.284 -                        val = difficult_type_recognizer(fld.type, val)
   1.285 -
   1.286 -                    except (ValueError, TypeError):
   1.287 -                        raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"'
   1.288 -                                         f' с типом {type(val).__name__} поскольку не может быть преобразован в'
   1.289 -                                         f' тип {fld.type.__name__}, заданный в классе данных: {e}')
   1.290 +                    raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"'
   1.291 +                                     f' с типом {type(val).__name__} поскольку не может быть преобразован в'
   1.292 +                                     f' тип {typedesc}, заданный в классе данных: {e}')
   1.293  
   1.294              params[fld.name] = val
   1.295  
   1.296 @@ -114,3 +286,26 @@
   1.297                           f' ошибка: {e}')
   1.298  
   1.299      return res
   1.300 +
   1.301 +
   1.302 +def json_type_sanitizer(val):
   1.303 +    """\
   1.304 +    Преобразует значение ``val`` в пригодное для преобразования в json значение.
   1.305 +    """
   1.306 +
   1.307 +    val_t = type(val)
   1.308 +
   1.309 +    if is_dataclass(val):
   1.310 +        return json_type_sanitizer(asdict(val))
   1.311 +
   1.312 +    elif val_t in (int, float, str, bool) or val is None:
   1.313 +        return val
   1.314 +
   1.315 +    elif val_t in (list, tuple):
   1.316 +        return list(map(json_type_sanitizer, val))
   1.317 +
   1.318 +    elif val_t == dict:
   1.319 +        return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items())
   1.320 +
   1.321 +    else:
   1.322 +        return str(val)