Fix Optional[Shape] unwrapping and add comprehensive shapes stress tests

_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>
This commit is contained in:
2026-03-31 02:56:11 -04:00
parent 5a56d7a4a5
commit 625d8cf9b9
3 changed files with 582 additions and 332 deletions

View File

@@ -42,33 +42,91 @@ class EmailUser(AbstractBaseUser, PermissionsMixin):
# ─── Shape test models ──────────────────────────────────────────────────────
import uuid
class Author(models.Model):
name = models.CharField(max_length=100)
bio = models.TextField(blank=True, default="")
class TimestampMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Publisher(TimestampMixin):
name = models.CharField(max_length=200)
country = models.CharField(max_length=100, default="")
class Meta:
app_label = "tests"
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
pages = models.IntegerField(default=0)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
class Author(TimestampMixin):
name = models.CharField(max_length=200)
bio = models.TextField(default="")
publisher = models.ForeignKey(
Publisher, on_delete=models.CASCADE, related_name="authors"
)
mentor = models.ForeignKey(
"self", on_delete=models.SET_NULL, null=True, blank=True, related_name="mentees"
)
class Meta:
app_label = "tests"
def __str__(self):
return self.title
class Tag(models.Model):
name = models.CharField(max_length=50)
books = models.ManyToManyField(Book, related_name="tags", blank=True)
slug = models.SlugField(primary_key=True, max_length=100)
label = models.CharField(max_length=100)
class Meta:
app_label = "tests"
class Book(TimestampMixin):
title = models.CharField(max_length=300)
isbn = models.CharField(max_length=13, unique=True)
page_count = models.IntegerField(default=0)
is_published = models.BooleanField(default=False)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
editor = models.ForeignKey(
Author, on_delete=models.SET_NULL, null=True, blank=True, related_name="edited_books",
)
tags = models.ManyToManyField(Tag, blank=True, related_name="books")
class Meta:
app_label = "tests"
class Chapter(TimestampMixin):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="chapters")
number = models.IntegerField()
title = models.CharField(max_length=300)
word_count = models.IntegerField(default=0)
class Meta:
app_label = "tests"
ordering = ["number"]
unique_together = [("book", "number")]
class Section(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE, related_name="sections")
heading = models.CharField(max_length=300)
body = models.TextField(default="")
position = models.IntegerField(default=0)
class Meta:
app_label = "tests"
ordering = ["position"]
class Category(models.Model):
name = models.CharField(max_length=200)
parent = models.ForeignKey(
"self", on_delete=models.CASCADE, null=True, blank=True, related_name="children"
)
class Meta:
app_label = "tests"