Skip to content

Commit 0832f0a

Browse files
committed
Added partial support for repeatable directives
Replicates graphql/graphql-js@2c7224c
1 parent 027e8f6 commit 0832f0a

16 files changed

+192
-52
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ a query language for APIs created by Facebook.
1313
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
1414

1515
The current version 1.0.5 of GraphQL-core-next is up-to-date with GraphQL.js version
16-
14.3.1. All parts of the API are covered by an extensive test suite of currently 1816
16+
14.3.1. All parts of the API are covered by an extensive test suite of currently 1822
1717
unit tests.
1818

1919

graphql/language/ast.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,11 +453,12 @@ class InputObjectTypeDefinitionNode(TypeDefinitionNode):
453453

454454

455455
class DirectiveDefinitionNode(TypeSystemDefinitionNode):
456-
__slots__ = "description", "name", "arguments", "locations"
456+
__slots__ = "description", "name", "arguments", "repeatable", "locations"
457457

458458
description: Optional[StringValueNode]
459459
name: NameNode
460460
arguments: Optional[List[InputValueDefinitionNode]]
461+
repeatable: bool
461462
locations: List[NameNode]
462463

463464

graphql/language/parser.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,19 +984,21 @@ def parse_input_object_type_extension(lexer: Lexer) -> InputObjectTypeExtensionN
984984

985985

986986
def parse_directive_definition(lexer: Lexer) -> DirectiveDefinitionNode:
987-
"""InputObjectTypeExtension"""
987+
"""DirectiveDefinition"""
988988
start = lexer.token
989989
description = parse_description(lexer)
990990
expect_keyword(lexer, "directive")
991991
expect_token(lexer, TokenKind.AT)
992992
name = parse_name(lexer)
993993
args = parse_argument_defs(lexer)
994+
repeatable = expect_optional_keyword(lexer, "repeatable")
994995
expect_keyword(lexer, "on")
995996
locations = parse_directive_locations(lexer)
996997
return DirectiveDefinitionNode(
997998
description=description,
998999
name=name,
9991000
arguments=args,
1001+
repeatable=repeatable,
10001002
locations=locations,
10011003
loc=loc(lexer, start),
10021004
)

graphql/language/printer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,9 @@ def leave_directive_definition(self, node, *_args):
243243
if has_multiline_items(args)
244244
else wrap("(", join(args, ", "), ")")
245245
)
246+
repeatable = " repeatable" if node.repeatable else ""
246247
locations = join(node.locations, " | ")
247-
return f"directive @{node.name}{args} on {locations}"
248+
return f"directive @{node.name}{args}{repeatable} on {locations}"
248249

249250
def leave_schema_extension(self, node, *_args):
250251
return join(

graphql/type/directives.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class GraphQLDirective:
2828

2929
name: str
3030
locations: Sequence[DirectiveLocation]
31+
is_repeatable: bool
3132
args: Dict[str, GraphQLArgument]
3233
description: Optional[str]
3334
ast_node: Optional[ast.DirectiveDefinitionNode]
@@ -37,6 +38,7 @@ def __init__(
3738
name: str,
3839
locations: Sequence[DirectiveLocation],
3940
args: Dict[str, GraphQLArgument] = None,
41+
is_repeatable: bool = False,
4042
description: str = None,
4143
ast_node: ast.DirectiveDefinitionNode = None,
4244
) -> None:
@@ -46,6 +48,8 @@ def __init__(
4648
raise TypeError("The directive name must be a string.")
4749
if not isinstance(locations, (list, tuple)):
4850
raise TypeError(f"{name} locations must be a list/tuple.")
51+
if not isinstance(is_repeatable, bool):
52+
raise TypeError(f"{name} is_repeatable flag must be True or False.")
4953
if not all(isinstance(value, DirectiveLocation) for value in locations):
5054
try:
5155
locations = [
@@ -83,6 +87,7 @@ def __init__(
8387
self.name = name
8488
self.locations = locations
8589
self.args = args
90+
self.is_repeatable = is_repeatable
8691
self.description = description
8792
self.ast_node = ast_node
8893

@@ -97,6 +102,7 @@ def to_kwargs(self) -> Dict[str, Any]:
97102
name=self.name,
98103
locations=self.locations,
99104
args=self.args,
105+
is_repeatable=self.is_repeatable,
100106
description=self.description,
101107
ast_node=self.ast_node,
102108
)

graphql/utilities/build_ast_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ def build_directive(self, directive: DirectiveDefinitionNode) -> GraphQLDirectiv
211211
name=directive.name.value,
212212
description=directive.description.value if directive.description else None,
213213
locations=locations,
214+
is_repeatable=directive.repeatable,
214215
args={
215216
arg.name.value: self.build_arg(arg) for arg in directive.arguments or []
216217
},

graphql/utilities/schema_printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ def print_directive(directive: GraphQLDirective) -> str:
239239
print_description(directive)
240240
+ f"directive @{directive.name}"
241241
+ print_args(directive.args)
242+
+ (" repeatable" if directive.is_repeatable else "")
242243
+ " on "
243244
+ " | ".join(location.name for location in directive.locations)
244245
)
Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from typing import Dict, List
1+
from typing import Dict, List, Union, cast
22

33
from ...error import GraphQLError
4-
from ...language import DirectiveNode, Node
5-
from . import ASTValidationRule
4+
from ...language import DirectiveDefinitionNode, DirectiveNode, Node
5+
from ...type import specified_directives
6+
from . import ASTValidationRule, SDLValidationContext, ValidationContext
67

78
__all__ = ["UniqueDirectivesPerLocationRule", "duplicate_directive_message"]
89

@@ -14,10 +15,28 @@ def duplicate_directive_message(directive_name: str) -> str:
1415
class UniqueDirectivesPerLocationRule(ASTValidationRule):
1516
"""Unique directive names per location
1617
17-
A GraphQL document is only valid if all directives at a given location are uniquely
18-
named.
18+
A GraphQL document is only valid if all non-repeatable directives at a given
19+
location are uniquely named.
1920
"""
2021

22+
context: Union[ValidationContext, SDLValidationContext]
23+
24+
def __init__(self, context: Union[ValidationContext, SDLValidationContext]) -> None:
25+
super().__init__(context)
26+
unique_directive_map: Dict[str, bool] = {}
27+
28+
schema = context.schema
29+
defined_directives = (
30+
schema.directives if schema else cast(List, specified_directives)
31+
)
32+
for directive in defined_directives:
33+
unique_directive_map[directive.name] = not directive.is_repeatable
34+
ast_definitions = context.document.definitions
35+
for def_ in ast_definitions:
36+
if isinstance(def_, DirectiveDefinitionNode):
37+
unique_directive_map[def_.name.value] = not def_.repeatable
38+
self.unique_directive_map = unique_directive_map
39+
2140
# Many different AST nodes may contain directives. Rather than listing them all,
2241
# just listen for entering any node, and check to see if it defines any directives.
2342
def enter(self, node: Node, *_args):
@@ -26,12 +45,14 @@ def enter(self, node: Node, *_args):
2645
known_directives: Dict[str, DirectiveNode] = {}
2746
for directive in directives:
2847
directive_name = directive.name.value
29-
if directive_name in known_directives:
30-
self.report_error(
31-
GraphQLError(
32-
duplicate_directive_message(directive_name),
33-
[known_directives[directive_name], directive],
48+
49+
if self.unique_directive_map.get(directive_name):
50+
if directive_name in known_directives:
51+
self.report_error(
52+
GraphQLError(
53+
duplicate_directive_message(directive_name),
54+
[known_directives[directive_name], directive],
55+
)
3456
)
35-
)
36-
else:
37-
known_directives[directive_name] = directive
57+
else:
58+
known_directives[directive_name] = directive

tests/fixtures/schema_kitchen_sink.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ directive @include2(if: Boolean!) on
142142
| FRAGMENT_SPREAD
143143
| INLINE_FRAGMENT
144144

145+
directive @myRepeatableDir(name: String!) repeatable on
146+
| OBJECT
147+
| INTERFACE
148+
145149
extend schema @onSchema
146150

147151
extend schema @onSchema {

tests/language/test_schema_parser.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from graphql.error import GraphQLSyntaxError
66
from graphql.language import (
77
BooleanValueNode,
8+
DirectiveDefinitionNode,
9+
DirectiveNode,
810
DocumentNode,
911
EnumTypeDefinitionNode,
1012
EnumValueDefinitionNode,
@@ -22,7 +24,6 @@
2224
OperationTypeDefinitionNode,
2325
ScalarTypeDefinitionNode,
2426
SchemaExtensionNode,
25-
DirectiveNode,
2627
StringValueNode,
2728
UnionTypeDefinitionNode,
2829
parse,
@@ -610,6 +611,32 @@ def simple_input_object_with_args_should_fail():
610611
(3, 8),
611612
)
612613

614+
def directive_definition():
615+
body = "directive @foo on OBJECT | INTERFACE"
616+
definition = assert_definitions(body, (0, 36))
617+
assert isinstance(definition, DirectiveDefinitionNode)
618+
assert definition.name == name_node("foo", (11, 14))
619+
assert definition.description is None
620+
assert definition.arguments == []
621+
assert definition.repeatable is False
622+
assert definition.locations == [
623+
name_node("OBJECT", (18, 24)),
624+
name_node("INTERFACE", (27, 36)),
625+
]
626+
627+
def repeatable_directive_definition():
628+
body = "directive @foo repeatable on OBJECT | INTERFACE"
629+
definition = assert_definitions(body, (0, 47))
630+
assert isinstance(definition, DirectiveDefinitionNode)
631+
assert definition.name == name_node("foo", (11, 14))
632+
assert definition.description is None
633+
assert definition.arguments == []
634+
assert definition.repeatable is True
635+
assert definition.locations == [
636+
name_node("OBJECT", (29, 35)),
637+
name_node("INTERFACE", (38, 47)),
638+
]
639+
613640
def directive_with_incorrect_locations():
614641
assert_syntax_error(
615642
"\ndirective @foo on FIELD | INCORRECT_LOCATION",

0 commit comments

Comments
 (0)