{"id":"larger-application-files","slug":"files","title":"Reading and saving the data","text":"A reading log that forgets every book when the program ends isn't much use. The books should be saved in a file, and read back the next time the program starts. Reading and writing the file is a job of its own, so it gets a class of its own: the third part of the application, next to the logic and the user interface.\n\nEach line of the file holds one book: the title, the number of pages in the book, and then the pages of each reading session, all separated by `|` characters:\n\n```text\nDune|412|50|38\nEmma|474\n```\n\nThe title and the length are always there, but the number of sessions varies. Emma hasn't been opened yet, so its line has none.\n\n### Taking the rest of a list\n\nWhen a line is split at the `|` characters, the first two parts are the title and the length, and whatever comes after them is the sessions, however many there are. A **starred assignment** takes a list apart in just this way: each variable without a star gets one item, and the one with a star gets a list of all the items that are left over.\n\n```pycon\n>>> title, length, *sessions = \"Dune|412|50|38\".split(\"|\")\n>>> title\n'Dune'\n>>> length\n'412'\n>>> sessions\n['50', '38']\n>>> title, length, *sessions = \"Emma|474\".split(\"|\")\n>>> sessions\n[]\n>>> *rest, last = [7, 8, 9]\n>>> rest\n[7, 8]\n>>> last\n9\n```\n\nThe parts of a split line are strings, so the numbers still need `int`. When nothing is left over, the starred variable gets an empty list. And the star doesn't have to be on the last variable, as the last example shows: whichever variable has it gets what the others leave.\n\n### A class for the file\n\n`BookFile` gets the file's name in its constructor. `load` reads the file and returns a list of `Book` objects, and `save` writes a list of books back, one line for each. The first time the program runs there's no file yet, so `load` catches the `FileNotFoundError` and returns an empty list: an empty log is the right start. Before the class goes into the app, it's tried out on its own:\n\n```file books.txt\nDune|412|50|38\nEmma|474\n```\n\n```python\nclass Book:\n    def __init__(self, title, length):\n        self.__title = title\n        self.__length = length\n        self.__sessions = []\n\n    def title(self):\n        return self.__title\n\n    def length(self):\n        return self.__length\n\n    def sessions(self):\n        return self.__sessions\n\n    def read(self, pages):\n        self.__sessions.append(pages)\n\n    def pages_read(self):\n        return sum(self.__sessions)\n\n    def pages_left(self):\n        return self.__length - self.pages_read()\n\n    def __str__(self):\n        return f\"{self.__title}: {self.pages_read()} of {self.__length} pages read\"\n\n\nclass BookFile:\n    def __init__(self, filename):\n        self.__filename = filename\n\n    def load(self):\n        books = []\n        try:\n            with open(self.__filename) as file:\n                for line in file:\n                    title, length, *sessions = line.strip().split(\"|\")\n                    book = Book(title, int(length))\n                    for pages in sessions:\n                        book.read(int(pages))\n                    books.append(book)\n        except FileNotFoundError:\n            pass\n        return books\n\n    def save(self, books):\n        with open(self.__filename, \"w\") as file:\n            for book in books:\n                line = f\"{book.title()}|{book.length()}\"\n                for pages in book.sessions():\n                    line += f\"|{pages}\"\n                file.write(line + \"\\n\")\n\n\nbook_file = BookFile(\"books.txt\")\nbooks = book_file.load()\nfor book in books:\n    print(book)\nbooks[1].read(25)\nbook_file.save(books)\nprint(BookFile(\"nothing-here.txt\").load())\n\nwith open(\"books.txt\") as file:\n    for line in file:\n        print(line.strip())\n```\n\n```output\nDune: 88 of 412 pages read\nEmma: 0 of 474 pages read\n[]\nDune|412|50|38\nEmma|474|25\n```\n\n`load` turns each line back into a `Book`: it creates the book with its title and length, and then lets it record each session. `save` does the opposite. It builds each line from the book's own methods, the title and the length first and then a `|` and the pages for each session.\n\n### Into the application\n\nThe app reads the file once, when it starts. From then on, the books live in its objects, and every command works with them, not with the file. When the user quits, `save` writes all the books back, and the new contents replace the old. `ReadingLog` gets two small methods for this: `add(book)` puts a ready-made `Book` in the log, and `books()` returns a list of all of them.\n\n```python\ndef add(self, book):\n    self.__books[book.title()] = book\n\ndef books(self):\n    return list(self.__books.values())\n```\n\nIn `ReadingApp`, the constructor loads the books and the command `quit` saves them:\n\n```python\nclass ReadingApp:\n    def __init__(self, storage):\n        self.__storage = storage\n        self.__log = ReadingLog()\n        for book in self.__storage.load():\n            self.__log.add(book)\n\n    def save(self):\n        self.__storage.save(self.__log.books())\n\n    # show_commands, add_book, read and left as before. In run,\n    # the command quit calls self.save() before the break.\n\n\nif __name__ == \"__main__\":\n    app = ReadingApp(BookFile(\"books.txt\"))\n    app.run()\n```\n\n### Giving the app its storage\n\nNotice that `ReadingApp` doesn't create its `BookFile`: it gets one as the parameter `storage`. The app only ever calls `storage.load()` and `storage.save(...)`, so any object with those two methods will do. That makes it easy, for example, to try out the app without touching the real file at all:\n\n```python\nclass TestStorage:\n    def load(self):\n        return [Book(\"Test book\", 100)]\n\n    def save(self, books):\n        print(\"would save\", len(books), \"books\")\n\n\napp = ReadingApp(TestStorage())\n```\n\nGiving an object the things it depends on from outside, instead of letting it create them itself, is called **dependency injection**. Which file the books are kept in, or whether there's a file at all, is decided where the app is created, and `ReadingApp` stays the same.\n","task":"Each line of the file `clubs.txt` holds a school club: its name, its leader and then its members, separated by commas. Fill in the blank so that the name and the leader each get one part of the line and `members` gets a list of the rest, and the program prints:\n\n```\nChess (Ms. Park), members: 2\nChoir (Mr. Diaz), members: 1\nDrama (Ms. Lund), members: 0\n```\n\nThe file `clubs.txt` holds:\n\n```text\nChess,Ms. Park,Ana,Ben\nChoir,Mr. Diaz,Cy\nDrama,Ms. Lund\n```\n","kind":"fill","seed":"class Club:\n    def __init__(self, name, leader, members):\n        self.name = name\n        self.leader = leader\n        self.members = members\n\n    def __str__(self):\n        return f\"{self.name} ({self.leader}), members: {len(self.members)}\"\n\n\nclass ClubFile:\n    def __init__(self, filename):\n        self.__filename = filename\n\n    def load(self):\n        clubs = []\n        with open(self.__filename) as file:\n            for line in file:\n                name, leader, [[*members]] = line.strip().split(\",\")\n                clubs.append(Club(name, leader, members))\n        return clubs\n\n\nif __name__ == \"__main__\":\n    for club in ClubFile(\"clubs.txt\").load():\n        print(club)\n","solution":"class Club:\n    def __init__(self, name, leader, members):\n        self.name = name\n        self.leader = leader\n        self.members = members\n\n    def __str__(self):\n        return f\"{self.name} ({self.leader}), members: {len(self.members)}\"\n\n\nclass ClubFile:\n    def __init__(self, filename):\n        self.__filename = filename\n\n    def load(self):\n        clubs = []\n        with open(self.__filename) as file:\n            for line in file:\n                name, leader, *members = line.strip().split(\",\")\n                clubs.append(Club(name, leader, members))\n        return clubs\n\n\nif __name__ == \"__main__\":\n    for club in ClubFile(\"clubs.txt\").load():\n        print(club)\n","hints":["The name and the leader are the first two parts of the line, and the variable `members` should get a list of all the parts after them.","A `*` in front of a variable gives it the rest of the list: the blank is `*members`."],"tests":[{"name":"Test 1","stdin":"","files":{"clubs.txt":"Chess,Ms. Park,Ana,Ben\nChoir,Mr. Diaz,Cy\nDrama,Ms. Lund\n"},"expect":"Chess (Ms. Park), members: 2\nChoir (Mr. Diaz), members: 1\nDrama (Ms. Lund), members: 0","hidden":false},{"name":"Test 2","stdin":"","files":{"clubs.txt":"Robotics,Mr. Ito,Kim,Lee,Max,Noa\nArt,Ms. Berg\n"},"expect":"Robotics (Mr. Ito), members: 4\nArt (Ms. Berg), members: 0","hidden":true}],"require":[],"forbid":[],"more":[{"task":"`ClubFile` can load the clubs from a file. Add the method `save(clubs)`, which writes a list of `Club` objects to the file, replacing what was there, in the format `load` reads: one line for each club, with the name, the leader and then each member, separated by commas. A club with no members gets a line with just its name and leader. For example, after\n\n```python\nclub_file = ClubFile(\"clubs.txt\")\nclub_file.save([Club(\"Chess\", \"Ms. Park\", [\"Ana\", \"Ben\"]), Club(\"Drama\", \"Ms. Lund\", [])])\n```\n\nthe file `clubs.txt` should hold:\n\n```text\nChess,Ms. Park,Ana,Ben\nDrama,Ms. Lund\n```\n","kind":"code","seed":"class Club:\n    def __init__(self, name, leader, members):\n        self.name = name\n        self.leader = leader\n        self.members = members\n\n    def __str__(self):\n        return f\"{self.name} ({self.leader}), members: {len(self.members)}\"\n\n\nclass ClubFile:\n    def __init__(self, filename):\n        self.__filename = filename\n\n    def load(self):\n        clubs = []\n        try:\n            with open(self.__filename) as file:\n                for line in file:\n                    name, leader, *members = line.strip().split(\",\")\n                    clubs.append(Club(name, leader, members))\n        except FileNotFoundError:\n            pass\n        return clubs\n\n\nif __name__ == \"__main__\":\n    club_file = ClubFile(\"clubs.txt\")\n    club_file.save([Club(\"Chess\", \"Ms. Park\", [\"Ana\", \"Ben\"]), Club(\"Drama\", \"Ms. Lund\", [])])\n    for club in club_file.load():\n        print(club)\n","solution":"class Club:\n    def __init__(self, name, leader, members):\n        self.name = name\n        self.leader = leader\n        self.members = members\n\n    def __str__(self):\n        return f\"{self.name} ({self.leader}), members: {len(self.members)}\"\n\n\nclass ClubFile:\n    def __init__(self, filename):\n        self.__filename = filename\n\n    def load(self):\n        clubs = []\n        try:\n            with open(self.__filename) as file:\n                for line in file:\n                    name, leader, *members = line.strip().split(\",\")\n                    clubs.append(Club(name, leader, members))\n        except FileNotFoundError:\n            pass\n        return clubs\n\n    def save(self, clubs):\n        with open(self.__filename, \"w\") as file:\n            for club in clubs:\n                parts = [club.name, club.leader] + club.members\n                file.write(\",\".join(parts) + \"\\n\")\n\n\nif __name__ == \"__main__\":\n    club_file = ClubFile(\"clubs.txt\")\n    club_file.save([Club(\"Chess\", \"Ms. Park\", [\"Ana\", \"Ben\"]), Club(\"Drama\", \"Ms. Lund\", [])])\n    for club in club_file.load():\n        print(club)\n","hints":["Open the file for writing with `with open(self.__filename, \"w\") as file:`. The mode `\"w\"` replaces what was in the file.","Go through the list with `for club in clubs:` and write one line for each club. The parts of a line are `club.name`, `club.leader` and the names in `club.members`.","`[club.name, club.leader] + club.members` is one list of all the parts, and `\",\".join(...)` puts commas between them. Remember the `\"\\n\"` at the end of each line."],"tests":[{"name":"Test 1","stdin":"","call":"club_file = ClubFile(\"clubs.txt\")\nclub_file.save([Club(\"Chess\", \"Ms. Park\", [\"Ana\", \"Ben\"]), Club(\"Drama\", \"Ms. Lund\", [])])\n","writes":{"clubs.txt":"Chess,Ms. Park,Ana,Ben\nDrama,Ms. Lund\n"},"expect":"","hidden":false},{"name":"Test 2","stdin":"","call":"club_file = ClubFile(\"clubs.txt\")\nclubs = club_file.load()\nclubs[1].members.append(\"Kim\")\nclubs[1].members.append(\"Lee\")\nclubs.append(Club(\"Garden\", \"Mr. Ruiz\", [\"Ola\"]))\nclub_file.save(clubs)\nfor club in club_file.load():\n    print(club)\n","files":{"clubs.txt":"Art,Ms. Berg,Uma\nRobotics,Mr. Ito\n"},"writes":{"clubs.txt":"Art,Ms. Berg,Uma\nRobotics,Mr. Ito,Kim,Lee\nGarden,Mr. Ruiz,Ola\n"},"expect":"Art (Ms. Berg), members: 1\nRobotics (Mr. Ito), members: 2\nGarden (Mr. Ruiz), members: 1","hidden":true}],"require":[],"forbid":[]},{"task":"Put the clubs application together. `Club`, the storage class `ClubFile` and the logic class `Clubs` are ready in the starter code. Write the user interface, `ClubApp`:\n\n- The constructor gets a `ClubFile` as its parameter `storage`, and adds every club that `storage.load()` gives to the `Clubs` object, with its method `add`.\n- `run` first prints `commands: join, show, quit`, and then reads commands with the prompt `> ` until the user types `quit`.\n- `join` asks for a club and then for a name, and adds the name to the club's members. For a club that doesn't exist it prints `no such club`, and asks nothing more.\n- `show` asks for a club and prints its members, one on each line, or `no members yet` when it has none. For a club that doesn't exist it prints `no such club`.\n- `quit` saves all the clubs with `storage.save(...)` and ends the program. Any other command prints the commands again.\n\nGive each command a method of its own. For example, when `clubs.txt` holds\n\n```text\nChess,Ms. Park,Ana,Ben\nDrama,Ms. Lund\n```\n\na run could go like this:\n\n```\ncommands: join, show, quit\n> show\nclub: Chess\nAna\nBen\n> join\nclub: Drama\nname: Eli\n> join\nclub: Golf\nno such club\n> show\nclub: Drama\nEli\n> quit\n```\n\nand after it, `clubs.txt` holds:\n\n```text\nChess,Ms. Park,Ana,Ben\nDrama,Ms. Lund,Eli\n```\n","kind":"code","seed":"class Club:\n    def __init__(self, name, leader, members):\n        self.name = name\n        self.leader = leader\n        self.members = members\n\n\nclass ClubFile:\n    def __init__(self, filename):\n        self.__filename = filename\n\n    def load(self):\n        clubs = []\n        try:\n            with open(self.__filename) as file:\n                for line in file:\n                    name, leader, *members = line.strip().split(\",\")\n                    clubs.append(Club(name, leader, members))\n        except FileNotFoundError:\n            pass\n        return clubs\n\n    def save(self, clubs):\n        with open(self.__filename, \"w\") as file:\n            for club in clubs:\n                parts = [club.name, club.leader] + club.members\n                file.write(\",\".join(parts) + \"\\n\")\n\n\nclass Clubs:\n    def __init__(self):\n        self.__clubs = {}\n\n    def add(self, club):\n        self.__clubs[club.name] = club\n\n    def has_club(self, name):\n        return name in self.__clubs\n\n    def join(self, name, member):\n        self.__clubs[name].members.append(member)\n\n    def members(self, name):\n        return self.__clubs[name].members\n\n    def all_clubs(self):\n        return list(self.__clubs.values())\n\n\nclass ClubApp:\n    def __init__(self, storage):\n        self.__clubs = Clubs()\n        self.__storage = storage\n\n    def run(self):\n        pass\n\n\nif __name__ == \"__main__\":\n    app = ClubApp(ClubFile(\"clubs.txt\"))\n    app.run()\n","solution":"class Club:\n    def __init__(self, name, leader, members):\n        self.name = name\n        self.leader = leader\n        self.members = members\n\n\nclass ClubFile:\n    def __init__(self, filename):\n        self.__filename = filename\n\n    def load(self):\n        clubs = []\n        try:\n            with open(self.__filename) as file:\n                for line in file:\n                    name, leader, *members = line.strip().split(\",\")\n                    clubs.append(Club(name, leader, members))\n        except FileNotFoundError:\n            pass\n        return clubs\n\n    def save(self, clubs):\n        with open(self.__filename, \"w\") as file:\n            for club in clubs:\n                parts = [club.name, club.leader] + club.members\n                file.write(\",\".join(parts) + \"\\n\")\n\n\nclass Clubs:\n    def __init__(self):\n        self.__clubs = {}\n\n    def add(self, club):\n        self.__clubs[club.name] = club\n\n    def has_club(self, name):\n        return name in self.__clubs\n\n    def join(self, name, member):\n        self.__clubs[name].members.append(member)\n\n    def members(self, name):\n        return self.__clubs[name].members\n\n    def all_clubs(self):\n        return list(self.__clubs.values())\n\n\nclass ClubApp:\n    def __init__(self, storage):\n        self.__clubs = Clubs()\n        self.__storage = storage\n        for club in self.__storage.load():\n            self.__clubs.add(club)\n\n    def show_commands(self):\n        print(\"commands: join, show, quit\")\n\n    def join(self):\n        name = input(\"club: \")\n        if not self.__clubs.has_club(name):\n            print(\"no such club\")\n            return\n        member = input(\"name: \")\n        self.__clubs.join(name, member)\n\n    def show(self):\n        name = input(\"club: \")\n        if not self.__clubs.has_club(name):\n            print(\"no such club\")\n            return\n        members = self.__clubs.members(name)\n        if len(members) == 0:\n            print(\"no members yet\")\n        for member in members:\n            print(member)\n\n    def save(self):\n        self.__storage.save(self.__clubs.all_clubs())\n\n    def run(self):\n        self.show_commands()\n        while True:\n            command = input(\"> \")\n            if command == \"quit\":\n                self.save()\n                break\n            elif command == \"join\":\n                self.join()\n            elif command == \"show\":\n                self.show()\n            else:\n                self.show_commands()\n\n\nif __name__ == \"__main__\":\n    app = ClubApp(ClubFile(\"clubs.txt\"))\n    app.run()\n","hints":["In the constructor, go through `self.__storage.load()` and add each club with `self.__clubs.add(club)`.","Write a method for each command, as in `ReadingApp`, and a `run` with a `while True` loop that reads `input(\"> \")` and calls them. `join` and `show` both check `self.__clubs.has_club(name)` first, and `return` after printing `no such club`.","Saving is one line, `self.__storage.save(self.__clubs.all_clubs())`. Do it for the command `quit`, before the `break`."],"tests":[{"name":"Test 1","stdin":"show\nChess\njoin\nDrama\nEli\njoin\nGolf\nshow\nDrama\nquit\n","files":{"clubs.txt":"Chess,Ms. Park,Ana,Ben\nDrama,Ms. Lund\n"},"writes":{"clubs.txt":"Chess,Ms. Park,Ana,Ben\nDrama,Ms. Lund,Eli\n"},"expect":"commands: join, show, quit\n> show\nclub: Chess\nAna\nBen\n> join\nclub: Drama\nname: Eli\n> join\nclub: Golf\nno such club\n> show\nclub: Drama\nEli\n> quit","hidden":false},{"name":"Test 2","stdin":"show\nRobotics\njoin\nRobotics\nKim\nhelp\njoin\nRobotics\nLee\nshow\nRobotics\nshow\nArt\nquit\n","files":{"clubs.txt":"Robotics,Mr. Ito\nArt,Ms. Berg,Uma\n"},"writes":{"clubs.txt":"Robotics,Mr. Ito,Kim,Lee\nArt,Ms. Berg,Uma\n"},"expect":"commands: join, show, quit\n> show\nclub: Robotics\nno members yet\n> join\nclub: Robotics\nname: Kim\n> help\ncommands: join, show, quit\n> join\nclub: Robotics\nname: Lee\n> show\nclub: Robotics\nKim\nLee\n> show\nclub: Art\nUma\n> quit","hidden":true},{"name":"Test 3","stdin":"show\nChess\njoin\nChess\nquit\n","writes":{"clubs.txt":""},"expect":"commands: join, show, quit\n> show\nclub: Chess\nno such club\n> join\nclub: Chess\nno such club\n> quit","hidden":true}],"require":[{"pattern":"\\.load\\(","message":"Load the clubs in the constructor with the storage's `load()`."},{"pattern":"\\.save\\(","message":"Save the clubs with the storage's `save(...)` when the user quits."}],"forbid":[]}]}