py.lib
35:ab4cf9f4f10a Browse Files
* Исправлена работа со сложными аннотациями в классах данных
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)