py.lib

Yohn Y. 2022-08-19 Parent:f1a05e880961

37:ae0107755941 Go to Latest

py.lib/dataclass_utils.py

. Исправление ошибок и рефакторинг

History
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)