Skip to content

Commit 5ba5b5b

Browse files
authored
Merge pull request #751 from realpython/gemini-cli-vs-claude-code
Sample code for the article on Gemini CLI vs Claude Code
2 parents 89ec497 + a311f9a commit 5ba5b5b

File tree

6 files changed

+655
-0
lines changed

6 files changed

+655
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Gemini CLI vs Claude Code: Which to Choose for Python Tasks
2+
3+
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/).
4+
5+
## Important Note
6+
7+
The code in this folder has been modified from the original tutorial version to meet this repository's quality rules. The changes are as follows:
8+
9+
- Used double quotes consistently in string literals
10+
- Removed unused imports
11+
- Reformatted lines longer than 79 characters
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""Unit tests for the to-do application."""
2+
3+
import json
4+
import os
5+
6+
import tempfile
7+
import unittest
8+
from unittest.mock import patch
9+
10+
import todo
11+
12+
# Point store at a temp file for every test
13+
import todo_store as store
14+
15+
16+
class BaseTest(unittest.TestCase):
17+
"""Set up a temporary tasks file for each test."""
18+
19+
def setUp(self):
20+
self._tmp = tempfile.NamedTemporaryFile(
21+
suffix=".json",
22+
delete=False,
23+
mode="w",
24+
)
25+
self._tmp.write("[]")
26+
self._tmp.close()
27+
self._orig = store.TASKS_FILE
28+
store.TASKS_FILE = self._tmp.name
29+
30+
def tearDown(self):
31+
store.TASKS_FILE = self._orig
32+
os.unlink(self._tmp.name)
33+
34+
35+
# ── store tests ─────────────────────────────────────────────────────────────
36+
37+
38+
class TestAddTask(BaseTest):
39+
def test_add_returns_task(self):
40+
task = store.add_task("Buy milk")
41+
self.assertEqual(task["description"], "Buy milk")
42+
self.assertFalse(task["completed"])
43+
self.assertEqual(task["id"], 1)
44+
45+
def test_ids_increment(self):
46+
t1 = store.add_task("First")
47+
t2 = store.add_task("Second")
48+
self.assertEqual(t1["id"], 1)
49+
self.assertEqual(t2["id"], 2)
50+
51+
def test_empty_description_raises(self):
52+
with self.assertRaises(ValueError):
53+
store.add_task("")
54+
55+
def test_whitespace_only_raises(self):
56+
with self.assertRaises(ValueError):
57+
store.add_task(" ")
58+
59+
def test_persists_to_disk(self):
60+
store.add_task("Persisted")
61+
with open(store.TASKS_FILE) as f:
62+
data = json.load(f)
63+
self.assertEqual(len(data), 1)
64+
self.assertEqual(data[0]["description"], "Persisted")
65+
66+
67+
class TestCompleteTask(BaseTest):
68+
def test_complete_task(self):
69+
store.add_task("Write tests")
70+
task = store.complete_task(1)
71+
self.assertTrue(task["completed"])
72+
self.assertIsNotNone(task["completed_at"])
73+
74+
def test_complete_nonexistent_raises(self):
75+
with self.assertRaises(KeyError):
76+
store.complete_task(999)
77+
78+
def test_complete_already_done_raises(self):
79+
store.add_task("Already done")
80+
store.complete_task(1)
81+
with self.assertRaises(ValueError):
82+
store.complete_task(1)
83+
84+
85+
class TestDeleteTask(BaseTest):
86+
def test_delete_task(self):
87+
store.add_task("To delete")
88+
deleted = store.delete_task(1)
89+
self.assertEqual(deleted["description"], "To delete")
90+
self.assertEqual(store.load_tasks(), [])
91+
92+
def test_delete_nonexistent_raises(self):
93+
with self.assertRaises(KeyError):
94+
store.delete_task(42)
95+
96+
def test_remaining_tasks_intact(self):
97+
store.add_task("Keep me")
98+
store.add_task("Delete me")
99+
store.delete_task(2)
100+
tasks = store.load_tasks()
101+
self.assertEqual(len(tasks), 1)
102+
self.assertEqual(tasks[0]["description"], "Keep me")
103+
104+
105+
class TestFilterTasks(BaseTest):
106+
def setUp(self):
107+
super().setUp()
108+
store.add_task("Pending task")
109+
store.add_task("Completed task")
110+
store.complete_task(2)
111+
self.tasks = store.load_tasks()
112+
113+
def test_filter_all(self):
114+
self.assertEqual(len(store.filter_tasks(self.tasks, "all")), 2)
115+
116+
def test_filter_pending(self):
117+
result = store.filter_tasks(self.tasks, "pending")
118+
self.assertEqual(len(result), 1)
119+
self.assertFalse(result[0]["completed"])
120+
121+
def test_filter_completed(self):
122+
result = store.filter_tasks(self.tasks, "completed")
123+
self.assertEqual(len(result), 1)
124+
self.assertTrue(result[0]["completed"])
125+
126+
def test_filter_unknown_raises(self):
127+
with self.assertRaises(ValueError):
128+
store.filter_tasks(self.tasks, "invalid")
129+
130+
131+
class TestCorruptedFile(BaseTest):
132+
def test_corrupted_json_raises(self):
133+
with open(store.TASKS_FILE, "w") as f:
134+
f.write("not valid json{{{")
135+
with self.assertRaises(ValueError):
136+
store.load_tasks()
137+
138+
def test_non_array_json_raises(self):
139+
with open(store.TASKS_FILE, "w") as f:
140+
json.dump({"key": "value"}, f)
141+
with self.assertRaises(ValueError):
142+
store.load_tasks()
143+
144+
def test_missing_file_returns_empty(self):
145+
os.unlink(store.TASKS_FILE)
146+
self.assertEqual(store.load_tasks(), [])
147+
# restore so tearDown doesn't crash
148+
with open(store.TASKS_FILE, "w") as f:
149+
f.write("[]")
150+
151+
152+
# ── CLI integration tests ───────────────────────────────────────────────────
153+
154+
155+
class TestCLI(BaseTest):
156+
def _run(self, argv):
157+
"""Run CLI with given argv list, return exit code."""
158+
with patch("sys.argv", ["todo"] + argv):
159+
parser = todo.build_parser()
160+
args = parser.parse_args()
161+
return args.func(args)
162+
163+
def test_add_command(self):
164+
code = self._run(["add", "CLI task"])
165+
self.assertEqual(code, 0)
166+
self.assertEqual(len(store.load_tasks()), 1)
167+
168+
def test_list_command(self):
169+
store.add_task("Listed task")
170+
code = self._run(["list"])
171+
self.assertEqual(code, 0)
172+
173+
def test_list_pending_filter(self):
174+
store.add_task("Pending")
175+
store.add_task("Done")
176+
store.complete_task(2)
177+
code = self._run(["list", "--status", "pending"])
178+
self.assertEqual(code, 0)
179+
180+
def test_done_command(self):
181+
store.add_task("Mark done")
182+
code = self._run(["done", "1"])
183+
self.assertEqual(code, 0)
184+
self.assertTrue(store.load_tasks()[0]["completed"])
185+
186+
def test_delete_command(self):
187+
store.add_task("Remove me")
188+
code = self._run(["delete", "1"])
189+
self.assertEqual(code, 0)
190+
self.assertEqual(store.load_tasks(), [])
191+
192+
def test_done_missing_id_returns_error(self):
193+
code = self._run(["done", "99"])
194+
self.assertEqual(code, 1)
195+
196+
def test_delete_missing_id_returns_error(self):
197+
code = self._run(["delete", "99"])
198+
self.assertEqual(code, 1)
199+
200+
def test_add_empty_returns_error(self):
201+
code = self._run(["add", ""])
202+
self.assertEqual(code, 1)
203+
204+
205+
if __name__ == "__main__":
206+
unittest.main()
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python3
2+
"""CLI to-do application.
3+
4+
Usage:
5+
todo.py add "Buy groceries"
6+
todo.py list
7+
todo.py list --status pending
8+
todo.py list --status completed
9+
todo.py done <id>
10+
todo.py delete <id>
11+
"""
12+
13+
import argparse
14+
import sys
15+
16+
import todo_store as store
17+
18+
# ── Formatting helpers ──────────────────────────────────────────────────────
19+
20+
CHECK = "[x]"
21+
EMPTY = "[ ]"
22+
23+
24+
def _fmt_task(task: dict) -> str:
25+
status = CHECK if task["completed"] else EMPTY
26+
suffix = (
27+
f" (done {task['completed_at'][:10]})" if task["completed_at"] else ""
28+
)
29+
return f" {task['id']:>3} {status} {task['description']}{suffix}"
30+
31+
32+
# ── Command handlers ────────────────────────────────────────────────────────
33+
34+
35+
def cmd_add(args: argparse.Namespace) -> int:
36+
try:
37+
task = store.add_task(args.description)
38+
print(f"Added task #{task['id']}: {task['description']}")
39+
return 0
40+
except ValueError as e:
41+
print(f"Error: {e}", file=sys.stderr)
42+
return 1
43+
44+
45+
def cmd_list(args: argparse.Namespace) -> int:
46+
try:
47+
tasks = store.load_tasks()
48+
filtered = store.filter_tasks(tasks, args.status)
49+
except ValueError as e:
50+
print(f"Error: {e}", file=sys.stderr)
51+
return 1
52+
53+
if not filtered:
54+
label = "" if args.status == "all" else f"{args.status} "
55+
print(f"No {label}tasks found.")
56+
return 0
57+
58+
label = "" if args.status == "all" else f"{args.status} "
59+
print(f"\n--- {label}tasks ({len(filtered)}) ---")
60+
for task in filtered:
61+
print(_fmt_task(task))
62+
print()
63+
return 0
64+
65+
66+
def cmd_done(args: argparse.Namespace) -> int:
67+
try:
68+
task = store.complete_task(args.id)
69+
print(f"Completed task #{task['id']}: {task['description']}")
70+
return 0
71+
except (KeyError, ValueError) as e:
72+
print(f"Error: {e}", file=sys.stderr)
73+
return 1
74+
75+
76+
def cmd_delete(args: argparse.Namespace) -> int:
77+
try:
78+
task = store.delete_task(args.id)
79+
print(f"Deleted task #{task['id']}: {task['description']}")
80+
return 0
81+
except KeyError as e:
82+
print(f"Error: {e}", file=sys.stderr)
83+
return 1
84+
85+
86+
# ── Argument parsing ────────────────────────────────────────────────────────
87+
88+
89+
def build_parser() -> argparse.ArgumentParser:
90+
parser = argparse.ArgumentParser(
91+
prog="todo",
92+
description="A simple CLI to-do application.",
93+
)
94+
sub = parser.add_subparsers(dest="command", metavar="<command>")
95+
sub.required = True
96+
97+
# add
98+
p_add = sub.add_parser("add", help="Add a new task")
99+
p_add.add_argument("description", help="Task description")
100+
p_add.set_defaults(func=cmd_add)
101+
102+
# list
103+
p_list = sub.add_parser("list", help="List tasks")
104+
p_list.add_argument(
105+
"--status",
106+
choices=["all", "pending", "completed"],
107+
default="all",
108+
help="Filter by status (default: all)",
109+
)
110+
p_list.set_defaults(func=cmd_list)
111+
112+
# done
113+
p_done = sub.add_parser("done", help="Mark a task as completed")
114+
p_done.add_argument("id", type=int, help="Task ID")
115+
p_done.set_defaults(func=cmd_done)
116+
117+
# delete
118+
p_del = sub.add_parser("delete", help="Delete a task")
119+
p_del.add_argument("id", type=int, help="Task ID")
120+
p_del.set_defaults(func=cmd_delete)
121+
122+
return parser
123+
124+
125+
def main() -> int:
126+
parser = build_parser()
127+
args = parser.parse_args()
128+
return args.func(args)
129+
130+
131+
if __name__ == "__main__":
132+
sys.exit(main())

0 commit comments

Comments
 (0)