Python Forum
[Intermediate] Command Line Interfaces
Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Intermediate] Command Line Interfaces
#1
This tutorial is about command list interfaces, or CLIs. These were some of the earliest of computer interfaces. They are easier to put together than GUIs: you can put them together with some print and input statements, rather than needing a whole host of class instances for the various parts of the interface. This makes them useful for prototyping, test environments, and new programmers.

CLIs involve the user typing in some text, and getting some text back in response. They may also interact with other systems. For example, you can use the console on most systems to interact with the file system, changing the contents of your hard drive. I'm going to start with a simple menu system, where you are given a list of choices and can only type one character to cause one of the choices to run. Later on I will show you a more versatile command based system.

All of the programs I am going to reference in this tutorial are available on [url=https://github.com/ichabod801/cli_tutorial Github]. This post uses the menu_funcs.py program.

So we'll start with three functions that we want to put into a menu. Each of these functions recreates a Monty Python skit.

import random
import time

def argument():
    """
    An intellectual discussion. (None)

    No it isn't.
    """
    # Prep the argument.
    start = time.perf_counter()
    user_text = input('Please state an assertion to argue about: ')
    # Argue for two minutes
    while time.perf_counter() - start < 120:
        # Automatically gainsay whatever the user says. 
        user_words = user_text.lower().split()
        for negative in ('no', 'not', "isn't", "ain't", "doesn't", "wasn't"):
            if negative in user_words:
                user_text = input('Yes it is. ')
        else:
            user_text = input("No it isn't. ")
    # Say goodbye.
    print("I'm sorry, your five minutes is up.")
    input('Press Enter to continue: ')

def knight():
    """Some vigorous exercise. (None)"""
    # Set up the combat.
    limbs = ['other leg', 'leg', 'other arm', 'arm']
    combat = False
    # Loop while the knight has limbs.
    while limbs:
        # None shall pass.
        if not combat:
            print('None shall pass.')
        # Get the user's action.
        user_action = input('What do you do? ')
        # Attacking chops off a limb.
        if user_action.lower() == 'attack':
            print("Excellent attack. You chop off the black knight's {}".format(limbs.pop()))
            combat = True
        # Anything else after attacking provokes an attack.
        elif combat:
            print('The black knight attacks, but you easily block his blow.')
    # Say goodbye.
    input('Press Enter to call it a draw: ')

def spam():
    """Fine dining. (None)"""
    # Get the user's order.
    food = input('What would you like to eat? ')
    # Prepare the meal.
    pre_spam = ['spam'] * random.randint(2, 4)
    post_spam = ['spam'] * random.randint(0, 2) + ['and spam.']
    meal = pre_spam + [food] + post_spam
    # Deliver the food and say goodbye.
    print('Here is your ' + ', '.join(meal))
    input('Press Enter to eat a wafer thin wafer and explode: ')
As I said, we can make a menu with just some simple print and input statements (we also need a loop). Here is a function that implements a very simple method with just those tools:

def simple_menu():
    """
    The most basic of menu functions. (None)
    """
    # Loop until the user quits.
    while True:
        # Show the menu.
        print('A: Have an intellectual discussion.')
        print('B: Get some vigorous exercise.')
        print('C: Enjoy some fine dining.')
        print('D: Quit.')
        # Get the user's choice.
        choice = input('Please enter your choice: ').lower()
        # Process the user's choice.
        if choice == 'a':
            argument()
        elif choice == 'b':
            knight()
        elif choice == 'c':
            spam()
        elif choice == 'd':
            break
        else:
            # Handle invalid choices.
            print('That is not a valid choice.')
    # Be polite.
    print('Have a nice day.')
Everything is in a while loop, until the user selects the quit option and we break out of the loop (line 22). Each time through the loop we print the menu with a bunch of print statements, get the user's choice with an input statement, and then use an if/elif/else block to call the appropriate function based on the user's choice. The final else handles anything the user types in that doesn't fit one of our pre-defined choices.

This works, but it is very specific. It only works for one set of functions. We can't simply reuse it, we would have to rewrite it every time we want to use it. But this is just a mapping between choices (A), descriptions (Have an intellectual discussion), and functions (argument). Why not make a function that accepts such a mapping, and generates the menu from the mapping? Then we can pass any menu definition we want to the menu. Here is such a function:

from collections import OrderedDict
import string


def menu(menu_data, prompt = 'Please enter your choice: '):
    """
    A generic menu function. (None)

    The menu_data dictionary keys are strings that are shown in the menu. The 
    values of the dictionary should be functions that are called when the menu
    item is chosen. If the order of the menu items is important, this should
    be an OrderedDict from collections.

    Parameters:
    menu_data: The menu items and actions for the menu. (dict of str: callable)
    prompt: The text for getting a menu choice from the user. (str)
    """
    # Set up the menu.
    menu_choices = OrderedDict(zip(string.ascii_uppercase, menu_data.keys()))
    quit_char = string.ascii_uppercase[len(menu_data)]
    # Loop until the user quits.
    while True:
        # Display the menu.
        for char, menu_text in menu_choices.items():
            print('{}: {}'.format(char, menu_text))
        print('{}: Quit\n'.format(quit_char))
        # Get the response.
        choice = input(prompt).upper()
        # Handle the response.
        if choice == quit_char:
            break
        elif choice in menu_choices:
            menu_data[menu_choices[choice]]()
        else:
            print('That is not a valid choice.')
    # Say goodbye.
    print('Have a nice day.')

if __name__ == '__main__':
    monty_menu = [('Have an intellectual discussion.', argument), ('Get some vigorous exercise.', knight),
        ('Enjoy some fine dining.', spam)]
    menu(OrderedDict(monty_menu))
Now we have a generic menu function. It takes the menu data as a parameter. The data is a dictionary of the functions to be called keyed to the descriptions to be shown on the menu. Note that we don't pass in the letters of the choices. The menu function sets the letters for us on the first line. That means if we pass a normal dictionary of menu data, we can't be sure which letters will go with which letters. I got around that by passing the menu data as an OrderedDict from the collections package. Also note that I did not pass in a quit option. That is created automatically by the menu function as the quit_char variable.

The loop is another while True: loop. First it displays the menu, using OrderedDict it created with the characters in the menu. Then it prints the quit option last. Getting the user's choice is a simple input statement (with upper for case insensitivity). Then it just has to check for the quit char, a valid choice, or an invalid choice. When it gets a valid menu choice, it gets the description from the menu_choices dict using the user's input, then gets the function to call from the menu_data dictionary using the description it just got. Note that the actual body of the menu function is shorter than the body of the simple_menu function, for a menu with only four options.

Now either of these functions can be used to create a tree of menus, where choosing an item in the first menu will bring you two a second menu. For the simple menu function, you just write another menu function and call it. For the general menu function, you just have to write a helper function that sets up the menu data for the second menu, and then uses it to call the menu function again. That helper function goes into the menu_data that describes the first menu. In either case, once the sub-menu function is done processing, it will just pass control back to the first menu function.

While the menu function is better than the simple_menu function, there are still some problems with it. The big one is that it has no state. Generally when you have an interface, its for some object or system in the background, and the interface allows you to manipulate what's in the background. Like you might have a menu in a game to allow you to buy equipment for your character. So you are modifying the character's inventory (and wallet) every time through the loop. You can do this with function by passing a character object back and forth between the functions. But there's just one function call in menu: menu_data[menu_choices[choice]](). To make that generalizable, you'd need to use *args and **kwargs, or maybe pass a dictionary back and forth. But then you might be passing around a lot of unnecessary information, if you functions don't all work on the same parts of the state being passed around. A better solution (IMHO) is something that quietly maintains it's own state: a class. Next we'll look at making a menu class. If you are not up to speed on classes, check out the [url=https://python-forum.io/Thread-Class-Basics Class Tutorial] in the tutorials section first.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#2
This is just some random idea I came up with for a simple menu that has a state that needs to be tracked, so I can highlight the use of a class for making a menu (the code for this post is in menu_class.py on the GitHub repository):

"""
menu_class.py

A simple menu example with a class.

Classes:
Menu: A basic menu interface. (object)
"""

from collections import OrderedDict
from string import ascii_uppercase

class Menu(object):
   """
   A basic menu interface for an integer graph. (object)

   Class Attributes:
   menu_data: The menu descriptions and function names. (dict of str: str)
   primes: The prime numbers up to the first one over 100. (list of int)

   Attributes:
   choices: The letter choices and their descriptions. (OderedDict of str: str)
   numbers: The current sequence of numbers. (list of int)
   prompt: The text displayed when requesting a user's choice. (str)
   quit_char: The letter choice for exiting the menu loop. (str)

   Methods:
   collatz: Collatz the last number in the sequence and append it. (None)
   fibonacci: Add the last two numbers in the sequence and append the sum. (None)
   menu_loop: Loop through menu choices, performing the relevant actions. (None)
   prime: Append the next highest prime to the sequence. (None)

   Overridden Methods:
   __init__
   """

   # The menu descriptions and function names.
   menu_data = OrderedDict([('Add the last two numbers.', 'fibonacci'), 
        ('Get the next prime number.', 'prime'), ('Collatz the last number.', 'collatz')])
   # All of the prime numbers up to just over 100.
   primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89]
   primes += [97, 101]

   def __init__(self, prompt='Please enter your choice: '):
       """
       Set up the class attributes. (None)

       Parameters:
       prompt: The text displayed when requesting a user's choice. (str)
       """
       # Store the parameters.
       self.prompt = prompt
       # Set up the menu.
       self.choices = OrderedDict(zip(ascii_uppercase, self.menu_data.keys()))
       self.quit_char = ascii_uppercase[len(self.menu_data)]
       # Set up the sequence information.
       self.numbers = [0, 1]

   def collatz(self):
       """Collatz the last number in the sequence and append it. (None)"""
       # Triple and add one to odd numbers.
       if self.numbers[-1] % 2:
           self.numbers.append(self.numbers[-1] * 3 + 1)
       # Halve even numbers.
       else:
           self.numbers.append(self.numbers[-1] // 2)

   def fibonacci(self):
       """Add the last two numbers in the sequence and append the sum. (None)"""
       self.numbers.append(self.numbers[-1] + self.numbers[-2])

   def menu_loop(self):
       """Loop through menu choices, performing the relevant actions. (None)"""
       # Loop until the user quits or the sequence goes over 100.
       while self.numbers[-1] < 100:
           # Display the menu.
           for char, menu_text in self.choices.items():
               print('{}: {}'.format(char, menu_text))
           print('{}: Quit\n'.format(self.quit_char))
           # Get the response.
           choice = input(self.prompt).upper()
           # Handle the response.
           if choice == self.quit_char:
               break
           elif choice in self.choices:
               method = self.menu_data[self.choices[choice]]
               getattr(self, method)()
               print('The current number is {}.'.format(self.numbers[-1]))
           else:
               print('That is not a valid choice.')
       # Clean up.
       print('Have a nice day.')

   def prime(self):
       """Append the next highest prime to the sequence. (None)"""
       self.numbers.append([p for p in self.primes if p > self.numbers[-1]][0])

if __name__ == '__main__':
   menu = Menu()
   menu.menu_loop()
The Menu class we see above closely matches the menu function we saw in the last post. It has a class attribute named menu_data, which stores similar data about the menu to be displayed. The menu choices are created the same way (but in the __init__ method). The main loop (in the menu_loop method) displays the menu the same way, gets the user input in the same way, processes the menu choices in (mostly) the same way, and exits the loop in the same way. Instead of unbound functions, it now calls bound methods of the class. Now they are stored by name rather than directly, and we have to use getattr to get them before calling them.

But now we have some state information. There is the primes class attribute, which is used by the prime method. We also have a numbers attribute that is manipulated by the tree methods. There are three methods we use: collatz, fibonacci, and primes. Each one append to the numbers attribute. Collatz modifies the number as in the Collatz Conjecture, fibonacci adds the last two numbers together as in the Fibonacci Sequence, and prime just appends the smallest prime number that is greater than the last item in the sequence.

So the menu lets you choose which way to get the next number, appends that number to the sequence, and tells you what the number is. It works fine as a menu which maintains a state. However, we have lost the ability to generalize with this class. The while loop is no longer a while True: loop, it has a condition that is specific to this menu. Likewise, after the method selected by the user is called, the class prints out information that is specific to this menu. So we've taken a step forward in terms of having a state, but we've taken a step back in terms of generalization. This is certainly fixable, and I will fix it. But first I want to show you a different kind of CLI, a command interface. As it turns out, there is a command based CLI package that comes with Python. I'm going to show you how it works, and how it works will inform how I fix the Menu class.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#3
I did say that the three functions I came up with for the last example was just some random idea. But I liked it. I became intrigued by it. So I'm going to take a short digression to talk about it.

The quickest way to end the loop is CACACA and then A or C: [0, 1, 4, 5, 16, 21, 64, 95, 159 or 286]. Collatzing gives you the biggest number from an odd number. Adding gives you the biggest number from an even number (generally), and if the next-to-last number was odd it gives you an odd number to Collatz.

The biggest number you can get is obviously to Collatz 99 to get 298. There are three shortest ways to do that:

Output:
[0, 1, 4, 5, 16, 21, 23, 44, 22, 66, 33, 99, 298] [0, 1, 4, 5,  9, 11, 20, 23, 43, 66, 33, 99, 298] [0, 1, 4, 5,  7, 11, 18,  9, 27, 36, 63, 99, 298]
I find it interesting that they all start the same. Since there are three of them and three functions, that means that once you get to [0, 1, 4, 5], no matter what you do at that point you can still get to the shortest maximum. Even more interesting is that the first two end the same. So they split on different paths, and then come back to the same one to finish.

As far as I can tell, there are only 307 pairs of numbers that you can get to. And yet there is a huge number of possible sequences, even if you reject loops (or terminate on a loop). Obviously, if you don't reject loops, there's an infinite number of sequences. I tried calculating them all with Python. After ten minutes the first several digits hadn't changed, so I gave up.

In all of those 307 pairs, the numbers 62 and 74 do not show up at all. I'm assuming that's because of the limit at 100. If you increased that, perhaps you could get to 124, and then Collatz back down to 62. But that makes wonder: for each terminal number t, how many numbers from 0 to t are missing, and what numbers are they?

The 307 pairs are the nodes in the graph, or as I call it, the integer graph. We have integer sequences. There's a whole encyclopedia of them online. Why can't we have integer graphs? We can generalize this idea of an integer graph. Didn't I show you that generalizing the menu function was a good idea? Therefore generalizing the idea of an integer graph is a good idea. So, an integer graph is a graph where the nodes are integer tuples of length n, starting with a specific tuple (traditionally (0, 1, ..., n-1), but it may be different.). Each edge leading from a tuple node is associated with one or more of m sequence functions (f1, f2, ..., fm) that take the tuple (x1, x2, ..., xn) as an input, and return a new tuple (x2, x3, ..., xn+1), with xn+1 being a new number generated by the function. Finally, each integer graph has a terminal number t such that any node containing a number greater than or equal to t has no edges leading from it (t is traditionally 108. Yeah, I know I used 100 the first time, but 108 is a much cooler number. t can be infinity if you really want.) Each integer graph gives rise to a set of integer sequences, being paths through the graph from the initial node to a terminal node or a loop.

I now return you to your regularly scheduled tutorial.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#4
As I was saying before we were so rudely interrupted, there is a command based CLI that comes with Python. The menu based systems that I have shown you so far are like doing everything based on multiple choice questions. That limits you significantly. With a command based system, you have more options. It's more like short answer than multiple choice. The commands themselves are not really any different than the menu choices, but you can add arguments to them, and vary the actions and responses based on the arguments.

The command based system that comes with Python is the cmd package. It has one class, Cmd, which provides a basic command processing loop. It also provides "hook" methods that allow you to customize how it processes the commands.

The basic tool of the Cmd class is do methods. Say you type in 'foo 23 bar'. The Cmd class splits that in to the command ('foo') and the arguments ('23 bar'). Then it searches for the 'do_foo' method, and runs it with the arguments as a parameter. If no do_foo method has been provided to the Cmd subclass, it sends the full input to the 'default' method for processing.

Here is a simple maze that I put together as a Cmd subclass (this code is in cmd_example.py on the GitHub repository):

"""
cmd_example.py

An example of using the cmd module.

This will be a simple maze game, using the below maze stolen from Wikipedia.

+   +-------+-----------+---------------+
|           |           |               |
|   +---+   |   +---+   +   +---+---+   |
|   |       |       |       |   |       |
|   +---+---+---+   +---+   +   |   +---+
|       |       |       |       |   |   |
+---+   |   +---+   +   +---+---+   +   |
|           |       |       |           |
|   +-------+   +   +---+---+   +-------+
|               |       |               |
+---------------+-------+-----------+   +

Constants:
MAP: What directions you can move from each cell in the maze. (list of list)
MAZE: The details of the maze to solve. (dict)

Classes:
Maze: A maze game. (cmd.Cmd)
"""

import cmd
import random

# What directions you can move from each cell in the maze.
MAP = [['se', 'ew', 'ws', 'es', 'we', 'ws', 'es', 'we', 'we', 'ws'],
    ['sn', 'e', 'wn', 'ne', 'sw', 'ne', 'wns', 's', 'se', 'nw'],
    ['ne', 'sw', 'se', 'w', 'nse', 'ws', 'en', 'wn', 'ns', 's'],
    ['se', 'wen', 'wn', 'se', 'nsw', 'ne', 'w', 'se', 'ewn', 'wn'],
    ['ne', 'ew', 'ew', 'nw', 'ne', 'w', 'e', 'wen', 'ew', 'w']]
# The details of the maze to solve.
MAZE = {'map': MAP, 'start': (0, 0), 'end': (9, 4)}

class Maze(cmd.Cmd):
    """
    A maze game. (cmd.Cmd)

    Class Attributes:
    directions: Abbreviations for movement directions. (dict of str: str)

    Attributes:
    map: What directions you can move from each cell in the maze. (list of list)
    start: The starting coordinates of the player. (tuple)

    Methods:
    do_east: Move to the east. (bool)
    do_north: Move to the north. (bool)
    do_south: Move to the couth. (bool)
    do_west: Move to the west. (bool)
    move: Move in the maze. (bool)
    ow: Bump into a wall. (None)
    show_directions: Show the ways the player can move. (None)

    Overridden Methods:
    preloop
    """

    directions = {'e': 'east', 'n': 'north', 's': 'south', 'w': 'west'}

    def do_east(self, arg):
        """Move to the east. Add an integer argument to move multiple times."""
        return self.move('e', 1, 0, arg)

    def do_north(self, arg):
        """Move to the north. Add an integer argument to move multiple times."""
        return self.move('n', 0, -1, arg)

    def do_south(self, arg):
        """Move to the south. Add an integer argument to move multiple times."""
        return self.move('s', 0, 1, arg)

    def do_west(self, arg):
        """Move to the west. Add an integer argument to move multiple times."""
        return self.move('w', -1, 0, arg)

    def move(self, check, delta_x, delta_y, arg):
        """
        Move in the maze. (bool)

        Parameters:
        check: The character for checking for valid directions. (str)
        delta_x: How much to move on the x axis. (int)
        delta_y: How much to move on the y axis. (int)
        arg: The arguments passed with the movement command. (str)
        """
        # Check for moving multiple times.
        if arg.isdigit():
            times = int(arg)
        else:
            times = 1
        for movement in range(times):
            # Check for a valid move
            if check not in self.current:
                self.ow()
                break
            # Update the player's postion.
            self.x += delta_x
            self.y += delta_y
            self.current = self.map[self.y][self.x]
            print('moving...')
            # Check for solving the maze.
            if (self.x, self.y) == self.end:
                print('You made it out of the maze!')
                return True
        # Show the next location if not solved.
        self.show_directions()

    def ow(self):
        """Bump into a wall. (None)"""
        print('Ow! You bump into a wall.')

    def preloop(self):
        """Prep for the command loop. (None)"""
        # Extract the maze data.
        self.map = MAZE['map']
        self.x = MAZE['start'][0]
        self.y = MAZE['start'][1]
        self.end = MAZE['end']
        # Set the starting location for validity checks.
        self.current = self.map[self.y][self.x]
        # Print an introduction.
        print('You are in a maze.')
        print('You have a torch, but it barely lights past the end of your hand.')
        # Show the current location.
        self.show_directions()

    def show_directions(self):
        """Show the ways the player can move. (None)"""
        # Get the valid moves.
        direction_words = [self.directions[direction] for direction in self.current]
        # Select message based on number of valid moves.
        if len(self.current) == 1:
            message = 'You are in a dead end. You can only move to the {}.'
        elif len(self.current) == 2:
            message = 'You are in a hallway. You can move {} or {}.'
        elif len(self.current) == 3:
            message = 'You are at an intersection. You can move {}, {}, or {}.'
        elif len(self.current) == 4:
            message = 'You are in an open space. You can move {}, {}, {}, or {}'
        # Display the message.
        print(message.format(*direction_words))

if __name__ == '__main__':
    maze = Maze()
    maze.cmdloop()
So let's walk through the program. At the start, we have some global constants defining the maze. The map of the maze is a matrix of the locations in the maze, and what moves are possible from each location. The full maze definition includes the map, the starting square, and the ending square. Ideally, we would have a bunch of files with different mazes and a way to load them, or an algorithm that generated mazes. I just put in a simple maze for a test case.

We create a Maze class, inheriting from the Cmd class. There is one class attribute, which just stores abbreviations for the possible moves. This is used in the show_directions method, which gives a description of the possible moves from the current location.

The first methods are four do_ methods, one for each possible move. These will be called when the user enters one of the directions to move. Each one gives details of the movement to the move method that does the actual moving: the character for checking the movement is valid, the change in the x coordinate, the change in the y coordinate, and any arguments that were passed with the movement command.

Next is the move method that does the actual movement. It first checks the arguments the used typed in to see if they typed an integer. If so, that is taken a number of times to move. Otherwise the argument is ignored and the player moves once. The validity of each move is checked against the map definition. If the move invalid movement is stopped with a message. If the move is valid the current coordinates and location information is updated, and a message indicating successful movement is printed. Finally, there is a check to see if the maze has been solved by getting to the defined end of the maze.

Note that if the maze is solved, the move method returns True, otherwise it returns None. The return value of the move method is returned by each of the do_ methods. If a do_ method returns True, command processing is stopped. If it returns something that evaluates to False (like None), command processing continues. So in this case, command processing continues until the maze is solved.

Next is an ow method, which is used for an invalid move. Its one print statement doesn't really merit its own method, but I put it there for potential expansion. Maybe later in development you lose a hit point when you hit a wall or something.

Now we get to the preloop method. This is the only method from the parent Cmd class that we mess with. It is a stub method in Cmd that does nothing. It is run before the command processing loop starts, and is there to allow set up of any attributes needed for command processing. The Maze class uses it to read the maze from the global variables, to set up the starting location for the player, and to give some introductory text.

Finally there is the aforementioned show_directions method that gives the user information about the current location.

To get the whole thing rolling, we create an instance of the Maze class and call its cmdloop method. The cmdloop method is inherited from the Cmd class. It handles a loop of printing a prompt, getting a response from the user, parsing the response, calling the appropriate method, and checking the return value to see if it should stop processing commands. It also handles pre and post loop processing. If you play the game you can even type 'help north'. It will give you the docstring of the do_north method as help text. All of this is handled by code in the Cmd class, we don't have to mess with it at all.

Now, there's a lot more we can mess with. So in the next post we will look at a lot of ways we can use the attributes and methods provide by the Cmd class to simplify our code and improve our maze program.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#5
So let's make more use of the Cmd class features. (The code for this post is in cmd_example2.py on the GitHub repository) The first thing you might have noticed is that when the program wants a command from you, it prints '(Cmd) '. That doesn't really fit our program. However, we can change what it prints there by modifying the prompt class attribute:

class (Maze):
    # ...

    directions = {'e': 'east', 'n': 'north', 's': 'south', 'w': 'west'}
    prompt = 'In the maze: '
There is also and intro class attribute of Cmd. If it is non-empty it is printed before the command processing starts. So we can move our introductory text to a class attribute as well.

class (Maze):
    # ...

    directions = {'e': 'east', 'n': 'north', 's': 'south', 'w': 'west'}
    intro = 'You are in a maze.\nYou have a torch, but it barely lights past the end of your hand.'
    prompt = 'In the maze: '
And we can remove that from our preloop method:

def preloop(self):
    """ Prep for the command loop. (None)"""
    # Extract the information from the MAZE global.
    self.map = MAZE['map']
    self.x = MAZE['start'][0]
    self.y = MAZE['start'][1]
    self.end = MAZE['end']
    # Get the moves for the start position
    self.current = self.map[self.y][self.x]
    self.show_directions()
If you try this, you will note that the intro text gets a bit messed up:

Output:
You are in a hallway. You can move south or east. You are in a maze. You have a torch, but it barely lights pas the end of your hand.
The order of our text as changed. It doesn't really make sense to describe the current spot you are in in the maze before you describe that you are in a maze. The problem here is that the Cmd class prints the intro after it processes preloop. To get around that we can modify intro in preloop:

def preloop(self):
    """ Prep for the command loop. (None)"""
    # Extract the information from the MAZE global.
    self.map = MAZE['map']
    self.x = MAZE['start'][0]
    self.y = MAZE['start'][1]
    self.end = MAZE['end']
    # Get the moves for the start position
    self.current = self.map[self.y][self.x]
    self.intro = '{}\n{}'.format(self.intro, self.show_directions())
Note that you can also modify intro by passing a non-None intro parameter to the cmdloop method when you call it. That will replace the intro attribute, but if you are modifying it in preloop, those modifications will still apply.

There are other class attributes that Cmd uses. Most apply to how the default help is displayed. There is also identchars, which specifies which characters are allowed for command names. The first non-indentchar in a line of input is where Cmd splits the line into a command and the arguments. There is also the use_rawinput parameter, which comes into play with command completion. I never mess with that, and I'm not going to cover it here. You can check out the docs for the Cmd module if you are interested.

You may have noticed trying to get through the maze (you did get through the maze, right?) that it can get pretty tedious typing out north and south and all that. Normally in a text puzzle like this (or many other command line interfaces) you would have aliases: shorter ways to type the various commands. One way to do this would make a do_n method, which just called do_north. However, we can use the precmd method to do this with less clutter:

def precmd(self, line):
    """
    Pre-command handling. (str)

    Parameters:
    line: The orignal user command input. (str)
    """
    # Replace alases with commands.
    cmd, space, arg = line.partition(' ')
    cmd = self.directions.get(cmd, cmd)
    return '{} {}'.format(cmd, arg)
As you can see from the helpful docstring, the original line of user input is sent to precmd. The precmd method is then supposed to return a line of text with the one that actually gets processed. In the example above, we split out the first word, and use the get method of the directions attribute to replace it (or not, if we can't). As you may recall, the directions method keys the full words for the directions to the single characters. So now we can type n, w, e, or s to move around the maze.

The Cmd class actually already has two hard coded aliases: ? and !. ? is an alias for help, and runs the do_help method. ! is an alias for shell, so it runs the do_shell method, if there is one. The base Cmd class has a do_help method, but does not have a do_shell method. I often put in a do_shell method that evals the argument. It's very handy in testing when your code does something odd but doesn't cause an error. Cmd was actually designed for a testing and prototyping platform, although I use it for the final interface on a lot of my programs. In any case, I left out the eval in this example, because I knew some of the regulars would have colonic spasms if I put it in.

Note that before each command is entered, we want to display to the user the moves they can make from where they are. You might think that precmd is a good place to do something before the command. But if the original line of input is being sent to precmd, that means it's happening after the command is entered. The order in the command processing loop is get the input, run precmd on the input, run a method called onecmd on the result of precmd, and then run a method named postcmd on the result of onecmd (and the result of precmd).

The onecmd method you don't generally need to overwrite, but we can use postcmd method to print the location information we want:

def postcmd(self, stop, line):
    """
    Post-command handling. (bool)

    Parameters:
    stop: A flag for stopping command processing. (bool)
    line: The user command input. (str)
    """
    print(self.show_directions())
    return stop
Just remember to do it at the end of the postcmd method, in case earlier processing interferes with it. What earlier processing might that be. Note that the first parameter and the return value of postcmd is stop. If the return value of postcmd (stop) resolves to True, then the command loop stops processing. So this is a good place to determine if the command loop should stop processing. Currently the move method does that, but we can move that to postcmd:

def postcmd(self, stop, line):
    """
    Post-command handling. (bool)

    Parameters:
    stop: A flag for stopping command processing. (bool)
    line: The user command input. (str)
    """
    # Check for a solution.
    if (self.x, self.y) == self.end:
        print('You made it out of the maze!')
        stop = True
    elif not stop:
        print(self.show_directions())
    return stop
So now postcmd checks to see if you get to the end of the maze. If you don't, and there's no other reason to stop, it prints the possible moves from the current location. Why would there be another reason to stop? Maybe we've added a quit command for when people get tired of wandering around a dark maze:

def do_quit(self, arg):
    """Give up and quit."""
    return True
At the moment there's no real reason to move the solution check to postcmd, because the only way to solve the maze is through the move method. But if you had a secret teleport command, you might have two ways to win. So let's add one:

def do_xyzzy(self, arg):
    if random.random() < 0.23:
        self.x = random.randrange(len(self.map[0]))
        self.y = random.randrange(len(self.map))
        print('Poof! You have been teleported!')
    else:
        print('Nothing happens.')
So now there are two ways to get to the end, although xyzzy is rather unlikely (there is a 0.46% chance that any given use of xyzzy will win). Now we did want xyzzy to be secret. Since we gave it no docstring, the automatic help will return '*** No help on xyzzy', which is similar to what would be returned for a command with no do_ method, like fred. However, if we just type 'help', it will still give us a list of valid commands, including xyzzy. But we can override that too. Let's make a global constant with some help text:

# The text to display for general help.
HELP_TEXT = """This is a maze game. The only info you get is what directions you can move from
where you are. You may move by typing in any of the four cardinal compass
points: north, south, east, or west. You may abbreviate any of these
commands by just using the first letter: n, s, e, or w."""
and then override the do_help command that processes help requests:

def do_help(self, arg):
    """Get help on a command, or just help for general help."""
    if arg:
        super().do_help(arg)
    else:
        print(HELP_TEXT)
Now our help text shows for general help, but the help text for commands is processed normally. This allows xyzzy to be the secret command it always should be.

There are lots of other things you can override in Cmd that I have not shown here. I showed you precmd, preloop, and postcmd, so as you might expect there is a postloop method you can override as well. It takes no parameters and returns None, and is run just before the cmdloop method exits.

There is the emptyline method, which is run when a blank line is entered. By default it runs the last command entered, which is stored in the attribute lastcmd. There is also an attribute named cmdqueue, which is a list of strings. If cmdqueue is not empty, cmdloop pulls the first thing from cmdqueue and runs it instead of asking for input from the user. I often use cmdqueue by putting code in precmd to split the input by semi-colon if there is one, and assigning that to cmdqueue. This allows the user to enter multiple commands separated by semi-colons and have them all run sequentially.

There is also the default method, which is run if there is no do_ method for the command supplied. I used this in a suite of solitaire games where you used 'play game-name' to play one of the games. I changed the default to check if the command was a game name and if so play that game, allowing the user to just type in the name of the game to play it.

As I noted earlier, one of the original purposes of the Cmd class was as a testing frame work. As part of that, there are two parameters when creating an instance: stdin and stdout. These allow you to get the input from a file and send the output to another file. So you make a file of test commands, and then check the output to make sure it's correct. Note that this is not done by redirecting sys.stdin and sys.stdout. It just stores the files as self.stdin and self.stdout. That means that if you want to make use of this you can't use print or input in your methods, you need to use self.stdout.write or self.stdin.readline.

Another thing similar to an alias is end of file. If an end of file character is received by the system, it tries to pass it to the do_EOF method. There isn't one in Cmd, but you can implement one if you want to do special processing at the end of file input with self.stdin.

That pretty much covers the Cmd class, except the command completion stuff. The full code of the Cmd class is available in the documentation, and it's not that long (401 lines).
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#6
Now that we've seen a command based CLI that is versatile and customizable, let's make a similar menu based CLI. (The code for this post is in menu.py on the GitHub repository) Here's the docstring to give you and overview of where we're going:

import string
import sys

class Menu(object):
    """
    A simple framework for writing command line menus. (object)

    There is no reason to instantiate Menu itself. It should be used as a
    parent class for a menu system that you define yourself.

    Class Attributes:
    intro: Text displayed at the beginning of the menu loop. (str)
    prompt: Text displayed when getting user choices. (str)

    Attributes:
    choice_queue: Automatic commands yet to be proccessed. (list of str)
    lastchoice: The last choice made by the user. (str)
    methods: The mapping of menu choices to methods. (dict of str:bound method)
    status: The status of the menu system, if any. (str)
    stdin_save: Storage for when stdin is redirected. (file)
    stdout_save: Storage for when stdout is redirected. (file)
    text: The text of the menu. (str)

    Methods:
    emptyline: Handle blank choices. (bool)
    menuloop: Repeatedly display a menu, get a choice, and process tit. (None)
    onechoice: Act on a single menu choice. (bool)
    postchoice: Common processing after the choice is proccessed. (bool)
    postloop: Processing done after the menu loop ends. (None)
    prechoice: Process the choice before acting on it. (str)
    preloop: Processing done before starting the menu loop. (None)
    set_menu: Set up the menu text and dictionary. (None)
    sort_menu: Sort the lines of the menu text. (None)
    unrecognized: Handle choices not in the menu. (bool)

    Overridden Methods:
    __init__
    """
Class attributes are similar, although none of the ones for the help system. That's because we're not going to have a help system. I didn't see how one would work into a traditional menu system.

The instance attributes have some familiar if changed choices. The choice_queue and lastchoice attributes are equivalent to the cmdqueue and lastcmd attributes of Cmd. The stdin_save and stdout_save attributes should not be messed with. See the section below on __init__ to see how to redirect input and output.

The new instance attributes are methods, status, and text. The status attribute is text that is displayed after the menu when asking for input. It is only displayed if it is not empty, so it can just be ignored if you want. The methods attribute is a mapping of menu choices to the methods to call, and the text attribute is the text of the menu. Both are calculated once during initialization.

The methods mostly correspond to the Cmd methods. The only new methods are set_menu and sort, which are used during initialization to set up the menu text and choices. In the Cmd class, methods starting with do_ defined what commands could be handled. In the Menu class, methods starting with menu_ define what menu options there are. The set_menu method extracts all of the information from the menu_ methods to create text to display for the menu, and a mapping of menu choices to methods.

    # Text displayed at the beginning of the menu loop.
    intro = ''
    # Text displayed when getting user choices.
    prompt = 'Please enter your selection: '
The intro attribute is blank to start, as with Cmd. The prompt defaults to just a generic menu prompt.

    def __init__(self, stdin=None, stdout=None):
        """
        Initialize the file interface for the menu system. (None)

        Parameters:
        sort_key: The key parameter when sorting menu choices. (callable)
        stdin: The input file for the menu interface. (file)
        stdout: The output file for the menu interface. (file)
        """
        # Save the stdin before redirecting.
        self.stdin_save = sys.__stdin__
        if stdin is not None:
            sys.stdin = stdin
        # Save the stdout before redirecting.
        self.stdout_save = sys.__stdout__
        if stdout is not None:
            sys.stdout = stdout
        # Set up the menu.
        self.set_menu()
        # Set up the tracking attributes.
        self.lastchoice = ''
        self.status = ''
        self.choice_queue = []
The __init__ method sets the file redirection. I do it differently than in Cmd, and actually fully redirect sys.stdin and sys.stdout. That way you don't need to worry about using self.stdin and self.stdout, and can just write everything with inputs and prints. The set_menu method is called to set up the menu, and the tracking attributes are all given blank values.

    def emptyline(self):
        """Handle blank choices. (bool)"""
        # Do the last choice over again, if there is one.
        if self.lastchoice:
            return self.onechoice(self.lastchoice)
        else:
            return False
As in Cmd, the emptyline method handles blank user input. And as in Cmd, it does the last valid choice, if one has been stored.

    def menuloop(self, intro=None):
        """
        Repeatedly display a menu, get a choice, and process that choice. (None)

        If the intro parameter is None, the intro attribute of the class is used
        instead.

        Parameters:
        intro: The text to display before the loop begins. (str or None)
        """
        # User defined processing before the loop starts.
        self.preloop()
        # Display any introductory text.
        if intro is not None:
            self.intro = intro
        if self.intro:
            print(self.intro)
        # Loop through the menu choices.
        while True:
            # Process any queued tasks first.
            if self.choice_queue:
                choice = self.choice_queue.pop(0)
            else:
                # Display the menu, with any status.
                print(self.text)
                print()
                if self.status:
                    print('Status:', self.status)
                    print()
                    self.status = ''
                # Get the user's choice.
                choice = input(self.prompt).strip()
            # Process the choice.
            choice = self.prechoice(choice)
            stop = self.onechoice(choice)
            stop = self.postchoice(stop, choice)
            # Check for loop termination.
            if stop:
                break
        # Clean up after the menu loop.
        self.postloop()
        sys.stdin = self.stdin_save
        sys.stdout = self.stdout_save
The menuloop method, corresponding to Cmd's cmdloop method, is the meat of the Menu class. It starts by running preloop, which contains any set up required for a specific sub-class of Menu. Then it displays the introductory text, just as in Cmd: any intro given to menuloop as a paramter takes priority, the intro class is used as a second priority, and if they're both blank nothing is displayed.

Then we get to the actual processing loop. If there are any choices in the choice_queue attribute, those are taken first. Otherwise, the menu (self.text) is displayed, then the status attribute is displayed if there is one, and then a simple input gets the user's choice.

Once the choice is obtained, it is processed in three steps. First, the prechoice method is run, allowing for changes to the choice. Then the onechoice method is run, to actually process the choice. This returns a stop value, which if True will end the menu loop. Then the postchoice method is run, allowing for changes to the stop value.

Finally, the stop value is checked, and the loop either continues or ends.

After the loop ends, the postloop method is run for any finally processing or messages, and stdin/stdout are reset to their saved values.

    def onechoice(self, choice):
        """
        Act on a single menu choice. (bool)

        Parameters:
        choice: The user's menu choice. (str)
        """
        if not choice:
            stop = self.emptyline()
        elif choice.lower() in self.methods:
            stop = self.methods[choice.lower()]()
            self.lastchoice = choice
        else:
            stop = self.unrecognized(choice)
        return stop
The onechoice method does the determination of which method handles a given user choice. If the choice is empty, it uses the emptyline method. If it recognizes the choice, it sends it to the appropriate method. 'Recognize' in this context means that it can find the choice in the methods dictionary that the set_up method created. Finally, if it has a non-empty choice that it doesn't recognize, it sends it to the unrecognized method.

    def postchoice(self, stop, choice):
        """
        Common processing after the choice is proccessed. (bool)

        Parameters:
        stop: Flag for stopping the menu loop. (bool)
        choice: The user's choice. (str)
        """
        return stop

    def postloop(self):
        """Processing done after the menu loop ends. (None)"""
        pass

    def prechoice(self, choice):
        """
        Process the choice before acting on it. (str)

        Parameters:
        choice: The original user's choice. (str)
        """
        return choice

    def preloop(self):
        """Processing done before starting the menu loop. (None)"""
        pass
The post- and pre- methods for Menu are analogous to the ones in Cmd. As you can see, they are just stub methods, waiting to be overridden by sub-classes of Menu.

    def set_menu(self):
        """Set up the menu text and dictionary. (None)"""
        menu_lines = []
        self.methods = {}
        for attribute in dir(self):
            if attribute.startswith('menu_'):
                attr = getattr(self, attribute)
                if hasattr(attr, '__doc__'):
                    menu_lines.append(attr.__doc__.strip().split('\n')[0].strip())
                    self.methods[attr.__doc__.split(':')[0].strip().lower()] = attr
        self.sort_menu(menu_lines)
        self.text = '\n' + '\n'.join(menu_lines)

    def sort_menu(self, menu_lines):
        """
        Sort the lines of the menu text. (None)

        Parameters:
        menu_lines: the lines of the menu. (list of str)
        """
        menu_lines.sort(key = lambda line: line.split(':')[0])
The set_menu and sort_menu methods create the data about the menu that menuloop and onechoice use to get and process the menu choices. The set_menu method runs through all the attributes of itself that start with 'menu_'. If that attribute has a docstring, the first line of that docstring is taken. That is stored as a line in the menu itself. Everything on that line before the first colon is used as a key for the methods dictionary attribute. The value for that key is the attribute (method) itself. For example, say you have a method menu_foo whose docstring is 'F: Do foo'. The line 'F: Do foo' will be added to the menu text. Additionally, self.methods['f'] will be set to do_foo. Note that the 'f' is lowercased for case insensitive choices.

After the lines of the actual menu are discovered, they are sorted by the sort_menu method. The sorting is done in a different method so that it is easier to override the sort order for subclasses. For example, say you want to number your menu items, and you have 12 of them. Given the default string sorting, 11 and 12 would end up between 1 and 2.

Note that you could theoretically modify this data (probably in postchoice) to create a dynamic menu that changes based on user responses. I'm not sure I would recommend doing that, but it is possible.

    def unrecognized(self, choice):
        """
        Handle choices not in the menu. (bool)

        Parameters:
        choice: The user's menu choice. (str)
        """
        self.status = 'I do not recognize the choice {!r}. Please make another choice.'.format(choice)
        return False
Finally, there is the unrecognized method, which handles any responses that aren't in the methods dictionary. It just updates the status with a message. This method is meant to be overridden. Really, with this class you just don't want to mess with __init__, menuloop, onechoice, or set_menu. And you shouldn't need to. Almost any functionality you would get by modifying those four methods should be achievable by modifying one of the other methods.

In the next post I will give a basic example of a menu system using this class.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#7
So lets do a few examples of menus using the Menu class. At the end we'll tie them all together in one big menu. (The code for this post is in menu_test.py on the GitHub repository) First, some necessary inputs:

import random
import time

from menu import Menu
from cmd_example2 import Maze
For our first example, we will take the functions we had for Monty Python skits and make them methods of a menu subclass:

class MontyMenu(Menu):
    """
    A menu of Monty Python skits. (Menu)

    Methods:
    menu_argument: A: Have an intellectual discussion. (bool)
    menu_knight: B: Get some vigorous exercise. (bool)
    menu_quit: D: Stop it, that's just silly. (bool)
    menu_spam: C: Enjoy some fine dining.
    """

    def menu_argument(self):
        """A: Have an intellectual discussion."""
        # Prep the argument.
        start = time.perf_counter()
        user_text = input('Please state an assertion to argue about: ')
        # Argue for two minutes
        while time.perf_counter() - start < 120:
            # Automatically gainsay whatever the user says. 
            user_words = user_text.lower().split()
            for negative in ('no', 'not', "isn't", "ain't", "doesn't", "wasn't"):
                if negative in user_words:
                    user_text = input('Yes it is. ')
            else:
                user_text = input("No it isn't. ")
        # Say goodbye.
        print("I'm sorry, your five minutes is up.")
        input('Press Enter to continue: ')

    def menu_knight(self):
        """B: Get some vigorous exercise."""
        # Set up the combat.
        limbs = ['other leg', 'leg', 'other arm', 'arm']
        combat = False
        # Loop while the knight has limbs.
        while limbs:
            # None shall pass.
            if not combat:
                print('None shall pass.')
            # Get the user's action.
            user_action = input('What do you do? ')
            # Attacking chops off a limb.
            if user_action.lower() == 'attack':
                print("Excellent attack. You chop off the black knight's {}".format(limbs.pop()))
                combat = True
            # Anything else after attacking provokes an attack.
            elif combat:
                print('The black knight attacks, but you easily block his blow.')
        # Say goodbye.
        input('Press Enter to call it a draw: ')

    def menu_quit(self):
        """D: Stop it, that's just silly."""
        return True

    def menu_spam(self):
        """C: Enjoy some fine dining."""
        # Get the user's order.
        food = input('What would you like to eat? ')
        # Prepare the meal.
        pre_spam = ['spam'] * random.randint(2, 4)
        post_spam = ['spam'] * random.randint(0, 2) + ['and spam.']
        meal = pre_spam + [food] + post_spam
        # Deliver the food and say goodbye.
        print('Here is your ' + ', '.join(meal))
        input('Press Enter to eat a wafer thin wafer and explode: ')
All we had to do was make them methods, give them a doc string equal to what should be shown menu, and the Menu class takes care of everything else.

Let's look at something a little more complicated, like redoing the integer graph menu with the Menu class:

class NumberMenu(Menu):
    """
    A menu of integer graphs. (Menu)

    Class Attributes:
    primes: All of the prime numbers up to just over 100. (list of int)

    Attributes:
    numbers: The number sequence generated so far. (list of int)

    Methods:
    menu_collatz: 3: Collatz the last number. (bool)
    menu_fibonacci: 1: Add the last two numbers. (bool)
    menu_prime: 2: Go up to the next prime. (bool)
    menu_quit: 4: Quit. (bool)

    Overridden Methods:
    preloop
    postchoice
    postloop
    """
    
    # All of the prime numbers up to just over 100.
    primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89]
    primes += [97, 101]

    def menu_collatz(self):
        """3: Collatz the last number."""
        if self.numbers[-1] % 2:
            self.numbers.append(self.numbers[-1] * 3 + 1)
        else:
            self.numbers.append(self.numbers[-1] // 2)

    def menu_fibonacci(self):
        """1: Add the last two numbers."""
        self.numbers.append(self.numbers[-1] + self.numbers[-2])

    def menu_prime(self):
        """2: Go up to the next prime."""
        self.numbers.append([p for p in self.primes if p > self.numbers[-1]][0])

    def menu_quit(self):
        """4: Quit."""
        return True

    def preloop(self):
        """Processing done before starting the choice/action loop. (None)"""
        # Set up number lists.
        self.numbers = [0, 1]
        # Show the starting number.
        self.status = 'The number is now {}.'.format(self.numbers[-1])

    def postchoice(self, stop, choice):
        """
        Common processing after the choice is proccessed. (bool)

        Parameters:
        stop: Flag for stopping the menu loop. (bool)
        choice: The user's choice. (str)
        """
        # Show the current number.
        if not self.status:
            self.status = 'The number is now {}.'.format(self.numbers[-1])
        # Check the current number.
        if self.numbers[-1] > 99:
            return True
        else:
            return stop

    def postloop(self):
        """Processing done after the choice/action loop ends. (None)"""
        print('The final number is {}.'.format(self.numbers[-1]))
        print('Have a nice day.')

    def sort_menu(self, menu_lines):
        """
        Sort the lines of the menu text. (None)

        Parameters:
        menu_lines: the lines of the menu. (list of str)
        """
        menu_lines.sort(key = lambda line: int(line.split(':')[0]))
As with the Monty Python menu, we took the original functions, turned them into methods of a Menu sub-class, and gave them doc strings matching what we wanted to see in the menu.

However, we also overloaded a lot of the methods of Menu (and we added the class attribute primes). We use preloop to set up the list of numbers, and to give an opening message with the current final number in the list. Then we use postchoice to show the current number on the status line, and to check for the list getting to 100 or more. We use postloop to tell the user the final number and to be polite. We also use sort_menu to sort the menu numerically rather than alphabetically. (It doesn't matter in this example, but I wanted to give an example of how to do it.)

Let's tie it all together:

class TopMenu(Menu):
    """
    A top level menu. (Menu)

    Class Attributes:
    rps_wins: What beats what in rock-paper-scissors. (dict of str: str)

    Methods:
    menu_maze: A: Play in a maze. (bool)
    menu_numbers: B. Play with numbers. (bool)
    menu_rps: C: Play with your hands. (bool)
    menu_quit: E: Quit. (bool)
    menu_words: D: Play with words. (bool)
    """

    # What beats what in rock-paper-scissors. 
    rps_wins = {'rock': 'scissors', 'paper': 'rock', 'scissors': 'paper'}

    def menu_maze(self):
        """A: Play in a maze."""
        maze = Maze()
        maze.cmdloop()

    def menu_numbers(self):
        """B: Play with numbers."""
        numbers = NumberMenu()
        numbers.menuloop()

    def menu_rps(self):
        """
        C: Play with your hands.

        This is just a game of rock-paper-scissors.
        """
        while True:
            play = input('Rock, paper, or scissors? ').lower()
            bot = random.choice(list(self.rps_wins.keys()))
            if play not in self.rps_wins:
                print("Invalid play. Come on, this is kid's stuff.")
            elif play == bot:
                print('Draw, play again.')
            elif self.rps_wins[play] == bot:
                print('I chose {}. You won!'.format(bot))
                break
            else:
                print('I chose {}. You lose.'.format(bot))
                break

    def menu_quit(self):
        """E: Quit."""
        return True

    def menu_words(self):
        """D: Play with words."""
        words = MontyMenu()
        words.menuloop()

if __name__ = '__main__':
	top = TopMenu()
	top.menuloop()
The TopMenu class has three different types of menu items. First you have your basic menu option in menu_rps, which just plays rock-paper-scissors. Then you have two methods with sub-menus: menu_numbers and menu_words. Both make an instance of one of the other Menu sub-classes seen above, and runs it's menuloop. When the sub-menu's menuloop is done, it passes control right back to TopMenu. Finally, we have menu_maze, which makes an instance of a Cmd sub-class, and runs its cmdloop. Again, when cmdloop is done, control passes right back to the TopMenu instance.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#8
Thumbs Up 
Great tutorial. Far exceeds my expectations. Thank you. Btw, I think you may need a closing '[/url]' at the top of the post for your link?

This should become a 'sticky' so as not to get buried.
If it ain't broke, I just haven't gotten to it yet.
OS: Windows 10, openSuse 42.3, freeBSD 11, Raspian "Stretch"
Python 3.6.5, IDE: PyCharm 2018 Community Edition
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020