From df0d8c5113821d0ae9d3a82c594582e825154491 Mon Sep 17 00:00:00 2001 From: Yehuda Deutsch Date: Mon, 11 Nov 2024 18:56:10 -0500 Subject: [PATCH] Enhance ToDo --- README.md | 2 +- todo.py | 241 +++++++++++++++++++++++++++--------------------------- 2 files changed, 120 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 86dabed..054b20b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Ajal Todo CLI -A todo utility +A todo utility - enhanced diff --git a/todo.py b/todo.py index 03b802a..b529938 100755 --- a/todo.py +++ b/todo.py @@ -1,152 +1,149 @@ #!/usr/bin/env python3 -import os +import argparse import re -import sys +from dataclasses import dataclass, field +from pathlib import Path -base_dir = os.path.abspath(os.path.dirname(__file__)) -todo_file = os.path.join(base_dir, "todo.md") +base_dir = Path(__file__).absolute().parent -def add_task(task: str, parent=None): - todo = parse_todo() - if parent and parent not in todo: - print(f"Could not find parent task '{parent}' in root task list") - exit(1) - - print(f"Adding task '{task}' to '{parent if parent else 'root'}'") - if parent: - todo[parent]["sub_tasks"][task] = {"completed": False} - else: - todo[task] = {"completed": False, "sub_tasks": {}} - write_todo(todo) +@dataclass +class LeafTask: + completed: bool = False -def list_tasks(parent=None): - todo = parse_todo() - if parent and parent not in todo: - print(f"Could not find parent task '{parent}' in root task list") - exit(1) - - print(f"Listing tasks in '{parent if parent else 'root'}'") - if parent: - todo = {parent: todo[parent]} - print(render_todo(todo)) +@dataclass +class RootTask(LeafTask): + tasks: dict[str, LeafTask] = field(default_factory=dict) -def complete_task(task, parent=None): - todo = parse_todo() - if parent: - if parent not in todo: - print(f"Could not find parent task '{parent}' in root task list") - exit(1) - if task not in todo[parent]["sub_tasks"]: - print(f"Could not find task '{task}' in parent task '{parent}'") - exit(1) - if task not in todo: - print(f"Could not find task '{task}' in root task list") - exit(1) - - print(f"Marking task '{task}' in '{parent if parent else 'root'}' as completed") - if parent: - task = todo[parent]["sub_tasks"][task] - else: - task = todo[task] - task["completed"] = True - write_todo(todo) +@dataclass +class Command: + func: callable + read_only: bool = False + help: str | None = None -def remove_task(task, parent=None): - todo = parse_todo() - if parent: - if parent not in todo: - print(f"Could not find parent task '{parent}' in root task list") - exit(1) - if task not in todo[parent]["sub_tasks"]: - print(f"Could not find task '{task}' in parent task '{parent}'") - exit(1) - if task not in todo: - print(f"Could not find task '{task}' in root task list") - exit(1) +@dataclass +class CommandList: + commands: dict[str, Command] = field(default_factory=dict) - print(f"Removing task '{task}' from '{parent if parent else 'root'}'") - if parent: - del todo[parent]["sub_tasks"][task] - else: - del todo[task] - write_todo(todo) + def mark(self, read_only: bool = False): + def wrap(func): + self.commands[func.__name__] = Command(func=func, read_only=read_only, help=func.__doc__) + return func + return wrap + + @property + def read_list(self): + return [(command, cmd) for command, cmd in self.commands.items() if cmd.read_only] + + @property + def write_list(self): + return [(command, cmd) for command, cmd in self.commands.items() if not cmd.read_only] -def print_help(): - print("""Usage: todo.py [add|complete|remove] TASK [PARENT] - todo.py list [PARENT] +class ToDo: + args: argparse.Namespace + tasks: dict[str, RootTask] = {} + commands: CommandList = CommandList() - Manages todo.md file as a ToDo file""") + def __init__(self): + self.args = self.parse_args() + self.tasks_root = self.tasks + self.task_class = RootTask + def parse_args(self): + parser = argparse.ArgumentParser(description="Manages todo.md file as a ToDo file") + parser.add_argument("--todo-file", "-f", help="Path to the todo file.", default=base_dir / "todo.md") -def parse_todo() -> dict[str, dict]: - todo = {} - with open(todo_file, "r") as f: - lines = f.readlines() + sub_parsers = parser.add_subparsers(dest="command", help="Command to run", required=True) + for command, cmd in self.commands.write_list: + sub_parser = sub_parsers.add_parser(command, help=cmd.help.lstrip().splitlines()[0], description=cmd.help) + sub_parser.add_argument("--root", "-r", help="Root task") + sub_parser.add_argument("task", help="Task name") + for command, cmd in self.commands.read_list: + sub_parser = sub_parsers.add_parser(command, help=cmd.help.lstrip().splitlines()[0], description=cmd.help) + sub_parser.add_argument("--root", "-r", help="Root task") + + return parser.parse_args() + + @property + def command(self) -> Command: + return self.commands.commands[self.args.command] + + def run(self) -> None: + self.parse() + self.augment_root() + self.command.func(self) + if not self.command.read_only: + self.write() + self.list() + + def parse(self): root_task = None - for line in lines: + for line in self.args.todo_file.read_text().splitlines(): if matches := re.match(r"^-\s+(\[(?P[x ])]\s+)?(?P.+)$", line.rstrip()): - root_task_name = matches.group("task") - root_task = { - "completed": matches.group("status") == "x", - "sub_tasks": {}, - } - todo[root_task_name] = root_task + task_name = matches.group("task") + root_task = RootTask(completed=matches.group("status") == "x") + self.tasks[task_name] = root_task elif matches := re.match(r"^\s+-\s+(\[(?P[x ])]\s+)?(?P.+)$", line.rstrip()): - leaf_task = matches.group("task") - root_task["sub_tasks"][leaf_task] = { - "completed": matches.group("status") == "x", - } - return todo + root_task.tasks[matches.group("task")] = LeafTask(completed=matches.group("status") == "x") + def render(self, tasks: dict | None = None): + lines = ["# A ToDo list", ""] + for root_task_name, root_task in (tasks or self.tasks).items(): # type: str, RootTask + lines.append(f"- [{'x' if root_task.completed else ' '}] {root_task_name}") + for leaf_task_name, task in root_task.tasks.items(): + lines.append(f" - [{'x' if task.completed else ' '}] {leaf_task_name}") + lines += [""] + return "\n".join(lines) -def render_todo(todo: dict[str, dict]) -> str: - lines = ["# A ToDo list", ""] - for task, task_data in todo.items(): # type: str, dict - lines.append(f"- [{'x' if task_data["completed"] else ' '}] {task}") - for leaf_task, leaf_task_data in task_data["sub_tasks"].items(): - lines.append(f" - [{'x' if leaf_task_data["completed"] else ' '}] {leaf_task}") - lines += [""] - return "\n".join(lines) + def write(self, todo: dict | None = None): + self.args.todo_file.write_text(self.render(tasks=todo)) + def augment_root(self): + if self.args.root: + if self.args.root not in self.tasks: + print(f"Root task '{self.args.root}' does not exist") + exit(1) + else: + self.tasks_root = self.tasks[self.args.root].tasks + self.task_class = LeafTask -def write_todo(todo: dict[str, dict]): - with open(todo_file, "w") as f: - f.write(render_todo(todo)) + @commands.mark(read_only=True) + def list(self): + """ + List all tasks in todo.md + Returns all tasks if no root task is passed, otherwise only the root task and subtasks + """ + print(f"Listing tasks in '{self.args.root if self.args.root else 'root'}'") + print(self.render(tasks={self.args.root: self.tasks[self.args.root]} if self.args.root else None)) -def main(): - if len(sys.argv) < 2: - print_help() - exit(1) + @commands.mark() + def add(self): + """ + Add a task to todo.md + """ + print(f"Adding task '{self.args.task}' to '{self.args.root if self.args.root else 'root'}'") + self.tasks_root[self.args.task] = self.task_class() - match sys.argv[1]: - case "help" | "--help" | "h" | "-h": - print_help() - case "list": - match sys.argv[2:]: - case [parent, *_]: - list_tasks(parent) - case _: - list_tasks() - case "add" | "complete" | "remove" as task_name: - task_func = f"{task_name}_task" - match sys.argv[2:]: - case [task]: - globals()[task_func](task) - case [parent, task, *_]: - globals()[task_func](task, parent) - case _: - print_help() - exit(1) - case _: - print_help() - exit(1) + @commands.mark() + def complete(self): + """ + Mark a task in todo.md as completed + """ + print(f"Marking task '{self.args.task}' in '{self.args.root if self.args.root else 'root'}' as completed") + self.tasks_root[self.args.task].completed = True + + @commands.mark() + def remove(self): + """ + Remove a task from todo.md + """ + del self.tasks_root[self.args.task] if __name__ == "__main__": - main() + ToDo().run()