docs: 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