This project must be submitted to Gradescope by 6:00pm CST on Friday, February 5, 2021.
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.
Contact the instructor or TA by email, or visit office hours.
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:
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:
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
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.
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.
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 taskactive
: 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.
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.
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
.
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
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.
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.
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.
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
.
# 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
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.
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
.
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
This section contains rules your code must follow, in addition to it needing to do everything documented in the previous section.
Like everything you submit for credit, your code must follow the rules in the course coding standards document.
You are building a class hierarchy. You should make proper use of inheritance by:
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.
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!
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!
UnboundedRecurringTask.retire(...)
.