This question is generic, under-specified. The menu offers two reasonably
clear choices (help and quit) and two that raise questions (view and delete).
View and delete what exactly? Typically operations like that involve additional
arguments: "view database record 123", "delete items older that 10 days", and
so forth. Maybe view and delete refer to "everything", in which case no
arguments are required. But unless we know the nature of the required
arguments, the most natural way to arrange the relationships and data-flow
between main() and the menu functions is not entirely clear.
Don't cause side effects in the menu functions. In spite of that ambiguity,
one general piece advice -- at least in more serious projects where you intend
to subject the code to a battery of automated tests -- is to spend the extra
design time to create a system where the functions doing the real work (the
menu functions in our case) don't cause side effects like printing or exiting.
Instead, arrange things so that those functions take arguments (data) and
return information (more data). Functions like that can be directly tested
without hassle. In the illustration below, that data is wrapped in a Result
object, which can be extended or modified as needed.
Questionable functions: menu_text() and menu(). The former is a
constant pretending to be a function. Just define the constant and be done with
it. The latter is a dict.get() call pretending to be a function. It's also a
function so intimately tied to its caller that it doesn't have much independent
utility. I would suggest moving the dispatching data (the dict) into main()
and then writing a function that does something independently useful: take a
prompt and a valid set of choices (the dict keys in this case) and return a
validated user reply. In the case at hand, I also arranged the function to
return additional arguments in the reply in anticipation that some of the
operations (like view and delete) will require additional parameters.
DRY up the code. In scripts like this, you have various tasks that need to
be dispatched. Those tasks have names, one or more ways for the user to choose
them (eg, a number or first letter from the name), an associated function to
perform the task, and maybe some task-specific help text. Your current
menu_text() and m_help() implementations work against DRYing up the code.
To illustrate a different way of approaching the script, I might start with
some constants and a dataclass similar in spirit to the one suggested in another
review:
import sys
from dataclasses import dataclass
from typing import Callable
WELCOME = '\nWelcome to Micro Menu\n'
MENU_TASK_FMT = '{}] {} ({})'
REPORTING_FMT = '`menu_{}` function was called with {}.'
PROMPT = '>> Make your choice: '
INVALID_CHOICE = 'Invalid choice.'
TASK_HELP = dict(
View = 'View...',
Delete = 'Delete...',
Help = 'Help...',
Quit = 'Quit...',
)
@dataclass(frozen = True)
class MenuTask:
name: str
num: int
func: Callable
help_text: str
@property
def letter(self):
return self.name[0].lower()
Then the task functions might look like this. Currently, they don't do anything
interesting other than report how they were called, except
perhaps in the case of the quit-task.
@dataclass(frozen = True)
class Result:
out: str = None
err: str = None
exit_code: int = None
def menu_view(args):
msg = REPORTING_FMT.format('view', args)
return Result(out = msg)
def menu_delete(args):
msg = REPORTING_FMT.format('delete', args)
return Result(out = msg)
def menu_help(args):
help_text = '\n'.join(t.help_text for t in TASKS)
msg = REPORTING_FMT.format('help', args) + '\n' + help_text
return Result(out = msg)
def menu_quit(args):
msg = REPORTING_FMT.format('quit', args)
return Result(out = msg, exit_code = 0)
You can also define constant holding those tasks. The current definition relies on
a naming convention and globals(). Adjust as desired if you prefer a more
explicit or direct approach.
TASKS = tuple(
MenuTask(
name,
i + 1,
globals()['menu_' + name.lower()],
help_text,
)
for i, (name, help_text) in enumerate(TASK_HELP.items())
)
Finally, we have the portion of the program that cannot easily avoid side
effects: main() and its primary helper functions.
def main():
# Assemble the menu text.
menu_text = WELCOME + '\n'.join(
MENU_TASK_FMT.format(t.num, t.name, t.letter)
for t in TASKS
)
# Set up dispatching data.
dispatch = {
choice : t
for t in TASKS
for choice in (str(t.num), t.letter)
}
choices = tuple(dispatch)
# Perform tasks requested by user.
while True:
print(menu_text)
choice, args = get_reply(PROMPT, choices)
task = dispatch[choice]
res = task.func(args)
act_on_result(res)
def get_reply(prompt, choices):
print(prompt + ': ')
while True:
if xs := input().split():
c, *args = xs
if c in choices:
return (c, args)
print(INVALID_CHOICE)
def act_on_result(res):
# Write messages.
if res.out is not None:
print(res.out)
if res.err is not None:
print(res.err, file = sys.stderr)
# Maybe exit.
if res.exit_code is not None:
sys.exit(res.exit_code)
if __name__ == '__main__':
main()
What do you do if you also want to be able to subject main() and its
immediate helpers to hassle-free automated testing? One approach is to stick
them in a simple class that takes optional parameters for their own stdout,
stderr, and stdin attributes. During automated testing you initialize the
class with io.StringIO objects which can be inspected by the test code. In
real usage, those attributes default the standard streams supplied by sys. The
class never uses print() or input(); instead it writes to its own stdout
and stderr and reads from its own stdin. In addition, the class never calls
sys.exit(). Instead, if main() gets a Result having an exit code it just
breaks from the loop and the function ends in a peaceful fashion. At a certain
point, even the most determined must embrace the conditional.
eval(). I've built several simple menus with conditionals andmatch(), but I thought this was a more readable and simpler approach. Conditional seem messy: computinglearner.com/… The menu was just something I wanted to build to practice with Python, since I've been away from Python for many years. I'm writing a complete tutorial on TinyDB, and wanted a simple menu to demonstrate all the database functions. \$\endgroup\$