py.lib

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

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