A document from MCS 275 Spring 2021, instructor David Dumas. You can also get the notebook file.

Project 1

MCS 275 Spring 2021 - David Dumas

Instructions:

Deadline

This project must be submitted to Gradescope by 6:00pm CST on Friday, February 5, 2021.

Collaboration policy & academic honesty

This project must be completed individually. Seeking or giving aid on this assignment is prohibited; doing so constitutes academic misconduct which can have serious consequences. The only resources you are allowed to consult are the ones listed below. It is very important to follow these rules. If you are unsure about whether something is allowed, ask. The course syllabus contains more information about the course and university policies regarding academic honesty.

Resources you are allowed to consult

  • All documents and videos posted to the course web page (including lecture slides, lecture videos, the Python tour)
  • Course textbooks

What to do if you're stuck

Contact the instructor or TA by email, or visit office hours.

Conceptual overview

This section gives a conceptual description of the project. To actually write code, you need to read (and follow!) the precise specifications given later. But reading this section first will probably help.

In this project you are going to make a module containing a class hierarchy for representing and managing a list of tasks that a computer program needs to perform at specific times. For this project, a task is something that might need to be done once or which might happen on a regular basis, similar to these real-world tasks:

  • Next Tuesday: Remagnetize the rain gutters
  • Every Monday for the next 15 weeks: Post the quiz
  • Every Saturday (forever): Organize sock drawer

In this list you see the three basic types of tasks you'll be working with: one-time tasks (to be represented by class OneTimeTask), recurring tasks that happen a fixed number of times (class BoundedRecurringTask), and recurring tasks that repeat forever (class UnboundedRecurringTask).

Dealing with real world times and dates is complicated, so to avoid that becoming a huge part of this project, we'll just work with a simplified abstraction. Time will be a nonnegative integer, whose meaning doesn't matter except in terms of comparisons: Time 5 means something that happens after time 3 and before time 6.

So to revise the examples above, this list is more tuned to the actual project specs:

  • A one time task: At time 15, do thing X
  • A bounded recurring task: Starting at time 4, and repeating every 6 units of time until it has happened 10 times, do thing Y
  • An unbounded recurring task: Starting at time 42, and repeating every 100 units of time, do thing Z

Tasks will be represented by objects using classes that you are going to write. Task objects will store a description of the task and the information about when it happens (including any recurrence data).

A program that has created a task object will only be able to ask the task object to do a few things, such as

  • report when it is supposed to happen next
  • run right now (perform the work of the task)
  • "retire" all scheduled instances of the task up to a certain time (think of this as deleting them, or marking them as "done")

A significant aspect of this design is that task objects don't know the current time. That's the responsibility of the program using them. But a program that has a bunch of task objects can do everything it needs to schedule them: It can check when the next task needs to happen, wait for that moment, run it, and then retire it. Then it can look again to see what needs to happen next. A loop of this kind can ensure all the tasks run at the right times.

What you need to do

First, read this project description document.

Before you start working, download the starter pack. It is a zip file that contains several python source files. Extract them into the directory where you want to work on your project. (Ask for help if you don't know how to extract a zip file in your operating system. It is a function built-in to Windows and MacOS, and which most Linux distributions support with the tools they install by default.)

Then, in the same directory where you extracted the starter pack, create a file called tasks.py. In that file, write a module that defines five classes:

  • Task
  • OneTimeTask, a subclass of Task
  • RecurringTask, a subclass of Task
  • BoundedRecurringTask, a subclass of RecurringTask
  • UnboundedRecurringTask, a subclass of RecurringTask These classes must do exactly what is described in the section titled "tasks MODULE DOCUMENTATION" below.

The starter pack includes a few programs that try to use the tasks module to do things. Of course, these programs won't work at first, because the tasks module doesn't exist. But once you've built the module, you can try these programs as a basic way of testing your work. The example programs are described in more detail in the section Example programs below.

Keep in mind that the example programs do not test everything! The autograder will run a different set of tests, and the only way to guarantee your work will be considered correct is to make classes that behave exactly as described in the tasks MODULE DOCUMENTATION section.

WHEN YOU ARE DONE, tasks.py IS THE ONLY FILE YOU SHOULD SUBMIT TO GRADESCOPE

tasks MODULE DOCUMENTATION

This section is the core of the assignment. The module tasks doesn't exist yet, but everything it needs to do is documented below. Write the module to match this documentation. A project submission will be considered correct if it does exactly what is written in this section, and if it follows the rules in the section "Other requirements" below.

class Task

Superclass: None

Purpose: Not intended to be instantiated. Serves as the base class for other task types, defining attributes and methods all subclasses must implement.

Required attributes:

  • description : A string describing the task
  • active : A boolean that always indicates whether this task is waiting to be run; once the last time the task needs to run has been retired, this attribute must be set to False. In this base class, it is always False.

Methods:

  • __init__(self,description) : Saves description as an instance attribute. Sets active to False.
  • next_time(self) : Raises an Exception because active is False (signaling to the caller that there is no "next" time to run the task); in subclasses, this method will return the next time the task needs to run.
  • run(self) : Raises an Exception because the base class Task is not intended to ever be used directly; in subclasses, this method will simulate running the task by printing a message.
  • retire(self,t) : Raises an Exception because the base class Task is not intended to ever be used directly; in subclasses, this method will retire all scheduled times the task is supposed to run that are less than or equal to t. In essence, this method is used to tell the Task object: "The current time is after t, so forget about everything up to that time."

Example of using this class:

It is not intended to be used directly.

class OneTimeTask

Superclass: Task

Purpose: Represent an Task that happens once, at a specified time.

Required attributes: This list does not include attributes of the superclass, which will also exist in this subclass.

  • None

Methods:

  • __init__(self,description,t) : Saves description as an instance attribute. Sets active to True. Stores t, which is the scheduled time of the task, as an attribute. The attribute storing the time is not meant to be accessed by any user of the class, so it can be given any name.
  • next_time(self) : If active is False, raise an Exception (because there isn't a "next" time to report). Otherwise, return the scheduled time of the task.
  • run(self) : If active is False, raise an Exception. Otherwise, print a message in exactly this format:
    run: class=OneTimeTask description="Post the quiz" time=58
    (printing the message is to simulate actually doing the task)
  • retire(self,t) : If the scheduled time of the task is less than or equal to t, set active to False. Otherwise, do nothing.

Example of using this class:

This example is elaborated upon in the program test_OneTimeTask.py.

In [ ]:
e = tasks.OneTimeTask("Receive Nobel Prize",t=1981823) # create a one-time Task object
print(e.next_time())  # prints 1981823
e.retire(100) # does nothing; it is not scheduled to run at or before time t=100
e.run() # prints the following: 'run: class=OneTimeTask description="Receive Nobel Prize" time=1981823'
e.retire(1981823) # retires the task
print(e.next_time()) # raises exception, because the task is no longer active

class RecurringTask

Superclass: Task

Purpose: Base class for Tasks that happen more than once. Not intended to be used directly.

Required attributes: This list does not include attributes of the superclass, which will also exist in this subclass.

  • None

Methods: This list does not include methods inherited from the superclass that do not need to be changed. This class adds a feature not present in the Task base class:

  • num_until(self,end) : Raises Exception because this class isn't meant to be used. In subclasses, returns the number of times the task is scheduled to run at or before time end (excluding any retired instances of the task).

Example of using this class:

It is not intended to be used directly.

class BoundedRecurringTask

Superclass: RecurringTask

Purpose: Represent a task scheduled to first occur at a specified time, and which then happens a fixed number of times, separated by the same time interval (e.g. start at time 5, and then do it every 3 units of time, stopping after 10 times).

Required attributes: This list does not include attributes of the superclass, which will also exist in this subclass.

  • None

Methods:

  • __init__(self,description,start,gap,n) : Saves description as an instance attribute. Sets active to True as long as n is positive. The arguments start, gap, and n are nonnegative integers, with start being the time the task first runs, gap the interval between times when it runs, and n the total number of times it must run. Information about this schedule must be stored as attributes, but will not be accessed directly by users of the class, so it is up to you to choose the best way to save and use this information. The task will only run a finite number of times, so just storing a list of all those times is one option.
  • next_time(self) : If active is False, raise an Exception. Otherwise, return the next time the task needs to run.
  • run(self) : If active is False, raise an Exception. Otherwise, print a message in exactly this format:

    run: class=BoundedRecurringTask description="Teach the class" time=1391

    (printing the message is to simulate actually doing the task)

  • retire(self,t) : Retire (forget about) all scheduled instances of this task that would happen at or before time t, so that a subsequent call to next_time() will return a time greater than t (or raise an exception). If the result of this is that every remaining instance of the ask is retired, then set active to False.

  • num_until(self,end) : Return the number times the task is scheduled to run at or before time end (an integer), not including any instances that have been retired. This would be 0 if all have been retired (equivalently, if active is False).
  • num_total(self) : Return the total number of times the task is scheduled to run, not including any instances that have been retired.

Example of using this class:

The code below is elaborated upon in the program test_BoundedRecurringTask.py.

In [ ]:
# create task that happens 15 times
e = tasks.BoundedRecurringTask("Lead MCS 275 discussion",start=16432,gap=40,n=15)
print(e.next_time())  # prints 16432
print(e.num_total())  # prints 15
print(e.num_until(16500)) # prints 2, for the actions at 16432 and 16472
e.retire(100) # does nothing; there are no actions at or before time t=100
e.run() # prints: 'run: class=BoundedRecurringTask description="Lead MCS 275 discussion" time=16432'
e.retire(16432) # retires the task run on the previous line (marks it done)
print(e.next_time()) # prints 16472
print(e.num_total())  # prints 14, since one of the originally scheduled instances has been retired

class UnboundedRecurringTask

Superclass: RecurringTask

Purpose: Represent an Task that starts at some time and repeats forever, waiting a fixed amount of time between successive instances. (e.g. The task might start at time 6 and repeat every 3 units of time.) Objects of this class keep track of the next time that still needs to run, which changes when tasks are retired.

Required attributes: This list does not include attributes inherited from the superclass.

  • None

Methods:

  • __init__(self,description,start,gap) : Saves description as an instance attribute. Sets active to True. The arguments start and gap are nonnegative integers, with start being the time this task first runs, gap the interval between times it will run. Information about this schedule must be stored as attributes, but will not be accessed directly by users of the class, so it is up to you to choose the best way to save and use this information. However, since this type of task can run infinitely many times, you can't just store a list of all the times it will run.
  • next_time(self) : Return the scheduled time of the earliest time the task must run that hasn't yet been retired.
  • run(self) : Print a message in exactly this format:
    run: class=UnboundedRecurringTask description="Organize sock drawer" time=7710
  • retire(self,t) : Retire (forget about) all scheduled instances of this task that would happen at or before time t, so that a subsequent call to next_time() will return a time greater than t.
  • num_until(self,end) : Return the number times the task is scheduled to run at or before time end (an integer), not including any instances that have been retired. This would be 0 if all have been retired (equivalently, if active is False).

Note: This class represents an infinite sequence of actions. Therefore, the attribute active is always True, and the class doesn't have a num_total(...) method (since there are always infinitely many actions left).

Example of using this class:

This example is elaborated upon in the program test_UnboundedRecurringTask.py.

In [ ]:
e = tasks.UnboundedRecurringTask("Read course evaluations",start=16100,gap=650)
print(e.next_time())  # prints 16100
print(e.num_until(17000)) # prints 2, for the actions at 16100 and 16750
e.retire(100) # does nothing; there are no actions at or before time t=100
e.run() # prints: 'run: class=UnboundedRecurringTask description="Read course evaluations" time=16100
e.retire(16100) # retires the action scheduled for time 16100
print(e.next_time()) # prints 16750

Other requirements

This section contains rules your code must follow, in addition to it needing to do everything documented in the previous section.

Coding standards

Like everything you submit for credit, your code must follow the rules in the course coding standards document.

Proper use of inheritance

You are building a class hierarchy. You should make proper use of inheritance by:

  • Having every constructor call its superclass constructor, followed by additional class-specific initialization
  • Only redefining a method in a subclass if its behavior must differ from that of the method defined in the superclass

No extra code in tasks.py

The file tasks.py should define the necessary classes, and not do anything else. Running

python tasks.py

should succeed (no exceptions) but not print or do anything.

Example programs

Seven example programs are included that test various parts of the module tasks. They are numbered roughly in order of increasing complexity; as you develop the module tasks, it is likely that the lower-numbered programs will work sooner than the higher-numbered ones.

The example programs are:

  • 01_tasks_importable.py -- Tries to import the module tasks. Should succeed as soon as you have a syntactically valid tasks.py file in the same directory. If this fails, no other example program will work.
  • 02_tasks_has_required_classes.py -- Checks that module tasks defines each of the five required classes. (It doesn't actually check that they are classes, but just that they are names defined in the module.)
  • 03_tasks_has_required_hierarchy.py -- Checks that the required classes inherit from one another in the specified manner.
  • 04_test_OneTimeTask.py -- Instantiate OneTimeTask and call various methods. At each step, shows both the expected output and the output obtained from your code.
  • 05_test_BoundedRecurringTask.py -- Instantiate BoundedRecurringTask and call various methods. At each step, shows both the expected output and the output obtained from your code.
  • 06_test_UnboundedRecurringTask.py -- Instantiate UnboundedRecurringTask and call various methods. At each step, shows both the expected output and the output obtained from your code.
  • 07_complete_scheduler.py -- A complete example of the kind of task scheduler than can be built using the classes in tasks; it creates several task objects of various types and then runs them in the proper order. At each step, shows both the expected output and the output obtained from your code.

Warning: None of the programs tests the behavior of the methods of classes Task and RecurringTask directly!

Closing remark

The documentation of the module tasks is quite long, but a correct solution can be created with relatively few lines of code (about 150). That's the way it should be---good documentation is often longer than the program!

Revision History

  • 2021-02-04 Minor clarification in description of UnboundedRecurringTask.retire(...).
  • 2021-01-29 Corrected a few typos (including one instance of calling the base class Event instead of Task)
  • 2021-01-25 Initial publication