strawberry-graphql 0.279.0.dev1754156227__py3-none-any.whl → 0.280.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. strawberry/__init__.py +1 -2
  2. strawberry/aiohttp/views.py +2 -48
  3. strawberry/asgi/__init__.py +2 -37
  4. strawberry/chalice/views.py +7 -75
  5. strawberry/channels/handlers/http_handler.py +30 -6
  6. strawberry/cli/commands/upgrade/__init__.py +2 -0
  7. strawberry/codemods/__init__.py +9 -0
  8. strawberry/codemods/maybe_optional.py +118 -0
  9. strawberry/django/views.py +4 -73
  10. strawberry/experimental/pydantic/_compat.py +1 -0
  11. strawberry/experimental/pydantic/error_type.py +1 -0
  12. strawberry/experimental/pydantic/fields.py +1 -0
  13. strawberry/experimental/pydantic/utils.py +1 -0
  14. strawberry/fastapi/router.py +8 -4
  15. strawberry/flask/views.py +4 -74
  16. strawberry/http/async_base_view.py +5 -31
  17. strawberry/http/base.py +2 -1
  18. strawberry/http/exceptions.py +5 -7
  19. strawberry/http/sync_base_view.py +1 -34
  20. strawberry/litestar/controller.py +1 -39
  21. strawberry/quart/views.py +3 -33
  22. strawberry/sanic/views.py +4 -43
  23. strawberry/schema/schema.py +2 -0
  24. strawberry/schema/schema_converter.py +7 -0
  25. strawberry/schema/validation_rules/maybe_null.py +136 -0
  26. strawberry/types/arguments.py +16 -2
  27. strawberry/types/maybe.py +1 -1
  28. {strawberry_graphql-0.279.0.dev1754156227.dist-info → strawberry_graphql-0.280.0.dist-info}/METADATA +2 -1
  29. {strawberry_graphql-0.279.0.dev1754156227.dist-info → strawberry_graphql-0.280.0.dist-info}/RECORD +32 -34
  30. strawberry/pydantic/__init__.py +0 -22
  31. strawberry/pydantic/error.py +0 -51
  32. strawberry/pydantic/fields.py +0 -202
  33. strawberry/pydantic/object_type.py +0 -348
  34. {strawberry_graphql-0.279.0.dev1754156227.dist-info → strawberry_graphql-0.280.0.dist-info}/LICENSE +0 -0
  35. {strawberry_graphql-0.279.0.dev1754156227.dist-info → strawberry_graphql-0.280.0.dist-info}/WHEEL +0 -0
  36. {strawberry_graphql-0.279.0.dev1754156227.dist-info → strawberry_graphql-0.280.0.dist-info}/entry_points.txt +0 -0
strawberry/flask/views.py CHANGED
@@ -3,67 +3,27 @@ from __future__ import annotations
3
3
  import warnings
4
4
  from typing import (
5
5
  TYPE_CHECKING,
6
- Any,
7
6
  ClassVar,
8
7
  Optional,
9
8
  Union,
10
- cast,
11
9
  )
12
10
  from typing_extensions import TypeGuard
13
11
 
12
+ from lia import AsyncFlaskHTTPRequestAdapter, FlaskHTTPRequestAdapter, HTTPException
13
+
14
14
  from flask import Request, Response, render_template_string, request
15
15
  from flask.views import View
16
- from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
17
- from strawberry.http.exceptions import HTTPException
18
- from strawberry.http.sync_base_view import (
19
- SyncBaseHTTPView,
20
- SyncHTTPRequestAdapter,
21
- )
22
- from strawberry.http.types import FormData, HTTPMethod, QueryParams
16
+ from strawberry.http.async_base_view import AsyncBaseHTTPView
17
+ from strawberry.http.sync_base_view import SyncBaseHTTPView
23
18
  from strawberry.http.typevars import Context, RootValue
24
19
 
25
20
  if TYPE_CHECKING:
26
- from collections.abc import Mapping
27
-
28
21
  from flask.typing import ResponseReturnValue
29
22
  from strawberry.http import GraphQLHTTPResponse
30
23
  from strawberry.http.ides import GraphQL_IDE
31
24
  from strawberry.schema.base import BaseSchema
32
25
 
33
26
 
34
- class FlaskHTTPRequestAdapter(SyncHTTPRequestAdapter):
35
- def __init__(self, request: Request) -> None:
36
- self.request = request
37
-
38
- @property
39
- def query_params(self) -> QueryParams:
40
- return self.request.args.to_dict()
41
-
42
- @property
43
- def body(self) -> Union[str, bytes]:
44
- return self.request.data.decode()
45
-
46
- @property
47
- def method(self) -> HTTPMethod:
48
- return cast("HTTPMethod", self.request.method.upper())
49
-
50
- @property
51
- def headers(self) -> Mapping[str, str]:
52
- return self.request.headers # type: ignore
53
-
54
- @property
55
- def post_data(self) -> Mapping[str, Union[str, bytes]]:
56
- return self.request.form
57
-
58
- @property
59
- def files(self) -> Mapping[str, Any]:
60
- return self.request.files
61
-
62
- @property
63
- def content_type(self) -> Optional[str]:
64
- return self.request.content_type
65
-
66
-
67
27
  class BaseGraphQLView:
68
28
  graphql_ide: Optional[GraphQL_IDE]
69
29
 
@@ -131,36 +91,6 @@ class GraphQLView(
131
91
  return render_template_string(self.graphql_ide_html) # type: ignore
132
92
 
133
93
 
134
- class AsyncFlaskHTTPRequestAdapter(AsyncHTTPRequestAdapter):
135
- def __init__(self, request: Request) -> None:
136
- self.request = request
137
-
138
- @property
139
- def query_params(self) -> QueryParams:
140
- return self.request.args.to_dict()
141
-
142
- @property
143
- def method(self) -> HTTPMethod:
144
- return cast("HTTPMethod", self.request.method.upper())
145
-
146
- @property
147
- def content_type(self) -> Optional[str]:
148
- return self.request.content_type
149
-
150
- @property
151
- def headers(self) -> Mapping[str, str]:
152
- return self.request.headers # type: ignore
153
-
154
- async def get_body(self) -> str:
155
- return self.request.data.decode()
156
-
157
- async def get_form_data(self) -> FormData:
158
- return FormData(
159
- files=self.request.files,
160
- form=self.request.form,
161
- )
162
-
163
-
164
94
  class AsyncGraphQLView(
165
95
  BaseGraphQLView,
166
96
  AsyncBaseHTTPView[
@@ -17,6 +17,7 @@ from typing import (
17
17
  from typing_extensions import TypeGuard
18
18
 
19
19
  from graphql import GraphQLError
20
+ from lia import AsyncHTTPRequestAdapter, HTTPException
20
21
 
21
22
  from strawberry.exceptions import MissingQueryError
22
23
  from strawberry.file_uploads.utils import replace_placeholders_with_files
@@ -44,9 +45,7 @@ from strawberry.types.graphql import OperationType
44
45
  from strawberry.types.unset import UNSET, UnsetType
45
46
 
46
47
  from .base import BaseView
47
- from .exceptions import HTTPException
48
48
  from .parse_content_type import parse_content_type
49
- from .types import FormData, HTTPMethod, QueryParams
50
49
  from .typevars import (
51
50
  Context,
52
51
  Request,
@@ -58,30 +57,6 @@ from .typevars import (
58
57
  )
59
58
 
60
59
 
61
- class AsyncHTTPRequestAdapter(abc.ABC):
62
- @property
63
- @abc.abstractmethod
64
- def query_params(self) -> QueryParams: ...
65
-
66
- @property
67
- @abc.abstractmethod
68
- def method(self) -> HTTPMethod: ...
69
-
70
- @property
71
- @abc.abstractmethod
72
- def headers(self) -> Mapping[str, str]: ...
73
-
74
- @property
75
- @abc.abstractmethod
76
- def content_type(self) -> Optional[str]: ...
77
-
78
- @abc.abstractmethod
79
- async def get_body(self) -> Union[str, bytes]: ...
80
-
81
- @abc.abstractmethod
82
- async def get_form_data(self) -> FormData: ...
83
-
84
-
85
60
  class AsyncWebSocketAdapter(abc.ABC):
86
61
  def __init__(self, view: "AsyncBaseHTTPView") -> None:
87
62
  self.view = view
@@ -284,8 +259,9 @@ class AsyncBaseHTTPView(
284
259
  except ValueError as e:
285
260
  raise HTTPException(400, "Unable to parse the multipart body") from e
286
261
 
287
- operations = form_data["form"].get("operations", "{}")
288
- files_map = form_data["form"].get("map", "{}")
262
+ operations = form_data.form.get("operations", "{}")
263
+ files_map = form_data.form.get("map", "{}")
264
+ files = form_data.files
289
265
 
290
266
  if isinstance(operations, (bytes, str)):
291
267
  operations = self.parse_json(operations)
@@ -294,9 +270,7 @@ class AsyncBaseHTTPView(
294
270
  files_map = self.parse_json(files_map)
295
271
 
296
272
  try:
297
- return replace_placeholders_with_files(
298
- operations, files_map, form_data["files"]
299
- )
273
+ return replace_placeholders_with_files(operations, files_map, files)
300
274
  except KeyError as e:
301
275
  raise HTTPException(400, "File(s) missing in form data") from e
302
276
 
strawberry/http/base.py CHANGED
@@ -3,12 +3,13 @@ from collections.abc import Mapping
3
3
  from typing import Any, Generic, Optional, Union
4
4
  from typing_extensions import Protocol
5
5
 
6
+ from lia import HTTPException
7
+
6
8
  from strawberry.http import GraphQLRequestData
7
9
  from strawberry.http.ides import GraphQL_IDE, get_graphql_ide_html
8
10
  from strawberry.http.types import HTTPMethod, QueryParams
9
11
  from strawberry.schema.base import BaseSchema
10
12
 
11
- from .exceptions import HTTPException
12
13
  from .typevars import Request
13
14
 
14
15
 
@@ -1,9 +1,3 @@
1
- class HTTPException(Exception):
2
- def __init__(self, status_code: int, reason: str) -> None:
3
- self.status_code = status_code
4
- self.reason = reason
5
-
6
-
7
1
  class NonTextMessageReceived(Exception):
8
2
  pass
9
3
 
@@ -16,4 +10,8 @@ class WebSocketDisconnected(Exception):
16
10
  pass
17
11
 
18
12
 
19
- __all__ = ["HTTPException"]
13
+ __all__ = [
14
+ "NonJsonMessageReceived",
15
+ "NonTextMessageReceived",
16
+ "WebSocketDisconnected",
17
+ ]
@@ -1,8 +1,6 @@
1
1
  import abc
2
2
  import json
3
- from collections.abc import Mapping
4
3
  from typing import (
5
- Any,
6
4
  Callable,
7
5
  Generic,
8
6
  Literal,
@@ -11,6 +9,7 @@ from typing import (
11
9
  )
12
10
 
13
11
  from graphql import GraphQLError
12
+ from lia import HTTPException, SyncHTTPRequestAdapter
14
13
 
15
14
  from strawberry.exceptions import MissingQueryError
16
15
  from strawberry.file_uploads.utils import replace_placeholders_with_files
@@ -30,42 +29,10 @@ from strawberry.types.graphql import OperationType
30
29
  from strawberry.types.unset import UNSET
31
30
 
32
31
  from .base import BaseView
33
- from .exceptions import HTTPException
34
32
  from .parse_content_type import parse_content_type
35
- from .types import HTTPMethod, QueryParams
36
33
  from .typevars import Context, Request, Response, RootValue, SubResponse
37
34
 
38
35
 
39
- class SyncHTTPRequestAdapter(abc.ABC):
40
- @property
41
- @abc.abstractmethod
42
- def query_params(self) -> QueryParams: ...
43
-
44
- @property
45
- @abc.abstractmethod
46
- def body(self) -> Union[str, bytes]: ...
47
-
48
- @property
49
- @abc.abstractmethod
50
- def method(self) -> HTTPMethod: ...
51
-
52
- @property
53
- @abc.abstractmethod
54
- def headers(self) -> Mapping[str, str]: ...
55
-
56
- @property
57
- @abc.abstractmethod
58
- def content_type(self) -> Optional[str]: ...
59
-
60
- @property
61
- @abc.abstractmethod
62
- def post_data(self) -> Mapping[str, Union[str, bytes]]: ...
63
-
64
- @property
65
- @abc.abstractmethod
66
- def files(self) -> Mapping[str, Any]: ...
67
-
68
-
69
36
  class SyncBaseHTTPView(
70
37
  abc.ABC,
71
38
  BaseView[Request],
@@ -13,10 +13,10 @@ from typing import (
13
13
  Optional,
14
14
  TypedDict,
15
15
  Union,
16
- cast,
17
16
  )
18
17
  from typing_extensions import TypeGuard
19
18
 
19
+ from lia import HTTPException, LitestarRequestAdapter
20
20
  from msgspec import Struct
21
21
 
22
22
  from litestar import (
@@ -41,16 +41,13 @@ from litestar.status_codes import HTTP_200_OK
41
41
  from strawberry.exceptions import InvalidCustomContext
42
42
  from strawberry.http.async_base_view import (
43
43
  AsyncBaseHTTPView,
44
- AsyncHTTPRequestAdapter,
45
44
  AsyncWebSocketAdapter,
46
45
  )
47
46
  from strawberry.http.exceptions import (
48
- HTTPException,
49
47
  NonJsonMessageReceived,
50
48
  NonTextMessageReceived,
51
49
  WebSocketDisconnected,
52
50
  )
53
- from strawberry.http.types import FormData, HTTPMethod, QueryParams
54
51
  from strawberry.http.typevars import Context, RootValue
55
52
  from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL
56
53
 
@@ -153,41 +150,6 @@ class GraphQLResource(Struct):
153
150
  extensions: Optional[dict[str, object]]
154
151
 
155
152
 
156
- class LitestarRequestAdapter(AsyncHTTPRequestAdapter):
157
- def __init__(self, request: Request[Any, Any, Any]) -> None:
158
- self.request = request
159
-
160
- @property
161
- def query_params(self) -> QueryParams:
162
- return self.request.query_params
163
-
164
- @property
165
- def method(self) -> HTTPMethod:
166
- return cast("HTTPMethod", self.request.method.upper())
167
-
168
- @property
169
- def headers(self) -> Mapping[str, str]:
170
- return self.request.headers
171
-
172
- @property
173
- def content_type(self) -> Optional[str]:
174
- content_type, params = self.request.content_type
175
-
176
- # combine content type and params
177
- if params:
178
- content_type += "; " + "; ".join(f"{k}={v}" for k, v in params.items())
179
-
180
- return content_type
181
-
182
- async def get_body(self) -> bytes:
183
- return await self.request.body()
184
-
185
- async def get_form_data(self) -> FormData:
186
- multipart_data = await self.request.form()
187
-
188
- return FormData(form=multipart_data, files=multipart_data)
189
-
190
-
191
153
  class LitestarWebSocketAdapter(AsyncWebSocketAdapter):
192
154
  def __init__(
193
155
  self, view: AsyncBaseHTTPView, request: WebSocket, response: WebSocket
strawberry/quart/views.py CHANGED
@@ -3,25 +3,24 @@ import warnings
3
3
  from collections.abc import AsyncGenerator, Mapping, Sequence
4
4
  from datetime import timedelta
5
5
  from json.decoder import JSONDecodeError
6
- from typing import TYPE_CHECKING, Callable, ClassVar, Optional, Union, cast
6
+ from typing import TYPE_CHECKING, Callable, ClassVar, Optional, Union
7
7
  from typing_extensions import TypeGuard
8
8
 
9
+ from lia import HTTPException, QuartHTTPRequestAdapter
10
+
9
11
  from quart import Request, Response, Websocket, request, websocket
10
12
  from quart.ctx import has_websocket_context
11
13
  from quart.views import View
12
14
  from strawberry.http.async_base_view import (
13
15
  AsyncBaseHTTPView,
14
- AsyncHTTPRequestAdapter,
15
16
  AsyncWebSocketAdapter,
16
17
  )
17
18
  from strawberry.http.exceptions import (
18
- HTTPException,
19
19
  NonJsonMessageReceived,
20
20
  NonTextMessageReceived,
21
21
  WebSocketDisconnected,
22
22
  )
23
23
  from strawberry.http.ides import GraphQL_IDE
24
- from strawberry.http.types import FormData, HTTPMethod, QueryParams
25
24
  from strawberry.http.typevars import Context, RootValue
26
25
  from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL
27
26
 
@@ -31,35 +30,6 @@ if TYPE_CHECKING:
31
30
  from strawberry.schema.base import BaseSchema
32
31
 
33
32
 
34
- class QuartHTTPRequestAdapter(AsyncHTTPRequestAdapter):
35
- def __init__(self, request: Request) -> None:
36
- self.request = request
37
-
38
- @property
39
- def query_params(self) -> QueryParams:
40
- return self.request.args.to_dict()
41
-
42
- @property
43
- def method(self) -> HTTPMethod:
44
- return cast("HTTPMethod", self.request.method.upper())
45
-
46
- @property
47
- def content_type(self) -> Optional[str]:
48
- return self.request.content_type
49
-
50
- @property
51
- def headers(self) -> Mapping[str, str]:
52
- return self.request.headers # type: ignore
53
-
54
- async def get_body(self) -> str:
55
- return (await self.request.data).decode()
56
-
57
- async def get_form_data(self) -> FormData:
58
- files = await self.request.files
59
- form = await self.request.form
60
- return FormData(files=files, form=form)
61
-
62
-
63
33
  class QuartWebSocketAdapter(AsyncWebSocketAdapter):
64
34
  def __init__(
65
35
  self, view: AsyncBaseHTTPView, request: Websocket, response: Response
strawberry/sanic/views.py CHANGED
@@ -8,68 +8,29 @@ from typing import (
8
8
  Callable,
9
9
  Optional,
10
10
  Union,
11
- cast,
12
11
  )
13
12
  from typing_extensions import TypeGuard
14
13
 
14
+ from lia import HTTPException, SanicHTTPRequestAdapter
15
+
15
16
  from sanic.request import Request
16
17
  from sanic.response import HTTPResponse, html
17
18
  from sanic.views import HTTPMethodView
18
- from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
19
- from strawberry.http.exceptions import HTTPException
19
+ from strawberry.http.async_base_view import AsyncBaseHTTPView
20
20
  from strawberry.http.temporal_response import TemporalResponse
21
- from strawberry.http.types import FormData, HTTPMethod, QueryParams
22
21
  from strawberry.http.typevars import (
23
22
  Context,
24
23
  RootValue,
25
24
  )
26
- from strawberry.sanic.utils import convert_request_to_files_dict
27
25
 
28
26
  if TYPE_CHECKING:
29
- from collections.abc import AsyncGenerator, Mapping
27
+ from collections.abc import AsyncGenerator
30
28
 
31
29
  from strawberry.http import GraphQLHTTPResponse
32
30
  from strawberry.http.ides import GraphQL_IDE
33
31
  from strawberry.schema import BaseSchema
34
32
 
35
33
 
36
- class SanicHTTPRequestAdapter(AsyncHTTPRequestAdapter):
37
- def __init__(self, request: Request) -> None:
38
- self.request = request
39
-
40
- @property
41
- def query_params(self) -> QueryParams:
42
- # Just a heads up, Sanic's request.args uses urllib.parse.parse_qs
43
- # to parse query string parameters. This returns a dictionary where
44
- # the keys are the unique variable names and the values are lists
45
- # of values for each variable name. To ensure consistency, we're
46
- # enforcing the use of the first value in each list.
47
- args = self.request.get_args(keep_blank_values=True)
48
- return {k: args.get(k, None) for k in args}
49
-
50
- @property
51
- def method(self) -> HTTPMethod:
52
- return cast("HTTPMethod", self.request.method.upper())
53
-
54
- @property
55
- def headers(self) -> Mapping[str, str]:
56
- return self.request.headers
57
-
58
- @property
59
- def content_type(self) -> Optional[str]:
60
- return self.request.content_type
61
-
62
- async def get_body(self) -> str:
63
- return self.request.body.decode()
64
-
65
- async def get_form_data(self) -> FormData:
66
- assert self.request.form is not None
67
-
68
- files = convert_request_to_files_dict(self.request)
69
-
70
- return FormData(form=self.request.form, files=files)
71
-
72
-
73
34
  class GraphQLView(
74
35
  AsyncBaseHTTPView[
75
36
  Request,
@@ -50,6 +50,7 @@ from strawberry.extensions.directives import (
50
50
  from strawberry.extensions.runner import SchemaExtensionsRunner
51
51
  from strawberry.printer import print_schema
52
52
  from strawberry.schema.schema_converter import GraphQLCoreConverter
53
+ from strawberry.schema.validation_rules.maybe_null import MaybeNullValidationRule
53
54
  from strawberry.schema.validation_rules.one_of import OneOfInputValidationRule
54
55
  from strawberry.types.base import (
55
56
  StrawberryObjectDefinition,
@@ -124,6 +125,7 @@ def validate_document(
124
125
  ) -> list[GraphQLError]:
125
126
  validation_rules = (
126
127
  *validation_rules,
128
+ MaybeNullValidationRule,
127
129
  OneOfInputValidationRule,
128
130
  )
129
131
  return validate(
@@ -854,6 +854,13 @@ class GraphQLCoreConverter:
854
854
  NoneType = type(None)
855
855
  if type_ is None or type_ is NoneType:
856
856
  return self.from_type(type_)
857
+ if isinstance(type_, StrawberryMaybe):
858
+ # StrawberryMaybe should always generate optional types
859
+ # because Maybe[T] = Union[Some[T], None] (field can be absent)
860
+ # But we need to handle the case where of_type is itself optional
861
+ if isinstance(type_.of_type, StrawberryOptional):
862
+ return self.from_type(type_.of_type.of_type)
863
+ return self.from_type(type_.of_type)
857
864
  if isinstance(type_, StrawberryOptional):
858
865
  return self.from_type(type_.of_type)
859
866
  return GraphQLNonNull(self.from_type(type_))
@@ -0,0 +1,136 @@
1
+ from typing import Any
2
+
3
+ from graphql import (
4
+ ArgumentNode,
5
+ GraphQLError,
6
+ GraphQLNamedType,
7
+ ObjectValueNode,
8
+ ValidationContext,
9
+ ValidationRule,
10
+ get_named_type,
11
+ )
12
+
13
+ from strawberry.types.base import StrawberryMaybe, StrawberryOptional
14
+ from strawberry.utils.str_converters import to_camel_case
15
+
16
+
17
+ class MaybeNullValidationRule(ValidationRule):
18
+ """Validates that Maybe[T] fields do not receive explicit null values.
19
+
20
+ This rule ensures that:
21
+ - Maybe[T] fields can only be omitted or have non-null values
22
+ - Maybe[T | None] fields can be omitted, null, or have non-null values
23
+
24
+ This provides clear semantics where Maybe[T] means "either present with value or absent"
25
+ and Maybe[T | None] means "present with value, present but null, or absent".
26
+ """
27
+
28
+ def __init__(self, validation_context: ValidationContext) -> None:
29
+ super().__init__(validation_context)
30
+
31
+ def enter_argument(self, node: ArgumentNode, *_args: Any) -> None:
32
+ # Check if this is a null value
33
+ if node.value.kind != "null_value":
34
+ return
35
+
36
+ # Get the argument definition from the schema
37
+ argument_def = self.context.get_argument()
38
+ if not argument_def:
39
+ return
40
+
41
+ # Check if this argument corresponds to a Maybe[T] (not Maybe[T | None])
42
+ # The argument type extensions should contain the Strawberry type info
43
+ strawberry_arg_info = argument_def.extensions.get("strawberry-definition")
44
+ if not strawberry_arg_info:
45
+ return
46
+
47
+ # Get the Strawberry type from the argument info
48
+ field_type = getattr(strawberry_arg_info, "type", None)
49
+ if not field_type:
50
+ return
51
+
52
+ if isinstance(field_type, StrawberryMaybe) and not isinstance(
53
+ field_type.of_type, StrawberryOptional
54
+ ):
55
+ # This is Maybe[T] - should not accept null values
56
+ type_name = self._get_type_name(field_type.of_type)
57
+
58
+ self.report_error(
59
+ GraphQLError(
60
+ f"Expected value of type '{type_name}', found null. "
61
+ f"Argument '{node.name.value}' of type 'Maybe[{type_name}]' cannot be explicitly set to null. "
62
+ f"Use 'Maybe[{type_name} | None]' if you need to allow null values.",
63
+ nodes=[node],
64
+ )
65
+ )
66
+
67
+ def enter_object_value(self, node: ObjectValueNode, *_args: Any) -> None:
68
+ # Get the input type for this object
69
+ input_type = get_named_type(self.context.get_input_type())
70
+ if not input_type:
71
+ return
72
+
73
+ # Get the Strawberry type definition from extensions
74
+ strawberry_type = input_type.extensions.get("strawberry-definition")
75
+ if not strawberry_type:
76
+ return
77
+
78
+ # Check each field in the object for null Maybe[T] violations
79
+ self.validate_maybe_fields(node, input_type, strawberry_type)
80
+
81
+ def validate_maybe_fields(
82
+ self, node: ObjectValueNode, input_type: GraphQLNamedType, strawberry_type: Any
83
+ ) -> None:
84
+ # Create a map of field names to field nodes for easy lookup
85
+ field_node_map = {field.name.value: field for field in node.fields}
86
+
87
+ # Check each field in the Strawberry type definition
88
+ if not hasattr(strawberry_type, "fields"):
89
+ return
90
+
91
+ for field_def in strawberry_type.fields:
92
+ # Resolve the actual GraphQL field name using the same logic as NameConverter
93
+ if field_def.graphql_name is not None:
94
+ field_name = field_def.graphql_name
95
+ else:
96
+ # Apply auto_camel_case conversion if enabled (default behavior)
97
+ field_name = to_camel_case(field_def.python_name)
98
+
99
+ # Check if this field is present in the input and has a null value
100
+ if field_name in field_node_map:
101
+ field_node = field_node_map[field_name]
102
+
103
+ # Check if this field has a null value
104
+ if field_node.value.kind == "null_value":
105
+ # Check if this is a Maybe[T] (not Maybe[T | None])
106
+ field_type = field_def.type
107
+ if isinstance(field_type, StrawberryMaybe) and not isinstance(
108
+ field_type.of_type, StrawberryOptional
109
+ ):
110
+ # This is Maybe[T] - should not accept null values
111
+ type_name = self._get_type_name(field_type.of_type)
112
+ self.report_error(
113
+ GraphQLError(
114
+ f"Expected value of type '{type_name}', found null. "
115
+ f"Field '{field_name}' of type 'Maybe[{type_name}]' cannot be explicitly set to null. "
116
+ f"Use 'Maybe[{type_name} | None]' if you need to allow null values.",
117
+ nodes=[field_node],
118
+ )
119
+ )
120
+
121
+ def _get_type_name(self, type_: Any) -> str:
122
+ """Get a readable type name for error messages."""
123
+ if hasattr(type_, "__name__"):
124
+ return type_.__name__
125
+ # Handle Strawberry types that don't have __name__
126
+ if hasattr(type_, "of_type") and hasattr(type_.of_type, "__name__"):
127
+ # For StrawberryList, StrawberryOptional, etc.
128
+ return (
129
+ f"list[{type_.of_type.__name__}]"
130
+ if "List" in str(type_.__class__)
131
+ else type_.of_type.__name__
132
+ )
133
+ return str(type_)
134
+
135
+
136
+ __all__ = ["MaybeNullValidationRule"]
@@ -191,10 +191,24 @@ def convert_argument(
191
191
  from strawberry.relay.types import GlobalID
192
192
 
193
193
  # TODO: move this somewhere else and make it first class
194
- if isinstance(type_, StrawberryOptional):
194
+ # Handle StrawberryMaybe first, since it extends StrawberryOptional
195
+ if isinstance(type_, StrawberryMaybe):
196
+ # Check if this is Maybe[T | None] (has StrawberryOptional as of_type)
197
+ if isinstance(type_.of_type, StrawberryOptional):
198
+ # This is Maybe[T | None] - allows null values
199
+ res = convert_argument(value, type_.of_type, scalar_registry, config)
200
+
201
+ return Some(res)
202
+
203
+ # This is Maybe[T] - validation for null values is handled by MaybeNullValidationRule
204
+ # Convert the value and wrap in Some()
195
205
  res = convert_argument(value, type_.of_type, scalar_registry, config)
196
206
 
197
- return Some(res) if isinstance(type_, StrawberryMaybe) else res
207
+ return Some(res)
208
+
209
+ # Handle regular StrawberryOptional (not Maybe)
210
+ if isinstance(type_, StrawberryOptional):
211
+ return convert_argument(value, type_.of_type, scalar_registry, config)
198
212
 
199
213
  if value is None:
200
214
  return None
strawberry/types/maybe.py CHANGED
@@ -31,7 +31,7 @@ class Some(Generic[T]):
31
31
 
32
32
 
33
33
  if TYPE_CHECKING:
34
- Maybe: TypeAlias = Union[Some[Union[T, None]], None]
34
+ Maybe: TypeAlias = Union[Some[T], None]
35
35
  else:
36
36
  # we do this trick so we can inspect that at runtime
37
37
  class Maybe(Generic[T]): ...