Skip to content

Reference

This part of the project documentation focuses on an information-oriented approach. Use it as a reference for the technical implementation of the todo project code.

To-Do entry point script.

main()

Run the todo CLI app.

Source code in src/todo/__main__.py
6
7
8
def main() -> None:
    """Run the todo CLI app."""
    cli.app(prog_name=__app_name__)

CLI for the Python To-Do app.

add(description=typer.Argument(...), priority=typer.Option(2, '--priority', '-p', min=1, max=3))

Add a new todo.

Source code in src/todo/cli.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@app.command()
def add(
    description: list[str] = typer.Argument(...),  # noqa: B008, function calls in argument
    priority: int = typer.Option(2, "--priority", "-p", min=1, max=3),
) -> None:
    """Add a new todo."""
    todoer = get_todoer()
    todo, error = todoer.add(description, priority)

    if error:
        typer.secho(f'Adding to-do failed with "{ERRORS[error]}"', fg=typer.colors.RED)
        raise typer.Exit(1)
    typer.secho(
        f"""to-do: "{todo['Description']}" was added """ f"""with priority: {priority}""",
        fg=typer.colors.GREEN,
    )

get_todoer()

Get the todo controller.

Source code in src/todo/cli.py
40
41
42
43
44
45
46
47
48
49
50
51
52
def get_todoer() -> todo.Todoer:
    """Get the  todo controller."""
    if not config.CONFIG_FILE_PATH.exists():
        typer.secho("Config file not found, please run `todo init`.", fg=typer.colors.RED)
        raise typer.Exit(1)

    db_path = database.get_database_path(config.CONFIG_FILE_PATH)

    if not db_path.exists():
        typer.secho("Database file not found, please run `todo init`.", fg=typer.colors.RED)
        raise typer.Exit(1)

    return todo.Todoer(db_path)

init(db_path=typer.Option(str(database.DEFAULT_DB_FILE_PATH), '--db-path', '-db', prompt='to-do databae location?'))

Initialise the to-do database.

Source code in src/todo/cli.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@app.command()
def init(
    db_path: str = typer.Option(
        str(database.DEFAULT_DB_FILE_PATH),
        "--db-path",
        "-db",
        prompt="to-do databae location?",
    ),
) -> None:
    """Initialise the to-do database."""
    app_init_error = config.init_app(db_path)
    if app_init_error:
        typer.secho(
            f'Creating config file failed with "{ERRORS[app_init_error]}"',
            fg=typer.colors.RED,
        )
        raise typer.Exit(1)

    db_init_error = database.init_database(Path(db_path))
    if db_init_error:
        typer.secho(f'Creating the to-do database failed with "{ERRORS[db_init_error]}"')
        raise typer.Exit(1)

    typer.secho(f"The to-do databse is {db_path}", fg=typer.colors.GREEN)

list_all()

List all to-do's.

Source code in src/todo/cli.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@app.command("list")
def list_all() -> None:
    """List all to-do's."""
    todoer = get_todoer()
    todo_list = todoer.get_todo_list()
    if len(todo_list) == 0:
        typer.secho("No to-do's found.", fg=typer.colors.RED)
        raise typer.Exit

    typer.secho("\nto-do list:\n", fg=typer.colors.BLUE)
    columns = (
        "ID  ",
        "| Priority  ",
        "| Done  ",
        "| Description  ",
    )
    headers = "".join(columns)
    typer.secho(headers, fg=typer.colors.BLUE, bold=True)
    typer.secho("-" * len(headers), fg=typer.colors.BLUE)

    # Loop over todo list and assign ID to each one
    for idx, item in enumerate(todo_list, start=1):
        desc, priority, done = item.values()
        typer.secho(
            # print the id, priority, done and description with proper padding
            f"{idx}{(len(columns[0]) - len(str(idx))) * ' '}"
            f"| ({priority}){(len(columns[1]) - len(str(priority)) - 4) * ' '}"
            f"| {done}{(len(columns[2]) - len(str(done)) - 2) * ' '}"
            f"| {desc}",
            fg=typer.colors.GREEN if done else typer.colors.YELLOW,
        )
    typer.secho("-" * len(headers) + "\n", fg=typer.colors.BLUE)

main(version=typer.Option(None, '--version', '-v', help="Show the application's version and exit.", callback=_version_callback, is_eager=True))

CLI arguments for the main todo command.

Source code in src/todo/cli.py
183
184
185
186
187
188
189
190
191
192
193
194
195
@app.callback()
def main(
    version: Optional[bool] = typer.Option(  # noqa: ARG001, UP007
        None,
        "--version",
        "-v",
        help="Show the application's version and exit.",
        callback=_version_callback,
        is_eager=True,
    ),
) -> None:
    """CLI arguments for the main todo command."""
    return

remove(todo_id=typer.Argument(...), *, force=typer.Option(False, '--force', '-f', help='Force deletion without confirmation.'))

Remove a todo using its ID.

Source code in src/todo/cli.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
@app.command("remove")
def remove(
    todo_id: int = typer.Argument(...),
    *,
    force: bool = typer.Option(False, "--force", "-f", help="Force deletion without confirmation."),  # noqa: FBT003
) -> None:
    """Remove a todo using its ID."""
    todoer = get_todoer()

    def _remove() -> None:
        todo, error = todoer.remove(todo_id)

        if error:
            typer.secho(
                f'Removing to-do {todo_id} failed with "{ERRORS[error]}"', fg=typer.colors.RED
            )
            raise typer.Exit(1)
        typer.secho(
            f"to-do: {todo['Description']} removed",
            fg=typer.colors.GREEN,
        )

    delete = force if force else typer.confirm(f"Delete todo #{todo_id}?")

    if delete:
        _remove()
    else:
        typer.secho("Operation canceled.")

remove_all(*, force=typer.Option(..., prompt='Delete all todos?', help='Force deletion without confirmation.'))

Remove all todo's.

Source code in src/todo/cli.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@app.command("clear")
def remove_all(
    *,
    force: bool = typer.Option(
        ...,
        prompt="Delete all todos?",
        help="Force deletion without confirmation.",
    ),
) -> None:
    """Remove all todo's."""
    todoer = get_todoer()

    if force:
        error = todoer.remove_all().error
        if error:
            typer.secho(f'Removing todos failed with "{ERRORS[error]}"', fg=typer.colors.RED)
            raise typer.Exit(1)
        typer.secho("All todos were removed", fg=typer.colors.GREEN)

    else:
        typer.secho("Operation canceled.")

set_done(todo_id=typer.Argument(...))

Mark a todo item as completed.

Source code in src/todo/cli.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@app.command("complete")
def set_done(todo_id: int = typer.Argument(...)) -> None:
    """Mark a todo item as completed."""
    todoer = get_todoer()
    todo, error = todoer.set_done(todo_id)

    if error:
        typer.secho(
            f'Completing to-do {todo_id} failed with "{ERRORS[error]}"', fg=typer.colors.RED
        )
        raise typer.Exit(1)
    typer.secho(
        f"to-do: {todo['Description']} marked as completed",
        fg=typer.colors.GREEN,
    )

To-do app config functionality.

init_app(db_path)

Initialise the application.

Source code in src/todo/config.py
14
15
16
17
18
19
20
21
22
23
24
25
26
def init_app(db_path: str) -> int:
    """Initialise the application."""
    ## Initialise config file
    config_code = _init_config_file()
    if config_code != SUCCESS:
        return config_code

    ## Create the database
    database_code = _create_database(db_path)
    if database_code != SUCCESS:
        return database_code

    return SUCCESS

Functionality for the to-do app database.

DBResponse

Bases: NamedTuple

The database response containing the list of todo's and an error code.

:param todo_list: the list of todo's :param error: a numeric error code, to be mapped to todo.ERRORS

Source code in src/todo/database.py
30
31
32
33
34
35
36
37
38
39
class DBResponse(NamedTuple):
    """
    The database response containing the list of todo's and an error code.

    :param todo_list: the list of todo's
    :param error: a numeric error code, to be mapped to `todo.ERRORS`
    """

    todo_list: list[dict[str, Any]]
    error: int

DatabaseHandler

Handle interactions with the to-do database.

Source code in src/todo/database.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class DatabaseHandler:
    """Handle interactions with the to-do database."""

    def __init__(self: "DatabaseHandler", db_path: Path) -> None:
        """
        Initialise the database handler for a given database.

        :param db_path: the file path to the todo database.
        :type db_path: pathlib.Path

        """
        self._db_path = db_path

    def read_todos(self: "DatabaseHandler") -> DBResponse:
        """Read the JSON todo database and return a `DBResponse` instance."""
        try:
            with self._db_path.open("r") as db:
                try:
                    return DBResponse(json.load(db), SUCCESS)
                except json.JSONDecodeError:  # Catch wrong JSON format
                    return DBResponse([], JSON_ERROR)
        except OSError:  # Catch file IO errors
            return DBResponse([], DB_READ_ERROR)

    def write_todos(self: "DatabaseHandler", todo_list: list[dict[str, Any]]) -> DBResponse:
        """Write to the JSON todo database and return a `DBResponse` isntance."""
        try:
            with self._db_path.open("w") as db:
                json.dump(todo_list, db, indent=4)
                return DBResponse(todo_list, SUCCESS)
        except OSError:  # Catch file IO errors
            return DBResponse(todo_list, DB_WRITE_ERROR)

__init__(db_path)

Initialise the database handler for a given database.

:param db_path: the file path to the todo database. :type db_path: pathlib.Path

Source code in src/todo/database.py
45
46
47
48
49
50
51
52
53
def __init__(self: "DatabaseHandler", db_path: Path) -> None:
    """
    Initialise the database handler for a given database.

    :param db_path: the file path to the todo database.
    :type db_path: pathlib.Path

    """
    self._db_path = db_path

read_todos()

Read the JSON todo database and return a DBResponse instance.

Source code in src/todo/database.py
55
56
57
58
59
60
61
62
63
64
def read_todos(self: "DatabaseHandler") -> DBResponse:
    """Read the JSON todo database and return a `DBResponse` instance."""
    try:
        with self._db_path.open("r") as db:
            try:
                return DBResponse(json.load(db), SUCCESS)
            except json.JSONDecodeError:  # Catch wrong JSON format
                return DBResponse([], JSON_ERROR)
    except OSError:  # Catch file IO errors
        return DBResponse([], DB_READ_ERROR)

write_todos(todo_list)

Write to the JSON todo database and return a DBResponse isntance.

Source code in src/todo/database.py
66
67
68
69
70
71
72
73
def write_todos(self: "DatabaseHandler", todo_list: list[dict[str, Any]]) -> DBResponse:
    """Write to the JSON todo database and return a `DBResponse` isntance."""
    try:
        with self._db_path.open("w") as db:
            json.dump(todo_list, db, indent=4)
            return DBResponse(todo_list, SUCCESS)
    except OSError:  # Catch file IO errors
        return DBResponse(todo_list, DB_WRITE_ERROR)

get_database_path(config_file)

Return the current path to the to-do database.

Source code in src/todo/database.py
13
14
15
16
17
def get_database_path(config_file: Path) -> Path:
    """Return the current path to the to-do database."""
    config_parser = ConfigParser()
    config_parser.read(config_file)
    return Path(config_parser["General"]["database"])

init_database(db_path)

Create the to-do database.

Source code in src/todo/database.py
20
21
22
23
24
25
26
27
def init_database(db_path: Path) -> int:
    """Create the to-do database."""
    try:
        db_path.write_text("[]")  # Empty to-do list
    except OSError:
        return DB_WRITE_ERROR
    else:
        return SUCCESS

The todo model-controller.

CurrentTodo

Bases: NamedTuple

The data model for a given todo.

Contains the todo's information in a dictionary and an error code, signaling the status of any operation performed on the todo.

Source code in src/todo/todo.py
10
11
12
13
14
15
16
17
18
19
class CurrentTodo(NamedTuple):
    """
    The data model for a given todo.

    Contains the todo's information in a dictionary and an error code, signaling the status of any
    operation performed on the todo.
    """

    todo: dict[str, Any]
    error: int

Todoer

The main Controller class, handling database connections.

Source code in src/todo/todo.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class Todoer:
    """The main Controller class, handling database connections."""

    def __init__(self: "Todoer", db_path: Path) -> None:
        """Initialise the Todoer class, composing it with an instance of DatabaseHandler."""
        self._db_handler = DatabaseHandler(db_path)  # <-- Composition!

    def add(self: "Todoer", description: list[str], priority: int = 2) -> CurrentTodo:
        """Add a new todo to the database."""
        description_text = " ".join(description)
        if not description_text.endswith("."):
            description_text += "."

        todo = {
            "Description": description_text,
            "Priority": priority,
            "Done": False,
        }

        read = self._db_handler.read_todos()
        if read.error == DB_READ_ERROR:
            return CurrentTodo(todo, read.error)

        read.todo_list.append(todo)
        write = self._db_handler.write_todos(read.todo_list)
        return CurrentTodo(todo, write.error)

    def get_todo_list(self: "Todoer") -> list[dict[str, Any]]:
        """Get the current list of todo's."""
        read = self._db_handler.read_todos()
        return read.todo_list

    def set_done(self: "Todoer", todo_id: int) -> CurrentTodo:
        """Set a todo as done."""
        read = self._db_handler.read_todos()

        if read.error == DB_READ_ERROR:
            return CurrentTodo({}, read.error)

        try:
            todo = read.todo_list[todo_id - 1]
        except IndexError:
            return CurrentTodo({}, ID_ERROR)

        todo["Done"] = True
        write = self._db_handler.write_todos(read.todo_list)
        return CurrentTodo(todo, write.error)

    def remove(self: "Todoer", todo_id: int) -> CurrentTodo:
        """Remove a todo from the database."""
        read = self._db_handler.read_todos()

        if read.error == DB_READ_ERROR:
            return CurrentTodo({}, read.error)

        try:
            todo = read.todo_list.pop(todo_id - 1)
        except IndexError:
            return CurrentTodo({}, ID_ERROR)

        write = self._db_handler.write_todos(read.todo_list)
        return CurrentTodo(todo, write.error)

    def remove_all(self: "Todoer") -> CurrentTodo:
        """Remove all todo's from the database."""
        write = self._db_handler.write_todos([])
        return CurrentTodo({}, write.error)

__init__(db_path)

Initialise the Todoer class, composing it with an instance of DatabaseHandler.

Source code in src/todo/todo.py
25
26
27
def __init__(self: "Todoer", db_path: Path) -> None:
    """Initialise the Todoer class, composing it with an instance of DatabaseHandler."""
    self._db_handler = DatabaseHandler(db_path)  # <-- Composition!

add(description, priority=2)

Add a new todo to the database.

Source code in src/todo/todo.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def add(self: "Todoer", description: list[str], priority: int = 2) -> CurrentTodo:
    """Add a new todo to the database."""
    description_text = " ".join(description)
    if not description_text.endswith("."):
        description_text += "."

    todo = {
        "Description": description_text,
        "Priority": priority,
        "Done": False,
    }

    read = self._db_handler.read_todos()
    if read.error == DB_READ_ERROR:
        return CurrentTodo(todo, read.error)

    read.todo_list.append(todo)
    write = self._db_handler.write_todos(read.todo_list)
    return CurrentTodo(todo, write.error)

get_todo_list()

Get the current list of todo's.

Source code in src/todo/todo.py
49
50
51
52
def get_todo_list(self: "Todoer") -> list[dict[str, Any]]:
    """Get the current list of todo's."""
    read = self._db_handler.read_todos()
    return read.todo_list

remove(todo_id)

Remove a todo from the database.

Source code in src/todo/todo.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def remove(self: "Todoer", todo_id: int) -> CurrentTodo:
    """Remove a todo from the database."""
    read = self._db_handler.read_todos()

    if read.error == DB_READ_ERROR:
        return CurrentTodo({}, read.error)

    try:
        todo = read.todo_list.pop(todo_id - 1)
    except IndexError:
        return CurrentTodo({}, ID_ERROR)

    write = self._db_handler.write_todos(read.todo_list)
    return CurrentTodo(todo, write.error)

remove_all()

Remove all todo's from the database.

Source code in src/todo/todo.py
85
86
87
88
def remove_all(self: "Todoer") -> CurrentTodo:
    """Remove all todo's from the database."""
    write = self._db_handler.write_todos([])
    return CurrentTodo({}, write.error)

set_done(todo_id)

Set a todo as done.

Source code in src/todo/todo.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def set_done(self: "Todoer", todo_id: int) -> CurrentTodo:
    """Set a todo as done."""
    read = self._db_handler.read_todos()

    if read.error == DB_READ_ERROR:
        return CurrentTodo({}, read.error)

    try:
        todo = read.todo_list[todo_id - 1]
    except IndexError:
        return CurrentTodo({}, ID_ERROR)

    todo["Done"] = True
    write = self._db_handler.write_todos(read.todo_list)
    return CurrentTodo(todo, write.error)