From 51ed2b28c5a761a5003e0056b61908d53c24528c Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Tue, 31 Mar 2026 03:04:15 -0400 Subject: [PATCH] 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) --- django/src/djarea/shapes/core.py | 7 ++++++- django/src/djarea/tests/test_shapes.py | 19 +++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/django/src/djarea/shapes/core.py b/django/src/djarea/shapes/core.py index 4536b3b..bdfa080 100644 --- a/django/src/djarea/shapes/core.py +++ b/django/src/djarea/shapes/core.py @@ -66,7 +66,7 @@ class Shape(BaseModel, Generic[_M]): cls._nested = {} 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 = [] for name, hint in hints.items(): @@ -78,6 +78,11 @@ class Shape(BaseModel, Generic[_M]): field_names.append(name) 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 = [ *field_names, *({name: shape._spec} for name, shape in cls._nested.items()), diff --git a/django/src/djarea/tests/test_shapes.py b/django/src/djarea/tests/test_shapes.py index 79d7406..106907d 100644 --- a/django/src/djarea/tests/test_shapes.py +++ b/django/src/djarea/tests/test_shapes.py @@ -105,13 +105,10 @@ class BookWithEditorShape(Shape[Book]): editor: FlatAuthorShape | None = None -# CategoryShape is commented out — self-referential forward refs crash -# get_type_hints() at __init_subclass__ time. Known gap. -# -# class CategoryShape(Shape[Category]): -# id: int | None = None -# name: str -# children: list["CategoryShape"] = [] +class CategoryShape(Shape[Category]): + id: int | None = None + name: str + children: list["CategoryShape"] = [] # ============================================================================= @@ -169,11 +166,9 @@ class TestShapeClassCreation(TestCase): ) def test_self_referential_shape(self): - """CategoryShape.children references itself. - Currently crashes at class definition time — known gap.""" - self.skipTest( - "Self-referential forward ref crashes get_type_hints() at __init_subclass__ time" - ) + """CategoryShape.children references itself.""" + self.assertIn("children", CategoryShape._nested) + self.assertIs(CategoryShape._nested["children"], CategoryShape) def test_multiple_shapes_same_model_independent(self): self.assertLess(len(FlatBookShape._field_names), len(BookDetailShape._field_names))