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/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 ba98e43..d345508 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,77 @@ 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 + + +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" 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