Skip to content

Commit 95daebf

Browse files
Birdi7jacobmoshipcobellini666
authored
Fix Maybe usage with Annotated and explicit field definition (#4084)
Co-authored-by: Jacob Allen <jacoballen@moshipco.com> Co-authored-by: Thiago Bellini Ribeiro <hackedbellini@gmail.com>
1 parent 3bde027 commit 95daebf

4 files changed

Lines changed: 183 additions & 4 deletions

File tree

RELEASE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Release type: patch
2+
3+
Fixed two bugs where using `strawberry.Maybe` wrapped in `Annotated` or using an explicit field definition would raise a `TypeError` about "missing 1 required keyword-only argument", even though a `Maybe` field should allow `None` in all cases.
4+
5+
This fix addresses this via custom handling for annotations wrapped with `Annotated` and handling custom `field` with no `default` and no `default_factory` as possible to be `None`.

strawberry/types/maybe.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22
import typing
3-
from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
3+
from typing import TYPE_CHECKING, Annotated, Any, Generic, TypeAlias, TypeVar
44

55
T = TypeVar("T")
66

@@ -47,7 +47,10 @@ def _annotation_is_maybe(annotation: Any) -> bool:
4747
# Checking for the pattern should be good enough for now.
4848
return _maybe_re.match(annotation) is not None
4949

50-
return (orig := typing.get_origin(annotation)) and orig is Maybe
50+
orig = typing.get_origin(annotation)
51+
if orig is Annotated:
52+
return _annotation_is_maybe(typing.get_args(annotation)[0])
53+
return orig is Maybe
5154

5255

5356
__all__ = [

strawberry/types/object_type.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,15 @@ def _inject_default_for_maybe_annotations(
109109
) -> None:
110110
"""Inject `= None` for fields with `Maybe` annotations and no default value."""
111111
for name, annotation in annotations.copy().items():
112-
if _annotation_is_maybe(annotation) and not hasattr(cls, name):
113-
setattr(cls, name, None)
112+
if _annotation_is_maybe(annotation):
113+
if not hasattr(cls, name):
114+
setattr(cls, name, None)
115+
elif (
116+
isinstance(attr := getattr(cls, name), StrawberryField)
117+
and attr.default is dataclasses.MISSING
118+
and attr.default_factory is dataclasses.MISSING
119+
):
120+
attr.default = None
114121

115122

116123
def _process_type(

tests/schema/test_maybe.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from textwrap import dedent
2+
from typing import Annotated
23

34
import pytest
45

@@ -998,3 +999,166 @@ def test_comprehensive(self, input: ComprehensiveInput) -> str:
998999
""")
9991000
assert not result5.errors # No error - both fields are optional in schema
10001001
assert result5.data == {"testComprehensive": "strict=absent, flexible=world"}
1002+
1003+
1004+
def test_maybe_with_explicit_field_description():
1005+
"""Handle case where strawberry.field annotation is used on a field with Maybe[T] type."""
1006+
1007+
@strawberry.input
1008+
class InputData:
1009+
name: strawberry.Maybe[str | None] = strawberry.field(
1010+
description="This strawberry.field annotation was breaking in default injection"
1011+
)
1012+
1013+
@strawberry.type
1014+
class Query:
1015+
@strawberry.field
1016+
def test(self, data: InputData) -> str:
1017+
if data.name is None:
1018+
return "I am a test, and I received: None"
1019+
return "I am a test, and I received: " + str(data.name.value)
1020+
1021+
schema = strawberry.Schema(Query)
1022+
1023+
assert str(schema) == dedent(
1024+
'''\
1025+
input InputData {
1026+
"""This strawberry.field annotation was breaking in default injection"""
1027+
name: String
1028+
}
1029+
1030+
type Query {
1031+
test(data: InputData!): String!
1032+
}'''
1033+
)
1034+
1035+
query1 = """
1036+
query {
1037+
test(data: { name: null })
1038+
}
1039+
"""
1040+
result1 = schema.execute_sync(query1)
1041+
assert not result1.errors
1042+
1043+
query2 = """
1044+
query {
1045+
test(data: { name: "hello" })
1046+
}
1047+
"""
1048+
result2 = schema.execute_sync(query2)
1049+
assert not result2.errors
1050+
1051+
query3 = """
1052+
query {
1053+
test(data: {})
1054+
}
1055+
"""
1056+
result3 = schema.execute_sync(query3)
1057+
assert not result3.errors
1058+
1059+
1060+
def test_maybe_wrapped_with_annotated_typing():
1061+
"""Handle case where Maybe is wrapped with Annotated typing."""
1062+
1063+
@strawberry.input
1064+
class InputData:
1065+
name: Annotated[strawberry.Maybe[str | None], "some meta"]
1066+
1067+
@strawberry.type
1068+
class Query:
1069+
@strawberry.field
1070+
def test(self, data: InputData) -> str:
1071+
if data.name is None:
1072+
return "I am a test, and I received: None"
1073+
return "I am a test, and I received: " + str(data.name.value)
1074+
1075+
schema = strawberry.Schema(Query)
1076+
1077+
assert str(schema) == dedent(
1078+
"""\
1079+
input InputData {
1080+
name: String
1081+
}
1082+
1083+
type Query {
1084+
test(data: InputData!): String!
1085+
}"""
1086+
)
1087+
query1 = """
1088+
query {
1089+
test(data: { name: null })
1090+
}
1091+
"""
1092+
result1 = schema.execute_sync(query1)
1093+
assert not result1.errors
1094+
1095+
query2 = """
1096+
query {
1097+
test(data: { name: "hello" })
1098+
}
1099+
"""
1100+
result2 = schema.execute_sync(query2)
1101+
assert not result2.errors
1102+
1103+
query3 = """
1104+
query {
1105+
test(data: {})
1106+
}
1107+
"""
1108+
result3 = schema.execute_sync(query3)
1109+
assert not result3.errors
1110+
1111+
1112+
def test_maybe_with_annotated_and_explicit_definition():
1113+
"""Handle case where Maybe is wrapped with Annotated typing."""
1114+
1115+
@strawberry.input
1116+
class InputData:
1117+
name: Annotated[strawberry.Maybe[str | None], "some meta"] = strawberry.field(
1118+
description="This strawberry.field annotation was breaking in default injection"
1119+
)
1120+
1121+
@strawberry.type
1122+
class Query:
1123+
@strawberry.field
1124+
def test(self, data: InputData) -> str:
1125+
if data.name is None:
1126+
return "I am a test, and I received: None"
1127+
return "I am a test, and I received: " + str(data.name.value)
1128+
1129+
schema = strawberry.Schema(Query)
1130+
1131+
assert str(schema) == dedent(
1132+
'''\
1133+
input InputData {
1134+
"""This strawberry.field annotation was breaking in default injection"""
1135+
name: String
1136+
}
1137+
1138+
type Query {
1139+
test(data: InputData!): String!
1140+
}'''
1141+
)
1142+
query1 = """
1143+
query {
1144+
test(data: { name: null })
1145+
}
1146+
"""
1147+
result1 = schema.execute_sync(query1)
1148+
assert not result1.errors
1149+
1150+
query2 = """
1151+
query {
1152+
test(data: { name: "hello" })
1153+
}
1154+
"""
1155+
result2 = schema.execute_sync(query2)
1156+
assert not result2.errors
1157+
1158+
query3 = """
1159+
query {
1160+
test(data: {})
1161+
}
1162+
"""
1163+
result3 = schema.execute_sync(query3)
1164+
assert not result3.errors

0 commit comments

Comments
 (0)