Commit cbc4899
committed
Fix qbmodel
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) ========
```is_link on _qb.Path1 parent e1e5ef3 commit cbc4899
1 file changed
+1
-0
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
| 178 | + | |
178 | 179 | | |
179 | 180 | | |
180 | 181 | | |
| |||
0 commit comments