16
16
EnumTypeExtensionNode ,
17
17
EnumValueDefinitionNode ,
18
18
EnumValueNode ,
19
+ ErrorBoundaryNode ,
19
20
FieldDefinitionNode ,
20
21
FieldNode ,
21
22
FloatValueNode ,
28
29
InterfaceTypeDefinitionNode ,
29
30
InterfaceTypeExtensionNode ,
30
31
IntValueNode ,
32
+ ListNullabilityOperatorNode ,
31
33
ListTypeNode ,
32
34
ListValueNode ,
33
35
Location ,
34
36
NamedTypeNode ,
35
37
NameNode ,
38
+ NonNullAssertionNode ,
36
39
NonNullTypeNode ,
40
+ NullabilityAssertionNode ,
37
41
NullValueNode ,
38
42
ObjectFieldNode ,
39
43
ObjectTypeDefinitionNode ,
@@ -81,6 +85,7 @@ def parse(
81
85
source : SourceType ,
82
86
no_location : bool = False ,
83
87
allow_legacy_fragment_variables : bool = False ,
88
+ experimental_client_controlled_nullability : bool = False ,
84
89
) -> DocumentNode :
85
90
"""Given a GraphQL source, parse it into a Document.
86
91
@@ -103,11 +108,31 @@ def parse(
103
108
fragment A($var: Boolean = false) on T {
104
109
...
105
110
}
111
+
112
+ EXPERIMENTAL:
113
+
114
+ If enabled, the parser will understand and parse Client Controlled Nullability
115
+ Designators contained in Fields. They'll be represented in the
116
+ :attr:`~graphql.language.FieldNode.nullability_assertion` field
117
+ of the :class:`~graphql.language.FieldNode`.
118
+
119
+ The syntax looks like the following::
120
+
121
+ {
122
+ nullableField!
123
+ nonNullableField?
124
+ nonNullableSelectionSet? {
125
+ childField!
126
+ }
127
+ }
128
+
129
+ Note: this feature is experimental and may change or be removed in the future.
106
130
"""
107
131
parser = Parser (
108
132
source ,
109
133
no_location = no_location ,
110
134
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
135
+ experimental_client_controlled_nullability = experimental_client_controlled_nullability , # noqa
111
136
)
112
137
return parser .parse_document ()
113
138
@@ -200,19 +225,24 @@ class Parser:
200
225
_lexer : Lexer
201
226
_no_location : bool
202
227
_allow_legacy_fragment_variables : bool
228
+ _experimental_client_controlled_nullability : bool
203
229
204
230
def __init__ (
205
231
self ,
206
232
source : SourceType ,
207
233
no_location : bool = False ,
208
234
allow_legacy_fragment_variables : bool = False ,
235
+ experimental_client_controlled_nullability : bool = False ,
209
236
):
210
237
if not is_source (source ):
211
238
source = Source (cast (str , source ))
212
239
213
240
self ._lexer = Lexer (source )
214
241
self ._no_location = no_location
215
242
self ._allow_legacy_fragment_variables = allow_legacy_fragment_variables
243
+ self ._experimental_client_controlled_nullability = (
244
+ experimental_client_controlled_nullability
245
+ )
216
246
217
247
def parse_name (self ) -> NameNode :
218
248
"""Convert a name lex token into a name parse node."""
@@ -376,13 +406,46 @@ def parse_field(self) -> FieldNode:
376
406
alias = alias ,
377
407
name = name ,
378
408
arguments = self .parse_arguments (False ),
409
+ # Experimental support for Client Controlled Nullability changes
410
+ # the grammar of Field:
411
+ nullability_assertion = self .parse_nullability_assertion (),
379
412
directives = self .parse_directives (False ),
380
413
selection_set = self .parse_selection_set ()
381
414
if self .peek (TokenKind .BRACE_L )
382
415
else None ,
383
416
loc = self .loc (start ),
384
417
)
385
418
419
+ def parse_nullability_assertion (self ) -> Optional [NullabilityAssertionNode ]:
420
+ """NullabilityAssertion (grammar not yet finalized)
421
+
422
+ # Note: Client Controlled Nullability is experimental and may be changed or
423
+ # removed in the future.
424
+ """
425
+ if not self ._experimental_client_controlled_nullability :
426
+ return None
427
+
428
+ start = self ._lexer .token
429
+ nullability_assertion : Optional [NullabilityAssertionNode ] = None
430
+
431
+ if self .expect_optional_token (TokenKind .BRACKET_L ):
432
+ inner_modifier = self .parse_nullability_assertion ()
433
+ self .expect_token (TokenKind .BRACKET_R )
434
+ nullability_assertion = ListNullabilityOperatorNode (
435
+ nullability_assertion = inner_modifier , loc = self .loc (start )
436
+ )
437
+
438
+ if self .expect_optional_token (TokenKind .BANG ):
439
+ nullability_assertion = NonNullAssertionNode (
440
+ nullability_assertion = nullability_assertion , loc = self .loc (start )
441
+ )
442
+ elif self .expect_optional_token (TokenKind .QUESTION_MARK ):
443
+ nullability_assertion = ErrorBoundaryNode (
444
+ nullability_assertion = nullability_assertion , loc = self .loc (start )
445
+ )
446
+
447
+ return nullability_assertion
448
+
386
449
def parse_arguments (self , is_const : bool ) -> List [ArgumentNode ]:
387
450
"""Arguments[Const]: (Argument[?Const]+)"""
388
451
item = self .parse_const_argument if is_const else self .parse_argument
0 commit comments