Skip to main content

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

CategoryTestsWhat They Verify
Initialization2Default author, tags start empty
Property2word_count computed, updates on body change
Classmethod1from_dict creates correct Note
Staticmethod2is_valid_title accepts/rejects
Decorator2@validate_input rejects empty strings on add_tag and remove_tag
Methods3add_tag, remove_tag, has_tag behavior
Repository3save/find_by_id, find_all, delete
Integration1Save via repository, find, verify properties
Total16

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:

  1. Read the failure message
  2. Identify which pattern is wrong (property? decorator? protocol?)
  3. Fix the implementation
  4. Run uv run pytest again

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."