py.lib

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

35:ab4cf9f4f10a Go to Latest

py.lib/dataclass_utils.py

* Исправлена работа со сложными аннотациями в классах данных

History
1 # coding: utf-8
3 from dataclasses import fields, is_dataclass, asdict
4 from typing import Union, Dict, Any, Iterable
7 def _dict_has(obj: Dict[str, Any], key: str) -> bool:
8 return key in obj
11 def _dict_get(obj: Dict[str, Any], key: str) -> Any:
12 return obj[key]
15 def _obj_has(obj: object, key: str) -> bool:
16 return hasattr(obj, key)
19 def _obj_get(obj: object, key: str) -> Any:
20 return getattr(obj, key)
23 class TypeDescriber:
24 """\
25 Реализует паттерн "адаптер" поверх типов, для упрощения приведения значений типов к объявленным формам
26 """
27 def __init__(self, name, cast, like, is_complex=False):
28 self.__name__ = name
29 self.cast = cast
30 self.like = like
31 self.is_complex = is_complex
33 def __instancecheck__(self, instance):
34 if self.like is None:
35 return False
37 else:
38 return isinstance(instance, self.like)
40 def __repr__(self):
41 return f'<TypeDescriber({self.__name__}, {self.like})>'
43 def __call__(self, val):
44 if val is None:
45 return None
47 else:
48 return self.cast(val)
51 def cast_iterator(t: Union[type, TypeDescriber], lst: Iterable):
52 """\
53 Обрабатывает последовательности единого типа.
54 """
55 for i in lst:
56 if isinstance(i, t):
57 yield i
59 try:
60 yield t(i)
62 except (TypeError, ValueError) as e:
63 raise ValueError(f'Не удалось привести значение к нужному типу: тип={t.__name__}; знач={i}')
66 def multi_item_tuple(tt, val):
67 """\
68 Обрабатывает кортежи, состоящие из нескольких значений типов (кортежи элементов разных типов)
69 :param tt: Последовательность составляющих кортеж типов
70 :param val: итерируемый объект, хранящий значения в указанном порядке.
71 """
72 val = list(val)
73 t_len = len(tt)
74 if t_len == 0:
75 raise ValueError('При вызове процедуры конвертации котежей, не были указаны типы')
77 if len(val) != t_len:
78 raise ValueError(f'Значение не содержит положенных {t_len} элементов: {len(val)} - {val}')
80 res = []
82 for i in range(t_len):
83 if isinstance(val[i], tt[i]):
84 yield val[i]
86 try:
87 res.append(tt[i](val[i]))
89 except (TypeError, ValueError) as e:
90 raise ValueError(f'Не удалось привести значение к нужному типу: тип={tt[i].__name__}; знач={val[i]}')
92 return tuple(res)
95 def union_processor(tt, val):
96 """\
97 Пытается привести значение к одному из указанных типов
98 :param tt: список возможных типов
99 :param val: приводимое значение
100 """
101 if val is None:
102 return val
104 res = None
105 ex = []
107 if len(tt) == 0:
108 raise ValueError('Не указан ни один тип в составном типе Union')
110 for t in tt:
111 try:
112 res = t(val)
113 break
115 except (TypeError, ValueError) as e:
116 ex.append(f'{t}: {e}')
118 if res is None:
119 raise ValueError('Не удалось привести значение не к одному из типов:\n' + '\n'.join(ex))
121 else:
122 return res
125 def dict_processor(tt, val):
126 if len(tt) != 2:
127 raise ValueError(f'Попытка воссоздать словарь со странным количеством аргументов типа: {tt}')
129 try:
130 _d = dict(val)
132 except (TypeError, ValueError) as e:
133 raise ValueError(f'Не удалось воссоздать словарь из представленного значения: {e}')
135 _p = []
137 for k, v in _d.items():
138 try:
139 _p.append((tt[0](k), tt[1](v)))
141 except (TypeError, ValueError) as e:
142 raise ValueError(f'Не удалось привести значения элемента словаря к требуемому типу: '
143 f'key="{k}" value="{v}"')
145 return dict(_p)
148 def get_type_describer(t) -> TypeDescriber:
149 if type(t).__name__ == '_GenericAlias':
150 try:
151 _args = t.__args__
153 except AttributeError:
154 raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного '
155 f'типа "_GenericAlias": {t}')
157 if t.__name__ == 'List':
158 try:
159 _t = _args[0]
161 except IndexError:
162 raise ValueError(f'Тип {t} не содержит в себе типа своих элементов')
164 return TypeDescriber(
165 name=f'{t.__name__}[{_t.__name__}]',
166 cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)),
167 like=list,
168 is_complex=True
171 elif t.__name__ == 'Tuple':
172 if not _args:
173 raise ValueError(f'Тип {t} не содержит в себе пояснений по составу хранящихся в нём типов')
175 if len(_args) == 1:
176 _t = _args[0]
178 return TypeDescriber(
179 name=f'Tuple[{_t.__name__}]',
180 cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)),
181 like=tuple,
182 is_complex=True
185 else:
186 _name = ', '.join(map(lambda x: x.__name__, _args))
187 _cast_args = tuple(get_type_describer(i) for i in _args)
189 return TypeDescriber(
190 name=f'Tuple[{_name}]',
191 cast=lambda x: multi_item_tuple(_cast_args, x),
192 like=tuple,
193 is_complex=True
196 elif t.__name__ == 'Dict':
197 if len(_args) != 2:
198 raise ValueError(f'Неожиданное количество значений типа в составном типе Dict: '
199 f'{len(_args)} values=({_args})')
201 _name = ', '.join(map(lambda x: x.__name__, _args))
203 return TypeDescriber(
204 name=f'Dict[{_name}]',
205 cast=lambda x: dict_processor(_args, x),
206 like=dict,
207 is_complex=True
210 else:
211 raise ValueError(f'Неизвестный представитель типа "_GenericAlias": {t.__name__}')
213 elif type(t).__name__ == '_UnionGenericAlias':
214 if t.__name__ not in ('Union', 'Optional'):
215 raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {t.__name__}')
217 try:
218 _args = t.__args__
220 except AttributeError:
221 raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного '
222 f'типа "_UnionGenericAlias": {t}')
224 if len(_args) == 0:
225 raise ValueError('Не указан ни один тип в конструкции Union')
227 _cast_args = tuple(map(get_type_describer, _args))
228 _args_name = ', '.join(map(lambda x: x.__name__, _args))
230 return TypeDescriber(
231 name=f'Union[{_args_name}]',
232 cast=lambda x: union_processor(_cast_args, x),
233 like=None
236 else:
237 return TypeDescriber(
238 name=t.__name__,
239 cast=t,
240 like=t
244 def dataobj_extract(obj: Union[object, Dict[str, Any]], dataclass_type: type) -> object:
245 """\
246 Извлекает объект данных из предоставленного объекта, путём получения из него
247 указанных в классе данных аттрибутов и поиска их в данном объекте.
248 """
250 params = {}
252 if isinstance(obj, dict):
253 _has = _dict_has
254 _get = _dict_get
256 else:
257 _has = _obj_has
258 _get = _obj_get
260 if not is_dataclass(dataclass_type):
261 raise ValueError(f'Не относится к классам данных: {dataclass_type.__name__}')
263 for fld in fields(dataclass_type):
264 if _has(obj, fld.name):
265 val = _get(obj, fld.name)
266 typedesc = get_type_describer(fld.type)
267 if val is not None and not isinstance(val, typedesc):
268 try:
269 val = typedesc(val)
271 except (ValueError, TypeError) as e:
272 raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"'
273 f' с типом {type(val).__name__} поскольку не может быть преобразован в'
274 f' тип {typedesc}, заданный в классе данных: {e}')
276 params[fld.name] = val
278 try:
279 res = dataclass_type(**params)
281 except (ValueError, TypeError) as e:
282 _params = ', '.join(map(lambda x: f'{x[0]}="{x[1]}"', params.items()))
283 raise ValueError(f'Не удалось получить объект'
284 f' класс {dataclass_type.__name__}'
285 f' из параметров: {_params}'
286 f' ошибка: {e}')
288 return res
291 def json_type_sanitizer(val):
292 """\
293 Преобразует значение ``val`` в пригодное для преобразования в json значение.
294 """
296 val_t = type(val)
298 if is_dataclass(val):
299 return json_type_sanitizer(asdict(val))
301 elif val_t in (int, float, str, bool) or val is None:
302 return val
304 elif val_t in (list, tuple):
305 return list(map(json_type_sanitizer, val))
307 elif val_t == dict:
308 return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items())
310 else:
311 return str(val)