Fix self-referential shapes (CategoryShape)

Set field-only _spec before building the full spec with nested shapes.
Self-references resolve because cls._spec already exists when the
generator encounters shape._spec where shape is cls.

Also: pass cls into get_type_hints localns so forward ref strings
like list["CategoryShape"] resolve to the class being defined.

49 shapes tests, 0 skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 03:04:15 -04:00
parent 625d8cf9b9
commit 51ed2b28c5
2 changed files with 13 additions and 13 deletions

View File

@@ -66,7 +66,7 @@ class Shape(BaseModel, Generic[_M]):
cls._nested = {} cls._nested = {}
cls._pk_field = model._meta.pk.name if model._meta.pk else "id" cls._pk_field = model._meta.pk.name if model._meta.pk else "id"
hints = get_type_hints(cls, include_extras=False) or cls.__annotations__ hints = get_type_hints(cls, include_extras=False, localns={cls.__name__: cls}) or cls.__annotations__
field_names = [] field_names = []
for name, hint in hints.items(): for name, hint in hints.items():
@@ -78,6 +78,11 @@ class Shape(BaseModel, Generic[_M]):
field_names.append(name) field_names.append(name)
cls._field_names = field_names cls._field_names = field_names
# Set field-only spec first so self-references can find it
cls._spec = [*field_names]
# Now rebuild with nested — self-refs resolve because cls._spec exists
cls._spec = [ cls._spec = [
*field_names, *field_names,
*({name: shape._spec} for name, shape in cls._nested.items()), *({name: shape._spec} for name, shape in cls._nested.items()),

View File

@@ -105,13 +105,10 @@ class BookWithEditorShape(Shape[Book]):
editor: FlatAuthorShape | None = None editor: FlatAuthorShape | None = None
# CategoryShape is commented out — self-referential forward refs crash class CategoryShape(Shape[Category]):
# get_type_hints() at __init_subclass__ time. Known gap. id: int | None = None
# name: str
# class CategoryShape(Shape[Category]): children: list["CategoryShape"] = []
# id: int | None = None
# name: str
# children: list["CategoryShape"] = []
# ============================================================================= # =============================================================================
@@ -169,11 +166,9 @@ class TestShapeClassCreation(TestCase):
) )
def test_self_referential_shape(self): def test_self_referential_shape(self):
"""CategoryShape.children references itself. """CategoryShape.children references itself."""
Currently crashes at class definition time — known gap.""" self.assertIn("children", CategoryShape._nested)
self.skipTest( self.assertIs(CategoryShape._nested["children"], CategoryShape)
"Self-referential forward ref crashes get_type_hints() at __init_subclass__ time"
)
def test_multiple_shapes_same_model_independent(self): def test_multiple_shapes_same_model_independent(self):
self.assertLess(len(FlatBookShape._field_names), len(BookDetailShape._field_names)) self.assertLess(len(FlatBookShape._field_names), len(BookDetailShape._field_names))