Skip to content

Commit fea4d0e

Browse files
committed
feat:add code generated by LLMS for tutorial
1 parent 89ec497 commit fea4d0e

24 files changed

Lines changed: 1817 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)