py.lib
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@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) |