From 972479275eca439f915bedbd25d87773ca64569a Mon Sep 17 00:00:00 2001 From: Make Studio Date: Sat, 16 May 2026 01:31:35 +0000 Subject: [PATCH 1/2] Refactor: extract validation and path utilities to shared utils module - Create utils/validation.py with shared validate_description, validate_task_id, validate_task_file - Create utils/paths.py with shared get_tasks_file and get_config_path - Remove duplicated get_tasks_file and validation logic from add.py, list.py, done.py - Update commands to import from utils modules - Update tests to cover utils modules and verify refactoring Closes #3 --- commands/add.py | 18 ++------------- commands/done.py | 16 ++------------ commands/list.py | 20 +++-------------- test_task.py | 53 +++++++++++++++++++++++++++++++++++++++++++-- utils/__init__.py | 0 utils/paths.py | 13 +++++++++++ utils/validation.py | 26 ++++++++++++++++++++++ 7 files changed, 97 insertions(+), 49 deletions(-) create mode 100644 utils/__init__.py create mode 100644 utils/paths.py create mode 100644 utils/validation.py diff --git a/commands/add.py b/commands/add.py index 1b1a943..dba12e4 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,22 +1,8 @@ """Add task command.""" import json -from pathlib import Path - - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -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() +from utils.paths import get_tasks_file +from utils.validation import validate_description def add_task(description): diff --git a/commands/done.py b/commands/done.py index c9dfd42..995f058 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,20 +1,8 @@ """Mark task done command.""" import json -from pathlib import Path - - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -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 +from utils.paths import get_tasks_file +from utils.validation import validate_task_id def mark_done(task_id): diff --git a/commands/list.py b/commands/list.py index 714315d..a4b0b87 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,27 +1,13 @@ """List tasks command.""" import json -from pathlib import Path - - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -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 +from utils.paths import get_tasks_file +from utils.validation import validate_task_file def list_tasks(): """List all tasks.""" - # NOTE: No --json flag support yet (feature bounty) - tasks_file = validate_task_file() + tasks_file = validate_task_file(get_tasks_file()) if not tasks_file: print("No tasks yet!") return diff --git a/test_task.py b/test_task.py index ba98e43..061ebee 100644 --- a/test_task.py +++ b/test_task.py @@ -3,8 +3,11 @@ import json import pytest from pathlib import Path -from commands.add import add_task, validate_description -from commands.done import validate_task_id +from commands.add import add_task +from commands.done import mark_done +from commands.list import list_tasks +from utils.paths import get_tasks_file +from utils.validation import validate_description, validate_task_id, validate_task_file def test_validate_description(): @@ -28,3 +31,49 @@ def test_validate_task_id(): with pytest.raises(ValueError): validate_task_id(tasks, 99) + + +def test_validate_task_file(): + """Test task file validation.""" + nonexistent = Path("/nonexistent/tasks.json") + assert validate_task_file(nonexistent) == [] + + tasks_file = Path("/tmp/test-tasks.json") + tasks_file.write_text("[]") + result = validate_task_file(tasks_file) + assert result == tasks_file + tasks_file.unlink() + + +def test_get_tasks_file(): + """Test shared path function.""" + path = get_tasks_file() + assert path.name == "tasks.json" + assert "task-cli" in str(path) + + +def test_add_task(tmp_path, monkeypatch): + """Test add task uses shared utils.""" + from utils import paths + import commands.add as add_mod + task_file = tmp_path / "tasks.json" + monkeypatch.setattr(paths, "get_tasks_file", lambda: task_file) + monkeypatch.setattr(add_mod, "get_tasks_file", lambda: task_file) + + add_task("test task") + assert task_file.exists() + tasks = json.loads(task_file.read_text()) + assert len(tasks) == 1 + assert tasks[0]["description"] == "test task" + + +def test_mark_done(tmp_path, monkeypatch): + """Test mark done uses shared utils.""" + import commands.done as done_mod + task_file = tmp_path / "tasks.json" + monkeypatch.setattr(done_mod, "get_tasks_file", lambda: task_file) + + task_file.write_text(json.dumps([{"id": 1, "description": "x", "done": False}])) + mark_done(1) + tasks = json.loads(task_file.read_text()) + assert tasks[0]["done"] is True diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/paths.py b/utils/paths.py new file mode 100644 index 0000000..a346fa2 --- /dev/null +++ b/utils/paths.py @@ -0,0 +1,13 @@ +"""Shared path utilities for task CLI.""" + +from pathlib import Path + + +def get_tasks_file(): + """Get path to tasks file.""" + return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" + + +def get_config_path(): + """Get path to config file.""" + return Path.home() / ".config" / "task-cli" / "config.yaml" diff --git a/utils/validation.py b/utils/validation.py new file mode 100644 index 0000000..ebb904d --- /dev/null +++ b/utils/validation.py @@ -0,0 +1,26 @@ +"""Shared validation utilities for task CLI.""" + +import json + + +def validate_description(description): + """Validate task description.""" + if not description: + raise ValueError("Description cannot be empty") + if len(description) > 200: + raise ValueError("Description too long (max 200 chars)") + return description.strip() + + +def validate_task_file(tasks_file): + """Validate tasks file exists. Returns path if valid, empty list if no file.""" + if not tasks_file.exists(): + return [] + return tasks_file + + +def validate_task_id(tasks, task_id): + """Validate task ID exists in task list.""" + if task_id < 1 or task_id > len(tasks): + raise ValueError(f"Invalid task ID: {task_id}") + return task_id From 6c1d1b3c37abc1b08f299e44a9ed6185439a2a15 Mon Sep 17 00:00:00 2001 From: Make Studio Date: Sat, 16 May 2026 01:32:55 +0000 Subject: [PATCH 2/2] Fix: handle missing config file gracefully instead of crashing - Create default config when config.yaml is missing - Use shared get_config_path from utils/paths.py - Add tests for config creation and existing config reading Closes #2 --- task.py | 23 +++++++++++++---------- test_task.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/task.py b/task.py index 53cc8ed..3716b81 100644 --- a/task.py +++ b/task.py @@ -3,33 +3,36 @@ import argparse import sys -from pathlib import Path - +from utils.paths import get_config_path from commands.add import add_task from commands.list import list_tasks from commands.done import mark_done +DEFAULT_CONFIG = """# Task CLI Configuration +# Edit this file to customize behavior +tasks_path: ~/.local/share/task-cli/tasks.json +""" + def load_config(): - """Load configuration from file.""" - config_path = Path.home() / ".config" / "task-cli" / "config.yaml" - # NOTE: This will crash if config doesn't exist - known bug for bounty testing - with open(config_path) as f: - return f.read() + """Load configuration from file. Creates default config if missing.""" + config_path = get_config_path() + if not config_path.exists(): + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text(DEFAULT_CONFIG) + print(f"Created default config at {config_path}") + return config_path.read_text() def main(): parser = argparse.ArgumentParser(description="Simple task manager") subparsers = parser.add_subparsers(dest="command", help="Command to run") - # Add command add_parser = subparsers.add_parser("add", help="Add a new task") add_parser.add_argument("description", help="Task description") - # List command list_parser = subparsers.add_parser("list", help="List all tasks") - # Done command done_parser = subparsers.add_parser("done", help="Mark task as complete") done_parser.add_argument("task_id", type=int, help="Task ID to mark done") diff --git a/test_task.py b/test_task.py index 061ebee..d345508 100644 --- a/test_task.py +++ b/test_task.py @@ -77,3 +77,31 @@ def test_mark_done(tmp_path, monkeypatch): mark_done(1) tasks = json.loads(task_file.read_text()) assert tasks[0]["done"] is True + + +def test_load_config_creates_default(tmp_path, monkeypatch): + """Test config creation when file is missing.""" + from utils import paths + import task as task_mod + + fake_config = tmp_path / "config.yaml" + monkeypatch.setattr(paths, "get_config_path", lambda: fake_config) + monkeypatch.setattr(task_mod, "get_config_path", lambda: fake_config) + + result = task_mod.load_config() + assert fake_config.exists() + assert "task-cli" in result + + +def test_load_config_reads_existing(tmp_path, monkeypatch): + """Test config is read when file exists.""" + from utils import paths + import task as task_mod + + fake_config = tmp_path / "config.yaml" + fake_config.write_text("custom: true") + monkeypatch.setattr(paths, "get_config_path", lambda: fake_config) + monkeypatch.setattr(task_mod, "get_config_path", lambda: fake_config) + + result = task_mod.load_config() + assert result == "custom: true"