Skip to content

Commit 8a4d5aa

Browse files
authored
Fix some decoding of subsubtypes in general and std::Object in particular (#938)
I ran into a few things here: - The object.pyx code to handle decoding to subclasses only looks at `__subclasses__`, and so misses grandchildren types. I clearly remember thinking about this and testing the grandchild case, but apparently I missed something there. - Descendants of std::Object in particular get immediately screwy, because user-facing types don't inherit from std::Object directly, but instead their `__shapes__` do. - The new "`tname__` in `__shapes__`" mechanism (#926) was putting wrong stuff in the map in some cases. My approach is to explicitly indicate which classes should be considered the canonical decoding classes with a `__gel_is_canonical__ = True` field on the class. And for now, I'm populating it explicitly, though we can probably write a bunch of nasty logic to figure it out too.
1 parent 06d8a21 commit 8a4d5aa

File tree

6 files changed

+30
-43
lines changed

6 files changed

+30
-43
lines changed

gel/_internal/_codegen/_models/_pydantic.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4358,6 +4358,9 @@ def _write_base_object_type_body(
43584358
include_tname: bool = False,
43594359
) -> None:
43604360
if include_tname:
4361+
if objtype.name in CORE_OBJECTS:
4362+
self.write('__gel_is_canonical__ = True')
4363+
43614364
type_name = objtype.schemapath
43624365
literal = self.import_name("typing", "Literal")
43634366
field = self.import_name("pydantic", "Field")
@@ -4734,6 +4737,7 @@ def write_object_type(
47344737
),
47354738
):
47364739
self.write(f'"""type {objtype.name}"""')
4740+
self.write('__gel_is_canonical__ = True')
47374741
self.write()
47384742
pointers = _get_object_type_body(objtype)
47394743
if pointers:
@@ -4746,9 +4750,6 @@ def write_object_type(
47464750
localns=localns,
47474751
)
47484752
self._write_model_attribute(ptr.name, ptr_type)
4749-
else:
4750-
self.write("pass")
4751-
self.write()
47524753

47534754
def _write_model_attribute(self, name: str, anno: str) -> None:
47544755
decl = f"{name}: {anno}"

gel/_internal/_qb/_reflection.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class GelSchemaMetadata:
3535
class __gel_reflection__: # noqa: N801
3636
name: ClassVar[SchemaPath]
3737

38+
# Whether this class is a "canonical" type - that is, the primary
39+
# representation of a database type, not an internal __shape__ class,
40+
# or a user-defined subtype, or anything else.
41+
__gel_is_canonical__: ClassVar[bool]
42+
3843

3944
class GelSourceMetadata(GelSchemaMetadata):
4045
class __gel_reflection__(GelSchemaMetadata.__gel_reflection__): # noqa: N801

gel/_internal/_qbmodel/_abstract/_base.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -167,30 +167,13 @@ def __new__(
167167
reflection = cls.__gel_reflection__
168168
if (
169169
# The class registry only tracks the canonical base instances,
170-
# which are the ones with a base that directly declares 'tname__'
171-
# but do not declare it themselves.
172-
#
173-
# Eg.
174-
#
175-
# default/__init__.py :
176-
# class Foo(base_shapes.Foo):
177-
# ...
178-
#
179-
# __shapes__/default/__init__.py :
180-
# class Foo(__Foo_default_shape__, __gel_shape__="RequiredId"):
181-
# tname__: Literal["default::Foo"] = Field(
182-
# "default::Foo", alias="__tname__")
183-
#
184-
# The user facing default.Foo does not directly declare tname__,
185-
# but base_shapes.Foo does.
186-
#
187-
# For a derived `type Bar extending Foo`, default.Bar does not
188-
# directly declare tname__, but base_shapes.Bar does.
170+
# which are declared with __gel_is_canonical__ = True.
189171
(tname := getattr(reflection, "name", None)) is not None
190-
and any('tname__' in base.__dict__ for base in bases)
191-
and 'tname__' not in namespace
172+
and namespace.get('__gel_is_canonical__')
192173
):
193174
mcls.__gel_class_registry__[str(tname)] = cls
175+
else:
176+
cls.__gel_is_canonical__ = False
194177
cls.__gel_shape__ = __gel_shape__
195178
return cls
196179

gel/protocol/codecs/object.pyx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -358,26 +358,27 @@ cdef class ObjectCodec(BaseNamedRecordCodec):
358358
self.cached_return_type = return_type
359359
self.cached_return_type_proxy = None
360360

361-
# Store base type's tname in the mapping *also* to exclude
362-
# subclasses of base type, e.g. if we have this:
363-
#
364-
# class CustomContent(default.Content):
365-
# pass
366-
#
367-
# then default.Content.__subclasses__() will contain
368-
# CustomContent, which we don't want to be there.
369-
370-
tname_map = {str(refl.name): self.cached_return_type}
371-
372-
for ch in self.cached_return_type.__subclasses__():
361+
# Build a map of descendant types that are marked as being
362+
# canonical targets. Make sure to descend through types not
363+
# marked canonical, though, because the
364+
# std::Object/std::BaseObject type only get inherited via the
365+
# __shapes__ types, and we'll need to descend through that to
366+
# get to the real ones.
367+
worklist = [self.cached_return_type]
368+
tname_map = {}
369+
while worklist:
370+
ch = worklist.pop()
373371
try:
374372
sub_name = ch.__gel_reflection__.name
373+
canonical = ch.__gel_is_canonical__
375374
except AttributeError:
376375
pass
377376
else:
378-
# setdefault only sets it if isn't set already, so
379-
# custom subtypes won't be picked up.
380-
tname_map.setdefault(str(sub_name), ch)
377+
sname = str(sub_name)
378+
if sname not in tname_map:
379+
worklist.extend(ch.__subclasses__())
380+
if canonical:
381+
tname_map[sname] = ch
381382

382383
subs = []
383384
dlists = []

tests/dbsetup/orm.gel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ type Party extending ExclusivelyNamed {
8282

8383
# Object with required multi link, has link props
8484
type Raid extending ExclusivelyNamed {
85-
required multi members: User {
85+
required multi members: Named {
8686
role: str;
8787
rank: int64;
8888
};

tests/test_model_generator.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ def test_modelgen_06(self):
261261
self.assertIsInstance(user, default.User)
262262
self.assertIsInstance(user, default.CustomUser)
263263

264-
@tb.xfail("links to std::Object don't decoded to the object's actual type")
265264
def test_modelgen_07(self):
266265
from models.orm import default, std
267266

@@ -1375,7 +1374,6 @@ def test_modelgen_pydantic_apis_11d(self):
13751374
self.assertPydanticPickles(t)
13761375
self.assertPydanticSerializes(t)
13771376

1378-
@tb.xfail("links to std::Object don't decoded to the object's actual type")
13791377
def test_modelgen_pydantic_apis_11e(self):
13801378
# Test model_dump() and model_dump_json() on models;
13811379
# test *single required* link serialization in all combinations
@@ -1392,7 +1390,6 @@ def test_modelgen_pydantic_apis_11e(self):
13921390
self.assertPydanticPickles(t)
13931391
self.assertPydanticSerializes(t)
13941392

1395-
@tb.xfail("links to std::Object don't decoded to the object's actual type")
13961393
def test_modelgen_pydantic_apis_11f(self):
13971394
# Test model_dump() and model_dump_json() on models;
13981395
# test *single required* link serialization in all combinations

0 commit comments

Comments
 (0)