Skip to content

Commit 672281d

Browse files
committed
Add support for directives on directive definitions
Replicates graphql/graphql-js@ad9c519
1 parent 1922ab7 commit 672281d

32 files changed

Lines changed: 818 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
@@ -160,6 +160,7 @@
160160
UnionTypeExtensionNode,
161161
EnumTypeExtensionNode,
162162
InputObjectTypeExtensionNode,
163+
DirectiveExtensionNode,
163164
# Schema Coordinates
164165
SchemaCoordinateNode,
165166
TypeCoordinateNode,
@@ -709,6 +710,7 @@
709710
"UnionTypeExtensionNode",
710711
"EnumTypeExtensionNode",
711712
"InputObjectTypeExtensionNode",
713+
"DirectiveExtensionNode",
712714
"SchemaCoordinateNode",
713715
"TypeCoordinateNode",
714716
"MemberCoordinateNode",

src/graphql/execution/values.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from ..error import GraphQLError
44
from ..language import (
5+
DirectiveDefinitionNode,
6+
DirectiveExtensionNode,
57
DirectiveNode,
68
EnumValueDefinitionNode,
79
ExecutableDefinitionNode,
@@ -224,6 +226,8 @@ def get_argument_values(
224226

225227

226228
NodeWithDirective = Union[
229+
DirectiveDefinitionNode,
230+
DirectiveExtensionNode,
227231
EnumValueDefinitionNode,
228232
ExecutableDefinitionNode,
229233
FieldDefinitionNode,

src/graphql/language/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
UnionTypeExtensionNode,
101101
EnumTypeExtensionNode,
102102
InputObjectTypeExtensionNode,
103+
DirectiveExtensionNode,
103104
SchemaCoordinateNode,
104105
TypeCoordinateNode,
105106
MemberCoordinateNode,
@@ -209,6 +210,7 @@
209210
"UnionTypeExtensionNode",
210211
"EnumTypeExtensionNode",
211212
"InputObjectTypeExtensionNode",
213+
"DirectiveExtensionNode",
212214
"SchemaCoordinateNode",
213215
"TypeCoordinateNode",
214216
"MemberCoordinateNode",

src/graphql/language/ast.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"UnionTypeExtensionNode",
7070
"EnumTypeExtensionNode",
7171
"InputObjectTypeExtensionNode",
72+
"DirectiveExtensionNode",
7273
"SchemaCoordinateNode",
7374
"TypeCoordinateNode",
7475
"MemberCoordinateNode",
@@ -312,8 +313,15 @@ class OperationType(Enum):
312313
"enum_type_definition": ("description", "name", "directives", "values"),
313314
"enum_value_definition": ("description", "name", "directives"),
314315
"input_object_type_definition": ("description", "name", "directives", "fields"),
315-
"directive_definition": ("description", "name", "arguments", "locations"),
316+
"directive_definition": (
317+
"description",
318+
"name",
319+
"arguments",
320+
"directives",
321+
"locations",
322+
),
316323
"schema_extension": ("directives", "operation_types"),
324+
"directive_extension": ("name", "directives"),
317325
"scalar_type_extension": ("name", "directives"),
318326
"object_type_extension": ("name", "interfaces", "directives", "fields"),
319327
"interface_type_extension": ("name", "interfaces", "directives", "fields"),
@@ -771,11 +779,19 @@ class InputObjectTypeDefinitionNode(TypeDefinitionNode):
771779

772780

773781
class DirectiveDefinitionNode(TypeSystemDefinitionNode):
774-
__slots__ = "description", "name", "arguments", "repeatable", "locations"
782+
__slots__ = (
783+
"description",
784+
"name",
785+
"arguments",
786+
"directives",
787+
"repeatable",
788+
"locations",
789+
)
775790

776791
description: Optional[StringValueNode]
777792
name: NameNode
778793
arguments: Tuple[InputValueDefinitionNode, ...]
794+
directives: Tuple[ConstDirectiveNode, ...]
779795
repeatable: bool
780796
locations: Tuple[NameNode, ...]
781797

@@ -790,6 +806,13 @@ class SchemaExtensionNode(Node):
790806
operation_types: Tuple[OperationTypeDefinitionNode, ...]
791807

792808

809+
class DirectiveExtensionNode(Node):
810+
__slots__ = "name", "directives"
811+
812+
name: NameNode
813+
directives: Tuple[ConstDirectiveNode, ...]
814+
815+
793816
# Type Extensions
794817

795818

@@ -800,7 +823,9 @@ class TypeExtensionNode(TypeSystemDefinitionNode):
800823
directives: Tuple[ConstDirectiveNode, ...]
801824

802825

803-
TypeSystemExtensionNode = Union[SchemaExtensionNode, TypeExtensionNode]
826+
TypeSystemExtensionNode = Union[
827+
SchemaExtensionNode, TypeExtensionNode, DirectiveExtensionNode
828+
]
804829

805830

806831
class ScalarTypeExtensionNode(TypeExtensionNode):

src/graphql/language/directive_locations.py

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

src/graphql/language/parser.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
DirectiveArgumentCoordinateNode,
1414
DirectiveCoordinateNode,
1515
DirectiveDefinitionNode,
16+
DirectiveExtensionNode,
1617
DirectiveNode,
1718
DocumentNode,
1819
EnumTypeDefinitionNode,
@@ -88,6 +89,7 @@ def parse(
8889
no_location: bool = False,
8990
max_tokens: Optional[int] = None,
9091
allow_legacy_fragment_variables: bool = False,
92+
experimental_directives_on_directive_definitions: bool = False,
9193
) -> DocumentNode:
9294
"""Given a GraphQL source, parse it into a Document.
9395
@@ -116,12 +118,23 @@ def parse(
116118
fragment A($var: Boolean = false) on T {
117119
...
118120
}
121+
122+
Experimental feature:
123+
124+
If ``experimental_directives_on_directive_definitions`` is set to ``True``, the
125+
parser will understand and parse directives on directive definitions. This syntax
126+
is not part of the GraphQL specification and may change. For example::
127+
128+
directive @foo @bar on FIELD
119129
"""
120130
parser = Parser(
121131
source,
122132
no_location=no_location,
123133
max_tokens=max_tokens,
124134
allow_legacy_fragment_variables=allow_legacy_fragment_variables,
135+
experimental_directives_on_directive_definitions=(
136+
experimental_directives_on_directive_definitions
137+
),
125138
)
126139
return parser.parse_document()
127140

@@ -131,6 +144,7 @@ def parse_value(
131144
no_location: bool = False,
132145
max_tokens: Optional[int] = None,
133146
allow_legacy_fragment_variables: bool = False,
147+
experimental_directives_on_directive_definitions: bool = False,
134148
) -> ValueNode:
135149
"""Parse the AST for a given string containing a GraphQL value.
136150
@@ -147,6 +161,9 @@ def parse_value(
147161
no_location=no_location,
148162
max_tokens=max_tokens,
149163
allow_legacy_fragment_variables=allow_legacy_fragment_variables,
164+
experimental_directives_on_directive_definitions=(
165+
experimental_directives_on_directive_definitions
166+
),
150167
)
151168
parser.expect_token(TokenKind.SOF)
152169
value = parser.parse_value_literal(False)
@@ -159,6 +176,7 @@ def parse_const_value(
159176
no_location: bool = False,
160177
max_tokens: Optional[int] = None,
161178
allow_legacy_fragment_variables: bool = False,
179+
experimental_directives_on_directive_definitions: bool = False,
162180
) -> ConstValueNode:
163181
"""Parse the AST for a given string containing a GraphQL constant value.
164182
@@ -170,6 +188,9 @@ def parse_const_value(
170188
no_location=no_location,
171189
max_tokens=max_tokens,
172190
allow_legacy_fragment_variables=allow_legacy_fragment_variables,
191+
experimental_directives_on_directive_definitions=(
192+
experimental_directives_on_directive_definitions
193+
),
173194
)
174195
parser.expect_token(TokenKind.SOF)
175196
value = parser.parse_const_value_literal()
@@ -182,6 +203,7 @@ def parse_type(
182203
no_location: bool = False,
183204
max_tokens: Optional[int] = None,
184205
allow_legacy_fragment_variables: bool = False,
206+
experimental_directives_on_directive_definitions: bool = False,
185207
) -> TypeNode:
186208
"""Parse the AST for a given string containing a GraphQL Type.
187209
@@ -198,6 +220,9 @@ def parse_type(
198220
no_location=no_location,
199221
max_tokens=max_tokens,
200222
allow_legacy_fragment_variables=allow_legacy_fragment_variables,
223+
experimental_directives_on_directive_definitions=(
224+
experimental_directives_on_directive_definitions
225+
),
201226
)
202227
parser.expect_token(TokenKind.SOF)
203228
type_ = parser.parse_type_reference()
@@ -247,6 +272,7 @@ class Parser:
247272
_no_location: bool
248273
_max_tokens: Optional[int]
249274
_allow_legacy_fragment_variables: bool
275+
_experimental_directives_on_directive_definitions: bool
250276
_token_counter: int
251277

252278
def __init__(
@@ -255,6 +281,7 @@ def __init__(
255281
no_location: bool = False,
256282
max_tokens: Optional[int] = None,
257283
allow_legacy_fragment_variables: bool = False,
284+
experimental_directives_on_directive_definitions: bool = False,
258285
lexer: Optional[Lexer] = None,
259286
):
260287
source = (
@@ -267,6 +294,9 @@ def __init__(
267294
self._no_location = no_location
268295
self._max_tokens = max_tokens
269296
self._allow_legacy_fragment_variables = allow_legacy_fragment_variables
297+
self._experimental_directives_on_directive_definitions = (
298+
experimental_directives_on_directive_definitions
299+
)
270300
self._token_counter = 0
271301

272302
def parse_name(self) -> NameNode:
@@ -710,6 +740,11 @@ def parse_type_system_extension(self) -> TypeSystemExtensionNode:
710740
)
711741
if method_name: # pragma: no cover
712742
return getattr(self, f"parse_{method_name}")()
743+
if (
744+
keyword_token.value == "directive"
745+
and self._experimental_directives_on_directive_definitions
746+
):
747+
return self.parse_directive_definition_extension()
713748
raise self.unexpected(keyword_token)
714749

715750
def peek_description(self) -> bool:
@@ -1057,6 +1092,22 @@ def parse_input_object_type_extension(self) -> InputObjectTypeExtensionNode:
10571092
name=name, directives=directives, fields=fields, loc=self.loc(start)
10581093
)
10591094

1095+
def parse_directive_definition_extension(self) -> DirectiveExtensionNode:
1096+
"""DirectiveDefinitionExtension"""
1097+
start = self._lexer.token
1098+
self.expect_keyword("extend")
1099+
self.expect_keyword("directive")
1100+
self.expect_token(TokenKind.AT)
1101+
name = self.parse_name()
1102+
directives = self.parse_const_directives()
1103+
if not directives:
1104+
raise self.unexpected()
1105+
return DirectiveExtensionNode(
1106+
name=name,
1107+
directives=directives,
1108+
loc=self.loc(start),
1109+
)
1110+
10601111
def parse_directive_definition(self) -> DirectiveDefinitionNode:
10611112
"""DirectiveDefinition"""
10621113
start = self._lexer.token
@@ -1065,13 +1116,19 @@ def parse_directive_definition(self) -> DirectiveDefinitionNode:
10651116
self.expect_token(TokenKind.AT)
10661117
name = self.parse_name()
10671118
args = self.parse_argument_defs()
1119+
directives = (
1120+
self.parse_const_directives()
1121+
if self._experimental_directives_on_directive_definitions
1122+
else []
1123+
)
10681124
repeatable = self.expect_optional_keyword("repeatable")
10691125
self.expect_keyword("on")
10701126
locations = self.parse_directive_locations()
10711127
return DirectiveDefinitionNode(
10721128
description=description,
10731129
name=name,
10741130
arguments=args,
1131+
directives=directives,
10751132
repeatable=repeatable,
10761133
locations=locations,
10771134
loc=self.loc(start),

src/graphql/language/predicates.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
MemberCoordinateNode,
66
Node,
77
DefinitionNode,
8+
DirectiveExtensionNode,
89
ExecutableDefinitionNode,
910
ListValueNode,
1011
ObjectValueNode,
@@ -84,7 +85,9 @@ def is_type_definition_node(node: Node) -> bool:
8485

8586
def is_type_system_extension_node(node: Node) -> bool:
8687
"""Check whether the given node represents a type system extension."""
87-
return isinstance(node, (SchemaExtensionNode, TypeExtensionNode))
88+
return isinstance(
89+
node, (SchemaExtensionNode, DirectiveExtensionNode, TypeExtensionNode)
90+
)
8891

8992

9093
def is_type_extension_node(node: Node) -> bool:

src/graphql/language/printer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,12 @@ def leave_directive_definition(node: PrintedNode, *_args: Any) -> str:
318318
if has_multiline_items(args)
319319
else wrap("(", join(args, ", "), ")")
320320
)
321+
directives = wrap(" ", join(node.directives, " "))
321322
repeatable = " repeatable" if node.repeatable else ""
322323
locations = join(node.locations, " | ")
323324
return (
324325
wrap("", node.description, "\n")
325-
+ f"directive @{node.name}{args}{repeatable} on {locations}"
326+
+ f"directive @{node.name}{args}{directives}{repeatable} on {locations}"
326327
)
327328

328329
@staticmethod
@@ -332,6 +333,10 @@ def leave_schema_extension(node: PrintedNode, *_args: Any) -> str:
332333
" ",
333334
)
334335

336+
@staticmethod
337+
def leave_directive_extension(node: PrintedNode, *_args: Any) -> str:
338+
return join((f"extend directive @{node.name}", join(node.directives, " ")), " ")
339+
335340
@staticmethod
336341
def leave_scalar_type_extension(node: PrintedNode, *_args: Any) -> str:
337342
return join(("extend scalar", node.name, join(node.directives, " ")), " ")

0 commit comments

Comments
 (0)