_extract_shape_class now handles `Shape | None` (Union types) by checking
isinstance(hint, types.UnionType) and iterating args for Shape subclasses.
This fixes nullable FK detection — any `editor: AuthorShape | None` field
is now correctly recognized as a nested shape.
48 stress tests covering:
- 5-level deep nesting (Publisher → Author → Book → Chapter → Section)
- Two FKs to same model (author + editor)
- Slug PK (Tag), UUID PK (Section)
- M2M relationships (Book.tags)
- Nullable FKs returning None
- Empty strings, zero integers, false booleans (truthiness traps)
- 100-record smoke test
- Query efficiency (assertNumQueries)
- All diff operations with deep nesting
Known gap documented: self-referential forward refs (CategoryShape)
crash get_type_hints() at __init_subclass__ time. Needs deferred resolution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
25 tests covering Shape, Diff, and NestedDiff:
- Shape metaclass: model resolution, field extraction, nested detection, spec/pair building
- Query: list, filter, nested relations, empty results, Pydantic serialization
- Diff (new): detects all fields as changed
- Diff (existing): no changes, single field, multiple fields, nonexistent ID
- Diff (nested): created, updated, deleted, combined, nonexistent relation
Fixes:
- django-readers moved from optional to core dependency
- Shape import lazy-loaded via __getattr__ (django_readers imports
contenttypes which can't happen during apps.populate())
- Added Author, Book, Tag test models
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>