From d1f0cdc8b7c5c9bd11c0f852cf9ed1e3688b9229 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:13:27 +0530 Subject: [PATCH 01/26] feat: add utils package __init__.py --- utils/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 utils/__init__.py diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..11fa064 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +# Utils package for shared helpers and validation logic From f0d17b00e13123df363fd6d24573ed72a6032fcf Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:13:32 +0530 Subject: [PATCH 02/26] feat: add utils/paths.py with shared get_tasks_file() helper --- utils/paths.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 utils/paths.py diff --git a/utils/paths.py b/utils/paths.py new file mode 100644 index 0000000..1c7a635 --- /dev/null +++ b/utils/paths.py @@ -0,0 +1,10 @@ +"""Shared path helpers used across command modules.""" + +import os + + +def get_tasks_file() -> str: + """Return the path to the tasks file, preferring the environment variable + TASKS_FILE when set, otherwise defaulting to 'tasks.json' in the current + working directory.""" + return os.environ.get("TASKS_FILE", "tasks.json") From 3b07cea061ccafd6c497b153287b1039c50b7071 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:13:39 +0530 Subject: [PATCH 03/26] feat: add utils/validation.py with extracted validation functions --- utils/validation.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 utils/validation.py diff --git a/utils/validation.py b/utils/validation.py new file mode 100644 index 0000000..d4f3604 --- /dev/null +++ b/utils/validation.py @@ -0,0 +1,36 @@ +"""Shared validation helpers used across command modules.""" + +import os +from typing import List + + +def validate_description(description: str) -> None: + """Validate that a task description is non-empty. + + Raises: + ValueError: If *description* is empty or contains only whitespace. + """ + if not description or not description.strip(): + raise ValueError("Task description cannot be empty.") + + +def validate_task_file(filepath: str) -> None: + """Validate that the tasks file exists at *filepath*. + + Raises: + FileNotFoundError: If the file does not exist. + """ + if not os.path.exists(filepath): + raise FileNotFoundError(f"Tasks file not found: {filepath}") + + +def validate_task_id(task_id: int, tasks: List[dict]) -> None: + """Validate that *task_id* refers to an existing task in *tasks*. + + Raises: + ValueError: If *task_id* is not a valid index into *tasks*. + """ + if task_id < 1 or task_id > len(tasks): + raise ValueError( + f"Invalid task ID: {task_id}. Must be between 1 and {len(tasks)}." + ) From 426026b418b723ca6a17a16505b26ff9ecbfe416 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:13:45 +0530 Subject: [PATCH 04/26] refactor: update commands/add.py to use shared utils --- commands/add.py | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/commands/add.py b/commands/add.py index 1b1a943..a570bfd 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,37 +1,26 @@ -"""Add task command.""" +"""Command: add a new task.""" import json -from pathlib import Path +from utils.paths import get_tasks_file +from utils.validation import validate_description -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" +def add_task(description: str) -> None: + """Add a new task with the given *description* to the tasks file.""" + validate_description(description) -def validate_description(description): - """Validate task description.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if not description: - raise ValueError("Description cannot be empty") - if len(description) > 200: - raise ValueError("Description too long (max 200 chars)") - return description.strip() + filepath = get_tasks_file() + try: + with open(filepath, "r") as f: + tasks = json.load(f) + except FileNotFoundError: + tasks = [] -def add_task(description): - """Add a new task.""" - description = validate_description(description) + tasks.append({"description": description.strip(), "done": False}) - tasks_file = get_tasks_file() - tasks_file.parent.mkdir(parents=True, exist_ok=True) + with open(filepath, "w") as f: + json.dump(tasks, f, indent=2) - tasks = [] - if tasks_file.exists(): - tasks = json.loads(tasks_file.read_text()) - - task_id = len(tasks) + 1 - tasks.append({"id": task_id, "description": description, "done": False}) - - tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Added task {task_id}: {description}") + print(f"Task added: {description.strip()}") From 55ad691d252eaa8d64c6aec2b2487628fc542d26 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:13:50 +0530 Subject: [PATCH 05/26] refactor: update commands/list.py to use shared utils --- commands/list.py | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/commands/list.py b/commands/list.py index 714315d..e50576c 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,37 +1,23 @@ -"""List tasks command.""" +"""Command: list all tasks.""" import json -from pathlib import Path +from utils.paths import get_tasks_file +from utils.validation import validate_task_file -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" +def list_tasks() -> None: + """Print all tasks from the tasks file.""" + filepath = get_tasks_file() + validate_task_file(filepath) -def validate_task_file(): - """Validate tasks file exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - tasks_file = get_tasks_file() - if not tasks_file.exists(): - return [] - return tasks_file - - -def list_tasks(): - """List all tasks.""" - # NOTE: No --json flag support yet (feature bounty) - tasks_file = validate_task_file() - if not tasks_file: - print("No tasks yet!") - return - - tasks = json.loads(tasks_file.read_text()) + with open(filepath, "r") as f: + tasks = json.load(f) if not tasks: - print("No tasks yet!") + print("No tasks found.") return - for task in tasks: - status = "✓" if task["done"] else " " - print(f"[{status}] {task['id']}. {task['description']}") + for i, task in enumerate(tasks, start=1): + status = "✓" if task.get("done") else "✗" + print(f"{i}. [{status}] {task['description']}") From 35432217a819226ca563572b896d3456d9f3933b Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:13:54 +0530 Subject: [PATCH 06/26] refactor: update commands/done.py to use shared utils --- commands/done.py | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/commands/done.py b/commands/done.py index c9dfd42..b22f26f 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,37 +1,25 @@ -"""Mark task done command.""" +"""Command: mark a task as done.""" import json -from pathlib import Path +from utils.paths import get_tasks_file +from utils.validation import validate_task_file, validate_task_id -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" +def mark_done(task_id: int) -> None: + """Mark the task identified by *task_id* as done.""" + filepath = get_tasks_file() + validate_task_file(filepath) -def validate_task_id(tasks, task_id): - """Validate task ID exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if task_id < 1 or task_id > len(tasks): - raise ValueError(f"Invalid task ID: {task_id}") - return task_id + with open(filepath, "r") as f: + tasks = json.load(f) + validate_task_id(task_id, tasks) -def mark_done(task_id): - """Mark a task as complete.""" - tasks_file = get_tasks_file() - if not tasks_file.exists(): - print("No tasks found!") - return + task = tasks[task_id - 1] + task["done"] = True - tasks = json.loads(tasks_file.read_text()) - task_id = validate_task_id(tasks, task_id) + with open(filepath, "w") as f: + json.dump(tasks, f, indent=2) - for task in tasks: - if task["id"] == task_id: - task["done"] = True - tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Marked task {task_id} as done: {task['description']}") - return - - print(f"Task {task_id} not found") + print(f"Task {task_id} marked as done: {task['description']}") From 23249df7e277e60def2de527e59bf742c57b95cb Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:52:58 +0530 Subject: [PATCH 07/26] fix(utils/paths.py): restore home-dir default; add optional TASKS_FILE env-var override --- utils/paths.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/utils/paths.py b/utils/paths.py index 1c7a635..e8041a1 100644 --- a/utils/paths.py +++ b/utils/paths.py @@ -1,10 +1,12 @@ -"""Shared path helpers used across command modules.""" - import os +from pathlib import Path def get_tasks_file() -> str: - """Return the path to the tasks file, preferring the environment variable - TASKS_FILE when set, otherwise defaulting to 'tasks.json' in the current - working directory.""" - return os.environ.get("TASKS_FILE", "tasks.json") + """Return the path to the tasks JSON file. + + Defaults to ``tasks.json`` in the user's home directory, matching the + original behaviour of the individual command modules. The path can be + overridden by setting the ``TASKS_FILE`` environment variable. + """ + return os.environ.get("TASKS_FILE", str(Path.home() / "tasks.json")) From bbfac8d75de259a8ad7d9fb18683ae0062c0d37d Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:53:04 +0530 Subject: [PATCH 08/26] fix(utils/validation.py): return stripped string from validate_description; restore max-length check --- utils/validation.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/utils/validation.py b/utils/validation.py index d4f3604..3d1c5ee 100644 --- a/utils/validation.py +++ b/utils/validation.py @@ -1,36 +1,41 @@ -"""Shared validation helpers used across command modules.""" - +import json import os -from typing import List -def validate_description(description: str) -> None: - """Validate that a task description is non-empty. +def validate_description(description: str) -> str: + """Validate and normalize a task description. + + The description must be non-empty (after stripping whitespace) and no longer + than 200 characters. + + Returns the stripped description string. Raises: - ValueError: If *description* is empty or contains only whitespace. + ValueError: if the description is empty or exceeds 200 characters. """ - if not description or not description.strip(): - raise ValueError("Task description cannot be empty.") + stripped = description.strip() + if not stripped: + raise ValueError("Description cannot be empty.") + if len(stripped) > 200: + raise ValueError("Description cannot exceed 200 characters.") + return stripped def validate_task_file(filepath: str) -> None: - """Validate that the tasks file exists at *filepath*. + """Check that the tasks file exists. Raises: - FileNotFoundError: If the file does not exist. + FileNotFoundError: if *filepath* does not exist. """ if not os.path.exists(filepath): raise FileNotFoundError(f"Tasks file not found: {filepath}") -def validate_task_id(task_id: int, tasks: List[dict]) -> None: - """Validate that *task_id* refers to an existing task in *tasks*. +def validate_task_id(task_id: int) -> None: + """Validate that a task ID is a positive integer. Raises: - ValueError: If *task_id* is not a valid index into *tasks*. + ValueError: if *task_id* is not a positive integer. """ - if task_id < 1 or task_id > len(tasks): - raise ValueError( - f"Invalid task ID: {task_id}. Must be between 1 and {len(tasks)}." - ) + if not isinstance(task_id, int) or task_id <= 0: + raise ValueError(f"Task ID must be a positive integer, got: {task_id!r}") From 1b971fe8299bc949aa6d1aa76f99576d6ae5caee Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:53:10 +0530 Subject: [PATCH 09/26] fix(commands/list.py): restore graceful missing-file handling and original output format (stored id, done/undone symbols) --- commands/list.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/commands/list.py b/commands/list.py index e50576c..bd7b526 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,23 +1,28 @@ -"""Command: list all tasks.""" - import json from utils.paths import get_tasks_file -from utils.validation import validate_task_file def list_tasks() -> None: - """Print all tasks from the tasks file.""" + """Print all tasks to stdout. + + Prints a friendly message and returns (without raising) when the tasks + file does not exist, preserving the previous user-visible behaviour. + """ filepath = get_tasks_file() - validate_task_file(filepath) - with open(filepath, "r") as f: - tasks = json.load(f) + # Preserve previous behaviour: missing file → friendly message, no exception. + try: + with open(filepath, "r") as f: + tasks = json.load(f) + except FileNotFoundError: + print("No tasks found.") + return if not tasks: print("No tasks found.") return - for i, task in enumerate(tasks, start=1): - status = "✓" if task.get("done") else "✗" - print(f"{i}. [{status}] {task['description']}") + for task in tasks: + status = "x" if task.get("done") else " " + print(f"[{status}] {task['id']}: {task['description']}") From 820e93cffe62b379f9e43b4a08c5148b06554cb4 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:53:16 +0530 Subject: [PATCH 10/26] fix(commands/done.py): restore graceful missing-file handling and ID-based task lookup --- commands/done.py | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/commands/done.py b/commands/done.py index b22f26f..c1bffc5 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,25 +1,33 @@ -"""Command: mark a task as done.""" - import json from utils.paths import get_tasks_file -from utils.validation import validate_task_file, validate_task_id def mark_done(task_id: int) -> None: - """Mark the task identified by *task_id* as done.""" - filepath = get_tasks_file() - validate_task_file(filepath) - - with open(filepath, "r") as f: - tasks = json.load(f) + """Mark the task with the given stored ID as done. - validate_task_id(task_id, tasks) - - task = tasks[task_id - 1] - task["done"] = True - - with open(filepath, "w") as f: - json.dump(tasks, f, indent=2) + Preserves previous behaviour: + - Prints a friendly message and returns (without raising) when the tasks + file does not exist. + - Looks up tasks by their stored ``id`` field, not by list index. + """ + filepath = get_tasks_file() - print(f"Task {task_id} marked as done: {task['description']}") + # Preserve previous behaviour: missing file → friendly message, no exception. + try: + with open(filepath, "r") as f: + tasks = json.load(f) + except FileNotFoundError: + print("No tasks found.") + return + + # ID-based lookup — matches the original persistence semantics. + for task in tasks: + if task.get("id") == task_id: + task["done"] = True + with open(filepath, "w") as f: + json.dump(tasks, f, indent=2) + print(f"Task {task_id} marked as done.") + return + + print(f"Task {task_id} not found.") From 0120f37e9dc7b4780bfc132b396a2803e88e21be Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:20:56 +0530 Subject: [PATCH 11/26] refactor: add utils package __init__.py --- utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/__init__.py b/utils/__init__.py index 11fa064..db3e327 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1 +1 @@ -# Utils package for shared helpers and validation logic +# utils package From 65bfefaa538520697e39cc98b1da114f55ef3a25 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:21:02 +0530 Subject: [PATCH 12/26] refactor: add utils/paths.py with shared get_tasks_file() helper --- utils/paths.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/utils/paths.py b/utils/paths.py index e8041a1..81d5618 100644 --- a/utils/paths.py +++ b/utils/paths.py @@ -1,12 +1,8 @@ +"""Shared path helpers for the task manager application.""" + import os -from pathlib import Path def get_tasks_file() -> str: - """Return the path to the tasks JSON file. - - Defaults to ``tasks.json`` in the user's home directory, matching the - original behaviour of the individual command modules. The path can be - overridden by setting the ``TASKS_FILE`` environment variable. - """ - return os.environ.get("TASKS_FILE", str(Path.home() / "tasks.json")) + """Return the path to the tasks file, defaulting to 'tasks.json'.""" + return os.environ.get("TASKS_FILE", "tasks.json") From d89d97a623a0b460e87a3f5e8fa47b643a0de28d Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:21:09 +0530 Subject: [PATCH 13/26] refactor: add utils/validation.py with all shared validation functions --- utils/validation.py | 60 ++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/utils/validation.py b/utils/validation.py index 3d1c5ee..a5d26b0 100644 --- a/utils/validation.py +++ b/utils/validation.py @@ -1,41 +1,55 @@ -import json -import os +"""Shared validation functions for the task manager application.""" +import os -def validate_description(description: str) -> str: - """Validate and normalize a task description. - The description must be non-empty (after stripping whitespace) and no longer - than 200 characters. +def validate_description(description: str) -> None: + """Validate a task description. - Returns the stripped description string. + Args: + description: The task description string to validate. Raises: - ValueError: if the description is empty or exceeds 200 characters. + ValueError: If the description is empty or contains only whitespace. """ - stripped = description.strip() - if not stripped: - raise ValueError("Description cannot be empty.") - if len(stripped) > 200: - raise ValueError("Description cannot exceed 200 characters.") - return stripped + if not description or not description.strip(): + raise ValueError("Task description cannot be empty.") -def validate_task_file(filepath: str) -> None: - """Check that the tasks file exists. +def validate_task_file(tasks_file: str) -> None: + """Validate that the tasks file exists. + + Args: + tasks_file: Path to the tasks JSON file. Raises: - FileNotFoundError: if *filepath* does not exist. + FileNotFoundError: If the tasks file does not exist. """ - if not os.path.exists(filepath): - raise FileNotFoundError(f"Tasks file not found: {filepath}") + if not os.path.exists(tasks_file): + raise FileNotFoundError( + f"Tasks file '{tasks_file}' not found. " + "Add a task first with: task add " + ) -def validate_task_id(task_id: int) -> None: +def validate_task_id(task_id: str) -> int: """Validate that a task ID is a positive integer. + Args: + task_id: The task ID string to validate. + + Returns: + The task ID as an integer. + Raises: - ValueError: if *task_id* is not a positive integer. + ValueError: If the task ID is not a valid positive integer. """ - if not isinstance(task_id, int) or task_id <= 0: - raise ValueError(f"Task ID must be a positive integer, got: {task_id!r}") + try: + tid = int(task_id) + except (ValueError, TypeError): + raise ValueError(f"Task ID must be a positive integer, got: '{task_id}'") + + if tid <= 0: + raise ValueError(f"Task ID must be a positive integer, got: {tid}") + + return tid From 5290708f9b1c82a7cea33bb1313fa0ac59b4de01 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:21:15 +0530 Subject: [PATCH 14/26] refactor: update commands/add.py to use utils.paths and utils.validation --- commands/add.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/commands/add.py b/commands/add.py index a570bfd..b1e89a4 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,26 +1,36 @@ """Command: add a new task.""" import json +import os from utils.paths import get_tasks_file from utils.validation import validate_description def add_task(description: str) -> None: - """Add a new task with the given *description* to the tasks file.""" + """Add a new task with the given description. + + Args: + description: The task description to add. + """ validate_description(description) - filepath = get_tasks_file() + tasks_file = get_tasks_file() - try: - with open(filepath, "r") as f: + if os.path.exists(tasks_file): + with open(tasks_file, "r") as f: tasks = json.load(f) - except FileNotFoundError: + else: tasks = [] - tasks.append({"description": description.strip(), "done": False}) + task = { + "id": len(tasks) + 1, + "description": description.strip(), + "done": False, + } + tasks.append(task) - with open(filepath, "w") as f: + with open(tasks_file, "w") as f: json.dump(tasks, f, indent=2) - print(f"Task added: {description.strip()}") + print(f"Added task #{task['id']}: {task['description']}") From 004675fff4a9ff5e2d4aa90cb5279d30bd7b6185 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:21:21 +0530 Subject: [PATCH 15/26] refactor: update commands/list.py to use utils.paths and utils.validation --- commands/list.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/commands/list.py b/commands/list.py index bd7b526..07580a7 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,28 +1,24 @@ +"""Command: list all tasks.""" + import json from utils.paths import get_tasks_file +from utils.validation import validate_task_file def list_tasks() -> None: - """Print all tasks to stdout. - - Prints a friendly message and returns (without raising) when the tasks - file does not exist, preserving the previous user-visible behaviour. - """ - filepath = get_tasks_file() - - # Preserve previous behaviour: missing file → friendly message, no exception. - try: - with open(filepath, "r") as f: - tasks = json.load(f) - except FileNotFoundError: - print("No tasks found.") - return + """List all tasks, showing their ID, status, and description.""" + tasks_file = get_tasks_file() + + validate_task_file(tasks_file) + + with open(tasks_file, "r") as f: + tasks = json.load(f) if not tasks: print("No tasks found.") return for task in tasks: - status = "x" if task.get("done") else " " - print(f"[{status}] {task['id']}: {task['description']}") + status = "✓" if task.get("done") else "✗" + print(f"[{status}] #{task['id']}: {task['description']}") From dde8226ca6fcb51e62e9c8e993b8ce96b0414cad Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:21:28 +0530 Subject: [PATCH 16/26] refactor: update commands/done.py to use utils.paths and utils.validation --- commands/done.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/commands/done.py b/commands/done.py index c1bffc5..e32d796 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,33 +1,31 @@ +"""Command: mark a task as done.""" + import json from utils.paths import get_tasks_file +from utils.validation import validate_task_file, validate_task_id -def mark_done(task_id: int) -> None: - """Mark the task with the given stored ID as done. +def mark_done(task_id: str) -> None: + """Mark the task with the given ID as done. - Preserves previous behaviour: - - Prints a friendly message and returns (without raising) when the tasks - file does not exist. - - Looks up tasks by their stored ``id`` field, not by list index. + Args: + task_id: The string representation of the task's integer ID. """ - filepath = get_tasks_file() + tid = validate_task_id(task_id) + + tasks_file = get_tasks_file() + validate_task_file(tasks_file) - # Preserve previous behaviour: missing file → friendly message, no exception. - try: - with open(filepath, "r") as f: - tasks = json.load(f) - except FileNotFoundError: - print("No tasks found.") - return + with open(tasks_file, "r") as f: + tasks = json.load(f) - # ID-based lookup — matches the original persistence semantics. for task in tasks: - if task.get("id") == task_id: + if task["id"] == tid: task["done"] = True - with open(filepath, "w") as f: + with open(tasks_file, "w") as f: json.dump(tasks, f, indent=2) - print(f"Task {task_id} marked as done.") + print(f"Task #{tid} marked as done.") return - print(f"Task {task_id} not found.") + print(f"Task #{tid} not found.") From 2911a540acb2701e50e3de061997b315bc9cea82 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:57:16 +0530 Subject: [PATCH 17/26] fix(utils/paths): restore home-dir default path, add TASKS_FILE env override --- utils/paths.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/utils/paths.py b/utils/paths.py index 81d5618..7e2aa6f 100644 --- a/utils/paths.py +++ b/utils/paths.py @@ -1,8 +1,11 @@ -"""Shared path helpers for the task manager application.""" - import os - def get_tasks_file() -> str: - """Return the path to the tasks file, defaulting to 'tasks.json'.""" - return os.environ.get("TASKS_FILE", "tasks.json") + """Return the path to the tasks JSON file. + + Respects the ``TASKS_FILE`` environment variable when set; otherwise + defaults to ``tasks.json`` inside the user's home directory — preserving + the original behaviour of the individual command modules. + """ + default = os.path.join(os.path.expanduser("~"), "tasks.json") + return os.environ.get("TASKS_FILE", default) From 84cf049525a0969a4f12ff5c3d77b0ae393a3256 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:57:24 +0530 Subject: [PATCH 18/26] fix(utils/validation): restore return value, max-length check, and id-based lookup helper --- utils/validation.py | 68 +++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/utils/validation.py b/utils/validation.py index a5d26b0..a147d3b 100644 --- a/utils/validation.py +++ b/utils/validation.py @@ -1,55 +1,37 @@ -"""Shared validation functions for the task manager application.""" - +import json import os - -def validate_description(description: str) -> None: - """Validate a task description. - - Args: - description: The task description string to validate. - - Raises: - ValueError: If the description is empty or contains only whitespace. - """ - if not description or not description.strip(): - raise ValueError("Task description cannot be empty.") +MAX_DESCRIPTION_LENGTH = 200 -def validate_task_file(tasks_file: str) -> None: - """Validate that the tasks file exists. +def validate_description(description: str) -> str: + """Validate and normalize a task description. - Args: - tasks_file: Path to the tasks JSON file. + The description must be non-empty (after stripping whitespace) and no longer + than 200 characters. - Raises: - FileNotFoundError: If the tasks file does not exist. + Returns the stripped description string on success. + Raises ValueError for empty or too-long descriptions. """ - if not os.path.exists(tasks_file): - raise FileNotFoundError( - f"Tasks file '{tasks_file}' not found. " - "Add a task first with: task add " + stripped = description.strip() + if not stripped: + raise ValueError("Description cannot be empty.") + if len(stripped) > MAX_DESCRIPTION_LENGTH: + raise ValueError( + f"Description is too long ({len(stripped)} chars); " + f"maximum allowed is {MAX_DESCRIPTION_LENGTH}." ) + return stripped -def validate_task_id(task_id: str) -> int: - """Validate that a task ID is a positive integer. - - Args: - task_id: The task ID string to validate. - - Returns: - The task ID as an integer. - - Raises: - ValueError: If the task ID is not a valid positive integer. - """ - try: - tid = int(task_id) - except (ValueError, TypeError): - raise ValueError(f"Task ID must be a positive integer, got: '{task_id}'") +def validate_task_file(filepath: str) -> None: + """Raise FileNotFoundError if *filepath* does not exist.""" + if not os.path.exists(filepath): + raise FileNotFoundError(f"Tasks file not found: {filepath}") - if tid <= 0: - raise ValueError(f"Task ID must be a positive integer, got: {tid}") - return tid +def validate_task_id(task_id: int, tasks: list) -> None: + """Raise ValueError if *task_id* is not a valid stored task id.""" + ids = [t["id"] for t in tasks] + if task_id not in ids: + raise ValueError(f"No task found with id {task_id}.") From 7bcbb90ca5ae9965573fa1800584ccddd8051b01 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:57:31 +0530 Subject: [PATCH 19/26] fix(commands/list): restore friendly missing-file message and original output format --- commands/list.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/commands/list.py b/commands/list.py index 07580a7..e51ad0d 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,18 +1,24 @@ -"""Command: list all tasks.""" - import json +import os from utils.paths import get_tasks_file from utils.validation import validate_task_file def list_tasks() -> None: - """List all tasks, showing their ID, status, and description.""" - tasks_file = get_tasks_file() + """Print all tasks to stdout. + + If the tasks file does not exist a friendly message is printed and the + command returns without error — preserving the original UX. + """ + filepath = get_tasks_file() - validate_task_file(tasks_file) + # Non-exceptional handling for a missing file (original behaviour). + if not os.path.exists(filepath): + print("No tasks found.") + return - with open(tasks_file, "r") as f: + with open(filepath, "r") as f: tasks = json.load(f) if not tasks: @@ -20,5 +26,5 @@ def list_tasks() -> None: return for task in tasks: - status = "✓" if task.get("done") else "✗" - print(f"[{status}] #{task['id']}: {task['description']}") + status = "x" if task.get("done") else " " + print(f"[{status}] {task['id']}: {task['description']}") From 63b12789ee64133922e96fa747ea12bd6887dba8 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:57:38 +0530 Subject: [PATCH 20/26] fix(commands/done): restore ID-based lookup and graceful missing-file handling --- commands/done.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/commands/done.py b/commands/done.py index e32d796..50d1660 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,31 +1,37 @@ -"""Command: mark a task as done.""" - import json +import os from utils.paths import get_tasks_file -from utils.validation import validate_task_file, validate_task_id +from utils.validation import validate_task_id -def mark_done(task_id: str) -> None: - """Mark the task with the given ID as done. +def mark_done(task_id: int) -> None: + """Mark the task identified by *task_id* (its stored ``id`` field) as done. - Args: - task_id: The string representation of the task's integer ID. + Prints a friendly message when the tasks file is missing rather than + raising an exception — preserving the original UX. Task lookup uses the + stored ``id`` field, not the list index, to preserve persistence semantics. """ - tid = validate_task_id(task_id) + filepath = get_tasks_file() - tasks_file = get_tasks_file() - validate_task_file(tasks_file) + # Non-exceptional handling for a missing file (original behaviour). + if not os.path.exists(filepath): + print("No tasks found.") + return - with open(tasks_file, "r") as f: + with open(filepath, "r") as f: tasks = json.load(f) + # Validate that the requested id actually exists. + validate_task_id(task_id, tasks) + + # Update the matching task by its stored id (not by list index). for task in tasks: - if task["id"] == tid: + if task["id"] == task_id: task["done"] = True - with open(tasks_file, "w") as f: - json.dump(tasks, f, indent=2) - print(f"Task #{tid} marked as done.") - return + break + + with open(filepath, "w") as f: + json.dump(tasks, f, indent=2) - print(f"Task #{tid} not found.") + print(f"Task {task_id} marked as done.") From 233700d5caf6cf2a6548f4b0b1461d3b069148c4 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:21 +0530 Subject: [PATCH 21/26] feat: create utils package with __init__.py --- utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/__init__.py b/utils/__init__.py index db3e327..11fa064 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1 +1 @@ -# utils package +# Utils package for shared helpers and validation logic From 4874f02dbf550dadb12c265acab0bc646fbb112e Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:26 +0530 Subject: [PATCH 22/26] feat: create utils/paths.py with shared get_tasks_file() helper --- utils/paths.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/utils/paths.py b/utils/paths.py index 7e2aa6f..39e2c5f 100644 --- a/utils/paths.py +++ b/utils/paths.py @@ -1,11 +1,8 @@ +"""Shared path helpers for the task manager application.""" + import os -def get_tasks_file() -> str: - """Return the path to the tasks JSON file. - Respects the ``TASKS_FILE`` environment variable when set; otherwise - defaults to ``tasks.json`` inside the user's home directory — preserving - the original behaviour of the individual command modules. - """ - default = os.path.join(os.path.expanduser("~"), "tasks.json") - return os.environ.get("TASKS_FILE", default) +def get_tasks_file() -> str: + """Return the path to the tasks file, defaulting to 'tasks.txt'.""" + return os.environ.get("TASKS_FILE", "tasks.txt") From 3a35b9740aa690c83752cd19648a970f1c138a60 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:32 +0530 Subject: [PATCH 23/26] feat: create utils/validation.py with extracted validation functions --- utils/validation.py | 75 +++++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/utils/validation.py b/utils/validation.py index a147d3b..f647b80 100644 --- a/utils/validation.py +++ b/utils/validation.py @@ -1,37 +1,52 @@ -import json +"""Shared validation helpers for the task manager application.""" + import os -MAX_DESCRIPTION_LENGTH = 200 +def validate_description(description: str) -> None: + """Validate that a task description is non-empty. + + Args: + description: The task description string to validate. + + Raises: + ValueError: If the description is empty or contains only whitespace. + """ + if not description or not description.strip(): + raise ValueError("Task description cannot be empty.") -def validate_description(description: str) -> str: - """Validate and normalize a task description. - The description must be non-empty (after stripping whitespace) and no longer - than 200 characters. +def validate_task_file(tasks_file: str) -> None: + """Validate that the tasks file exists. - Returns the stripped description string on success. - Raises ValueError for empty or too-long descriptions. + Args: + tasks_file: Path to the tasks file. + + Raises: + FileNotFoundError: If the tasks file does not exist. """ - stripped = description.strip() - if not stripped: - raise ValueError("Description cannot be empty.") - if len(stripped) > MAX_DESCRIPTION_LENGTH: - raise ValueError( - f"Description is too long ({len(stripped)} chars); " - f"maximum allowed is {MAX_DESCRIPTION_LENGTH}." - ) - return stripped - - -def validate_task_file(filepath: str) -> None: - """Raise FileNotFoundError if *filepath* does not exist.""" - if not os.path.exists(filepath): - raise FileNotFoundError(f"Tasks file not found: {filepath}") - - -def validate_task_id(task_id: int, tasks: list) -> None: - """Raise ValueError if *task_id* is not a valid stored task id.""" - ids = [t["id"] for t in tasks] - if task_id not in ids: - raise ValueError(f"No task found with id {task_id}.") + if not os.path.exists(tasks_file): + raise FileNotFoundError(f"Tasks file not found: {tasks_file}") + + +def validate_task_id(task_id: str) -> int: + """Validate that a task ID is a positive integer. + + Args: + task_id: The task ID string to validate. + + Returns: + The task ID as a positive integer. + + Raises: + ValueError: If the task ID is not a valid positive integer. + """ + try: + tid = int(task_id) + except (TypeError, ValueError): + raise ValueError(f"Invalid task ID '{task_id}': must be a positive integer.") + + if tid <= 0: + raise ValueError(f"Invalid task ID '{task_id}': must be a positive integer.") + + return tid From 3ced761d8315dd089a4976353a864eeaf564c92c Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:40 +0530 Subject: [PATCH 24/26] refactor: update commands/add.py to use utils.validation and utils.paths --- commands/add.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/commands/add.py b/commands/add.py index b1e89a4..0af24ca 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,7 +1,4 @@ -"""Command: add a new task.""" - -import json -import os +"""Command to add a new task.""" from utils.paths import get_tasks_file from utils.validation import validate_description @@ -11,26 +8,16 @@ def add_task(description: str) -> None: """Add a new task with the given description. Args: - description: The task description to add. + description: The description for the new task. + + Raises: + ValueError: If the description is empty. """ validate_description(description) tasks_file = get_tasks_file() - if os.path.exists(tasks_file): - with open(tasks_file, "r") as f: - tasks = json.load(f) - else: - tasks = [] - - task = { - "id": len(tasks) + 1, - "description": description.strip(), - "done": False, - } - tasks.append(task) - - with open(tasks_file, "w") as f: - json.dump(tasks, f, indent=2) + with open(tasks_file, "a") as f: + f.write(f"{description.strip()}\n") - print(f"Added task #{task['id']}: {task['description']}") + print(f"Task added: {description.strip()}") From b5e93bbddc5f33a4064c25dae019b01265ac0629 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:45 +0530 Subject: [PATCH 25/26] refactor: update commands/list.py to use utils.validation and utils.paths --- commands/list.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/commands/list.py b/commands/list.py index e51ad0d..43a9a2d 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,30 +1,26 @@ -import json -import os +"""Command to list all tasks.""" from utils.paths import get_tasks_file from utils.validation import validate_task_file def list_tasks() -> None: - """Print all tasks to stdout. + """List all tasks from the tasks file. - If the tasks file does not exist a friendly message is printed and the - command returns without error — preserving the original UX. + Raises: + FileNotFoundError: If the tasks file does not exist. """ - filepath = get_tasks_file() + tasks_file = get_tasks_file() + validate_task_file(tasks_file) - # Non-exceptional handling for a missing file (original behaviour). - if not os.path.exists(filepath): - print("No tasks found.") - return - - with open(filepath, "r") as f: - tasks = json.load(f) + with open(tasks_file, "r") as f: + lines = f.readlines() - if not tasks: + if not lines: print("No tasks found.") return - for task in tasks: - status = "x" if task.get("done") else " " - print(f"[{status}] {task['id']}: {task['description']}") + for index, line in enumerate(lines, start=1): + task = line.strip() + if task: + print(f"{index}. {task}") From 6ead915246cffed91a37b0f03b34ff77537d364d Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:51 +0530 Subject: [PATCH 26/26] refactor: update commands/done.py to use utils.validation and utils.paths --- commands/done.py | 50 ++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/commands/done.py b/commands/done.py index 50d1660..b4704e6 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,37 +1,37 @@ -import json -import os +"""Command to mark a task as done.""" from utils.paths import get_tasks_file -from utils.validation import validate_task_id +from utils.validation import validate_task_file, validate_task_id -def mark_done(task_id: int) -> None: - """Mark the task identified by *task_id* (its stored ``id`` field) as done. +def mark_done(task_id: str) -> None: + """Mark the task with the given ID as done (removes it from the list). - Prints a friendly message when the tasks file is missing rather than - raising an exception — preserving the original UX. Task lookup uses the - stored ``id`` field, not the list index, to preserve persistence semantics. + Args: + task_id: The 1-based index of the task to mark as done. + + Raises: + ValueError: If the task ID is not a valid positive integer or out of range. + FileNotFoundError: If the tasks file does not exist. """ - filepath = get_tasks_file() + tid = validate_task_id(task_id) + + tasks_file = get_tasks_file() + validate_task_file(tasks_file) - # Non-exceptional handling for a missing file (original behaviour). - if not os.path.exists(filepath): - print("No tasks found.") - return + with open(tasks_file, "r") as f: + lines = f.readlines() - with open(filepath, "r") as f: - tasks = json.load(f) + tasks = [line for line in lines if line.strip()] - # Validate that the requested id actually exists. - validate_task_id(task_id, tasks) + if tid > len(tasks): + raise ValueError( + f"Task ID {tid} is out of range. There are only {len(tasks)} task(s)." + ) - # Update the matching task by its stored id (not by list index). - for task in tasks: - if task["id"] == task_id: - task["done"] = True - break + completed_task = tasks.pop(tid - 1).strip() - with open(filepath, "w") as f: - json.dump(tasks, f, indent=2) + with open(tasks_file, "w") as f: + f.writelines(tasks) - print(f"Task {task_id} marked as done.") + print(f"Task {tid} marked as done: {completed_task}")