diff --git a/README.md b/README.md index 91da9a7..077365c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,11 @@ python task.py list # Complete a task python task.py done 1 + +# Emit machine-readable JSON for automation +python task.py add "Buy groceries" --json +python task.py list --json +python task.py done 1 --json ``` ## Testing diff --git a/commands/add.py b/commands/add.py index 1b1a943..ffaf0fc 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,25 +1,24 @@ """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: + if not description or not description.strip(): raise ValueError("Description cannot be empty") + description = description.strip() if len(description) > 200: - raise ValueError("Description too long (max 200 chars)") - return description.strip() + raise ValueError("Description too long") + return description -def add_task(description): +def get_tasks_file(): + """Get path to tasks file.""" + return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" + + +def add_task(description, json_output=False): """Add a new task.""" description = validate_description(description) @@ -31,7 +30,11 @@ def add_task(description): tasks = json.loads(tasks_file.read_text()) task_id = len(tasks) + 1 - tasks.append({"id": task_id, "description": description, "done": False}) - + task = {"id": task_id, "description": description, "done": False} + tasks.append(task) tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Added task {task_id}: {description}") + + if json_output: + print(json.dumps({"status": "added", "task": task})) + else: + print(f"Added task {task_id}: {description}") diff --git a/commands/done.py b/commands/done.py index c9dfd42..83b8a8c 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,5 +1,4 @@ """Mark task done command.""" - import json from pathlib import Path @@ -11,17 +10,19 @@ def get_tasks_file(): 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 -def mark_done(task_id): +def mark_done(task_id, json_output=False): """Mark a task as complete.""" tasks_file = get_tasks_file() if not tasks_file.exists(): - print("No tasks found!") + if json_output: + print(json.dumps({"status": "error", "error": "No tasks found"})) + else: + print("No tasks found!") return tasks = json.loads(tasks_file.read_text()) @@ -31,7 +32,13 @@ def mark_done(task_id): 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']}") + if json_output: + print(json.dumps({"status": "done", "task": task})) + else: + print(f"Marked task {task_id} as done: {task['description']}") return - print(f"Task {task_id} not found") + if json_output: + print(json.dumps({"status": "error", "error": f"Task {task_id} not found"})) + else: + print(f"Task {task_id} not found") diff --git a/commands/list.py b/commands/list.py index 714315d..c041020 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,5 +1,4 @@ """List tasks command.""" - import json from pathlib import Path @@ -9,24 +8,20 @@ def get_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) +def list_tasks(json_output=False): + """List all tasks.""" 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!") + if json_output: + print(json.dumps({"tasks": []})) + else: + print("No tasks yet!") return tasks = json.loads(tasks_file.read_text()) + if json_output: + print(json.dumps({"tasks": tasks})) + return if not tasks: print("No tasks yet!") diff --git a/task.py b/task.py index 53cc8ed..21ed983 100644 --- a/task.py +++ b/task.py @@ -25,22 +25,25 @@ def main(): # Add command add_parser = subparsers.add_parser("add", help="Add a new task") add_parser.add_argument("description", help="Task description") + add_parser.add_argument("--json", action="store_true", help="Output result as JSON") # List command list_parser = subparsers.add_parser("list", help="List all tasks") + list_parser.add_argument("--json", action="store_true", help="Output result as JSON") # 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") + done_parser.add_argument("--json", action="store_true", help="Output result as JSON") args = parser.parse_args() if args.command == "add": - add_task(args.description) + add_task(args.description, json_output=args.json) elif args.command == "list": - list_tasks() + list_tasks(json_output=args.json) elif args.command == "done": - mark_done(args.task_id) + mark_done(args.task_id, json_output=args.json) else: parser.print_help() diff --git a/test_json_output.py b/test_json_output.py new file mode 100644 index 0000000..b27f481 --- /dev/null +++ b/test_json_output.py @@ -0,0 +1,38 @@ +"""Tests for JSON output mode.""" +import json +import subprocess +import sys +from pathlib import Path + + +def run_cli(home, *args): + result = subprocess.run( + [sys.executable, "task.py", *args], + cwd=Path(__file__).parent, + env={"HOME": str(home)}, + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +def test_json_output_for_add_list_and_done(tmp_path): + added = json.loads(run_cli(tmp_path, "add", "Ship feature", "--json")) + assert added["status"] == "added" + assert added["task"] == {"id": 1, "description": "Ship feature", "done": False} + + listed = json.loads(run_cli(tmp_path, "list", "--json")) + assert listed["tasks"] == [added["task"]] + + done = json.loads(run_cli(tmp_path, "done", "1", "--json")) + assert done["status"] == "done" + assert done["task"] == {"id": 1, "description": "Ship feature", "done": True} + + listed_again = json.loads(run_cli(tmp_path, "list", "--json")) + assert listed_again["tasks"] == [done["task"]] + + +def test_json_list_empty_state(tmp_path): + listed = json.loads(run_cli(tmp_path, "list", "--json")) + assert listed == {"tasks": []}