-
-
Notifications
You must be signed in to change notification settings - Fork 543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Strawberry decorator #473
Conversation
Hi, thanks for contributing to Strawberry 🍓! We noticed that this PR is missing a So as soon as this PR is merged, a release will be made 🚀. Here's an example of Release type: patch
Description of the changes, ideally with some examples, if adding a new feature. Release type can be one of patch, minor or major. We use semver,so make sure to pick the appropriate type. If in doubt feel free to ask :) |
Codecov Report
@@ Coverage Diff @@
## main #473 +/- ##
=======================================
Coverage 98.37% 98.37%
=======================================
Files 70 70
Lines 2273 2273
Branches 312 312
=======================================
Hits 2236 2236
Misses 19 19
Partials 18 18 |
On naming, maybe |
Could this also be used to implement a |
strawberry/decorator.py
Outdated
# has the opportunity to modify them | ||
return func(wrapped, source, info=info, **kwargs) | ||
|
||
wrapped_resolver._strawberry_decorator = True |
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.
Could we return a Callable wrapper class instead of the function monkeypatched? A benefit now would be the ability to use isinstance
instead of hasattr
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 would also play into my effort to try to remove a lot of the monkeypatching from elsewhere in the codebase
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.
Sure that would also work. I'll refactor the code to that.
As an aside what's the benefit of using isinstance
instead of hasattr
? To me they seem pretty much equivalent and I'd imagine that isinstance
has (marginally) worse performance.
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.
At minimum, we can add typing annotations for mypy/IDEs to provide warnings/suggestions (without having to wait for typing.Protocol in 3.8).
Symbols are also more easily refactored and indexed than strings (although, this specific argument could potentially be fixed with variables for setattr
/getattr
/hasattr
).
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.
without having to wait for typing.Protocol in 3.8
we can already use them via typing_extensions :)
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.
So I've just had a go at this I don't actually think it's possible. I changed it to something like this:
class StrawberryDecorator:
def __init__(self, resolver):
self.resolver = resolver
functools.update_wrapper(self, resolver) # this is necessary so that `StrawberryField` can extract the arguments and name
def __call__(self, *args, **kwargs):
return self.resolver(*args, **kwargs)
def make_strawberry_decorator(func):
def decorator(resolver):
function_args = get_func_args(resolver)
@functools.wraps(resolver)
def wrapped_resolver(source, info, **kwargs):
def wrapped(**kwargs):
if isinstance(resolver, StrawberryDecorator): # <-- this does not work
return resolver(source, info, **kwargs)
args, extra_kwargs = get_resolver_arguments(function_args, source, info)
return resolver(*args, **extra_kwargs, **kwargs)
return func(wrapped, source, info=info, **kwargs)
return StrawberryDecorator(wrapped_resolver)
return decorator
Because of the functools.update_wrapper
call the isinstance(resolver, StrawberrryDecorator)
doesn't work which defeats the point of using the class in the first place.
I think the best option is to just stick with the monkeypatching even if it's not ideal.
Or even more simply: just |
I feel like being explicit about the use case is a good idea. I can't think of any further use cases, but that doesn't negate my main reasoning here. |
In GraphQL terms, deprecated isn't actually a directive, it's just represented as one in the SDL. Given Strawberry doesn't lean on the SDL, it doesn't feel right to parrot that misunderstanding |
Actually, scrub that. As long as it's clear in the docs that decorators != directives, a deprecated decorator feels like a good idea, it'd be quite ergonomic. Worth spiking out? |
How would the decorator mark the field as deprecated? The decorator gets applied before the |
I like the sound of this, but how can we replicate the same behaviour with simple fields, like this: @strawberry.type
class User:
email: str = strawberry.field(permission_classes=[]) ? |
What about exposing @strawberry.type
class User:
email: str = strawberry.field(resolver=permission_classes([a,b,c])(strawberry.default_resolver)) or @strawberry.type
class Query:
name: str = strawberry.field(resolver=upper_case(strawberry.default_resolver)) But if it becomes a common use case, maybe provide a little sugar: @strawberry.type
class Query:
name: str = strawberry.field(resolver_decorators=[permission_classes([a,b,c]), upper_case)]) But the latter could be done in user-land by wrapping strawberry.field anyway. |
Good point, I hadn't considered that. Exposing the default resolver (as @AndrewIngram suggested) makes sense to me. Replacing the |
Yup, didn't try to dismiss the proposal, I was curious about the plain field usecase, I think it is worth discussing it now as well, since this decorator pattern can change how we do things (for the better) :) |
strawberry/decorator.py
Outdated
# has the opportunity to modify them | ||
return func(wrapped, source, info=info, **kwargs) | ||
|
||
wrapped_resolver._strawberry_decorator = True |
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.
without having to wait for typing.Protocol in 3.8
we can already use them via typing_extensions :)
strawberry/resolvers.py
Outdated
@@ -6,7 +6,34 @@ | |||
from .utils.inspect import get_func_args | |||
|
|||
|
|||
def get_resolver_arguments(function_args, source, info): |
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.
👍
6bf308e
to
b44f7a4
Compare
So @patrick91 @AndrewIngram I've spent some time trying to get decorators working for simple fields and it's turned out to be much harder than I expected. Exposing the default resolver is not that straightforward since the resolver needs to know the name of the current field which is not available when the decorator is called. It could probably be done with lots of special casing but I don't think it's worth it. That has lead me to this API instead: @strawberry.type
class Query:
name: str = strawberry.field(decorators=[upper_case]) What do you think? |
@jkimbo what did the code look like where you needed the field but didn't have it? |
@AndrewIngram like this: @strawberry.type
class Query:
name: str = strawberry.field(resolver=upper_case(strawberry.default_field_resolver)) |
I'm still not seeing why this needs to know the field, isn't the default resolver just a function that looks at the info object when it's executed? What's not working at the time you're wrapping the function? |
Because strawberry camel cases the field name you can't rely on the name on the info object to get the right attribute. I guess we could un-camel-case it to try and get the correct attribute... |
What if we did something along the lines of this: class StrawberryDecorator:
def __init__(self, func: Callable, *args, **kwargs):
self.func = func
def __call__(self, field: StrawberryField) -> StrawberryField:
wrapped_resolver = lambda: self.func(field.resolver)
field.resolver = wrapped_resolver
return field
#######
upper_case = StrawberryDecorator(lambda string: string.upper())
@strawberry.type
class Query:
@upper_case
@strawberry.field
def name() -> str:
return "blah"
other_name: str = upper_case(strawberry.field()) Pros:
Cons:
|
@BryceBeagle hmm that might be a better option. I'd have to play around to see if it's possible. |
@jkimbo what if, at type generation we replace the default resolver with something that had the field name in it like |
@patrick91 I tried that but the issue is that the decorator function calls the resolver so it needs to know the field name when it's created (i.e. when |
Where did we land on this? I’d love to use this for some auth decorators and it looks pretty good to me :) |
@benwis we are working on some refactoring before landing this, can't commit to a timeline yet, but it shouldn't take too long. In the meanwhile I'll rebase the PR later today so it's easier to merge it in future :) |
fed79be
to
45a1737
Compare
@jkimbo @BryceBeagle I've rebased and broken this PR. Will try to fix it over the weekend |
And add check for strawberry decorators
45a1737
to
25f7419
Compare
b1c0c5c
to
eb6f014
Compare
I've spent a bit of time on this PR and looks @BryceBeagle updates plus @functools.wraps made things much easier to implement, this just works now: def upper_case(resolver):
@wraps(resolver)
def wrapped(*args, **kwargs):
return resolver(*args, **kwargs).upper()
return wrapped
@strawberry.type
class Query:
@strawberry.field
@upper_case
def greeting(self) -> str:
return "hi"
schema = strawberry.Schema(query=Query)
result = schema.execute_sync("query { greeting }")
assert not result.errors
assert result.data == {"greeting": "HI"} so I think we add the tests and add some docs for this. functools.wraps is used to copy type annotations (and other things too, but mostly that) |
I love this feature and have already started using it based on this PR. If you guys can point me in the right direction, I'm happy to try my hand at creating some docs/tests to get this merged in. |
hey @benwis I think you should be able to use decorators already (based on the tests), I think we might want to provide our own version of functools.wraps that know how to deal with the |
That's great news. Saves me a lot of trouble. Still willing to help write tests or docs if that's helpful! |
+1 for this feature, really looking forward as we need it in our prod setup. Would be glad to help out |
Closing at this was implemented by @erikwrede in #2567 |
Add
make_strawberry_decorator
function to create decorators that can be applied to resolvers.I'm creating this as draft PR to see if this approach seems like a viable one. If people are happy with it then I can add some documentation as well.
Description
Being able to apply decorators to resolver functions is a valuable feature because it allows the developer to encapsulate common logic that needs to be applied across multiple resolvers. It also solves most of the use cases for directives in SDL based GraphQL libraries. However, because Strawberry uses dependency injection to determine what arguments to pass to the resolver function, a plain decorator implementation would only have access to the arguments that the resolver function defines. This unnecessarily couples the decorator and the resolver function.
This PR introduces a new function
make_strawberry_decorator
that decorates a decorator function (functional programming ftw) and wraps up the required logic so that the decorator function body can have access to thesource
andinfo
arguments even if the resolver doesn't require them.For example:
Note: in the PR I also hoisted the
get_func_args
call to init time rather than runtime. This probably means the cache onget_func_args
can be removed but I haven't done that as part of this change because I'm not sure what other implications that would have.Extra note: this is an aside but IMO using decorators replaces the need to have specific permissions support on
strawberry.field
because it could be replaced with a decorator like this:Types of Changes
Issues Fixed or Closed by This PR
Checklist