def _get_parsing_plan_for_multifile_children( self, obj_on_fs: PersistedObject, desired_type: Type[Any], logger: Logger) -> Dict[str, Any]: """ Simply inspects the required type to find the names and types of its constructor arguments. Then relies on the inner ParserFinder to parse each of them. :param obj_on_fs: :param desired_type: :param logger: :return: """ if is_collection(desired_type, strict=True): # if the destination type is 'strictly a collection' (not a subclass of a collection) we know that we can't # handle it here, the constructor is not pep484-typed raise TypeError( 'Desired object type \'' + get_pretty_type_str(desired_type) + '\' is a collection, ' 'so it cannot be parsed with this default object parser') else: # First get the file children children_on_fs = obj_on_fs.get_multifile_children() # Try the type itself # try: return self.__get_parsing_plan_for_multifile_children( obj_on_fs, desired_type, children_on_fs, logger=logger)
def _get_parsing_plan_for_multifile_children(self, obj_on_fs: PersistedObject, desired_type: Type[Any], logger: Logger) -> Dict[str, Any]: """ Simply simply inspects the required type to find the names and types of its constructor arguments. Then relies on the inner ParserFinder to parse each of them. :param obj_on_fs: :param desired_type: :param logger: :return: """ if is_collection(desired_type, strict=True): raise TypeError('Desired object type \'' + get_pretty_type_str(desired_type)+ '\' is a collection, ' 'so it cannot be parsed with this default object parser') # First get the file children children_on_fs = obj_on_fs.get_multifile_children() # -- (a) extract the schema from the class constructor constructor_args_types_and_opt = get_constructor_attributes_types(desired_type) # -- (b) plan to parse each attribute required by the constructor children_plan = dict() # results will be put in this object # --use sorting in order to lead to reproducible results in case of multiple errors for attribute_name, att_desc in sorted(constructor_args_types_and_opt.items()): attribute_is_mandatory = att_desc[1] attribute_type = att_desc[0] # get the child if attribute_name in children_on_fs.keys(): child_on_fs = children_on_fs[attribute_name] # find a parser parser_found = self.parser_finder.build_parser_for_fileobject_and_desiredtype(child_on_fs, attribute_type, logger=logger) # create a parsing plan children_plan[attribute_name] = parser_found.create_parsing_plan(attribute_type, child_on_fs, logger=logger) else: if attribute_is_mandatory: raise MissingMandatoryAttributeFiles.create(obj_on_fs, desired_type, attribute_name) else: # we don't care : optional attribute # dont use warning since it does not show up nicely #print('----- WARNING: Attribute ' + attribute_name + ' was not found on file system. However ' # 'it is not mandatory for the constructor of type ' + get_pretty_type_str(desired_type) # + ', so we\'ll build the object without it...') logger.warning('----- Attribute ' + attribute_name + ' was not found on file system. However ' 'it is not mandatory for the constructor of type ' + get_pretty_type_str(desired_type) + ', so we\'ll build the object without it...') pass return children_plan
def is_able_to_parse(self, desired_type: Type[Any], desired_ext: str, strict: bool): """ Explicitly declare that we are not able to parse collections :param desired_type: :param desired_ext: :param strict: :return: """ if is_collection(desired_type, strict=True): return False, None else: return super(MultifileObjectParser, self).is_able_to_parse(desired_type, desired_ext, strict)
def _not_able_to_convert_collections(strict: bool, from_type: Type[Any] = None, to_type: Type[Any] = None): """ Explicitly declare that we are not able to parse collections :param strict: :param from_type: :param to_type: :return: """ # here strict means 'strictly a collection' (allow subclasses) if to_type is not None and is_collection(to_type, strict=True): return False else: return True
def dict_to_object(desired_type: Type[T], contents_dict: Dict[str, Any], logger: Logger, options: Dict[str, Dict[str, Any]], conversion_finder: ConversionFinder = None, is_dict_of_dicts: bool = False) -> T: """ Utility method to create an object from a dictionary of constructor arguments. Constructor arguments that dont have the correct type are intelligently converted if possible :param desired_type: :param contents_dict: :param logger: :param options: :param conversion_finder: :param is_dict_of_dicts: :return: """ check_var(desired_type, var_types=type, var_name='obj_type') check_var(contents_dict, var_types=dict, var_name='contents_dict') if is_collection(desired_type, strict=True): # if the destination type is 'strictly a collection' (not a subclass of a collection) we know that we can't # handle it here, the constructor is not pep484-typed raise TypeError( 'Desired object type \'' + get_pretty_type_str(desired_type) + '\' is a collection, ' 'so it cannot be created using this generic object creator') else: # Try the type itself # try: return _dict_to_object(desired_type, contents_dict, logger=logger, options=options, conversion_finder=conversion_finder, is_dict_of_dicts=is_dict_of_dicts)
def test_is_collection(typ, strict, expected): assert is_collection(typ, strict=strict) == expected
def dict_to_object(desired_type: Type[T], contents_dict: Dict[str, Any], logger: Logger, options: Dict[str, Dict[str, Any]], conversion_finder: ConversionFinder = None, is_dict_of_dicts: bool = False) -> T: """ Utility method to create an object from a dictionary of constructor arguments. Constructor arguments that dont have the correct type are intelligently converted if possible :param desired_type: :param contents_dict: :param logger: :param options: :param conversion_finder: :param is_dict_of_dicts: :return: """ check_var(desired_type, var_types=type, var_name='obj_type') check_var(contents_dict, var_types=dict, var_name='contents_dict') if is_collection(desired_type, strict=True): raise TypeError('Desired object type \'' + get_pretty_type_str(desired_type) + '\' is a collection, ' 'so it cannot be created using this generic object creator') constructor_args_types_and_opt = get_constructor_attributes_types(desired_type) try: # for each attribute, convert the types of its parsed values if required dict_for_init = dict() for attr_name, provided_attr_value in contents_dict.items(): # check if this attribute name is required by the constructor if attr_name in constructor_args_types_and_opt.keys(): # check the theoretical type wanted by the constructor attr_type_required = constructor_args_types_and_opt[attr_name][0] if not is_dict_of_dicts: if isinstance(attr_type_required, type): # this will not fail if type information is not present;the attribute will only be used 'as is' full_attr_name = get_pretty_type_str(desired_type) + '.' + attr_name dict_for_init[attr_name] = ConversionFinder.try_convert_value(conversion_finder, full_attr_name, provided_attr_value, attr_type_required, logger, options) else: warning('Constructor for type <' + get_pretty_type_str(desired_type) + '> has no PEP484 Type ' 'hint, trying to use the parsed value in the dict directly') dict_for_init[attr_name] = provided_attr_value else: # in that mode, the attribute value itself is a dict, so the attribute needs to be built from that # dict first if isinstance(provided_attr_value, dict): # recurse : try to build this attribute from the dictionary provided. We need to know the type # for this otherwise we wont be able to call the constructor :) if attr_type_required is Parameter.empty or not isinstance(attr_type_required, type): raise TypeInformationRequiredError.create_for_object_attributes(desired_type, attr_name) else: # we can build the attribute from the sub-dict dict_for_init[attr_name] = dict_to_object(attr_type_required, provided_attr_value, logger, options, conversion_finder=conversion_finder) else: raise ValueError('Error while trying to build object of type ' + str(desired_type) + ' from a ' 'dictionary of dictionaries. Entry \'' + attr_name + '\' is not a dictionary') else: if is_dict_of_dicts and attr_name is 'DEFAULT': # -- tolerate but ignore - this is probably due to a configparser # warning('Property name \'' + attr_name + '\' is not an attribute of the object constructor. <' # + get_pretty_type_str(desired_type) + '> constructor attributes are : ' # + list(set(constructor_args_types.keys()) - {'self'}) + '. However it is named DEFAULT') pass else: # the dictionary entry does not correspond to a valid attribute of the object raise InvalidAttributeNameForConstructorError.create(desired_type, list(set(constructor_args_types_and_opt.keys()) - {'self'}), attr_name) # create the object using its constructor try: return desired_type(**dict_for_init) except Exception as e: # Wrap into an Exception raise ObjectInstantiationException.create(desired_type, dict_for_init, e) except TypeError as e: raise CaughtTypeErrorDuringInstantiation.create(desired_type, contents_dict, e)
def __is_valid_for_dict_to_object_conversion(strict_mode: bool, from_type: Type, to_type: Type) -> bool: """ Returns true if the provided types are valid for dict_to_object conversion Explicitly declare that we are not able to parse collections nor able to create an object from a dictionary if the object's constructor is non correctly PEP484-specified. None should be treated as a Joker here (but we know that never from_type and to_type will be None at the same time) :param strict_mode: :param from_type: :param to_type: :return: """ # right now we're stuck with the default logger.. logr = default_logger if to_type is None or is_any_type(to_type): # explicitly handle the 'None' (joker) or 'any' type return True elif is_collection(to_type, strict=True): # if the destination type is 'strictly a collection' (not a subclass of a collection) we know that we can't # handle it here, the constructor is not pep484-typed return False else: # (1) Try the type itself try: # can we find enough pep-484 information in the constructor to be able to understand what is required ? get_constructor_attributes_types(to_type) return True except TypeInformationRequiredError as main_e: # # failed: we cant guess the required types of constructor arguments # if strict_mode: # # Warning and return NO # if should_display_warnings_for(to_type): # logr.warn('Object constructor signature for type {} does not allow parsyfiles to ' # 'automatically create instances from dict content. Caught {}: {}' # ''.format(get_pretty_type_str(to_type), type(main_e).__name__, main_e)) # return False # # # non-strict mode: (2) Check if any subclasses exist # subclasses = get_all_subclasses(to_type) # if len(subclasses) > GLOBAL_CONFIG.dict_to_object_subclass_limit: # logr.warn('WARNING: Type {} has {} subclasses, only {} will be tried by parsyfiles when attempting to ' # 'create it from a subclass. You can raise this limit by setting the appropriate option with ' # '`parsyfiles_global_config()`' # ''.format(to_type, len(subclasses), GLOBAL_CONFIG.dict_to_object_subclass_limit)) # # # Then for each subclass also try (with a configurable limit in nb of subclasses) # for subclass in subclasses[0:GLOBAL_CONFIG.dict_to_object_subclass_limit]: # try: # get_constructor_attributes_types(subclass) # # OK, but issue warning for the root type still # if should_display_warnings_for(to_type): # logr.warn('WARNING: Object constructor signature for type {} does not allow parsyfiles to ' # 'automatically create instances from dict content, but it can for at least one of ' # 'its subclasses ({}) so it might be ok for you. Caught {}: {}' # ''.format(get_pretty_type_str(to_type), get_pretty_type_str(subclass), # type(main_e).__name__, main_e)) # return True # except TypeInformationRequiredError as e: # # failed: we cant guess the required types of constructor arguments # if should_display_warnings_for(to_type): # logr.warn('WARNING: Object constructor signature for type {} does not allow parsyfiles to ' # 'automatically create instances from dict content. Caught {}: {}' # ''.format(subclass, type(e).__name__, e)) # # # Nothing succeeded if should_display_warnings_for(to_type): logr.warn( 'WARNING: Object constructor signature for type {} does not allow parsyfiles to ' 'automatically create instances from dict content. Caught {}: {}' ''.format(get_pretty_type_str(to_type), type(main_e).__name__, main_e)) return False