docs: def _log_failure(arg_num, msg=None):
      """ Retrace stack and log the failed expresion information """
  
      # stack() returns a list of frame records
      #   0 is the _log_failure() function
      #   1 is the expect() function 
      #   2 is the function that called expect(), that's what we want
      #
      # a frame record is a tuple like this:
      #   (frame, filename, line, funcname, contextlist, index)
      # we're only interested in the first 4. 
      frame,  filename, file_lineno, funcname = inspect.stack()[2][:4]
      # Note that a frame object should be deleted once used to be safe and stop possible 
      # memory leak from circular referencing 
      try:
          frame_source_lines, frame_start_lineno = (inspect.getsourcelines(frame)) 
      finally:
          del frame
  
      filename = os.path.basename(filename)
  
      # Build abstract syntax tree from source of frame
      source_ast = ast.parse(''.join(frame_source_lines))
  
      # Locate the executed expect function 
      func_body = source_ast.body[0].body
  
      map_lineno_to_node = {}
      for idx, node in enumerate(func_body):
          map_lineno_to_node[node.lineno] = node
      
      last_lineno = file_lineno - frame_start_lineno + 1
  
      element_idx = [x for x in map_lineno_to_node.keys() if x <= last_lineno]
      element_idx = max(element_idx)
  
      expect_function_ast = map_lineno_to_node[element_idx]
  
      # Return the source code of the numbered argument
      arg = expect_function_ast.value.args[arg_num]
      line = arg.lineno
      if isinstance(arg, (ast.Tuple, ast.List)):
          expr = astor.to_source(arg.elts[0])
      else:
          expr = astor.to_source(arg)
  
      filename = os.path.basename(filename)
  
      failure_info = {'file': filename, 'line': line, 'funcname': funcname, 'msg': msg, 'expression': expr}
  
      _failed_expectations.append(failure_info)