{"id":"larger-application-interface","slug":"user-interface","title":"A text user interface","text":"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.\n\n```python\nclass ReadingLog:\n    def __init__(self):\n        self.__lengths = {}\n        self.__pages_read = {}\n\n    def add_book(self, title, length):\n        self.__lengths[title] = length\n        self.__pages_read[title] = 0\n\n    def has_book(self, title):\n        return title in self.__lengths\n\n    def read(self, title, pages):\n        self.__pages_read[title] += pages\n\n    def pages_left(self, title):\n        return self.__lengths[title] - self.__pages_read[title]\n\n\nclass ReadingApp:\n    def __init__(self):\n        self.__log = ReadingLog()\n\n    def show_commands(self):\n        print(\"commands: add, read, left, quit\")\n\n    def add_book(self):\n        title = input(\"title: \")\n        length = int(input(\"pages in the book: \"))\n        self.__log.add_book(title, length)\n\n    def read(self):\n        title = input(\"title: \")\n        if not self.__log.has_book(title):\n            print(f\"{title} is not in the log\")\n            return\n        pages = int(input(\"pages read: \"))\n        self.__log.read(title, pages)\n\n    def left(self):\n        title = input(\"title: \")\n        if not self.__log.has_book(title):\n            print(f\"{title} is not in the log\")\n            return\n        print(f\"{self.__log.pages_left(title)} pages left\")\n\n    def run(self):\n        self.show_commands()\n        while True:\n            command = input(\"> \")\n            if command == \"quit\":\n                break\n            elif command == \"add\":\n                self.add_book()\n            elif command == \"read\":\n                self.read()\n            elif command == \"left\":\n                self.left()\n            else:\n                print(f\"unknown command: {command}\")\n                self.show_commands()\n\n\nif __name__ == \"__main__\":\n    app = ReadingApp()\n    app.run()\n```\n\n```input\nadd\nDune\n412\nread\nDune\n50\nread\nEmma\nleft\nDune\nhelp\nquit\n```\n\n```output\ncommands: add, read, left, quit\n> add\ntitle: Dune\npages in the book: 412\n> read\ntitle: Dune\npages read: 50\n> read\ntitle: Emma\nEmma is not in the log\n> left\ntitle: Dune\n362 pages left\n> help\nunknown command: help\ncommands: add, read, left, quit\n> quit\n```\n\nHere is who does what:\n\n- `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**.\n- `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.\n- The interface never touches the dictionaries. When it needs to know something about the books, it asks `ReadingLog`.\n- 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.\n\nWhy 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.\n\n`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.\n","task":"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:\n\n```\ncommand (1 add, 2 check, 0 quit): 1\nitem: rice\namount: 2\ncommand (1 add, 2 check, 0 quit): 1\nitem: rice\namount: 3\ncommand (1 add, 2 check, 0 quit): 2\nitem: rice\nrice: 5\ncommand (1 add, 2 check, 0 quit): 2\nitem: salt\nsalt: 0\ncommand (1 add, 2 check, 0 quit): 0\n```\n","kind":"code","seed":"class Pantry:\n    def __init__(self):\n        self.__items = {}\n\n    def add(self, item, amount):\n        if item not in self.__items:\n            self.__items[item] = 0\n        self.__items[item] += amount\n\n    def amount(self, item):\n        if item not in self.__items:\n            return 0\n        return self.__items[item]\n\n\nclass PantryApp:\n    def __init__(self):\n        self.__pantry = Pantry()\n\n    def execute(self):\n        while True:\n            command = input(\"command (1 add, 2 check, 0 quit): \")\n            if command == \"0\":\n                break\n            elif command == \"1\":\n                item = input(\"item: \")\n                amount = int(input(\"amount: \"))\n                self.__pantry.add(item, amount)\n            elif command == \"2\":\n                item = input(\"item: \")\n                print(f\"{item}: {self.__pantry.amount(item)}\")\n\n\nif __name__ == \"__main__\":\n    app = PantryApp()\n    app.execute()\n","solution":"class Pantry:\n    def __init__(self):\n        self.__items = {}\n\n    def add(self, item, amount):\n        if item not in self.__items:\n            self.__items[item] = 0\n        self.__items[item] += amount\n\n    def amount(self, item):\n        if item not in self.__items:\n            return 0\n        return self.__items[item]\n\n\nclass PantryApp:\n    def __init__(self):\n        self.__pantry = Pantry()\n\n    def add_item(self):\n        item = input(\"item: \")\n        amount = int(input(\"amount: \"))\n        self.__pantry.add(item, amount)\n\n    def check(self):\n        item = input(\"item: \")\n        print(f\"{item}: {self.__pantry.amount(item)}\")\n\n    def execute(self):\n        while True:\n            command = input(\"command (1 add, 2 check, 0 quit): \")\n            if command == \"0\":\n                break\n            elif command == \"1\":\n                self.add_item()\n            elif command == \"2\":\n                self.check()\n\n\nif __name__ == \"__main__\":\n    app = PantryApp()\n    app.execute()\n","hints":["Write `def add_item(self):` in `PantryApp`, and move the three lines of command 1 into it. Do the same for command 2 with `def check(self):`.","In `execute`, the branches become one line each: `self.add_item()` and `self.check()`.","The methods use the pantry through `self.__pantry`, just as the loop did."],"tests":[{"name":"Test 1","stdin":"1\nrice\n2\n1\nrice\n3\n2\nrice\n2\nsalt\n0\n","expect":"command (1 add, 2 check, 0 quit): 1\nitem: rice\namount: 2\ncommand (1 add, 2 check, 0 quit): 1\nitem: rice\namount: 3\ncommand (1 add, 2 check, 0 quit): 2\nitem: rice\nrice: 5\ncommand (1 add, 2 check, 0 quit): 2\nitem: salt\nsalt: 0\ncommand (1 add, 2 check, 0 quit): 0","hidden":false},{"name":"Test 2","stdin":"2\noats\n1\noats\n10\n1\nbeans\n4\n2\nbeans\n2\noats\n0\n","expect":"command (1 add, 2 check, 0 quit): 2\nitem: oats\noats: 0\ncommand (1 add, 2 check, 0 quit): 1\nitem: oats\namount: 10\ncommand (1 add, 2 check, 0 quit): 1\nitem: beans\namount: 4\ncommand (1 add, 2 check, 0 quit): 2\nitem: beans\nbeans: 4\ncommand (1 add, 2 check, 0 quit): 2\nitem: oats\noats: 10\ncommand (1 add, 2 check, 0 quit): 0","hidden":true}],"require":[{"pattern":"def\\s+add_item\\s*\\(\\s*self\\s*\\)","message":"Write the method `add_item(self)` for command 1."},{"pattern":"def\\s+check\\s*\\(\\s*self\\s*\\)","message":"Write the method `check(self)` for command 2."},{"pattern":"self\\.add_item\\(\\)","message":"`execute` should call `self.add_item()` for command 1."},{"pattern":"self\\.check\\(\\)","message":"`execute` should call `self.check()` for command 2."}],"forbid":[],"more":[{"task":"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:\n\n- `total()` returns the sum of the amounts, and 0 when there are none.\n- `largest()` returns the largest amount, and 0 when there are none.\n\nThen change `report` so that it only asks `Expenses` for these two numbers and prints them. The app should work exactly as before, for example:\n\n```\n> add\namount: 25\n> add\namount: 40\n> report\ntotal 65, largest 40\n> add\namount: 12\n> report\ntotal 77, largest 40\n> quit\n```\n","kind":"code","seed":"class Expenses:\n    def __init__(self):\n        self.__amounts = []\n\n    def add(self, amount):\n        self.__amounts.append(amount)\n\n    def amounts(self):\n        return self.__amounts\n\n\nclass ExpensesApp:\n    def __init__(self):\n        self.__expenses = Expenses()\n\n    def add(self):\n        amount = int(input(\"amount: \"))\n        self.__expenses.add(amount)\n\n    def report(self):\n        total = 0\n        largest = 0\n        for amount in self.__expenses.amounts():\n            total += amount\n            if amount > largest:\n                largest = amount\n        print(f\"total {total}, largest {largest}\")\n\n    def run(self):\n        while True:\n            command = input(\"> \")\n            if command == \"quit\":\n                break\n            elif command == \"add\":\n                self.add()\n            elif command == \"report\":\n                self.report()\n\n\nif __name__ == \"__main__\":\n    app = ExpensesApp()\n    app.run()\n","solution":"class Expenses:\n    def __init__(self):\n        self.__amounts = []\n\n    def add(self, amount):\n        self.__amounts.append(amount)\n\n    def amounts(self):\n        return self.__amounts\n\n    def total(self):\n        return sum(self.__amounts)\n\n    def largest(self):\n        if len(self.__amounts) == 0:\n            return 0\n        return max(self.__amounts)\n\n\nclass ExpensesApp:\n    def __init__(self):\n        self.__expenses = Expenses()\n\n    def add(self):\n        amount = int(input(\"amount: \"))\n        self.__expenses.add(amount)\n\n    def report(self):\n        total = self.__expenses.total()\n        largest = self.__expenses.largest()\n        print(f\"total {total}, largest {largest}\")\n\n    def run(self):\n        while True:\n            command = input(\"> \")\n            if command == \"quit\":\n                break\n            elif command == \"add\":\n                self.add()\n            elif command == \"report\":\n                self.report()\n\n\nif __name__ == \"__main__\":\n    app = ExpensesApp()\n    app.run()\n","hints":["Write `def total(self):` and `def largest(self):` in `Expenses`. Inside the class, the amounts are in `self.__amounts`.","`sum` adds up a list. `max` finds the largest item, but it stops with an error on an empty list, so check `len(self.__amounts) == 0` first and return 0.","In `report`, the loop goes away: get the two numbers with `self.__expenses.total()` and `self.__expenses.largest()`, and print them as before."],"tests":[{"name":"Test 1","stdin":"add\n25\nadd\n40\nreport\nadd\n12\nreport\nquit\n","expect":"> add\namount: 25\n> add\namount: 40\n> report\ntotal 65, largest 40\n> add\namount: 12\n> report\ntotal 77, largest 40\n> quit","hidden":false},{"name":"Test 2","stdin":"","call":"expenses = Expenses()\nprint(expenses.total())\nprint(expenses.largest())\nexpenses.add(12)\nexpenses.add(30)\nexpenses.add(8)\nprint(expenses.total())\nprint(expenses.largest())\n","expect":"0\n0\n50\n30","hidden":false},{"name":"Test 3","stdin":"report\nadd\n7\nreport\nadd\n120\nadd\n3\nreport\nquit\n","expect":"> report\ntotal 0, largest 0\n> add\namount: 7\n> report\ntotal 7, largest 7\n> add\namount: 120\n> add\namount: 3\n> report\ntotal 130, largest 120\n> quit","hidden":true},{"name":"Test 4","stdin":"","call":"expenses = Expenses()\nexpenses.add(5)\nprint(expenses.total())\nprint(expenses.largest())\nexpenses.add(5)\nexpenses.add(2)\nprint(expenses.total())\nprint(expenses.largest())\n","expect":"5\n5\n12\n5","hidden":true}],"require":[{"pattern":"self\\.__expenses\\.total\\(\\)","raw":true,"message":"`report` should ask the logic for the sum: `self.__expenses.total()`."},{"pattern":"self\\.__expenses\\.largest\\(\\)","raw":true,"message":"`report` should ask the logic for the largest amount: `self.__expenses.largest()`."}],"forbid":[]},{"task":"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:\n\n- `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.\n- `2` asks for a team and prints its points, as `Owls: 5 points`.\n- `3` prints `leader: ` and the leading team, or `no teams yet`.\n- Any other command prints the commands again.\n\nGive each command a method of its own. For example:\n\n```\ncommands: 1 add points, 2 show a team, 3 leader, 0 quit\ncommand: 3\nno teams yet\ncommand: 1\nteam: Owls\npoints: 5\ncommand: 1\nteam: Foxes\npoints: ten\npoints must be a whole number\ncommand: 1\nteam: Foxes\npoints: 7\ncommand: 2\nteam: Owls\nOwls: 5 points\ncommand: 3\nleader: Foxes\ncommand: 0\n```\n","kind":"code","seed":"class Scoreboard:\n    def __init__(self):\n        self.__points = {}\n\n    def add_points(self, team, points):\n        if team not in self.__points:\n            self.__points[team] = 0\n        self.__points[team] += points\n\n    def points(self, team):\n        if team not in self.__points:\n            return 0\n        return self.__points[team]\n\n    def leader(self):\n        best = None\n        for team in self.__points:\n            if best is None or self.__points[team] > self.__points[best]:\n                best = team\n        return best\n\n\nclass ScoreboardApp:\n    def __init__(self):\n        self.__board = Scoreboard()\n\n    def execute(self):\n        pass\n\n\nif __name__ == \"__main__\":\n    app = ScoreboardApp()\n    app.execute()\n","solution":"class Scoreboard:\n    def __init__(self):\n        self.__points = {}\n\n    def add_points(self, team, points):\n        if team not in self.__points:\n            self.__points[team] = 0\n        self.__points[team] += points\n\n    def points(self, team):\n        if team not in self.__points:\n            return 0\n        return self.__points[team]\n\n    def leader(self):\n        best = None\n        for team in self.__points:\n            if best is None or self.__points[team] > self.__points[best]:\n                best = team\n        return best\n\n\nclass ScoreboardApp:\n    def __init__(self):\n        self.__board = Scoreboard()\n\n    def help(self):\n        print(\"commands: 1 add points, 2 show a team, 3 leader, 0 quit\")\n\n    def add_points(self):\n        team = input(\"team: \")\n        try:\n            points = int(input(\"points: \"))\n        except ValueError:\n            print(\"points must be a whole number\")\n            return\n        self.__board.add_points(team, points)\n\n    def show(self):\n        team = input(\"team: \")\n        print(f\"{team}: {self.__board.points(team)} points\")\n\n    def leader(self):\n        leader = self.__board.leader()\n        if leader is None:\n            print(\"no teams yet\")\n        else:\n            print(\"leader:\", leader)\n\n    def execute(self):\n        self.help()\n        while True:\n            command = input(\"command: \")\n            if command == \"0\":\n                break\n            elif command == \"1\":\n                self.add_points()\n            elif command == \"2\":\n                self.show()\n            elif command == \"3\":\n                self.leader()\n            else:\n                self.help()\n\n\nif __name__ == \"__main__\":\n    app = ScoreboardApp()\n    app.execute()\n","hints":["Follow the structure of `ReadingApp`: a method that prints the commands, one method for each command, and an `execute` with a `while True` loop that reads `input(\"command: \")` and calls the right method.","Checking the points is the user interface's job. Put `int(input(\"points: \"))` inside `try`, and in `except ValueError:` print the message and `return` before anything is added.","The logic's `leader()` returns `None` for an empty board; check that with `is None` before printing the leader."],"tests":[{"name":"Test 1","stdin":"3\n1\nOwls\n5\n1\nFoxes\nten\n1\nFoxes\n7\n2\nOwls\n3\n0\n","expect":"commands: 1 add points, 2 show a team, 3 leader, 0 quit\ncommand: 3\nno teams yet\ncommand: 1\nteam: Owls\npoints: 5\ncommand: 1\nteam: Foxes\npoints: ten\npoints must be a whole number\ncommand: 1\nteam: Foxes\npoints: 7\ncommand: 2\nteam: Owls\nOwls: 5 points\ncommand: 3\nleader: Foxes\ncommand: 0","hidden":false},{"name":"Test 2","stdin":"2\nBears\n1\nBears\n2.5\nhello\n1\nBears\n4\n1\nLynx\n4\n1\nBears\n-1\n3\n2\nBears\n0\n","expect":"commands: 1 add points, 2 show a team, 3 leader, 0 quit\ncommand: 2\nteam: Bears\nBears: 0 points\ncommand: 1\nteam: Bears\npoints: 2.5\npoints must be a whole number\ncommand: hello\ncommands: 1 add points, 2 show a team, 3 leader, 0 quit\ncommand: 1\nteam: Bears\npoints: 4\ncommand: 1\nteam: Lynx\npoints: 4\ncommand: 1\nteam: Bears\npoints: -1\ncommand: 3\nleader: Lynx\ncommand: 2\nteam: Bears\nBears: 3 points\ncommand: 0","hidden":true}],"require":[{"pattern":"self\\.__board\\.add_points\\(","message":"Add the points through the logic class: `self.__board.add_points(...)`."},{"pattern":"self\\.__board\\.leader\\(","message":"Ask the logic class for the leader: `self.__board.leader()`."}],"forbid":[]}]}