SmartNotes Advanced Patterns Capstone
Emma pulls up the whiteboard. Four patterns across four lessons:
L1: @property word_count -- computed from self.body
L2: Note.from_dict() -- @classmethod factory
L3: @validate_input -- decorator for string arguments
L4: Repository Protocol -- structural interface for storage
"Every pattern you have learned across Phase 5. One architecture. TDG."
This capstone combines all Chapter 61 patterns with the class fundamentals from Chapters 58-60. You will build the final SmartNotes object model using the TDG cycle: design the architecture, write the tests, prompt AI for implementation, and verify everything works together.
The Architecture
Three files, one pattern per responsibility:
smartnotes/
├── decorators.py # @validate_input
├── note.py # Note class with @property, @classmethod, methods
├── repository.py # Repository Protocol + InMemoryRepository
└── test_smartnotes.py # 15+ tests covering all patterns
decorators.py
import functools
def validate_input(func):
"""Reject empty string arguments."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
for arg in args[1:]: # skip self
if isinstance(arg, str) and not arg.strip():
raise ValueError(f"{func.__name__}: empty string argument")
return func(*args, **kwargs)
return wrapper
note.py (Stub)
from decorators import validate_input
class Note:
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
... # set attributes, initialize tags
@property
def word_count(self) -> int:
... # compute from self.body
@classmethod
def from_dict(cls, data: dict[str, str]) -> "Note":
... # create instance from dictionary
@staticmethod
def is_valid_title(title: str) -> bool:
... # non-empty, max 200 chars
@validate_input
def add_tag(self, tag: str) -> None:
... # no duplicates
@validate_input
def remove_tag(self, tag: str) -> None:
... # raise ValueError if not found
def has_tag(self, tag: str) -> bool:
... # return True/False
def summarize(self) -> str:
... # formatted string with title, word_count, tags
def __repr__(self) -> str:
... # Note(title='...', word_count=N)
def __eq__(self, other: object) -> bool:
... # compare by title and body
repository.py (Stub)
from typing import Protocol
class Repository(Protocol):
def save(self, note: "Note") -> None: ...
def find_by_id(self, note_id: str) -> "Note | None": ...
def find_all(self) -> list["Note"]: ...
def delete(self, note_id: str) -> None: ...
class InMemoryRepository:
def __init__(self) -> None:
... # internal dict storage
def save(self, note: "Note") -> None:
... # store by title
def find_by_id(self, note_id: str) -> "Note | None":
... # return note or None
def find_all(self) -> list["Note"]:
... # return all stored notes
def delete(self, note_id: str) -> None:
... # remove by title
Step 1: Design the Tests
Before prompting AI for the implementation, write the test suite. This is the TDG "specify" step: your tests define what "correct" means.
Test Categories
| Category | Tests | What They Verify |
|---|---|---|
| Initialization | 2 | Default author, tags start empty |
| Property | 2 | word_count computed, updates on body change |
| Classmethod | 1 | from_dict creates correct Note |
| Staticmethod | 2 | is_valid_title accepts/rejects |
| Decorator | 2 | @validate_input rejects empty strings on add_tag and remove_tag |
| Methods | 3 | add_tag, remove_tag, has_tag behavior |
| Repository | 3 | save/find_by_id, find_all, delete |
| Integration | 1 | Save via repository, find, verify properties |
| Total | 16 |
Example Tests
import pytest
from note import Note
from repository import InMemoryRepository
# --- Initialization ---
def test_note_default_author():
note = Note("Title", "Body text")
assert note.author == "Anonymous"
def test_note_tags_start_empty():
note = Note("Title", "Body text")
assert note.tags == []
# --- Property ---
def test_word_count_computed():
note = Note("Title", "one two three")
assert note.word_count == 3
def test_word_count_updates_on_body_change():
note = Note("Title", "one two three")
note.body = "one two three four five"
assert note.word_count == 5
# --- Classmethod ---
def test_from_dict_creates_note():
data = {"title": "Test", "body": "Content", "author": "James"}
note = Note.from_dict(data)
assert note.title == "Test"
assert note.author == "James"
# --- Decorator ---
def test_add_tag_rejects_empty():
note = Note("Title", "Body")
with pytest.raises(ValueError, match="empty string"):
note.add_tag("")
def test_remove_tag_rejects_empty():
note = Note("Title", "Body")
with pytest.raises(ValueError, match="empty string"):
note.remove_tag("")
# --- Repository ---
def test_save_and_find():
repo = InMemoryRepository()
note = Note("Test", "Body")
repo.save(note)
found = repo.find_by_id("Test")
assert found is not None
assert found.title == "Test"
def test_find_all():
repo = InMemoryRepository()
repo.save(Note("A", "Body A"))
repo.save(Note("B", "Body B"))
assert len(repo.find_all()) == 2
def test_delete():
repo = InMemoryRepository()
repo.save(Note("A", "Body"))
repo.delete("A")
assert repo.find_by_id("A") is None
Write all 16 tests before moving to Step 2. Commit the stubs and tests (protect-the-spec commit).
Step 2: TDG -- Generate
Open Claude Code and prompt for the implementation:
I have three files with stubs and a test file with 16 tests.
Files:
- decorators.py (already complete)
- note.py (stubs only)
- repository.py (stubs only)
- test_smartnotes.py (16 tests written)
Implement note.py and repository.py so all 16 tests pass.
Constraints:
- @property for word_count (compute from self.body)
- @classmethod for from_dict (use cls(...))
- @staticmethod for is_valid_title
- @validate_input decorator on add_tag and remove_tag
- InMemoryRepository must NOT inherit from Repository
- Use dict[str, Note] for internal storage in repository
Step 3: TDG -- Verify
Run the verification stack in order:
uv run ruff check decorators.py note.py repository.py
uv run pyright decorators.py note.py repository.py
uv run pytest test_smartnotes.py -v
Expected output:
16 passed
If any test fails, use the TDG debug loop:
- Read the failure message
- Identify which pattern is wrong (property? decorator? protocol?)
- Fix the implementation
- Run
uv run pytestagain
Step 4: Architecture Verification
After all tests pass, verify the patterns manually:
# Property stays synchronized
note = Note("Test", "one two three")
print(note.word_count) # 3
note.body = "one"
print(note.word_count) # 1
# Classmethod factory works
data = {"title": "From Dict", "body": "Content"}
note2 = Note.from_dict(data)
print(note2.title) # From Dict
# Decorator catches bad input
try:
note.add_tag(" ")
except ValueError as e:
print(e) # add_tag: empty string argument
# Repository stores and retrieves
repo = InMemoryRepository()
repo.save(note)
found = repo.find_by_id("Test")
print(found.word_count) # 1
Output:
3
1
From Dict
add_tag: empty string argument
1
Try With AI
Prompt 1: Review My Architecture
Here is my SmartNotes architecture:
[paste note.py, repository.py, decorators.py]
Review the design. Are there any patterns I am using
incorrectly? Any missing edge cases in the decorator
or repository? Suggest improvements but explain the
tradeoff for each.
What you're learning: The AI reviews your completed architecture the way a senior developer would during a code review. You evaluate its suggestions and decide which tradeoffs are worth taking.
Prompt 2: Extend the Repository
My InMemoryRepository stores notes by title. What
happens if two notes have the same title? Design a
better key strategy. Should I use a UUID? A slug?
The title plus author? Show the implementation and
explain the tradeoff.
What you're learning: Real repository design involves key collision handling. The AI proposes solutions, but you evaluate which strategy fits SmartNotes.
Prompt 3: Full TDG Reflection
In Claude Code, type:
/tdg
Use the TDG workflow to add a new method: Note.edit(new_body: str) -> None that updates the body. Write the test first (verify word_count updates after edit), then generate the implementation, then verify.
What you're learning: You are practicing the full TDG cycle independently, using /tdg to structure the workflow. The capstone proves you can drive the cycle with advanced patterns.
Emma looks at the terminal. Sixteen tests, all green. The architecture spans three files, four patterns, and covers initialization, computation, validation, construction, storage, and retrieval.
"You started Phase 5 with a dataclass," she says. "Six fields, no methods. Now you have a full object model: classes with behavior, inheritance for specialization, composition for containment, special methods for Pythonic integration, properties for computation, decorators for cross-cutting concerns, Protocols for testable interfaces."
James looks at the SmartNotes codebase. "Four chapters. Fifty-something tests across all capstones. And every test was written before the implementation."
"That is the TDG discipline. The test defines what correct means. The AI generates the implementation. You verify. The cycle works whether you are building a single function or a multi-class architecture."
"But SmartNotes lives in memory," James says. "When the program stops, everything disappears."
Emma nods. "Phase 6 teaches persistence: files, databases, modules. Your objects survive."