py.lib

Yohn Y. 2022-08-19 Parent:f1a05e880961

37:ae0107755941 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 check(self, instance):
34 if self.like is None:
35 return False
37 else:
38 return check_instance(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 elif not self.is_complex and check_instance(val, self.like):
48 return val
50 else:
51 return self.cast(val)
54 def check_instance(obj, types):
55 if types is None:
56 return False
58 elif isinstance(types, (tuple, list)):
59 _flag = False
60 for t in types:
61 if check_instance(obj, t):
62 _flag = True
63 break
65 return _flag
67 elif isinstance(types, TypeDescriber):
68 return types.check(obj)
70 else:
71 return isinstance(obj, types)
74 def cast_iterator(t: Union[type, TypeDescriber], lst: Iterable):
75 """\
76 Обрабатывает последовательности единого типа.
77 """
78 for i in lst:
79 if check_instance(i, t):
80 yield i
82 else:
83 try:
84 yield t(i)
86 except (TypeError, ValueError) as e:
87 raise ValueError(f'Не удалось привести значение к нужному типу: '
88 f'тип={t.__name__}; знач={i}')
91 def multi_item_tuple(tt, val):
92 """\
93 Обрабатывает кортежи, состоящие из нескольких значений типов (кортежи элементов разных типов)
94 :param tt: Последовательность составляющих кортеж типов
95 :param val: итерируемый объект, хранящий значения в указанном порядке.
96 """
97 val = list(val)
98 t_len = len(tt)
99 if t_len == 0:
100 raise ValueError('При вызове процедуры конвертации котежей, не были указаны типы')
102 if len(val) != t_len:
103 raise ValueError(f'Значение не содержит положенных {t_len} элементов: {len(val)} - {val}')
105 res = []
107 for i in range(t_len):
108 if check_instance(val[i], tt[i]):
109 res.append(val[i])
111 else:
112 try:
113 res.append(tt[i](val[i]))
115 except (TypeError, ValueError) as e:
116 raise ValueError(f'Не удалось привести значение к нужному типу: '
117 f'тип={tt[i].__name__}; знач={val[i]}')
119 return tuple(res)
122 def union_processor(tt, val):
123 """\
124 Пытается привести значение к одному из указанных типов
125 :param tt: список возможных типов
126 :param val: приводимое значение
127 """
128 if val is None:
129 return val
131 res = None
132 ex = []
134 if len(tt) == 0:
135 raise ValueError('Не указан ни один тип в составном типе Union')
137 sorted_types_begin = [ t for t in tt if t in (int, float, bool)]
138 sorted_types_body = [ t for t in tt if t not in (int, float, bool, str)]
139 sorted_types_end = [ t for t in tt if t == str]
141 for t in sorted_types_begin + sorted_types_body + sorted_types_end:
142 _t = get_type_describer(t)
143 try:
144 res = _t(val)
145 break
147 except (TypeError, ValueError) as e:
148 ex.append(f'{t}: {e}')
150 if res is None:
151 raise ValueError('Не удалось привести значение не к одному из типов:\n' + '\n'.join(ex))
153 else:
154 return res
157 def dict_processor(tt, val):
158 if len(tt) != 2:
159 raise ValueError(f'Попытка воссоздать словарь со странным количеством аргументов типа: {tt}')
161 try:
162 _d = dict(val)
164 except (TypeError, ValueError) as e:
165 raise ValueError(f'Не удалось воссоздать словарь из представленного значения: {e}')
167 _p = []
169 tt_cast = [get_type_describer(t) for t in tt]
171 for k, v in _d.items():
172 try:
173 _p.append((tt_cast[0](k), tt_cast[1](v)))
175 except (TypeError, ValueError) as e:
176 raise ValueError(f'Не удалось привести значения элемента словаря к требуемому типу: '
177 f'key="{k}" value="{v}"')
179 return dict(_p)
182 def get_type_describer(t) -> TypeDescriber:
183 if type(t).__name__ == '_GenericAlias':
184 try:
185 _args = t.__args__
187 except AttributeError:
188 raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного '
189 f'типа "_GenericAlias": {t}')
191 if t.__name__ == 'List':
192 try:
193 _t = _args[0]
195 except IndexError:
196 raise ValueError(f'Тип {t} не содержит в себе типа своих элементов')
198 return TypeDescriber(
199 name=f'{t.__name__}[{_t.__name__}]',
200 cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)),
201 like=list,
202 is_complex=True
205 elif t.__name__ == 'Tuple':
206 if not _args:
207 raise ValueError(f'Тип {t} не содержит в себе пояснений по составу хранящихся в нём типов')
209 if len(_args) == 1:
210 _t = _args[0]
212 return TypeDescriber(
213 name=f'Tuple[{_t.__name__}]',
214 cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)),
215 like=tuple,
216 is_complex=True
219 else:
220 _name = ', '.join(map(lambda x: x.__name__, _args))
221 _cast_args = tuple(get_type_describer(i) for i in _args)
223 return TypeDescriber(
224 name=f'Tuple[{_name}]',
225 cast=lambda x: multi_item_tuple(_cast_args, x),
226 like=tuple,
227 is_complex=True
230 elif t.__name__ == 'Dict':
231 if len(_args) != 2:
232 raise ValueError(f'Неожиданное количество значений типа в составном типе Dict: '
233 f'{len(_args)} values=({_args})')
235 _name = ', '.join(map(lambda x: x.__name__, _args))
237 return TypeDescriber(
238 name=f'Dict[{_name}]',
239 cast=lambda x: dict_processor(_args, x),
240 like=dict,
241 is_complex=True
244 else:
245 raise ValueError(f'Неизвестный представитель типа "_GenericAlias": {t.__name__}')
247 elif type(t).__name__ == '_UnionGenericAlias':
248 if t.__name__ not in ('Union', 'Optional'):
249 raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {t.__name__}')
251 try:
252 _args = t.__args__
254 except AttributeError:
255 raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного '
256 f'типа "_UnionGenericAlias": {t}')
258 if len(_args) == 0:
259 raise ValueError('Не указан ни один тип в конструкции Union')
261 _cast_args = tuple(map(get_type_describer, [ i for i in _args if i is not None]))
262 _args_name = ', '.join(map(lambda x: x.__name__, _args))
264 return TypeDescriber(
265 name=f'Union[{_args_name}]',
266 cast=lambda x: union_processor(_cast_args, x),
267 like=None
270 else:
271 return TypeDescriber(
272 name=t.__name__,
273 cast=t,
274 like=t
278 def dataobj_extract(obj: Union[object, Dict[str, Any]], dataclass_type: type) -> object:
279 """\
280 Извлекает объект данных из предоставленного объекта, путём получения из него
281 указанных в классе данных аттрибутов и поиска их в данном объекте.
282 """
284 params = {}
286 if isinstance(obj, dict):
287 _has = _dict_has
288 _get = _dict_get
290 else:
291 _has = _obj_has
292 _get = _obj_get
294 if not is_dataclass(dataclass_type):
295 raise ValueError(f'Не относится к классам данных: {dataclass_type.__name__}')
297 for fld in fields(dataclass_type):
298 if _has(obj, fld.name):
299 val = _get(obj, fld.name)
300 type_desc = get_type_describer(fld.type)
301 if val is not None:
302 try:
303 val = type_desc(val)
305 except (ValueError, TypeError) as e:
306 raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"'
307 f' с типом {type(val).__name__} поскольку не может быть преобразован в'
308 f' тип {type_desc}, заданный в классе данных: {e}')
310 params[fld.name] = val
312 try:
313 res = dataclass_type(**params)
315 except (ValueError, TypeError) as e:
316 _params = ', '.join(map(lambda x: f'{x[0]}="{x[1]}"', params.items()))
317 raise ValueError(f'Не удалось получить объект'
318 f' класс {dataclass_type.__name__}'
319 f' из параметров: {_params}'
320 f' ошибка: {e}')
322 return res
325 def json_type_sanitizer(val):
326 """\
327 Преобразует значение ``val`` в пригодное для преобразования в json значение.
328 """
330 val_t = type(val)
332 if is_dataclass(val):
333 return json_type_sanitizer(asdict(val))
335 elif val_t in (int, float, str, bool) or val is None:
336 return val
338 elif val_t in (list, tuple):
339 return list(map(json_type_sanitizer, val))
341 elif val_t == dict:
342 return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items())
344 else:
345 return str(val)