Skip to content

Commit 1247877

Browse files
Add comprehensive directive support to DSL module (#563)
- `DSLDirective` class: Represents GraphQL directives with argument validation and AST generation - `DSLDirectable` mixin: Provides reusable `.directives()` method for all DSL elements that support directives - `DSLFragmentSpread` class: Represents fragment spreads with their own directives, separate from fragment definitions - Executable directive location support on query, mutation, subscription, fields, fragments, inline fragments, fragment spreads, and variable definitions ([spec](https://spec.graphql.org/October2021/#sec-Type-System.Directives)) - Automatic schema resolution: Fields automatically use their parent schema for custom directive validation - Fallback on builtin directives: Built-in directives are still available if a schema is not available to validate against The implementation follows the [October 2021 GraphQL specification](https://spec.graphql.org/October2021/) for executable directive locations and maintains backward compatibility with existing DSL code. Users can now use both built-in directives (`@skip`, `@include`) and custom schema directives across all supported GraphQL locations. Co-authored-by: Leszek Hanusz <[email protected]>
1 parent 76ff8ad commit 1247877

File tree

4 files changed

+1150
-28
lines changed

4 files changed

+1150
-28
lines changed

docs/advanced/dsl_module.rst

Lines changed: 303 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ from the :code:`ds` instance
6464

6565
ds.Query.hero.select(ds.Character.name)
6666

67-
The select method return the same instance, so it is possible to chain the calls::
67+
The select method returns the same instance, so it is possible to chain the calls::
6868

6969
ds.Query.hero.select(ds.Character.name).select(ds.Character.id)
7070

71-
Or do it sequencially::
71+
Or do it sequentially::
7272

7373
hero_query = ds.Query.hero
7474

@@ -279,7 +279,7 @@ will generate the request::
279279
Multiple operations in a document
280280
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
281281

282-
It is possible to create an Document with multiple operations::
282+
It is possible to create a Document with multiple operations::
283283

284284
query = dsl_gql(
285285
operation_name_1=DSLQuery( ... ),
@@ -384,6 +384,305 @@ you can use the :class:`DSLMetaField <gql.dsl.DSLMetaField>` class::
384384
DSLMetaField("__typename")
385385
)
386386

387+
Directives
388+
^^^^^^^^^^
389+
390+
`Directives`_ provide a way to describe alternate runtime execution and type validation
391+
behavior in a GraphQL document. The DSL module supports both built-in GraphQL directives
392+
(:code:`@skip`, :code:`@include`) and custom schema-defined directives.
393+
394+
To add directives to DSL elements, use the :meth:`DSLSchema.__call__ <gql.dsl.DSLSchema.__call__>`
395+
factory method and the :meth:`directives <gql.dsl.DSLDirectable.directives>` method::
396+
397+
# Using built-in @skip directive with DSLSchema.__call__ factory
398+
ds.Query.hero.select(
399+
ds.Character.name.directives(ds("@skip").args(**{"if": True}))
400+
)
401+
402+
Directive Arguments
403+
"""""""""""""""""""
404+
405+
Directive arguments can be passed using the :meth:`args <gql.dsl.DSLDirective.args>` method.
406+
For arguments that don't conflict with Python reserved words, you can pass them directly::
407+
408+
# Using the args method for non-reserved names
409+
ds("@custom").args(value="foo", reason="testing")
410+
411+
It can also be done by calling the directive directly::
412+
413+
ds("@custom")(value="foo", reason="testing")
414+
415+
However, when the GraphQL directive argument name conflicts with a Python reserved word
416+
(like :code:`if`), you need to unpack a dictionary to escape it::
417+
418+
# Dictionary unpacking for Python reserved words
419+
ds("@skip").args(**{"if": True})
420+
ds("@include")(**{"if": False})
421+
422+
This ensures that the exact GraphQL argument name is passed to the directive and that
423+
no post-processing of arguments is required.
424+
425+
The :meth:`DSLSchema.__call__ <gql.dsl.DSLSchema.__call__>` factory method automatically handles
426+
schema lookup and validation for both built-in directives (:code:`@skip`, :code:`@include`)
427+
and custom schema-defined directives using the same syntax.
428+
429+
Directive Locations
430+
"""""""""""""""""""
431+
432+
The DSL module supports all executable directive locations from the GraphQL specification:
433+
434+
.. list-table::
435+
:header-rows: 1
436+
:widths: 25 35 40
437+
438+
* - GraphQL Spec Location
439+
- DSL Class/Method
440+
- Description
441+
* - QUERY
442+
- :code:`DSLQuery.directives()`
443+
- Directives on query operations
444+
* - MUTATION
445+
- :code:`DSLMutation.directives()`
446+
- Directives on mutation operations
447+
* - SUBSCRIPTION
448+
- :code:`DSLSubscription.directives()`
449+
- Directives on subscription operations
450+
* - FIELD
451+
- :code:`DSLField.directives()`
452+
- Directives on fields (including meta-fields)
453+
* - FRAGMENT_DEFINITION
454+
- :code:`DSLFragment.directives()`
455+
- Directives on fragment definitions
456+
* - FRAGMENT_SPREAD
457+
- :code:`DSLFragmentSpread.directives()`
458+
- Directives on fragment spreads (via .spread())
459+
* - INLINE_FRAGMENT
460+
- :code:`DSLInlineFragment.directives()`
461+
- Directives on inline fragments
462+
* - VARIABLE_DEFINITION
463+
- :code:`DSLVariable.directives()`
464+
- Directives on variable definitions
465+
466+
Examples by Location
467+
""""""""""""""""""""
468+
469+
**Operation directives**::
470+
471+
# Query operation
472+
query = DSLQuery(ds.Query.hero.select(ds.Character.name)).directives(
473+
ds("@customQueryDirective")
474+
)
475+
476+
# Mutation operation
477+
mutation = DSLMutation(
478+
ds.Mutation.createReview.args(episode=6, review={"stars": 5}).select(
479+
ds.Review.stars
480+
)
481+
).directives(ds("@customMutationDirective"))
482+
483+
**Field directives**::
484+
485+
# Single directive on field
486+
ds.Query.hero.select(
487+
ds.Character.name.directives(ds("@customFieldDirective"))
488+
)
489+
490+
# Multiple directives on a field
491+
ds.Query.hero.select(
492+
ds.Character.appearsIn.directives(
493+
ds("@repeat").args(value="first"),
494+
ds("@repeat").args(value="second"),
495+
ds("@repeat").args(value="third"),
496+
)
497+
)
498+
499+
**Fragment directives**:
500+
501+
You can add directives to fragment definitions and to fragment spread instances.
502+
To do this, first define your fragment in the usual way::
503+
504+
name_and_appearances = (
505+
DSLFragment("NameAndAppearances")
506+
.on(ds.Character)
507+
.select(ds.Character.name, ds.Character.appearsIn)
508+
)
509+
510+
Then, use :meth:`spread() <gql.dsl.DSLFragment.spread>` when you need to add
511+
directives to the fragment spread::
512+
513+
query_with_fragment = DSLQuery(
514+
ds.Query.hero.select(
515+
name_and_appearances.spread().directives(
516+
ds("@customFragmentSpreadDirective")
517+
)
518+
)
519+
)
520+
521+
The :meth:`spread() <gql.dsl.DSLFragment.spread>` method creates a
522+
:class:`DSLFragmentSpread <gql.dsl.DSLFragmentSpread>` instance that allows you to add
523+
directives specific to the fragment spread location, separate from directives on the
524+
fragment definition itself.
525+
526+
Example with fragment definition and spread-specific directives::
527+
528+
# Fragment definition with directive
529+
name_and_appearances = (
530+
DSLFragment("CharacterInfo")
531+
.on(ds.Character)
532+
.select(ds.Character.name, ds.Character.appearsIn)
533+
.directives(ds("@customFragmentDefinitionDirective"))
534+
)
535+
536+
# Using fragment with spread-specific directives
537+
query_without_spread_directive = DSLQuery(
538+
# Direct usage (no spread directives)
539+
ds.Query.hero.select(name_and_appearances)
540+
)
541+
query_with_spread_directive = DSLQuery(
542+
# Enhanced usage with spread directives
543+
name_and_appearances.spread().directives(
544+
ds("@customFragmentSpreadDirective")
545+
)
546+
)
547+
548+
# Don't forget to include the fragment definition in dsl_gql
549+
query = dsl_gql(
550+
name_and_appearances,
551+
BaseQuery=query_without_spread_directive,
552+
QueryWithDirective=query_with_spread_directive,
553+
)
554+
555+
This generates GraphQL equivalent to::
556+
557+
fragment CharacterInfo on Character @customFragmentDefinitionDirective {
558+
name
559+
appearsIn
560+
}
561+
562+
{
563+
BaseQuery hero {
564+
...CharacterInfo
565+
}
566+
QueryWithDirective hero {
567+
...CharacterInfo @customFragmentSpreadDirective
568+
}
569+
}
570+
571+
**Inline fragment directives**:
572+
573+
Inline fragments also support directives using the
574+
:meth:`directives <gql.dsl.DSLInlineFragment.directives>` method::
575+
576+
query_with_directive = ds.Query.hero.args(episode=6).select(
577+
ds.Character.name,
578+
DSLInlineFragment().on(ds.Human).select(ds.Human.homePlanet).directives(
579+
ds("@customInlineFragmentDirective")
580+
)
581+
)
582+
583+
This generates::
584+
585+
{
586+
hero(episode: JEDI) {
587+
name
588+
... on Human @customInlineFragmentDirective {
589+
homePlanet
590+
}
591+
}
592+
}
593+
594+
**Variable definition directives**:
595+
596+
You can also add directives to variable definitions using the
597+
:meth:`directives <gql.dsl.DSLVariable.directives>` method::
598+
599+
var = DSLVariableDefinitions()
600+
var.episode.directives(ds("@customVariableDirective"))
601+
# Note: the directive is attached to the `.episode` variable definition (singular),
602+
# and not the `var` variable definitions (plural) holder.
603+
604+
op = DSLQuery(ds.Query.hero.args(episode=var.episode).select(ds.Character.name))
605+
op.variable_definitions = var
606+
607+
This will generate::
608+
609+
query ($episode: Episode @customVariableDirective) {
610+
hero(episode: $episode) {
611+
name
612+
}
613+
}
614+
615+
Complete Example for Directives
616+
"""""""""""""""""""""""""""""""
617+
618+
Here's a comprehensive example showing directives on multiple locations:
619+
620+
.. code-block:: python
621+
622+
from gql.dsl import DSLFragment, DSLInlineFragment, DSLQuery, dsl_gql
623+
624+
# Create variables for directive conditions
625+
var = DSLVariableDefinitions()
626+
627+
# Fragment with directive on definition
628+
character_fragment = DSLFragment("CharacterInfo").on(ds.Character).select(
629+
ds.Character.name, ds.Character.appearsIn
630+
).directives(ds("@fragmentDefinition"))
631+
632+
# Query with directives on multiple locations
633+
query = DSLQuery(
634+
ds.Query.hero.args(episode=var.episode).select(
635+
# Field with directive
636+
ds.Character.name.directives(ds("@skip").args(**{"if": var.skipName})),
637+
638+
# Fragment spread with directive
639+
character_fragment.spread().directives(
640+
ds("@include").args(**{"if": var.includeFragment})
641+
),
642+
643+
# Inline fragment with directive
644+
DSLInlineFragment().on(ds.Human).select(ds.Human.homePlanet).directives(
645+
ds("@skip").args(**{"if": var.skipHuman})
646+
),
647+
648+
# Meta field with directive
649+
DSLMetaField("__typename").directives(
650+
ds("@include").args(**{"if": var.includeType})
651+
)
652+
)
653+
).directives(ds("@query")) # Operation directive
654+
655+
# Variable definition with directive
656+
var.episode.directives(ds("@variableDefinition"))
657+
query.variable_definitions = var
658+
659+
# Generate the document
660+
document = dsl_gql(character_fragment, query)
661+
662+
This generates GraphQL equivalent to::
663+
664+
fragment CharacterInfo on Character @fragmentDefinition {
665+
name
666+
appearsIn
667+
}
668+
669+
query (
670+
$episode: Episode @variableDefinition
671+
$skipName: Boolean!
672+
$includeFragment: Boolean!
673+
$skipHuman: Boolean!
674+
$includeType: Boolean!
675+
) @query {
676+
hero(episode: $episode) {
677+
name @skip(if: $skipName)
678+
...CharacterInfo @include(if: $includeFragment)
679+
... on Human @skip(if: $skipHuman) {
680+
homePlanet
681+
}
682+
__typename @include(if: $includeType)
683+
}
684+
}
685+
387686
Executable examples
388687
-------------------
389688

@@ -399,4 +698,5 @@ Sync example
399698

400699
.. _Fragment: https://graphql.org/learn/queries/#fragments
401700
.. _Inline Fragment: https://graphql.org/learn/queries/#inline-fragments
701+
.. _Directives: https://graphql.org/learn/queries/#directives
402702
.. _issue #308: https://github.com/graphql-python/gql/issues/308

0 commit comments

Comments
 (0)