def _wrap(obj, wrapper=None, methods_to_add=(), name=None, skip=(), wrap_return_values=False, wrap_filenames=(),
          filename=None, wrapped_name_func=None, wrapped=None):
    """
    Wrap module, class, function or another variable recursively

    :param Any obj: Object to wrap recursively
    :param Optional[Callable] wrapper: Wrapper to wrap functions and methods in (accepts function as argument)
    :param Collection[Callable] methods_to_add: Container of functions, which accept class as argument, and return \
    tuple of method name and method to add to all classes
    :param Optional[str] name: Name of module to wrap to (if `obj` is module)
    :param Collection[Union[str, type, Any]] skip: Items to skip wrapping (if an item of a collection is the str, wrap \
    will check the obj name, if an item of a collection is the type, wrap will check the obj type, else wrap will \
    check an item itself)
    :param bool wrap_return_values: If try, wrap return values of callables (only types, supported by wrap function \
    are supported)
    :param Collection[str] wrap_filenames: Files to wrap
    :param Optional[str] filename: Source file of `obj`
    :param Optional[Callable[Any, str]] wrapped_name_func: Function that accepts `obj` as argument and returns the \
    name of wrapped `obj` that will be written into wrapped `obj`
    :param Any wrapped: Object to wrap to
    :return: Wrapped `obj`
    """
    # noinspection PyUnresolvedReferences
    class ModuleProxy(types.ModuleType, Proxy):
        # noinspection PyShadowingNames
        def __init__(self, name, doc=None):
            super().__init__(name=name, doc=doc)

    try:
        # Subclassing from obj to pass isinstance(some_object, obj) checks. If defining the class fails, it means that
        # `obj` was not a class, that means ClassProxy wouldn't be used, we can create a dummy class.
        class ClassProxy(obj, Proxy):
            @staticmethod
            def __new__(cls, *args, **kwargs):
                # noinspection PyUnresolvedReferences
                original_obj_object = cls._original_obj(*args, **kwargs)
                # noinspection PyArgumentList
                result = _wrap(obj=original_obj_object,
                               wrapper=wrapper,
                               methods_to_add=methods_to_add,
                               name=name,
                               skip=skip,
                               wrap_return_values=wrap_return_values,
                               wrap_filenames=wrap_filenames,
                               filename=filename,
                               wrapped_name_func=wrapped_name_func)
                return result
    except TypeError:
        class ClassProxy(Proxy):
            pass

    class ObjectProxy(Proxy):
        pass

    # noinspection PyShadowingNames
    def get_name(*names):
        name = None
        for obj in names:
            try:
                name = obj.__name__
            except AttributeError:
                if isinstance(obj, str):
                    name = obj
            if name is not None:
                return name
        return name

    # noinspection PyShadowingNames
    def make_key(obj, wrapper, methods_to_add, name, skip, wrap_return_values, wrap_filenames, filename,
                 wrapped_name_func):
        try:
            obj_key = 'hash', hash(obj)
        except TypeError:
            obj_key = 'id', id(obj)
        return obj_key + (wrapper, methods_to_add, name, skip, wrap_return_values, wrap_filenames, filename,
                          wrapped_name_func)

    # noinspection PyShadowingNames
    def wrap_(obj, name, members, wrapped=None):
        def get_obj_type():
            if inspect.ismodule(object=obj):
                result = ObjectType.MODULE
            elif inspect.isclass(object=obj):
                result = ObjectType.CLASS
            elif (inspect.isbuiltin(object=obj) or
                  inspect.isfunction(object=obj) or
                  inspect.ismethod(object=obj) or
                  inspect.ismethoddescriptor(object=obj) or
                  isinstance(obj, MethodWrapper)):
                result = ObjectType.FUNCTION_OR_METHOD
            elif inspect.iscoroutine(object=obj):
                result = ObjectType.COROUTINE
            else:
                result = ObjectType.OBJECT
            return result

        def create_proxy(proxy_type):
            return {
                ProxyType.MODULE: ModuleProxy(name=name),
                ProxyType.CLASS: ClassProxy,
                ProxyType.OBJECT: ObjectProxy(),
            }[proxy_type]

        def add_methods():
            for method_to_add in methods_to_add:
                method_name, method = method_to_add(wrapped)
                if method is not None:
                    setattr(wrapped, method_name, method)

        def set_original_obj():
            with suppress(AttributeError):
                what = type if obj_type == ObjectType.CLASS else object
                what.__setattr__(wrapped, wrapped_name_func(obj), obj)

        def need_to_wrap():
            return is_magic_name(name=attr_name) and attr_name not in ['__class__', '__new__']

        obj_type = get_obj_type()
        if wrapped is None:
            if obj_type in [ObjectType.MODULE, ObjectType.CLASS]:
                wrapped = create_proxy(proxy_type=ProxyType.MODULE if inspect.ismodule(obj) else ProxyType.CLASS)
            elif obj_type == ObjectType.FUNCTION_OR_METHOD:
                wrapped = function_or_method_wrapper()
            elif obj_type == ObjectType.COROUTINE:
                wrapped = coroutine_wrapper()
            else:
                wrapped = create_proxy(proxy_type=ProxyType.OBJECT)
        key = make_key(obj=obj,
                       wrapper=wrapper,
                       methods_to_add=methods_to_add,
                       name=name,
                       skip=skip,
                       wrap_return_values=wrap_return_values,
                       wrap_filenames=wrap_filenames,
                       filename=filename,
                       wrapped_name_func=wrapped_name_func)
        _wrapped_objs[key] = wrapped
        set_original_obj()
        if obj_type in [ObjectType.FUNCTION_OR_METHOD, ObjectType.COROUTINE]:
            return wrapped
        add_methods()
        if obj_type == ObjectType.CLASS:
            for attr_name, attr_value in members:
                if need_to_wrap():
                    raises_exception = (isinstance(attr_value, tuple) and
                                        len(attr_value) > 0 and
                                        attr_value[0] == RAISES_EXCEPTION)
                    if raises_exception and not obj_type == ObjectType.MODULE:
                        def raise_exception(self):
                            _ = self
                            raise attr_value[1]
                        attr_value = property(raise_exception)
                    with suppress(AttributeError, TypeError):
                        # noinspection PyArgumentList
                        attr_value_new = _wrap(obj=attr_value,
                                               wrapper=wrapper,
                                               methods_to_add=methods_to_add,
                                               name=get_name(attr_value, attr_name),
                                               skip=skip,
                                               wrap_return_values=wrap_return_values,
                                               wrap_filenames=wrap_filenames,
                                               filename=get_obj_file(obj=attr_value) or filename,
                                               wrapped_name_func=wrapped_name_func)
                        with suppress(Exception):
                            type.__setattr__(wrapped, attr_name, attr_value_new)
        if obj_type != ObjectType.CLASS:
            wrapped_class_name = get_name(obj.__class__)
            # noinspection PyArgumentList
            wrapped_class = _wrap(obj=obj.__class__,
                                  wrapper=wrapper,
                                  methods_to_add=methods_to_add,
                                  name=wrapped_class_name,
                                  skip=skip,
                                  wrap_return_values=wrap_return_values,
                                  wrap_filenames=wrap_filenames,
                                  filename=get_obj_file(obj=obj.__class__) or filename,
                                  wrapped_name_func=wrapped_name_func,
                                  wrapped=wrapped.__class__)
            object.__setattr__(wrapped, '__class__', wrapped_class)
        return wrapped

    def wrap_return_values_(result):
        if wrap_return_values:
            # noinspection PyArgumentList
            result = _wrap(obj=result,
                           wrapper=wrapper,
                           methods_to_add=methods_to_add,
                           name=get_name(result, 'result'),
                           skip=skip,
                           wrap_return_values=wrap_return_values,
                           wrap_filenames=wrap_filenames,
                           filename=filename,
                           wrapped_name_func=wrapped_name_func)
        return result

    # noinspection PyShadowingNames
    def is_magic_name(name):
        return name.startswith('__') and name.endswith('__')

    # noinspection PyShadowingNames
    def is_magic(obj):
        return is_magic_name(name=obj.__name__)

    # noinspection PyShadowingNames
    def is_coroutine_function(obj, wrapper):
        return inspect.iscoroutinefunction(object=wrapper(obj)) and not is_magic(obj=obj)

    # noinspection PyShadowingNames
    def wrap_call_and_wrap_return_values(obj, wrapper):
        if is_coroutine_function(obj=obj, wrapper=wrapper):
            # noinspection PyShadowingNames
            @wraps(obj)
            async def wrapper(*args, **kwargs):
                return wrap_return_values_(result=await obj(*args, **kwargs))
        else:
            # noinspection PyShadowingNames
            @wraps(obj)
            def wrapper(*args, **kwargs):
                return wrap_return_values_(result=obj(*args, **kwargs))
        return wrapper

    def function_or_method_wrapper():
        # noinspection PyShadowingNames
        @wraps(obj)
        def wrapped_obj(*args, **kwargs):
            return wrapper(obj)(*args, **kwargs)

        @wraps(obj)
        def obj_with_original_obj_as_self(*args, **kwargs):
            if len(args) > 0 and isinstance(args[0], Proxy):
                # noinspection PyProtectedMember
                args = (object.__getattribute__(args[0], '_original_obj'), ) + args[1:]
            return obj(*args, **kwargs)

        if wrapper is None:
            result = obj
        elif is_magic(obj=obj):
            if obj.__name__ == '__getattribute__':
                @wraps(obj)
                def result(*args, **kwargs):
                    # If we are trying to access magic attribute, call obj with args[0]._original_obj as self,
                    # else call original __getattribute__ and wrap the result before returning it.
                    # noinspection PyShadowingNames
                    name = args[1]
                    attr_value = obj_with_original_obj_as_self(*args, **kwargs)
                    if is_magic_name(name=name):
                        return attr_value
                    else:
                        # noinspection PyShadowingNames,PyArgumentList
                        return _wrap(obj=attr_value,
                                     wrapper=wrapper,
                                     methods_to_add=methods_to_add,
                                     name=name,
                                     skip=skip,
                                     wrap_return_values=wrap_return_values,
                                     wrap_filenames=wrap_filenames,
                                     filename=filename,
                                     wrapped_name_func=wrapped_name_func)
            else:
                result = obj_with_original_obj_as_self
        elif obj.__name__ == '__getattr__':
            @wraps(obj)
            def result(*args, **kwargs):
                return wrapper(obj(*args, **kwargs))
        else:
            result = wrapped_obj
        if wrap_return_values:
            result = wrap_call_and_wrap_return_values(obj=result, wrapper=wrapper)
        return result

    def coroutine_wrapper():
        @wraps(obj)
        async def result(*args, **kwargs):
            return await wrapper(obj)(*args, **kwargs)

        if wrap_return_values:
            result = wrap_call_and_wrap_return_values(obj=result, wrapper=wrapper)
        return result

    def is_in_skip():
        result = False
        for s in skip:
            if isinstance(s, str):
                if name == s:
                    result = True
            elif isinstance(s, type):
                if isinstance(obj, s):
                    result = True
            else:
                if obj == s:
                    result = True
        return result

    # noinspection PyShadowingNames
    def get_obj_file(obj):
        # noinspection PyShadowingNames
        def _get_obj_file(obj):
            try:
                result = (obj.__file__
                          if hasattr(obj, '__file__') else
                          sys.modules[obj.__module__].__file__
                          if hasattr(obj, '__module__') else
                          None)
            except (AttributeError, KeyError):
                result = None
            return result

        result = _get_obj_file(obj=obj)
        if result is None:
            result = _get_obj_file(obj=type(obj))
        return result

    def get_obj_library_files():
        obj_file = get_obj_file(obj=obj)
        if obj_file is not None:
            obj_file = Path(obj_file)
            if obj_file.name == '__init__.py':
                result = obj_file.parent.glob('**/*.py')
            else:
                result = [obj_file]
            result = [str(obj_file) for obj_file in result]
        else:
            result = []
        result = frozenset(result)
        return result

    methods_to_add = frozenset(methods_to_add)
    skip = frozenset(skip)
    wrap_filenames = frozenset(wrap_filenames)

    if wrapped_name_func is None:
        # noinspection PyShadowingNames
        def wrapped_name_func(obj):
            _ = obj
            return '_original_obj'

    name = get_name(name, obj)
    if name is None:
        raise ValueError("name was not passed and obj.__name__ not found")

    key = make_key(obj=obj,
                   wrapper=wrapper,
                   methods_to_add=methods_to_add,
                   name=name,
                   skip=skip,
                   wrap_return_values=wrap_return_values,
                   wrap_filenames=wrap_filenames,
                   filename=filename,
                   wrapped_name_func=wrapped_name_func)

    wrap_filenames = wrap_filenames or get_obj_library_files()
    filename = get_obj_file(obj=obj) or filename

    # noinspection PyUnusedLocal
    members = []
    with suppress(ModuleNotFoundError):
        members = getmembers(object=obj)

    try:
        already_wrapped = key in _wrapped_objs
    except TypeError:
        already_wrapped = False
    if filename not in wrap_filenames or is_in_skip():
        wrapped = obj
    elif already_wrapped:
        wrapped = _wrapped_objs[key]
    elif members:
        wrapped = wrap_(obj=obj, name=name, members=members, wrapped=wrapped)
    else:
        wrapped = obj
        _wrapped_objs[key] = wrapped

    return wrapped
def _wrap ( obj , wrapper = None , methods_to_add = ( ) , name = None , skip = ( ) , wrap_return_values = False , wrap_filenames = ( ) , filename = None , wrapped_name_func = None , wrapped = None ) : class ModuleProxy ( types . ModuleType , Proxy ) : def __init__ ( self , name , doc = None ) : super ( ) . __init__ ( name = name , doc = doc ) try : class ClassProxy ( obj , Proxy ) : @ staticmethod def __new__ ( cls , * args , ** kwargs ) : original_obj_object = cls . _original_obj ( * args , ** kwargs ) result = _wrap ( obj = original_obj_object , wrapper = wrapper , methods_to_add = methods_to_add , name = name , skip = skip , wrap_return_values = wrap_return_values , wrap_filenames = wrap_filenames , filename = filename , wrapped_name_func = wrapped_name_func ) return result except TypeError : class ClassProxy ( Proxy ) : pass class ObjectProxy ( Proxy ) : pass def get_name ( * names ) : name = None for obj in names : try : name = obj . __name__ except AttributeError : if isinstance ( obj , str ) : name = obj if name is not None : return name return name def make_key ( obj , wrapper , methods_to_add , name , skip , wrap_return_values , wrap_filenames , filename , wrapped_name_func ) : try : obj_key = 'hash' , hash ( obj ) except TypeError : obj_key = 'id' , id ( obj ) return obj_key + ( wrapper , methods_to_add , name , skip , wrap_return_values , wrap_filenames , filename , wrapped_name_func ) def wrap_ ( obj , name , members , wrapped = None ) : def get_obj_type ( ) : if inspect . ismodule ( object = obj ) : result = ObjectType . MODULE elif inspect . isclass ( object = obj ) : result = ObjectType . CLASS elif ( inspect . isbuiltin ( object = obj ) or inspect . isfunction ( object = obj ) or inspect . ismethod ( object = obj ) or inspect . ismethoddescriptor ( object = obj ) or isinstance ( obj , MethodWrapper ) ) : result = ObjectType . FUNCTION_OR_METHOD elif inspect . iscoroutine ( object = obj ) : result = ObjectType . COROUTINE else : result = ObjectType . OBJECT return result def create_proxy ( proxy_type ) : return { ProxyType . MODULE : ModuleProxy ( name = name ) , ProxyType . CLASS : ClassProxy , ProxyType . OBJECT : ObjectProxy ( ) , } [ proxy_type ] def add_methods ( ) : for method_to_add in methods_to_add : method_name , method = method_to_add ( wrapped ) if method is not None : setattr ( wrapped , method_name , method ) def set_original_obj ( ) : with suppress ( AttributeError ) : what = type if obj_type == ObjectType . CLASS else object what . __setattr__ ( wrapped , wrapped_name_func ( obj ) , obj ) def need_to_wrap ( ) : return is_magic_name ( name = attr_name ) and attr_name not in [ '__class__' , '__new__' ] obj_type = get_obj_type ( ) if wrapped is None : if obj_type in [ ObjectType . MODULE , ObjectType . CLASS ] : wrapped = create_proxy ( proxy_type = ProxyType . MODULE if inspect . ismodule ( obj ) else ProxyType . CLASS ) elif obj_type == ObjectType . FUNCTION_OR_METHOD : wrapped = function_or_method_wrapper ( ) elif obj_type == ObjectType . COROUTINE : wrapped = coroutine_wrapper ( ) else : wrapped = create_proxy ( proxy_type = ProxyType . OBJECT ) key = make_key ( obj = obj , wrapper = wrapper , methods_to_add = methods_to_add , name = name , skip = skip , wrap_return_values = wrap_return_values , wrap_filenames = wrap_filenames , filename = filename , wrapped_name_func = wrapped_name_func ) _wrapped_objs [ key ] = wrapped set_original_obj ( ) if obj_type in [ ObjectType . FUNCTION_OR_METHOD , ObjectType . COROUTINE ] : return wrapped add_methods ( ) if obj_type == ObjectType . CLASS : for attr_name , attr_value in members : if need_to_wrap ( ) : raises_exception = ( isinstance ( attr_value , tuple ) and len ( attr_value ) > 0 and attr_value [ 0 ] == RAISES_EXCEPTION ) if raises_exception and not obj_type == ObjectType . MODULE : def raise_exception ( self ) : _ = self raise attr_value [ 1 ] attr_value = property ( raise_exception ) with suppress ( AttributeError , TypeError ) : attr_value_new = _wrap ( obj = attr_value , wrapper = wrapper , methods_to_add = methods_to_add , name = get_name ( attr_value , attr_name ) , skip = skip , wrap_return_values = wrap_return_values , wrap_filenames = wrap_filenames , filename = get_obj_file ( obj = attr_value ) or filename , wrapped_name_func = wrapped_name_func ) with suppress ( Exception ) : type . __setattr__ ( wrapped , attr_name , attr_value_new ) if obj_type != ObjectType . CLASS : wrapped_class_name = get_name ( obj . __class__ ) wrapped_class = _wrap ( obj = obj . __class__ , wrapper = wrapper , methods_to_add = methods_to_add , name = wrapped_class_name , skip = skip , wrap_return_values = wrap_return_values , wrap_filenames = wrap_filenames , filename = get_obj_file ( obj = obj . __class__ ) or filename , wrapped_name_func = wrapped_name_func , wrapped = wrapped . __class__ ) object . __setattr__ ( wrapped , '__class__' , wrapped_class ) return wrapped def wrap_return_values_ ( result ) : if wrap_return_values : result = _wrap ( obj = result , wrapper = wrapper , methods_to_add = methods_to_add , name = get_name ( result , 'result' ) , skip = skip , wrap_return_values = wrap_return_values , wrap_filenames = wrap_filenames , filename = filename , wrapped_name_func = wrapped_name_func ) return result def is_magic_name ( name ) : return name . startswith ( '__' ) and name . endswith ( '__' ) def is_magic ( obj ) : return is_magic_name ( name = obj . __name__ ) def is_coroutine_function ( obj , wrapper ) : return inspect . iscoroutinefunction ( object = wrapper ( obj ) ) and not is_magic ( obj = obj ) def wrap_call_and_wrap_return_values ( obj , wrapper ) : if is_coroutine_function ( obj = obj , wrapper = wrapper ) : @ wraps ( obj ) async def wrapper ( * args , ** kwargs ) : return wrap_return_values_ ( result = await obj ( * args , ** kwargs ) ) else : @ wraps ( obj ) def wrapper ( * args , ** kwargs ) : return wrap_return_values_ ( result = obj ( * args , ** kwargs ) ) return wrapper def function_or_method_wrapper ( ) : @ wraps ( obj ) def wrapped_obj ( * args , ** kwargs ) : return wrapper ( obj ) ( * args , ** kwargs ) @ wraps ( obj ) def obj_with_original_obj_as_self ( * args , ** kwargs ) : if len ( args ) > 0 and isinstance ( args [ 0 ] , Proxy ) : args = ( object . __getattribute__ ( args [ 0 ] , '_original_obj' ) , ) + args [ 1 : ] return obj ( * args , ** kwargs ) if wrapper is None : result = obj elif is_magic ( obj = obj ) : if obj . __name__ == '__getattribute__' : @ wraps ( obj ) def result ( * args , ** kwargs ) : name = args [ 1 ] attr_value = obj_with_original_obj_as_self ( * args , ** kwargs ) if is_magic_name ( name = name ) : return attr_value else : return _wrap ( obj = attr_value , wrapper = wrapper , methods_to_add = methods_to_add , name = name , skip = skip , wrap_return_values = wrap_return_values , wrap_filenames = wrap_filenames , filename = filename , wrapped_name_func = wrapped_name_func ) else : result = obj_with_original_obj_as_self elif obj . __name__ == '__getattr__' : @ wraps ( obj ) def result ( * args , ** kwargs ) : return wrapper ( obj ( * args , ** kwargs ) ) else : result = wrapped_obj if wrap_return_values : result = wrap_call_and_wrap_return_values ( obj = result , wrapper = wrapper ) return result def coroutine_wrapper ( ) : @ wraps ( obj ) async def result ( * args , ** kwargs ) : return await wrapper ( obj ) ( * args , ** kwargs ) if wrap_return_values : result = wrap_call_and_wrap_return_values ( obj = result , wrapper = wrapper ) return result def is_in_skip ( ) : result = False for s in skip : if isinstance ( s , str ) : if name == s : result = True elif isinstance ( s , type ) : if isinstance ( obj , s ) : result = True else : if obj == s : result = True return result def get_obj_file ( obj ) : def _get_obj_file ( obj ) : try : result = ( obj . __file__ if hasattr ( obj , '__file__' ) else sys . modules [ obj . __module__ ] . __file__ if hasattr ( obj , '__module__' ) else None ) except ( AttributeError , KeyError ) : result = None return result result = _get_obj_file ( obj = obj ) if result is None : result = _get_obj_file ( obj = type ( obj ) ) return result def get_obj_library_files ( ) : obj_file = get_obj_file ( obj = obj ) if obj_file is not None : obj_file = Path ( obj_file ) if obj_file . name == '__init__.py' : result = obj_file . parent . glob ( '**/*.py' ) else : result = [ obj_file ] result = [ str ( obj_file ) for obj_file in result ] else : result = [ ] result = frozenset ( result ) return result methods_to_add = frozenset ( methods_to_add ) skip = frozenset ( skip ) wrap_filenames = frozenset ( wrap_filenames ) if wrapped_name_func is None : def wrapped_name_func ( obj ) : _ = obj return '_original_obj' name = get_name ( name , obj ) if name is None : raise ValueError ( "name was not passed and obj.__name__ not found" ) key = make_key ( obj = obj , wrapper = wrapper , methods_to_add = methods_to_add , name = name , skip = skip , wrap_return_values = wrap_return_values , wrap_filenames = wrap_filenames , filename = filename , wrapped_name_func = wrapped_name_func ) wrap_filenames = wrap_filenames or get_obj_library_files ( ) filename = get_obj_file ( obj = obj ) or filename members = [ ] with suppress ( ModuleNotFoundError ) : members = getmembers ( object = obj ) try : already_wrapped = key in _wrapped_objs except TypeError : already_wrapped = False if filename not in wrap_filenames or is_in_skip ( ) : wrapped = obj elif already_wrapped : wrapped = _wrapped_objs [ key ] elif members : wrapped = wrap_ ( obj = obj , name = name , members = members , wrapped = wrapped ) else : wrapped = obj _wrapped_objs [ key ] = wrapped return wrapped
Wrap module, class, function or another variable recursively

    :param Any obj: Object to wrap recursively
    :param Optional[Callable] wrapper: Wrapper to wrap functions and methods in (accepts function as argument)
    :param Collection[Callable] methods_to_add: Container of functions, which accept class as argument, and return \
    tuple of method name and method to add to all classes
    :param Optional[str] name: Name of module to wrap to (if `obj` is module)
    :param Collection[Union[str, type, Any]] skip: Items to skip wrapping (if an item of a collection is the str, wrap \
    will check the obj name, if an item of a collection is the type, wrap will check the obj type, else wrap will \
    check an item itself)
    :param bool wrap_return_values: If try, wrap return values of callables (only types, supported by wrap function \
    are supported)
    :param Collection[str] wrap_filenames: Files to wrap
    :param Optional[str] filename: Source file of `obj`
    :param Optional[Callable[Any, str]] wrapped_name_func: Function that accepts `obj` as argument and returns the \
    name of wrapped `obj` that will be written into wrapped `obj`
    :param Any wrapped: Object to wrap to
    :return: Wrapped `obj`