Skip to content

Commit f045a32

Browse files
committed
[RFC] Client Controlled Nullability experiment implementation w/o execution
Replicates graphql/graphql-js@699ec58
1 parent 87551f5 commit f045a32

18 files changed

+732
-22
lines changed

docs/modules/language.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Each kind of AST node has its own class:
3131
.. autoclass:: EnumTypeExtensionNode
3232
.. autoclass:: EnumValueDefinitionNode
3333
.. autoclass:: EnumValueNode
34+
.. autoclass:: ErrorBoundaryNode
3435
.. autoclass:: ExecutableDefinitionNode
3536
.. autoclass:: FieldDefinitionNode
3637
.. autoclass:: FieldNode
@@ -44,11 +45,14 @@ Each kind of AST node has its own class:
4445
.. autoclass:: IntValueNode
4546
.. autoclass:: InterfaceTypeDefinitionNode
4647
.. autoclass:: InterfaceTypeExtensionNode
48+
.. autoclass:: ListNullabilityOperatorNode
4749
.. autoclass:: ListTypeNode
4850
.. autoclass:: ListValueNode
4951
.. autoclass:: NameNode
5052
.. autoclass:: NamedTypeNode
53+
.. autoclass:: NonNullAssertionNode
5154
.. autoclass:: NonNullTypeNode
55+
.. autoclass:: NullabilityAssertionNode
5256
.. autoclass:: NullValueNode
5357
.. autoclass:: ObjectFieldNode
5458
.. autoclass:: ObjectTypeDefinitionNode

src/graphql/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
# Predicates
8484
is_definition_node,
8585
is_executable_definition_node,
86+
is_nullability_assertion_node,
8687
is_selection_node,
8788
is_value_node,
8889
is_const_value_node,
@@ -110,6 +111,10 @@
110111
SelectionNode,
111112
FieldNode,
112113
ArgumentNode,
114+
NullabilityAssertionNode,
115+
NonNullAssertionNode,
116+
ErrorBoundaryNode,
117+
ListNullabilityOperatorNode,
113118
ConstArgumentNode,
114119
FragmentSpreadNode,
115120
InlineFragmentNode,
@@ -606,6 +611,7 @@
606611
"DirectiveLocation",
607612
"is_definition_node",
608613
"is_executable_definition_node",
614+
"is_nullability_assertion_node",
609615
"is_selection_node",
610616
"is_value_node",
611617
"is_const_value_node",
@@ -630,6 +636,10 @@
630636
"SelectionNode",
631637
"FieldNode",
632638
"ArgumentNode",
639+
"NullabilityAssertionNode",
640+
"NonNullAssertionNode",
641+
"ErrorBoundaryNode",
642+
"ListNullabilityOperatorNode",
633643
"ConstArgumentNode",
634644
"FragmentSpreadNode",
635645
"InlineFragmentNode",

src/graphql/language/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
SelectionSetNode,
4747
SelectionNode,
4848
FieldNode,
49+
NullabilityAssertionNode,
50+
NonNullAssertionNode,
51+
ErrorBoundaryNode,
52+
ListNullabilityOperatorNode,
4953
ArgumentNode,
5054
ConstArgumentNode,
5155
FragmentSpreadNode,
@@ -98,6 +102,7 @@
98102
from .predicates import (
99103
is_definition_node,
100104
is_executable_definition_node,
105+
is_nullability_assertion_node,
101106
is_selection_node,
102107
is_value_node,
103108
is_const_value_node,
@@ -147,6 +152,10 @@
147152
"SelectionSetNode",
148153
"SelectionNode",
149154
"FieldNode",
155+
"NullabilityAssertionNode",
156+
"NonNullAssertionNode",
157+
"ErrorBoundaryNode",
158+
"ListNullabilityOperatorNode",
150159
"ArgumentNode",
151160
"ConstArgumentNode",
152161
"FragmentSpreadNode",
@@ -197,6 +206,7 @@
197206
"InputObjectTypeExtensionNode",
198207
"is_definition_node",
199208
"is_executable_definition_node",
209+
"is_nullability_assertion_node",
200210
"is_selection_node",
201211
"is_value_node",
202212
"is_const_value_node",

src/graphql/language/ast.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
"SelectionSetNode",
2929
"SelectionNode",
3030
"FieldNode",
31+
"NullabilityAssertionNode",
32+
"NonNullAssertionNode",
33+
"ErrorBoundaryNode",
34+
"ListNullabilityOperatorNode",
3135
"ArgumentNode",
3236
"ConstArgumentNode",
3337
"FragmentSpreadNode",
@@ -258,8 +262,22 @@ class OperationType(Enum):
258262
"variable_definition": ("variable", "type", "default_value", "directives"),
259263
"variable": ("name",),
260264
"selection_set": ("selections",),
261-
"field": ("alias", "name", "arguments", "directives", "selection_set"),
265+
"field": (
266+
"alias",
267+
"name",
268+
"arguments",
269+
"directives",
270+
"selection_set",
271+
# note: Client controlled Nullability is experimental and may be changed
272+
# or removed in the future.
273+
"nullability_assertion",
274+
),
262275
"argument": ("name", "value"),
276+
# note: Client controlled Nullability is experimental and may be changed
277+
# or removed in the future.
278+
"list_nullability_operator": ("nullability_assertion",),
279+
"non_null_assertion": ("nullability_assertion",),
280+
"error_boundary": ("nullability_assertion",),
263281
"fragment_spread": ("name", "directives"),
264282
"inline_fragment": ("type_condition", "directives", "selection_set"),
265283
"fragment_definition": (
@@ -462,14 +480,34 @@ class SelectionNode(Node):
462480

463481

464482
class FieldNode(SelectionNode):
465-
__slots__ = "alias", "name", "arguments", "selection_set"
483+
__slots__ = "alias", "name", "arguments", "nullability_assertion", "selection_set"
466484

467485
alias: Optional[NameNode]
468486
name: NameNode
469487
arguments: Tuple[ArgumentNode, ...]
488+
# Note: Client Controlled Nullability is experimental
489+
# and may be changed or removed in the future.
490+
nullability_assertion: NullabilityAssertionNode
470491
selection_set: Optional[SelectionSetNode]
471492

472493

494+
class NullabilityAssertionNode(Node):
495+
__slots__ = ("nullability_assertion",)
496+
nullability_assertion: Optional["NullabilityAssertionNode"]
497+
498+
499+
class ListNullabilityOperatorNode(NullabilityAssertionNode):
500+
pass
501+
502+
503+
class NonNullAssertionNode(NullabilityAssertionNode):
504+
nullability_assertion: ListNullabilityOperatorNode
505+
506+
507+
class ErrorBoundaryNode(NullabilityAssertionNode):
508+
nullability_assertion: ListNullabilityOperatorNode
509+
510+
473511
class ArgumentNode(Node):
474512
__slots__ = "name", "value"
475513

src/graphql/language/lexer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ def read_name(self, start: int) -> Token:
458458
_punctuator_token_kinds = frozenset(
459459
[
460460
TokenKind.BANG,
461+
TokenKind.QUESTION_MARK,
461462
TokenKind.DOLLAR,
462463
TokenKind.AMP,
463464
TokenKind.PAREN_L,
@@ -485,6 +486,7 @@ def is_punctuator_token_kind(kind: TokenKind) -> bool:
485486

486487
_KIND_FOR_PUNCT = {
487488
"!": TokenKind.BANG,
489+
"?": TokenKind.QUESTION_MARK,
488490
"$": TokenKind.DOLLAR,
489491
"&": TokenKind.AMP,
490492
"(": TokenKind.PAREN_L,

src/graphql/language/parser.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
EnumTypeExtensionNode,
1717
EnumValueDefinitionNode,
1818
EnumValueNode,
19+
ErrorBoundaryNode,
1920
FieldDefinitionNode,
2021
FieldNode,
2122
FloatValueNode,
@@ -28,12 +29,15 @@
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

src/graphql/language/predicates.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
ExecutableDefinitionNode,
66
ListValueNode,
77
Node,
8+
NullabilityAssertionNode,
89
ObjectValueNode,
910
SchemaExtensionNode,
1011
SelectionNode,
@@ -26,6 +27,7 @@
2627
__all__ = [
2728
"is_definition_node",
2829
"is_executable_definition_node",
30+
"is_nullability_assertion_node",
2931
"is_selection_node",
3032
"is_value_node",
3133
"is_const_value_node",
@@ -52,6 +54,11 @@ def is_selection_node(node: Node) -> TypeGuard[SelectionNode]:
5254
return isinstance(node, SelectionNode)
5355

5456

57+
def is_nullability_assertion_node(node: Node) -> TypeGuard[NullabilityAssertionNode]:
58+
"""Check whether the given node represents a nullability assertion node."""
59+
return isinstance(node, NullabilityAssertionNode)
60+
61+
5562
def is_value_node(node: Node) -> TypeGuard[ValueNode]:
5663
"""Check whether the given node represents a value."""
5764
return isinstance(node, ValueNode)

0 commit comments

Comments
 (0)