Skip to content

Commit 5e910bc

Browse files
authored
Merge pull request #9 from lirsacc/repeatable-directives
Repeatable directives
2 parents 7ca1c1b + 86d072c commit 5e910bc

23 files changed

+408
-68
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ Unreleased
2020
- Add `py_gql.exts.scalars.Base64String` scalar type.
2121
- Add support for schema description (see: [graphql/graphql-spec/pull/466](https://github.com/graphql/graphql-spec/pull/466)).
2222
- Add `py_gql.exc.UnknownDirective` for cleaner errors in `py_gql.ResolveInfo.get_directive_arguments`.
23+
- Add support for repeatable directives (see: [graphql/graphql-spec/pull/472](https://github.com/graphql/graphql-spec/pull/472)).
24+
25+
- Language support (parser, ast, SDL and schema definitions)
26+
- Schema directives: repeatable directives applied multiple times when calling `build_schema()` will be called multiple times in order.
27+
- `ResolveInfo.get_directive_arguments` has not been modified to not break exising code. It returns the first set of arguments for repeated directives.
28+
- `ResolveInfo.get_all_directive_arguments` has been added to handle repeated directives.
2329

2430
[0.6.1](https://github.com/lirsacc/py-gql/releases/tag/0.6.1) - 2020-04-01
2531
--------------------------------------------------------------------------

src/py_gql/execution/wrappers.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
TYPE_NAME_INTROSPECTION_FIELD,
2323
)
2424
from ..utilities import (
25+
all_directive_arguments,
2526
coerce_argument_values,
2627
collect_fields,
27-
directive_arguments,
2828
selected_fields,
2929
)
3030
from .runtime import Runtime
@@ -192,6 +192,10 @@ class ResolveInfo:
192192
193193
This is the 3rd positional argument provided to resolver functions and is
194194
constructed internally during query execution.
195+
196+
Warning:
197+
This class assumes that the document and schema have been validated for
198+
execution and may break unexectedly if used outside of such a context.
195199
"""
196200

197201
__slots__ = (
@@ -225,9 +229,7 @@ def __init__(
225229
self.runtime = runtime
226230

227231
self._context = context
228-
self._directive_arguments = (
229-
{}
230-
) # type: Dict[str, Optional[Dict[str, Any]]]
232+
self._directive_arguments = {} # type: Dict[str, List[Dict[str, Any]]]
231233

232234
@property
233235
def schema(self) -> Schema: # noqa: D401
@@ -254,9 +256,7 @@ def get_directive_arguments(self, name: str) -> Optional[Dict[str, Any]]:
254256
"""
255257
Extract arguments for a given directive on the current field.
256258
257-
Warning:
258-
This method assumes the document has been validated and the
259-
definition exists and is valid at this position.
259+
This has the same semantics as `py_gql.utilities.directive_arguments`.
260260
261261
Args:
262262
name: The name of the directive to extract.
@@ -265,6 +265,21 @@ def get_directive_arguments(self, name: str) -> Optional[Dict[str, Any]]:
265265
``None`` if the directive is not present on the current field and a
266266
dictionary of coerced arguments otherwise.
267267
"""
268+
args = self.get_all_directive_arguments(name)
269+
return args[0] if args else None
270+
271+
def get_all_directive_arguments(self, name: str) -> List[Dict[str, Any]]:
272+
"""
273+
Extract arguments for a given directive on the current field.
274+
275+
This has the same semantics as `py_gql.utilities.all_directive_arguments`.
276+
277+
Args:
278+
name: The name of the directive to extract.
279+
280+
Returns:
281+
List of directive arguments in order of occurrence.
282+
"""
268283
try:
269284
return self._directive_arguments[name]
270285
except KeyError:
@@ -273,7 +288,7 @@ def get_directive_arguments(self, name: str) -> Optional[Dict[str, Any]]:
273288
except KeyError:
274289
raise UnknownDirective(name) from None
275290

276-
args = self._directive_arguments[name] = directive_arguments(
291+
args = self._directive_arguments[name] = all_directive_arguments(
277292
directive_def, self.nodes[0], self._context.variables,
278293
)
279294
return args

src/py_gql/lang/ast.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,20 +944,23 @@ class DirectiveDefinition(SupportDescription, TypeSystemDefinition):
944944
"description",
945945
"name",
946946
"arguments",
947+
"repeatable",
947948
"locations",
948949
)
949950

950951
def __init__(
951952
self,
952953
name: Name,
953954
arguments: Optional[List[InputValueDefinition]] = None,
955+
repeatable: bool = False,
954956
locations: Optional[List[Name]] = None,
955957
source: Optional[str] = None,
956958
loc: Optional[Tuple[int, int]] = None,
957959
description: Optional[StringValue] = None,
958960
):
959961
self.name = name
960962
self.arguments = arguments or [] # type: List[InputValueDefinition]
963+
self.repeatable = repeatable
961964
self.locations = locations or [] # type: List[Name]
962965
self.source = source
963966
self.loc = loc

src/py_gql/lang/parser.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,25 @@ def skip(self, kind: Kind) -> bool:
394394
return True
395395
return False
396396

397+
def skip_keyword(self, keyword: str) -> bool:
398+
"""
399+
Conditionally advance the parser asserting over a given keyword.
400+
401+
Args:
402+
keyword (str): Expected keyword
403+
404+
Returns:
405+
``True`` if the next token was the given keyword and we've advanced
406+
the parser, ``False`` otherwise.
407+
408+
"""
409+
next_token = self.peek()
410+
if next_token.__class__ is Name and next_token.value == keyword:
411+
self.advance()
412+
return True
413+
414+
return False
415+
397416
def many(
398417
self, open_kind: Kind, parse_fn: Callable[[], N], close_kind: Kind
399418
) -> List[N]:
@@ -1430,19 +1449,22 @@ def parse_input_object_type_extension(
14301449
def parse_directive_definition(self) -> _ast.DirectiveDefinition:
14311450
"""
14321451
DirectiveDefinition :
1433-
Description? directive @ Name ArgumentsDefinition? on DirectiveLocations
1452+
Description? directive @ Name ArgumentsDefinition? repeatable?
1453+
on DirectiveLocations
14341454
"""
14351455
start = self.peek()
14361456
desc = self.parse_description()
14371457
self.expect_keyword("directive")
14381458
self.expect(At)
14391459
name = self.parse_name()
14401460
args = self.parse_argument_definitions()
1461+
repeatable = self.skip_keyword("repeatable")
14411462
self.expect_keyword("on")
14421463
return _ast.DirectiveDefinition(
14431464
description=desc,
14441465
name=name,
14451466
arguments=args,
1467+
repeatable=repeatable,
14461468
locations=self.parse_directive_locations(),
14471469
loc=self._loc(start),
14481470
source=self._source,

src/py_gql/lang/printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ def print_directive_definition(self, node: _ast.DirectiveDefinition) -> str:
484484
"directive @",
485485
node.name.value,
486486
self.print_argument_definitions(node),
487+
" repeatable" if node.repeatable else "",
487488
" on ",
488489
_join(map(self, node.locations), " | "),
489490
]

src/py_gql/schema/schema_visitor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def on_directive(self, directive: Directive) -> Optional[Directive]:
178178
directive.name,
179179
directive.locations,
180180
args=updated_args,
181+
repeatable=directive.repeatable,
181182
description=directive.description,
182183
node=directive.node,
183184
)

src/py_gql/schema/types.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,30 +1007,42 @@ class Directive:
10071007
Type system creators will usually not create these directly.
10081008
10091009
Args:
1010-
name: Directive name
1010+
name: Directive name.
10111011
1012-
locations: Possible locations for that directive
1012+
locations: Possible locations for that directive.
10131013
1014-
args: Argument definitions
1014+
args: Argument definitions.
10151015
1016-
description: Directive description
1016+
repeatable: Specify that the directive can be applied multiple times to
1017+
the same target. Repeatable directives are often useful when the
1018+
same directive should be used with different arguments at a single
1019+
location, especially in cases where additional information needs to
1020+
be provided to a type or schema extension via a directive.
1021+
1022+
description: Directive description.
10171023
10181024
node: Source node used when building type from the SDL.
10191025
10201026
Attributes:
1021-
name (str): Directive name
1027+
name (str): Directive name.
10221028
1023-
description (Optional[str]): Directive description
1029+
description (Optional[str]): Directive description.
10241030
1025-
locations (List[str]): Possible locations for that directive
1031+
locations (List[str]): Possible locations for that directive.
10261032
10271033
arguments (List[py_gql.schema.Argument]): Directive arguments.
10281034
10291035
argument_map (Dict[str, py_gql.schema.Argument]):
10301036
Directive arguments in indexed form.
10311037
1038+
repeatable: Specify that the directive can be applied multiple times to
1039+
the same target. Repeatable directives are often useful when the
1040+
same directive should be used with different arguments at a single
1041+
location, especially in cases where additional information needs to
1042+
be provided to a type or schema extension via a directive.
1043+
10321044
node (Optional[py_gql.lang.ast.DirectiveDefinition]):
1033-
Source node used when building type from the SDL
1045+
Source node used when building type from the SDL.
10341046
"""
10351047

10361048
ALL_LOCATIONS = DIRECTIVE_LOCATIONS
@@ -1042,6 +1054,7 @@ def __init__(
10421054
name: str,
10431055
locations: Sequence[str],
10441056
args: Optional[List[Argument]] = None,
1057+
repeatable: bool = False,
10451058
description: Optional[str] = None,
10461059
node: Optional[_ast.DirectiveDefinition] = None,
10471060
):
@@ -1057,6 +1070,7 @@ def __init__(
10571070
self.name = name
10581071
self.description = description
10591072
self.locations = locations
1073+
self.repeatable = repeatable
10601074
self.arguments = args if args is not None else []
10611075
self.argument_map = {arg.name: arg for arg in self.arguments}
10621076
self.node = node

src/py_gql/sdl/ast_schema_printer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,10 +342,11 @@ def print_input_object_type(self, type_: InputObjectType) -> str:
342342
)
343343

344344
def print_directive_definition(self, directive: Directive) -> str:
345-
return "%sdirective @%s%s on %s" % (
345+
return "%sdirective @%s%s%son %s" % (
346346
self.print_description(directive),
347347
directive.name,
348348
self.print_arguments(directive.arguments, 0),
349+
" repeatable " if directive.repeatable else " ",
349350
" | ".join(directive.locations),
350351
)
351352

src/py_gql/sdl/ast_type_builder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def build_directive(
154154
return Directive(
155155
name=directive_def.name.value,
156156
description=_desc(directive_def),
157+
repeatable=directive_def.repeatable,
157158
locations=[loc.value for loc in directive_def.locations],
158159
args=(
159160
[self._build_argument(arg) for arg in directive_def.arguments]
@@ -203,6 +204,7 @@ def extend_directive(self, directive: Directive) -> Directive:
203204
return Directive(
204205
directive.name,
205206
description=directive.description,
207+
repeatable=directive.repeatable,
206208
locations=directive.locations,
207209
args=[self._extend_argument(a) for a in directive.arguments],
208210
node=directive.node,

src/py_gql/sdl/schema_directives.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def _collect_schema_directives(
177177
[node],
178178
)
179179

180-
if name in applied:
180+
if name in applied and not directive_def.repeatable:
181181
raise SDLError('Directive "@%s" already applied' % name, [node])
182182

183183
args = coerce_argument_values(directive_def, node)

src/py_gql/utilities/__init__.py

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

99
from .ast_node_from_value import ast_node_from_value
1010
from .coerce_value import (
11+
all_directive_arguments,
1112
coerce_argument_values,
1213
coerce_value,
1314
coerce_variable_values,
@@ -33,6 +34,7 @@
3334
"collect_fields_untyped",
3435
"selected_fields",
3536
"directive_arguments",
37+
"all_directive_arguments",
3638
"introspection_query",
3739
"untyped_value_from_ast",
3840
"value_from_ast",

src/py_gql/utilities/coerce_value.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,48 @@ def coerce_argument_values(
264264
return coerced_values
265265

266266

267+
def all_directive_arguments(
268+
definition: Directive,
269+
node: _ast.SupportDirectives,
270+
variables: Optional[Mapping[str, Any]] = None,
271+
) -> List[Dict[str, Any]]:
272+
"""
273+
Extract all directive arguments given node and a directive definition.
274+
275+
Args:
276+
definition: Directive definition from which to extract arguments
277+
node: Parse node
278+
variables: Coerced variable values
279+
280+
Returns:
281+
List of coerced directive arguments for all occurrences of the directive.
282+
If the directive is not present the list is empty, otherwise this returns
283+
one or more (for repeatable directives) dictionnaries of arguments in
284+
the order they appear on the node.
285+
286+
Raises:
287+
CoercionError: If any argument fails to coerce, is missing, etc.
288+
289+
"""
290+
return [
291+
coerce_argument_values(definition, directive, variables)
292+
for directive in node.directives
293+
if directive.name.value == definition.name
294+
]
295+
296+
267297
def directive_arguments(
268298
definition: Directive,
269299
node: _ast.SupportDirectives,
270300
variables: Optional[Mapping[str, Any]] = None,
271301
) -> Optional[Dict[str, Any]]:
272302
"""
273-
Extract directive argument given node and a directive definition.
303+
Extract first directive arguments given node and a directive definition.
304+
305+
Warning:
306+
This extracts at most a single set of arguments which may not be
307+
suitable for repeatable directives. In that case `py_gql.utilities.
308+
all_directive_arguments`. should be preferred.
274309
275310
Args:
276311
definition: Directive definition from which to extract arguments
@@ -283,7 +318,6 @@ def directive_arguments(
283318
284319
Raises:
285320
CoercionError: If any argument fails to coerce, is missing, etc.
286-
287321
"""
288322
directive = find_one(
289323
node.directives, lambda d: d.name.value == definition.name

src/py_gql/validation/rules/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ def _validate_unique_directive_names(self, node):
731731
name = directive.name.value
732732
directive_def = self.schema.directives.get(name)
733733

734-
if directive_def is None:
734+
if directive_def is None or directive_def.repeatable:
735735
continue
736736

737737
if name in seen:

tests/fixtures/schema-kitchen-sink.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ directive @include2(if: Boolean!) on
125125
| FRAGMENT_SPREAD
126126
| INLINE_FRAGMENT
127127

128+
directive @myRepeatableDir(name: String!) repeatable on
129+
| OBJECT
130+
| INTERFACE
131+
128132
extend schema @onSchema
129133

130134
extend schema @onSchema {

tests/fixtures/schema-kitchen-sink.printed.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
108108

109109
directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
110110

111+
directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE
112+
111113
extend schema @onSchema
112114

113115
extend schema @onSchema {

0 commit comments

Comments
 (0)