diff --git a/README.md b/README.md index 91da9a7..0de3502 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,24 @@ python task.py list python task.py done 1 ``` +### JSON Output + +All commands support a `--json` flag for machine-readable output: + +```bash +# Add a task with JSON output +python task.py add --json "Buy groceries" +# {"success": true, "message": "Added task 1", "task": {"id": 1, "description": "Buy groceries", "done": false}} + +# List tasks in JSON format +python task.py list --json +# {"success": true, "tasks": [{"id": 1, "description": "Buy groceries", "done": false}]} + +# Mark done with JSON output +python task.py done --json 1 +# {"success": true, "message": "Marked task 1 as done", "task": {"id": 1, "description": "Buy groceries", "done": true}} +``` + ## Testing ```bash diff --git a/commands/add.py b/commands/add.py index 1b1a943..7a24a2e 100644 --- a/commands/add.py +++ b/commands/add.py @@ -19,7 +19,7 @@ def validate_description(description): return description.strip() -def add_task(description): +def add_task(description, json_output=False): """Add a new task.""" description = validate_description(description) @@ -31,7 +31,12 @@ 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({"success": True, "message": f"Added task {task_id}", "task": task})) + else: + print(f"Added task {task_id}: {description}") diff --git a/commands/done.py b/commands/done.py index c9dfd42..86e8040 100644 --- a/commands/done.py +++ b/commands/done.py @@ -17,11 +17,14 @@ def validate_task_id(tasks, 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({"success": False, "message": "No tasks found"})) + else: + print("No tasks found!") return tasks = json.loads(tasks_file.read_text()) @@ -31,7 +34,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({"success": True, "message": f"Marked task {task_id} as 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({"success": False, "message": 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..ad8e378 100644 --- a/commands/list.py +++ b/commands/list.py @@ -18,20 +18,28 @@ def validate_task_file(): return tasks_file -def list_tasks(): +def list_tasks(json_output=False): """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({"success": True, "tasks": []})) + else: + print("No tasks yet!") return tasks = json.loads(tasks_file.read_text()) if not tasks: - print("No tasks yet!") + if json_output: + print(json.dumps({"success": True, "tasks": []})) + else: + print("No tasks yet!") return - for task in tasks: - status = "✓" if task["done"] else " " - print(f"[{status}] {task['id']}. {task['description']}") + if json_output: + print(json.dumps({"success": True, "tasks": tasks})) + else: + for task in tasks: + status = "✓" if task["done"] else " " + print(f"[{status}] {task['id']}. {task['description']}") diff --git a/task.py b/task.py index 53cc8ed..745e401 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 in JSON format") # List command list_parser = subparsers.add_parser("list", help="List all tasks") + list_parser.add_argument("--json", action="store_true", help="Output in JSON format") # 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 in JSON format") 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_task.py b/test_task.py index ba98e43..151f5d7 100644 --- a/test_task.py +++ b/test_task.py @@ -2,9 +2,11 @@ import json import pytest +import subprocess from pathlib import Path from commands.add import add_task, validate_description -from commands.done import validate_task_id +from commands.list import list_tasks +from commands.done import validate_task_id, mark_done def test_validate_description(): @@ -28,3 +30,84 @@ def test_validate_task_id(): with pytest.raises(ValueError): validate_task_id(tasks, 99) + + +# ── JSON output tests ───────────────────────────────────────────────────── + + +def test_add_json_output(capsys, tmp_path, monkeypatch): + """Test add command with --json flag produces valid JSON.""" + monkeypatch.setattr( + "commands.add.get_tasks_file", lambda: tmp_path / "tasks.json" + ) + add_task("Test task", json_output=True) + output = json.loads(capsys.readouterr().out) + assert output["success"] is True + assert output["task"]["description"] == "Test task" + assert output["task"]["id"] == 1 + assert output["task"]["done"] is False + + +def test_add_text_output(capsys, tmp_path, monkeypatch): + """Test add command without --json flag produces plain text.""" + monkeypatch.setattr( + "commands.add.get_tasks_file", lambda: tmp_path / "tasks.json" + ) + add_task("Test task", json_output=False) + output = capsys.readouterr().out.strip() + assert "Added task 1: Test task" == output + + +def test_list_json_output(capsys, tmp_path, monkeypatch): + """Test list command with --json flag produces valid JSON.""" + monkeypatch.setattr( + "commands.add.get_tasks_file", lambda: tmp_path / "tasks.json" + ) + monkeypatch.setattr( + "commands.list.get_tasks_file", lambda: tmp_path / "tasks.json" + ) + add_task("Task one") + add_task("Task two") + list_tasks(json_output=True) + output = json.loads(capsys.readouterr().out.strip().split("\n")[-1]) + assert output["success"] is True + assert len(output["tasks"]) == 2 + assert output["tasks"][0]["description"] == "Task one" + + +def test_list_empty_json(capsys, tmp_path, monkeypatch): + """Test list --json with no tasks returns empty array.""" + monkeypatch.setattr( + "commands.list.get_tasks_file", lambda: tmp_path / "tasks.json" + ) + list_tasks(json_output=True) + output = json.loads(capsys.readouterr().out) + assert output["success"] is True + assert output["tasks"] == [] + + +def test_done_json_output(capsys, tmp_path, monkeypatch): + """Test done command with --json flag produces valid JSON.""" + monkeypatch.setattr( + "commands.add.get_tasks_file", lambda: tmp_path / "tasks.json" + ) + monkeypatch.setattr( + "commands.done.get_tasks_file", lambda: tmp_path / "tasks.json" + ) + add_task("Task to complete") + mark_done(1, json_output=True) + lines = capsys.readouterr().out.strip().split("\n") + output = json.loads(lines[-1]) + assert output["success"] is True + assert output["task"]["done"] is True + + +def test_cli_json_flag(): + """Test --json flag works via CLI invocation.""" + result = subprocess.run( + ["python3", "task.py", "list", "--json"], + capture_output=True, text=True, cwd=str(Path(__file__).parent), + ) + output = json.loads(result.stdout) + assert "success" in output + assert "tasks" in output