Python task manager from scratch, part 41: Setting up event handling

We are confronting the problem of how to coordinate tasks that have some special relationship, beginning with the case where completing a task kicks off a new task due some fixed interval after the completion. Doing that has prompted us to figure out how to properly implement a domain object that is not the Task but is closely related to it. And doing that has prompted us to implement some event-handling.

The newest set of changes implements rudimentary event-handling infrastructure without changing any behavior or indeed actually handling the case where a KickoffCoordinator is being created. So, those coordinators exist and can be persisted; events involving them exist; and we have some basic infrastructure to process those events. But that infrastructure will throw a NotImplementedError if we give it any event that actually exercises the new coordination functionality.

This is a common pattern in development:

  1. Notice that something you want to implement requires some new infrastructure.
  2. Find the smallest piece of that infrastructure that can be independently implemented.
  3. Implement it without supporting the new feature.
  4. Test it.
  5. Make sure that the old behavior still works.
  6. Start implementing the new behavior.

If you can get in the habit of separating infrastructure from functionality (the distinction isn't always sharp), you'll often find ways to factor your work into small, independently testable changes.

A few Veery-specific notes for the changes between the last installment and this one:

  1. In the spirit of making small changes, there are a few commits covered in this installment. If you are following along by clicking through to the latest commit, you'll also want to look at its parent and grandparent.
  2. I removed the task_repository parameter from the decorated functions in main.py. There's no mechanism for actually passing in such a repository, and it's better, for now, simply to define that repository at the top of the file and let all the functions beneath use it. The bad part of this solution is that functions are reaching outside their local scopes when they call into repositories. I'll address this problem in a sturdier way when I get around to implementing several environments (for testing and production). Moreover, this problem always existed, because the default parameters to those functions were never being overridden, so this cleanup makes things no worse but more honest.
  3. There was a circular import for a while among the AddTask event, which has a coordinator field typed as a TaskCoordinator, while the TaskCoordinator had a proc_event() method that checked for the AddTask subclass. I resolved this by removing the typing on the coordinator field. This is an insufficient solution, which removes a dependency at the code level but does not remove it at the logical level. Stay tuned for a better solution.
  4. Eventually, we will not want to have all the event-routing and -handling in main.py. Because there is only one event so far, it doesn't clutter up that file too much. The practical danger to adding code in places where it won't ultimately live is not that deferring file creation and moving functions is a significant form of tech debt: that's easy. Rather, it's that you'll be tempted to mismanage dependencies, put some method on the wrong object, or similar. If you're keeping your changes small and thinking carefully, you'll do as well implementing first and splitting code into separate files later. That said, this is largely a matter of one's personal workflow and preference.

Here's the current commit from the veery/ repository.


Next post: Python task manager from scratch, part 42: Resolving a circular import


Home page