Skip to content
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

Implement oneOf #3429

Merged
merged 28 commits into from
May 22, 2024
Merged

Implement oneOf #3429

merged 28 commits into from
May 22, 2024

Conversation

patrick91
Copy link
Member

@patrick91 patrick91 commented Mar 31, 2024

We are still missing the equivalent of the check in coerce value

Mostly based on https://github.com/graphql/graphql-js/pull/3513/files

Closes #2421 #2560

TODO:

  • Check print of schema directive
  • Test with a frontend library
  • schema codegen support
  • Introspection support?

We are still missing the equivalent of the check in coerce value
Copy link

codecov bot commented Mar 31, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 96.53%. Comparing base (fc322a8) to head (a79e961).
Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3429      +/-   ##
==========================================
+ Coverage   96.51%   96.53%   +0.02%     
==========================================
  Files         511      516       +5     
  Lines       32965    33174     +209     
  Branches     5482     5521      +39     
==========================================
+ Hits        31815    32025     +210     
+ Misses        917      916       -1     
  Partials      233      233              

@patrick91
Copy link
Member Author

also, I'm not sure this is best approach for us, I'll think about it in the next few days

Copy link

codspeed-hq bot commented Mar 31, 2024

CodSpeed Performance Report

Merging #3429 will not alter performance

Comparing feature/one-of (a79e961) with main (a8b108e)

Summary

✅ 12 untouched benchmarks

@patrick91
Copy link
Member Author

so far I've added a out_type on the GraphQL input which seems to be the best place to check for one of.

But I'm not sure on the check 🤔 At the moment we need to use unset, otherwise we'd get an error since some of the keys won't be populated. Maybe we can allow None and change the check? I need to read the spec 😊

@patrick91
Copy link
Member Author

spec is here: graphql/graphql-spec#825

@patrick91
Copy link
Member Author

I don't want to overcomplicate this, but it would be interesting to investigate if we can use @overload for this too, something like:

import strawberry

@strawberry.type
class Query:
    @strawberry.field
    def search(self, query: str) -> list[str]:
        ...

    @strawberry.field
    @overload
    def search(self, something_else: str) -> list[str]:
        ...

(I'm actually not sure this is valid in python either, but just writing it down in case it might be useful in future)

@patrick91
Copy link
Member Author

I think this is almost ready to go! I just need to test it with codegen 😊

@patrick91
Copy link
Member Author

actually, maybe we should introduce a new decorator to make this easier to use:

@strawberry.one_of_input
class ExampleInputTagged:
    a: str | None
    b: int | None

@patrick91
Copy link
Member Author

/pre-release

@botberry
Copy link
Member

botberry commented Apr 18, 2024

Pre-release

👋

Pre-release 0.230.0.dev.1716318708 [2dc1a07] has been released on PyPi! 🚀
You can try it by doing:

poetry add strawberry-graphql==0.230.0.dev.1716318708

@botberry
Copy link
Member

Pre-release

👋

Pre-release 0.227.0.dev.1713463204 [a9b1edd] has been released on PyPi! 🚀
You can try it by doing:

poetry add strawberry-graphql==0.227.0.dev.1713463204

Copy link
Contributor

@bradleyoesch bradleyoesch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for coding this up!! Looks great

strawberry/object_type.py Outdated Show resolved Hide resolved
strawberry/schema/execute.py Outdated Show resolved Hide resolved
# We are populating all missing keys with `None`, so users
# don't have to set an explicit default for the input type
# The alternative is to tell them to use `UNSET`, but it looks
# a bit unfriendly to use
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW having to manually default to UNSET for optional input fields is the current behavior/expectation (unless I'm mistaken/misusing), so not requiring the same here is a little inconsistent

i.e. you'd have to add UNSET default on normal inputs but not one-of inputs

@strawberry.input
class FooInput:
  a: str | None = UNSET
  b: str | None = field(default_value=UNSET, description="foo bar")

@strawberry.input(directives=[OneOf()])
class FooInput:
  a: str | None
  b: str | None = field(description="foo bar")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you prefer to use UNSET to keep the consistency? 😊

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think UNSET would be better, as null can sometimes have meaning

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bellini666 just to clarify, we don't use null, as in we, don't show it in the schema, it's just a shortcut to prevent to do this:

@strawberry.input(one_of=True)
class FooInput:
  a: str | None = strawberry.UNSET
  b: str | None = strawberry.UNSET

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patrick91 ah got it! I think I also misread the original comment here

I agree that not requiring setting this to strawberry.UNSET is an unexpected behavior, but at the same time I can see how this would be justified by the fact that one_of=True is somewhat implicitly telling that the input need to receive specific one argument.

I will generally choose the consistency path, so requiring to set this to UNSET would be my option. But I don't have literally nothing against not doing that.

Also, maybe we should reconsider having to do that in first place for strawberry in general? I mean, GraphQL itself will accept null and UNSET for optional fields, so maybe we should default anything that doesn't define a default value to UNSET as long as it is optional? What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok! I'll make the change 😊

Also, maybe we should reconsider having to do that in first place for strawberry in general? I mean, GraphQL itself will accept null and UNSET for optional fields, so maybe we should default anything that doesn't define a default value to UNSET as long as it is optional? What do you think?

mmh, this is probably a big breaking change, maybe we can discuss it on a call? we could make this configurable, but I think I need to think about it more 😊

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will generally choose the consistency path, so requiring to set this to UNSET would be my option. But I don't have literally nothing against not doing that.

+1 on consistency but I don't feel too strongly about it here because "the fact that one_of=True is somewhat implicitly telling that the input need to receive specific one argument."

Also, maybe we should reconsider having to do that in first place for strawberry in general? I mean, GraphQL itself will accept null and UNSET for optional fields, so maybe we should default anything that doesn't define a default value to UNSET as long as it is optional? What do you think?

Agreed that if an argument is optional I would expect it automatically to default to UNSET, but I also understand this is a big change...

I don't remember what actually happens if you don't set the default to UNSET... I think it generates the schema fine but requires clients to explicitly send null? I can't remember.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradleyoesch sorry for the late reply

I don't remember what actually happens if you don't set the default to UNSET... I think it generates the schema fine but requires clients to explicitly send null? I can't remember.

you can't send explicit null (it's forbidden by the specification), the issue you get is that the __init__ function will complain because we didn't send all the arguments

@bradleyoesch
Copy link
Contributor

/pre-release

Oh woops didn't see this was marked pre-release literally hours before making my comments!

@patrick91
Copy link
Member Author

I've tested this with GraphQL-codegen and it works when using the SDL, but doesn't work when using introspection, but that's not our issue 😊

@patrick91 patrick91 marked this pull request as ready for review April 18, 2024 21:18
@botberry
Copy link
Member

botberry commented Apr 18, 2024

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


This release adds support for @oneOf on input types! 🎉 You can use
one_of=True on input types to create an input type that should only have one
of the fields set.

import strawberry


@strawberry.input(one_of=True)
class ExampleInputTagged:
    a: str | None = strawberry.UNSET
    b: int | None = strawberry.UNSET

Here's the tweet text:

🆕 Release (next) is out! Thanks to @patrick91 for the PR 👏


We now support the `@oneOf` directive on input types, ensuring exactly one
key is specified.

Check out the details and improvements! Get it here 👉 https://strawberry.rocks/release/(next)

@botberry
Copy link
Member

botberry commented Apr 18, 2024

Apollo Federation Subgraph Compatibility Results

Federation 1 Support Federation 2 Support
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢

Learn more:

@patrick91
Copy link
Member Author

/pre-release

@patrick91
Copy link
Member Author

@bradleyoesch feel free to try the new pre-release 😊

Copy link
Contributor

@bellini666 bellini666 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great! :)

Left a small nit and I agree with the suggestion of using UNSET instead of null

# We are populating all missing keys with `None`, so users
# don't have to set an explicit default for the input type
# The alternative is to tell them to use `UNSET`, but it looks
# a bit unfriendly to use
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think UNSET would be better, as null can sometimes have meaning

strawberry/types/types.py Outdated Show resolved Hide resolved
name: String
email: String
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think supplying the @strawberry.input(directives=[OneOf()]) directive example too would be nice. I could see some developers wanting to use the directive over the argument to keep the code more similar to the generated schema

(or otherwise document the new OneOf() directive somewhere

@@ -83,6 +85,9 @@ def _impl_type(
if is_interface_object:
directives.append(InterfaceObject())

if one_of:
directives.append(OneOf())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as familiar with dynamic imports but should from strawberry.schema_directives import OneOf go in this conditional so it's only imported when used?

Same question in strawberry/object_type.py

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is ok to have the import there, just to keep the code simple 😊

@patrick91
Copy link
Member Author

/pre-release

@patrick91 patrick91 merged commit 82eeaa5 into main May 22, 2024
65 checks passed
@patrick91 patrick91 deleted the feature/one-of branch May 22, 2024 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement oneOf for input types
4 participants