py.lib
38:4f4cc2fc9805 Browse Files
. Полный рефакторинг кода модулей dataclass_utils.py и config_parse_helper.py. Теперь логика предсказуема. + функция dataobj_extract не просто бездумно загоняет данные в класс данных, но имеет функционал проверки данных с возбуждением исключения при разнице (по умолчанию) и принудительного приведения типов.
config_parse_helper.py dataclass_utils.py type_utils/__init__.py type_utils/config_parse_helper.py type_utils/dataclass_utils.py type_utils/json_tools.py type_utils/type_descriptor.py
1.1 --- a/config_parse_helper.py Fri Aug 19 02:36:30 2022 +0300 1.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 1.3 @@ -1,517 +0,0 @@ 1.4 -# coding: utf-8 1.5 -from configparser import ConfigParser 1.6 -from dataclasses import is_dataclass, fields, Field, MISSING 1.7 -from typing import Iterable, Optional, Dict, Any, List, Callable, Union 1.8 -from threading import RLock 1.9 -from os import getenv 1.10 - 1.11 - 1.12 -class ConfigParseHelperError(Exception): 1.13 - """\ 1.14 - Ошибки в модуле помощника разборщика конфигураций 1.15 - """ 1.16 - 1.17 - 1.18 -class NoSectionNotification(ConfigParseHelperError): 1.19 - """\ 1.20 - Оповещение об отсутствующей секции конфигурации 1.21 - """ 1.22 - 1.23 - 1.24 -class TypeDescriber: 1.25 - """\ 1.26 - Реализует паттерн "адаптер" поверх типов, для упрощения приведения значений типов к объявленным формам 1.27 - """ 1.28 - def __init__(self, name, cast, like, is_complex=False): 1.29 - self.__name__ = name 1.30 - self.cast = cast 1.31 - self.like = like 1.32 - self.is_complex = is_complex 1.33 - 1.34 - def check(self, instance): 1.35 - if self.like is None: 1.36 - return False 1.37 - 1.38 - else: 1.39 - return check_instance(instance, self.like) 1.40 - 1.41 - def __repr__(self): 1.42 - return f'<TypeDescriber({self.__name__}, {self.like})>' 1.43 - 1.44 - def __call__(self, val): 1.45 - if val is None: 1.46 - return None 1.47 - 1.48 - elif not self.is_complex and check_instance(val, self.like): 1.49 - return val 1.50 - 1.51 - else: 1.52 - return self.cast(val) 1.53 - 1.54 - 1.55 -def check_instance(obj, types): 1.56 - if types is None: 1.57 - return False 1.58 - 1.59 - elif isinstance(types, (tuple, list)): 1.60 - _flag = False 1.61 - for t in types: 1.62 - if check_instance(obj, t): 1.63 - _flag = True 1.64 - break 1.65 - 1.66 - return _flag 1.67 - 1.68 - elif isinstance(types, TypeDescriber): 1.69 - return types.check(obj) 1.70 - 1.71 - else: 1.72 - return isinstance(obj, types) 1.73 - 1.74 - 1.75 -def cast_iterator(t: Union[type, TypeDescriber], lst: Iterable): 1.76 - """\ 1.77 - Обрабатывает последовательности единого типа. 1.78 - """ 1.79 - for i in lst: 1.80 - if check_instance(i, t): 1.81 - yield i 1.82 - 1.83 - else: 1.84 - try: 1.85 - yield t(i) 1.86 - 1.87 - except (TypeError, ValueError) as e: 1.88 - raise ValueError(f'Не удалось привести значение к нужному типу: ' 1.89 - f'тип={t.__name__}; знач={i}') 1.90 - 1.91 - 1.92 -def multi_item_tuple(tt, val): 1.93 - """\ 1.94 - Обрабатывает кортежи, состоящие из нескольких значений типов (кортежи элементов разных типов) 1.95 - :param tt: Последовательность составляющих кортеж типов 1.96 - :param val: итерируемый объект, хранящий значения в указанном порядке. 1.97 - """ 1.98 - val = list(val) 1.99 - t_len = len(tt) 1.100 - if t_len == 0: 1.101 - raise ValueError('При вызове процедуры конвертации котежей, не были указаны типы') 1.102 - 1.103 - if len(val) != t_len: 1.104 - raise ValueError(f'Значение не содержит положенных {t_len} элементов: {len(val)} - {val}') 1.105 - 1.106 - res = [] 1.107 - 1.108 - for i in range(t_len): 1.109 - if check_instance(val[i], tt[i]): 1.110 - res.append(val[i]) 1.111 - 1.112 - else: 1.113 - try: 1.114 - res.append(tt[i](val[i])) 1.115 - 1.116 - except (TypeError, ValueError) as e: 1.117 - raise ValueError(f'Не удалось привести значение к нужному типу: ' 1.118 - f'тип={tt[i].__name__}; знач={val[i]}') 1.119 - 1.120 - return tuple(res) 1.121 - 1.122 - 1.123 -def union_processor(tt, val): 1.124 - """\ 1.125 - Пытается привести значение к одному из указанных типов 1.126 - :param tt: список возможных типов 1.127 - :param val: приводимое значение 1.128 - """ 1.129 - if val is None: 1.130 - return val 1.131 - 1.132 - res = None 1.133 - ex = [] 1.134 - 1.135 - if len(tt) == 0: 1.136 - raise ValueError('Не указан ни один тип в составном типе Union') 1.137 - 1.138 - sorted_types_begin = [ t for t in tt if t in (int, float, bool)] 1.139 - sorted_types_body = [ t for t in tt if t not in (int, float, bool, str)] 1.140 - sorted_types_end = [ t for t in tt if t == str] 1.141 - 1.142 - for t in sorted_types_begin + sorted_types_body + sorted_types_end: 1.143 - _t = get_type_describer(t) 1.144 - try: 1.145 - res = _t(val) 1.146 - break 1.147 - 1.148 - except (TypeError, ValueError) as e: 1.149 - ex.append(f'{t}: {e}') 1.150 - 1.151 - if res is None: 1.152 - raise ValueError('Не удалось привести значение не к одному из типов:\n' + '\n'.join(ex)) 1.153 - 1.154 - else: 1.155 - return res 1.156 - 1.157 - 1.158 -def dict_processor(tt, val): 1.159 - if len(tt) != 2: 1.160 - raise ValueError(f'Попытка воссоздать словарь со странным количеством аргументов типа: {tt}') 1.161 - 1.162 - try: 1.163 - _d = dict(val) 1.164 - 1.165 - except (TypeError, ValueError) as e: 1.166 - raise ValueError(f'Не удалось воссоздать словарь из представленного значения: {e}') 1.167 - 1.168 - _p = [] 1.169 - 1.170 - for k, v in _d.items(): 1.171 - try: 1.172 - _p.append((tt[0](k), tt[1](v))) 1.173 - 1.174 - except (TypeError, ValueError) as e: 1.175 - raise ValueError(f'Не удалось привести значения элемента словаря к требуемому типу: ' 1.176 - f'key="{k}" value="{v}"') 1.177 - 1.178 - return dict(_p) 1.179 - 1.180 - 1.181 -def get_type_describer(t) -> TypeDescriber: 1.182 - if type(t).__name__ == '_GenericAlias': 1.183 - try: 1.184 - _args = t.__args__ 1.185 - 1.186 - except AttributeError: 1.187 - raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' 1.188 - f'типа "_GenericAlias": {t}') 1.189 - 1.190 - if t.__name__ == 'List': 1.191 - try: 1.192 - _t = _args[0] 1.193 - 1.194 - except IndexError: 1.195 - raise ValueError(f'Тип {t} не содержит в себе типа своих элементов') 1.196 - 1.197 - return TypeDescriber( 1.198 - name=f'{t.__name__}[{_t.__name__}]', 1.199 - cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)), 1.200 - like=list, 1.201 - is_complex=True 1.202 - ) 1.203 - 1.204 - elif t.__name__ == 'Tuple': 1.205 - if not _args: 1.206 - raise ValueError(f'Тип {t} не содержит в себе пояснений по составу хранящихся в нём типов') 1.207 - 1.208 - if len(_args) == 1: 1.209 - _t = _args[0] 1.210 - 1.211 - return TypeDescriber( 1.212 - name=f'Tuple[{_t.__name__}]', 1.213 - cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)), 1.214 - like=tuple, 1.215 - is_complex=True 1.216 - ) 1.217 - 1.218 - else: 1.219 - _name = ', '.join(map(lambda x: x.__name__, _args)) 1.220 - _cast_args = tuple(get_type_describer(i) for i in _args) 1.221 - 1.222 - return TypeDescriber( 1.223 - name=f'Tuple[{_name}]', 1.224 - cast=lambda x: multi_item_tuple(_cast_args, x), 1.225 - like=tuple, 1.226 - is_complex=True 1.227 - ) 1.228 - 1.229 - elif t.__name__ == 'Dict': 1.230 - if len(_args) != 2: 1.231 - raise ValueError(f'Неожиданное количество значений типа в составном типе Dict: ' 1.232 - f'{len(_args)} values=({_args})') 1.233 - 1.234 - _name = ', '.join(map(lambda x: x.__name__, _args)) 1.235 - 1.236 - return TypeDescriber( 1.237 - name=f'Dict[{_name}]', 1.238 - cast=lambda x: dict_processor(_args, x), 1.239 - like=dict, 1.240 - is_complex=True 1.241 - ) 1.242 - 1.243 - else: 1.244 - raise ValueError(f'Неизвестный представитель типа "_GenericAlias": {t.__name__}') 1.245 - 1.246 - elif type(t).__name__ == '_UnionGenericAlias': 1.247 - if t.__name__ not in ('Union', 'Optional'): 1.248 - raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {t.__name__}') 1.249 - 1.250 - try: 1.251 - _args = t.__args__ 1.252 - 1.253 - except AttributeError: 1.254 - raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' 1.255 - f'типа "_UnionGenericAlias": {t}') 1.256 - 1.257 - if len(_args) == 0: 1.258 - raise ValueError('Не указан ни один тип в конструкции Union') 1.259 - 1.260 - _cast_args = tuple(map(get_type_describer, _args)) 1.261 - _args_name = ', '.join(map(lambda x: x.__name__, _args)) 1.262 - 1.263 - return TypeDescriber( 1.264 - name=f'Union[{_args_name}]', 1.265 - cast=lambda x: union_processor(_cast_args, x), 1.266 - like=None 1.267 - ) 1.268 - 1.269 - else: 1.270 - return TypeDescriber( 1.271 - name=t.__name__, 1.272 - cast=t, 1.273 - like=t 1.274 - ) 1.275 - 1.276 - 1.277 -class CPHSectionBase: 1.278 - """\ 1.279 - Базовый класс обработки секции конфигурационного файла 1.280 - """ 1.281 - 1.282 - def get(self, config_prop_name: str, dc_prop_name: str): 1.283 - """\ 1.284 - Получить свойство из конфигурационного файла 1.285 - """ 1.286 - raise NotImplemented() 1.287 - 1.288 - def __enter__(self): 1.289 - return self 1.290 - 1.291 - def __exit__(self, exc_type, exc_val, exc_tb): 1.292 - raise NotImplemented() 1.293 - 1.294 - 1.295 -class CPHAbsentSection(CPHSectionBase): 1.296 - """\ 1.297 - Класс создаваемый на отсутствующую секцию конфигурационного файла 1.298 - """ 1.299 - def get(self, config_prop_name: str, dc_prop_name: str): 1.300 - raise NoSectionNotification() 1.301 - 1.302 - def __exit__(self, exc_type, exc_val, exc_tb): 1.303 - if exc_type == NoSectionNotification: 1.304 - return True 1.305 - 1.306 - 1.307 -class CPHParamGetter(CPHSectionBase): 1.308 - def __init__(self, parser_helper_object): 1.309 - self.ph = parser_helper_object 1.310 - self.params = {} 1.311 - 1.312 - def _add_param(self, param_name: str, param_val: Any, parser: Optional[Callable[[Any], Any]] = None): 1.313 - """\ 1.314 - Непосредственное добавление полученного параметра со всеми проверками. 1.315 - """ 1.316 - 1.317 - if parser is not None and param_val is not None: 1.318 - param_val = parser(param_val) 1.319 - 1.320 - fld = self.ph.fields.get(param_name) 1.321 - if not isinstance(fld, Field): 1.322 - raise ConfigParseHelperError(f'В классе данных отсутствует свойство "{param_name}", ' 1.323 - f'которое мы должны заполнить из параметра конфигурации: {fld}') 1.324 - 1.325 - if param_val is not None: 1.326 - type_desc = get_type_describer(fld.type) 1.327 - try: 1.328 - res = type_desc(param_val) 1.329 - 1.330 - except (ValueError, TypeError) as e: 1.331 - raise ConfigParseHelperError(f'При приведении параметра к ' 1.332 - f'заданному типу произошла ошибка: ' 1.333 - f'значение="{param_val}" ошибка="{e}"') 1.334 - 1.335 - else: 1.336 - if fld.default is not MISSING: 1.337 - res = fld.default 1.338 - 1.339 - elif fld.default_factory is not MISSING: 1.340 - res = fld.default_factory() 1.341 - 1.342 - else: 1.343 - raise ConfigParseHelperError('В конфигурации не заданна обязательная опция') 1.344 - 1.345 - self.params[param_name] = res 1.346 - 1.347 - def __exit__(self, exc_type, exc_val, exc_tb): 1.348 - if exc_type is None: 1.349 - self.ph.add_params(self.params) 1.350 - 1.351 - def get(self, config_prop_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 1.352 - raise NoSectionNotification() 1.353 - 1.354 - 1.355 -class CPHSection(CPHParamGetter): 1.356 - """\ 1.357 - Класс производящий разбор конкретной секции конфигурации 1.358 - """ 1.359 - 1.360 - def __init__(self, parser_helper_object, section: str): 1.361 - super().__init__(parser_helper_object) 1.362 - self.section_name = section 1.363 - self.section = parser_helper_object.conf_parser[section] 1.364 - 1.365 - def get(self, config_prop_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 1.366 - """\ 1.367 - :param config_prop_name: Имя опции в файле конфигурации 1.368 - :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 1.369 - :param parser: Исполнимый обработчик значения, перед его помещением в конфигурацию 1.370 - """ 1.371 - try: 1.372 - self._add_param(dc_prop_name, self.section.get(config_prop_name), parser) 1.373 - 1.374 - except ConfigParseHelperError as e: 1.375 - raise ConfigParseHelperError(f'Ошибка при разборе параметра "{config_prop_name}" ' 1.376 - f'в секции "{self.section_name}": {e}') 1.377 - 1.378 - 1.379 -class CPHEnvParser(CPHParamGetter): 1.380 - """\ 1.381 - Класс для разбора переменных окружения в том же ключе, что и файла конфигурации 1.382 - """ 1.383 - 1.384 - def get(self, env_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 1.385 - """\ 1.386 - :param env_name: Имя переменной окружения, хранящей опцию 1.387 - :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 1.388 - :param parser: Исполнимый обработчик значения, перед его помещением в конфигурацию 1.389 - """ 1.390 - 1.391 - try: 1.392 - self._add_param(dc_prop_name, getenv(env_name), parser) 1.393 - 1.394 - except ConfigParseHelperError as e: 1.395 - raise ConfigParseHelperError(f'Ошибка при получении значения из переменной окружения "{env_name}": {e}') 1.396 - 1.397 - 1.398 -class CPHObjectsListGetter: 1.399 - """\ 1.400 - Помощник для случаев, когда в наборе секций хранится однотипный набор объектов 1.401 - """ 1.402 - def __init__(self, config_object_class: type, config_parser: ConfigParser, sections: Iterable[str]): 1.403 - self.sections = sections 1.404 - self.conf_parser = config_parser 1.405 - 1.406 - if not is_dataclass(config_object_class): 1.407 - raise ConfigParseHelperError(f'Представленный в качестве представления объекта конфигурации ' 1.408 - f'класс не является классом данных: {config_object_class.__name__}') 1.409 - 1.410 - self.res_obj = config_object_class 1.411 - self.fields = dict((fld.name, fld) for fld in fields(config_object_class)) 1.412 - self.obj_list = [] 1.413 - self.ident_list = [] 1.414 - 1.415 - def add_params(self, params: Dict[str, Any]): 1.416 - try: 1.417 - self.obj_list.append(self.res_obj(**params)) 1.418 - 1.419 - except (ValueError, TypeError) as e: 1.420 - raise ConfigParseHelperError(f'Ошибка создания объекта объекта конфигурации, ' 1.421 - f'списка объектов конфигурации: {e}') 1.422 - 1.423 - def get(self, config_prop_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 1.424 - """\ 1.425 - Подготавливаем список соответствия названий параметров в секции конкретным свойствам данного 1.426 - в помощник класса 1.427 - 1.428 - :param config_prop_name: Имя опции в файле конфигурации 1.429 - :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 1.430 - :param parser: Исполнимый обработчик значения, перед его помещением в конфигурацию 1.431 - """ 1.432 - 1.433 - self.ident_list.append((config_prop_name, dc_prop_name, parser)) 1.434 - 1.435 - def get_config_objects(self) -> List[object]: 1.436 - for section in self.sections: 1.437 - try: 1.438 - with CPHSection(self, section) as section_helper: 1.439 - for conf_prop, dc_prop, parser in self.ident_list: 1.440 - section_helper.get(conf_prop, dc_prop, parser) 1.441 - 1.442 - except ConfigParseHelperError as e: 1.443 - raise ConfigParseHelperError(f'Ошибка при разборе секции "{section}": {e}') 1.444 - 1.445 - res = self.obj_list[:] 1.446 - self.obj_list.clear() 1.447 - 1.448 - return res 1.449 - 1.450 - 1.451 -class ConfigParseHelper: 1.452 - """\ 1.453 - Помощник разбора конфигурации 1.454 - """ 1.455 - def __init__(self, config_object_class: type, required_sections: Optional[Iterable[str]] = None): 1.456 - """\ 1.457 - :param config_object_class: Dataclass, который мы подготовили как хранилище параметров конфигурации 1.458 - :param required_sections: Перечисление секций конфигурации, которые мы требуем, чтобы были в файле 1.459 - """ 1.460 - 1.461 - if required_sections is not None: 1.462 - self.req_sections = set(required_sections) 1.463 - 1.464 - else: 1.465 - self.req_sections = set() 1.466 - 1.467 - if not is_dataclass(config_object_class): 1.468 - raise ConfigParseHelperError(f'Представленный в качестве объекта конфигурации класс не является ' 1.469 - f'классом данных: {config_object_class.__name__}') 1.470 - 1.471 - self.res_obj = config_object_class 1.472 - self.fields = dict((fld.name, fld) for fld in fields(config_object_class)) 1.473 - self.conf_parser: Optional[ConfigParser] = None 1.474 - self.config_params = {} 1.475 - self.config_params_lock = RLock() 1.476 - 1.477 - def add_params(self, params: Dict[str, Any]): 1.478 - self.config_params_lock.acquire() 1.479 - try: 1.480 - self.config_params.update(params) 1.481 - 1.482 - finally: 1.483 - self.config_params_lock.release() 1.484 - 1.485 - def section(self, section_name: str) -> CPHSectionBase: 1.486 - if self.conf_parser is None: 1.487 - raise ConfigParseHelperError(f'Прежде чем приступать к разбору файла конфигурации стоит его загрузить') 1.488 - 1.489 - if self.conf_parser.has_section(section_name): 1.490 - return CPHSection(self, section_name) 1.491 - 1.492 - else: 1.493 - return CPHAbsentSection() 1.494 - 1.495 - def load(self, filename: str): 1.496 - res = ConfigParser() 1.497 - try: 1.498 - res.read(filename) 1.499 - 1.500 - except (TypeError, IOError, OSError, ValueError) as e: 1.501 - raise ConfigParseHelperError(f'Не удалось загрузить файл конфигурации: файл="{filename}" ' 1.502 - f'ошибка="{e}"') 1.503 - 1.504 - missing_sections = self.req_sections - set(res.sections()) 1.505 - 1.506 - if missing_sections: 1.507 - missing_sections = ', '.join(missing_sections) 1.508 - raise ConfigParseHelperError(f'В конфигурационном файле отсутствуют секции: {missing_sections}') 1.509 - 1.510 - self.conf_parser = res 1.511 - 1.512 - def get_config(self): 1.513 - try: 1.514 - return self.res_obj(**self.config_params) 1.515 - 1.516 - except (ValueError, TypeError) as e: 1.517 - raise ConfigParseHelperError(f'Не удалось инициализировать объект конфигурации: {e}') 1.518 - 1.519 - def get_sections(self): 1.520 - return self.conf_parser.sections()
2.1 --- a/dataclass_utils.py Fri Aug 19 02:36:30 2022 +0300 2.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 2.3 @@ -1,345 +0,0 @@ 2.4 -# coding: utf-8 2.5 - 2.6 -from dataclasses import fields, is_dataclass, asdict 2.7 -from typing import Union, Dict, Any, Iterable 2.8 - 2.9 - 2.10 -def _dict_has(obj: Dict[str, Any], key: str) -> bool: 2.11 - return key in obj 2.12 - 2.13 - 2.14 -def _dict_get(obj: Dict[str, Any], key: str) -> Any: 2.15 - return obj[key] 2.16 - 2.17 - 2.18 -def _obj_has(obj: object, key: str) -> bool: 2.19 - return hasattr(obj, key) 2.20 - 2.21 - 2.22 -def _obj_get(obj: object, key: str) -> Any: 2.23 - return getattr(obj, key) 2.24 - 2.25 - 2.26 -class TypeDescriber: 2.27 - """\ 2.28 - Реализует паттерн "адаптер" поверх типов, для упрощения приведения значений типов к объявленным формам 2.29 - """ 2.30 - def __init__(self, name, cast, like, is_complex=False): 2.31 - self.__name__ = name 2.32 - self.cast = cast 2.33 - self.like = like 2.34 - self.is_complex = is_complex 2.35 - 2.36 - def check(self, instance): 2.37 - if self.like is None: 2.38 - return False 2.39 - 2.40 - else: 2.41 - return check_instance(instance, self.like) 2.42 - 2.43 - def __repr__(self): 2.44 - return f'<TypeDescriber({self.__name__}, {self.like})>' 2.45 - 2.46 - def __call__(self, val): 2.47 - if val is None: 2.48 - return None 2.49 - 2.50 - elif not self.is_complex and check_instance(val, self.like): 2.51 - return val 2.52 - 2.53 - else: 2.54 - return self.cast(val) 2.55 - 2.56 - 2.57 -def check_instance(obj, types): 2.58 - if types is None: 2.59 - return False 2.60 - 2.61 - elif isinstance(types, (tuple, list)): 2.62 - _flag = False 2.63 - for t in types: 2.64 - if check_instance(obj, t): 2.65 - _flag = True 2.66 - break 2.67 - 2.68 - return _flag 2.69 - 2.70 - elif isinstance(types, TypeDescriber): 2.71 - return types.check(obj) 2.72 - 2.73 - else: 2.74 - return isinstance(obj, types) 2.75 - 2.76 - 2.77 -def cast_iterator(t: Union[type, TypeDescriber], lst: Iterable): 2.78 - """\ 2.79 - Обрабатывает последовательности единого типа. 2.80 - """ 2.81 - for i in lst: 2.82 - if check_instance(i, t): 2.83 - yield i 2.84 - 2.85 - else: 2.86 - try: 2.87 - yield t(i) 2.88 - 2.89 - except (TypeError, ValueError) as e: 2.90 - raise ValueError(f'Не удалось привести значение к нужному типу: ' 2.91 - f'тип={t.__name__}; знач={i}') 2.92 - 2.93 - 2.94 -def multi_item_tuple(tt, val): 2.95 - """\ 2.96 - Обрабатывает кортежи, состоящие из нескольких значений типов (кортежи элементов разных типов) 2.97 - :param tt: Последовательность составляющих кортеж типов 2.98 - :param val: итерируемый объект, хранящий значения в указанном порядке. 2.99 - """ 2.100 - val = list(val) 2.101 - t_len = len(tt) 2.102 - if t_len == 0: 2.103 - raise ValueError('При вызове процедуры конвертации котежей, не были указаны типы') 2.104 - 2.105 - if len(val) != t_len: 2.106 - raise ValueError(f'Значение не содержит положенных {t_len} элементов: {len(val)} - {val}') 2.107 - 2.108 - res = [] 2.109 - 2.110 - for i in range(t_len): 2.111 - if check_instance(val[i], tt[i]): 2.112 - res.append(val[i]) 2.113 - 2.114 - else: 2.115 - try: 2.116 - res.append(tt[i](val[i])) 2.117 - 2.118 - except (TypeError, ValueError) as e: 2.119 - raise ValueError(f'Не удалось привести значение к нужному типу: ' 2.120 - f'тип={tt[i].__name__}; знач={val[i]}') 2.121 - 2.122 - return tuple(res) 2.123 - 2.124 - 2.125 -def union_processor(tt, val): 2.126 - """\ 2.127 - Пытается привести значение к одному из указанных типов 2.128 - :param tt: список возможных типов 2.129 - :param val: приводимое значение 2.130 - """ 2.131 - if val is None: 2.132 - return val 2.133 - 2.134 - res = None 2.135 - ex = [] 2.136 - 2.137 - if len(tt) == 0: 2.138 - raise ValueError('Не указан ни один тип в составном типе Union') 2.139 - 2.140 - sorted_types_begin = [ t for t in tt if t in (int, float, bool)] 2.141 - sorted_types_body = [ t for t in tt if t not in (int, float, bool, str)] 2.142 - sorted_types_end = [ t for t in tt if t == str] 2.143 - 2.144 - for t in sorted_types_begin + sorted_types_body + sorted_types_end: 2.145 - _t = get_type_describer(t) 2.146 - try: 2.147 - res = _t(val) 2.148 - break 2.149 - 2.150 - except (TypeError, ValueError) as e: 2.151 - ex.append(f'{t}: {e}') 2.152 - 2.153 - if res is None: 2.154 - raise ValueError('Не удалось привести значение не к одному из типов:\n' + '\n'.join(ex)) 2.155 - 2.156 - else: 2.157 - return res 2.158 - 2.159 - 2.160 -def dict_processor(tt, val): 2.161 - if len(tt) != 2: 2.162 - raise ValueError(f'Попытка воссоздать словарь со странным количеством аргументов типа: {tt}') 2.163 - 2.164 - try: 2.165 - _d = dict(val) 2.166 - 2.167 - except (TypeError, ValueError) as e: 2.168 - raise ValueError(f'Не удалось воссоздать словарь из представленного значения: {e}') 2.169 - 2.170 - _p = [] 2.171 - 2.172 - tt_cast = [get_type_describer(t) for t in tt] 2.173 - 2.174 - for k, v in _d.items(): 2.175 - try: 2.176 - _p.append((tt_cast[0](k), tt_cast[1](v))) 2.177 - 2.178 - except (TypeError, ValueError) as e: 2.179 - raise ValueError(f'Не удалось привести значения элемента словаря к требуемому типу: ' 2.180 - f'key="{k}" value="{v}"') 2.181 - 2.182 - return dict(_p) 2.183 - 2.184 - 2.185 -def get_type_describer(t) -> TypeDescriber: 2.186 - if type(t).__name__ == '_GenericAlias': 2.187 - try: 2.188 - _args = t.__args__ 2.189 - 2.190 - except AttributeError: 2.191 - raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' 2.192 - f'типа "_GenericAlias": {t}') 2.193 - 2.194 - if t.__name__ == 'List': 2.195 - try: 2.196 - _t = _args[0] 2.197 - 2.198 - except IndexError: 2.199 - raise ValueError(f'Тип {t} не содержит в себе типа своих элементов') 2.200 - 2.201 - return TypeDescriber( 2.202 - name=f'{t.__name__}[{_t.__name__}]', 2.203 - cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)), 2.204 - like=list, 2.205 - is_complex=True 2.206 - ) 2.207 - 2.208 - elif t.__name__ == 'Tuple': 2.209 - if not _args: 2.210 - raise ValueError(f'Тип {t} не содержит в себе пояснений по составу хранящихся в нём типов') 2.211 - 2.212 - if len(_args) == 1: 2.213 - _t = _args[0] 2.214 - 2.215 - return TypeDescriber( 2.216 - name=f'Tuple[{_t.__name__}]', 2.217 - cast=lambda x: list(cast_iterator(get_type_describer(_t).cast, x)), 2.218 - like=tuple, 2.219 - is_complex=True 2.220 - ) 2.221 - 2.222 - else: 2.223 - _name = ', '.join(map(lambda x: x.__name__, _args)) 2.224 - _cast_args = tuple(get_type_describer(i) for i in _args) 2.225 - 2.226 - return TypeDescriber( 2.227 - name=f'Tuple[{_name}]', 2.228 - cast=lambda x: multi_item_tuple(_cast_args, x), 2.229 - like=tuple, 2.230 - is_complex=True 2.231 - ) 2.232 - 2.233 - elif t.__name__ == 'Dict': 2.234 - if len(_args) != 2: 2.235 - raise ValueError(f'Неожиданное количество значений типа в составном типе Dict: ' 2.236 - f'{len(_args)} values=({_args})') 2.237 - 2.238 - _name = ', '.join(map(lambda x: x.__name__, _args)) 2.239 - 2.240 - return TypeDescriber( 2.241 - name=f'Dict[{_name}]', 2.242 - cast=lambda x: dict_processor(_args, x), 2.243 - like=dict, 2.244 - is_complex=True 2.245 - ) 2.246 - 2.247 - else: 2.248 - raise ValueError(f'Неизвестный представитель типа "_GenericAlias": {t.__name__}') 2.249 - 2.250 - elif type(t).__name__ == '_UnionGenericAlias': 2.251 - if t.__name__ not in ('Union', 'Optional'): 2.252 - raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {t.__name__}') 2.253 - 2.254 - try: 2.255 - _args = t.__args__ 2.256 - 2.257 - except AttributeError: 2.258 - raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' 2.259 - f'типа "_UnionGenericAlias": {t}') 2.260 - 2.261 - if len(_args) == 0: 2.262 - raise ValueError('Не указан ни один тип в конструкции Union') 2.263 - 2.264 - _cast_args = tuple(map(get_type_describer, [ i for i in _args if i is not None])) 2.265 - _args_name = ', '.join(map(lambda x: x.__name__, _args)) 2.266 - 2.267 - return TypeDescriber( 2.268 - name=f'Union[{_args_name}]', 2.269 - cast=lambda x: union_processor(_cast_args, x), 2.270 - like=None 2.271 - ) 2.272 - 2.273 - else: 2.274 - return TypeDescriber( 2.275 - name=t.__name__, 2.276 - cast=t, 2.277 - like=t 2.278 - ) 2.279 - 2.280 - 2.281 -def dataobj_extract(obj: Union[object, Dict[str, Any]], dataclass_type: type) -> object: 2.282 - """\ 2.283 - Извлекает объект данных из предоставленного объекта, путём получения из него 2.284 - указанных в классе данных аттрибутов и поиска их в данном объекте. 2.285 - """ 2.286 - 2.287 - params = {} 2.288 - 2.289 - if isinstance(obj, dict): 2.290 - _has = _dict_has 2.291 - _get = _dict_get 2.292 - 2.293 - else: 2.294 - _has = _obj_has 2.295 - _get = _obj_get 2.296 - 2.297 - if not is_dataclass(dataclass_type): 2.298 - raise ValueError(f'Не относится к классам данных: {dataclass_type.__name__}') 2.299 - 2.300 - for fld in fields(dataclass_type): 2.301 - if _has(obj, fld.name): 2.302 - val = _get(obj, fld.name) 2.303 - type_desc = get_type_describer(fld.type) 2.304 - if val is not None: 2.305 - try: 2.306 - val = type_desc(val) 2.307 - 2.308 - except (ValueError, TypeError) as e: 2.309 - raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"' 2.310 - f' с типом {type(val).__name__} поскольку не может быть преобразован в' 2.311 - f' тип {type_desc}, заданный в классе данных: {e}') 2.312 - 2.313 - params[fld.name] = val 2.314 - 2.315 - try: 2.316 - res = dataclass_type(**params) 2.317 - 2.318 - except (ValueError, TypeError) as e: 2.319 - _params = ', '.join(map(lambda x: f'{x[0]}="{x[1]}"', params.items())) 2.320 - raise ValueError(f'Не удалось получить объект' 2.321 - f' класс {dataclass_type.__name__}' 2.322 - f' из параметров: {_params}' 2.323 - f' ошибка: {e}') 2.324 - 2.325 - return res 2.326 - 2.327 - 2.328 -def json_type_sanitizer(val): 2.329 - """\ 2.330 - Преобразует значение ``val`` в пригодное для преобразования в json значение. 2.331 - """ 2.332 - 2.333 - val_t = type(val) 2.334 - 2.335 - if is_dataclass(val): 2.336 - return json_type_sanitizer(asdict(val)) 2.337 - 2.338 - elif val_t in (int, float, str, bool) or val is None: 2.339 - return val 2.340 - 2.341 - elif val_t in (list, tuple): 2.342 - return list(map(json_type_sanitizer, val)) 2.343 - 2.344 - elif val_t == dict: 2.345 - return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items()) 2.346 - 2.347 - else: 2.348 - return str(val)
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/type_utils/__init__.py Sat Aug 20 23:56:16 2022 +0300 3.3 @@ -0,0 +1,5 @@ 3.4 +# coding: utf-8 3.5 +"""\ 3.6 +Модуль, выполняющий различные операции над сложными типами. Должен быть вставлен в проект полностью, без изъятий. 3.7 +""" 3.8 +
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/type_utils/config_parse_helper.py Sat Aug 20 23:56:16 2022 +0300 4.3 @@ -0,0 +1,272 @@ 4.4 +# coding: utf-8 4.5 +"""\ 4.6 +Получение из конфигурационного файла параметров в виде класса данных. 4.7 + 4.8 +НЕ РАБОТАЕТ БЕЗ ОСТАЛЬНОГО МОДУЛЯ type_utils 4.9 +""" 4.10 + 4.11 +from configparser import ConfigParser 4.12 +from dataclasses import is_dataclass, fields, Field, MISSING 4.13 +from typing import Iterable, Optional, Dict, Any, List, Callable, Union 4.14 +from threading import RLock 4.15 +from os import getenv 4.16 + 4.17 +from .type_descriptor import get_type_describer 4.18 + 4.19 + 4.20 +class ConfigParseHelperError(Exception): 4.21 + """\ 4.22 + Ошибки в модуле помощника разборщика конфигураций 4.23 + """ 4.24 + 4.25 + 4.26 +class NoSectionNotification(ConfigParseHelperError): 4.27 + """\ 4.28 + Оповещение об отсутствующей секции конфигурации 4.29 + """ 4.30 + 4.31 + 4.32 +class CPHSectionBase: 4.33 + """\ 4.34 + Базовый класс обработки секции конфигурационного файла 4.35 + """ 4.36 + 4.37 + def get(self, config_prop_name: str, dc_prop_name: str): 4.38 + """\ 4.39 + Получить свойство из конфигурационного файла 4.40 + """ 4.41 + raise NotImplemented() 4.42 + 4.43 + def __enter__(self): 4.44 + return self 4.45 + 4.46 + def __exit__(self, exc_type, exc_val, exc_tb): 4.47 + raise NotImplemented() 4.48 + 4.49 + 4.50 +class CPHAbsentSection(CPHSectionBase): 4.51 + """\ 4.52 + Класс создаваемый на отсутствующую секцию конфигурационного файла 4.53 + """ 4.54 + def get(self, config_prop_name: str, dc_prop_name: str): 4.55 + raise NoSectionNotification() 4.56 + 4.57 + def __exit__(self, exc_type, exc_val, exc_tb): 4.58 + if exc_type == NoSectionNotification: 4.59 + return True 4.60 + 4.61 + 4.62 +class CPHParamGetter(CPHSectionBase): 4.63 + def __init__(self, parser_helper_object): 4.64 + self.ph = parser_helper_object 4.65 + self.params = {} 4.66 + 4.67 + def _add_param(self, param_name: str, param_val: Any, parser: Optional[Callable[[Any], Any]] = None): 4.68 + """\ 4.69 + Непосредственное добавление полученного параметра со всеми проверками. 4.70 + """ 4.71 + 4.72 + if parser is not None and param_val is not None: 4.73 + param_val = parser(param_val) 4.74 + 4.75 + fld = self.ph.fields.get(param_name) 4.76 + if not isinstance(fld, Field): 4.77 + raise ConfigParseHelperError(f'В классе данных отсутствует свойство "{param_name}", ' 4.78 + f'которое мы должны заполнить из параметра конфигурации: {fld}') 4.79 + 4.80 + if param_val is not None: 4.81 + type_desc = get_type_describer(fld.type) 4.82 + try: 4.83 + res = type_desc(param_val) 4.84 + 4.85 + except (ValueError, TypeError) as e: 4.86 + raise ConfigParseHelperError(f'При приведении параметра к ' 4.87 + f'заданному типу произошла ошибка: ' 4.88 + f'значение="{param_val}" ошибка="{e}"') 4.89 + 4.90 + else: 4.91 + if fld.default is not MISSING: 4.92 + res = fld.default 4.93 + 4.94 + elif fld.default_factory is not MISSING: 4.95 + res = fld.default_factory() 4.96 + 4.97 + else: 4.98 + raise ConfigParseHelperError('В конфигурации не заданна обязательная опция') 4.99 + 4.100 + self.params[param_name] = res 4.101 + 4.102 + def __exit__(self, exc_type, exc_val, exc_tb): 4.103 + if exc_type is None: 4.104 + self.ph.add_params(self.params) 4.105 + 4.106 + def get(self, config_prop_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 4.107 + raise NoSectionNotification() 4.108 + 4.109 + 4.110 +class CPHSection(CPHParamGetter): 4.111 + """\ 4.112 + Класс производящий разбор конкретной секции конфигурации 4.113 + """ 4.114 + 4.115 + def __init__(self, parser_helper_object, section: str): 4.116 + super().__init__(parser_helper_object) 4.117 + self.section_name = section 4.118 + self.section = parser_helper_object.conf_parser[section] 4.119 + 4.120 + def get(self, config_prop_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 4.121 + """\ 4.122 + :param config_prop_name: Имя опции в файле конфигурации 4.123 + :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 4.124 + :param parser: Исполнимый обработчик значения, перед его помещением в конфигурацию 4.125 + """ 4.126 + try: 4.127 + self._add_param(dc_prop_name, self.section.get(config_prop_name), parser) 4.128 + 4.129 + except ConfigParseHelperError as e: 4.130 + raise ConfigParseHelperError(f'Ошибка при разборе параметра "{config_prop_name}" ' 4.131 + f'в секции "{self.section_name}": {e}') 4.132 + 4.133 + 4.134 +class CPHEnvParser(CPHParamGetter): 4.135 + """\ 4.136 + Класс для разбора переменных окружения в том же ключе, что и файла конфигурации 4.137 + """ 4.138 + 4.139 + def get(self, env_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 4.140 + """\ 4.141 + :param env_name: Имя переменной окружения, хранящей опцию 4.142 + :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 4.143 + :param parser: Исполнимый обработчик значения, перед его помещением в конфигурацию 4.144 + """ 4.145 + 4.146 + try: 4.147 + self._add_param(dc_prop_name, getenv(env_name), parser) 4.148 + 4.149 + except ConfigParseHelperError as e: 4.150 + raise ConfigParseHelperError(f'Ошибка при получении значения из переменной окружения "{env_name}": {e}') 4.151 + 4.152 + 4.153 +class CPHObjectsListGetter: 4.154 + """\ 4.155 + Помощник для случаев, когда в наборе секций хранится однотипный набор объектов 4.156 + """ 4.157 + def __init__(self, config_object_class: type, config_parser: ConfigParser, sections: Iterable[str]): 4.158 + self.sections = sections 4.159 + self.conf_parser = config_parser 4.160 + 4.161 + if not is_dataclass(config_object_class): 4.162 + raise ConfigParseHelperError(f'Представленный в качестве представления объекта конфигурации ' 4.163 + f'класс не является классом данных: {config_object_class.__name__}') 4.164 + 4.165 + self.res_obj = config_object_class 4.166 + self.fields = dict((fld.name, fld) for fld in fields(config_object_class)) 4.167 + self.obj_list = [] 4.168 + self.ident_list = [] 4.169 + 4.170 + def add_params(self, params: Dict[str, Any]): 4.171 + try: 4.172 + self.obj_list.append(self.res_obj(**params)) 4.173 + 4.174 + except (ValueError, TypeError) as e: 4.175 + raise ConfigParseHelperError(f'Ошибка создания объекта объекта конфигурации, ' 4.176 + f'списка объектов конфигурации: {e}') 4.177 + 4.178 + def get(self, config_prop_name: str, dc_prop_name: str, parser: Optional[Callable[[Any], Any]] = None): 4.179 + """\ 4.180 + Подготавливаем список соответствия названий параметров в секции конкретным свойствам данного 4.181 + в помощник класса 4.182 + 4.183 + :param config_prop_name: Имя опции в файле конфигурации 4.184 + :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 4.185 + :param parser: Исполнимый обработчик значения, перед его помещением в конфигурацию 4.186 + """ 4.187 + 4.188 + self.ident_list.append((config_prop_name, dc_prop_name, parser)) 4.189 + 4.190 + def get_config_objects(self) -> List[object]: 4.191 + for section in self.sections: 4.192 + try: 4.193 + with CPHSection(self, section) as section_helper: 4.194 + for conf_prop, dc_prop, parser in self.ident_list: 4.195 + section_helper.get(conf_prop, dc_prop, parser) 4.196 + 4.197 + except ConfigParseHelperError as e: 4.198 + raise ConfigParseHelperError(f'Ошибка при разборе секции "{section}": {e}') 4.199 + 4.200 + res = self.obj_list[:] 4.201 + self.obj_list.clear() 4.202 + 4.203 + return res 4.204 + 4.205 + 4.206 +class ConfigParseHelper: 4.207 + """\ 4.208 + Помощник разбора конфигурации 4.209 + """ 4.210 + def __init__(self, config_object_class: type, required_sections: Optional[Iterable[str]] = None): 4.211 + """\ 4.212 + :param config_object_class: Dataclass, который мы подготовили как хранилище параметров конфигурации 4.213 + :param required_sections: Перечисление секций конфигурации, которые мы требуем, чтобы были в файле 4.214 + """ 4.215 + 4.216 + if required_sections is not None: 4.217 + self.req_sections = set(required_sections) 4.218 + 4.219 + else: 4.220 + self.req_sections = set() 4.221 + 4.222 + if not is_dataclass(config_object_class): 4.223 + raise ConfigParseHelperError(f'Представленный в качестве объекта конфигурации класс не является ' 4.224 + f'классом данных: {config_object_class.__name__}') 4.225 + 4.226 + self.res_obj = config_object_class 4.227 + self.fields = dict((fld.name, fld) for fld in fields(config_object_class)) 4.228 + self.conf_parser: Optional[ConfigParser] = None 4.229 + self.config_params = {} 4.230 + self.config_params_lock = RLock() 4.231 + 4.232 + def add_params(self, params: Dict[str, Any]): 4.233 + self.config_params_lock.acquire() 4.234 + try: 4.235 + self.config_params.update(params) 4.236 + 4.237 + finally: 4.238 + self.config_params_lock.release() 4.239 + 4.240 + def section(self, section_name: str) -> CPHSectionBase: 4.241 + if self.conf_parser is None: 4.242 + raise ConfigParseHelperError(f'Прежде чем приступать к разбору файла конфигурации стоит его загрузить') 4.243 + 4.244 + if self.conf_parser.has_section(section_name): 4.245 + return CPHSection(self, section_name) 4.246 + 4.247 + else: 4.248 + return CPHAbsentSection() 4.249 + 4.250 + def load(self, filename: str): 4.251 + res = ConfigParser() 4.252 + try: 4.253 + res.read(filename) 4.254 + 4.255 + except (TypeError, IOError, OSError, ValueError) as e: 4.256 + raise ConfigParseHelperError(f'Не удалось загрузить файл конфигурации: файл="{filename}" ' 4.257 + f'ошибка="{e}"') 4.258 + 4.259 + missing_sections = self.req_sections - set(res.sections()) 4.260 + 4.261 + if missing_sections: 4.262 + missing_sections = ', '.join(missing_sections) 4.263 + raise ConfigParseHelperError(f'В конфигурационном файле отсутствуют секции: {missing_sections}') 4.264 + 4.265 + self.conf_parser = res 4.266 + 4.267 + def get_config(self): 4.268 + try: 4.269 + return self.res_obj(**self.config_params) 4.270 + 4.271 + except (ValueError, TypeError) as e: 4.272 + raise ConfigParseHelperError(f'Не удалось инициализировать объект конфигурации: {e}') 4.273 + 4.274 + def get_sections(self): 4.275 + return self.conf_parser.sections()
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 5.2 +++ b/type_utils/dataclass_utils.py Sat Aug 20 23:56:16 2022 +0300 5.3 @@ -0,0 +1,89 @@ 5.4 +# coding: utf-8 5.5 +"""\ 5.6 +Наполнение заданного класса данных из предоставленного словаря или объекта. 5.7 + 5.8 +НЕ РАБОТАЕТ БЕЗ ОСТАЛЬНОГО МОДУЛЯ type_utils 5.9 +""" 5.10 + 5.11 +from dataclasses import fields, is_dataclass, asdict 5.12 +from typing import Union, Dict, Any, Iterable 5.13 +from .type_descriptor import get_type_describer 5.14 + 5.15 + 5.16 +def _dict_has(obj: Dict[str, Any], key: str) -> bool: 5.17 + return key in obj 5.18 + 5.19 + 5.20 +def _dict_get(obj: Dict[str, Any], key: str) -> Any: 5.21 + return obj[key] 5.22 + 5.23 + 5.24 +def _obj_has(obj: object, key: str) -> bool: 5.25 + return hasattr(obj, key) 5.26 + 5.27 + 5.28 +def _obj_get(obj: object, key: str) -> Any: 5.29 + return getattr(obj, key) 5.30 + 5.31 + 5.32 +def dataobj_extract(obj: Union[object, Dict[str, Any]], dataclass_type: type, type_convert: bool = False) -> object: 5.33 + """\ 5.34 + Извлекает объект данных из предоставленного объекта, путём получения из него 5.35 + указанных в классе данных аттрибутов и поиска их в данном объекте. 5.36 + :param obj: Объект, из которого берутся данные для класса данных 5.37 + :param dataclass_type: Класс данных, наполняемый из ``obj`` 5.38 + :param type_convert: Признак конвертирования данных. Если задан, выполняет попытку сконвертировать имеющееся 5.39 + в параметре значение в тип, указанных в классе данных. Чудес не бывает, и процесс может 5.40 + ошибиться особенно с Union 5.41 + """ 5.42 + 5.43 + params = {} 5.44 + 5.45 + if isinstance(obj, dict): 5.46 + _has = _dict_has 5.47 + _get = _dict_get 5.48 + 5.49 + else: 5.50 + _has = _obj_has 5.51 + _get = _obj_get 5.52 + 5.53 + if not is_dataclass(dataclass_type): 5.54 + raise ValueError(f'Не относится к классам данных: {dataclass_type.__name__}') 5.55 + 5.56 + for fld in fields(dataclass_type): 5.57 + if _has(obj, fld.name): 5.58 + val = _get(obj, fld.name) 5.59 + type_desc = get_type_describer(fld.type) 5.60 + if val is not None: 5.61 + if not type_desc.check(val): 5.62 + if not type_convert: 5.63 + raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"' 5.64 + f' с типом {type(val).__name__} поскольку не может быть преобразован в' 5.65 + f' тип {type_desc}, заданный в классе данных: {type_desc.event_description}') 5.66 + 5.67 + try: 5.68 + val = type_desc(val) 5.69 + 5.70 + except (ValueError, TypeError) as e: 5.71 + raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"' 5.72 + f' с типом {type(val).__name__} поскольку не может быть преобразован в' 5.73 + f' тип {type_desc}, заданный в классе данных: {e}') 5.74 + 5.75 + elif not (type_desc.is_nullable or type_convert): 5.76 + raise ValueError(f'Передан "None" в поле "{fld.name}", хотя он не ожидался') 5.77 + 5.78 + params[fld.name] = val 5.79 + 5.80 + try: 5.81 + res = dataclass_type(**params) 5.82 + 5.83 + except (ValueError, TypeError) as e: 5.84 + _params = ', '.join(map(lambda x: f'{x[0]}="{x[1]}"', params.items())) 5.85 + raise ValueError(f'Не удалось получить объект' 5.86 + f' класс {dataclass_type.__name__}' 5.87 + f' из параметров: {_params}' 5.88 + f' ошибка: {e}') 5.89 + 5.90 + return res 5.91 + 5.92 +
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 6.2 +++ b/type_utils/json_tools.py Sat Aug 20 23:56:16 2022 +0300 6.3 @@ -0,0 +1,25 @@ 6.4 +# coding: utf-8 6.5 +from dataclasses import is_dataclass, asdict 6.6 + 6.7 + 6.8 +def json_type_sanitizer(val): 6.9 + """\ 6.10 + Преобразует значение ``val`` в пригодное для преобразования в json значение. 6.11 + """ 6.12 + 6.13 + val_t = type(val) 6.14 + 6.15 + if is_dataclass(val): 6.16 + return json_type_sanitizer(asdict(val)) 6.17 + 6.18 + elif val_t in (int, float, str, bool) or val is None: 6.19 + return val 6.20 + 6.21 + elif val_t in (list, tuple): 6.22 + return list(map(json_type_sanitizer, val)) 6.23 + 6.24 + elif val_t == dict: 6.25 + return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items()) 6.26 + 6.27 + else: 6.28 + return str(val)
7.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 7.2 +++ b/type_utils/type_descriptor.py Sat Aug 20 23:56:16 2022 +0300 7.3 @@ -0,0 +1,367 @@ 7.4 +# coding: utf-8 7.5 + 7.6 +class TypeDescriptorInterface: 7.7 + """\ 7.8 + Определяем общий интерфейс для описателей типов 7.9 + """ 7.10 + def __init__(self, name, type_class, is_nullable=False): 7.11 + self.__name__ = name 7.12 + self.t_class = type_class 7.13 + self.is_nullable = is_nullable 7.14 + self.event_description = '' 7.15 + self.union_sort_order = 3 7.16 + 7.17 + def raise_nullable(self, msg=''): 7.18 + msg = msg if not msg else f': {msg}' 7.19 + 7.20 + if not self.is_nullable: 7.21 + raise ValueError(f'Передача значения "None" переменной, которая этого не ожидает{msg}') 7.22 + 7.23 + def __call__(self, val): 7.24 + """\ 7.25 + Реализация приведения типа 7.26 + """ 7.27 + raise NotImplemented() 7.28 + 7.29 + def __repr__(self): 7.30 + if self.t_class is None: 7.31 + return self.__name__ 7.32 + 7.33 + else: 7.34 + return f'{self.__name__}({self.t_class.__name__})' 7.35 + 7.36 + def check(self, val): 7.37 + """\ 7.38 + Реализация проверки соответствия типу 7.39 + """ 7.40 + raise NotImplemented() 7.41 + 7.42 + 7.43 +class BaseTypeDescriptor(TypeDescriptorInterface): 7.44 + """\ 7.45 + Базовый класс, объявляющий общий интерфейс классов адаптеров проверки и преобразования типов 7.46 + """ 7.47 + def check(self, val): 7.48 + event_description = '' 7.49 + res = False 7.50 + if val is None: 7.51 + event_description = 'Передано значение "None"' 7.52 + res = self.is_nullable 7.53 + 7.54 + elif isinstance(self.t_class, TypeDescriptorInterface): 7.55 + res = self.t_class.check(val) 7.56 + event_description = self.t_class.event_description 7.57 + 7.58 + else: 7.59 + event_description = f'Требуется тип "{self.t_class.__name__}" а получаем "{type(val).__name__}"' 7.60 + res = isinstance(val, self.t_class) 7.61 + 7.62 + if not res: 7.63 + self.event_description = event_description 7.64 + 7.65 + return res 7.66 + 7.67 + def check_fail(self, msg): 7.68 + self.event_description = msg 7.69 + return False 7.70 + 7.71 + 7.72 +class ScalarTypeDescriptor(BaseTypeDescriptor): 7.73 + """\ 7.74 + Реализация адаптера над простыми типами 7.75 + """ 7.76 + def __init__(self, type_class, is_nullable=False): 7.77 + super().__init__(type_class.__name__, type_class, is_nullable) 7.78 + if type_class in (int, float, bool): 7.79 + self.union_sort_order = 0 7.80 + 7.81 + elif type_class == str: 7.82 + self.union_sort_order = 2 7.83 + 7.84 + else: 7.85 + self.union_sort_order = 1 7.86 + 7.87 + def __call__(self, val): 7.88 + if val is None: 7.89 + return self.raise_nullable() 7.90 + 7.91 + if self.check(val): 7.92 + return val 7.93 + 7.94 + return self.t_class(val) 7.95 + 7.96 + 7.97 +class IterableTypeDescriptor(BaseTypeDescriptor): 7.98 + """\ 7.99 + Реализация адаптера над последовательностями 7.100 + """ 7.101 + def __init__(self, iterator_class, value_class, is_nullable=False): 7.102 + super().__init__( 7.103 + name=f'{iterator_class.__name__}[{value_class.__name__}]', 7.104 + type_class=value_class, 7.105 + is_nullable=is_nullable 7.106 + ) 7.107 + 7.108 + self.iterator_class = iterator_class 7.109 + 7.110 + def check(self, vals): 7.111 + idx = 0 7.112 + for val in vals: 7.113 + if not super().check(val): 7.114 + return self.check_fail(f'Элемент {idx}: {self.event_description}') 7.115 + 7.116 + else: 7.117 + idx += 1 7.118 + 7.119 + return True 7.120 + 7.121 + def __call__(self, vals): 7.122 + if vals is None: 7.123 + return self.raise_nullable() 7.124 + 7.125 + res = [] 7.126 + for val in vals: 7.127 + if val is None: 7.128 + self.raise_nullable(f'Элемент "{len(res)}"') 7.129 + 7.130 + try: 7.131 + res.append(self.t_class(val)) 7.132 + 7.133 + except (TypeError, ValueError) as e: 7.134 + raise ValueError(f'Не удалось преобразовать элемент "{len(res)}" со значением "{val}" ' 7.135 + f'в результирующий тип: {e}') 7.136 + 7.137 + return self.iterator_class(res) 7.138 + 7.139 + 7.140 +class TupleTypeDescriptor(BaseTypeDescriptor): 7.141 + """\ 7.142 + Адаптер над кортежами 7.143 + """ 7.144 + def __init__(self, type_classes, is_nullable=False): 7.145 + if not isinstance(type_classes, (list, tuple)) or not type_classes: 7.146 + raise TypeError(f'В конструктор прокси для обработки кортежей не передано типов кортежа') 7.147 + 7.148 + names = ', '.join([i.__name__ for i in type_classes]) 7.149 + 7.150 + super().__init__(f'Tuple[{names}]', None, is_nullable) 7.151 + 7.152 + self.type_classes = tuple(map( 7.153 + lambda x: x if isinstance(x, TypeDescriptorInterface) else ScalarTypeDescriptor(x), 7.154 + type_classes 7.155 + )) 7.156 + 7.157 + def check(self, vals): 7.158 + if not isinstance(vals, tuple): 7.159 + return self.check_fail(f'Переданная переменная не является кортежем, а относится к типу: ' 7.160 + f'{type(vals).__name__}') 7.161 + 7.162 + if len(vals) != len(self.type_classes): 7.163 + return self.check_fail(f'Не достаточно элементов в кортеже: ' 7.164 + f'имеется={len(vals)} нужно={len(self.type_classes)}') 7.165 + 7.166 + for i in range(len(self.type_classes)): 7.167 + if not self.type_classes[i].check(vals[i]): 7.168 + return self.check_fail(f'Элемент {i}, значение "{vals[i]}": ' 7.169 + f'{self.type_classes[i].event_description}') 7.170 + 7.171 + def __call__(self, vals): 7.172 + if vals is None: 7.173 + return self.raise_nullable() 7.174 + 7.175 + if not isinstance(vals, tuple): 7.176 + raise ValueError(f'Переданная переменная не является кортежем, а относится к типу: ' 7.177 + f'{type(vals).__name__}') 7.178 + 7.179 + if len(vals) != len(self.type_classes): 7.180 + raise ValueError(f'Не достаточно элементов в кортеже: ' 7.181 + f'имеется={len(vals)} нужно={len(self.type_classes)}') 7.182 + 7.183 + res = [] 7.184 + for i in range(len(self.type_classes)): 7.185 + try: 7.186 + res.append(self.type_classes[i](vals[i])) 7.187 + 7.188 + except (ValueError, TypeError) as e: 7.189 + raise ValueError(f'Не удалось привести к результирующему виду ' 7.190 + f'элемент {i} со значением "{vals[i]}": {e}') 7.191 + 7.192 + return tuple(res) 7.193 + 7.194 + 7.195 +class UnionTypeDescriptor(BaseTypeDescriptor): 7.196 + """\ 7.197 + Адаптер для объединения типов 7.198 + """ 7.199 + def __init__(self, type_classes, is_nullable=False): 7.200 + if not isinstance(type_classes, (list, tuple)) or not type_classes: 7.201 + raise TypeError(f'В конструктор прокси для обработки объединений типов не передано типов') 7.202 + 7.203 + names = ', '.join([i.__name__ for i in type_classes]) 7.204 + 7.205 + super().__init__(f'Union[{names}]', None, is_nullable) 7.206 + 7.207 + self.type_classes = tuple(sorted(map( 7.208 + lambda x: x if isinstance(x, BaseTypeDescriptor) else ScalarTypeDescriptor(x), 7.209 + type_classes 7.210 + ), key=lambda x: x.union_sort_order)) 7.211 + 7.212 + def check(self, val): 7.213 + for t in self.type_classes: 7.214 + if t.check(val): 7.215 + return True 7.216 + 7.217 + self.event_description = f'Значение "{val}" типа "{type(val).__name__}" не соответствует ' \ 7.218 + f'ни одному типу из моего набора' 7.219 + return False 7.220 + 7.221 + def __call__(self, val): 7.222 + if val is None: 7.223 + return self.raise_nullable() 7.224 + 7.225 + if self.check(val): 7.226 + return val 7.227 + 7.228 + errs = [] 7.229 + 7.230 + for t in self.type_classes: 7.231 + try: 7.232 + return t(val) 7.233 + 7.234 + except (TypeError, ValueError) as e: 7.235 + errs.append(f'{repr(t)}: {e}') 7.236 + 7.237 + raise ValueError(f'Не удалось преобразовать значение "{val}" типа "{type(val).__name__}" ' 7.238 + f'ни к одному из имеющихся типов: ' + '\n'.join(errs)) 7.239 + 7.240 + 7.241 +class DictTypeDescriptor(BaseTypeDescriptor): 7.242 + """\ 7.243 + Адаптер словарей 7.244 + """ 7.245 + def __init__(self, key_class, value_class, is_nullable=False): 7.246 + super().__init__(f'Dict[{key_class.__name__}, {value_class.__name__}]', None, is_nullable) 7.247 + 7.248 + if isinstance(key_class, BaseTypeDescriptor): 7.249 + self.key_class = key_class 7.250 + 7.251 + else: 7.252 + self.key_class = ScalarTypeDescriptor(key_class) 7.253 + 7.254 + if isinstance(value_class, BaseTypeDescriptor): 7.255 + self.value_class = value_class 7.256 + 7.257 + else: 7.258 + self.value_class = ScalarTypeDescriptor(value_class) 7.259 + 7.260 + def check(self, val): 7.261 + try: 7.262 + d = dict(val) 7.263 + 7.264 + except (TypeError, ValueError) as e: 7.265 + self.event_description = f'Не удалось преобразовать переданное значение в словарь: {e}' 7.266 + return False 7.267 + 7.268 + for k, v in d.items(): 7.269 + if not self.key_class.check(k): 7.270 + return self.check_fail(f'В паре ["{k}": "{v}"] ключ не соответствует ожидаемому типу: ' 7.271 + f'{self.key_class.event_description}') 7.272 + 7.273 + if not self.value_class.check(v): 7.274 + return self.check_fail(f'В паре ["{k}": "{v}"] значение не соответствует ожидаемому типу: ' 7.275 + f'{self.value_class.event_description}') 7.276 + 7.277 + return True 7.278 + 7.279 + def __call__(self, val): 7.280 + try: 7.281 + d = dict(val) 7.282 + 7.283 + except (TypeError, ValueError) as e: 7.284 + raise ValueError(f'Не удалось преобразовать переданное значение в словарь: {e}') 7.285 + 7.286 + p = [] 7.287 + for k, v in d.items(): 7.288 + p.append((self.key_class(k), self.value_class(v))) 7.289 + 7.290 + return dict(p) 7.291 + 7.292 + 7.293 +def get_type_describer(t) -> BaseTypeDescriptor: 7.294 + if type(t).__name__ == '_GenericAlias': 7.295 + try: 7.296 + _args = t.__args__ 7.297 + 7.298 + except AttributeError: 7.299 + raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' 7.300 + f'типа "_GenericAlias": {t}') 7.301 + 7.302 + if t.__name__ == 'List': 7.303 + try: 7.304 + _t = _args[0] 7.305 + 7.306 + except IndexError: 7.307 + raise ValueError(f'Тип {t} не содержит в себе типа своих элементов') 7.308 + 7.309 + return IterableTypeDescriptor( 7.310 + iterator_class=list, 7.311 + value_class=get_type_describer(_t) 7.312 + ) 7.313 + 7.314 + elif t.__name__ == 'Tuple': 7.315 + if not _args: 7.316 + raise ValueError(f'Тип {t} не содержит в себе пояснений по составу хранящихся в нём типов') 7.317 + 7.318 + if len(_args) == 1: 7.319 + _t = _args[0] 7.320 + 7.321 + return IterableTypeDescriptor( 7.322 + iterator_class=tuple, 7.323 + value_class=get_type_describer(_t) 7.324 + ) 7.325 + 7.326 + else: 7.327 + return TupleTypeDescriptor( 7.328 + type_classes=_args 7.329 + ) 7.330 + 7.331 + elif t.__name__ == 'Dict': 7.332 + if len(_args) != 2: 7.333 + raise ValueError(f'Неожиданное количество значений типа в составном типе Dict: ' 7.334 + f'{len(_args)} values=({_args})') 7.335 + 7.336 + return DictTypeDescriptor( 7.337 + key_class=get_type_describer(_args[0]), 7.338 + value_class=get_type_describer(_args[1]) 7.339 + ) 7.340 + 7.341 + else: 7.342 + raise ValueError(f'Неизвестный представитель типа "_GenericAlias": {t.__name__}') 7.343 + 7.344 + elif type(t).__name__ == '_UnionGenericAlias': 7.345 + if t.__name__ not in ('Union', 'Optional'): 7.346 + raise TypeError(f'Неизвестный подтип _UnionGenericAlias: {t.__name__}') 7.347 + 7.348 + nullable = False 7.349 + if t.__name__ == 'Optional': 7.350 + nullable = True 7.351 + 7.352 + try: 7.353 + _args = t.__args__ 7.354 + 7.355 + except AttributeError: 7.356 + raise TypeError(f'Неизвестный тип хранения внутренних типов для представителя сложного ' 7.357 + f'типа "_UnionGenericAlias": {t}') 7.358 + 7.359 + if len(_args) == 0: 7.360 + raise ValueError('Не указан ни один тип в конструкции Union') 7.361 + 7.362 + type_classes = tuple(map(lambda x: get_type_describer(x), [ i for i in _args if i is not None])) 7.363 + 7.364 + return UnionTypeDescriptor( 7.365 + type_classes=type_classes, 7.366 + is_nullable=nullable 7.367 + ) 7.368 + 7.369 + else: 7.370 + return ScalarTypeDescriptor(type_class=t)