Python Arena
Step 2 of 4

A text user interface

With the logic tried out, the reading log needs a user interface. A text interface is a loop: read a command, carry it out, and do it again until the user is done. The interface gets a class of its own, ReadingApp, and its commands are words: add, read, left and quit. The app creates a ReadingLog and keeps it in a private attribute. Everything about books goes through it.

class ReadingLog:
    def __init__(self):
        self.__lengths = {}
        self.__pages_read = {}

    def add_book(self, title, length):
        self.__lengths[title] = length
        self.__pages_read[title] = 0

    def has_book(self, title):
        return title in self.__lengths

    def read(self, title, pages):
        self.__pages_read[title] += pages

    def pages_left(self, title):
        return self.__lengths[title] - self.__pages_read[title]


class ReadingApp:
    def __init__(self):
        self.__log = ReadingLog()

    def show_commands(self):
        print("commands: add, read, left, quit")

    def add_book(self):
        title = input("title: ")
        length = int(input("pages in the book: "))
        self.__log.add_book(title, length)

    def read(self):
        title = input("title: ")
        if not self.__log.has_book(title):
            print(f"{title} is not in the log")
            return
        pages = int(input("pages read: "))
        self.__log.read(title, pages)

    def left(self):
        title = input("title: ")
        if not self.__log.has_book(title):
            print(f"{title} is not in the log")
            return
        print(f"{self.__log.pages_left(title)} pages left")

    def run(self):
        self.show_commands()
        while True:
            command = input("> ")
            if command == "quit":
                break
            elif command == "add":
                self.add_book()
            elif command == "read":
                self.read()
            elif command == "left":
                self.left()
            else:
                print(f"unknown command: {command}")
                self.show_commands()


if __name__ == "__main__":
    app = ReadingApp()
    app.run()
Input
add
Dune
412
read
Dune
50
read
Emma
left
Dune
help
quit
Output
commands: add, read, left, quit
> add
title: Dune
pages in the book: 412
> read
title: Dune
pages read: 50
> read
title: Emma
Emma is not in the log
> left
title: Dune
362 pages left
> help
unknown command: help
commands: add, read, left, quit
> quit

Here is who does what:

  • run is the main loop. It reads a command and passes the work on to the method for that command. Handing work over like this is called delegating.
  • add_book, read and left each handle one command. They ask the user for what they need, call the logic, and show what it returns.
  • The interface never touches the dictionaries. When it needs to know something about the books, it asks ReadingLog.
  • Checking what the user typed is the interface's job. Before it asks for pages, read checks with has_book that the book exists, and tells the user if it doesn't.

Why not write everything inside the loop? Read run again: it works like a table of contents. It lists every command the app knows, and the details of each one are a method away. A new command is a new method and one more elif, and the commands that already work stay as they are.

ReadingLog and ReadingApp both have methods called add_book and read, and that's fine. The app's versions talk to the user, while the log's versions store the data. They never get mixed up, since each one is called on an object of its own class.

Your task

Challenge 1 of 3

The pantry app works, but its execute does everything itself. Move the code of each command into a method of its own: add_item() for command 1 and check() for command 2. Then execute only reads the command and calls the right method. The app should work exactly as before, for example:

command (1 add, 2 check, 0 quit): 1
item: rice
amount: 2
command (1 add, 2 check, 0 quit): 1
item: rice
amount: 3
command (1 add, 2 check, 0 quit): 2
item: rice
rice: 5
command (1 add, 2 check, 0 quit): 2
item: salt
salt: 0
command (1 add, 2 check, 0 quit): 0

Challenge 2

In this trip expenses app, the user interface does work that belongs to the logic: report adds up the amounts and finds the largest one itself. Move that work into Expenses, where it can be used and tested without the user interface:

  • total() returns the sum of the amounts, and 0 when there are none.
  • largest() returns the largest amount, and 0 when there are none.

Then change report so that it only asks Expenses for these two numbers and prints them. The app should work exactly as before, for example:

> add
amount: 25
> add
amount: 40
> report
total 65, largest 40
> add
amount: 12
> report
total 77, largest 40
> quit

Challenge 3

The quiz night's Scoreboard from the previous step is ready. Write its text user interface, the class ScoreboardApp. Its method execute first prints the commands, commands: 1 add points, 2 show a team, 3 leader, 0 quit, and then reads commands until the user types 0:

  • 1 asks for a team and points, and adds them. If the points aren't a whole number, it prints points must be a whole number and adds nothing.
  • 2 asks for a team and prints its points, as Owls: 5 points.
  • 3 prints leader: and the leading team, or no teams yet.
  • Any other command prints the commands again.

Give each command a method of its own. For example:

commands: 1 add points, 2 show a team, 3 leader, 0 quit
command: 3
no teams yet
command: 1
team: Owls
points: 5
command: 1
team: Foxes
points: ten
points must be a whole number
command: 1
team: Foxes
points: 7
command: 2
team: Owls
Owls: 5 points
command: 3
leader: Foxes
command: 0