From b613feffce66613fa09a09fa18c30e004bb8009d Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Thu, 26 Aug 2021 22:09:47 +0200 Subject: [PATCH 01/10] Simple implementation of DSL inline fragments --- gql/dsl.py | 37 +++++++++++++++++++++++++++++-------- tests/starwars/test_dsl.py | 19 +++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index 6542d6a6..bcbb0be7 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -17,6 +17,7 @@ GraphQLObjectType, GraphQLSchema, GraphQLWrappingType, + InlineFragmentNode, ListTypeNode, ListValueNode, NamedTypeNode, @@ -407,6 +408,10 @@ class DSLField: method. """ + _type: Union[GraphQLObjectType, GraphQLInterfaceType] + ast_field: FieldNode + field: GraphQLField + def __init__( self, name: str, @@ -423,11 +428,9 @@ def __init__( :param graphql_type: the GraphQL type definition from the schema :param graphql_field: the GraphQL field definition from the schema """ - self._type: Union[GraphQLObjectType, GraphQLInterfaceType] = graphql_type - self.field: GraphQLField = graphql_field - self.ast_field: FieldNode = FieldNode( - name=NameNode(value=name), arguments=FrozenList() - ) + self._type = graphql_type + self.field = graphql_field + self.ast_field = FieldNode(name=NameNode(value=name), arguments=FrozenList()) log.debug(f"Creating {self!r}") @staticmethod @@ -585,7 +588,25 @@ def __str__(self) -> str: return print_ast(self.ast_field) def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__} {self._type.name}" - f"::{self.ast_field.name.value}>" + name = self._type.name + try: + name += f"::{self.ast_field.name.value}" + except AttributeError: + pass + return f"<{self.__class__.__name__} {name}>" + + +class DSLFragment(DSLField): + def __init__( + self, type_condition: Optional[DSLType] = None, + ): + self.ast_field = InlineFragmentNode() # type: ignore + if type_condition: + self.on(type_condition) + + def on(self, type_condition: DSLType): + self._type = type_condition._type + self.ast_field.type_condition = NamedTypeNode( # type: ignore + name=NameNode(value=self._type.name) ) + return self diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index 8fdaf426..dbc06e07 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -15,6 +15,7 @@ from gql import Client from gql.dsl import ( + DSLFragment, DSLMutation, DSLQuery, DSLSchema, @@ -416,6 +417,24 @@ def test_multiple_operations(ds): ) +def test_inline_fragments(ds): + query = """hero(episode: JEDI) { + name + ... on Droid { + primaryFunction + } + ... on Human { + homePlanet + } +}""" + query_dsl = ds.Query.hero.args(episode=6).select( + ds.Character.name, + DSLFragment().on(ds.Droid).select(ds.Droid.primaryFunction), + DSLFragment().on(ds.Human).select(ds.Human.homePlanet), + ) + assert query == str(query_dsl) + + def test_dsl_query_all_fields_should_be_instances_of_DSLField(): with pytest.raises( TypeError, match="fields must be instances of DSLField. Received type:" From d505962abdb4795e76f994ef93a80b7e3fae8df4 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sat, 28 Aug 2021 18:16:17 +0200 Subject: [PATCH 02/10] DSLField and DSLFragment inherit new DSLSelection class --- gql/dsl.py | 172 +++++++++++++++++++++++-------------- tests/starwars/test_dsl.py | 7 ++ 2 files changed, 113 insertions(+), 66 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index bcbb0be7..f0d64f0f 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -245,7 +245,7 @@ def __init__( self.variable_definitions: DSLVariableDefinitions = DSLVariableDefinitions() # Concatenate fields without and with alias - all_fields: Tuple["DSLField", ...] = DSLField.get_aliased_fields( + all_fields: Tuple["DSLSelection", ...] = DSLField.get_aliased_fields( fields, fields_with_alias ) @@ -265,7 +265,7 @@ def __init__( ) self.selection_set: SelectionSetNode = SelectionSetNode( - selections=FrozenList(DSLField.get_ast_fields(all_fields)) + selections=FrozenList(DSLSelection.get_ast_fields(all_fields)) ) @@ -397,44 +397,23 @@ def __repr__(self) -> str: return f"<{self.__class__.__name__} {self._type!r}>" -class DSLField: - """The DSLField represents a GraphQL field for the DSL code. - - Instances of this class are generated for you automatically as attributes - of the :class:`DSLType` +class DSLSelection(ABC): + """DSLSelection is an abstract class which define the + :meth:`select ` method to select + children fields in the query. - If this field contains children fields, then you need to select which ones - you want in the request using the :meth:`select ` - method. + subclasses: + :class:`DSLField` + :class:`DSLFragment` """ _type: Union[GraphQLObjectType, GraphQLInterfaceType] - ast_field: FieldNode - field: GraphQLField - - def __init__( - self, - name: str, - graphql_type: Union[GraphQLObjectType, GraphQLInterfaceType], - graphql_field: GraphQLField, - ): - """Initialize the DSLField. - - .. warning:: - Don't instantiate this class yourself. - Use attributes of the :class:`DSLType` instead. - - :param name: the name of the field - :param graphql_type: the GraphQL type definition from the schema - :param graphql_field: the GraphQL field definition from the schema - """ - self._type = graphql_type - self.field = graphql_field - self.ast_field = FieldNode(name=NameNode(value=name), arguments=FrozenList()) - log.debug(f"Creating {self!r}") + ast_field: Union[FieldNode, InlineFragmentNode] @staticmethod - def get_ast_fields(fields: Iterable["DSLField"]) -> List[FieldNode]: + def get_ast_fields( + fields: Iterable["DSLSelection"], + ) -> List[Union[FieldNode, InlineFragmentNode]]: """ :meta private: @@ -442,11 +421,11 @@ def get_ast_fields(fields: Iterable["DSLField"]) -> List[FieldNode]: But with a type check for each field in the list. :raises TypeError: if any of the provided fields are not instances - of the :class:`DSLField` class. + of the :class:`DSLSelection` class. """ ast_fields = [] for field in fields: - if isinstance(field, DSLField): + if isinstance(field, DSLSelection): ast_fields.append(field.ast_field) else: raise TypeError(f'Received incompatible field: "{field}".') @@ -455,8 +434,8 @@ def get_ast_fields(fields: Iterable["DSLField"]) -> List[FieldNode]: @staticmethod def get_aliased_fields( - fields: Iterable["DSLField"], fields_with_alias: Dict[str, "DSLField"] - ) -> Tuple["DSLField", ...]: + fields: Iterable["DSLSelection"], fields_with_alias: Dict[str, "DSLField"] + ) -> Tuple["DSLSelection", ...]: """ :meta private: @@ -471,8 +450,8 @@ def get_aliased_fields( ) def select( - self, *fields: "DSLField", **fields_with_alias: "DSLField" - ) -> "DSLField": + self, *fields: "DSLSelection", **fields_with_alias: "DSLField" + ) -> "DSLSelection": r"""Select the new children fields that we want to receive in the request. @@ -480,21 +459,23 @@ def select( to the existing children fields. :param \*fields: new children fields - :type \*fields: DSLField + :type \*fields: DSLSelection (DSLField or DSLFragment) :param \**fields_with_alias: new children fields with alias as key :type \**fields_with_alias: DSLField :return: itself :raises TypeError: if any of the provided fields are not instances - of the :class:`DSLField` class. + of the :class:`DSLSelection` class. """ # Concatenate fields without and with alias - added_fields: Tuple["DSLField", ...] = self.get_aliased_fields( + added_fields: Tuple["DSLSelection", ...] = self.get_aliased_fields( fields, fields_with_alias ) - added_selections: List[FieldNode] = self.get_ast_fields(added_fields) + added_selections: List[ + Union[FieldNode, InlineFragmentNode] + ] = self.get_ast_fields(added_fields) current_selection_set: Optional[SelectionSetNode] = self.ast_field.selection_set @@ -511,6 +492,58 @@ def select( return self + @property + def type_name(self): + """:meta private:""" + return self._type.name + + def __str__(self) -> str: + return print_ast(self.ast_field) + + +class DSLField(DSLSelection): + """The DSLField represents a GraphQL field for the DSL code. + + Instances of this class are generated for you automatically as attributes + of the :class:`DSLType` + + If this field contains children fields, then you need to select which ones + you want in the request using the :meth:`select ` + method. + """ + + ast_field: FieldNode + field: GraphQLField + + def __init__( + self, + name: str, + graphql_type: Union[GraphQLObjectType, GraphQLInterfaceType], + graphql_field: GraphQLField, + ): + """Initialize the DSLField. + + .. warning:: + Don't instantiate this class yourself. + Use attributes of the :class:`DSLType` instead. + + :param name: the name of the field + :param graphql_type: the GraphQL type definition from the schema + :param graphql_field: the GraphQL field definition from the schema + """ + self._type = graphql_type + self.field = graphql_field + self.ast_field = FieldNode(name=NameNode(value=name), arguments=FrozenList()) + log.debug(f"Creating {self!r}") + + def select( + self, *fields: "DSLSelection", **fields_with_alias: "DSLField" + ) -> "DSLField": + """Calling :meth:`select ` method with + corrected typing hints + """ + return cast("DSLField", super().select(*fields, **fields_with_alias)) + def __call__(self, **kwargs) -> "DSLField": return self.args(**kwargs) @@ -519,7 +552,7 @@ def alias(self, alias: str) -> "DSLField": .. note:: You can also pass the alias directly at the - :meth:`select ` method. + :meth:`select ` method. :code:`ds.Query.human.select(my_name=ds.Character.name)` is equivalent to: :code:`ds.Query.human.select(ds.Character.name.alias("my_name"))` @@ -579,34 +612,41 @@ def _get_argument(self, name: str) -> GraphQLArgument: return arg - @property - def type_name(self): - """:meta private:""" - return self._type.name + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self._type.name}" + f"::{self.ast_field.name.value}>" + ) - def __str__(self) -> str: - return print_ast(self.ast_field) - def __repr__(self) -> str: - name = self._type.name - try: - name += f"::{self.ast_field.name.value}" - except AttributeError: - pass - return f"<{self.__class__.__name__} {name}>" +class DSLFragment(DSLSelection): + ast_field: InlineFragmentNode -class DSLFragment(DSLField): - def __init__( - self, type_condition: Optional[DSLType] = None, - ): - self.ast_field = InlineFragmentNode() # type: ignore - if type_condition: - self.on(type_condition) + def __init__(self): + self.ast_field = InlineFragmentNode() + + def select( + self, *fields: "DSLSelection", **fields_with_alias: "DSLField" + ) -> "DSLFragment": + """Calling :meth:`select ` method with + corrected typing hints + """ + return cast("DSLFragment", super().select(*fields, **fields_with_alias)) def on(self, type_condition: DSLType): self._type = type_condition._type - self.ast_field.type_condition = NamedTypeNode( # type: ignore + self.ast_field.type_condition = NamedTypeNode( name=NameNode(value=self._type.name) ) return self + + def __repr__(self) -> str: + type_info = "" + + try: + type_info += f" on {self._type.name}" + except AttributeError: + pass + + return f"<{self.__class__.__name__}{type_info}>" diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index dbc06e07..cee7a20d 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -435,6 +435,13 @@ def test_inline_fragments(ds): assert query == str(query_dsl) +def test_inline_fragments_repr(ds): + + assert repr(DSLFragment()) == "" + + assert repr(DSLFragment().on(ds.Droid)) == "" + + def test_dsl_query_all_fields_should_be_instances_of_DSLField(): with pytest.raises( TypeError, match="fields must be instances of DSLField. Received type:" From fdf08e5aa53f89e59ed3d10cf9d5ce3b3f02ef5a Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sat, 28 Aug 2021 23:04:33 +0200 Subject: [PATCH 03/10] Split DSLSelection into DSLSelectable and DSLSelector --- gql/dsl.py | 122 ++++++++++++++++++++++--------------- tests/starwars/test_dsl.py | 12 ++++ 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index f0d64f0f..e13e3c98 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -221,7 +221,7 @@ class DSLOperation(ABC): operation_type: OperationType def __init__( - self, *fields: "DSLField", **fields_with_alias: "DSLField", + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias", ): r"""Given arguments of type :class:`DSLField` containing GraphQL requests, generate an operation which can be converted to a Document @@ -245,7 +245,7 @@ def __init__( self.variable_definitions: DSLVariableDefinitions = DSLVariableDefinitions() # Concatenate fields without and with alias - all_fields: Tuple["DSLSelection", ...] = DSLField.get_aliased_fields( + all_fields: Tuple["DSLSelectable", ...] = DSLField.get_aliased_fields( fields, fields_with_alias ) @@ -265,7 +265,7 @@ def __init__( ) self.selection_set: SelectionSetNode = SelectionSetNode( - selections=FrozenList(DSLSelection.get_ast_fields(all_fields)) + selections=FrozenList(DSLSelectable.get_ast_fields(all_fields)) ) @@ -397,14 +397,14 @@ def __repr__(self) -> str: return f"<{self.__class__.__name__} {self._type!r}>" -class DSLSelection(ABC): - """DSLSelection is an abstract class which define the - :meth:`select ` method to select - children fields in the query. +class DSLSelectable(ABC): + """DSLSelectable is an abstract class which indicates that + the subclasses can be used as arguments of the + :meth:`select ` method. - subclasses: - :class:`DSLField` - :class:`DSLFragment` + Inherited by + :class:`DSLField `, + :class:`DSLFragment ` """ _type: Union[GraphQLObjectType, GraphQLInterfaceType] @@ -412,7 +412,7 @@ class DSLSelection(ABC): @staticmethod def get_ast_fields( - fields: Iterable["DSLSelection"], + fields: Iterable["DSLSelectable"], ) -> List[Union[FieldNode, InlineFragmentNode]]: """ :meta private: @@ -421,11 +421,11 @@ def get_ast_fields( But with a type check for each field in the list. :raises TypeError: if any of the provided fields are not instances - of the :class:`DSLSelection` class. + of the :class:`DSLSelectable` class. """ ast_fields = [] for field in fields: - if isinstance(field, DSLSelection): + if isinstance(field, DSLSelectable): ast_fields.append(field.ast_field) else: raise TypeError(f'Received incompatible field: "{field}".') @@ -434,8 +434,9 @@ def get_ast_fields( @staticmethod def get_aliased_fields( - fields: Iterable["DSLSelection"], fields_with_alias: Dict[str, "DSLField"] - ) -> Tuple["DSLSelection", ...]: + fields: Iterable["DSLSelectable"], + fields_with_alias: Dict[str, "DSLSelectableWithAlias"], + ) -> Tuple["DSLSelectable", ...]: """ :meta private: @@ -449,9 +450,22 @@ def get_aliased_fields( *(field.alias(alias) for alias, field in fields_with_alias.items()), ) + +class DSLSelector(ABC): + """DSLSelector is an abstract class which defines the + :meth:`select ` method to select + children fields in the query. + + Inherited by + :class:`DSLField `, + :class:`DSLFragment ` + """ + + ast_field: Union[FieldNode, InlineFragmentNode] + def select( - self, *fields: "DSLSelection", **fields_with_alias: "DSLField" - ) -> "DSLSelection": + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" + ) -> "DSLSelector": r"""Select the new children fields that we want to receive in the request. @@ -459,23 +473,23 @@ def select( to the existing children fields. :param \*fields: new children fields - :type \*fields: DSLSelection (DSLField or DSLFragment) + :type \*fields: DSLSelectable (DSLField or DSLFragment) :param \**fields_with_alias: new children fields with alias as key :type \**fields_with_alias: DSLField :return: itself :raises TypeError: if any of the provided fields are not instances - of the :class:`DSLSelection` class. + of the :class:`DSLSelectable` class. """ # Concatenate fields without and with alias - added_fields: Tuple["DSLSelection", ...] = self.get_aliased_fields( + added_fields: Tuple["DSLSelectable", ...] = DSLSelectable.get_aliased_fields( fields, fields_with_alias ) added_selections: List[ Union[FieldNode, InlineFragmentNode] - ] = self.get_ast_fields(added_fields) + ] = DSLSelectable.get_ast_fields(added_fields) current_selection_set: Optional[SelectionSetNode] = self.ast_field.selection_set @@ -501,7 +515,32 @@ def __str__(self) -> str: return print_ast(self.ast_field) -class DSLField(DSLSelection): +class DSLSelectableWithAlias(DSLSelectable): + """DSLSelectableWithAlias is an abstract class which indicates that + the subclasses can be selected with an alias. + """ + + ast_field: FieldNode + + def alias(self, alias: str) -> "DSLSelectableWithAlias": + """Set an alias + + .. note:: + You can also pass the alias directly at the + :meth:`select ` method. + :code:`ds.Query.human.select(my_name=ds.Character.name)` is equivalent to: + :code:`ds.Query.human.select(ds.Character.name.alias("my_name"))` + + :param alias: the alias + :type alias: str + :return: itself + """ + + self.ast_field.alias = NameNode(value=alias) + return self + + +class DSLField(DSLSelectableWithAlias, DSLSelector): """The DSLField represents a GraphQL field for the DSL code. Instances of this class are generated for you automatically as attributes @@ -536,34 +575,9 @@ def __init__( self.ast_field = FieldNode(name=NameNode(value=name), arguments=FrozenList()) log.debug(f"Creating {self!r}") - def select( - self, *fields: "DSLSelection", **fields_with_alias: "DSLField" - ) -> "DSLField": - """Calling :meth:`select ` method with - corrected typing hints - """ - return cast("DSLField", super().select(*fields, **fields_with_alias)) - def __call__(self, **kwargs) -> "DSLField": return self.args(**kwargs) - def alias(self, alias: str) -> "DSLField": - """Set an alias - - .. note:: - You can also pass the alias directly at the - :meth:`select ` method. - :code:`ds.Query.human.select(my_name=ds.Character.name)` is equivalent to: - :code:`ds.Query.human.select(ds.Character.name.alias("my_name"))` - - :param alias: the alias - :type alias: str - :return: itself - """ - - self.ast_field.alias = NameNode(value=alias) - return self - def args(self, **kwargs) -> "DSLField": r"""Set the arguments of a field @@ -612,6 +626,14 @@ def _get_argument(self, name: str) -> GraphQLArgument: return arg + def select( + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" + ) -> "DSLField": + """Calling :meth:`select ` method with + corrected typing hints + """ + return cast("DSLField", super().select(*fields, **fields_with_alias)) + def __repr__(self) -> str: return ( f"<{self.__class__.__name__} {self._type.name}" @@ -619,7 +641,7 @@ def __repr__(self) -> str: ) -class DSLFragment(DSLSelection): +class DSLFragment(DSLSelectable, DSLSelector): ast_field: InlineFragmentNode @@ -627,9 +649,9 @@ def __init__(self): self.ast_field = InlineFragmentNode() def select( - self, *fields: "DSLSelection", **fields_with_alias: "DSLField" + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" ) -> "DSLFragment": - """Calling :meth:`select ` method with + """Calling :meth:`select ` method with corrected typing hints """ return cast("DSLFragment", super().select(*fields, **fields_with_alias)) diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index cee7a20d..a5632200 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -188,6 +188,14 @@ def test_hero_name_and_friends_query(ds): ) assert query == str(query_dsl) + # Should also work with a chain of selects + query_dsl = ( + ds.Query.hero.select(ds.Character.id) + .select(ds.Character.name) + .select(ds.Character.friends.select(ds.Character.name,),) + ) + assert query == str(query_dsl) + def test_hero_id_and_name(ds): query = """ @@ -245,6 +253,10 @@ def test_fetch_luke_aliased(ds): query_dsl = ds.Query.human.args(id=1000).alias("luke").select(ds.Character.name,) assert query == str(query_dsl) + # Should also work with select before alias + query_dsl = ds.Query.human.args(id=1000).select(ds.Character.name,).alias("luke") + assert query == str(query_dsl) + def test_fetch_name_aliased(ds: DSLSchema): query = """ From dbba108a37d197537eafb49107d7107b918d3994 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Mon, 30 Aug 2021 17:56:37 +0200 Subject: [PATCH 04/10] Introduce normal fragments, rename DSLFragment to DSLInlineFragment --- gql/dsl.py | 218 ++++++++++++++++++++++++++----------- tests/starwars/test_dsl.py | 106 +++++++++++++++++- 2 files changed, 254 insertions(+), 70 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index e13e3c98..97da9cb9 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -1,11 +1,13 @@ import logging -from abc import ABC +from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, cast from graphql import ( ArgumentNode, DocumentNode, FieldNode, + FragmentDefinitionNode, + FragmentSpreadNode, GraphQLArgument, GraphQLField, GraphQLInputObjectType, @@ -110,10 +112,10 @@ def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]: def dsl_gql( - *operations: "DSLOperation", **operations_with_name: "DSLOperation" + *operations: "DSLExecutable", **operations_with_name: "DSLExecutable" ) -> DocumentNode: - r"""Given arguments instances of :class:`DSLOperation` - containing GraphQL operations, + r"""Given arguments instances of :class:`DSLExecutable` + containing GraphQL operations or fragments, generate a Document which can be executed later in a gql client or a gql session. @@ -133,11 +135,11 @@ def dsl_gql( :class:`async session ` or by a :class:`sync session ` - :raises TypeError: if an argument is not an instance of :class:`DSLOperation` + :raises TypeError: if an argument is not an instance of :class:`DSLExecutable` """ # Concatenate operations without and with name - all_operations: Tuple["DSLOperation", ...] = ( + all_operations: Tuple["DSLExecutable", ...] = ( *operations, *(operation for operation in operations_with_name.values()), ) @@ -148,7 +150,7 @@ def dsl_gql( # Check the type for operation in all_operations: - if not isinstance(operation, DSLOperation): + if not isinstance(operation, DSLExecutable): raise TypeError( "Operations should be instances of DSLOperation " "(DSLQuery, DSLMutation or DSLSubscription).\n" @@ -156,17 +158,7 @@ def dsl_gql( ) return DocumentNode( - definitions=[ - OperationDefinitionNode( - operation=OperationType(operation.operation_type), - selection_set=operation.selection_set, - variable_definitions=FrozenList( - operation.variable_definitions.get_ast_definitions() - ), - **({"name": NameNode(value=operation.name)} if operation.name else {}), - ) - for operation in all_operations - ] + definitions=[operation.executable_ast for operation in all_operations] ) @@ -202,23 +194,29 @@ def __getattr__(self, name: str) -> "DSLType": if type_def is None: raise AttributeError(f"Type '{name}' not found in the schema!") - assert isinstance(type_def, GraphQLObjectType) or isinstance( - type_def, GraphQLInterfaceType - ) + assert isinstance(type_def, (GraphQLObjectType, GraphQLInterfaceType)) return DSLType(type_def) -class DSLOperation(ABC): - """Interface for GraphQL operations. +class DSLExecutable(ABC): + """Interface for the root elements which can be executed + in the :func:`dsl_gql ` function Inherited by - :class:`DSLQuery `, - :class:`DSLMutation ` and - :class:`DSLSubscription ` + :class:`DSLOperation ` and + :class:`DSLFragment ` """ - operation_type: OperationType + variable_definitions: "DSLVariableDefinitions" + name: Optional[str] + + @property + @abstractmethod + def executable_ast(self): + raise NotImplementedError( + "Any DSLExecutable subclass must have executable_ast property" + ) # pragma: no cover def __init__( self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias", @@ -241,8 +239,8 @@ def __init__( to the operation type """ - self.name: Optional[str] = None - self.variable_definitions: DSLVariableDefinitions = DSLVariableDefinitions() + self.name = None + self.variable_definitions = DSLVariableDefinitions() # Concatenate fields without and with alias all_fields: Tuple["DSLSelectable", ...] = DSLField.get_aliased_fields( @@ -259,16 +257,40 @@ def __init__( f"Received type: {type(field)}" ) ) - assert field.type_name.upper() == self.operation_type.name, ( - f"Invalid root field for operation {self.operation_type.name}.\n" - f"Received: {field.type_name}" - ) + if isinstance(self, DSLOperation): + assert field.type_name.upper() == self.operation_type.name, ( + f"Invalid root field for operation {self.operation_type.name}.\n" + f"Received: {field.type_name}" + ) - self.selection_set: SelectionSetNode = SelectionSetNode( + self.selection_set = SelectionSetNode( selections=FrozenList(DSLSelectable.get_ast_fields(all_fields)) ) +class DSLOperation(DSLExecutable): + """Interface for GraphQL operations. + + Inherited by + :class:`DSLQuery `, + :class:`DSLMutation ` and + :class:`DSLSubscription ` + """ + + operation_type: OperationType + + @property + def executable_ast(self) -> OperationDefinitionNode: + return OperationDefinitionNode( + operation=OperationType(self.operation_type), + selection_set=self.selection_set, + variable_definitions=FrozenList( + self.variable_definitions.get_ast_definitions() + ), + **({"name": NameNode(value=self.name)} if self.name else {}), + ) + + class DSLQuery(DSLOperation): operation_type = OperationType.QUERY @@ -405,15 +427,15 @@ class DSLSelectable(ABC): Inherited by :class:`DSLField `, :class:`DSLFragment ` + :class:`DSLInlineFragment ` """ - _type: Union[GraphQLObjectType, GraphQLInterfaceType] - ast_field: Union[FieldNode, InlineFragmentNode] + ast_field: Union[FieldNode, InlineFragmentNode, FragmentSpreadNode] @staticmethod def get_ast_fields( fields: Iterable["DSLSelectable"], - ) -> List[Union[FieldNode, InlineFragmentNode]]: + ) -> List[Union[FieldNode, InlineFragmentNode, FragmentSpreadNode]]: """ :meta private: @@ -450,6 +472,9 @@ def get_aliased_fields( *(field.alias(alias) for alias, field in fields_with_alias.items()), ) + def __str__(self) -> str: + return print_ast(self.ast_field) + class DSLSelector(ABC): """DSLSelector is an abstract class which defines the @@ -458,10 +483,14 @@ class DSLSelector(ABC): Inherited by :class:`DSLField `, - :class:`DSLFragment ` + :class:`DSLFragment `, + :class:`DSLInlineFragment ` """ - ast_field: Union[FieldNode, InlineFragmentNode] + selection_set: SelectionSetNode + + def __init__(self): + self.selection_set = SelectionSetNode(selections=FrozenList([])) def select( self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" @@ -487,33 +516,20 @@ def select( fields, fields_with_alias ) + # Get a list of AST Nodes for each added field added_selections: List[ - Union[FieldNode, InlineFragmentNode] + Union[FieldNode, InlineFragmentNode, FragmentSpreadNode] ] = DSLSelectable.get_ast_fields(added_fields) - current_selection_set: Optional[SelectionSetNode] = self.ast_field.selection_set - - if current_selection_set is None: - self.ast_field.selection_set = SelectionSetNode( - selections=FrozenList(added_selections) - ) - else: - current_selection_set.selections = FrozenList( - current_selection_set.selections + added_selections - ) + # Update the current selection list with new selections + self.selection_set.selections = FrozenList( + self.selection_set.selections + added_selections + ) - log.debug(f"Added fields: {fields} in {self!r}") + log.debug(f"Added fields: {added_fields} in {self!r}") return self - @property - def type_name(self): - """:meta private:""" - return self._type.name - - def __str__(self) -> str: - return print_ast(self.ast_field) - class DSLSelectableWithAlias(DSLSelectable): """DSLSelectableWithAlias is an abstract class which indicates that @@ -551,6 +567,7 @@ class DSLField(DSLSelectableWithAlias, DSLSelector): method. """ + _type: Union[GraphQLObjectType, GraphQLInterfaceType] ast_field: FieldNode field: GraphQLField @@ -570,6 +587,7 @@ def __init__( :param graphql_type: the GraphQL type definition from the schema :param graphql_field: the GraphQL field definition from the schema """ + DSLSelector.__init__(self) self._type = graphql_type self.field = graphql_field self.ast_field = FieldNode(name=NameNode(value=name), arguments=FrozenList()) @@ -632,7 +650,16 @@ def select( """Calling :meth:`select ` method with corrected typing hints """ - return cast("DSLField", super().select(*fields, **fields_with_alias)) + + super().select(*fields, **fields_with_alias) + self.ast_field.selection_set = self.selection_set + + return self + + @property + def type_name(self): + """:meta private:""" + return self._type.name def __repr__(self) -> str: return ( @@ -641,22 +668,32 @@ def __repr__(self) -> str: ) -class DSLFragment(DSLSelectable, DSLSelector): +class DSLInlineFragment(DSLSelectable, DSLSelector): + _type: Union[GraphQLObjectType, GraphQLInterfaceType] ast_field: InlineFragmentNode - def __init__(self): + def __init__( + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias", + ): + + DSLSelector.__init__(self) self.ast_field = InlineFragmentNode() + self.select(*fields, **fields_with_alias) def select( self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" - ) -> "DSLFragment": + ) -> "DSLInlineFragment": """Calling :meth:`select ` method with corrected typing hints """ - return cast("DSLFragment", super().select(*fields, **fields_with_alias)) + super().select(*fields, **fields_with_alias) + self.ast_field.selection_set = self.selection_set + + return self + + def on(self, type_condition: DSLType) -> "DSLInlineFragment": - def on(self, type_condition: DSLType): self._type = type_condition._type self.ast_field.type_condition = NamedTypeNode( name=NameNode(value=self._type.name) @@ -672,3 +709,54 @@ def __repr__(self) -> str: pass return f"<{self.__class__.__name__}{type_info}>" + + +class DSLFragment(DSLSelectable, DSLSelector, DSLExecutable): + + _type: Union[GraphQLObjectType, GraphQLInterfaceType] + ast_field: FragmentSpreadNode + + def __init__( + self, + name: str, + *fields: "DSLSelectable", + **fields_with_alias: "DSLSelectableWithAlias", + ): + + DSLSelector.__init__(self) + DSLExecutable.__init__(self, *fields, **fields_with_alias) + self.name = name + self.ast_field = FragmentSpreadNode() + self.ast_field.name = NameNode(value=self.name) + + def select( + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" + ) -> "DSLFragment": + """Calling :meth:`select ` method with + corrected typing hints + """ + super().select(*fields, **fields_with_alias) + + return self + + def on(self, type_condition: DSLType) -> "DSLFragment": + + self._type = type_condition._type + + return self + + @property + def executable_ast(self) -> FragmentDefinitionNode: + assert self.name is not None + + return FragmentDefinitionNode( + type_condition=NamedTypeNode(name=NameNode(value=self._type.name)), + selection_set=self.selection_set, + variable_definitions=FrozenList( + self.variable_definitions.get_ast_definitions() + ), + name=NameNode(value=self.name), + ) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name!s}>" diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index a5632200..ee7340cd 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -16,6 +16,7 @@ from gql import Client from gql.dsl import ( DSLFragment, + DSLInlineFragment, DSLMutation, DSLQuery, DSLSchema, @@ -441,17 +442,112 @@ def test_inline_fragments(ds): }""" query_dsl = ds.Query.hero.args(episode=6).select( ds.Character.name, - DSLFragment().on(ds.Droid).select(ds.Droid.primaryFunction), - DSLFragment().on(ds.Human).select(ds.Human.homePlanet), + DSLInlineFragment().on(ds.Droid).select(ds.Droid.primaryFunction), + DSLInlineFragment().on(ds.Human).select(ds.Human.homePlanet), ) assert query == str(query_dsl) -def test_inline_fragments_repr(ds): +def test_fragments_repr(ds): - assert repr(DSLFragment()) == "" + assert repr(DSLInlineFragment()) == "" + assert repr(DSLInlineFragment().on(ds.Droid)) == "" + assert repr(DSLFragment("fragment_1")) == "" + assert repr(DSLFragment("fragment_2").on(ds.Droid)) == "" - assert repr(DSLFragment().on(ds.Droid)) == "" + +def test_fragments(ds): + query = """fragment NameAndAppearances on Character { + name + appearsIn +} + +{ + hero { + ...NameAndAppearances + } +} +""" + + name_and_appearances = ( + DSLFragment("NameAndAppearances") + .on(ds.Character) + .select(ds.Character.name, ds.Character.appearsIn) + ) + + query_dsl = DSLQuery(ds.Query.hero.select(name_and_appearances)) + + document = dsl_gql(name_and_appearances, query_dsl) + + print(print_ast(document)) + + assert query == print_ast(document) + + +def test_dsl_nested_query_with_fragment(ds): + query = """fragment NameAndAppearances on Character { + name + appearsIn +} + +query NestedQueryWithFragment { + hero { + ...NameAndAppearances + friends { + ...NameAndAppearances + friends { + ...NameAndAppearances + } + } + } +} +""" + + name_and_appearances = ( + DSLFragment("NameAndAppearances") + .on(ds.Character) + .select(ds.Character.name, ds.Character.appearsIn) + ) + + query_dsl = DSLQuery( + ds.Query.hero.select( + name_and_appearances, + ds.Character.friends.select( + name_and_appearances, ds.Character.friends.select(name_and_appearances) + ), + ) + ) + + document = dsl_gql(name_and_appearances, NestedQueryWithFragment=query_dsl) + + print(print_ast(document)) + + assert query == print_ast(document) + + # Same thing, but incrementaly + + name_and_appearances = DSLFragment("NameAndAppearances") + name_and_appearances.on(ds.Character) + name_and_appearances.select(ds.Character.name) + name_and_appearances.select(ds.Character.appearsIn) + + level_2 = ds.Character.friends + level_2.select(name_and_appearances) + level_1 = ds.Character.friends + level_1.select(name_and_appearances) + level_1.select(level_2) + + hero = ds.Query.hero + hero.select(name_and_appearances) + hero.select(level_1) + + query_dsl = DSLQuery(hero) + + document = dsl_gql(name_and_appearances, NestedQueryWithFragment=query_dsl) + + print(print_ast(document)) + + assert query == print_ast(document) def test_dsl_query_all_fields_should_be_instances_of_DSLField(): From ce0334d7ff81b397c39d18dd77960f584f5352b2 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sun, 12 Sep 2021 14:17:35 +0200 Subject: [PATCH 05/10] Raise an error if trying to use a fragment without a type condition Adding debug logs when creating fragments --- gql/dsl.py | 22 ++++++++++++++++------ tests/starwars/test_dsl.py | 19 +++++++++++++++++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index 97da9cb9..2275967e 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -125,10 +125,10 @@ def dsl_gql( by instances of :class:`DSLType` which themselves originated from a :class:`DSLSchema` class. - :param \*operations: the GraphQL operations - :type \*operations: DSLOperation (DSLQuery, DSLMutation, DSLSubscription) + :param \*operations: the GraphQL operations and fragments + :type \*operations: DSLQuery, DSLMutation, DSLSubscription, DSLFragment :param \**operations_with_name: the GraphQL operations with an operation name - :type \**operations_with_name: DSLOperation (DSLQuery, DSLMutation, DSLSubscription) + :type \**operations_with_name: DSLQuery, DSLMutation, DSLSubscription :return: a Document which can be later executed or subscribed by a :class:`Client `, by an @@ -152,8 +152,8 @@ def dsl_gql( for operation in all_operations: if not isinstance(operation, DSLExecutable): raise TypeError( - "Operations should be instances of DSLOperation " - "(DSLQuery, DSLMutation or DSLSubscription).\n" + "Operations should be instances of DSLExecutable " + "(DSLQuery, DSLMutation, DSLSubscription or DSLFragment).\n" f"Received: {type(operation)}." ) @@ -680,6 +680,7 @@ def __init__( DSLSelector.__init__(self) self.ast_field = InlineFragmentNode() self.select(*fields, **fields_with_alias) + log.debug(f"Creating {self!r}") def select( self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" @@ -713,8 +714,9 @@ def __repr__(self) -> str: class DSLFragment(DSLSelectable, DSLSelector, DSLExecutable): - _type: Union[GraphQLObjectType, GraphQLInterfaceType] + _type: Optional[Union[GraphQLObjectType, GraphQLInterfaceType]] ast_field: FragmentSpreadNode + name: str def __init__( self, @@ -725,9 +727,12 @@ def __init__( DSLSelector.__init__(self) DSLExecutable.__init__(self, *fields, **fields_with_alias) + self.name = name + self._type = None self.ast_field = FragmentSpreadNode() self.ast_field.name = NameNode(value=self.name) + log.debug(f"Creating {self!r}") def select( self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" @@ -749,6 +754,11 @@ def on(self, type_condition: DSLType) -> "DSLFragment": def executable_ast(self) -> FragmentDefinitionNode: assert self.name is not None + if self._type is None: + raise AttributeError( + "Missing type condition. Please use .on(type_condition) method" + ) + return FragmentDefinitionNode( type_condition=NamedTypeNode(name=NameNode(value=self._type.name)), selection_set=self.selection_set, diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index ee7340cd..060a1da1 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -484,6 +484,21 @@ def test_fragments(ds): assert query == print_ast(document) +def test_fragment_without_type_condition_error(ds): + + # We create a fragment without using the .on(type_condition) method + name_and_appearances = DSLFragment("NameAndAppearances").select( + ds.Character.name, ds.Character.appearsIn + ) + + # If we try to use this fragment, gql generates an error + with pytest.raises( + AttributeError, + match=r"Missing type condition. Please use .on\(type_condition\) method", + ): + dsl_gql(name_and_appearances) + + def test_dsl_nested_query_with_fragment(ds): query = """fragment NameAndAppearances on Character { name @@ -566,9 +581,9 @@ def test_dsl_query_all_fields_should_correspond_to_the_root_type(ds): ) -def test_dsl_gql_all_arguments_should_be_operations(): +def test_dsl_gql_all_arguments_should_be_operations_or_fragments(): with pytest.raises( - TypeError, match="Operations should be instances of DSLOperation " + TypeError, match="Operations should be instances of DSLExecutable " ): dsl_gql("I am a string") From 2228a9a0290cda2c7b5caf42fd5dd80377b28237 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sun, 12 Sep 2021 17:05:26 +0200 Subject: [PATCH 06/10] Add rst documentation --- docs/advanced/dsl_module.rst | 89 ++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/docs/advanced/dsl_module.rst b/docs/advanced/dsl_module.rst index 2ec544b7..d41d496e 100644 --- a/docs/advanced/dsl_module.rst +++ b/docs/advanced/dsl_module.rst @@ -252,6 +252,93 @@ It is possible to create an Document with multiple operations:: operation_name_3=DSLMutation( ... ), ) +Fragments +^^^^^^^^^ + +To define a `Fragment`_, you have to: + +* Instanciate a :class:`DSLFragment ` with a name:: + + name_and_appearances = DSLFragment("NameAndAppearances") + +* Provide the GraphQL type of the fragment with the + :meth:`on ` method:: + + name_and_appearances.on(ds.Character) + +* Add children fields using the :meth:`select ` method:: + + name_and_appearances.select(ds.Character.name, ds.Character.appearsIn) + +Once your fragment is defined, to use it you should: + +* select it as a field somewhere in your query:: + + query_with_fragment = DSLQuery(ds.Query.hero.select(name_and_appearances)) + +* add it as an attribute of :func:`dsl_gql ` with your query:: + + query = dsl_gql(name_and_appearances, query_with_fragment) + +The above example will generate the following request:: + + fragment NameAndAppearances on Character { + name + appearsIn + } + + { + hero { + ...NameAndAppearances + } + } + +Inline Fragments +^^^^^^^^^^^^^^^^ + +To define an `Inline Fragment`_, you have to: + +* Instanciate a :class:`DSLInlineFragment `:: + + human_fragment = DSLInlineFragment() + +* Provide the GraphQL type of the fragment with the + :meth:`on ` method:: + + human_fragment.on(ds.Human) + +* Add children fields using the :meth:`select ` method:: + + human_fragment.select(ds.Human.homePlanet) + +Once your fragment is defined, to use it you should: + +* select it as a field somewhere in your query:: + + query_with_inline_fragment = ds.Query.hero.args(episode=6).select( + ds.Character.name, + human_fragment + ) + +The above example will generate the following request:: + + hero(episode: JEDI) { + name + ... on Human { + homePlanet + } + } + +Note: because the :meth:`on ` and +:meth:`select ` methods return :code:`self`, +this can be written in a concise manner:: + + query_with_inline_fragment = ds.Query.hero.args(episode=6).select( + ds.Character.name, + DSLInlineFragment().on(ds.Human).select(ds.Human.homePlanet) + ) + + Executable examples ------------------- @@ -265,3 +352,5 @@ Sync example .. literalinclude:: ../code_examples/requests_sync_dsl.py +.. _Fragment: https://graphql.org/learn/queries/#fragments +.. _Inline Fragment: https://graphql.org/learn/queries/#inline-fragments From b92d1417faf13ef7b500210a604b109b1729fe3b Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sun, 12 Sep 2021 17:38:33 +0200 Subject: [PATCH 07/10] Complete code documentation --- gql/dsl.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/gql/dsl.py b/gql/dsl.py index 2275967e..359bcb10 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -136,6 +136,7 @@ def dsl_gql( :class:`sync session ` :raises TypeError: if an argument is not an instance of :class:`DSLExecutable` + :raises AttributeError: if a type has not been provided in a :class:`DSLFragment` """ # Concatenate operations without and with name @@ -214,6 +215,7 @@ class DSLExecutable(ABC): @property @abstractmethod def executable_ast(self): + """Generates the ast for :func:`dsl_gql `.""" raise NotImplementedError( "Any DSLExecutable subclass must have executable_ast property" ) # pragma: no cover @@ -281,6 +283,8 @@ class DSLOperation(DSLExecutable): @property def executable_ast(self) -> OperationDefinitionNode: + """Generates the ast for :func:`dsl_gql `.""" + return OperationDefinitionNode( operation=OperationType(self.operation_type), selection_set=self.selection_set, @@ -502,7 +506,7 @@ def select( to the existing children fields. :param \*fields: new children fields - :type \*fields: DSLSelectable (DSLField or DSLFragment) + :type \*fields: DSLSelectable (DSLField, DSLFragment or DSLInlineFragment) :param \**fields_with_alias: new children fields with alias as key :type \**fields_with_alias: DSLField :return: itself @@ -669,6 +673,7 @@ def __repr__(self) -> str: class DSLInlineFragment(DSLSelectable, DSLSelector): + """DSLInlineFragment represents an inline fragment for the DSL code.""" _type: Union[GraphQLObjectType, GraphQLInterfaceType] ast_field: InlineFragmentNode @@ -676,6 +681,13 @@ class DSLInlineFragment(DSLSelectable, DSLSelector): def __init__( self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias", ): + r"""Initialize the DSLInlineFragment. + + :param \*fields: new children fields + :type \*fields: DSLSelectable (DSLField, DSLFragment or DSLInlineFragment) + :param \**fields_with_alias: new children fields with alias as key + :type \**fields_with_alias: DSLField + """ DSLSelector.__init__(self) self.ast_field = InlineFragmentNode() @@ -694,6 +706,7 @@ def select( return self def on(self, type_condition: DSLType) -> "DSLInlineFragment": + """Provides the GraphQL type of this inline fragment.""" self._type = type_condition._type self.ast_field.type_condition = NamedTypeNode( @@ -713,6 +726,7 @@ def __repr__(self) -> str: class DSLFragment(DSLSelectable, DSLSelector, DSLExecutable): + """DSLFragment represents a named GraphQL fragment for the DSL code.""" _type: Optional[Union[GraphQLObjectType, GraphQLInterfaceType]] ast_field: FragmentSpreadNode @@ -724,6 +738,15 @@ def __init__( *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias", ): + r"""Initialize the DSLFragment. + + :param name: the name of the fragment + :type name: str + :param \*fields: new children fields + :type \*fields: DSLSelectable (DSLField, DSLFragment or DSLInlineFragment) + :param \**fields_with_alias: new children fields with alias as key + :type \**fields_with_alias: DSLField + """ DSLSelector.__init__(self) DSLExecutable.__init__(self, *fields, **fields_with_alias) @@ -745,6 +768,11 @@ def select( return self def on(self, type_condition: DSLType) -> "DSLFragment": + """Provides the GraphQL type of this fragment. + + :param type_condition: the provided type + :type type_condition: DSLType + """ self._type = type_condition._type @@ -752,6 +780,10 @@ def on(self, type_condition: DSLType) -> "DSLFragment": @property def executable_ast(self) -> FragmentDefinitionNode: + """Generates the ast for :func:`dsl_gql `. + + :raises AttributeError: if a type has not been provided + """ assert self.name is not None if self._type is None: From 13c243e31cc7a9ec629500d7ed082bb0185ff2b0 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sun, 12 Sep 2021 17:59:18 +0200 Subject: [PATCH 08/10] fix doc typos --- docs/advanced/dsl_module.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/dsl_module.rst b/docs/advanced/dsl_module.rst index d41d496e..3c11ac82 100644 --- a/docs/advanced/dsl_module.rst +++ b/docs/advanced/dsl_module.rst @@ -276,7 +276,7 @@ Once your fragment is defined, to use it you should: query_with_fragment = DSLQuery(ds.Query.hero.select(name_and_appearances)) -* add it as an attribute of :func:`dsl_gql ` with your query:: +* add it as an argument of :func:`dsl_gql ` with your query:: query = dsl_gql(name_and_appearances, query_with_fragment) @@ -311,7 +311,7 @@ To define an `Inline Fragment`_, you have to: human_fragment.select(ds.Human.homePlanet) -Once your fragment is defined, to use it you should: +Once your inline fragment is defined, to use it you should: * select it as a field somewhere in your query:: From 27dd5b2b995bde04c49eea269e72e6b2a40472d3 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sun, 12 Sep 2021 18:01:44 +0200 Subject: [PATCH 09/10] instanciate -> instantiate --- docs/advanced/dsl_module.rst | 6 +++--- docs/code_examples/aiohttp_async_dsl.py | 2 +- docs/code_examples/requests_sync_dsl.py | 2 +- gql/cli.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/advanced/dsl_module.rst b/docs/advanced/dsl_module.rst index 3c11ac82..afaa3bc6 100644 --- a/docs/advanced/dsl_module.rst +++ b/docs/advanced/dsl_module.rst @@ -164,7 +164,7 @@ Variable arguments To provide variables instead of argument values directly for an operation, you have to: -* Instanciate a :class:`DSLVariableDefinitions `:: +* Instantiate a :class:`DSLVariableDefinitions `:: var = DSLVariableDefinitions() @@ -257,7 +257,7 @@ Fragments To define a `Fragment`_, you have to: -* Instanciate a :class:`DSLFragment ` with a name:: +* Instantiate a :class:`DSLFragment ` with a name:: name_and_appearances = DSLFragment("NameAndAppearances") @@ -298,7 +298,7 @@ Inline Fragments To define an `Inline Fragment`_, you have to: -* Instanciate a :class:`DSLInlineFragment `:: +* Instantiate a :class:`DSLInlineFragment `:: human_fragment = DSLInlineFragment() diff --git a/docs/code_examples/aiohttp_async_dsl.py b/docs/code_examples/aiohttp_async_dsl.py index d558ef6d..958ea490 100644 --- a/docs/code_examples/aiohttp_async_dsl.py +++ b/docs/code_examples/aiohttp_async_dsl.py @@ -17,7 +17,7 @@ async def main(): # GQL will fetch the schema just after the establishment of the first session async with client as session: - # Instanciate the root of the DSL Schema as ds + # Instantiate the root of the DSL Schema as ds ds = DSLSchema(client.schema) # Create the query using dynamically generated attributes from ds diff --git a/docs/code_examples/requests_sync_dsl.py b/docs/code_examples/requests_sync_dsl.py index 23c40e18..925b9aa2 100644 --- a/docs/code_examples/requests_sync_dsl.py +++ b/docs/code_examples/requests_sync_dsl.py @@ -17,7 +17,7 @@ # We should have received the schema now that the session is established assert client.schema is not None - # Instanciate the root of the DSL Schema as ds + # Instantiate the root of the DSL Schema as ds ds = DSLSchema(client.schema) # Create the query using dynamically generated attributes from ds diff --git a/gql/cli.py b/gql/cli.py index f971859e..c75ad120 100644 --- a/gql/cli.py +++ b/gql/cli.py @@ -183,7 +183,7 @@ def get_execute_args(args: Namespace) -> Dict[str, Any]: def get_transport(args: Namespace) -> AsyncTransport: - """Instanciate a transport from the parsed command line arguments + """Instantiate a transport from the parsed command line arguments :param args: parsed command line arguments """ @@ -196,7 +196,7 @@ def get_transport(args: Namespace) -> AsyncTransport: # (headers) transport_args = get_transport_args(args) - # Instanciate transport depending on url scheme + # Instantiate transport depending on url scheme transport: AsyncTransport if scheme in ["ws", "wss"]: from gql.transport.websockets import WebsocketsTransport @@ -226,7 +226,7 @@ async def main(args: Namespace) -> int: logging.basicConfig(level=args.loglevel) try: - # Instanciate transport from command line arguments + # Instantiate transport from command line arguments transport = get_transport(args) # Get extra execute parameters from command line arguments From 7354976a0a15491947278f03ff88bf1744632d48 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sun, 12 Sep 2021 19:21:09 +0200 Subject: [PATCH 10/10] Allow to change a fragment name after initialization --- gql/dsl.py | 17 +++++++++++++++-- tests/starwars/test_dsl.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index 359bcb10..f3bd1fe2 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -753,10 +753,23 @@ def __init__( self.name = name self._type = None - self.ast_field = FragmentSpreadNode() - self.ast_field.name = NameNode(value=self.name) + log.debug(f"Creating {self!r}") + @property # type: ignore + def ast_field(self) -> FragmentSpreadNode: # type: ignore + """ast_field property will generate a FragmentSpreadNode with the + provided name. + + Note: We need to ignore the type because of + `issue #4125 of mypy `_. + """ + + spread_node = FragmentSpreadNode() + spread_node.name = NameNode(value=self.name) + + return spread_node + def select( self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" ) -> "DSLFragment": diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index 060a1da1..93de6c03 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -499,6 +499,17 @@ def test_fragment_without_type_condition_error(ds): dsl_gql(name_and_appearances) +def test_fragment_with_name_changed(ds): + + fragment = DSLFragment("ABC") + + assert str(fragment) == "...ABC" + + fragment.name = "DEF" + + assert str(fragment) == "...DEF" + + def test_dsl_nested_query_with_fragment(ds): query = """fragment NameAndAppearances on Character { name