Module osbot_utils.utils.Objects

Expand source code
# todo add tests
import inspect
import io
import json
import os
import pickle
import sys
import types
import typing
from typing import Union

from osbot_utils.utils.Misc import list_set
from osbot_utils.utils.Str  import str_unicode_escape, str_max_width

# Backport implementations of get_origin and get_args for Python 3.7
if sys.version_info < (3, 8):
    def get_origin(tp):
        if isinstance(tp, typing._GenericAlias):
            return tp.__origin__
        elif tp is typing.Generic:
            return typing.Generic
        else:
            return None

    def get_args(tp):
        if isinstance(tp, typing._GenericAlias):
            return tp.__args__
        else:
            return ()
else:
    from typing import get_origin, get_args


def are_types_compatible_for_assigment(source_type, target_type):
    if source_type is target_type:
        return True
    if source_type is int and target_type is float:
        return True
    if target_type in source_type.__mro__:          # this means that the source_type has the target_type has of its base types
        return True

    return False

def are_types_magic_mock(source_type, target_type):
    from unittest.mock import MagicMock
    if isinstance(source_type, MagicMock):
        return True
    if isinstance(target_type, MagicMock):
        return True
    if source_type is MagicMock:
        return True
    if target_type is MagicMock:
        return True
    # if class_full_name(source_type) == 'unittest.mock.MagicMock':
    #     return True
    # if class_full_name(target_type) == 'unittest.mock.MagicMock':
    #     return True
    return False

def base_classes(cls):
    if type(cls) is type:
        target = cls
    else:
        target = type(cls)
    return type_base_classes(target)

def class_functions_names(target):
    return list_set(class_functions(target))

def class_functions(target):
    functions = {}
    for function_name, function_ref in inspect.getmembers(type(target), predicate=inspect.isfunction):
        functions[function_name] = function_ref
    return functions

def class_name(target):
    if target:
        return type(target).__name__

def class_full_name(target):
    if target:
        type_target = type(target)
        type_module = type_target.__module__
        type_name   = type_target.__name__
        return f'{type_module}.{type_name}'

def default_value(target : type):
    try:
        return target()                 #  try to create the object using the default constructor
    except TypeError:
        return None                     # if not return None

def dict_remove(data, target):
    if type(data) is dict:
        if type(target) is list:
            for key in list(data.keys()):
                if key in target:
                    del data[key]
        else:
            if target in data:
                del data[target]
    return data

def enum_from_value(enum_type, value):
    try:
        return enum_type[value]         # Attempt to convert the value to an Enum member by name
    except KeyError:
        raise ValueError(f"Value '{value}' is not a valid member of {enum_type.__name__}.")     # Handle the case where the value does not match any Enum member


def get_field(target, field, default=None):
    if target is not None:
        try:
            value = getattr(target, field)
            if value is not None:
                return value
        except:
            pass
    return default

def get_missing_fields(target,fields):
    missing_fields = []
    if fields:
        for field in fields:
            if get_field(target, field) is None:
                missing_fields.append(field)
    return missing_fields

def get_value(target, key, default=None):
    if target is not None:
        value = target.get(key)
        if value is not None:
            return value
    return default

def print_object_methods(target, name_width=30, value_width=100, show_private=False, show_internals=False):
    print_object_members(target, name_width=name_width, value_width=value_width,show_private=show_private,show_internals=show_internals, only_show_methods=True)

def print_obj_data_aligned(obj_data):
    print(obj_data_aligned(obj_data))

def print_obj_data_as_dict(target, **kwargs):
    data           = obj_data(target, **kwargs)
    indented_items = obj_data_aligned(data, tab_size=5)
    print("dict(" + indented_items + " )")
    return data

def obj_data_aligned(obj_data, tab_size=0):
    max_key_length = max(len(k) for k in obj_data.keys())                                 # Find the maximum key length
    items          = [f"{k:<{max_key_length}} = {v!r:6}," for k, v in obj_data.items()]   # Format each key-value pair
    items[-1]      = items[-1][:-2]                                                       # Remove comma from the last item
    tab_string = f"\n{' ' * tab_size }"                                                   # apply tabbing (if needed)
    indented_items = tab_string.join(items)                                               # Join the items with newline and
    return indented_items

# todo: add option to not show class methods that are not bultin types
def print_object_members(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False):
    max_width = name_width + value_width
    print()
    print(f"Members for object:\n\t {target} of type:{type(target)}")
    print(f"Settings:\n\t name_width: {name_width} | value_width: {value_width} | show_private: {show_private} | show_internals: {show_internals}")
    print()
    if only_show_methods:
        show_methods = True                                             # need to make sure this setting is True, or there will be no methods to show
        print(f"{'method':<{name_width}} (params)")
    else:
        if show_value_class:
            print(f"{'field':<{name_width}} | {'type':<{name_width}} |value")
        else:
            print(f"{'field':<{name_width}} | value")

    print(f"{'-' * max_width}")
    for name, value in obj_data(target, name_width=name_width, value_width=value_width, show_private=show_private, show_internals=show_internals, show_value_class=show_value_class, show_methods=show_methods, only_show_methods=only_show_methods).items():
        if only_show_methods:
            print(f"{name:<{name_width}} {value}"[:max_width])
        else:
            if show_value_class:
                value_class = obj_full_name(value)
                print(f"{name:<{name_width}} | {value_class:{name_width}} | {value}"[:max_width])
            else:
                print(f"{name:<{name_width}} | {value}"[:max_width])

def obj_base_classes(obj):
    return [obj_type for obj_type in type_base_classes(type(obj))]

def type_mro(target):
    if type(target) is type:
        cls = target
    else:
        cls = type(target)
    return list(inspect.getmro(cls))

def type_base_classes(cls):
    base_classes = cls.__bases__
    all_base_classes = list(base_classes)
    for base in base_classes:
        all_base_classes.extend(type_base_classes(base))
    return all_base_classes

def obj_base_classes_names(obj, show_module=False):
    names = []
    for base in obj_base_classes(obj):
        if show_module:
            names.append(base.__module__ + '.' + base.__name__)
        else:
            names.append(base.__name__)
    return names

def obj_data(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False):
    result = {}
    if show_internals:
        show_private = True                                     # show_private will skip all internals, so need to make sure it is True
    for name, value in inspect.getmembers(target):
        if show_methods is False and type(value) is types.MethodType:
            continue
        if only_show_methods and type(value) is not types.MethodType:
            continue
        if not show_private and name.startswith("_"):
            continue
        if not show_internals and name.startswith("__"):
            continue
        if only_show_methods:
            value = inspect.signature(value)
        if value !=None and type(value) not in [bool, int, float]:
            value       = str(value).encode('unicode_escape').decode("utf-8")
            value       = str_unicode_escape(value)
            value       = str_max_width(value, value_width)
            name        = str_max_width(name, name_width)
        result[name] = value
    return result

# def obj_data(target=None):
#     data = {}
#     for key,value in obj_items(target):
#         data[key] = value
#     return data

def obj_dict(target=None):
    if target and hasattr(target,'__dict__'):
        return target.__dict__
    return {}

def obj_items(target=None):
    return sorted(list(obj_dict(target).items()))

def obj_keys(target=None):
    return sorted(list(obj_dict(target).keys()))

def obj_full_name(target):
    module = target.__class__.__module__
    name   = target.__class__.__qualname__
    return f"{module}.{name}"

def obj_get_value(target=None, key=None, default=None):
    return get_field(target=target, field=key, default=default)

def obj_values(target=None):
    return list(obj_dict(target).values())

def raise_exception_on_obj_type_annotation_mismatch(target, attr_name, value):
    # todo : check if this is is not causing the type safety issues
    if value_type_matches_obj_annotation_for_attr(target, attr_name, value) is False:               # handle case with normal types
        if value_type_matches_obj_annotation_for_union_attr(target, attr_name, value) is True:      # handle union cases
            return                                                                                  #     this is done like this because value_type_matches_obj_annotation_for_union_attr will return None when there is no Union objects
        raise Exception(f"Invalid type for attribute '{attr_name}'. Expected '{target.__annotations__.get(attr_name)}' but got '{type(value)}'")

def obj_attribute_annotation(target, attr_name):
    if target is not None and attr_name is not None:
        if hasattr(target, '__annotations__'):
            obj_annotations  = target.__annotations__
            if hasattr(obj_annotations,'get'):
                attribute_annotation = obj_annotations.get(attr_name)
                return attribute_annotation
    return None

def obj_is_attribute_annotation_of_type(target, attr_name, expected_type):
    attribute_annotation = obj_attribute_annotation(target, attr_name)
    attribute_type       = type(attribute_annotation)
    return attribute_type is expected_type

def obj_is_type_union_compatible(var_type, compatible_types):
    origin = get_origin(var_type)
    if origin is Union:                                                     # For Union types, including Optionals
        args = get_args(var_type)                                           # Get the argument types
        for arg in args:                                                    # Iterate through each argument in the Union
            if not (arg in compatible_types or arg is type(None)):          # Check if the argument is either in the compatible_types or is type(None)
                return False                                                # If any arg doesn't meet the criteria, return False immediately
        return True                                                         # If all args are compatible, return True
    return var_type in compatible_types or var_type is type(None)           # Check for direct compatibility or type(None) for non-Union types

def value_type_matches_obj_annotation_for_union_attr(target, attr_name, value):
    value_type           = type(value)
    attribute_annotation = obj_attribute_annotation(target,attr_name)
    origin               = get_origin(attribute_annotation)
    if origin is Union:                                                     # For Union types, including Optionals
        args = get_args(attribute_annotation)                               # Get the argument types
        return value_type in args
    return None                                                             # if it is not an Union type just return None (to give an indication to the caller that the comparison was not made)


def pickle_save_to_bytes(target: object) -> bytes:
    return pickle.dumps(target)

def pickle_load_from_bytes(pickled_data: bytes):
    if type(pickled_data) is bytes:
        return pickle.loads(pickled_data)

def value_type_matches_obj_annotation_for_attr(target, attr_name, value):
    if hasattr(target, '__annotations__'):
        obj_annotations  = target.__annotations__
        if hasattr(obj_annotations,'get'):
            attr_type        = obj_annotations.get(attr_name)
            if attr_type:
                origin_attr_type = get_origin(attr_type)                # to handle when type definion contains an generic
                if origin_attr_type:
                    attr_type = origin_attr_type
                value_type = type(value)
                if are_types_compatible_for_assigment(source_type=value_type, target_type=attr_type):
                    return True
                if are_types_magic_mock(source_type=value_type, target_type=attr_type):
                    return True

                return value_type is attr_type
    return None





# helper duplicate methods
base_types          = base_classes

full_type_name      = class_full_name

obj_list_set        = obj_keys
obj_info            = print_object_members
obj_methods         = print_object_methods

obj_to_bytes        = pickle_save_to_bytes
bytes_to_obj        = pickle_load_from_bytes

type_full_name      = class_full_name

Functions

def are_types_compatible_for_assigment(source_type, target_type)
Expand source code
def are_types_compatible_for_assigment(source_type, target_type):
    if source_type is target_type:
        return True
    if source_type is int and target_type is float:
        return True
    if target_type in source_type.__mro__:          # this means that the source_type has the target_type has of its base types
        return True

    return False
def are_types_magic_mock(source_type, target_type)
Expand source code
def are_types_magic_mock(source_type, target_type):
    from unittest.mock import MagicMock
    if isinstance(source_type, MagicMock):
        return True
    if isinstance(target_type, MagicMock):
        return True
    if source_type is MagicMock:
        return True
    if target_type is MagicMock:
        return True
    # if class_full_name(source_type) == 'unittest.mock.MagicMock':
    #     return True
    # if class_full_name(target_type) == 'unittest.mock.MagicMock':
    #     return True
    return False
def base_classes(cls)
Expand source code
def base_classes(cls):
    if type(cls) is type:
        target = cls
    else:
        target = type(cls)
    return type_base_classes(target)
def base_types(cls)
Expand source code
def base_classes(cls):
    if type(cls) is type:
        target = cls
    else:
        target = type(cls)
    return type_base_classes(target)
def bytes_to_obj(pickled_data: bytes)
Expand source code
def pickle_load_from_bytes(pickled_data: bytes):
    if type(pickled_data) is bytes:
        return pickle.loads(pickled_data)
def class_full_name(target)
Expand source code
def class_full_name(target):
    if target:
        type_target = type(target)
        type_module = type_target.__module__
        type_name   = type_target.__name__
        return f'{type_module}.{type_name}'
def class_functions(target)
Expand source code
def class_functions(target):
    functions = {}
    for function_name, function_ref in inspect.getmembers(type(target), predicate=inspect.isfunction):
        functions[function_name] = function_ref
    return functions
def class_functions_names(target)
Expand source code
def class_functions_names(target):
    return list_set(class_functions(target))
def class_name(target)
Expand source code
def class_name(target):
    if target:
        return type(target).__name__
def default_value(target: type)
Expand source code
def default_value(target : type):
    try:
        return target()                 #  try to create the object using the default constructor
    except TypeError:
        return None                     # if not return None
def dict_remove(data, target)
Expand source code
def dict_remove(data, target):
    if type(data) is dict:
        if type(target) is list:
            for key in list(data.keys()):
                if key in target:
                    del data[key]
        else:
            if target in data:
                del data[target]
    return data
def enum_from_value(enum_type, value)
Expand source code
def enum_from_value(enum_type, value):
    try:
        return enum_type[value]         # Attempt to convert the value to an Enum member by name
    except KeyError:
        raise ValueError(f"Value '{value}' is not a valid member of {enum_type.__name__}.")     # Handle the case where the value does not match any Enum member
def full_type_name(target)
Expand source code
def class_full_name(target):
    if target:
        type_target = type(target)
        type_module = type_target.__module__
        type_name   = type_target.__name__
        return f'{type_module}.{type_name}'
def get_field(target, field, default=None)
Expand source code
def get_field(target, field, default=None):
    if target is not None:
        try:
            value = getattr(target, field)
            if value is not None:
                return value
        except:
            pass
    return default
def get_missing_fields(target, fields)
Expand source code
def get_missing_fields(target,fields):
    missing_fields = []
    if fields:
        for field in fields:
            if get_field(target, field) is None:
                missing_fields.append(field)
    return missing_fields
def get_value(target, key, default=None)
Expand source code
def get_value(target, key, default=None):
    if target is not None:
        value = target.get(key)
        if value is not None:
            return value
    return default
def obj_attribute_annotation(target, attr_name)
Expand source code
def obj_attribute_annotation(target, attr_name):
    if target is not None and attr_name is not None:
        if hasattr(target, '__annotations__'):
            obj_annotations  = target.__annotations__
            if hasattr(obj_annotations,'get'):
                attribute_annotation = obj_annotations.get(attr_name)
                return attribute_annotation
    return None
def obj_base_classes(obj)
Expand source code
def obj_base_classes(obj):
    return [obj_type for obj_type in type_base_classes(type(obj))]
def obj_base_classes_names(obj, show_module=False)
Expand source code
def obj_base_classes_names(obj, show_module=False):
    names = []
    for base in obj_base_classes(obj):
        if show_module:
            names.append(base.__module__ + '.' + base.__name__)
        else:
            names.append(base.__name__)
    return names
def obj_data(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False)
Expand source code
def obj_data(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False):
    result = {}
    if show_internals:
        show_private = True                                     # show_private will skip all internals, so need to make sure it is True
    for name, value in inspect.getmembers(target):
        if show_methods is False and type(value) is types.MethodType:
            continue
        if only_show_methods and type(value) is not types.MethodType:
            continue
        if not show_private and name.startswith("_"):
            continue
        if not show_internals and name.startswith("__"):
            continue
        if only_show_methods:
            value = inspect.signature(value)
        if value !=None and type(value) not in [bool, int, float]:
            value       = str(value).encode('unicode_escape').decode("utf-8")
            value       = str_unicode_escape(value)
            value       = str_max_width(value, value_width)
            name        = str_max_width(name, name_width)
        result[name] = value
    return result
def obj_data_aligned(obj_data, tab_size=0)
Expand source code
def obj_data_aligned(obj_data, tab_size=0):
    max_key_length = max(len(k) for k in obj_data.keys())                                 # Find the maximum key length
    items          = [f"{k:<{max_key_length}} = {v!r:6}," for k, v in obj_data.items()]   # Format each key-value pair
    items[-1]      = items[-1][:-2]                                                       # Remove comma from the last item
    tab_string = f"\n{' ' * tab_size }"                                                   # apply tabbing (if needed)
    indented_items = tab_string.join(items)                                               # Join the items with newline and
    return indented_items
def obj_dict(target=None)
Expand source code
def obj_dict(target=None):
    if target and hasattr(target,'__dict__'):
        return target.__dict__
    return {}
def obj_full_name(target)
Expand source code
def obj_full_name(target):
    module = target.__class__.__module__
    name   = target.__class__.__qualname__
    return f"{module}.{name}"
def obj_get_value(target=None, key=None, default=None)
Expand source code
def obj_get_value(target=None, key=None, default=None):
    return get_field(target=target, field=key, default=default)
def obj_info(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False)
Expand source code
def print_object_members(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False):
    max_width = name_width + value_width
    print()
    print(f"Members for object:\n\t {target} of type:{type(target)}")
    print(f"Settings:\n\t name_width: {name_width} | value_width: {value_width} | show_private: {show_private} | show_internals: {show_internals}")
    print()
    if only_show_methods:
        show_methods = True                                             # need to make sure this setting is True, or there will be no methods to show
        print(f"{'method':<{name_width}} (params)")
    else:
        if show_value_class:
            print(f"{'field':<{name_width}} | {'type':<{name_width}} |value")
        else:
            print(f"{'field':<{name_width}} | value")

    print(f"{'-' * max_width}")
    for name, value in obj_data(target, name_width=name_width, value_width=value_width, show_private=show_private, show_internals=show_internals, show_value_class=show_value_class, show_methods=show_methods, only_show_methods=only_show_methods).items():
        if only_show_methods:
            print(f"{name:<{name_width}} {value}"[:max_width])
        else:
            if show_value_class:
                value_class = obj_full_name(value)
                print(f"{name:<{name_width}} | {value_class:{name_width}} | {value}"[:max_width])
            else:
                print(f"{name:<{name_width}} | {value}"[:max_width])
def obj_is_attribute_annotation_of_type(target, attr_name, expected_type)
Expand source code
def obj_is_attribute_annotation_of_type(target, attr_name, expected_type):
    attribute_annotation = obj_attribute_annotation(target, attr_name)
    attribute_type       = type(attribute_annotation)
    return attribute_type is expected_type
def obj_is_type_union_compatible(var_type, compatible_types)
Expand source code
def obj_is_type_union_compatible(var_type, compatible_types):
    origin = get_origin(var_type)
    if origin is Union:                                                     # For Union types, including Optionals
        args = get_args(var_type)                                           # Get the argument types
        for arg in args:                                                    # Iterate through each argument in the Union
            if not (arg in compatible_types or arg is type(None)):          # Check if the argument is either in the compatible_types or is type(None)
                return False                                                # If any arg doesn't meet the criteria, return False immediately
        return True                                                         # If all args are compatible, return True
    return var_type in compatible_types or var_type is type(None)           # Check for direct compatibility or type(None) for non-Union types
def obj_items(target=None)
Expand source code
def obj_items(target=None):
    return sorted(list(obj_dict(target).items()))
def obj_keys(target=None)
Expand source code
def obj_keys(target=None):
    return sorted(list(obj_dict(target).keys()))
def obj_list_set(target=None)
Expand source code
def obj_keys(target=None):
    return sorted(list(obj_dict(target).keys()))
def obj_methods(target, name_width=30, value_width=100, show_private=False, show_internals=False)
Expand source code
def print_object_methods(target, name_width=30, value_width=100, show_private=False, show_internals=False):
    print_object_members(target, name_width=name_width, value_width=value_width,show_private=show_private,show_internals=show_internals, only_show_methods=True)
def obj_to_bytes(target: object) ‑> bytes
Expand source code
def pickle_save_to_bytes(target: object) -> bytes:
    return pickle.dumps(target)
def obj_values(target=None)
Expand source code
def obj_values(target=None):
    return list(obj_dict(target).values())
def pickle_load_from_bytes(pickled_data: bytes)
Expand source code
def pickle_load_from_bytes(pickled_data: bytes):
    if type(pickled_data) is bytes:
        return pickle.loads(pickled_data)
def pickle_save_to_bytes(target: object) ‑> bytes
Expand source code
def pickle_save_to_bytes(target: object) -> bytes:
    return pickle.dumps(target)
def print_obj_data_aligned(obj_data)
Expand source code
def print_obj_data_aligned(obj_data):
    print(obj_data_aligned(obj_data))
def print_obj_data_as_dict(target, **kwargs)
Expand source code
def print_obj_data_as_dict(target, **kwargs):
    data           = obj_data(target, **kwargs)
    indented_items = obj_data_aligned(data, tab_size=5)
    print("dict(" + indented_items + " )")
    return data
def print_object_members(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False)
Expand source code
def print_object_members(target, name_width=30, value_width=100, show_private=False, show_internals=False, show_value_class=False, show_methods=False, only_show_methods=False):
    max_width = name_width + value_width
    print()
    print(f"Members for object:\n\t {target} of type:{type(target)}")
    print(f"Settings:\n\t name_width: {name_width} | value_width: {value_width} | show_private: {show_private} | show_internals: {show_internals}")
    print()
    if only_show_methods:
        show_methods = True                                             # need to make sure this setting is True, or there will be no methods to show
        print(f"{'method':<{name_width}} (params)")
    else:
        if show_value_class:
            print(f"{'field':<{name_width}} | {'type':<{name_width}} |value")
        else:
            print(f"{'field':<{name_width}} | value")

    print(f"{'-' * max_width}")
    for name, value in obj_data(target, name_width=name_width, value_width=value_width, show_private=show_private, show_internals=show_internals, show_value_class=show_value_class, show_methods=show_methods, only_show_methods=only_show_methods).items():
        if only_show_methods:
            print(f"{name:<{name_width}} {value}"[:max_width])
        else:
            if show_value_class:
                value_class = obj_full_name(value)
                print(f"{name:<{name_width}} | {value_class:{name_width}} | {value}"[:max_width])
            else:
                print(f"{name:<{name_width}} | {value}"[:max_width])
def print_object_methods(target, name_width=30, value_width=100, show_private=False, show_internals=False)
Expand source code
def print_object_methods(target, name_width=30, value_width=100, show_private=False, show_internals=False):
    print_object_members(target, name_width=name_width, value_width=value_width,show_private=show_private,show_internals=show_internals, only_show_methods=True)
def raise_exception_on_obj_type_annotation_mismatch(target, attr_name, value)
Expand source code
def raise_exception_on_obj_type_annotation_mismatch(target, attr_name, value):
    # todo : check if this is is not causing the type safety issues
    if value_type_matches_obj_annotation_for_attr(target, attr_name, value) is False:               # handle case with normal types
        if value_type_matches_obj_annotation_for_union_attr(target, attr_name, value) is True:      # handle union cases
            return                                                                                  #     this is done like this because value_type_matches_obj_annotation_for_union_attr will return None when there is no Union objects
        raise Exception(f"Invalid type for attribute '{attr_name}'. Expected '{target.__annotations__.get(attr_name)}' but got '{type(value)}'")
def type_base_classes(cls)
Expand source code
def type_base_classes(cls):
    base_classes = cls.__bases__
    all_base_classes = list(base_classes)
    for base in base_classes:
        all_base_classes.extend(type_base_classes(base))
    return all_base_classes
def type_full_name(target)
Expand source code
def class_full_name(target):
    if target:
        type_target = type(target)
        type_module = type_target.__module__
        type_name   = type_target.__name__
        return f'{type_module}.{type_name}'
def type_mro(target)
Expand source code
def type_mro(target):
    if type(target) is type:
        cls = target
    else:
        cls = type(target)
    return list(inspect.getmro(cls))
def value_type_matches_obj_annotation_for_attr(target, attr_name, value)
Expand source code
def value_type_matches_obj_annotation_for_attr(target, attr_name, value):
    if hasattr(target, '__annotations__'):
        obj_annotations  = target.__annotations__
        if hasattr(obj_annotations,'get'):
            attr_type        = obj_annotations.get(attr_name)
            if attr_type:
                origin_attr_type = get_origin(attr_type)                # to handle when type definion contains an generic
                if origin_attr_type:
                    attr_type = origin_attr_type
                value_type = type(value)
                if are_types_compatible_for_assigment(source_type=value_type, target_type=attr_type):
                    return True
                if are_types_magic_mock(source_type=value_type, target_type=attr_type):
                    return True

                return value_type is attr_type
    return None
def value_type_matches_obj_annotation_for_union_attr(target, attr_name, value)
Expand source code
def value_type_matches_obj_annotation_for_union_attr(target, attr_name, value):
    value_type           = type(value)
    attribute_annotation = obj_attribute_annotation(target,attr_name)
    origin               = get_origin(attribute_annotation)
    if origin is Union:                                                     # For Union types, including Optionals
        args = get_args(attribute_annotation)                               # Get the argument types
        return value_type in args
    return None                                                             # if it is not an Union type just return None (to give an indication to the caller that the comparison was not made)