Skip to content

Commit 3a39a41

Browse files
committed
chore: forward-port directives on directive definitions
Replicates graphql/graphql-js@65fc947
1 parent d91beca commit 3a39a41

32 files changed

Lines changed: 789 additions & 19 deletions

docs/modules/language.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Each kind of AST node has its own class:
2525
.. autoclass:: ConstValueNode
2626
.. autoclass:: DefinitionNode
2727
.. autoclass:: DirectiveDefinitionNode
28+
.. autoclass:: DirectiveExtensionNode
2829
.. autoclass:: DirectiveNode
2930
.. autoclass:: DocumentNode
3031
.. autoclass:: EnumTypeDefinitionNode

src/graphql/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
UnionTypeExtensionNode,
169169
EnumTypeExtensionNode,
170170
InputObjectTypeExtensionNode,
171+
DirectiveExtensionNode,
171172
SchemaCoordinateNode,
172173
TypeCoordinateNode,
173174
MemberCoordinateNode,
@@ -573,6 +574,7 @@
573574
"DirectiveArgumentCoordinateNode",
574575
"DirectiveCoordinateNode",
575576
"DirectiveDefinitionNode",
577+
"DirectiveExtensionNode",
576578
"DirectiveLocation",
577579
"DirectiveNode",
578580
"DocumentNode",

src/graphql/execution/values.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from ..error import GraphQLError
88
from ..language import (
9+
DirectiveDefinitionNode,
10+
DirectiveExtensionNode,
911
DirectiveNode,
1012
EnumValueDefinitionNode,
1113
ExecutableDefinitionNode,
@@ -425,7 +427,9 @@ def print_argument_or_fragment_variable(
425427

426428

427429
NodeWithDirective: TypeAlias = (
428-
EnumValueDefinitionNode
430+
DirectiveDefinitionNode
431+
| DirectiveExtensionNode
432+
| EnumValueDefinitionNode
429433
| ExecutableDefinitionNode
430434
| FieldDefinitionNode
431435
| InputValueDefinitionNode

src/graphql/language/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
UnionTypeExtensionNode,
102102
EnumTypeExtensionNode,
103103
InputObjectTypeExtensionNode,
104+
DirectiveExtensionNode,
104105
SchemaCoordinateNode,
105106
TypeCoordinateNode,
106107
MemberCoordinateNode,
@@ -142,6 +143,7 @@
142143
"DirectiveArgumentCoordinateNode",
143144
"DirectiveCoordinateNode",
144145
"DirectiveDefinitionNode",
146+
"DirectiveExtensionNode",
145147
"DirectiveLocation",
146148
"DirectiveNode",
147149
"DocumentNode",

src/graphql/language/ast.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"DirectiveArgumentCoordinateNode",
3333
"DirectiveCoordinateNode",
3434
"DirectiveDefinitionNode",
35+
"DirectiveExtensionNode",
3536
"DirectiveNode",
3637
"DocumentNode",
3738
"EnumTypeDefinitionNode",
@@ -331,8 +332,15 @@ class OperationType(Enum):
331332
"enum_type_definition": ("description", "name", "directives", "values"),
332333
"enum_value_definition": ("description", "name", "directives"),
333334
"input_object_type_definition": ("description", "name", "directives", "fields"),
334-
"directive_definition": ("description", "name", "arguments", "locations"),
335+
"directive_definition": (
336+
"description",
337+
"name",
338+
"arguments",
339+
"directives",
340+
"locations",
341+
),
335342
"schema_extension": ("directives", "operation_types"),
343+
"directive_extension": ("name", "directives"),
336344
"scalar_type_extension": ("name", "directives"),
337345
"object_type_extension": ("name", "interfaces", "directives", "fields"),
338346
"interface_type_extension": ("name", "interfaces", "directives", "fields"),
@@ -765,6 +773,7 @@ class DirectiveDefinitionNode(TypeSystemDefinitionNode):
765773
locations: tuple[NameNode, ...]
766774
description: StringValueNode | None = None
767775
arguments: tuple[InputValueDefinitionNode, ...] | None = None
776+
directives: tuple[ConstDirectiveNode, ...] | None = None
768777
repeatable: bool = False
769778

770779

@@ -777,7 +786,15 @@ class SchemaExtensionNode(Node):
777786
operation_types: tuple[OperationTypeDefinitionNode, ...] | None = None
778787

779788

780-
TypeSystemExtensionNode: TypeAlias = SchemaExtensionNode | TypeExtensionNode
789+
@node_class
790+
class DirectiveExtensionNode(Node):
791+
name: NameNode
792+
directives: tuple[ConstDirectiveNode, ...] | None = None
793+
794+
795+
TypeSystemExtensionNode: TypeAlias = (
796+
SchemaExtensionNode | TypeExtensionNode | DirectiveExtensionNode
797+
)
781798

782799

783800
# Type Extension nodes

src/graphql/language/directive_locations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ class DirectiveLocation(Enum):
3131
ENUM_VALUE = "enum value"
3232
INPUT_OBJECT = "input object"
3333
INPUT_FIELD_DEFINITION = "input field definition"
34+
DIRECTIVE_DEFINITION = "directive definition"

src/graphql/language/parser.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
DirectiveArgumentCoordinateNode,
1818
DirectiveCoordinateNode,
1919
DirectiveDefinitionNode,
20+
DirectiveExtensionNode,
2021
DirectiveNode,
2122
DocumentNode,
2223
EnumTypeDefinitionNode,
@@ -96,6 +97,7 @@ def parse(
9697
no_location: bool = False,
9798
max_tokens: int | None = None,
9899
experimental_fragment_arguments: bool = False,
100+
experimental_directives_on_directive_definitions: bool = False,
99101
) -> DocumentNode:
100102
"""Given a GraphQL source, parse it into a Document.
101103
@@ -130,12 +132,23 @@ def parse(
130132
fragment A($var: Boolean = false) on T {
131133
...B(x: $var)
132134
}
135+
136+
Experimental feature:
137+
138+
If ``experimental_directives_on_directive_definitions`` is set to ``True``, the
139+
parser will understand and parse directives on directive definitions. This syntax
140+
is not part of the GraphQL specification and may change. For example::
141+
142+
directive @foo @bar on FIELD
133143
"""
134144
parser = Parser(
135145
source,
136146
no_location=no_location,
137147
max_tokens=max_tokens,
138148
experimental_fragment_arguments=experimental_fragment_arguments,
149+
experimental_directives_on_directive_definitions=(
150+
experimental_directives_on_directive_definitions
151+
),
139152
)
140153
return parser.parse_document()
141154

@@ -145,6 +158,7 @@ def parse_value(
145158
no_location: bool = False,
146159
max_tokens: int | None = None,
147160
experimental_fragment_arguments: bool = False,
161+
experimental_directives_on_directive_definitions: bool = False,
148162
) -> ValueNode:
149163
"""Parse the AST for a given string containing a GraphQL value.
150164
@@ -158,6 +172,9 @@ def parse_value(
158172
no_location=no_location,
159173
max_tokens=max_tokens,
160174
experimental_fragment_arguments=experimental_fragment_arguments,
175+
experimental_directives_on_directive_definitions=(
176+
experimental_directives_on_directive_definitions
177+
),
161178
)
162179
parser.expect_token(TokenKind.SOF)
163180
value = parser.parse_value_literal(False)
@@ -170,6 +187,7 @@ def parse_const_value(
170187
no_location: bool = False,
171188
max_tokens: int | None = None,
172189
experimental_fragment_arguments: bool = False,
190+
experimental_directives_on_directive_definitions: bool = False,
173191
) -> ConstValueNode:
174192
"""Parse the AST for a given string containing a GraphQL constant value.
175193
@@ -181,6 +199,9 @@ def parse_const_value(
181199
no_location=no_location,
182200
max_tokens=max_tokens,
183201
experimental_fragment_arguments=experimental_fragment_arguments,
202+
experimental_directives_on_directive_definitions=(
203+
experimental_directives_on_directive_definitions
204+
),
184205
)
185206
parser.expect_token(TokenKind.SOF)
186207
value = parser.parse_const_value_literal()
@@ -193,6 +214,7 @@ def parse_type(
193214
no_location: bool = False,
194215
max_tokens: int | None = None,
195216
experimental_fragment_arguments: bool = False,
217+
experimental_directives_on_directive_definitions: bool = False,
196218
) -> TypeNode:
197219
"""Parse the AST for a given string containing a GraphQL Type.
198220
@@ -209,6 +231,9 @@ def parse_type(
209231
no_location=no_location,
210232
max_tokens=max_tokens,
211233
experimental_fragment_arguments=experimental_fragment_arguments,
234+
experimental_directives_on_directive_definitions=(
235+
experimental_directives_on_directive_definitions
236+
),
212237
)
213238
parser.expect_token(TokenKind.SOF)
214239
type_ = parser.parse_type_reference()
@@ -258,6 +283,7 @@ class Parser:
258283
_no_location: bool
259284
_max_tokens: int | None
260285
_experimental_fragment_arguments: bool
286+
_experimental_directives_on_directive_definitions: bool
261287
_lexer: Lexer
262288
_token_counter: int
263289

@@ -267,6 +293,7 @@ def __init__(
267293
no_location: bool = False,
268294
max_tokens: int | None = None,
269295
experimental_fragment_arguments: bool = False,
296+
experimental_directives_on_directive_definitions: bool = False,
270297
lexer: Lexer | None = None,
271298
) -> None:
272299
if not is_source(source):
@@ -275,6 +302,9 @@ def __init__(
275302
self._no_location = no_location
276303
self._max_tokens = max_tokens
277304
self._experimental_fragment_arguments = experimental_fragment_arguments
305+
self._experimental_directives_on_directive_definitions = (
306+
experimental_directives_on_directive_definitions
307+
)
278308
# You may override the lexer used to lex the source; this is used by schema
279309
# coordinates to introduce a lexer with a restricted syntax.
280310
self._lexer = lexer if lexer is not None else Lexer(source)
@@ -754,6 +784,11 @@ def parse_type_system_extension(self) -> TypeSystemExtensionNode:
754784
)
755785
if method_name: # pragma: no cover
756786
return getattr(self, f"parse_{method_name}")()
787+
if (
788+
keyword_token.value == "directive"
789+
and self._experimental_directives_on_directive_definitions
790+
):
791+
return self.parse_directive_definition_extension()
757792
raise self.unexpected(keyword_token)
758793

759794
def peek_description(self) -> bool:
@@ -1105,6 +1140,22 @@ def parse_input_object_type_extension(self) -> InputObjectTypeExtensionNode:
11051140
name=name, directives=directives, fields=fields, loc=self.loc(start)
11061141
)
11071142

1143+
def parse_directive_definition_extension(self) -> DirectiveExtensionNode:
1144+
"""DirectiveDefinitionExtension"""
1145+
start = self._lexer.token
1146+
self.expect_keyword("extend")
1147+
self.expect_keyword("directive")
1148+
self.expect_token(TokenKind.AT)
1149+
name = self.parse_name()
1150+
directives = self.parse_const_directives()
1151+
if not directives:
1152+
raise self.unexpected()
1153+
return DirectiveExtensionNode(
1154+
name=name,
1155+
directives=directives,
1156+
loc=self.loc(start),
1157+
)
1158+
11081159
def parse_directive_definition(self) -> DirectiveDefinitionNode:
11091160
"""DirectiveDefinition"""
11101161
start = self._lexer.token
@@ -1113,13 +1164,19 @@ def parse_directive_definition(self) -> DirectiveDefinitionNode:
11131164
self.expect_token(TokenKind.AT)
11141165
name = self.parse_name()
11151166
args = self.parse_argument_defs()
1167+
directives = (
1168+
self.parse_const_directives()
1169+
if self._experimental_directives_on_directive_definitions
1170+
else None
1171+
)
11161172
repeatable = self.expect_optional_keyword("repeatable")
11171173
self.expect_keyword("on")
11181174
locations = self.parse_directive_locations()
11191175
return DirectiveDefinitionNode(
11201176
description=description,
11211177
name=name,
11221178
arguments=args,
1179+
directives=directives,
11231180
repeatable=repeatable,
11241181
locations=locations,
11251182
loc=self.loc(start),

src/graphql/language/predicates.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
DefinitionNode,
1010
DirectiveArgumentCoordinateNode,
1111
DirectiveCoordinateNode,
12+
DirectiveExtensionNode,
1213
ExecutableDefinitionNode,
1314
ListValueNode,
1415
MemberCoordinateNode,
@@ -102,9 +103,11 @@ def is_type_definition_node(node: Node) -> TypeGuard[TypeDefinitionNode]:
102103

103104
def is_type_system_extension_node(
104105
node: Node,
105-
) -> TypeGuard[SchemaExtensionNode | TypeExtensionNode]:
106+
) -> TypeGuard[SchemaExtensionNode | DirectiveExtensionNode | TypeExtensionNode]:
106107
"""Check whether the given node represents a type system extension."""
107-
return isinstance(node, (SchemaExtensionNode, TypeExtensionNode))
108+
return isinstance(
109+
node, (SchemaExtensionNode, DirectiveExtensionNode, TypeExtensionNode)
110+
)
108111

109112

110113
def is_type_extension_node(node: Node) -> TypeGuard[TypeExtensionNode]:

src/graphql/language/printer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,12 @@ def leave_directive_definition(node: PrintedNode, *_args: Any) -> str:
343343
if has_multiline_items(args)
344344
else wrap("(", join(args, ", "), ")")
345345
)
346+
directives = wrap(" ", join(node.directives, " "))
346347
repeatable = " repeatable" if node.repeatable else ""
347348
locations = join(node.locations, " | ")
348349
return (
349350
wrap("", node.description, "\n")
350-
+ f"directive @{node.name}{args}{repeatable} on {locations}"
351+
+ f"directive @{node.name}{args}{directives}{repeatable} on {locations}"
351352
)
352353

353354
@staticmethod
@@ -357,6 +358,10 @@ def leave_schema_extension(node: PrintedNode, *_args: Any) -> str:
357358
" ",
358359
)
359360

361+
@staticmethod
362+
def leave_directive_extension(node: PrintedNode, *_args: Any) -> str:
363+
return join((f"extend directive @{node.name}", join(node.directives, " ")), " ")
364+
360365
@staticmethod
361366
def leave_scalar_type_extension(node: PrintedNode, *_args: Any) -> str:
362367
return join(("extend scalar", node.name, join(node.directives, " ")), " ")

0 commit comments

Comments
 (0)