DEV Community

Ian Johnson
Ian Johnson

Posted on • Originally published at tacoda.github.io on

Re-Organizing Our To Do App

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
Enter fullscreen mode Exit fullscreen mode

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

mkdir todo
Enter fullscreen mode Exit fullscreen mode

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

touch todo/task.py
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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

poetry shell
Enter fullscreen mode Exit fullscreen mode

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

python todo.py
Enter fullscreen mode Exit fullscreen mode

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

ModuleNotFoundError: No module named 'todo.task'; 'todo' is not a package
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)