docs: def _apply_interception(
          target, interception_fn, ctx=None, _globals=None
  ):
      """Apply interception on input target and return the final target.
  
      :param Callable target: target on applying the interception_fn.
      :param function interception_fn: interception function to apply on
          target
      :param ctx: target ctx (instance or class) if not None.
  
      :return: both interception and intercepted
          - if target is a builtin function,
              the result is a (wrapper function, builtin).
          - if target is a function, interception is target where
              code is intercepted code, and interception is a new function where
              code is target code.
      :rtype: tuple(callable, function, ctx)
      :raises: TypeError if target is not a routine.
      """
  
      if not callable(target):
          raise TypeError('target {0} is not callable.'.format(target))
  
      intercepted = target
      interception = interception_fn
  
      # try to get the right ctx
      if ctx is None:
          ctx = find_ctx(elt=target)
  
      # if target is a builtin
      if isbuiltin(target) or getmodule(target) is builtins:
          # update builtin function reference in module with wrapper
          module = getmodule(target)
          found = False  # check for found function
  
          if module is not None:
              # update all references by value
              for name, _ in getmembers(
                      module, lambda member: member is target):
                  setattr(module, name, interception_fn)
                  found = True
  
              if not found:  # raise Exception if not found
                  raise JoinpointError(
                      "Impossible to weave on not modifiable function {0}. \
                      Must be contained in module {1}".format(target, module)
                  )
  
      elif ctx is None:
          # update code with interception code
          target_fn = _get_function(target)
          # switch interception and intercepted
          interception, intercepted = target, interception_fn
          # switch of code between target_fn and
          # interception_fn
          target_fn.__code__, interception_fn.__code__ = \
              interception_fn.__code__, target_fn.__code__
  
      else:
          # get target name
          if isclass(target):  # if target is a class, get constructor name
              target_name = _get_function(target).__name__
          else:  # else get target name
              target_name = target.__name__
          # get the right intercepted
          intercepted = getattr(ctx, target_name)
          # in case of method
          if ismethod(intercepted):  # in creating eventually a new method
              args = [interception, ctx]
              if PY2:  # if py2, specify the ctx class
                  # and unbound method type
                  if intercepted.__self__ is None:
                      args = [interception, None, ctx]
                  else:
                      args.append(ctx.__class__)
              # instantiate a new method
              interception = MethodType(*args)
          # get the right intercepted function
          if is_intercepted(intercepted):
              intercepted, _ = get_intercepted(intercepted)
          else:
              intercepted = _get_function(intercepted)
          # set in ctx the new method
          setattr(ctx, target_name, interception)
  
      # add intercepted into interception_fn globals and attributes
      interception_fn = _get_function(interception)
      # set intercepted
      setattr(interception_fn, _INTERCEPTED, intercepted)
      # set intercepted ctx
      if ctx is not None:
          setattr(interception_fn, _INTERCEPTED_CTX, ctx)
  
      interception_fn.__globals__[_INTERCEPTED] = intercepted
      interception_fn.__globals__[_INTERCEPTION] = interception
  
      if _globals is not None:
          interception_fn.__globals__.update(_globals)
  
      return interception, intercepted, ctx