diff --git a/gemini-cli-vs-claude-code/README.md b/gemini-cli-vs-claude-code/README.md new file mode 100644 index 0000000000..a3318f89ab --- /dev/null +++ b/gemini-cli-vs-claude-code/README.md @@ -0,0 +1,3 @@ +# Gemini CLI vs Claude Code: Which To Choose for Python Tasks + +This folder provides the code examples for the Real Python tutorial [Gemini CLI vs Claude Code: Which To Choose for Python Tasks](https://realpython.com/gemini-cli-vs-claude-code/). diff --git a/gemini-cli-vs-claude-code/claude_code_run_1/test_todo.py b/gemini-cli-vs-claude-code/claude_code_run_1/test_todo.py new file mode 100644 index 0000000000..b54f578128 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_1/test_todo.py @@ -0,0 +1,207 @@ +"""Unit tests for the to-do application.""" + +import json +import os +import sys +import tempfile +import unittest +from unittest.mock import patch + +# Point store at a temp file for every test +import todo_store as store +import todo + + +class BaseTest(unittest.TestCase): + """Set up a temporary tasks file for each test.""" + + def setUp(self): + self._tmp = tempfile.NamedTemporaryFile( + suffix=".json", delete=False, mode="w" + ) + self._tmp.write("[]") + self._tmp.close() + self._orig = store.TASKS_FILE + store.TASKS_FILE = self._tmp.name + + def tearDown(self): + store.TASKS_FILE = self._orig + os.unlink(self._tmp.name) + + +# ── store tests ─────────────────────────────────────────────────────────────── + +class TestAddTask(BaseTest): + + def test_add_returns_task(self): + task = store.add_task("Buy milk") + self.assertEqual(task["description"], "Buy milk") + self.assertFalse(task["completed"]) + self.assertEqual(task["id"], 1) + + def test_ids_increment(self): + t1 = store.add_task("First") + t2 = store.add_task("Second") + self.assertEqual(t1["id"], 1) + self.assertEqual(t2["id"], 2) + + def test_empty_description_raises(self): + with self.assertRaises(ValueError): + store.add_task("") + + def test_whitespace_only_raises(self): + with self.assertRaises(ValueError): + store.add_task(" ") + + def test_persists_to_disk(self): + store.add_task("Persisted") + with open(store.TASKS_FILE) as f: + data = json.load(f) + self.assertEqual(len(data), 1) + self.assertEqual(data[0]["description"], "Persisted") + + +class TestCompleteTask(BaseTest): + + def test_complete_task(self): + store.add_task("Write tests") + task = store.complete_task(1) + self.assertTrue(task["completed"]) + self.assertIsNotNone(task["completed_at"]) + + def test_complete_nonexistent_raises(self): + with self.assertRaises(KeyError): + store.complete_task(999) + + def test_complete_already_done_raises(self): + store.add_task("Already done") + store.complete_task(1) + with self.assertRaises(ValueError): + store.complete_task(1) + + +class TestDeleteTask(BaseTest): + + def test_delete_task(self): + store.add_task("To delete") + deleted = store.delete_task(1) + self.assertEqual(deleted["description"], "To delete") + self.assertEqual(store.load_tasks(), []) + + def test_delete_nonexistent_raises(self): + with self.assertRaises(KeyError): + store.delete_task(42) + + def test_remaining_tasks_intact(self): + store.add_task("Keep me") + store.add_task("Delete me") + store.delete_task(2) + tasks = store.load_tasks() + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0]["description"], "Keep me") + + +class TestFilterTasks(BaseTest): + + def setUp(self): + super().setUp() + store.add_task("Pending task") + store.add_task("Completed task") + store.complete_task(2) + self.tasks = store.load_tasks() + + def test_filter_all(self): + self.assertEqual(len(store.filter_tasks(self.tasks, "all")), 2) + + def test_filter_pending(self): + result = store.filter_tasks(self.tasks, "pending") + self.assertEqual(len(result), 1) + self.assertFalse(result[0]["completed"]) + + def test_filter_completed(self): + result = store.filter_tasks(self.tasks, "completed") + self.assertEqual(len(result), 1) + self.assertTrue(result[0]["completed"]) + + def test_filter_unknown_raises(self): + with self.assertRaises(ValueError): + store.filter_tasks(self.tasks, "invalid") + + +class TestCorruptedFile(BaseTest): + + def test_corrupted_json_raises(self): + with open(store.TASKS_FILE, "w") as f: + f.write("not valid json{{{") + with self.assertRaises(ValueError): + store.load_tasks() + + def test_non_array_json_raises(self): + with open(store.TASKS_FILE, "w") as f: + json.dump({"key": "value"}, f) + with self.assertRaises(ValueError): + store.load_tasks() + + def test_missing_file_returns_empty(self): + os.unlink(store.TASKS_FILE) + self.assertEqual(store.load_tasks(), []) + # restore so tearDown doesn't crash + with open(store.TASKS_FILE, "w") as f: + f.write("[]") + + +# ── CLI integration tests ───────────────────────────────────────────────────── + +class TestCLI(BaseTest): + + def _run(self, argv): + """Run CLI with given argv list, return exit code.""" + with patch("sys.argv", ["todo"] + argv): + parser = todo.build_parser() + args = parser.parse_args() + return args.func(args) + + def test_add_command(self): + code = self._run(["add", "CLI task"]) + self.assertEqual(code, 0) + self.assertEqual(len(store.load_tasks()), 1) + + def test_list_command(self): + store.add_task("Listed task") + code = self._run(["list"]) + self.assertEqual(code, 0) + + def test_list_pending_filter(self): + store.add_task("Pending") + store.add_task("Done") + store.complete_task(2) + code = self._run(["list", "--status", "pending"]) + self.assertEqual(code, 0) + + def test_done_command(self): + store.add_task("Mark done") + code = self._run(["done", "1"]) + self.assertEqual(code, 0) + self.assertTrue(store.load_tasks()[0]["completed"]) + + def test_delete_command(self): + store.add_task("Remove me") + code = self._run(["delete", "1"]) + self.assertEqual(code, 0) + self.assertEqual(store.load_tasks(), []) + + def test_done_missing_id_returns_error(self): + code = self._run(["done", "99"]) + self.assertEqual(code, 1) + + def test_delete_missing_id_returns_error(self): + code = self._run(["delete", "99"]) + self.assertEqual(code, 1) + + def test_add_empty_returns_error(self): + code = self._run(["add", ""]) + self.assertEqual(code, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/gemini-cli-vs-claude-code/claude_code_run_1/todo.py b/gemini-cli-vs-claude-code/claude_code_run_1/todo.py new file mode 100644 index 0000000000..fc7abdd429 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_1/todo.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""CLI to-do application. + +Usage: + todo.py add "Buy groceries" + todo.py list + todo.py list --status pending + todo.py list --status completed + todo.py done + todo.py delete +""" + +import argparse +import sys + +import todo_store as store + +# ── Formatting helpers ──────────────────────────────────────────────────────── + +CHECK = "[x]" +EMPTY = "[ ]" + + +def _fmt_task(task: dict) -> str: + status = CHECK if task["completed"] else EMPTY + suffix = f" (done {task['completed_at'][:10]})" if task["completed_at"] else "" + return f" {task['id']:>3} {status} {task['description']}{suffix}" + + +# ── Command handlers ────────────────────────────────────────────────────────── + +def cmd_add(args: argparse.Namespace) -> int: + try: + task = store.add_task(args.description) + print(f"Added task #{task['id']}: {task['description']}") + return 0 + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +def cmd_list(args: argparse.Namespace) -> int: + try: + tasks = store.load_tasks() + filtered = store.filter_tasks(tasks, args.status) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + if not filtered: + label = "" if args.status == "all" else f"{args.status} " + print(f"No {label}tasks found.") + return 0 + + label = "" if args.status == "all" else f"{args.status} " + print(f"\n--- {label}tasks ({len(filtered)}) ---") + for task in filtered: + print(_fmt_task(task)) + print() + return 0 + + +def cmd_done(args: argparse.Namespace) -> int: + try: + task = store.complete_task(args.id) + print(f"Completed task #{task['id']}: {task['description']}") + return 0 + except (KeyError, ValueError) as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +def cmd_delete(args: argparse.Namespace) -> int: + try: + task = store.delete_task(args.id) + print(f"Deleted task #{task['id']}: {task['description']}") + return 0 + except KeyError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +# ── Argument parsing ────────────────────────────────────────────────────────── + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="todo", + description="A simple CLI to-do application.", + ) + sub = parser.add_subparsers(dest="command", metavar="") + sub.required = True + + # add + p_add = sub.add_parser("add", help="Add a new task") + p_add.add_argument("description", help="Task description") + p_add.set_defaults(func=cmd_add) + + # list + p_list = sub.add_parser("list", help="List tasks") + p_list.add_argument( + "--status", + choices=["all", "pending", "completed"], + default="all", + help="Filter by status (default: all)", + ) + p_list.set_defaults(func=cmd_list) + + # done + p_done = sub.add_parser("done", help="Mark a task as completed") + p_done.add_argument("id", type=int, help="Task ID") + p_done.set_defaults(func=cmd_done) + + # delete + p_del = sub.add_parser("delete", help="Delete a task") + p_del.add_argument("id", type=int, help="Task ID") + p_del.set_defaults(func=cmd_delete) + + return parser + + +def main() -> int: + parser = build_parser() + args = parser.parse_args() + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gemini-cli-vs-claude-code/claude_code_run_1/todo_store.py b/gemini-cli-vs-claude-code/claude_code_run_1/todo_store.py new file mode 100644 index 0000000000..3832557643 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_1/todo_store.py @@ -0,0 +1,88 @@ +"""Task persistence layer — reads/writes tasks to a local JSON file.""" + +import json +import os +from datetime import datetime, timezone + +TASKS_FILE = "tasks.json" + + +def _load_raw() -> list[dict]: + if not os.path.exists(TASKS_FILE): + return [] + try: + with open(TASKS_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, list): + raise ValueError("Corrupted tasks file: expected a JSON array.") + return data + except json.JSONDecodeError as e: + raise ValueError(f"Corrupted tasks file: {e}") from e + + +def _save_raw(tasks: list[dict]) -> None: + with open(TASKS_FILE, "w", encoding="utf-8") as f: + json.dump(tasks, f, indent=2) + + +def _next_id(tasks: list[dict]) -> int: + return max((t["id"] for t in tasks), default=0) + 1 + + +def load_tasks() -> list[dict]: + """Return all tasks from disk.""" + return _load_raw() + + +def add_task(description: str) -> dict: + """Create a new task and persist it. Returns the new task.""" + description = description.strip() + if not description: + raise ValueError("Task description cannot be empty.") + tasks = _load_raw() + task = { + "id": _next_id(tasks), + "description": description, + "completed": False, + "created_at": datetime.now(timezone.utc).isoformat(), + "completed_at": None, + } + tasks.append(task) + _save_raw(tasks) + return task + + +def complete_task(task_id: int) -> dict: + """Mark a task as completed. Returns the updated task.""" + tasks = _load_raw() + for task in tasks: + if task["id"] == task_id: + if task["completed"]: + raise ValueError(f"Task {task_id} is already completed.") + task["completed"] = True + task["completed_at"] = datetime.now(timezone.utc).isoformat() + _save_raw(tasks) + return task + raise KeyError(f"Task {task_id} not found.") + + +def delete_task(task_id: int) -> dict: + """Delete a task by ID. Returns the deleted task.""" + tasks = _load_raw() + for i, task in enumerate(tasks): + if task["id"] == task_id: + deleted = tasks.pop(i) + _save_raw(tasks) + return deleted + raise KeyError(f"Task {task_id} not found.") + + +def filter_tasks(tasks: list[dict], status: str) -> list[dict]: + """Filter tasks by status: 'all', 'pending', or 'completed'.""" + if status == "all": + return tasks + if status == "pending": + return [t for t in tasks if not t["completed"]] + if status == "completed": + return [t for t in tasks if t["completed"]] + raise ValueError(f"Unknown status filter '{status}'. Use: all, pending, completed.") diff --git a/gemini-cli-vs-claude-code/claude_code_run_2/test_todo.py b/gemini-cli-vs-claude-code/claude_code_run_2/test_todo.py new file mode 100644 index 0000000000..79dc8f31d5 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_2/test_todo.py @@ -0,0 +1,202 @@ +"""Unit tests for todo.py.""" + +import json +import unittest +from pathlib import Path +from unittest.mock import patch + +import todo + + +def _make_tasks(*titles: str) -> list[dict]: + """Helper: build an in-memory task list.""" + return [ + { + "id": i + 1, + "title": t, + "completed": False, + "created_at": "2026-03-24T10:00:00", + } + for i, t in enumerate(titles) + ] + + +class TestStorage(unittest.TestCase): + """Tests for _load / _save / _next_id / _find.""" + + def setUp(self): + self.path = Path("test_tasks_tmp.json") + patcher = patch.object(todo, "STORAGE_FILE", self.path) + patcher.start() + self.addCleanup(patcher.stop) + self.addCleanup(self._cleanup) + + def _cleanup(self): + if self.path.exists(): + self.path.unlink() + + def test_load_returns_empty_list_when_file_missing(self): + self.assertFalse(self.path.exists()) + self.assertEqual(todo._load(), []) + + def test_save_and_load_roundtrip(self): + tasks = _make_tasks("Task A", "Task B") + todo._save(tasks) + loaded = todo._load() + self.assertEqual(len(loaded), 2) + self.assertEqual(loaded[0]["title"], "Task A") + self.assertEqual(loaded[1]["title"], "Task B") + + def test_load_raises_on_corrupt_json(self): + self.path.write_text("not-json", encoding="utf-8") + with self.assertRaises(SystemExit) as ctx: + todo._load() + self.assertIn("Could not parse", str(ctx.exception)) + + def test_load_raises_on_non_list_json(self): + self.path.write_text('{"key": "value"}', encoding="utf-8") + with self.assertRaises(SystemExit) as ctx: + todo._load() + self.assertIn("Corrupt", str(ctx.exception)) + + def test_next_id_on_empty(self): + self.assertEqual(todo._next_id([]), 1) + + def test_next_id_increments(self): + tasks = _make_tasks("A", "B", "C") + self.assertEqual(todo._next_id(tasks), 4) + + def test_find_returns_task(self): + tasks = _make_tasks("X", "Y") + self.assertEqual(todo._find(tasks, 2)["title"], "Y") + + def test_find_returns_none_for_missing(self): + tasks = _make_tasks("X") + self.assertIsNone(todo._find(tasks, 99)) + + +class TestCmdAdd(unittest.TestCase): + def setUp(self): + self.path = Path("test_tasks_tmp.json") + patcher = patch.object(todo, "STORAGE_FILE", self.path) + patcher.start() + self.addCleanup(patcher.stop) + self.addCleanup(lambda: self.path.exists() and self.path.unlink()) + + def test_add_creates_task(self): + todo.main(["add", "Buy milk"]) + tasks = todo._load() + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0]["title"], "Buy milk") + self.assertFalse(tasks[0]["completed"]) + self.assertEqual(tasks[0]["id"], 1) + + def test_add_multiple_tasks_increments_ids(self): + todo.main(["add", "Task 1"]) + todo.main(["add", "Task 2"]) + tasks = todo._load() + self.assertEqual(tasks[0]["id"], 1) + self.assertEqual(tasks[1]["id"], 2) + + def test_add_empty_title_raises(self): + with self.assertRaises(SystemExit) as ctx: + todo.main(["add", " "]) + self.assertIn("empty", str(ctx.exception)) + + +class TestCmdDone(unittest.TestCase): + def setUp(self): + self.path = Path("test_tasks_tmp.json") + patcher = patch.object(todo, "STORAGE_FILE", self.path) + patcher.start() + self.addCleanup(patcher.stop) + self.addCleanup(lambda: self.path.exists() and self.path.unlink()) + todo._save(_make_tasks("Task A", "Task B")) + + def test_mark_done(self): + todo.main(["done", "1"]) + tasks = todo._load() + self.assertTrue(tasks[0]["completed"]) + self.assertFalse(tasks[1]["completed"]) + + def test_done_invalid_id_raises(self): + with self.assertRaises(SystemExit) as ctx: + todo.main(["done", "99"]) + self.assertIn("No task with id 99", str(ctx.exception)) + + def test_done_already_completed_is_idempotent(self): + todo.main(["done", "1"]) + todo.main(["done", "1"]) # should not raise + tasks = todo._load() + self.assertTrue(tasks[0]["completed"]) + + +class TestCmdDelete(unittest.TestCase): + def setUp(self): + self.path = Path("test_tasks_tmp.json") + patcher = patch.object(todo, "STORAGE_FILE", self.path) + patcher.start() + self.addCleanup(patcher.stop) + self.addCleanup(lambda: self.path.exists() and self.path.unlink()) + todo._save(_make_tasks("Task A", "Task B", "Task C")) + + def test_delete_removes_task(self): + todo.main(["delete", "2"]) + tasks = todo._load() + self.assertEqual(len(tasks), 2) + self.assertIsNone(todo._find(tasks, 2)) + + def test_delete_invalid_id_raises(self): + with self.assertRaises(SystemExit) as ctx: + todo.main(["delete", "99"]) + self.assertIn("No task with id 99", str(ctx.exception)) + + +class TestCmdList(unittest.TestCase): + def setUp(self): + self.path = Path("test_tasks_tmp.json") + patcher = patch.object(todo, "STORAGE_FILE", self.path) + patcher.start() + self.addCleanup(patcher.stop) + self.addCleanup(lambda: self.path.exists() and self.path.unlink()) + + tasks = _make_tasks("Task A", "Task B", "Task C") + tasks[0]["completed"] = True # Task A is done + todo._save(tasks) + + def test_list_all(self): + # Should not raise + todo.main(["list"]) + + def test_list_completed_filter(self, ): + # Should not raise; only task A is completed + todo.main(["list", "--filter", "completed"]) + + def test_list_pending_filter(self): + todo.main(["list", "--filter", "pending"]) + + def test_list_empty_shows_message(self): + todo._save([]) + with patch("builtins.print") as mock_print: + todo.main(["list"]) + printed = " ".join(str(c) for c in mock_print.call_args_list) + self.assertIn("No", printed) + + def test_list_completed_filter_content(self): + with patch("builtins.print") as mock_print: + todo.main(["list", "--filter", "completed"]) + output = " ".join(str(c) for c in mock_print.call_args_list) + self.assertIn("Task A", output) + self.assertNotIn("Task B", output) + + def test_list_pending_filter_content(self): + with patch("builtins.print") as mock_print: + todo.main(["list", "--filter", "pending"]) + output = " ".join(str(c) for c in mock_print.call_args_list) + self.assertNotIn("Task A", output) + self.assertIn("Task B", output) + self.assertIn("Task C", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/gemini-cli-vs-claude-code/claude_code_run_2/todo.py b/gemini-cli-vs-claude-code/claude_code_run_2/todo.py new file mode 100644 index 0000000000..c4f6101314 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_2/todo.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""CLI-based mini to-do application with JSON persistence.""" + +import argparse +import json +import sys +from datetime import datetime +from pathlib import Path + +STORAGE_FILE = Path("tasks.json") + + +# --------------------------------------------------------------------------- +# Storage +# --------------------------------------------------------------------------- + +def _load() -> list[dict]: + if not STORAGE_FILE.exists(): + return [] + try: + with STORAGE_FILE.open("r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, list): + raise SystemExit(f"Error: Corrupt tasks file: {STORAGE_FILE} must contain a JSON array.") + return data + except json.JSONDecodeError as e: + raise SystemExit(f"Error: Could not parse {STORAGE_FILE}: {e}") from e + + +def _save(tasks: list[dict]) -> None: + try: + with STORAGE_FILE.open("w", encoding="utf-8") as f: + json.dump(tasks, f, indent=2) + except OSError as e: + raise SystemExit(f"Error: Could not write to {STORAGE_FILE}: {e}") from e + + +def _next_id(tasks: list[dict]) -> int: + return max((t["id"] for t in tasks), default=0) + 1 + + +def _find(tasks: list[dict], task_id: int) -> dict | None: + return next((t for t in tasks if t["id"] == task_id), None) + + +# --------------------------------------------------------------------------- +# Commands +# --------------------------------------------------------------------------- + +def cmd_add(args: argparse.Namespace) -> None: + title = args.title.strip() + if not title: + raise SystemExit("Error: Task title cannot be empty.") + + tasks = _load() + task = { + "id": _next_id(tasks), + "title": title, + "completed": False, + "created_at": datetime.now().isoformat(timespec="seconds"), + } + tasks.append(task) + _save(tasks) + print(f"Added task #{task['id']}: {task['title']}") + + +def cmd_done(args: argparse.Namespace) -> None: + tasks = _load() + task = _find(tasks, args.id) + if task is None: + raise SystemExit(f"Error: No task with id {args.id}.") + if task["completed"]: + print(f"Task #{args.id} is already marked as completed.") + return + task["completed"] = True + _save(tasks) + print(f"Marked task #{args.id} as completed: {task['title']}") + + +def cmd_list(args: argparse.Namespace) -> None: + tasks = _load() + filter_by = args.filter + + if filter_by == "completed": + filtered = [t for t in tasks if t["completed"]] + elif filter_by == "pending": + filtered = [t for t in tasks if not t["completed"]] + else: + filtered = tasks + + if not filtered: + label = "" if filter_by == "all" else f"{filter_by} " + print(f"No {label}tasks found.") + return + + print(f"\n{'ID':<5} {'Status':<12} {'Created':<22} Title") + print("-" * 65) + for t in filtered: + status = "done" if t["completed"] else "pending" + created = t.get("created_at", "")[:19] + print(f"{t['id']:<5} {status:<12} {created:<22} {t['title']}") + print() + + +def cmd_delete(args: argparse.Namespace) -> None: + tasks = _load() + task = _find(tasks, args.id) + if task is None: + raise SystemExit(f"Error: No task with id {args.id}.") + tasks.remove(task) + _save(tasks) + print(f"Deleted task #{args.id}: {task['title']}") + + +# --------------------------------------------------------------------------- +# CLI wiring +# --------------------------------------------------------------------------- + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="todo", + description="A simple CLI to-do manager.", + ) + sub = parser.add_subparsers(dest="command", metavar="COMMAND") + sub.required = True + + # add + p_add = sub.add_parser("add", help="Add a new task") + p_add.add_argument("title", help="Task description") + p_add.set_defaults(func=cmd_add) + + # done + p_done = sub.add_parser("done", help="Mark a task as completed") + p_done.add_argument("id", type=int, help="Task ID") + p_done.set_defaults(func=cmd_done) + + # list + p_list = sub.add_parser("list", help="List tasks") + p_list.add_argument( + "--filter", + choices=["all", "completed", "pending"], + default="all", + help="Filter tasks (default: all)", + ) + p_list.set_defaults(func=cmd_list) + + # delete + p_del = sub.add_parser("delete", help="Delete a task") + p_del.add_argument("id", type=int, help="Task ID") + p_del.set_defaults(func=cmd_delete) + + return parser + + +def main(argv: list[str] | None = None) -> None: + parser = build_parser() + args = parser.parse_args(argv) + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/gemini-cli-vs-claude-code/claude_code_run_3/tasks.json b/gemini-cli-vs-claude-code/claude_code_run_3/tasks.json new file mode 100644 index 0000000000..8d9e81e718 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_3/tasks.json @@ -0,0 +1,8 @@ +[ + { + "id": "9c23b59f", + "title": "Buy groceries", + "done": false, + "created_at": "2026-03-24T16:03:06" + } +] \ No newline at end of file diff --git a/gemini-cli-vs-claude-code/claude_code_run_3/test_todo.py b/gemini-cli-vs-claude-code/claude_code_run_3/test_todo.py new file mode 100644 index 0000000000..297ab7af34 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_3/test_todo.py @@ -0,0 +1,224 @@ +"""Unit tests for the to-do CLI application.""" + +import argparse +import json +import os +import tempfile +import unittest +from unittest.mock import patch + +from todo_storage import load_tasks, make_task, save_tasks +from todo import cmd_add, cmd_clear, cmd_delete, cmd_done, cmd_list + + +# ── helpers ─────────────────────────────────────────────────────────────────── + +def _args(**kwargs) -> argparse.Namespace: + """Build a minimal Namespace for command handlers.""" + defaults = {"file": None} # file is always set in tests via tmp_file + defaults.update(kwargs) + return argparse.Namespace(**defaults) + + +class TmpFileMixin(unittest.TestCase): + """Creates a fresh temp JSON file before each test and removes it after.""" + + def setUp(self): + fd, self.tmp_file = tempfile.mkstemp(suffix=".json") + os.close(fd) + os.remove(self.tmp_file) # start with no file (tests creation from scratch) + + def tearDown(self): + if os.path.exists(self.tmp_file): + os.remove(self.tmp_file) + + +# ── storage layer tests ─────────────────────────────────────────────────────── + +class TestStorage(TmpFileMixin): + + def test_load_missing_file_returns_empty(self): + tasks = load_tasks(self.tmp_file) + self.assertEqual(tasks, []) + + def test_save_and_reload(self): + task = make_task("Hello world") + save_tasks([task], self.tmp_file) + loaded = load_tasks(self.tmp_file) + self.assertEqual(len(loaded), 1) + self.assertEqual(loaded[0]["title"], "Hello world") + self.assertFalse(loaded[0]["done"]) + + def test_make_task_strips_whitespace(self): + task = make_task(" trim me ") + self.assertEqual(task["title"], "trim me") + + def test_make_task_has_required_keys(self): + task = make_task("x") + for key in ("id", "title", "done", "created_at"): + self.assertIn(key, task) + + def test_load_corrupt_json_raises(self): + with open(self.tmp_file, "w") as f: + f.write("{not valid json}") + with self.assertRaises(ValueError): + load_tasks(self.tmp_file) + + def test_load_wrong_type_raises(self): + with open(self.tmp_file, "w") as f: + json.dump({"oops": "a dict"}, f) + with self.assertRaises(ValueError): + load_tasks(self.tmp_file) + + +# ── add command ─────────────────────────────────────────────────────────────── + +class TestCmdAdd(TmpFileMixin): + + def test_add_creates_task(self): + args = _args(title=["Buy", "milk"], file=self.tmp_file) + cmd_add(args) + tasks = load_tasks(self.tmp_file) + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0]["title"], "Buy milk") + + def test_add_multiple_tasks(self): + for title in ["Task A", "Task B", "Task C"]: + cmd_add(_args(title=[title], file=self.tmp_file)) + tasks = load_tasks(self.tmp_file) + self.assertEqual(len(tasks), 3) + + def test_add_empty_title_exits(self): + args = _args(title=[" "], file=self.tmp_file) + with self.assertRaises(SystemExit): + cmd_add(args) + + def test_add_new_task_is_pending(self): + cmd_add(_args(title=["Check this"], file=self.tmp_file)) + tasks = load_tasks(self.tmp_file) + self.assertFalse(tasks[0]["done"]) + + +# ── done command ────────────────────────────────────────────────────────────── + +class TestCmdDone(TmpFileMixin): + + def _seed(self, *titles): + tasks = [make_task(t) for t in titles] + save_tasks(tasks, self.tmp_file) + return tasks + + def test_mark_done(self): + tasks = self._seed("Read book") + task_id = tasks[0]["id"] + cmd_done(_args(id=task_id, file=self.tmp_file)) + loaded = load_tasks(self.tmp_file) + self.assertTrue(loaded[0]["done"]) + + def test_mark_done_invalid_id_exits(self): + self._seed("Read book") + with self.assertRaises(SystemExit): + cmd_done(_args(id="no-such", file=self.tmp_file)) + + def test_mark_already_done_is_idempotent(self): + tasks = self._seed("Read book") + tid = tasks[0]["id"] + cmd_done(_args(id=tid, file=self.tmp_file)) + cmd_done(_args(id=tid, file=self.tmp_file)) # second call — should not crash + loaded = load_tasks(self.tmp_file) + self.assertTrue(loaded[0]["done"]) + + +# ── delete command ──────────────────────────────────────────────────────────── + +class TestCmdDelete(TmpFileMixin): + + def test_delete_removes_task(self): + task = make_task("Temp task") + save_tasks([task], self.tmp_file) + cmd_delete(_args(id=task["id"], file=self.tmp_file)) + tasks = load_tasks(self.tmp_file) + self.assertEqual(tasks, []) + + def test_delete_nonexistent_exits(self): + save_tasks([], self.tmp_file) + with self.assertRaises(SystemExit): + cmd_delete(_args(id="ghost", file=self.tmp_file)) + + def test_delete_leaves_others_intact(self): + a, b = make_task("Keep"), make_task("Remove") + save_tasks([a, b], self.tmp_file) + cmd_delete(_args(id=b["id"], file=self.tmp_file)) + tasks = load_tasks(self.tmp_file) + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0]["id"], a["id"]) + + +# ── list command ────────────────────────────────────────────────────────────── + +class TestCmdList(TmpFileMixin): + + def _seed_mixed(self): + tasks = [make_task("Pending A"), make_task("Done B"), make_task("Pending C")] + tasks[1]["done"] = True + save_tasks(tasks, self.tmp_file) + return tasks + + def test_list_all(self): + self._seed_mixed() + with patch("builtins.print") as mock_print: + cmd_list(_args(filter="all", file=self.tmp_file)) + output = " ".join(str(c) for call in mock_print.call_args_list for c in call[0]) + self.assertIn("Done B", output) + self.assertIn("Pending A", output) + + def test_list_filter_done(self): + self._seed_mixed() + with patch("builtins.print") as mock_print: + cmd_list(_args(filter="done", file=self.tmp_file)) + output = " ".join(str(c) for call in mock_print.call_args_list for c in call[0]) + self.assertIn("Done B", output) + self.assertNotIn("Pending A", output) + + def test_list_filter_pending(self): + self._seed_mixed() + with patch("builtins.print") as mock_print: + cmd_list(_args(filter="pending", file=self.tmp_file)) + output = " ".join(str(c) for call in mock_print.call_args_list for c in call[0]) + self.assertIn("Pending A", output) + self.assertNotIn("Done B", output) + + def test_list_empty(self): + with patch("builtins.print") as mock_print: + cmd_list(_args(filter="all", file=self.tmp_file)) + output = " ".join(str(c) for call in mock_print.call_args_list for c in call[0]) + self.assertIn("No tasks", output) + + +# ── clear command ───────────────────────────────────────────────────────────── + +class TestCmdClear(TmpFileMixin): + + def test_clear_removes_done_tasks(self): + tasks = [make_task("Keep"), make_task("Remove")] + tasks[1]["done"] = True + save_tasks(tasks, self.tmp_file) + cmd_clear(_args(file=self.tmp_file)) + remaining = load_tasks(self.tmp_file) + self.assertEqual(len(remaining), 1) + self.assertEqual(remaining[0]["title"], "Keep") + + def test_clear_no_done_tasks(self): + save_tasks([make_task("Still here")], self.tmp_file) + with patch("builtins.print") as mock_print: + cmd_clear(_args(file=self.tmp_file)) + output = " ".join(str(c) for call in mock_print.call_args_list for c in call[0]) + self.assertIn("No completed", output) + + def test_clear_empty_list(self): + # should not crash on an empty list + cmd_clear(_args(file=self.tmp_file)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/gemini-cli-vs-claude-code/claude_code_run_3/todo.py b/gemini-cli-vs-claude-code/claude_code_run_3/todo.py new file mode 100644 index 0000000000..94d7856034 --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_3/todo.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +todo — a minimal CLI to-do manager. + +Usage: + python todo.py add "Buy groceries" + python todo.py list + python todo.py list --filter pending + python todo.py list --filter done + python todo.py done + python todo.py delete + python todo.py clear +""" + +import argparse +import sys + +from todo_storage import TASKS_FILE, load_tasks, make_task, save_tasks + +# ── helpers ────────────────────────────────────────────────────────────────── + +GREEN = "\033[32m" +YELLOW = "\033[33m" +RED = "\033[31m" +RESET = "\033[0m" +DIM = "\033[2m" + + +def _find_task(tasks: list, task_id: str) -> dict: + matches = [t for t in tasks if t["id"] == task_id] + if not matches: + raise KeyError(f"No task found with id '{task_id}'") + return matches[0] + + +def _status(task: dict) -> str: + return f"{GREEN}x{RESET}" if task["done"] else f"{YELLOW}-{RESET}" + + +def _fmt(task: dict) -> str: + title = task["title"] + if task["done"]: + title = f"{DIM}{title}{RESET}" + return f" [{_status(task)}] {task['id']} {title} {DIM}({task['created_at']}){RESET}" + + +# ── sub-commands ────────────────────────────────────────────────────────────── + +def cmd_add(args): + title = " ".join(args.title).strip() + if not title: + print(f"{RED}Error:{RESET} Task title cannot be empty.", file=sys.stderr) + sys.exit(1) + + tasks = load_tasks(args.file) + task = make_task(title) + tasks.append(task) + save_tasks(tasks, args.file) + print(f"Added [{task['id']}] {task['title']}") + + +def cmd_list(args): + tasks = load_tasks(args.file) + + filter_val = args.filter + if filter_val == "done": + visible = [t for t in tasks if t["done"]] + elif filter_val == "pending": + visible = [t for t in tasks if not t["done"]] + else: + visible = tasks + + if not visible: + label = f" ({filter_val})" if filter_val else "" + print(f"No tasks{label}.") + return + + total = len(tasks) + done = sum(1 for t in tasks if t["done"]) + pending = total - done + print(f"Tasks: {GREEN}{done} done{RESET}, {YELLOW}{pending} pending{RESET}, {total} total") + print() + for task in visible: + print(_fmt(task)) + + +def cmd_done(args): + tasks = load_tasks(args.file) + try: + task = _find_task(tasks, args.id) + except KeyError as e: + print(f"{RED}Error:{RESET} {e}", file=sys.stderr) + sys.exit(1) + + if task["done"]: + print(f"Task [{args.id}] is already marked as done.") + return + + task["done"] = True + save_tasks(tasks, args.file) + print(f"{GREEN}x{RESET} Marked [{args.id}] as done: {task['title']}") + + +def cmd_delete(args): + tasks = load_tasks(args.file) + try: + task = _find_task(tasks, args.id) + except KeyError as e: + print(f"{RED}Error:{RESET} {e}", file=sys.stderr) + sys.exit(1) + + tasks.remove(task) + save_tasks(tasks, args.file) + print(f"{RED}!{RESET} Deleted [{args.id}] {task['title']}") + + +def cmd_clear(args): + tasks = load_tasks(args.file) + done_tasks = [t for t in tasks if t["done"]] + if not done_tasks: + print("No completed tasks to clear.") + return + remaining = [t for t in tasks if not t["done"]] + save_tasks(remaining, args.file) + print(f"Cleared {len(done_tasks)} completed task(s).") + + +# ── argument parser ─────────────────────────────────────────────────────────── + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="todo", + description="A minimal CLI to-do manager.", + ) + parser.add_argument( + "--file", default=TASKS_FILE, metavar="PATH", + help=f"Path to the tasks JSON file (default: {TASKS_FILE})", + ) + + sub = parser.add_subparsers(dest="command", metavar="") + sub.required = True + + # add + p_add = sub.add_parser("add", help="Add a new task") + p_add.add_argument("title", nargs="+", help="Task title (words joined automatically)") + p_add.set_defaults(func=cmd_add) + + # list + p_list = sub.add_parser("list", help="List tasks") + p_list.add_argument( + "--filter", choices=["all", "done", "pending"], default="all", + help="Filter tasks (default: all)", + ) + p_list.set_defaults(func=cmd_list) + + # done + p_done = sub.add_parser("done", help="Mark a task as completed") + p_done.add_argument("id", help="Task ID") + p_done.set_defaults(func=cmd_done) + + # delete + p_del = sub.add_parser("delete", help="Delete a task") + p_del.add_argument("id", help="Task ID") + p_del.set_defaults(func=cmd_delete) + + # clear + p_clear = sub.add_parser("clear", help="Remove all completed tasks") + p_clear.set_defaults(func=cmd_clear) + + return parser + + +def main(): + parser = build_parser() + args = parser.parse_args() + try: + args.func(args) + except ValueError as e: + # e.g. corrupt JSON file + print(f"{RED}Error:{RESET} {e}", file=sys.stderr) + sys.exit(1) + except OSError as e: + print(f"{RED}Error:{RESET} Could not read/write tasks file: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/gemini-cli-vs-claude-code/claude_code_run_3/todo_storage.py b/gemini-cli-vs-claude-code/claude_code_run_3/todo_storage.py new file mode 100644 index 0000000000..5216f7256a --- /dev/null +++ b/gemini-cli-vs-claude-code/claude_code_run_3/todo_storage.py @@ -0,0 +1,45 @@ +"""Persistence layer for the to-do app — reads/writes tasks to a JSON file.""" + +import json +import os +import uuid +from datetime import datetime +from typing import List, Optional + + +TASKS_FILE = "tasks.json" + + +def _load_raw(filepath: str) -> list: + if not os.path.exists(filepath): + return [] + with open(filepath, "r", encoding="utf-8") as f: + try: + data = json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"Corrupt tasks file '{filepath}': {e}") from e + if not isinstance(data, list): + raise ValueError(f"Expected a list in '{filepath}', got {type(data).__name__}") + return data + + +def _save_raw(filepath: str, tasks: list) -> None: + with open(filepath, "w", encoding="utf-8") as f: + json.dump(tasks, f, indent=2) + + +def load_tasks(filepath: str = TASKS_FILE) -> List[dict]: + return _load_raw(filepath) + + +def save_tasks(tasks: List[dict], filepath: str = TASKS_FILE) -> None: + _save_raw(filepath, tasks) + + +def make_task(title: str) -> dict: + return { + "id": str(uuid.uuid4())[:8], + "title": title.strip(), + "done": False, + "created_at": datetime.now().isoformat(timespec="seconds"), + } diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_1/tasks.json b/gemini-cli-vs-claude-code/gemini_cli_run_1/tasks.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_1/tasks.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_1/test_todo.py b/gemini-cli-vs-claude-code/gemini_cli_run_1/test_todo.py new file mode 100644 index 0000000000..85f9553b56 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_1/test_todo.py @@ -0,0 +1,66 @@ +import unittest +import os +import json +from todo import TodoManager + +class TestTodoManager(unittest.TestCase): + def setUp(self): + self.test_file = 'test_tasks.json' + # Ensure a clean slate for each test + if os.path.exists(self.test_file): + os.remove(self.test_file) + self.manager = TodoManager(filename=self.test_file) + + def tearDown(self): + if os.path.exists(self.test_file): + os.remove(self.test_file) + + def test_add_task(self): + task = self.manager.add_task("Test task") + self.assertEqual(task['description'], "Test task") + self.assertEqual(task['id'], 1) + self.assertFalse(task['completed']) + self.assertEqual(len(self.manager.tasks), 1) + + def test_complete_task(self): + self.manager.add_task("Test task") + success = self.manager.complete_task(1) + self.assertTrue(success) + self.assertTrue(self.manager.tasks[0]['completed']) + + def test_complete_non_existent_task(self): + success = self.manager.complete_task(999) + self.assertFalse(success) + + def test_delete_task(self): + self.manager.add_task("Task 1") + self.manager.add_task("Task 2") + success = self.manager.delete_task(1) + self.assertTrue(success) + self.assertEqual(len(self.manager.tasks), 1) + self.assertEqual(self.manager.tasks[0]['id'], 2) + + def test_list_tasks_filtering(self): + self.manager.add_task("Pending task") + self.manager.add_task("Completed task") + self.manager.complete_task(2) + + all_tasks = self.manager.list_tasks() + pending_tasks = self.manager.list_tasks(filter_status='pending') + completed_tasks = self.manager.list_tasks(filter_status='completed') + + self.assertEqual(len(all_tasks), 2) + self.assertEqual(len(pending_tasks), 1) + self.assertEqual(len(completed_tasks), 1) + self.assertEqual(pending_tasks[0]['description'], "Pending task") + self.assertEqual(completed_tasks[0]['description'], "Completed task") + + def test_persistence(self): + self.manager.add_task("Persistent task") + # Create a new manager instance pointing to the same file + new_manager = TodoManager(filename=self.test_file) + self.assertEqual(len(new_manager.tasks), 1) + self.assertEqual(new_manager.tasks[0]['description'], "Persistent task") + +if __name__ == '__main__': + unittest.main() diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_1/todo.py b/gemini-cli-vs-claude-code/gemini_cli_run_1/todo.py new file mode 100644 index 0000000000..0b39efb0f9 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_1/todo.py @@ -0,0 +1,120 @@ +import argparse +import json +import os +import sys + +class TodoManager: + def __init__(self, filename='tasks.json'): + self.filename = filename + self.tasks = self.load_tasks() + + def load_tasks(self): + if not os.path.exists(self.filename): + return [] + try: + with open(self.filename, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + print(f"Error loading tasks: {e}", file=sys.stderr) + return [] + + def save_tasks(self): + try: + with open(self.filename, 'w') as f: + json.dump(self.tasks, f, indent=4) + except IOError as e: + print(f"Error saving tasks: {e}", file=sys.stderr) + + def add_task(self, description): + task_id = 1 + if self.tasks: + task_id = max(task['id'] for task in self.tasks) + 1 + + new_task = { + 'id': task_id, + 'description': description, + 'completed': False + } + self.tasks.append(new_task) + self.save_tasks() + return new_task + + def complete_task(self, task_id): + for task in self.tasks: + if task['id'] == task_id: + task['completed'] = True + self.save_tasks() + return True + return False + + def list_tasks(self, filter_status=None): + if filter_status == 'completed': + return [t for t in self.tasks if t['completed']] + elif filter_status == 'pending': + return [t for t in self.tasks if not t['completed']] + return self.tasks + + def delete_task(self, task_id): + initial_count = len(self.tasks) + self.tasks = [t for t in self.tasks if t['id'] != task_id] + if len(self.tasks) < initial_count: + self.save_tasks() + return True + return False + +def main(): + parser = argparse.ArgumentParser(description="CLI To-Do Application") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Add command + add_parser = subparsers.add_parser("add", help="Add a new task") + add_parser.add_argument("description", help="Description of the task") + + # Complete command + complete_parser = subparsers.add_parser("complete", help="Mark a task as completed") + complete_parser.add_argument("id", type=int, help="ID of the task to complete") + + # List command + list_parser = subparsers.add_parser("list", help="List tasks") + list_parser.add_argument("--filter", choices=["all", "completed", "pending"], default="all", help="Filter tasks by status") + + # Delete command + delete_parser = subparsers.add_parser("delete", help="Delete a task") + delete_parser.add_argument("id", type=int, help="ID of the task to delete") + + args = parser.parse_args() + manager = TodoManager() + + if args.command == "add": + task = manager.add_task(args.description) + print(f"Added task: [{task['id']}] {task['description']}") + + elif args.command == "complete": + if manager.complete_task(args.id): + print(f"Marked task {args.id} as completed.") + else: + print(f"Error: Task {args.id} not found.", file=sys.stderr) + + elif args.command == "list": + filter_status = args.filter if args.filter != "all" else None + tasks = manager.list_tasks(filter_status) + if not tasks: + print("No tasks found.") + else: + print(f"{'ID':<5} {'Status':<10} {'Description'}") + print("-" * 30) + for task in tasks: + status = "Done" if task['completed'] else "Pending" + print(f"{task['id']:<5} {status:<10} {task['description']}") + + elif args.command == "delete": + if manager.delete_task(args.id): + print(f"Deleted task {args.id}.") + else: + print(f"Error: Task {args.id} not found.", file=sys.stderr) + + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_2/cli.py b/gemini-cli-vs-claude-code/gemini_cli_run_2/cli.py new file mode 100644 index 0000000000..21582c759d --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_2/cli.py @@ -0,0 +1,57 @@ +import argparse +from todo_manager import TodoManager + +def main(): + manager = TodoManager() + parser = argparse.ArgumentParser(description="Mini To-Do CLI Application") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Add command + add_parser = subparsers.add_parser("add", help="Add a new task") + add_parser.add_argument("description", type=str, help="Task description") + + # List command + list_parser = subparsers.add_parser("list", help="List all tasks") + list_parser.add_argument("--filter", choices=["all", "completed", "pending"], default="all", help="Filter tasks") + + # Complete command + complete_parser = subparsers.add_parser("complete", help="Mark a task as completed") + complete_parser.add_argument("id", type=int, help="Task ID") + + # Delete command + delete_parser = subparsers.add_parser("delete", help="Delete a task") + delete_parser.add_argument("id", type=int, help="Task ID") + + args = parser.parse_args() + + if args.command == "add": + task = manager.add_task(args.description) + print(f"Task added: [{task.id}] {task.description}") + + elif args.command == "list": + tasks = manager.list_tasks(args.filter) + if not tasks: + print(f"No {args.filter} tasks found.") + else: + print(f"{args.filter.capitalize()} Tasks:") + for task in tasks: + status = "[x]" if task.completed else "[ ]" + print(f"{task.id:3}: {status} {task.description}") + + elif args.command == "complete": + if manager.mark_completed(args.id): + print(f"Task {args.id} marked as completed.") + else: + print(f"Error: Task {args.id} not found.") + + elif args.command == "delete": + if manager.delete_task(args.id): + print(f"Task {args.id} deleted.") + else: + print(f"Error: Task {args.id} not found.") + + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_2/main.py b/gemini-cli-vs-claude-code/gemini_cli_run_2/main.py new file mode 100644 index 0000000000..2aa71401cd --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_2/main.py @@ -0,0 +1,4 @@ +from cli import main + +if __name__ == "__main__": + main() diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_2/storage.py b/gemini-cli-vs-claude-code/gemini_cli_run_2/storage.py new file mode 100644 index 0000000000..0d5e56cf9d --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_2/storage.py @@ -0,0 +1,25 @@ +import json +import os + +class StorageHandler: + def __init__(self, file_path='tasks.json'): + self.file_path = file_path + + def load_tasks(self): + """Load tasks from a JSON file. Returns an empty list if file doesn't exist.""" + if not os.path.exists(self.file_path): + return [] + try: + with open(self.file_path, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + # Return empty list if the file is corrupted or can't be read. + return [] + + def save_tasks(self, tasks): + """Save a list of tasks to a JSON file.""" + try: + with open(self.file_path, 'w') as f: + json.dump(tasks, f, indent=4) + except IOError as e: + print(f"Error saving tasks: {e}") diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_2/tasks.json b/gemini-cli-vs-claude-code/gemini_cli_run_2/tasks.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_2/tasks.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_2/test_todo.py b/gemini-cli-vs-claude-code/gemini_cli_run_2/test_todo.py new file mode 100644 index 0000000000..d104d0a964 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_2/test_todo.py @@ -0,0 +1,56 @@ +import unittest +from unittest.mock import MagicMock +from todo_manager import TodoManager, Task + +class TestTodoManager(unittest.TestCase): + def setUp(self): + self.mock_storage = MagicMock() + self.mock_storage.load_tasks.return_value = [] + self.manager = TodoManager(storage=self.mock_storage) + + def test_add_task(self): + task = self.manager.add_task("Test task") + self.assertEqual(len(self.manager.tasks), 1) + self.assertEqual(task.description, "Test task") + self.assertEqual(task.id, 1) + self.mock_storage.save_tasks.assert_called() + + def test_mark_completed(self): + self.manager.add_task("Test task") + result = self.manager.mark_completed(1) + self.assertTrue(result) + self.assertTrue(self.manager.tasks[0].completed) + self.mock_storage.save_tasks.assert_called() + + def test_mark_completed_invalid_id(self): + result = self.manager.mark_completed(99) + self.assertFalse(result) + + def test_delete_task(self): + self.manager.add_task("Test task") + result = self.manager.delete_task(1) + self.assertTrue(result) + self.assertEqual(len(self.manager.tasks), 0) + self.mock_storage.save_tasks.assert_called() + + def test_delete_task_invalid_id(self): + result = self.manager.delete_task(99) + self.assertFalse(result) + + def test_list_tasks_filter(self): + self.manager.add_task("Pending task") + task2 = self.manager.add_task("Completed task") + self.manager.mark_completed(task2.id) + + pending_tasks = self.manager.list_tasks(filter_type='pending') + completed_tasks = self.manager.list_tasks(filter_type='completed') + all_tasks = self.manager.list_tasks(filter_type='all') + + self.assertEqual(len(pending_tasks), 1) + self.assertEqual(pending_tasks[0].description, "Pending task") + self.assertEqual(len(completed_tasks), 1) + self.assertEqual(completed_tasks[0].description, "Completed task") + self.assertEqual(len(all_tasks), 2) + +if __name__ == "__main__": + unittest.main() diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_2/todo_manager.py b/gemini-cli-vs-claude-code/gemini_cli_run_2/todo_manager.py new file mode 100644 index 0000000000..360f36ee73 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_2/todo_manager.py @@ -0,0 +1,61 @@ +from storage import StorageHandler + +class Task: + def __init__(self, id, description, completed=False): + self.id = id + self.description = description + self.completed = completed + + def to_dict(self): + return { + 'id': self.id, + 'description': self.description, + 'completed': self.completed + } + + @classmethod + def from_dict(cls, data): + return cls(data['id'], data['description'], data['completed']) + +class TodoManager: + def __init__(self, storage=None): + self.storage = storage or StorageHandler() + self.tasks = [Task.from_dict(t) for t in self.storage.load_tasks()] + + def add_task(self, description): + """Add a new task with a unique ID.""" + new_id = 1 if not self.tasks else max(t.id for t in self.tasks) + 1 + new_task = Task(new_id, description) + self.tasks.append(new_task) + self.save() + return new_task + + def list_tasks(self, filter_type='all'): + """Return tasks based on filter: 'all', 'completed', or 'pending'.""" + if filter_type == 'completed': + return [t for t in self.tasks if t.completed] + elif filter_type == 'pending': + return [t for t in self.tasks if not t.completed] + return self.tasks + + def mark_completed(self, task_id): + """Mark a task as completed by ID. Returns True if task exists, else False.""" + for task in self.tasks: + if task.id == task_id: + task.completed = True + self.save() + return True + return False + + def delete_task(self, task_id): + """Delete a task by ID. Returns True if deleted, else False.""" + initial_len = len(self.tasks) + self.tasks = [t for t in self.tasks if t.id != task_id] + if len(self.tasks) < initial_len: + self.save() + return True + return False + + def save(self): + """Persist current tasks to storage.""" + self.storage.save_tasks([t.to_dict() for t in self.tasks]) diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_3/cli.py b/gemini-cli-vs-claude-code/gemini_cli_run_3/cli.py new file mode 100644 index 0000000000..5c85ca05eb --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_3/cli.py @@ -0,0 +1,77 @@ +import argparse +import sys +from .models import TodoList +from .storage import Storage + +def setup_cli(todo_list: TodoList, storage: Storage): + parser = argparse.ArgumentParser(description="Mini To-Do CLI Application") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # 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") + list_parser.add_argument("--completed", action="store_true", help="List only completed tasks") + list_parser.add_argument("--pending", action="store_true", help="List only pending tasks") + + # Complete command + complete_parser = subparsers.add_parser("complete", help="Mark a task as completed") + complete_parser.add_argument("index", type=int, help="Task index (1-based)") + + # Delete command + delete_parser = subparsers.add_parser("delete", help="Delete a task") + delete_parser.add_argument("index", type=int, help="Task index (1-based)") + + args = parser.parse_args() + + try: + if args.command == "add": + todo_list.add_task(args.description) + storage.save_tasks(todo_list) + print(f"Added task: {args.description}") + + elif args.command == "list": + status = "all" + if args.completed: + status = "completed" + elif args.pending: + status = "pending" + + # Show absolute index even when filtered + tasks_with_index = [(idx, task) for idx, task in enumerate(todo_list.tasks, 1)] + + if status == "completed": + filtered_tasks = [(idx, t) for idx, t in tasks_with_index if t.completed] + elif status == "pending": + filtered_tasks = [(idx, t) for idx, t in tasks_with_index if not t.completed] + else: + filtered_tasks = tasks_with_index + + if not filtered_tasks: + print(f"No {status if status != 'all' else ''} tasks found.") + else: + for idx, task in filtered_tasks: + status_icon = "[x]" if task.completed else "[ ]" + print(f"{idx}. {status_icon} {task.description}") + + elif args.command == "complete": + todo_list.complete_task(args.index - 1) + storage.save_tasks(todo_list) + print(f"Marked task {args.index} as completed.") + + elif args.command == "delete": + todo_list.delete_task(args.index - 1) + storage.save_tasks(todo_list) + print(f"Deleted task {args.index}.") + + else: + parser.print_help() + + except (ValueError, IndexError) as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"An unexpected error occurred: {e}", file=sys.stderr) + sys.exit(1) diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_3/main.py b/gemini-cli-vs-claude-code/gemini_cli_run_3/main.py new file mode 100644 index 0000000000..7474f738df --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_3/main.py @@ -0,0 +1,16 @@ +from todo.models import TodoList +from todo.storage import Storage +from todo.cli import setup_cli + +def main(): + todo_list = TodoList() + storage = Storage() + + # Load existing tasks from storage + storage.load_tasks(todo_list) + + # Run the CLI + setup_cli(todo_list, storage) + +if __name__ == "__main__": + main() diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_3/models.py b/gemini-cli-vs-claude-code/gemini_cli_run_3/models.py new file mode 100644 index 0000000000..b5dcd2b6d1 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_3/models.py @@ -0,0 +1,44 @@ +from typing import List, Dict, Any + +class Task: + def __init__(self, description: str, completed: bool = False): + self.description = description + self.completed = completed + + def to_dict(self) -> Dict[str, Any]: + return { + "description": self.description, + "completed": self.completed + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Task": + return cls(data["description"], data["completed"]) + +class TodoList: + def __init__(self): + self.tasks: List[Task] = [] + + def add_task(self, description: str): + if not description: + raise ValueError("Task description cannot be empty.") + self.tasks.append(Task(description)) + + def complete_task(self, index: int): + if 0 <= index < len(self.tasks): + self.tasks[index].completed = True + else: + raise IndexError("Task index out of range.") + + def delete_task(self, index: int): + if 0 <= index < len(self.tasks): + self.tasks.pop(index) + else: + raise IndexError("Task index out of range.") + + def list_tasks(self, status: str = "all") -> List[Task]: + if status == "completed": + return [t for t in self.tasks if t.completed] + elif status == "pending": + return [t for t in self.tasks if not t.completed] + return self.tasks diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_3/storage.py b/gemini-cli-vs-claude-code/gemini_cli_run_3/storage.py new file mode 100644 index 0000000000..91f48f0443 --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_3/storage.py @@ -0,0 +1,24 @@ +import json +import os +from .models import TodoList, Task + +class Storage: + def __init__(self, file_path: str = "tasks.json"): + self.file_path = file_path + + def load_tasks(self, todo_list: TodoList): + if not os.path.exists(self.file_path): + return + + try: + with open(self.file_path, "r") as f: + data = json.load(f) + todo_list.tasks = [Task.from_dict(t) for t in data] + except (json.JSONDecodeError, KeyError): + # In case of malformed JSON, start fresh + todo_list.tasks = [] + + def save_tasks(self, todo_list: TodoList): + with open(self.file_path, "w") as f: + data = [t.to_dict() for t in todo_list.tasks] + json.dump(data, f, indent=4) diff --git a/gemini-cli-vs-claude-code/gemini_cli_run_3/tasks.json b/gemini-cli-vs-claude-code/gemini_cli_run_3/tasks.json new file mode 100644 index 0000000000..6b9a9a118d --- /dev/null +++ b/gemini-cli-vs-claude-code/gemini_cli_run_3/tasks.json @@ -0,0 +1,10 @@ +[ + { + "description": "Task 2", + "completed": false + }, + { + "description": "Paint a picture", + "completed": true + } +] \ No newline at end of file