How to Build a Menu Tree for a Python CLI App
A step-by-step guide to build a Python CLI application using a Tree with Nodes
In this article I will walk through the basic functionality for creating a menu tree in Python using Nodes, then populating the menu tree to create a basic CLI application.
The repo for this example can be found here:
menu_tree_cli_example Repository
For simplicity, I have omitted data validation procedures, including property methods.
For a more robust example of how I used menu trees to build a CLI app, check out my rental management tool on Github:
rental_management_tool Repository
Why Build a CLI Application?
User interfaces are great for appealing to a broad audience, but many tasks don’t need them—CLI tools provide faster, scriptable solutions for efficient workflows.
Some benefits of CLI applications include:
Automation and Scripting: Python CLI applications allow users to automate repetitive tasks and integrate them into scripts for streamlined workflows.
Ease of Development and Extensibility: Python's readability and extensive libraries make it easy to build and expand CLI tools for various use cases.
Cross-Platform Compatibility: Python is available on all major operating systems, making CLI tools portable and accessible to a wide audience.
Versatility and Integration: Python CLI tools can handle diverse tasks, from data processing to interacting with APIs and databases, fitting seamlessly into development, DevOps, or data workflows.
Building the MenuTree and Node Classes
The following dependencies will be required for this example:
from pick import pick
import os
First, create and initalize MenuTree and Node classes.
For this basic example, the MenuTree class will just need to store the root Node of the tree.
The Node class will have the following attributes:
option_label: label when node is displayed as a menu option
title_label: title after node is selected from menu options
parent: parent node of the current instance
children: list of child Node instances
procedure: function to run when instance is selected from menu (default None)
- procedures can optionally include nodes as return values to tell the application where to go next if a procedure is run
data_ref: any object that the user would like to store in the instance
- note: for those familiar with Javascript/HTML you can think of this like the dataset attribute
class MenuTree:
def __init__(self, node):
self.root = node
class Node:
last_node = None
def __init__(self, option_label, title_label=None):
self.option_label = option_label
self.title_label = option_label if title_label is None else title_label
self.parent = None
self.children = []
self.procedure = None
self.data_ref = None
Next, add methods to the Node class which are used to populate the tree:
def add_procedure(self, procedure):
'''
validates and adds procedure to instance
Parameters
---------
procedure: callable object
- function to be run when node is activated
'''
if callable(procedure):
_procedure = procedure
else:
raise ValueError("Function parameter must be a function")
self.procedure = _procedure
def add_child(self, node):
'''
adds child node to list of children
Parameters
---------
node: Node instance
- node to be added as a child of instance
'''
node.parent = self
self.children.append(node)
def add_children(self, nodes):
'''
adds multiple children to instance
Parameters
---------
nodes: list
- list of Node instances to be added as children of instance
'''
for node in nodes:
self.add_child(node)
Then, add methods to the Node class which are used to run the CLI application:
show_menu: run from the cli to show menu items based on user selections
-
the return of this function is a node to tell the application where to go after a menu item is selected
-
run_procedure: run from the cli to run procedure stored in a Node if that Node is selected by the user
the return of this function is a node to tell the application where to go after a procedure is run
if a procedure includes a node as a return value, this will override the default next node
def show_menu(self):
'''
navigates to next menu based on user selection
Returns
---------
self.children[index]: Node instance
- Node to display in the application after user selection
'''
Node.last_node = self
os.system('cls' if os.name == 'nt' else 'clear')
user_selection, index = pick([child.option_label for child in self.children], self.title_label)
return self.children[index]
def run_procedure(self):
'''
runs procedure stored in instance
Returns
---------
Node instance
- Node to display in the application after procedure runs
'''
default_next = self if len(self.children) > 0 else self.parent
next_node = self.procedure()
return next_node if next_node else default_next
Populating the Tree
Create generic menu tree nodes
Start by initializing the tree and adding reusable menu items as nodes
Note that procedures can be added as lambda functions or regular functions if a function does not include parameters.
The to_main and go_back node procedures return nodes to tell the CLI where to go next if these procedures are run.
from menu_tree import MenuTree, Node
import sys
from pick import pick
# //////////////////////////////////////
# INITIALIZE TREE
main = Node(option_label="Main Menu")
menu = MenuTree(main)
# //////////////////////////////////////
# BASIC REUSABLE FUNCTIONALITY
# go to main menu
to_main = Node(option_label="Main Menu")
to_main.add_procedure(lambda: menu.root)
# go to previous menu
go_back = Node(option_label="Previous Menu")
go_back.add_procedure(lambda: Node.last_node.parent)
# exit application
exit_app = Node(option_label="Exit App")
exit_app.add_procedure(lambda: sys.exit())
Create application-specific menu tree nodes
In the same py file, add in any additional nodes that should be used for your application.
For this example, I have added nodes to select a pet, then greet that selected pet.
A few things to note here:
Store pet selection utilizes the data_ref attribute to store user-selected data inside a Node instance
After a pet is selected, the say_hi_to_pet and say_bye_to_pet nodes utilize this attribute by referencing data_ref from the select_pet node
# //////////////////////////////////////
# ADD APPLICATION-SPECIFIC NODES
pet_names = ["Brooke", "Buster", "Baxter"]
pets = Node(option_label="Pets")
# select pet
def store_pet_selection(ref_node):
user_input, index = pick(pet_names, f"Select pet from the following options")
ref_node.data_ref = user_input
select_pet = Node(option_label="Select Pet")
select_pet.add_procedure(lambda: store_pet_selection(ref_node=select_pet))
# greet pet
def greet_pet(greeting, pet_name):
print(f'{greeting}, {pet_name}!')
print("press any key to continue")
input("")
# say hi to pet
say_hi_to_pet = Node(option_label="Say Hi to Pet")
say_hi_to_pet.add_procedure(lambda: greet_pet("Hi", select_pet.data_ref))
# say bye to pet
say_bye_to_pet= Node(option_label="Say Bye to Pet")
say_bye_to_pet.add_procedure(lambda: greet_pet("Bye", select_pet.data_ref))
Attach nodes to parent elements to create hierarchy of menu items
# attach nodes to parent elements
select_pet.add_children([say_hi_to_pet, say_bye_to_pet, go_back, to_main, exit_app])
pets.add_children([select_pet, to_main, exit_app])
main.add_children([pets, exit_app])
Building the Application
Finally, add the code to run the CLI
# //////////////////////////////////////
# RUN CLI
if __name__ == "__main__":
node = menu.root # set initial node to root
while True:
if node.procedure:
node = node.run_procedure() # invoke callback function if user is at the end of the menu tree
if len(node.children) > 0:
node = node.show_menu() # show next menu if the user is not at the end of the menu tree
Running the Application
In the terminal, run the application by navigating to the the codebase and running “python cli.py”
note: cli.py is the name of the file which includes population of the tree and application code
(3.8.13) jtrapp@LAPTOP-9VMAHRSC:~/example_tree$ python cli.py
The Result
After following these steps you should now be able to create a fully functioning cli application with highly flexible and easy-to-update menus!
Appendix
Full code for example:
#menu_tree.py
from pick import pick
import os
class MenuTree:
'''
A class to create and manage a menu tree for a cli application
Attributes
---------
root: Node instance
- Root node of the tree instance
'''
def __init__(self, node):
'''
Constructs the necessary attributes for the MenuTree object.
Parameters
---------
node: Node instance
- Node to be used for the root of the tree instance
'''
self.root = node
def __repr__(self):
return (
f"<Root Node: {self.root.option_label}>"
)
class Node:
'''
A class to create and manage nodes on a menu tree for rental management application
Attributes
---------
option_label: str
- label when node is displayed as a menu option
title_label: str
- title after node is selected from menu options
menu_tree: MenuTree instance
- menu tree that current instance is connected to
parent: Node instance
- parent of current instance
children: list
- list of child Node instances
procedure: callable object
- function to run when instance is selected from menu
data_ref: class instance
Methods
---------
- add_procedure: validates and adds dictionary with procedure information to instance
- add_child: adds child node to list of children
- add_children: adds multiple children to instance
- show_menu: navigates to next menu based on user selection
- run_procedure: runs procedure stored in instance
'''
last_node = None
def __init__(self, option_label, title_label=None):
self.option_label = option_label
self.title_label = option_label if title_label is None else title_label
self.parent = None
self.children = []
self.procedure = None
self.data_ref = None
def __repr__(self):
return (
f"<Node: {self.option_label}>"
)
def add_procedure(self, procedure):
'''
validates and adds procedure to instance
Parameters
---------
procedure: callable object
- function to be run when node is activated
'''
if callable(procedure):
_procedure = procedure
else:
raise ValueError("Function parameter must be a function")
self.procedure = _procedure
def add_child(self, node):
'''
adds child node to list of children
Parameters
---------
node: Node instance
- node to be added as a child of instance
'''
node.parent = self
self.children.append(node)
def add_children(self, nodes):
'''
adds multiple children to instance
Parameters
---------
nodes: list
- list of Node instances to be added as children of instance
'''
for node in nodes:
self.add_child(node)
def show_menu(self):
'''
navigates to next menu based on user selection
Returns
---------
self.children[index]: Node instance
- Node to display in the application after user selection
'''
Node.last_node = self
os.system('cls' if os.name == 'nt' else 'clear')
user_selection, index = pick([child.option_label for child in self.children], self.title_label)
return self.children[index]
def run_procedure(self):
'''
runs procedure stored in instance
Returns
---------
Node instance
- Node to display in the application after procedure runs
'''
default_next = self if len(self.children) > 0 else self.parent
next_node = self.procedure()
return next_node if next_node else default_next
#cli.py
from menu_tree import MenuTree, Node
import sys
from pick import pick
# //////////////////////////////////////
# INITIALIZE TREE
main = Node(option_label="Main Menu")
menu = MenuTree(main)
# //////////////////////////////////////
# BASIC REUSABLE FUNCTIONALITY
# go to main menu
to_main = Node(option_label="Main Menu")
to_main.add_procedure(lambda: menu.root)
# go to previous menu
go_back = Node(option_label="Previous Menu")
go_back.add_procedure(lambda: Node.last_node.parent)
# exit application
exit_app = Node(option_label="Exit App")
exit_app.add_procedure(lambda: sys.exit())
# //////////////////////////////////////
# ADD APPLICATION-SPECIFIC NODES
pet_names = ["Brooke", "Buster", "Baxter"]
pets = Node(option_label="Pets")
# select pet
def store_pet_selection(ref_node):
user_input, index = pick(pet_names, f"Select pet from the following options")
ref_node.data_ref = user_input
select_pet = Node(option_label="Select Pet")
select_pet.add_procedure(lambda: store_pet_selection(ref_node=select_pet))
# greet pet
def greet_pet(greeting, pet_name):
print(f'{greeting}, {pet_name}!')
print("press any key to continue")
input("")
# say hi to pet
say_hi_to_pet = Node(option_label="Say Hi to Pet")
say_hi_to_pet.add_procedure(lambda: greet_pet("Hi", select_pet.data_ref))
# say bye to pet
say_bye_to_pet= Node(option_label="Say Bye to Pet")
say_bye_to_pet.add_procedure(lambda: greet_pet("Bye", select_pet.data_ref))
# attach nodes to parent elements
select_pet.add_children([say_hi_to_pet, say_bye_to_pet, go_back, to_main, exit_app])
pets.add_children([select_pet, to_main, exit_app])
main.add_children([pets, exit_app])
# //////////////////////////////////////
# RUN CLI
if __name__ == "__main__":
node = menu.root # set initial node to root
while True:
if node.procedure:
node = node.run_procedure() # invoke callback function if user is at the end of the menu tree
if len(node.children) > 0:
node = node.show_menu() # show next menu if the user is not at the end of the menu tree