docs: def debug(victim=None, ignore_exceptions=(), catch_exception=None, depth=0):
      """A decorator function to catch exceptions and enter debug mode.
  
      Args:
          victim (typing.Union(type, function)): either a class or function to
              wrap and debug.
          ignore_exceptions (list): list of classes of exceptions not to catch.
          catch_exception (type): class of exception to catch and debug.
              default is None, meaning catch all exceptions.
          depth (number): how many levels of inner function calls to propagate.
  
      Returns:
          object. wrapped class or function.
  
      Note:
          This wrapper avoids recursion by setting a flag to each wrapped item.
      """
      if victim is None:
          # Debug is used as a decorator so we need to return wrap function to
          # get the real victim
          def wrapper(real_victim):
              return debug(real_victim, ignore_exceptions,
                           catch_exception, depth)
  
          return wrapper
  
      if inspect.isfunction(victim):
          if hasattr(victim, '_ipdebug_wrapped'):
              # Don't wrap the function more than once
              return victim
  
          _transformer = ErrorsCatchTransformer(
              ignore_exceptions=ignore_exceptions,
              catch_exception=catch_exception,
              depth=depth)
  
          try:
              # Try to get the source code of the wrapped object.
              sourcelines, start_num = inspect.getsourcelines(victim.__code__)
              indent = re.match(r'\s*', sourcelines[0]).group()
              source = ''.join(l.replace(indent, '', 1) for l in sourcelines)
  
          except IOError:
              # Worst-case scenario we can only catch errors at a granularity
              # of the whole function
              return victim
  
          else:
              # If we have access to the source, we can silence errors on a
              # per-expression basis, which is "better"
  
              old_code_tree = ast.parse(source)
              # Reposition the line numbers to their original value
              ast.increment_lineno(old_code_tree, start_num - 1)
              tree = _transformer.visit(old_code_tree)
  
              import_debug_cmd = ast.ImportFrom(
                  __name__, [ast.alias("start_debugging", None),
                             ast.alias("debug", None)], 0)
  
              # Add import to the debugger as first command
              tree.body[0].body.insert(0, import_debug_cmd)
  
              # Add import to the exception classes
              if catch_exception is not None:
                  import_exception_cmd = ast.ImportFrom(
                      catch_exception.__module__,
                      [ast.alias(catch_exception.__name__, None)], 0)
  
                  tree.body[0].body.insert(1, import_exception_cmd)
  
              if ignore_exceptions is not None:
                  for exception_class in ignore_exceptions:
                      import_exception_cmd = ast.ImportFrom(
                          exception_class.__module__,
                          [ast.alias(exception_class.__name__, None)], 0)
  
                      tree.body[0].body.insert(1, import_exception_cmd)
  
              # Delete the debugger decorator of the function
              del tree.body[0].decorator_list[:]
  
              # Add pass at the end (to enable debugging the last command)
              pass_cmd = ast.Pass()
              func_body = tree.body[0].body
              pass_cmd.lineno = get_last_lineno(func_body[-1]) + 1
              pass_cmd.col_offset = func_body[-1].col_offset
              func_body.append(pass_cmd)
  
              # Fix missing line numbers and column offsets before compiling
              for node in ast.walk(tree):
                  if not hasattr(node, 'lineno'):
                      node.lineno = 0
  
              ast.fix_missing_locations(tree)
  
              # Define the wrapping function object
              function_definition = "def _free_vars_wrapper(): pass"
              wrapping_function = ast.parse(function_definition).body[0]
  
              # Initialize closure's variables to None
              body_list = [ast.parse("{var} = None".format(var=free_var)).body[0]
                           for free_var in victim.__code__.co_freevars]
  
              # Add the original function ("victim") to the wrapping function
              body_list.append(tree.body[0])
  
              wrapping_function.body = body_list
  
              # Replace original function ("victim") with the wrapping function
              tree.body[0] = wrapping_function
  
              # Create a new runnable code object to replace the original code
              code = compile(tree, victim.__code__.co_filename, 'exec')
              victim.__code__ = code.co_consts[0].co_consts[1]
  
              # Set a flag to indicate that the method was wrapped
              victim._ipdebug_wrapped = True
  
              return victim
  
      elif inspect.ismethod(victim):
          debug(victim.__func__, ignore_exceptions, catch_exception)
          return victim
  
      elif isinstance(victim, type):
          # Wrap each method of the class with the debugger
          for name, member in vars(victim).items():
              if isinstance(member, (type, types.FunctionType,
                                     types.LambdaType, types.MethodType)):
                  setattr(victim, name,
                          debug(member, ignore_exceptions, catch_exception))
  
          return victim
  
      else:
          raise TypeError(
              "Debugger can only wrap functions and classes. "
              "Got object {!r} of type {}".format(victim, type(victim).__name__))