Until now we've been reading objects from our (primitive, flat-text-file) database and writing to that database and directly treating those objects as our tasks. That is:
get_all_tasks()
function gives us strings;This is a state of sin. (Though it is a very common state of sin: many large, mature codebases, written by hordes of well-respected and thoughtful programmers, in effect require their users to do the moral equivalent of this.)
You will do better:
You can think about this division functionally: The first layer does things like presenting tasks to users and prompting task objects to do computations. (A task might, e.g., calculate how long until it's due.) The second layer does things like saving and restoring tasks, handling the inevitable fiddly problems that need careful management at these stages.
You can also think about the division morally and/or metaphysically. The first layer is a Platonic realm wherein you create and manage things as they actually are. The second layer manages those objects' connection to a fundamentally dirty and clunky realm of disks and databases and CSV files.
Either way, it's essential to distinguish between these layers and do it early, right here in step 10. In practice, the trick is to do it flexibly enough to make it easy to change later--however carefully we plan, we'll be changing things often--but faithfully enough that our work isn't utterly pointless. (I've made many mistakes in both directions.)
objects
directory in veery/
.task.py
there (so it lives at veery/objects/task.py
).Task
class there: from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class Task:
description: str
due: Optional[datetime] = None
There's a lot to note here:
str
and list
all along. In all modern programming languages, you can make your own classes also. Python is great at this. It's also great at documenting it.The basic idea of this class is that: (i) there's such a thing as a Task
; (ii) a Task
has a description (represented by a string) and, optionally, a time when it's due (represented by a datetime).
If you don't supply the due field, it will automatically be set to the None
object (that's what the = None
part of the last line is doing).
We'll be writing at least a few lines of code to make sure that our Task
class basically works, so (in the spirit of the last post) why not preserve them as tests?
veery/tests/test_object_task.py
; from objects.task import Task
def test_basic_task_creation():
assert Task("do ten push-ups")
def test_task_can_be_created_with_empty_description():
assert Task(description="")
def test_task_is_initialized_without_due_date_if_not_provided():
task_without_due_datetime = Task("foo")
assert task_without_due_datetime.due is None
Some notes about these:
pytest
command.Task
is what we think it is.Task
currently handles a certain edge case a certain way. Maybe we'll want to preserve it, maybe not--but now it's documented and tested, and the test is easy to change later if need be. This sort of thing is an important practical aspect of testing.Task
without a due date/time) is there.Here's the current commit from the veery repository.
Next post: Python task manager from scratch, part 11: First bugfix