def _mapping_to_instance(mapping: Mapping, destination_type: Type[T], instantiator: Callable = None, expand_kwargs=True) -> T: """Convert a dictionary-like object into an instance of the given type This conversion is performed recursively. If the passed type class contains child structures, then the corresponding child keys of the dictionary will be mapped. If `instantiator` is provided, it will be called rather than simply calling `destination_type`. If `expand_kwargs` is True then the the parameters will be expended (i.e. **) when the `destination_type` / `instantiator` is called. """ import lightbus.config.structure hints = get_type_hints(destination_type, None, lightbus.config.structure.__dict__) parameters = {} if mapping is None: return None # Iterate through each key/type-hint pairing in the destination type for key, hint in hints.items(): value = mapping.get(key) if key not in mapping: # This attribute has not been provided by in the mapping. Skip it continue hint_type, hint_args = parse_hint(hint) # Is this an Optional[] hint (which looks like Union[Thing, None]) if is_optional(hint) and value is not None: # Yep, it's an Optional. So unwrap it. hint = hint_args[0] if issubclass_safe(hint_type, Mapping) and hint_args and len(hint_args) == 2: parameters[key] = dict() for k, v in value.items(): parameters[key][k] = cast_to_hint(v, hint_args[1]) else: parameters[key] = cast_to_hint(value, hint) instantiator = instantiator or destination_type if expand_kwargs: return instantiator(**parameters) else: return instantiator(parameters)
def mapping_to_named_tuple(mapping: Mapping, named_tuple: Type[T]) -> T: """Convert a dictionary-like object into the given named tuple This conversion is performed recursively. If the passed named tuple class contains child named tuples, then the the corresponding child keys of the dictionary will be mapped. This is used to take the supplied configuration and load it into the expected configuration structures. """ import lightbus.config.structure hints = get_type_hints(named_tuple, None, lightbus.config.structure.__dict__) parameters = {} if mapping is None: return None for key, hint in hints.items(): is_class = inspect.isclass(hint) value = mapping.get(key) if key not in mapping: continue # Is this an Optional[] hint (which looks like Union[Thing, None]) subs_tree = hint._subs_tree() if hasattr(hint, "_subs_tree") else None if is_optional(hint) and value is not None: hint = subs_tree[1] if type_is_namedtuple(hint): parameters[key] = mapping_to_named_tuple(value, hint) elif is_class and issubclass( hint, Mapping) and subs_tree and len(subs_tree) == 3: parameters[key] = dict() for k, v in value.items(): parameters[key][k] = mapping_to_named_tuple( v, hint._subs_tree()[2]) else: parameters[key] = cast_to_hint(value, hint) return named_tuple(**parameters)
def cast_to_hint(value: V, hint: H) -> Union[V, H]: """Cast a value into a given type hint If a value cannot be cast then the original value will be returned and a warning emitted """ # pylint: disable=too-many-return-statements if value is None: return None elif hint in (Any, ...): return value optional_hint = is_optional(hint) if optional_hint and value is not None: hint = optional_hint hint_type, hint_args = parse_hint(hint) is_class = inspect.isclass(hint_type) if hint_type == Union: # We don't attempt to deal with unions for now return value elif hint == inspect.Parameter.empty: # Empty annotation return value elif isinstance_safe(value, hint): # Already correct type return value elif hasattr(hint, "__from_bus__"): # Hint supports custom deserializing. return _mapping_to_instance( mapping=value, destination_type=hint, instantiator=hint.__from_bus__, expand_kwargs=False, ) elif issubclass_safe(hint, bytes): return b64decode(value.encode("utf8")) elif type_is_namedtuple(hint) and isinstance_safe(value, Mapping): return _mapping_to_instance(mapping=value, destination_type=hint) elif type_is_dataclass(hint) and isinstance_safe(value, Mapping): return _mapping_to_instance(mapping=value, destination_type=hint) elif is_class and issubclass_safe( hint_type, datetime.datetime) and isinstance_safe(value, str): # Datetime as a string return dateutil.parser.parse(value) elif is_class and issubclass_safe( hint_type, datetime.date) and isinstance_safe(value, str): # Date as a string return cast_or_warning(lambda v: dateutil.parser.parse(v).date(), value) elif is_class and issubclass_safe(hint_type, list): # Lists if hint_args and hasattr(value, "__iter__"): value = [cast_to_hint(i, hint_args[0]) for i in value] return cast_or_warning(list, value) elif is_class and issubclass_safe(hint_type, tuple): # Tuples if hint_args and hasattr(value, "__iter__"): value = [ cast_to_hint(h, hint_args[i]) for i, h in enumerate(value) ] return cast_or_warning(tuple, value) elif is_class and issubclass_safe(hint_type, set): # Sets if hint_args and hasattr(value, "__iter__"): value = [cast_to_hint(i, hint_args[0]) for i in value] return cast_or_warning(set, value) elif (inspect.isclass(hint) and hasattr(hint, "__annotations__") and not issubclass_safe(hint_type, Enum)): logger.warning( f"Cannot cast to arbitrary class {hint}, using un-casted value. " f"If you want to receive custom objects you can 1) " f"use a NamedTuple, 2) use a dataclass, or 3) specify the " f"__from_bus__() and __to_bus__() magic methods.") return value else: return cast_or_warning(hint, value)
def cast_to_hint(value: V, hint: H) -> Union[V, H]: optional_hint = is_optional(hint) if optional_hint and value is not None: hint = optional_hint subs_tree = hint._subs_tree() if hasattr(hint, "_subs_tree") else None subs_tree = subs_tree if isinstance(subs_tree, tuple) else None is_class = inspect.isclass(hint) if type(hint) == type(Union): # We don't attempt to deal with unions for now return value elif hint == inspect.Parameter.empty: # Empty annotation return value elif isinstance_safe(value, hint): # Already correct type return value elif hasattr(hint, "__from_bus__"): # Hint supports custom deserializing. return hint.__from_bus__(value) elif type_is_namedtuple(hint) and isinstance_safe(value, Mapping): return mapping_to_named_tuple(mapping=value, named_tuple=hint) elif type_is_dataclass(hint) and isinstance_safe(value, Mapping): # We can treat dataclasses the same as named tuples return mapping_to_named_tuple(mapping=value, named_tuple=hint) elif is_class and issubclass(hint, datetime.datetime) and isinstance_safe( value, str): # Datetime as a string return dateutil.parser.parse(value) elif is_class and issubclass(hint, datetime.date) and isinstance_safe( value, str): # Date as a string return dateutil.parser.parse(value).date() elif is_class and issubclass(hint, list): # Lists if subs_tree: return [cast_to_hint(i, subs_tree[1]) for i in value] else: return list(value) elif is_class and issubclass(hint, tuple): # Tuples if subs_tree: return tuple(cast_to_hint(i, subs_tree[1]) for i in value) else: return tuple(value) elif inspect.isclass(hint) and hasattr( hint, "__annotations__") and not issubclass(hint, Enum): logger.warning( f"Cannot cast to arbitrary class {hint}, using un-casted value. " f"If you want to receive custom objects you can 1) " f"use a NamedTuple, 2) use a dataclass, or 3) specify the " f"__from_bus__() and __to_bus__() magic methods.") return value else: try: return hint(value) except Exception as e: logger.warning( f"Failed to cast value {repr(value)} to type {hint}. Will " f"continue without casting, but this may cause errors in any " f"called code. Error was: {e}") return value