-
-
Notifications
You must be signed in to change notification settings - Fork 128
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Support annotate parameter in field to allow ORM annotations #377
Support annotate parameter in field to allow ORM annotations #377
Conversation
TypeOrIterable: TypeAlias = Union[_T, Iterable[_T]] | ||
UserType: TypeAlias = Union["AbstractBaseUser", "AnonymousUser"] | ||
PrefetchCallable: TypeAlias = Callable[[GraphQLResolveInfo], Prefetch] | ||
PrefetchType: TypeAlias = Union[str, Prefetch, PrefetchCallable] | ||
AnnotateCallable: TypeAlias = Callable[[GraphQLResolveInfo], BaseExpression] | ||
AnnotateType: TypeAlias = Union[BaseExpression, AnnotateCallable] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to double-check if BaseExpression
is the best or Expression
is better. BaseExpression
also allows Subquery
, and I believe I have examples in some projects were we had to use annotate
with Subquery
because other annotation types didn't cover our use cases. I'll check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be wrong here, but I do think BaseExpression
is the correct annotation indeed.
# Instead of the more redundant: | ||
field_store.annotate = { | ||
field.name: field_store.annotate[_annotate_placeholder], | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love the necessity to use _annotate_placeholder
. So please check if my comment here really makes sense or we can get rid of that. I think the field
object at instantiation time cannot know about what attr name it has inside the parent Type
class, because there's no metaclass in Type
. AFAIK, in other libraries (such as Django REST Framework), fields know what attrname they got on their parent class because of metaclasses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think your comment does make sense, and I actually love the fact that you can use annotate=Sum("price")
as a shortcut to annotate={"total": Sum("price")}
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting back to this comment after checking the whole PR: Maybe we don't need this, and instead we can just keep the expression in annotate
directly, and this if would be if isinstance(field_store.annotate, BaseExpression)
or even if field_store.annotate is not None and not isinstance(field_store.annotate, dict)
?
I mean, it would make sense to use _annotate_placeholder
if we could have more than 1 annotation together, but I'm assuming that it will never be the case. Or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the problem of not using _annotate_placeholder
is that typing will get more confusing in the OptimizerStore
. But I'll check if this is doable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we make annotate: dict[str, AnnotateType] | AnnotateType = dataclasses.field(default_factory=dict)
inside OptimizerStore
, this conditional logic will need handling in __ior__
, copy
, with_prefix
and apply
methods. Right now, the placeholder logic is only in with_hints
. Therefore, to avoid spreading this logic, I guess it's best to leave as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, forgot about that. I agree with you, let's leave as it for now :)
@@ -37,10 +39,13 @@ | |||
_Type = TypeVar("_Type", bound="StrawberryType | type") | |||
|
|||
TypeOrSequence: TypeAlias = Union[_T, Sequence[_T]] | |||
TypeOrMapping: TypeAlias = Union[_T, Mapping[str, _T]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love that annotate
needs a dict instead of a list, but I see no other way of specifying the annotation name in a case like this:
@strawberry_django.type(Milestone)
class MilestoneType(relay.Node):
@strawberry_django.field(
annotate={
"_issues_count": Count("issue")
},
)
def issues_count(self) -> int:
return self._issues_count # type: ignore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other way would be to create an object to hold that info, like this:
@dataclasses.dataclass
class Annotation:
field_name: str
annotation: BaseExpression | Callable
# and then use as
annotate=[Annotation(field_name="_issues_count", annotation=Count("issue"))]
But IMO I actually prefer the dict approach as it seems more straight forward
@@ -11,6 +11,8 @@ | |||
|
|||
DEBUG = True | |||
SECRET_KEY = 1 | |||
USE_TZ = True | |||
TIME_ZONE = "UTC" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to add this to avoid test warnings.
annotate=ExpressionWrapper( | ||
Q(due_date__lt=Now()), | ||
output_field=BooleanField(), | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Complex annotation example.
tests/projects/schema.py
Outdated
}, | ||
) | ||
def my_bugs_count(self) -> int: | ||
return self._my_bugs_count # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Complex callable annotation example. Considers current user.
{"node_id": e["id"]}, | ||
asserts_errors=False, | ||
) | ||
assert res.errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please LMK about additional tests I should implement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course more tests are always nice, but I think you got basically all of the core features covered nicely :)
Would it make sense to split the annotate functionality for Regarding type support: The current field support supports also works on the type's queryset itself, correct? I can see how this might help for downstream tasks on the objects which are not explicit fields on the type. |
IMHO, yes, as long as nothing that is expected to work in regular field actually doesn't.
Yes, since |
cd49dfb
to
a431bf2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks amazing!!
The -1 is for a couple of nitpicks
# Instead of the more redundant: | ||
field_store.annotate = { | ||
field.name: field_store.annotate[_annotate_placeholder], | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think your comment does make sense, and I actually love the fact that you can use annotate=Sum("price")
as a shortcut to annotate={"total": Sum("price")}
:)
@@ -37,10 +39,13 @@ | |||
_Type = TypeVar("_Type", bound="StrawberryType | type") | |||
|
|||
TypeOrSequence: TypeAlias = Union[_T, Sequence[_T]] | |||
TypeOrMapping: TypeAlias = Union[_T, Mapping[str, _T]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other way would be to create an object to hold that info, like this:
@dataclasses.dataclass
class Annotation:
field_name: str
annotation: BaseExpression | Callable
# and then use as
annotate=[Annotation(field_name="_issues_count", annotation=Count("issue"))]
But IMO I actually prefer the dict approach as it seems more straight forward
TypeOrIterable: TypeAlias = Union[_T, Iterable[_T]] | ||
UserType: TypeAlias = Union["AbstractBaseUser", "AnonymousUser"] | ||
PrefetchCallable: TypeAlias = Callable[[GraphQLResolveInfo], Prefetch] | ||
PrefetchType: TypeAlias = Union[str, Prefetch, PrefetchCallable] | ||
AnnotateCallable: TypeAlias = Callable[[GraphQLResolveInfo], BaseExpression] | ||
AnnotateType: TypeAlias = Union[BaseExpression, AnnotateCallable] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be wrong here, but I do think BaseExpression
is the correct annotation indeed.
{"node_id": e["id"]}, | ||
asserts_errors=False, | ||
) | ||
assert res.errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course more tests are always nice, but I think you got basically all of the core features covered nicely :)
# Instead of the more redundant: | ||
field_store.annotate = { | ||
field.name: field_store.annotate[_annotate_placeholder], | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting back to this comment after checking the whole PR: Maybe we don't need this, and instead we can just keep the expression in annotate
directly, and this if would be if isinstance(field_store.annotate, BaseExpression)
or even if field_store.annotate is not None and not isinstance(field_store.annotate, dict)
?
I mean, it would make sense to use _annotate_placeholder
if we could have more than 1 annotation together, but I'm assuming that it will never be the case. Or am I missing something?
5f4bc16
to
5a09351
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍🏼
# Instead of the more redundant: | ||
field_store.annotate = { | ||
field.name: field_store.annotate[_annotate_placeholder], | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, forgot about that. I agree with you, let's leave as it for now :)
The typing issue is due to pyright's yesterday release. Going to merge this as is and fix the pyright issue myself, as it is not this PRs fault. |
Description
Adds support for ORM annotations as fields. Looks like this:
And this:
There are some shortcomings and implementation details that I discuss more in-depth in PR comments. The main one is this only works if
DjangoOptimizerExtension
is enabled. Although that doesn't look like a major issue to me, because custom prefetches also have the same limitation (see the existingtests/test_optimizer.py::test_query_prefetch_with_callable
).As of Sept 27 2023, this PR is still missing docs. I'll wait for feedback on implementation and field "syntax" before making the docs.I already added some tests, but perhaps we need more. I still also have to test manually a bit more.
--- EDIT: I found more missing stuff:
ModelProperty
(seestrawberry_django/descriptors.py
)annotate
support toPrefetchInspector
. (EDIT 2: seems this is already done viaquery.annotations
)annotate
support intype
? (seestrawberry_django/type.py
)Types of Changes
Issues Fixed or Closed by This PR
Checklist