Skip to content

Commit 0856f11

Browse files
authored
DSL meta fields implementation (#259)
1 parent 6b53422 commit 0856f11

File tree

5 files changed

+335
-26
lines changed

5 files changed

+335
-26
lines changed

docs/advanced/dsl_module.rst

+10
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,16 @@ this can be written in a concise manner::
338338
DSLInlineFragment().on(ds.Human).select(ds.Human.homePlanet)
339339
)
340340

341+
Meta-fields
342+
^^^^^^^^^^^
343+
344+
To define meta-fields (:code:`__typename`, :code:`__schema` and :code:`__type`),
345+
you can use the :class:`DSLMetaField <gql.dsl.DSLMetaField>` class::
346+
347+
query = ds.Query.hero.select(
348+
ds.Character.name,
349+
DSLMetaField("__typename")
350+
)
341351

342352
Executable examples
343353
-------------------

gql/dsl.py

+86-19
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
GraphQLNonNull,
2626
GraphQLObjectType,
2727
GraphQLSchema,
28+
GraphQLString,
2829
GraphQLWrappingType,
2930
InlineFragmentNode,
3031
IntValueNode,
@@ -46,6 +47,7 @@
4647
VariableDefinitionNode,
4748
VariableNode,
4849
assert_named_type,
50+
introspection_types,
4951
is_enum_type,
5052
is_input_object_type,
5153
is_leaf_type,
@@ -301,6 +303,7 @@ class DSLExecutable(ABC):
301303

302304
variable_definitions: "DSLVariableDefinitions"
303305
name: Optional[str]
306+
selection_set: SelectionSetNode
304307

305308
@property
306309
@abstractmethod
@@ -349,11 +352,31 @@ def __init__(
349352
f"Received type: {type(field)}"
350353
)
351354
)
355+
valid_type = False
352356
if isinstance(self, DSLOperation):
353-
assert field.type_name.upper() == self.operation_type.name, (
354-
f"Invalid root field for operation {self.operation_type.name}.\n"
355-
f"Received: {field.type_name}"
356-
)
357+
operation_name = self.operation_type.name
358+
if isinstance(field, DSLMetaField):
359+
if field.name in ["__schema", "__type"]:
360+
valid_type = operation_name == "QUERY"
361+
if field.name == "__typename":
362+
valid_type = operation_name != "SUBSCRIPTION"
363+
else:
364+
valid_type = field.parent_type.name.upper() == operation_name
365+
366+
else: # Fragments
367+
if isinstance(field, DSLMetaField):
368+
valid_type = field.name == "__typename"
369+
370+
if not valid_type:
371+
if isinstance(self, DSLOperation):
372+
error_msg = (
373+
"Invalid root field for operation "
374+
f"{self.operation_type.name}"
375+
)
376+
else:
377+
error_msg = f"Invalid field for fragment {self.name}"
378+
379+
raise AssertionError(f"{error_msg}: {field!r}")
357380

358381
self.selection_set = SelectionSetNode(
359382
selections=FrozenList(DSLSelectable.get_ast_fields(all_fields))
@@ -610,6 +633,11 @@ def select(
610633
fields, fields_with_alias
611634
)
612635

636+
# Check that we don't receive an invalid meta-field
637+
for field in added_fields:
638+
if isinstance(field, DSLMetaField) and field.name != "__typename":
639+
raise AssertionError(f"Invalid field for {self!r}: {field!r}")
640+
613641
# Get a list of AST Nodes for each added field
614642
added_selections: List[
615643
Union[FieldNode, InlineFragmentNode, FragmentSpreadNode]
@@ -668,8 +696,8 @@ class DSLField(DSLSelectableWithAlias, DSLSelector):
668696
def __init__(
669697
self,
670698
name: str,
671-
graphql_type: Union[GraphQLObjectType, GraphQLInterfaceType],
672-
graphql_field: GraphQLField,
699+
parent_type: Union[GraphQLObjectType, GraphQLInterfaceType],
700+
field: GraphQLField,
673701
):
674702
"""Initialize the DSLField.
675703
@@ -678,15 +706,21 @@ def __init__(
678706
Use attributes of the :class:`DSLType` instead.
679707
680708
:param name: the name of the field
681-
:param graphql_type: the GraphQL type definition from the schema
682-
:param graphql_field: the GraphQL field definition from the schema
709+
:param parent_type: the GraphQL type definition from the schema of the
710+
parent type of the field
711+
:param field: the GraphQL field definition from the schema
683712
"""
684713
DSLSelector.__init__(self)
685-
self._type = graphql_type
686-
self.field = graphql_field
714+
self.parent_type = parent_type
715+
self.field = field
687716
self.ast_field = FieldNode(name=NameNode(value=name), arguments=FrozenList())
688717
log.debug(f"Creating {self!r}")
689718

719+
@property
720+
def name(self):
721+
""":meta private:"""
722+
return self.ast_field.name.value
723+
690724
def __call__(self, **kwargs) -> "DSLField":
691725
return self.args(**kwargs)
692726

@@ -750,16 +784,49 @@ def select(
750784

751785
return self
752786

753-
@property
754-
def type_name(self):
755-
""":meta private:"""
756-
return self._type.name
757-
758787
def __repr__(self) -> str:
759-
return (
760-
f"<{self.__class__.__name__} {self._type.name}"
761-
f"::{self.ast_field.name.value}>"
762-
)
788+
return f"<{self.__class__.__name__} {self.parent_type.name}" f"::{self.name}>"
789+
790+
791+
class DSLMetaField(DSLField):
792+
"""DSLMetaField represents a GraphQL meta-field for the DSL code.
793+
794+
meta-fields are reserved field in the GraphQL type system prefixed with
795+
"__" two underscores and used for introspection.
796+
"""
797+
798+
meta_type = GraphQLObjectType(
799+
"meta-field",
800+
fields={
801+
"__typename": GraphQLField(GraphQLString),
802+
"__schema": GraphQLField(
803+
cast(GraphQLObjectType, introspection_types["__Schema"])
804+
),
805+
"__type": GraphQLField(
806+
cast(GraphQLObjectType, introspection_types["__Type"]),
807+
args={"name": GraphQLArgument(type_=GraphQLNonNull(GraphQLString))},
808+
),
809+
},
810+
)
811+
812+
def __init__(self, name: str):
813+
"""Initialize the meta-field.
814+
815+
:param name: the name between __typename, __schema or __type
816+
"""
817+
818+
try:
819+
field = self.meta_type.fields[name]
820+
except KeyError:
821+
raise AssertionError(f'Invalid meta-field "{name}"')
822+
823+
super().__init__(name, self.meta_type, field)
824+
825+
def alias(self, alias: str) -> "DSLSelectableWithAlias":
826+
"""
827+
:meta private:
828+
"""
829+
pass
763830

764831

765832
class DSLInlineFragment(DSLSelectable, DSLSelector):

gql/utilities/__init__.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
from .get_introspection_query_ast import get_introspection_query_ast
12
from .parse_result import parse_result
23
from .serialize_variable_values import serialize_value, serialize_variable_values
34
from .update_schema_enum import update_schema_enum
45
from .update_schema_scalars import update_schema_scalar, update_schema_scalars
56

67
__all__ = [
7-
"update_schema_scalars",
8-
"update_schema_scalar",
9-
"update_schema_enum",
108
"parse_result",
9+
"get_introspection_query_ast",
1110
"serialize_variable_values",
1211
"serialize_value",
12+
"update_schema_enum",
13+
"update_schema_scalars",
14+
"update_schema_scalar",
1315
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from itertools import repeat
2+
3+
from graphql import DocumentNode, GraphQLSchema
4+
5+
from gql.dsl import DSLFragment, DSLMetaField, DSLQuery, DSLSchema, dsl_gql
6+
7+
8+
def get_introspection_query_ast(
9+
descriptions: bool = True,
10+
specified_by_url: bool = False,
11+
directive_is_repeatable: bool = False,
12+
schema_description: bool = False,
13+
type_recursion_level: int = 7,
14+
) -> DocumentNode:
15+
"""Get a query for introspection as a document using the DSL module.
16+
17+
Equivalent to the get_introspection_query function from graphql-core
18+
but using the DSL module and allowing to select the recursion level.
19+
20+
Optionally, you can exclude descriptions, include specification URLs,
21+
include repeatability of directives, and specify whether to include
22+
the schema description as well.
23+
"""
24+
25+
ds = DSLSchema(GraphQLSchema())
26+
27+
fragment_FullType = DSLFragment("FullType").on(ds.__Type)
28+
fragment_InputValue = DSLFragment("InputValue").on(ds.__InputValue)
29+
fragment_TypeRef = DSLFragment("TypeRef").on(ds.__Type)
30+
31+
schema = DSLMetaField("__schema")
32+
33+
if descriptions and schema_description:
34+
schema.select(ds.__Schema.description)
35+
36+
schema.select(
37+
ds.__Schema.queryType.select(ds.__Type.name),
38+
ds.__Schema.mutationType.select(ds.__Type.name),
39+
ds.__Schema.subscriptionType.select(ds.__Type.name),
40+
)
41+
42+
schema.select(ds.__Schema.types.select(fragment_FullType))
43+
44+
directives = ds.__Schema.directives.select(ds.__Directive.name)
45+
46+
if descriptions:
47+
directives.select(ds.__Directive.description)
48+
if directive_is_repeatable:
49+
directives.select(ds.__Directive.isRepeatable)
50+
directives.select(
51+
ds.__Directive.locations, ds.__Directive.args.select(fragment_InputValue),
52+
)
53+
54+
schema.select(directives)
55+
56+
fragment_FullType.select(
57+
ds.__Type.kind, ds.__Type.name,
58+
)
59+
if descriptions:
60+
fragment_FullType.select(ds.__Type.description)
61+
if specified_by_url:
62+
fragment_FullType.select(ds.__Type.specifiedByUrl)
63+
64+
fields = ds.__Type.fields(includeDeprecated=True).select(ds.__Field.name)
65+
66+
if descriptions:
67+
fields.select(ds.__Field.description)
68+
69+
fields.select(
70+
ds.__Field.args.select(fragment_InputValue),
71+
ds.__Field.type.select(fragment_TypeRef),
72+
ds.__Field.isDeprecated,
73+
ds.__Field.deprecationReason,
74+
)
75+
76+
enum_values = ds.__Type.enumValues(includeDeprecated=True).select(
77+
ds.__EnumValue.name
78+
)
79+
80+
if descriptions:
81+
enum_values.select(ds.__EnumValue.description)
82+
83+
enum_values.select(
84+
ds.__EnumValue.isDeprecated, ds.__EnumValue.deprecationReason,
85+
)
86+
87+
fragment_FullType.select(
88+
fields,
89+
ds.__Type.inputFields.select(fragment_InputValue),
90+
ds.__Type.interfaces.select(fragment_TypeRef),
91+
enum_values,
92+
ds.__Type.possibleTypes.select(fragment_TypeRef),
93+
)
94+
95+
fragment_InputValue.select(ds.__InputValue.name)
96+
97+
if descriptions:
98+
fragment_InputValue.select(ds.__InputValue.description)
99+
100+
fragment_InputValue.select(
101+
ds.__InputValue.type.select(fragment_TypeRef), ds.__InputValue.defaultValue,
102+
)
103+
104+
fragment_TypeRef.select(
105+
ds.__Type.kind, ds.__Type.name,
106+
)
107+
108+
if type_recursion_level >= 1:
109+
current_field = ds.__Type.ofType.select(ds.__Type.kind, ds.__Type.name)
110+
fragment_TypeRef.select(current_field)
111+
112+
for _ in repeat(None, type_recursion_level - 1):
113+
new_oftype = ds.__Type.ofType.select(ds.__Type.kind, ds.__Type.name)
114+
current_field.select(new_oftype)
115+
current_field = new_oftype
116+
117+
query = DSLQuery(schema)
118+
119+
query.name = "IntrospectionQuery"
120+
121+
dsl_query = dsl_gql(query, fragment_FullType, fragment_InputValue, fragment_TypeRef)
122+
123+
return dsl_query

0 commit comments

Comments
 (0)