CS21 Lab 11: Swindle (objects)

Due Monday, May 4, before midnight


Please read through the entire lab before starting!

In this lab, you will build an e-reader called the Swindle (any resemblance to current e-readers is completely coincidental). You will use object-oriented programming to create two classes.

All of the standard style guidelines from previous labs still apply. Be sure to follow good top-down-design practices.

As you write programs, use good programming practices:

  • Use a comment at the top of the file to describe the purpose of the program (see example).

  • All programs should have a main() function (see example).

  • Use variable names that describe the contents of the variables.

  • Write your programs incrementally and test them as you go. This is really crucial to success: don’t write lots of code and then test it all at once! Write a little code, make sure it works, then add some more and test it again.

  • Don’t assume that if your program passes the sample tests we provide that it is completely correct. Come up with your own test cases and verify that the program is producing the right output on them.

  • Avoid writing any lines of code that exceed 80 columns.

    • Always work in a terminal window that is 80 characters wide (resize it to be this wide)

    • In vscode, at the bottom right in the window, there is an indication of both the line and the column of the cursor.

Function Comments

All functions should include a comment explaining their purpose, parameters, return value, and describe any side effects. Please see our function example page if you are confused about writing function comments.

Goals

The goals for this lab assignment are:

  • Gain experience writing classes.

  • Gain experience reading and using classes and provided library functions.

1. Introduction

The first class you will develop is the Book class that encapsulates one electronic book (or ebook). We have made several free ebooks available for you to load and experiment with to get your Book class working.

The second class you will develop is the Swindle class that encapsulates the concept of an e-reader. You will be able to perform several core operations of an e-reader, including buying books, picking a book from your virtual shelf, and reading a book.

To get started, take note of the files that we have provided for you:

  • book.py — the definition of the Book class to be completed by you

  • swindle.py — the definition of the Swindle class to be completed by you (we give you a partially implemented constructor and a few other completed methods)

  • bookdb.txt — a file containing information about all of the books available for download

  • ereader.py — the main program to create a Swindle and interact with the device (we give you all of this)

1.1. Sample runs

Read through the two sample runs provided below. The first one is annotated with comments so that you can see how the e-reader should operate, and understand which parts you need to implement and which parts are already implemented for you. The second one is an uninterrupted run.

1.2. Testing

  • As always, you should test your program as you go.

  • Since you’re working with three files, it’s a good idea to put the tests for your class in the corresponding file.

  • Like we saw in lecture, this can be done by writing a main() method, and then calling it at the bottom using:

    if __name__ == "__main__":
        main()

    This way, your testing code will run when you execute python3 book.py on the command line, but it won’t run when you import from book in swindle.py

2. The Book class

First, complete the Book class in the file book.py.

A Book object represents an electronic book to read on your e-reader. You should define your class such that each book maintains the following data (i.e., instance variables):

  • title (string) — the title of the book

  • author (string) — the author of the book

  • year (int) — the year the book was published

  • filename (string) — where the book is stored on the CS lab machines, for example: "/usr/local/doc/alice.txt"

  • bookmark (int) — the page number where the owner left off when last reading the book

Your Book class should define the following methods:

  • Constructor (__init__)

    • The constructor should take parameters for the title, author, year published, and the name of the file containing the book. It should save these variables as instance variables for the object (i.e., initializing the title, author, year, and filename instance variables).

    • The constructor should also set the bookmark instance variable to page 0.

  • get_title(…​)

    • The getter method for the title should take no parameters (other than self), and it should return the title of the book (a string instance variable stored in the object).

  • get_author(…​)

    • The getter method for the author should take no parameters (other than self), and it should return the author of the book (a string instance variable stored in the object).

  • get_year(…​)

    • The getter method for the publication year should take no parameters (other than self), and it should return the publication year of the book (an int instance variable stored in the object).

  • get_filename(…​)

    • The getter method for the book’s filename should take no parameters (other than self), and it should return the full filename of the book (a str instance variable stored in the object).

  • get_bookmark(…​)

    • The getter method for the bookmark should take no parameters (other than self), and it should return the page number of the current bookmark (an int instance variable stored in the object).

  • set_bookmark(…​)

    • The setter method for the bookmark; it should take one parameter (in addition to self): an int indicating the page number of the new bookmark for the book. The method should save this new page number to the bookmark instance variable stored within the object. It should not return any value.

  • get_text(…​): method to retrieve the entire contents of the book as a single string. This method will need to:

    1. Open the filename stored in the Book object

    2. Ignore any lines in the file that start with a pound sign (#)

    3. Store all lines of the file in one long string. Recall that each line, when read from the file, will end with a newline character \n; your final string should include these newline characters.

    4. Close the file

    For example, if the file contained the "Roses are red…​" poem:

    # Here is a silly poem
    Roses are red,
    Violets are blue.
    # Second verse
    Sugar is sweet,
    And so are you!

    Then get_text(…​) would return a string that looks like this, where the comment lines (starting with the pound sign) have been removed:

    "Roses are red,\nViolets are blue.\nSugar is sweet,\nAnd so are you!\n"

    When you print this string it will look like this:

    Roses are red,
    Violets are blue.
    Sugar is sweet,
    And so are you!

    You should use the same method we have used in the past for reading a file, but you will need to combine all of the lines of text into a single string (Hint: you may need a string accumulator). Don’t forget to close the file when you’re finished reading it, before returning.

  • __str__: method that returns a string representation of a Book object. Your method should return a string containing the title, author, and date published in a readable format. Use string formatting to give the title a width of 25 and the author a width of 25. When you print information about multiple books, this will line the book information up nicely as shown below:

           Alice in Wonderland by             Lewis Carroll (1865)
            Gettysburg Address by               Abe Lincoln (1863)

    Note that you will not call this method directly. Python will automatically call it whenever a Book object needs to be converted to a string (e.g., when you pass the object to the function str(…​) or when printing it with print(…​)).

2.1. Incrementally test the Book class

As you complete each method of the Book class you should test it. The following is an example of some test code that should work and run without errors if you’ve implemented the Book class properly. You can use this as a starting point, but you should also add additional test code as you implement the above methods to further test your implementation.

def main():
    print("Testing the Book class...")
    mybook = Book("Gettysburg Address", "Abe Lincoln", 1863,
                  "/usr/local/doc/GettysburgAddress")
    print("Testing __str__...")
    print(mybook)
    print("Testing get_filename...")
    print(mybook.get_filename())
    print("Testing get_text...")
    text = mybook.get_text()
    print(text[:300]) #only print the first couple of lines
    print("bookmark is: %d" % mybook.get_bookmark())
    mybook.set_bookmark(12)
    print("now bookmark is: %d" % mybook.get_bookmark())
    ################ Write additional tests below ###################

if __name__ == '__main__':
    main()

Enhance this test code by adding a number of your own tests. Do not move on to the Swindle class until you are sure that your Book class is working correctly! Include all of your test code in the code you submit.

3. Complete the Swindle class

Next, write the Swindle class in the file swindle.py.

The Swindle class defines a Swindle e-reader device. Every instance must maintain a few pieces of data (as instance variables):

  • owner (string) — the owner’s name

  • available_books (list of Book objects) — books available for sale

  • owned_books (list of Book objects) — books owned by the user

  • page_length (int) — the number of lines to display per page; 20 is a good default page length

The Swindle class should contain the following methods:

  • Constructor (__init__)

    • Part of this has been implemented for you, and you are responsible for completing the implementation. This constructor includes functionality for loading the database of available books). We recommend you read the code provided to familiarize yourself with the instance variables of the Swindle class and how they are initialized. You only need to add a few lines of code where the "bookdb.txt" file is opened and read in to create the list of available books; specifically, you should add code where you see the comment:

      #TODO: using the information just obtained from the file,
      # create a Book object with this data and add it to the
      # available_books list

      When first creating a Swindle object, the user does not own any books (i.e., the owned_books instance variable is an empty list), and all books in the database should be considered books available for purchase (in the available_books instance variable).

  • display_text(…​), display_page(…​), get_letter(…​) (provided for you)

    • These three methods are provided for you and you should not modify them. You will need to make use of the display_text(…​) method in other parts of your code for this lab. The display_text(…​) method takes one parameter (other than self), which is a book object which is to be read; it then displays the text of the book, one page at a time. After each page is displayed, the user has the option of choosing to go to the next page, the previous page, or to quit. When the user selects the quit option, display_text(…​) completes and returns the page number that was displayed last. You should use the display_text method provided as-is, without modifying it.

  • get_owner(…​)

    • A getter method that returns the Swindle’s owner.

  • show_available(…​)

    • This should print all books that are available to purchase but are not yet owned. If there are no books available, print a short message saying so. See the sample output to see how the list is formatted. It takes no parameters and does not return anything.

  • buy(…​)

    • This allows the user to purchase a book. This method should print all available titles and allow the user to choose one book. Hint: use the show_available() method as a helper. If the choice is valid, the purchased book should be added to the list of owned books and removed from the list of available books. You can use the pop() method of lists to accomplish this, as the following example demonstrates. You should print a message verifying that purchase was successful.

      $ python3
      >>> ls = ["this", "is", "a", "test"]
      >>> word = ls.pop(1) #remove and return the item at index 1
      >>> print(word)
      is
      >>> print(ls)
      ['this', 'a', 'test']
  • show_owned(…​)

    • This method takes no additional parameters (other than self). It prints all currently owned books. If there are no currently owned books, it should print a short message saying so. See the example output for an example of how the list is formatted. The method should not return anything.

  • read(…​)

    • This method allows the user to choose a book to read. It takes no additional parameters (other than self). This method should print all owned books and allow the user to choose one book. Hint: Use the show_owned() method as a helper. It then calls the display_text() method, using the appropriate Book object as an argument to display_text(). When the user quits reading a book, set the bookmark to the current page, and print a message verifying that the bookmark was set.

For both the buy() and read() methods, you should validate the user’s input to ensure that they select a valid book number from the menu. You can assume that the user always enters an integer.

3.1. Incrementally test the Swindle class

As you complete each method of the Swindle class you should test it. Test code should be placed in main. The below is an example of some test code you might include at the end of your file, but you should include more test cases than this as well:

def main():
    print("Testing the Swindle class...")
    myswindle = Swindle()
    print("Testing show_available...")
    myswindle.show_available()
    print("Testing show_owned...")
    myswindle.show_owned()
    ################ Write additional tests below ###################

if __name__ == '__main__':
    main()

4. Putting it all together

Once you have written (and thoroughly tested!) the Book and Swindle classes, according to the above requirements, you should be able to successfully run the main e-reader program: $ python3 ereader.py

Here again is the sample run demonstrating how the e-reader should behave.

Test out all of features of your classes by trying every possible menu option from the e-reader. Be sure to gracefully handle these edge cases:

  • when the user wants to buy books, but none are available

  • when the user wants to read books, but none are available

Additional sample output showing various edge cases is here: edge cases sample run.

5. Requirements

The code you submit for labs is expected to follow good style practices, and to meet one of the course standards, you’ll need to demonstrate good style on six or more of the lab assignments across the semester. To meet the good style expectations, you should:

  • Write a comment that describes the program as a whole using a comment at the top of the file.

  • All programs should have a main() function.

  • Use descriptive variable names.

  • Write code with readable line length, avoiding writing any lines of code that exceed 80 columns.

  • Add comments to explain complex code segments (as applicable).

  • Write a comment for each function (except main) that describes its purpose, parameters and return value.

In addition, you’ll need to demonstrate good top-down design practices on two or more lab assignments across the semester. To meet the top-down design expectations, you should:

  • Have a main() function that serves as an outline for your program.

  • Your main() function should call other functions that implement subtasks.

  • The functions that are called by main() should be well-named and have good comments explaining their purpose, parameters (including their types), and return value (including its type).

Your program should meet the following requirements:

  1. Your implementation of the Book class should be in the file book.py and its methods should behave as described in section 2 above.

  2. Your implementation of the Swindle class should be in the file swindle.py and its methods should behave as described in section 3 above.

  3. Your book.py and swindle.py files should contain tests of the methods you’ve written, as mentioned in section 1 and as described in sections 2.1 and 3.1.

  4. Your implementation of the buy and read methods should perform basic input validation, and re-prompt the user if they enter an invalid number (either negative, or larger than the number of books)

  5. Your implementation of the read method should update the bookmark of the book when the user finishes reading.

  6. When using Book objects from within the Swindle class, you should not access instance variables directly, but rather only get or modify them using getter and setter methods.

  7. The show_owned and show_available methods of the Swindle class should display a message if there are no books that are owned (for show_owned) or available to purchase (for show_available).

  8. The show_owned and show_available methods should display a numbered list of books.

  9. The read and buy methods should allow the user to enter 0 in order to return to the main menu without reading or buying anything.

  10. In the read, method, if no books are owned, a message should be displayed saying this and the user should not be prompted to select a book to read. Similarly, in the buy method, if there are no books available for purchase, a short message should be displayed and the user should not be prompted to select a book to buy.

6. Optional Enhancements

This section is completely optional. Only attempt this once you have successfully completed all of the required portions of the lab. Because this is optional extension, we are leaving a lot of the details for you to figure out on your own.

If you wish to attempt this part, be sure to make copies of your orignal code as shown below. Only modify the copies, leaving the original solution alone.

$ cd cs21/labs/10
$ cp swindle.py enhancedSwindle.py
$ cp ereader.py enhancedEreader.py

The original version of the e-reader starts from scratch every time, which is unrealistic. Instead, we would like the e-reader to remember the owner’s name, the books that have been purchased (and what page number the owner is on), as well as the books that are still available.

Here are several sample runs demonstrating how the enhanced e-reader should behave.

To accomplish this you will need to learn how to write to a file. Below is a short example program showing how to do this.

def main():
    name = "Sedi"
    age = 21
    fp = open("test.txt", "w")
    fp.write("%s is %d years old\n" % (name, age))
    fp.write("this is a test\n")
    fp.write("here's another line\n")
    fp.write("goodbye\n")
    fp.close()

main()

Here’s what the file test.txt would now contain:

Sedi is 21 years old
this is a test
here's another line
goodbye

One other new python feature you’ll need is to be able to check whether a particular file exists or not. You can try this in the python intepreter:

$ python3
>>> from os.path import isfile
>>> isfile("ereader.py")
True
>>> isfile("foobar")
False

Now that you know how to write a file and how to check whether a file exists, you are ready to implement the enhanced e-reader. The basic idea is as follows.

  • Every time the user quits the e-reader, write out a status file that stores all of the relevant information about the e-reader. You can decide on the format for this status file.

  • When you start up the e-reader, check if this status file exists.

  • If not, everything works as it did in the original e-reader; you’ll need to read in the available books from the file bookdb.txt.

  • If it does exist, welcome the owner back and read in the status information from the file you created (you can ignore the file bookdb.txt).

Without too much effort you’ll have an enhanced e-reader that is much more like an actual device.

7. Answer the Questionnaire

After each lab, please complete the short Google Forms questionnaire. Please select the right lab number (Lab 11) from the dropdown menu on the first question.

Once you’re done with that, you should run handin21 again.

Submitting lab assignments

Remember to run handin21 to turn in your lab files! You may run handin21 as many times as you want. Each time it will turn in any new work. We recommend running handin21 after you complete each program or after you complete significant work on any one program.

Logging out

When you’re done working in the lab, you should log out of the computer you’re using.

First quit any applications you are running, including your vscode editor, the browser and the terminal. Then click on the logout icon (logout icon or other logout icon) and choose "log out".

If you plan to leave the lab for just a few minutes, you do not need to log out. It is, however, a good idea to lock your machine while you are gone. You can lock your screen by clicking on the lock xlock icon. PLEASE do not leave a session locked for a long period of time. Power may go out, someone might reboot the machine, etc. You don’t want to lose any work!