We are going to change a lot in our to do app, so it’s a good idea to re-organize it now. Additionally, making our app adhere to standards helps others to effectively contribute to it.

Right now we have two files in our project:

ls
# pyproject.toml todo.py

First, let’s make a folder to hold all of our domain-specific application code.

mkdir todo

We will make a task.py file to hold our contents for now, but we will be changing this later.

touch todo/task.py

Let’s break out our app code from the main runner. Here’s what our code looks like after we’re done:

todo.py

import sys
import datetime
from todo.task import help


if __name__ == '__main__':
    try:
        d = {}
        don = {}
        args = sys.argv
        if(args[1] == 'del'):
            args[1] = 'deL'
        if(args[1] == 'add' and len(args[2:]) == 0):
            sys.stdout.buffer.write(
                "Error: Missing todo string. Nothing added!".encode('utf8'))

        elif(args[1] == 'done' and len(args[2:]) == 0):
            sys.stdout.buffer.write(
                "Error: Missing NUMBER for marking todo as done.".encode('utf8'))

        elif(args[1] == 'deL' and len(args[2:]) == 0):
            sys.stdout.buffer.write(
                "Error: Missing NUMBER for deleting todo.".encode('utf8'))
        else:
            globals()[args[1]](*args[2:])

    except Exception as e:

        s = """Usage :-
$ ./todo add "todo item" # Add a new todo
$ ./todo ls			 # Show remaining todos
$ ./todo del NUMBER	 # Delete a todo
$ ./todo done NUMBER	 # Complete a todo
$ ./todo help			 # Show usage
$ ./todo report		 # Statistics"""
        sys.stdout.buffer.write(s.encode('utf8'))

In addition to separating the functions into a new file, we have also added an import in the main file to reference those functions.

todo/task.py

import sys
import datetime


def help():
    sa = """Usage :-
$ ./todo add "todo item" # Add a new todo
$ ./todo ls			 # Show remaining todos
$ ./todo del NUMBER	 # Delete a todo
$ ./todo done NUMBER	 # Complete a todo
$ ./todo help			 # Show usage
$ ./todo report		 # Statistics"""
    sys.stdout.buffer.write(sa.encode('utf8'))


def add(s):
    f = open('todo.txt', 'a')
    f.write(s)
    f.write("\n")
    f.close()
    s = '"'+s+'"'
    print(f"Added todo: {s}")


def ls():
    try:

        nec()
        l = len(d)
        k = l

        for i in d:
            sys.stdout.buffer.write(f"[{l}] {d[l]}".encode('utf8'))
            sys.stdout.buffer.write("\n".encode('utf8'))
            l = l-1

    except Exception as e:
        raise e


def deL(no):
    try:
        no = int(no)
        nec()
        with open("todo.txt", "r+") as f:
            lines = f.readlines()
            f.seek(0)
            for i in lines:
                if i.strip('\n') != d[no]:
                    f.write(i)
            f.truncate()
        print(f"Deleted todo #{no}")

    except Exception as e:
        print(f"Error: todo #{no} does not exist. Nothing deleted.")


def done(no):
    try:

        nec()
        no = int(no)
        f = open('done.txt', 'a')
        st = 'x '+str(datetime.datetime.today()).split()[0]+' '+d[no]
        f.write(st)
        f.write("\n")
        f.close()
        print(f"Marked todo #{no} as done.")
        
        with open("todo.txt", "r+") as f:
            lines = f.readlines()
            f.seek(0)
            for i in lines:
                if i.strip('\n') != d[no]:
                    f.write(i)
            f.truncate()
    except:
        print(f"Error: todo #{no} does not exist.")


def report():
    nec()
    try:

        nf = open('done.txt', 'r')
        c = 1
        for line in nf:
            line = line.strip('\n')
            don.update({c: line})
            c = c+1
        print(
            f'{str(datetime.datetime.today()).split()[0]} Pending : {len(d)} Completed : {len(don)}')
    except:
        print(
            f'{str(datetime.datetime.today()).split()[0]} Pending : {len(d)} Completed : {len(don)}')


def nec():
    try:
        f = open('todo.txt', 'r')
        c = 1
        for line in f:
            line = line.strip('\n')
            d.update({c: line})
            c = c+1
    except:
        sys.stdout.buffer.write("There are no pending todos!".encode('utf8'))

Now, let’s run this! First, we will enter a shell.

poetry shell

Now we can interact with python as we are used to.

python todo.py

When we initially run this, we run into this error:

ModuleNotFoundError: No module named 'todo.task'; 'todo' is not a package

The reason for this error is because Python does not see our folder as a package. To make that happen, we just have to add an __init__.py file to that folder.

touch todo/__init__.py

Now we run again:

python todo.py

# Usage :-
# $ ./todo add "todo item" # Add a new todo
# $ ./todo ls			 # Show remaining todos
# $ ./todo del NUMBER	 # Delete a todo
# $ ./todo done NUMBER	 # Complete a todo
# $ ./todo help			 # Show usage
# $ ./todo report		 # Statistics

Success!

This is great, but our code is still a little messy (and it only runs help!) so let’s clean it up before we start to change things. Let’s stick with only help for now as we get this working, then we’ll add in the other functions. With that in place, we will then move into talking about object-oriented programming, design, and testing.

todo.py

import sys
from todo.task import help


if __name__ == '__main__':
    help()

Run again to verify:

python todo.py

# Usage :-
# $ ./todo add "todo item" # Add a new todo
# $ ./todo ls			 # Show remaining todos
# $ ./todo del NUMBER	 # Delete a todo
# $ ./todo done NUMBER	 # Complete a todo
# $ ./todo help			 # Show usage
# $ ./todo report		 # Statistics

And it works!

Now we can add back our other cases, but instead of sending them all to be processed by globals(), we will invoke them directly. Why? Well, because global values tend to be a problem in programs. Also, since we are breaking this up, it would necessarily change that implementation. Why invoke them directly? In this case, it gives us much more control over flow. So let’s add that in:

todo.py

import sys
from todo.task import help, add, done, deL


if __name__ == '__main__':
    try:
        args = sys.argv
        if(args[1] == 'add'):
            if(len(args[2:]) == 0):
                sys.stdout.buffer.write(
                    "Error: Missing todo string. Nothing added!".encode('utf8'))
            else:
                add(args[2:])

        elif(args[1] == 'done' and len(args[2:]) == 0):
            if(len(args[2:]) == 0):
                sys.stdout.buffer.write(
                    "Error: Missing NUMBER for marking todo as done.".encode('utf8'))
            else:
                done(args[2:])

        elif(args[1] == 'del' and len(args[2:]) == 0):
            if(len(args[2:]) == 0):
                sys.stdout.buffer.write(
                    "Error: Missing NUMBER for deleting todo.".encode('utf8'))
            else:
                deL(args[2:])

    except Exception:
        help()

Still a lot more work to do here, but this is enough to get us started.

Run again to verify:

python todo.py

# Usage :-
# $ ./todo add "todo item" # Add a new todo
# $ ./todo ls			 # Show remaining todos
# $ ./todo del NUMBER	 # Delete a todo
# $ ./todo done NUMBER	 # Complete a todo
# $ ./todo help			 # Show usage
# $ ./todo report		 # Statistics

So far, this is looking great! Next up, we are going to wrap our task in a class.

Key Takeaways

  • If Python cannot find your module, you are probably missing an __init__.py file
  • Avoid global state
  • Separate domain logic from interface logic