code: def function_info(function_index=1, function_name=None, line_number=None):
      
      frm = func_frame(function_index + 1, function_name)
  
      file_ = os.path.abspath(frm.f_code.co_filename)
      class_name = frm.f_locals.get('self', None)
      if class_name is not None: # and not skip_class:
          class_name = str(type(class_name)).split('.',1)[-1].split("'")[0]
          # try:
          #     class_name = str(class_name).split(None, 1)[1]
          #     class_name = class_name.split('.')[-1].replace(')', '')
          # except:
          #     class_name = repr(class_name).split()[0].split('.')[-1]
          # if 'object at' in str(class_name):
          #     class_name = str(class_name).split(' object at')[0].split('.')[-1]
  
      args, _, _, kwargs = inspect.getargvalues(frm)
      line_number = line_number or frm.f_lineno
      return {'class_name': class_name or '',
              'function_name': frm.f_code.co_name,
              'file': file_,
              'path': os.path.split(file_)[0],
              'basename': os.path.basename(file_).split('.')[0],
              'line_number': line_number or frm.f_lineno,
              'globals': frm.f_globals,
              'locals': frm.f_locals,
              'arguments': args,
              'kwargs': kwargs,
              'frame': frm}
docs: This will return the class_name and function_name of the
      function traced back two functions.
  
      :param function_index: int of how many frames back the program 
                             should look (2 will give the parent of the caller)
      :param function_name: str of what function to look for (should 
                             not be used with function_index)
      :param line_number: int, some times the user may want to override 
                             this for testing purposes
      :return tuple: ('cls_name','func_name',line_number,globals())