diff --git a/claude-code/README.md b/claude-code/README.md new file mode 100644 index 0000000000..67f55a4d06 --- /dev/null +++ b/claude-code/README.md @@ -0,0 +1,34 @@ +# How to Use Claude Code to Write and Refactor Python + +This folder contains code associated with the Real Python tutorial [How to Use Claude Code to Write and Refactor Python](https://realpython.com/how-to-use-claude-code/). + +The `mini-contacts/` project is the finished state of the command-line contact manager that you build with Claude Code in the tutorial. The prompts that produced it are collected in [`prompts.md`](prompts.md), in the order they appear. + +Because Claude Code is nondeterministic, your own run won't match this code line for line. Expect the same structure, a storage module for CSV operations and a CLI module for argument parsing, with different naming and implementation details. + +## Run the Project + +```sh +$ cd mini-contacts/ +$ python -m mini_contacts add --name "Alice" --email "alice@example.com" --phone "555-1234" +$ python -m mini_contacts list +``` + +Contacts are stored at `~/.mini-contacts.csv` by default. Pass `--path` to use a different file. + +## Run the Tests + +```sh +$ cd mini-contacts/ +$ python -m unittest +``` + +The suite uses `unittest` from the standard library, so there's nothing to install. + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. diff --git a/claude-code/mini-contacts/CLAUDE.md b/claude-code/mini-contacts/CLAUDE.md new file mode 100644 index 0000000000..165f07aa9c --- /dev/null +++ b/claude-code/mini-contacts/CLAUDE.md @@ -0,0 +1,5 @@ +# Project Conventions + +- Python 3.14+, four-space indentation, PEP 8 compliant +- Prefer the standard library; use third-party packages only when needed +- Use type hints on all public functions diff --git a/claude-code/mini-contacts/mini_contacts/__init__.py b/claude-code/mini-contacts/mini_contacts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/claude-code/mini-contacts/mini_contacts/__main__.py b/claude-code/mini-contacts/mini_contacts/__main__.py new file mode 100644 index 0000000000..a5b38ffdb0 --- /dev/null +++ b/claude-code/mini-contacts/mini_contacts/__main__.py @@ -0,0 +1,3 @@ +from mini_contacts.cli import main + +main() diff --git a/claude-code/mini-contacts/mini_contacts/cli.py b/claude-code/mini-contacts/mini_contacts/cli.py new file mode 100644 index 0000000000..8a9fe8d781 --- /dev/null +++ b/claude-code/mini-contacts/mini_contacts/cli.py @@ -0,0 +1,51 @@ +import argparse +import sys + +from mini_contacts import storage + + +def _print_table(contacts: list[dict[str, str]]) -> None: + if not contacts: + print("No contacts found.") + return + headers = {"name": "Name", "email": "Email", "phone": "Phone"} + widths = { + field: max( + len(headers[field]), max(len(row[field]) for row in contacts) + ) + for field in storage.FIELDNAMES + } + header_line = " | ".join( + headers[f].ljust(widths[f]) for f in storage.FIELDNAMES + ) + separator = "-+-".join("-" * widths[f] for f in storage.FIELDNAMES) + print(header_line) + print(separator) + for row in contacts: + print(" | ".join(row[f].ljust(widths[f]) for f in storage.FIELDNAMES)) + + +def main(argv: list[str] | None = None) -> None: + parser = argparse.ArgumentParser(description="Minimal contact manager") + parser.add_argument( + "--path", default="~/.mini-contacts.csv", help="Path to the CSV file" + ) + subparsers = parser.add_subparsers(dest="command") + subparsers.required = True + add_parser = subparsers.add_parser("add", help="Add a new contact") + add_parser.add_argument("--name", required=True) + add_parser.add_argument("--email", required=True) + add_parser.add_argument("--phone", required=True) + subparsers.add_parser("list", help="List all contacts") + args = parser.parse_args(argv) + + try: + if args.command == "add": + storage.add_contact(args.path, args.name, args.email, args.phone) + print(f"Added contact: {args.name}") + elif args.command == "list": + contacts = storage.read_contacts(args.path) + _print_table(contacts) + except (ValueError, OSError) as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/claude-code/mini-contacts/mini_contacts/storage.py b/claude-code/mini-contacts/mini_contacts/storage.py new file mode 100644 index 0000000000..7a87f2efa2 --- /dev/null +++ b/claude-code/mini-contacts/mini_contacts/storage.py @@ -0,0 +1,32 @@ +import csv +import os + +FIELDNAMES = ("name", "email", "phone") + + +def read_contacts(path: str) -> list[dict[str, str]]: + path = os.path.expanduser(path) + try: + with open(path, newline="") as f: + reader = csv.DictReader(f, restval="") + if reader.fieldnames is None: + return [] + if tuple(reader.fieldnames) != FIELDNAMES: + raise ValueError( + f"Expected CSV headers {FIELDNAMES}, got {tuple(reader.fieldnames)}" + ) + return list(reader) + except FileNotFoundError: + return [] + + +def add_contact(path: str, name: str, email: str, phone: str) -> None: + if not all([name, email, phone]): + raise ValueError("name, email, and phone must not be empty") + path = os.path.expanduser(path) + write_header = not os.path.exists(path) or os.path.getsize(path) == 0 + with open(path, "a", newline="") as f: + writer = csv.DictWriter(f, fieldnames=FIELDNAMES) + if write_header: + writer.writeheader() + writer.writerow({"name": name, "email": email, "phone": phone}) diff --git a/claude-code/mini-contacts/tests/__init__.py b/claude-code/mini-contacts/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/claude-code/mini-contacts/tests/test_cli.py b/claude-code/mini-contacts/tests/test_cli.py new file mode 100644 index 0000000000..3735dcd173 --- /dev/null +++ b/claude-code/mini-contacts/tests/test_cli.py @@ -0,0 +1,47 @@ +import contextlib +import io +import tempfile +import unittest +from pathlib import Path + +from mini_contacts.cli import main + + +class CLITests(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp_path = Path(tmp.name) + + def run_cli(self, *argv): + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + main(list(argv)) + return stdout.getvalue() + + def test_add_then_list(self): + path = str(self.tmp_path / "contacts.csv") + self.run_cli( + "--path", + path, + "add", + "--name", + "Alice", + "--email", + "alice@example.com", + "--phone", + "555-1234", + ) + out = self.run_cli("--path", path, "list") + self.assertIn("Alice", out) + self.assertIn("alice@example.com", out) + + def test_list_empty(self): + out = self.run_cli("--path", str(self.tmp_path / "empty.csv"), "list") + self.assertIn("No contacts found.", out) + + def test_list_short_row_exits_cleanly(self): + path = self.tmp_path / "contacts.csv" + path.write_text("name,email,phone\nAlice,alice@example.com\n") + out = self.run_cli("--path", str(path), "list") + self.assertIn("Alice", out) diff --git a/claude-code/mini-contacts/tests/test_storage.py b/claude-code/mini-contacts/tests/test_storage.py new file mode 100644 index 0000000000..e68b3c921a --- /dev/null +++ b/claude-code/mini-contacts/tests/test_storage.py @@ -0,0 +1,48 @@ +import tempfile +import unittest +from pathlib import Path + +from mini_contacts import storage + + +class StorageTests(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp_path = Path(tmp.name) + + def test_round_trip(self): + path = self.tmp_path / "contacts.csv" + storage.add_contact( + str(path), "Alice", "alice@example.com", "555-1234" + ) + expected = { + "name": "Alice", + "email": "alice@example.com", + "phone": "555-1234", + } + self.assertEqual(storage.read_contacts(str(path)), [expected]) + + def test_header_written_once(self): + path = self.tmp_path / "contacts.csv" + storage.add_contact( + str(path), "Alice", "alice@example.com", "555-1234" + ) + storage.add_contact(str(path), "Bob", "bob@example.com", "555-5678") + self.assertEqual(path.read_text().count("name,email,phone"), 1) + + def test_read_missing_file_returns_empty(self): + missing = self.tmp_path / "missing.csv" + self.assertEqual(storage.read_contacts(str(missing)), []) + + def test_short_row_filled_with_blanks(self): + path = self.tmp_path / "contacts.csv" + path.write_text("name,email,phone\nAlice,alice@example.com\n") + contacts = storage.read_contacts(str(path)) + self.assertEqual(contacts[0]["phone"], "") + + def test_add_blank_field_raises(self): + with self.assertRaises(ValueError): + storage.add_contact( + str(self.tmp_path / "c.csv"), "Alice", "", "555-1234" + ) diff --git a/claude-code/prompts.md b/claude-code/prompts.md new file mode 100644 index 0000000000..7de1d0540e --- /dev/null +++ b/claude-code/prompts.md @@ -0,0 +1,97 @@ +# Prompts + +These are the prompts used in the Real Python tutorial +[How to Use Claude Code to Write and Refactor Python](https://realpython.com/how-to-use-claude-code/), +in the order they appear. + +Claude Code is nondeterministic, so your results will differ in wording and +detail. Expect the same general shape, not identical output. + +## 1. Plan the project structure (Plan Mode) + +```text +I want to build a minimal command-line contact manager in Python. + +It should have two commands: + +- `add` to append a new contact with name, email, and phone to a CSV file +- `list` to print all contacts in a readable table format + +Use `argparse` for the CLI. Store the CSV at ~/.mini-contacts.csv +by default with a --path flag to override it. + +Split the project into a mini_contacts/ package with storage.py for CSV +read/write and cli.py for the `argparse` interface. +``` + +## 2. Implement the plan + +```text +Implement the plan. +``` + +## 3. Exercise the happy path + +```text +Add a contact named "John Doe" with email john@example.com and +phone 555-0100, then list all contacts to verify it was saved. +``` + +## 4. Write the tests + +```text +Write tests for the storage and CLI modules. Cover the happy path for +add and list, and test what happens when the CSV file doesn't exist yet. +``` + +## 5. Run the tests with Shell mode + +```text +! python -m unittest +``` + +## 6. Commit the initial implementation + +```text +Create a commit with the message "Add initial mini-contacts implementation". +``` + +## 7. Explore the codebase in a fresh session + +```text +Review this codebase. Tell me what the application does, describe +each module and how they connect, and summarize the test coverage. +``` + +## 8. Hunt for bugs and edge cases + +```text +Now look for bugs, security issues, and edge cases that could cause +crashes or data loss. Be specific about each finding. +``` + +## 9. Plan the fixes (Plan Mode) + +```text +Fix the crash bugs and the edge case for empty-string validation. +For the crash on short CSV rows, handle rows with missing fields gracefully. +For the empty-string fields, reject blank values for name, email, and phone. +``` + +## 10. Implement the fixes + +```text +Implement the fixes. +``` + +## 11. Re-run the tests + +```text +Run the tests and show me the results. +``` + +## 12. Commit the fixes + +```text +Create a commit with the message "Add input validation and fix crash bugs". +```