python-3.xfunctionstack

Function exits program instead of returning to previous call


trying to make this a minimal reproduction of the problem. If I run the following code as follows:

  1. select client (enter 2)
  2. select client from list (enter 2)
  3. hit return twice

The program terminates instead of reprinting the top_menu.

here's the code:

import os
from time import sleep
from pprint import pprint
import json
import traceback

os.chdir(os.path.dirname(__file__))

active_client = {}
flag = True
menus = {
            'top_menu': ['Create Client', 'Select Client'],            
            'select_menu':['dynamic'],
            'action_menu':['Transactions', 'View Portfolio', 'View Transactions'],
            'trans_menu':['Buy', 'Sell', 'Contribution', 'Withdrawal'],
            'port_menu': ['Current Portfolio', 'Portfolio History']            
}
menu_stack = []  # Stack to hold function references

def top_menu():     
    print('enter top')
    pprint(menu_stack)
    
    header ="Equity Portfolio Management System"
    msg = "Select Task : "
    tasks = menus['top_menu']
    
    choice = print_menu(header, msg, tasks, False)  

    if choice == 2:
        print('before')
        menu_stack.append(top_menu)
        print(menu_stack)
        
        select_client()
        print('after')
        pprint(menu_stack)
    elif choice == "":
        # If return is pressed and we're at the top menu, exit directly
        print("Exiting...")
        with open('clients.json', 'w') as json_file:
            json.dump(clients, json_file, indent=4) 
            
        if flag: print(1)
        return
    
        
    if flag: print(2)

    pprint(menu_stack)
    traceback.print_stack()    
    menu_stack.pop()()
   
    return
    
    
    
        


def select_client():
    global active_client
    print('appending')
    #menu_stack.append(top_menu)
    pprint(menu_stack)
    if not clients:
        print("\nNo clients available. Please create one first.")
        sleep(2)
        # Pop the current menu and return to the previous menu
        if flag: print(5)
        return  # Go back to the previous menu        
    
    names = []
    for index, client in enumerate(clients, 1):
        names.append(client['first name'] + ' ' + client['last name'])

    header = 'Client Names'
    msg = ' Select client : '

    choice = print_menu(header, msg, names, False)    

    if choice == "":
        # If return is pressed, go back to the top menu
        if flag: print(6)
        pprint(menu_stack)
        traceback.print_stack()
        return  # Go back to the previous menu
    
    active_client = clients[choice-1]
    action_menu()
    
    if flag: print(7)
    pprint(menu_stack)
    traceback.print_stack()
    return
           
            

def action_menu():
    menu_stack.append(select_client)
    pprint(menu_stack)
    header = 'Actions for ' + active_client['first name'] + ' ' + active_client['last name']
    msg = 'Select Action : '
    tasks = menus['action_menu']    

    choice = print_menu(header, msg, tasks, False) 

    if choice == "":             
        if flag: print(8) 
        pprint(menu_stack)  
        return  # Go back to select_client if input is empty
    
    
    if flag: print(9)
    return

def print_menu(header, msg, items, header_only):
    
    # Get the terminal width
    terminal_width = os.get_terminal_size().columns

    # Field sizes
    item_no_size = 10
    item_size = 40

    # Calculate the starting position to center the content
    center_padding = ((terminal_width - item_no_size - item_size) // 2 ) 

    # Input validation loop
    
    while True:
        #clear_terminal()


        print("\n" + "Babson Enterprises".center(terminal_width))
        print(header.center(terminal_width, "*"))
        print("*" * terminal_width + "\n")

        if header_only: return

        print(" " * center_padding + "Task No".ljust(item_no_size) + "    Task".ljust(item_size))
        print(" " * center_padding + "-" * (item_no_size + item_size) ) # Print a line underneath the headers        

        # Print tasks with extra spacing
        for index, item_name in enumerate(items):
            print(" " * center_padding + str(index+1).center(item_no_size) + item_name.ljust(item_size))
        print("\n")

        return  int_input(" " * center_padding + msg, items)

def int_input(msg, items):
    while True:
        item_selected = input(msg)
        if item_selected == '':
            return ''
        try:
            item_selected = int(item_selected)
            if 1 <= item_selected <= len(items):                
                return item_selected  # Valid input, exit loop
            else:
                msg = "******Invalid choice. Please enter a number " + \
                      "between 1 and " + str(len(items)) + " : "
        except ValueError:
            msg = "Please enter a valid integer."



if __name__ == "__main__":
   
    # Start at the top_menu and add it to the stack
    with open('clients.json', 'r') as json_file:
        clients = json.load(json_file)
    top_menu()
    print('program end')

    #traceback.print_stack()

Solution

  • menu_stack.pop()()
    

    At the end of your method always assumes there's something left to catch in the stack.

    When you hit the last Enter, and top_menu() also returns, the stack is empty.

    https://mimo.org/glossary/python/pop()

    While the pop() method is highly useful, it's important to take care when you use the method. Using pop() on an empty list or with an out-of-range index results in an IndexError (pop() index out of range).

    You are not able to see this exception (IndexError) as the stack trace gets "devoured" due to top-level context ending.

    Easiest way to fix this is to check if there's something in the stack:

    if menu_stack:
        menu_stack.pop()()
    else:
        top_menu()