1616 EnumTypeExtensionNode ,
1717 EnumValueDefinitionNode ,
1818 EnumValueNode ,
19+ ErrorBoundaryNode ,
1920 FieldDefinitionNode ,
2021 FieldNode ,
2122 FloatValueNode ,
2829 InterfaceTypeDefinitionNode ,
2930 InterfaceTypeExtensionNode ,
3031 IntValueNode ,
32+ ListNullabilityOperatorNode ,
3133 ListTypeNode ,
3234 ListValueNode ,
3335 Location ,
3436 NamedTypeNode ,
3537 NameNode ,
38+ NonNullAssertionNode ,
3639 NonNullTypeNode ,
40+ NullabilityAssertionNode ,
3741 NullValueNode ,
3842 ObjectFieldNode ,
3943 ObjectTypeDefinitionNode ,
@@ -81,6 +85,7 @@ def parse(
8185 source : SourceType ,
8286 no_location : bool = False ,
8387 allow_legacy_fragment_variables : bool = False ,
88+ experimental_client_controlled_nullability : bool = False ,
8489) -> DocumentNode :
8590 """Given a GraphQL source, parse it into a Document.
8691
@@ -103,11 +108,31 @@ def parse(
103108 fragment A($var: Boolean = false) on T {
104109 ...
105110 }
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.
106130 """
107131 parser = Parser (
108132 source ,
109133 no_location = no_location ,
110134 allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
135+ experimental_client_controlled_nullability = experimental_client_controlled_nullability , # noqa
111136 )
112137 return parser .parse_document ()
113138
@@ -200,19 +225,24 @@ class Parser:
200225 _lexer : Lexer
201226 _no_location : bool
202227 _allow_legacy_fragment_variables : bool
228+ _experimental_client_controlled_nullability : bool
203229
204230 def __init__ (
205231 self ,
206232 source : SourceType ,
207233 no_location : bool = False ,
208234 allow_legacy_fragment_variables : bool = False ,
235+ experimental_client_controlled_nullability : bool = False ,
209236 ):
210237 if not is_source (source ):
211238 source = Source (cast (str , source ))
212239
213240 self ._lexer = Lexer (source )
214241 self ._no_location = no_location
215242 self ._allow_legacy_fragment_variables = allow_legacy_fragment_variables
243+ self ._experimental_client_controlled_nullability = (
244+ experimental_client_controlled_nullability
245+ )
216246
217247 def parse_name (self ) -> NameNode :
218248 """Convert a name lex token into a name parse node."""
@@ -376,13 +406,46 @@ def parse_field(self) -> FieldNode:
376406 alias = alias ,
377407 name = name ,
378408 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 (),
379412 directives = self .parse_directives (False ),
380413 selection_set = self .parse_selection_set ()
381414 if self .peek (TokenKind .BRACE_L )
382415 else None ,
383416 loc = self .loc (start ),
384417 )
385418
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+
386449 def parse_arguments (self , is_const : bool ) -> List [ArgumentNode ]:
387450 """Arguments[Const]: (Argument[?Const]+)"""
388451 item = self .parse_const_argument if is_const else self .parse_argument
0 commit comments