py.lib
2022-08-19
Parent:f1a05e880961
py.lib/dataclass_utils.py
. Исправление ошибок и рефакторинг
| awgur@31 | 1 # coding: utf-8 |
| awgur@31 | 2 |
| awgur@35 | 3 from dataclasses import fields, is_dataclass, asdict |
| awgur@35 | 4 from typing import Union, Dict, Any, Iterable |
| awgur@31 | 5 |
| awgur@31 | 6 |
| awgur@31 | 7 def _dict_has(obj: Dict[str, Any], key: str) -> bool: |
| awgur@31 | 8 return key in obj |
| awgur@31 | 9 |
| awgur@31 | 10 |
| awgur@31 | 11 def _dict_get(obj: Dict[str, Any], key: str) -> Any: |
| awgur@31 | 12 return obj[key] |
| awgur@31 | 13 |
| awgur@31 | 14 |
| awgur@31 | 15 def _obj_has(obj: object, key: str) -> bool: |
| awgur@31 | 16 return hasattr(obj, key) |
| awgur@31 | 17 |
| awgur@31 | 18 |
| awgur@31 | 19 def _obj_get(obj: object, key: str) -> Any: |
| awgur@31 | 20 return getattr(obj, key) |
| awgur@31 | 21 |
| awgur@31 | 22 |
| awgur@35 | 23 class TypeDescriber: |
| awgur@33 | 24 """\ |
| awgur@35 | 25 Реализует паттерн "адаптер" поверх типов, для упрощения приведения значений типов к объявленным формам |
| awgur@33 | 26 """ |
| awgur@35 | 27 def __init__(self, name, cast, like, is_complex=False): |
| awgur@35 | 28 self.__name__ = name |
| awgur@35 | 29 self.cast = cast |
| awgur@35 | 30 self.like = like |
| awgur@35 | 31 self.is_complex = is_complex |
| awgur@35 | 32 |
| awgur@37 | 33 def check(self, instance): |
| awgur@35 | 34 if self.like is None: |
| awgur@35 | 35 return False |
| awgur@33 | 36 |
| awgur@35 | 37 else: |
| awgur@37 | 38 return check_instance(instance, self.like) |
| awgur@33 | 39 |
| awgur@35 | 40 def __repr__(self): |
| awgur@35 | 41 return f'<TypeDescriber({self.__name__}, {self.like})>' |
| awgur@35 | 42 |
| awgur@35 | 43 def __call__(self, val): |
| awgur@35 | 44 if val is None: |
| awgur@35 | 45 return None |
| awgur@33 | 46 |
| awgur@37 | 47 elif not self.is_complex and check_instance(val, self.like): |
| awgur@37 | 48 return val |
| awgur@37 | 49 |
| awgur@33 | 50 else: |
| awgur@37 | 51 return self.cast(val) |
| awgur@37 | 52 |
| awgur@37 | 53 |
| awgur@37 | 54 def check_instance(obj, types): |
| awgur@37 | 55 if types is None: |
| awgur@37 | 56 return False |
| awgur@37 | 57 |
| awgur@37 | 58 elif isinstance(types, (tuple, list)): |
| awgur@37 | 59 _flag = False |
| awgur@37 | 60 for t in types: |
| awgur@37 | 61 if check_instance(obj, t): |
| awgur@37 | 62 _flag = True |
| awgur@37 | 63 break |
| awgur@37 | 64 |
| awgur@37 | 65 return _flag |
| awgur@37 | 66 |
| awgur@37 | 67 elif isinstance(types, TypeDescriber): |
| awgur@37 | 68 return types.check(obj) |
| awgur@37 | 69 |
| awgur@37 | 70 else: |
| awgur@37 | 71 return isinstance(obj, types) |
| awgur@35 | 72 |
| awgur@35 | 73 |
| awgur@35 | 74 def cast_iterator(t: Union[type, TypeDescriber], lst: Iterable): |
| awgur@35 | 75 """\ |
| awgur@35 | 76 Обрабатывает последовательности единого типа. |
| awgur@35 | 77 """ |
| awgur@35 | 78 for i in lst: |
| awgur@37 | 79 if check_instance(i, t): |
| awgur@35 | 80 yield i |
| awgur@35 | 81 |
| awgur@37 | 82 else: |
| awgur@37 | 83 try: |
| awgur@37 | 84 yield t(i) |
| awgur@35 | 85 |
| awgur@37 | 86 except (TypeError, ValueError) as e: |
| awgur@37 | 87 raise ValueError(f'Не удалось привести значение к нужному типу: ' |
| awgur@37 | 88 f'тип={t.__name__}; знач={i}') |
| awgur@35 | 89 |
| awgur@35 | 90 |
| awgur@35 | 91 def multi_item_tuple(tt, val): |
| awgur@35 | 92 """\ |
| awgur@35 | 93 Обрабатывает кортежи, состоящие из нескольких значений типов (кортежи элементов разных типов) |
| awgur@35 | 94 :param tt: Последовательность составляющих кортеж типов |
| awgur@35 | 95 :param val: итерируемый объект, хранящий значения в указанном порядке. |
| awgur@35 | 96 """ |
| awgur@35 | 97 val = list(val) |
| awgur@35 | 98 t_len = len(tt) |
| awgur@35 | 99 if t_len == 0: |
| awgur@35 | 100 raise ValueError('При вызове процедуры конвертации котежей, не были указаны типы') |
| awgur@35 | 101 |
| awgur@35 | 102 if len(val) != t_len: |
| awgur@35 | 103 raise ValueError(f'Значение не содержит положенных {t_len} элементов: {len(val)} - {val}') |
| awgur@35 | 104 |
| awgur@35 | 105 res = [] |
| awgur@35 | 106 |
| awgur@35 | 107 for i in range(t_len): |
| awgur@37 | 108 if check_instance(val[i], tt[i]): |
| awgur@37 | 109 res.append(val[i]) |
| awgur@35 | 110 |
| awgur@37 | 111 else: |
| awgur@37 | 112 try: |
| awgur@37 | 113 res.append(tt[i](val[i])) |
| awgur@35 | 114 |
| awgur@37 | 115 except (TypeError, ValueError) as e: |
| awgur@37 | 116 raise ValueError(f'Не удалось привести значение к нужному типу: ' |
| awgur@37 | 117 f'тип={tt[i].__name__}; знач={val[i]}') |
| awgur@35 | 118 |
| awgur@35 | 119 return tuple(res) |
| awgur@35 | 120 |
| awgur@33 | 121 |
| awgur@35 | 122 def union_processor(tt, val): |
| awgur@35 | 123 """\ |
| awgur@35 | 124 Пытается привести значение к одному из указанных типов |
| awgur@35 | 125 :param tt: список возможных типов |
| awgur@35 | 126 :param val: приводимое значение |
| awgur@35 | 127 """ |
| awgur@35 | 128 if val is None: |
| awgur@35 | 129 return val |
| awgur@35 | 130 |
| awgur@35 | 131 res = None |
| awgur@35 | 132 ex = [] |
| awgur@35 | 133 |
| awgur@35 | 134 if len(tt) == 0: |
| awgur@35 | 135 raise ValueError('Не указан ни один тип в составном типе Union') |
| awgur@35 | 136 |
| awgur@37 | 137 sorted_types_begin = [ t for t in tt if t in (int, float, bool)] |
| awgur@37 | 138 sorted_types_body = [ t for t in tt if t not in (int, float, bool, str)] |
| awgur@37 | 139 sorted_types_end = [ t for t in tt if t == str] |
| awgur@37 | 140 |
| awgur@37 | 141 for t in sorted_types_begin + sorted_types_body + sorted_types_end: |
| awgur@37 | 142 _t = get_type_describer(t) |
| awgur@35 | 143 try: |
| awgur@37 | 144 res = _t(val) |
| awgur@35 | 145 break |
| awgur@35 | 146 |
| awgur@35 | 147 except (TypeError, ValueError) as e: |
| awgur@35 | 148 ex.append(f'{t}: {e}') |
| awgur@35 | 149 |
| awgur@35 | 150 if res is None: |
| awgur@35 | 151 raise ValueError('Не удалось привести значение не к одному из типов:\n' + '\n'.join(ex)) |
| awgur@35 | 152 |
| awgur@35 | 153 else: |
| awgur@35 | 154 return res |
| awgur@35 | 155 |
| awgur@35 | 156 |
| awgur@35 | 157 def dict_processor(tt, val): |
| awgur@35 | 158 if len(tt) != 2: |
| awgur@35 | 159 raise ValueError(f'Попытка воссоздать словарь со странным количеством аргументов типа: {tt}') |
| awgur@35 | 160 |
| awgur@35 | 161 try: |
| awgur@35 | 162 _d = dict(val) |
| awgur@35 | 163 |
| awgur@35 | 164 except (TypeError, ValueError) as e: |
| awgur@35 | 165 raise ValueError(f'Не удалось воссоздать словарь из представленного значения: {e}') |
| awgur@35 | 166 |
| awgur@35 | 167 _p = [] |
| awgur@35 | 168 |
| awgur@37 | 169 tt_cast = [get_type_describer(t) for t in tt] |
| awgur@37 | 170 |
| awgur@35 | 171 for k, v in _d.items(): |
| awgur@35 | 172 try: |
| awgur@37 | 173 _p.append((tt_cast[0](k), tt_cast[1](v))) |
| awgur@35 | 174 |
| awgur@35 | 175 except (TypeError, ValueError) as e: |
| awgur@35 | 176 raise ValueError(f'Не удалось привести значения элемента словаря к требуемому типу: ' |
| awgur@35 | 177 f'key="{k}" value="{v}"') |
| awgur@33 | 178 |
| awgur@35 | 179 return dict(_p) |
| awgur@35 | 180 |
| awgur@35 | 181 |
| awgur@35 | 182 def get_type_describer(t) -> TypeDescriber: |
| awgur@35 | 183 if type(t).__name__ == '_GenericAlias': |
| awgur@35 | 184 try: |
| awgur@35 | 185 _args = t.__args__ |
| awgur@35 | 186 |
| awgur@35 | 187 except AttributeError: |
| awgur@35 | 188 raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' |
| awgur@35 | 189 f'типа "_GenericAlias": {t}') |
| awgur@35 | 190 |
| awgur@35 | 191 if t.__name__ == 'List': |
| awgur@35 | 192 try: |
| awgur@35 | 193 _t = _args[0] |
| awgur@35 | 194 |
| awgur@35 | 195 except IndexError: |
| awgur@35 | 196 raise ValueError(f'Тип {t} не содержит в себе типа своих элементов') |
| awgur@35 | 197 |
| awgur@35 | 198 return TypeDescriber( |
| awgur@35 | 199 name=f'{t.__name__}[{_t.__name__}]', |
| awgur@35 | 200 cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)), |
| awgur@35 | 201 like=list, |
| awgur@35 | 202 is_complex=True |
| awgur@35 | 203 ) |
| awgur@35 | 204 |
| awgur@35 | 205 elif t.__name__ == 'Tuple': |
| awgur@35 | 206 if not _args: |
| awgur@35 | 207 raise ValueError(f'Тип {t} не содержит в себе пояснений по составу хранящихся в нём типов') |
| awgur@35 | 208 |
| awgur@35 | 209 if len(_args) == 1: |
| awgur@35 | 210 _t = _args[0] |
| awgur@35 | 211 |
| awgur@35 | 212 return TypeDescriber( |
| awgur@35 | 213 name=f'Tuple[{_t.__name__}]', |
| awgur@35 | 214 cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)), |
| awgur@35 | 215 like=tuple, |
| awgur@35 | 216 is_complex=True |
| awgur@35 | 217 ) |
| awgur@35 | 218 |
| awgur@35 | 219 else: |
| awgur@35 | 220 _name = ', '.join(map(lambda x: x.__name__, _args)) |
| awgur@35 | 221 _cast_args = tuple(get_type_describer(i) for i in _args) |
| awgur@33 | 222 |
| awgur@35 | 223 return TypeDescriber( |
| awgur@35 | 224 name=f'Tuple[{_name}]', |
| awgur@35 | 225 cast=lambda x: multi_item_tuple(_cast_args, x), |
| awgur@35 | 226 like=tuple, |
| awgur@35 | 227 is_complex=True |
| awgur@35 | 228 ) |
| awgur@35 | 229 |
| awgur@35 | 230 elif t.__name__ == 'Dict': |
| awgur@35 | 231 if len(_args) != 2: |
| awgur@35 | 232 raise ValueError(f'Неожиданное количество значений типа в составном типе Dict: ' |
| awgur@35 | 233 f'{len(_args)} values=({_args})') |
| awgur@35 | 234 |
| awgur@35 | 235 _name = ', '.join(map(lambda x: x.__name__, _args)) |
| awgur@35 | 236 |
| awgur@35 | 237 return TypeDescriber( |
| awgur@35 | 238 name=f'Dict[{_name}]', |
| awgur@35 | 239 cast=lambda x: dict_processor(_args, x), |
| awgur@35 | 240 like=dict, |
| awgur@35 | 241 is_complex=True |
| awgur@35 | 242 ) |
| awgur@35 | 243 |
| awgur@35 | 244 else: |
| awgur@35 | 245 raise ValueError(f'Неизвестный представитель типа "_GenericAlias": {t.__name__}') |
| awgur@33 | 246 |
| awgur@35 | 247 elif type(t).__name__ == '_UnionGenericAlias': |
| awgur@35 | 248 if t.__name__ not in ('Union', 'Optional'): |
| awgur@35 | 249 raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {t.__name__}') |
| awgur@35 | 250 |
| awgur@35 | 251 try: |
| awgur@35 | 252 _args = t.__args__ |
| awgur@35 | 253 |
| awgur@35 | 254 except AttributeError: |
| awgur@35 | 255 raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' |
| awgur@35 | 256 f'типа "_UnionGenericAlias": {t}') |
| awgur@35 | 257 |
| awgur@35 | 258 if len(_args) == 0: |
| awgur@35 | 259 raise ValueError('Не указан ни один тип в конструкции Union') |
| awgur@33 | 260 |
| awgur@37 | 261 _cast_args = tuple(map(get_type_describer, [ i for i in _args if i is not None])) |
| awgur@35 | 262 _args_name = ', '.join(map(lambda x: x.__name__, _args)) |
| awgur@33 | 263 |
| awgur@35 | 264 return TypeDescriber( |
| awgur@35 | 265 name=f'Union[{_args_name}]', |
| awgur@35 | 266 cast=lambda x: union_processor(_cast_args, x), |
| awgur@35 | 267 like=None |
| awgur@35 | 268 ) |
| awgur@35 | 269 |
| awgur@35 | 270 else: |
| awgur@35 | 271 return TypeDescriber( |
| awgur@35 | 272 name=t.__name__, |
| awgur@35 | 273 cast=t, |
| awgur@35 | 274 like=t |
| awgur@35 | 275 ) |
| awgur@33 | 276 |
| awgur@33 | 277 |
| awgur@34 | 278 def dataobj_extract(obj: Union[object, Dict[str, Any]], dataclass_type: type) -> object: |
| awgur@31 | 279 """\ |
| awgur@31 | 280 Извлекает объект данных из предоставленного объекта, путём получения из него |
| awgur@31 | 281 указанных в классе данных аттрибутов и поиска их в данном объекте. |
| awgur@31 | 282 """ |
| awgur@31 | 283 |
| awgur@31 | 284 params = {} |
| awgur@31 | 285 |
| awgur@31 | 286 if isinstance(obj, dict): |
| awgur@31 | 287 _has = _dict_has |
| awgur@31 | 288 _get = _dict_get |
| awgur@31 | 289 |
| awgur@31 | 290 else: |
| awgur@31 | 291 _has = _obj_has |
| awgur@31 | 292 _get = _obj_get |
| awgur@31 | 293 |
| awgur@31 | 294 if not is_dataclass(dataclass_type): |
| awgur@31 | 295 raise ValueError(f'Не относится к классам данных: {dataclass_type.__name__}') |
| awgur@31 | 296 |
| awgur@31 | 297 for fld in fields(dataclass_type): |
| awgur@31 | 298 if _has(obj, fld.name): |
| awgur@31 | 299 val = _get(obj, fld.name) |
| awgur@36 | 300 type_desc = get_type_describer(fld.type) |
| awgur@37 | 301 if val is not None: |
| awgur@31 | 302 try: |
| awgur@36 | 303 val = type_desc(val) |
| awgur@31 | 304 |
| awgur@31 | 305 except (ValueError, TypeError) as e: |
| awgur@35 | 306 raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"' |
| awgur@35 | 307 f' с типом {type(val).__name__} поскольку не может быть преобразован в' |
| awgur@36 | 308 f' тип {type_desc}, заданный в классе данных: {e}') |
| awgur@31 | 309 |
| awgur@31 | 310 params[fld.name] = val |
| awgur@31 | 311 |
| awgur@31 | 312 try: |
| awgur@31 | 313 res = dataclass_type(**params) |
| awgur@31 | 314 |
| awgur@31 | 315 except (ValueError, TypeError) as e: |
| awgur@31 | 316 _params = ', '.join(map(lambda x: f'{x[0]}="{x[1]}"', params.items())) |
| awgur@31 | 317 raise ValueError(f'Не удалось получить объект' |
| awgur@31 | 318 f' класс {dataclass_type.__name__}' |
| awgur@31 | 319 f' из параметров: {_params}' |
| awgur@31 | 320 f' ошибка: {e}') |
| awgur@31 | 321 |
| awgur@31 | 322 return res |
| awgur@35 | 323 |
| awgur@35 | 324 |
| awgur@35 | 325 def json_type_sanitizer(val): |
| awgur@35 | 326 """\ |
| awgur@35 | 327 Преобразует значение ``val`` в пригодное для преобразования в json значение. |
| awgur@35 | 328 """ |
| awgur@35 | 329 |
| awgur@35 | 330 val_t = type(val) |
| awgur@35 | 331 |
| awgur@35 | 332 if is_dataclass(val): |
| awgur@35 | 333 return json_type_sanitizer(asdict(val)) |
| awgur@35 | 334 |
| awgur@35 | 335 elif val_t in (int, float, str, bool) or val is None: |
| awgur@35 | 336 return val |
| awgur@35 | 337 |
| awgur@35 | 338 elif val_t in (list, tuple): |
| awgur@35 | 339 return list(map(json_type_sanitizer, val)) |
| awgur@35 | 340 |
| awgur@35 | 341 elif val_t == dict: |
| awgur@35 | 342 return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items()) |
| awgur@35 | 343 |
| awgur@35 | 344 else: |
| awgur@35 | 345 return str(val) |