diff --git a/docs/advanced/dsl_module.rst b/docs/advanced/dsl_module.rst index 2ec544b7..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() @@ -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: + +* Instantiate 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 argument 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: + +* Instantiate 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 inline 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 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 diff --git a/gql/dsl.py b/gql/dsl.py index 6542d6a6..f3bd1fe2 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, @@ -17,6 +19,7 @@ GraphQLObjectType, GraphQLSchema, GraphQLWrappingType, + InlineFragmentNode, ListTypeNode, ListValueNode, NamedTypeNode, @@ -109,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. @@ -122,21 +125,22 @@ 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 :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` + :raises AttributeError: if a type has not been provided in a :class:`DSLFragment` """ # Concatenate operations without and with name - all_operations: Tuple["DSLOperation", ...] = ( + all_operations: Tuple["DSLExecutable", ...] = ( *operations, *(operation for operation in operations_with_name.values()), ) @@ -147,25 +151,15 @@ 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" + "Operations should be instances of DSLExecutable " + "(DSLQuery, DSLMutation, DSLSubscription or DSLFragment).\n" f"Received: {type(operation)}." ) 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] ) @@ -201,26 +195,33 @@ 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): + """Generates the ast for :func:`dsl_gql `.""" + raise NotImplementedError( + "Any DSLExecutable subclass must have executable_ast property" + ) # pragma: no cover 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 @@ -240,11 +241,11 @@ 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["DSLField", ...] = DSLField.get_aliased_fields( + all_fields: Tuple["DSLSelectable", ...] = DSLField.get_aliased_fields( fields, fields_with_alias ) @@ -258,13 +259,39 @@ 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( + 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 - self.selection_set: SelectionSetNode = SelectionSetNode( - selections=FrozenList(DSLField.get_ast_fields(all_fields)) + @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, + variable_definitions=FrozenList( + self.variable_definitions.get_ast_definitions() + ), + **({"name": NameNode(value=self.name)} if self.name else {}), ) @@ -396,42 +423,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 DSLSelectable(ABC): + """DSLSelectable is an abstract class which indicates that + the subclasses can be used as arguments of the + :meth:`select ` method. - If this field contains children fields, then you need to select which ones - you want in the request using the :meth:`select ` - method. + Inherited by + :class:`DSLField `, + :class:`DSLFragment ` + :class:`DSLInlineFragment ` """ - 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: Union[GraphQLObjectType, GraphQLInterfaceType] = graphql_type - self.field: GraphQLField = graphql_field - self.ast_field: FieldNode = FieldNode( - name=NameNode(value=name), arguments=FrozenList() - ) - log.debug(f"Creating {self!r}") + ast_field: Union[FieldNode, InlineFragmentNode, FragmentSpreadNode] @staticmethod - def get_ast_fields(fields: Iterable["DSLField"]) -> List[FieldNode]: + def get_ast_fields( + fields: Iterable["DSLSelectable"], + ) -> List[Union[FieldNode, InlineFragmentNode, FragmentSpreadNode]]: """ :meta private: @@ -439,11 +447,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:`DSLSelectable` class. """ ast_fields = [] for field in fields: - if isinstance(field, DSLField): + if isinstance(field, DSLSelectable): ast_fields.append(field.ast_field) else: raise TypeError(f'Received incompatible field: "{field}".') @@ -452,8 +460,9 @@ 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["DSLSelectable"], + fields_with_alias: Dict[str, "DSLSelectableWithAlias"], + ) -> Tuple["DSLSelectable", ...]: """ :meta private: @@ -467,9 +476,29 @@ 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 + :meth:`select ` method to select + children fields in the query. + + Inherited by + :class:`DSLField `, + :class:`DSLFragment `, + :class:`DSLInlineFragment ` + """ + + selection_set: SelectionSetNode + + def __init__(self): + self.selection_set = SelectionSetNode(selections=FrozenList([])) + def select( - self, *fields: "DSLField", **fields_with_alias: "DSLField" - ) -> "DSLField": + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" + ) -> "DSLSelector": r"""Select the new children fields that we want to receive in the request. @@ -477,46 +506,48 @@ def select( to the existing children fields. :param \*fields: new children fields - :type \*fields: DSLField + :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 :raises TypeError: if any of the provided fields are not instances - of the :class:`DSLField` class. + of the :class:`DSLSelectable` class. """ # Concatenate fields without and with alias - added_fields: Tuple["DSLField", ...] = self.get_aliased_fields( + added_fields: Tuple["DSLSelectable", ...] = DSLSelectable.get_aliased_fields( fields, fields_with_alias ) - added_selections: List[FieldNode] = self.get_ast_fields(added_fields) + # Get a list of AST Nodes for each added field + added_selections: List[ + 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 - def __call__(self, **kwargs) -> "DSLField": - return self.args(**kwargs) - def alias(self, alias: str) -> "DSLField": +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. + :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"))` @@ -528,6 +559,47 @@ def alias(self, alias: str) -> "DSLField": 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 + 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. + """ + + _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 + """ + DSLSelector.__init__(self) + 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 __call__(self, **kwargs) -> "DSLField": + return self.args(**kwargs) + def args(self, **kwargs) -> "DSLField": r"""Set the arguments of a field @@ -576,16 +648,170 @@ 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 + """ + + 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 __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}>" ) + + +class DSLInlineFragment(DSLSelectable, DSLSelector): + """DSLInlineFragment represents an inline fragment for the DSL code.""" + + _type: Union[GraphQLObjectType, GraphQLInterfaceType] + ast_field: InlineFragmentNode + + 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() + self.select(*fields, **fields_with_alias) + log.debug(f"Creating {self!r}") + + def select( + self, *fields: "DSLSelectable", **fields_with_alias: "DSLSelectableWithAlias" + ) -> "DSLInlineFragment": + """Calling :meth:`select ` method with + corrected typing hints + """ + super().select(*fields, **fields_with_alias) + self.ast_field.selection_set = self.selection_set + + 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( + 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}>" + + +class DSLFragment(DSLSelectable, DSLSelector, DSLExecutable): + """DSLFragment represents a named GraphQL fragment for the DSL code.""" + + _type: Optional[Union[GraphQLObjectType, GraphQLInterfaceType]] + ast_field: FragmentSpreadNode + name: str + + def __init__( + self, + name: str, + *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) + + self.name = name + self._type = None + + 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": + """Calling :meth:`select ` method with + corrected typing hints + """ + super().select(*fields, **fields_with_alias) + + 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 + + return self + + @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: + 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, + 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 8fdaf426..93de6c03 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -15,6 +15,8 @@ from gql import Client from gql.dsl import ( + DSLFragment, + DSLInlineFragment, DSLMutation, DSLQuery, DSLSchema, @@ -187,6 +189,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 = """ @@ -244,6 +254,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 = """ @@ -416,6 +430,152 @@ 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, + DSLInlineFragment().on(ds.Droid).select(ds.Droid.primaryFunction), + DSLInlineFragment().on(ds.Human).select(ds.Human.homePlanet), + ) + assert query == str(query_dsl) + + +def test_fragments_repr(ds): + + assert repr(DSLInlineFragment()) == "" + assert repr(DSLInlineFragment().on(ds.Droid)) == "" + assert repr(DSLFragment("fragment_1")) == "" + assert repr(DSLFragment("fragment_2").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_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_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 + 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(): with pytest.raises( TypeError, match="fields must be instances of DSLField. Received type:" @@ -432,9 +592,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")