Package: meny
MenyMeny is a super light weight framework for creating CLI menusImagine this:You have implemented some functions in some Python file (regardless of the intention of creating a CLI menu).Then you realize it would be nice to have a CLI interface to call said functions.Today is your lucky day! Because Meny is especially designed for this scenario! (which I have encountered surprisingly many times, hence this package). Just install and importmenyand domeny yourfile.pyYou can also usemenyprogrammatically as a python package, of which this README will cover most of its usage.But why exactly this package?There already exists python packages to do so, but seemingly all of them require you torefactoryour code in order to use them, and you need tolearnhow to use their APIs, which is kind of annoying since you just want an convenient interface for your functions. You don't want to spend even more to time to learn yet another library, let alone refactor your code for a CLI menu. Withmenyyou can use the commandmenyor addmeny.menu(locals())to the bottom of your Python file and you are good to go.Of course, this package can do much more which you can see below, but its intention is to cover the "It would be nice to just have a cli menu for my functions now, but its too much effort to make / learn another library" scenario, which I believe it does well.Also, you can define the menus as JSONs!Table of contentsHow to setupNote for Windows usersTerminal interfaceOn JSON filesUsageProgrammatic interfaceSimple examplesCase namesChoosing frontendsSpecial casesArgumentsProgrammatic ArgumentsNested casesReturn valuesWhat if I want to define functions without having them displayed in the menu?Optional: DecoratorReal examplesHow to setupFirst install the package with the command (make sure you have Python 3.7 or higher)pip install menyThen you can importmenyin Python. This will makemenyavailable in your shell. For programmatic use the most central functions in this package aremeny.menuandmeny.title, which will be covered below.This package has been tested on Windows 10, Windows 11, MacOS (idk which version that was), and Ubuntu (18.04, 20.04, 22.04) with Python 3.7, 3.8, 3.9, 3.10Note for Windows usersTL;DR: If you want to have the fancy frontend (like in the gif) dopip install windows-cursesAn original goal for this package was to rely on built-in Python packages only, which it does, for Linux and Mac. This package requires thecurseslibrary to use the fancy frontend. It is built-in with CPython for Linux and Mac installations but not in Windows.menywill still work withoutcursesas it also ships with a simple frontend that only uses the built-inprintfunction.A way to getcursesfor Windows is to installwindows-curses:pip install windows-cursesI use Windows personally andwindows-curseshas worked fine so far. Thewindows-cursessource code is availabe on github andcan be found here.Terminal interfaceAs mentioned above, you can domeny your_python_file.pyin your terminal and it will parse the file and present its functions. For example you have the fileos_example.py:importplatformdefprint_os():operating_system=platform.system()print(f"Your operating is{platform.system()}")returnoperating_systemThen you can domeny print_osand you will see---- Functions in os_example.py ----
1. print_osIf you selectprint_os, the function will be executed and you will see its output and also return value in your terminal:Your operating system is Linux
'Linux'On JSON filesYou can also define your menu as a JSON! The JSON will define the case names along with a terminal command. There is an example file in the repository calledreadme_examples.json:{"English greeting":"echo Hello @name! It is very nice to meet you!","Japanese greeting using braced syntax":"echo Konichiwa @{namae}san! Hajimemashite!","Spanish introduction with default argument":"echo Hola! Me llamo @{nombre=John Wick}!","Weather menu":{"Get weather in current location":"curl wttr.in?0","Get weather in given location":"curl wttr.in/@place?0"}}You can simply run the following command in your shell (assuming you are in the examples directory):meny readme_examples.jsonThen it will create the following menu:As you can see it is possible to specify parameters in the json by using@thisSyntaxor@{thisSyntax}, and even parameters with default arguments like@{this=123}. The braced syntax is usefull when you want an argument to be directly adjacent to other letters as you see in the Japanese greeting example.UsageIt easiest to explain the fundamental idea with the simple frontend, which will look something like this:---- Title ----
1. functionA
2. functionB

input:To select a case you specify its number on the left. For example to runfunctionA, type1then press enter.To go back press enter without any inputEnterhto see usageWhen you use the fancy frontend you can traverse the options using your arrow keys (which will save the hassle of typing which function you want to run). You can find how to switch between fancy and simple frontendhere.Programmatic InterfaceSimply implement the menu cases (as functions) in a Python file, then to initialize the interface you simply call themenufunction after you have defined your functions.frommenyimportmenu...menu(locals(),title=' Main menu title here ')Thelocals()function is a Python built-in function that returns a dictionary with variable names as keys and the corresponding objects as values from the local scope. You can import whatever modules, classes, and functions you want in the file without them interfering with the functions defined in your file. The order of the cases is by definition order.The function signature ofmenualong with its docstring is as follows:
Factory function for the CLI class. This function initializes a menu.defmenu(cases:Union[Iterable[FunctionType],Dict[str,FunctionType],ModuleType],title:Optional[str]=None,*,case_args:Optional[Dict[FunctionType,tuple]]=None,case_kwargs:Optional[Dict[FunctionType,dict]]=None,decorator:Optional[FunctionType]=None,frontend:Optional[str]=None,on_blank:Optional[str]=None,on_kbinterrupt:Optional[str]=None,once:Optional[bool]=None,return_mode:Optional[str]=None,)->Dict[str,Any]:Parameterscases: can beDict[str, FunctionType]: a dictionary where keys are functions names and values are functionsIterable[FunctionType]an iterable of functionsModuleType: a module containing functionstitle: title of menucases_args: dictionary with function as key and tuple of positional arguments as valuescases_kwargs: dictionary with function as key and dict of keyword arguments as valuesonce: If you want menu to return after a a single choice.on_blank: What to do the when given blank input. Available options are:"return", will return to parent menu"pass", does nothing. This should only be used for the root menu.on_kbinterrupt: Behavior when encountering KeyboardInterrupt exception when the menu is running.
If"raise", then will raiseKeyboardInterrupt, if"return"the menu returns.decorator: Decorator to applied for all case functions.frontend: specify desired frontend:"auto": Will try to use fancy frontend if curses module is available, else
use simple frontend (default)"fancy": Use fancy front end (if on Windows, install
windows-curses first or Python will not be able to find the required"curses"package that the fancy frontend uses)"simple": Use the simple (but compatible with basically everything) frontendreturn_mode: the dictionary structure to be returned after the menu is done running. Only effective
menu is root menu, as nested menus will use root's. Return mode options are:"flat": This is the default. Returns dictionary with function names (asstr)
as keys, and their return values as values (if they have been called), if not their names
will not be in the dictionary (see examples). The downside of this return mode is if you have
nested menus, where the nested menus reuse function names in from parent menus. The
parent menus may overwrite the return values from the nested menus."tree": Returns a nested dictionary structure, representing the structure of nested menus
(if you have that).ReturnsDict[str, Any]: Dictionary where functions names are keys, and values are anything. Represents return
values of case functions.Examples>>>defreturnsOne():...defreturnsOne():...return"1"...menu(locals())...return1...>>>defreturnsTwo():...return2...>>>returns=menu(locals(),return_mode="flat")# Assume we have entered all cases and returned{"returnsOne":1,"returnsTwo":2,}>>>returns=menu(locals(),return_mode="tree")# Assume we have entered all cases and returned{"returnsOne":{"returnsOne":{"return":"1"},"return":1},"returnsTwo":{"returns":2},}>>>returns["returnsOne"]["returnsOne"]["return"]'1'Simple examplesSay we are editingconsole.pyfromrandomimportrandintfromtimeimportsleepimportmeny# fizzbuzz() and random_integer() are just examples, you can make whatever you want :)deffizzbuzz():foriinrange(21):stringy=''fizz=i%3==0buzz=i%5==0iffizz:stringy=stringy+'Fizz'ifbuzz:stringy=stringy+'Buzz'ifnot(fizzorbuzz):stringy=iprint(stringy)sleep(0.1)defrandom_integer():print(randint(0,9))sleep(1)meny.menu(locals(),title=' Main menu ')will result with this when running:python console.py:-------------- Main menu ---------------
1. fizzbuzz
2. random_integer

Input:You then specify which case you want to run by entering the input number as the first token. The tokens after (delimited by space) will be passed to the case function as positional arguments. The argument tokens will be evaluated as Python literals.Choosing frontendsThere are two frontends implemented; the simple frontend and the fancy frontend. The selection of frontend will be selected based on the detected operating system. One can pass the choice of frontend:menu(..., frontend="auto"). The possible choices areauto: Will try to use the fancy front end (usingcurses) by checking if thecursesmodule is available, else use simple frontend.simple: Use simple frontend, should work on all systems since it is completely based on the built-in print function. Use by typing the corresponding key (e.g. 1) to the displayed cases and press enter.fancy: Use fancy frontend, will raiseImportErrorifcursesis unavailable. The fancy frontend is "fancy" as in it gives visual indicators on what you are doing, and also adds the ability to traverse the options using thearrow keyssuch that you don't have to type the number yourself.It is possible to override the default frontend throughout the Python program by doingimportmenymeny.set_default_frontend("auto")# auto, fancy, or simpleas opposed to specifying the choice of frontend for everymeny.menu(..., frontend="...")call.Case namesBy default, it will use the function names as the case names. However, you can use themeny.titledecorator to apply a title that will be used instead:fromrandomimportrandintfromtimeimportsleepimportmeny# fizzbuzz() and random_integer() are just example functions, you can make anything you want.@meny.title("FizzBuzz!")deffizzbuzz():foriinrange(21):stringy=''fizz=i%3==0buzz=i%5==0iffizz:stringy=stringy+'Fizz'ifbuzz:stringy=stringy+'Buzz'ifnot(fizzorbuzz):stringy=iprint(stringy)sleep(0.1)@meny.title("Get random integer")defrandom_integer():print(randint(0,9))sleep(1)meny.menu(locals(),title=' Main menu ')Which will produce:-------------- Main menu ---------------
1. FizzBuzz!
2. Get random integer

Input:Special casesEntering..is equivalent to just pressing enter with an empty input. I implemented this because I just had
a habit of writing..to "change directory" to the previous directory.Enteringqwill exit the menu interface.Enteringhwill display this text that explains the special cases.Enter-1or any integer will "reverse" the choices, such that you take the last choice. This is inspired by Python lists where you can index likelist[-1]Enteringrwill restart thewholePython program. This is usefull when debugging such that one can easily refresh code changes.Enteringawill run all cases in the current menu.ArgumentsThe cases can take arguments as well!importmenyfromtimeimportsleepdefaddints(a,b):print(a+b)sleep(1)defappendstrings(a,b):print(a+b)sleep(1)# Type hints won't interfere with meny, and will actually be displayed when using the fancy frontend@meny.title("Print elements and their types")defdisplaylist(a:list):[print(f'Element{i}:{elem}, type:{type(elem)}')fori,eleminenumerate(a)]sleep(1)meny.menu(locals(),title=' Main menu ')Then simply give the arguments along with the choice:-------------- Main menu ---------------
1. addints
2. appendstrings
3. Print elements and their types


Input: 1 60 9

69Input: 2 "cat and dog" mathemathics

cat and dogmathemathicsInput: 3 ['cat', 69, 420.0]

Element 0: cat, type: <class 'str'>
Element 1: 69, type: <class 'int'>
Element 2: 420.0, type: <class 'float'>Programmatic ArgumentsYou can supply arguments programmtically to your case functions:importmenydefprogrammatic_args(a,b,c,d):print(a,b,c,d)sleep(1)case_args={programmatic_args:(1,2)}case_kwargs={programmatic_args:{"d":4,"c":3}}meny.menu(locals(),case_args=case_args,case_kwargs=case_kwargs)Functions that takes arguments programmatically cannot take arguments through the cli, that is you cannot both supply programmatic arguments as well as arguments through the cli. In that case the menu will raisa a MenuError.Nested casesIf you want to implement nested cases, then you can simply reuse the menu function in the function scope.frommenyimportmenudefparent():defchild1():passdefchild2():passmenu(locals(),title=' Title here ')menu(locals(),title=' Main menu ')You can create another module for the other cases and pass them as well:frommenyimportmenuimportother_casesdefsamplecase():'''Foo'''menu(other_cases,title=' Title here ')menu(other_cases,title=' Main menu ')or you can give a list of functions, which will enable you to force the ordering of the cases as well:importmenydefparent2():defchild1():passdefchild2():passmeny.menu([subcase2,subcase1],title=' Title here ')meny.menu(locals(),title=' Main menu ')Return valuesThe menu will store the return values of the case functions (if you have entered the cases). The usage
is explained in thedocstring.What if I want to define functions without having them displayed in the menu?Easy! Simply apply themeny.ignoredecorator on functions to makemenyignore them. You can also create a class of static methods to hide functions within a class since classes will be ignored bymenyanyways. This problem is also naturally avoided if just specifies the functions manually either using adictorlist.importmeny@meny.ignoredefignored():passOptional: DecoratorTo enforce a common behavior when entering and leaving a case within a menu, you give a decorator to themenufunction. However, it is important that the decorator implements the__wrapped__attribute (this is to handle docstrings of wrappers as arguments for wrapped functions). Generally, it should look like thisimportsleepfromfunctoolsimportwrapsdefcase_decorator(func):'''Decorator to enforce commmon behavior for cases'''@wraps(func)# VERY IMPORTANT TO WRAP FUNCTIONS TO ENSURE THAT# case_wrapper.__wrapped__ is set properlydefcase_wrapper(*args,**kwargs):'''Verbosity wrapper'''print('Yeah! Going in!')sleep(1)retobj=func(*args,**kwargs)print('Woah! Going out!')sleep(1)returnretobjreturncase_wrapperIt can then easily be applied to all functions like so:frommenyimportmenufromcase_decoratorimportcase_decorator# A lot of cases heremenu(locals(),decorator=case_decorator)Real examplesHere are some applications that I have implemented usingmeny:Stock data pipelineData scraping and data cleaning pipeline for stock data------------- Mulababy420 --------------
1. Update all data
2. Obtain Oslo Bors quotes and returns
3. Scrape Oslo bors HTML files
4. Scrape Yahoo Finance
5. Backup current data
6. Exit program
Enter choice:Sometimes I don't want to run everything at once. Maybe I just want to backup data instead of doing all everything.menywill enable a very quick implementation of a console.
Without the console I would need to find the right file to run (and maybe comment things out first as well). The console organizes everything into one place.Database interface------BergenDB------
1. Log rent
2. Log power
3. Print table
4. Plot table
5. Commit changes
6. Discard changes
7. ExitI log my rent and power bills in a SQL database. I have made a Python API to manage the database, and I just do everything through the interface. No need to script anything or write any SQL queries.Control your Google Compute VM----------------GCE-----------------
1. SSH to personal instance
2. SSH to project instance
3. start/stop personal instance (0 to stop, 1 to start, 2 to restart)
4. start/stop project instance (0 to stop, 1 to start, 2 to restart)
5. Get status personal instance
6. Get status project instance
7. Other instance control

Entering blank returns to parent menu
Input: