Retrace stack and log the failed expresion information

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)