Skip to content

Commit cbc4899

Browse files
committed
Fix qbmodel is_link on _qb.Path
The following snippet constructed `_qb.Path`, with `is_link=False`. ```python default.User.posts ``` I need `is_link`, because I need to distinguish between links and properties, when constructing EdgeQL for paths without `.select()`. Links needs a splat shape (`{ * }`). --- Here is my "debug log" of how I stumbled through the codebase to find what I needed to do. I have this expr: ```python default.User.posts ``` ... and if I invoke `.__edgeql_qb_expr__()` on it, I get: ```python Path(symrefs=frozenset(), outside_refs=frozenset(), must_bind_refs=frozenset(), type_=SchemaPath('default::Post'), source=SchemaSet(symrefs=frozenset(), outside_refs=frozenset(), must_bind_refs=frozenset(), type_=SchemaPath('default::User')), name='posts', is_lprop=False, is_link=False) ``` The `is_link` is obviously wrong, because `posts` is a link. I need to fix this. But where is this `qb.Path` coming from? Where is implementation of `def __edgeql_qb_expr__()`? If I grep the codebase for it, and I ignore all definitions behind `if TYPE_CHECKING`, I find following impls: 1. `_qb._abtract.Expr`, that just returns self, makes sense. 2. `_qb._generics.BaseAlias`, which returns `self.__gel_metadata__`. This might be it, but guessing from filename, it is not - this for generic types, probably. 3. `_qb._base.GelType`, might be it - `default.User.posts` could be interpreted as a type. But it is not a type really - it is a reference to link that has some target type. Also, impl is returning either `ExprPlaceholder` or `_qb.Literal`, which is not what I'm getting. So no. 4. `_qb._base.AbstractGelModel`, `default.User.posts` is not a model, it's a link. It is returning a `SchemaSet`, which is definetly not what I'm getting, so this is not it. 5. `_qbmodel._abstract._methods.BaseGelModel`, again, not a model, it always returns `SchemaSet`, so cannot be what I'm looking for. So best bet is `_qb._generics.BaseAlias`. It returns `self.__gel_metadata__`, which is set in constructor. Is `default.User.posts` a subclass of `BaseAlias`? Let's check: ```python from gel._internal._qb._generics import BaseAlias q = default.User.posts print(q) print(isinstance(q, BaseAlias)) ``` ``` gel._internal._qb._generics.PathAlias[models.default.Post, Path(symrefs=frozenset(), outside_refs=frozenset(), must_bind_refs=frozenset(), type_=SchemaPath('default::Post'), source=SchemaSet(symrefs=frozenset(), outside_refs=frozenset(), must_bind_refs=frozenset(), type_=SchemaPath('default::User')), name='posts', is_lprop=False, is_link=False)] True ``` Ok, so `default.User.posts` is `_qb._generics.PathAlias`, and a subclass of `BaseAlias`. Now I need to find location where it is constructed. I cannot find the code in generated `models/` that would assign `posts` to the `User` class. Which means this must be happening with some meta class wizardry. Can I find it by looking for references to `PathAlias`'s costructor? It is constructed by function, confusingly named `def AnnotatedPath`. That is being called from `_qbmodel._abstract._descriptors.ModelFieldDescriptor.get` method. Which hints that `posts` is not actually a attribute on `User`, but is getting created dynamically by calling `ModelFieldDescriptor.get` method. Just a sanity check, if I tweak `_qb.Path(is_link=True, ...)` here, does it work in my case? Am I looking at correct code snippet? It does, nice. Ok, so I have to find information about this "field" being a link on `ModelFieldDescriptor`. It might be in any of the following properties: - `__gel_annotation__`, - `__gel_name__`, - `__gel_origin__`, - `__gel_resolved_descriptor__`, - `__gel_resolved_type__`, Let's rule out name and origin, and look into annotation, descriptor and type. The latter two might be `None`, so they might be an unreliable way of retrieving this info. Wait, no - the latter two are getting "resolved" (whatever that means here) at some stage, hopefully always before `def get()` is called. So let's look into `__gel_resolved_type__`, can I just check if represents an object type? Oh, I see - we are calling `get_resolved_type()` in `get()`, so I can use result of that. This `t` is supposed to be a subclass of `type[GelType]`, but let's check what the concrete type is here. ```python print(type(t)) print(t) print(type(type_)) print(type_) ``` ``` default.User.posts default.User.name ``` ``` <class 'models.__shapes__.std.__anyobject_meta__'> <class 'models.default.Post'> <class 'gel._internal._schemapath.SchemaPath'> default::Post <class 'gel._internal._qbmodel._abstract._base.GelTypeMeta'> <class 'models.__shapes__.std.str'> <class 'gel._internal._schemapath.SchemaPath'> std::str ``` Hmm, this confusing. We are dealing with three levels of types here (type of python variable, gel reflection type classes and then the actual gel types (default::Post, std::str)). What if I look into `__gel_annotation__` directly? It is supposed to be of type `type[Any]`, which is almost worthless annotation, so let's print it as well. ```python print(self.__gel_annotation__) ``` ``` OptionalMultiLink[models.default.Post] OptionalProperty[models.__shapes__.std.str, str] ``` Ok yup, this is what I need. These two type are defined in `_qbmodel._pydantic._fields` and are type aliases for `PointerInfo`, which is a data class, which has `kind: _edgeql.PointerKind`, which is either a property or a link. So let's try to print this `kind`: ```python print(self.__gel_annotation__.kind) ``` ... results in an exception in the middle of handling another exception. None of the files in the stack trace match my `_decriptors.py`, so ... ? Let's print more about `__gel_annotation__`: ```python print(type(self.__gel_annotation__)) ``` ``` <class 'types.GenericAlias'> ``` Ok, yes, to be fair `OptionalMultiLink` is a `types.TypeAliasType` of `types.Annotated` of `PointerInfo`. How is `__gel_annotation__` used elsewhere? Well, it is passed into `_typing_eval.resolve_type`. And that implemented recusivly, looking `value.__module__.__dict__`, accorning to PEP695, which feels like something internal I should try to avoid - at least is higher level code like our `ModelFieldDescriptor`. Again looking at code of `ModelFieldDescriptor.get()`, I see that I have to handle pointers that are: - defined in user schema (e.g. `User.posts`), - defined in built-in schema (e.g. `schema.Alias.type`), - ad-hoc user-defined computeds, which don't even have a `__gel_annotation__`. So the only reliable way of doing this is to actually check if the type is an object type. To do that, let's try to do an `isinstance` check on `t`. But I need to find the class to check for. Let's look at ancestors of `models.default.Post`, that are: - defined in `gel._internal`, not the generated `models/`, - imply that this is an object type, not a scalar, - is not a meta class, but a normal class. I'll try to avoid `__..._shape__` classes, they don't seem relevant here. I guess it's just `GelModel`? Let's try with `_base.AbstractGelModel`, because it is imported already, which is a hint that this is the preferred way of checking it. ```python print(isinstance(t, AbstractGelModel)) ``` ``` False False ``` Nope, either of `default.User.posts` and `default.User.name` returns `True`. It should have returned true for `posts`. Wait, I should be using `issubclass`. ```python print(isinstance(t, AbstractGelModel)) ``` ``` True False ``` Ladies and gentlemen, we got him. ```python metadata = _qb.Path( type_=type_, source=source, name=self.__gel_name__, is_lprop=False, is_link=issubclass(t, AbstractGelModel) ) return _qb.AnnotatedPath(t, metadata) ``` ... and it works. Let's run tests: ``` > python -m pytest -k test_modelgen_ =========================== 178 passed, 1 skipped, 861 deselected, 12 xfailed in 777.35s (0:12:57) ======== ```
1 parent e1e5ef3 commit cbc4899

File tree

1 file changed

+1
-0
lines changed

1 file changed

+1
-0
lines changed

gel/_internal/_qbmodel/_abstract/_descriptors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def get(
175175
source=source,
176176
name=self.__gel_name__,
177177
is_lprop=False,
178+
is_link=issubclass(t, AbstractGelModel)
178179
)
179180
return _qb.AnnotatedPath(t, metadata)
180181

0 commit comments

Comments
 (0)