From 03a389bad33b0cb657e18aee75933933ac538143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Thu, 20 Jul 2023 14:10:25 +0200 Subject: [PATCH 01/63] Initial next API experiments --- ariadne_graphql_modules/next/__init__.py | 12 ++ ariadne_graphql_modules/next/base.py | 52 ++++++++ ariadne_graphql_modules/next/convert_name.py | 14 ++ .../next/executable_schema.py | 121 ++++++++++++++++++ ariadne_graphql_modules/next/objecttype.py | 75 +++++++++++ ariadne_graphql_modules/next/typing.py | 0 tests_next/__init__.py | 0 tests_next/conftest.py | 15 +++ tests_next/test_object_type.py | 28 ++++ tests_next/test_object_type_with_schema.py | 2 + 10 files changed, 319 insertions(+) create mode 100644 ariadne_graphql_modules/next/__init__.py create mode 100644 ariadne_graphql_modules/next/base.py create mode 100644 ariadne_graphql_modules/next/convert_name.py create mode 100644 ariadne_graphql_modules/next/executable_schema.py create mode 100644 ariadne_graphql_modules/next/objecttype.py create mode 100644 ariadne_graphql_modules/next/typing.py create mode 100644 tests_next/__init__.py create mode 100644 tests_next/conftest.py create mode 100644 tests_next/test_object_type.py create mode 100644 tests_next/test_object_type_with_schema.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py new file mode 100644 index 0000000..3fec79f --- /dev/null +++ b/ariadne_graphql_modules/next/__init__.py @@ -0,0 +1,12 @@ +from .base import GraphQLModel, GraphQLType +from .executable_schema import make_executable_schema +from .objecttype import GraphQLObject, GraphQLObjectModel, object_field + +__all__ = [ + "GraphQLModel", + "GraphQLObject", + "GraphQLObjectModel", + "GraphQLType", + "make_executable_schema", + "object_field", +] \ No newline at end of file diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py new file mode 100644 index 0000000..3682b31 --- /dev/null +++ b/ariadne_graphql_modules/next/base.py @@ -0,0 +1,52 @@ +from typing import Iterable, Optional + +from graphql import GraphQLSchema, TypeDefinitionNode + + +class GraphQLType: + __graphql_name__: Optional[str] + __abstract__: bool = True + + @classmethod + def __get_graphql_name__(cls) -> "str": + if getattr(cls, "__graphql_name__", None): + return cls.__graphql_name__ + + name = cls.__name__ + if name.endswith("GraphQLType"): + # 'UserGraphQLType' will produce the 'User' name + return name[:-11] or name + if name.endswith("Type"): + # 'UserType' will produce the 'User' name + return name[:-4] or name + if name.endswith("GraphQL"): + # 'UserGraphQL' will produce the 'User' name + return name[:-7] or name + return name + + @classmethod + def __get_graphql_model__(cls) -> "GraphQLModel": + raise NotImplementedError( + "Subclasses of 'GraphQLType' must define '__get_graphql_model__'" + ) + + @classmethod + def __get_graphql_types__(cls) -> Iterable["GraphQLType"]: + """Returns iterable with GraphQL types associated with this type""" + return [cls] + + +class GraphQLModel: + name: str + ast_type: TypeDefinitionNode + + def __init__(self, name: str): + self.name = name + + def get_ast(self) -> TypeDefinitionNode: + raise NotImplementedError( + "Subclasses of 'GraphQLModel' need to define a 'get_ast' method." + ) + + def bind_to_schema(self, schema: GraphQLSchema): + pass diff --git a/ariadne_graphql_modules/next/convert_name.py b/ariadne_graphql_modules/next/convert_name.py new file mode 100644 index 0000000..41072e4 --- /dev/null +++ b/ariadne_graphql_modules/next/convert_name.py @@ -0,0 +1,14 @@ +def convert_python_name_to_graphql(python_name: str) -> str: + final_name = "" + for i, c in enumerate(python_name): + if not i: + final_name += c.lower() + else: + if c == "_": + continue + if python_name[i - 1] == "_": + final_name += c.upper() + else: + final_name += c.lower() + + return final_name \ No newline at end of file diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py new file mode 100644 index 0000000..ef39115 --- /dev/null +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -0,0 +1,121 @@ +from enum import Enum +from typing import Any, Dict, List, Optional, Sequence, Type, Union + +from ariadne import SchemaBindable, SchemaDirectiveVisitor, SchemaNameConverter +from graphql import GraphQLSchema + +from .base import GraphQLModel, GraphQLType + +SchemaType = Union[ + str, + Enum, + SchemaBindable, + Type[GraphQLType], +] + + +def make_executable_schema( + *types: Union[SchemaType, List[SchemaType]], + directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, + convert_names_case: Union[bool, SchemaNameConverter] = False, +) -> GraphQLSchema: + type_defs: List[str] = find_type_defs(types) + types_list: List[SchemaType] = flatten_types(types) + + assert_types_unique(types_list) + assert_types_not_abstract(types_list) + + # TODO: + # Convert unique types list to models/bindables list + # Deal with deferred types + + +def find_type_defs(types: Sequence[SchemaType]) -> List[str]: + type_defs: List[str] = [] + + for type_def in types: + if isinstance(type_def, str): + type_defs.append(type_def) + elif isinstance(type_def, list): + type_defs += find_type_defs(type_def) + + return type_defs + + +def flatten_types(types: Sequence[Union[SchemaType, List[SchemaType]]]) -> List[Union[Enum, SchemaBindable, GraphQLModel]]: + flat_schema_types_list: List[SchemaType] = flatten_schema_types(types) + + types_list: List[Union[Enum, SchemaBindable, GraphQLModel]] = [] + for type_def in flat_schema_types_list: + if issubclass(type_def, GraphQLType): + type_name = type_def.__name__ + + if getattr(type_def, "__abstract__", None): + raise ValueError( + f"Type '{type_def.__name__}' is an abstract type and can't be used " + "for schema creation." + ) + + types_list.append(type_def) + + if isinstance(type_def, list): + types_list += find_type_defs(type_def) + + return types_list + + +def flatten_schema_types( + types: Sequence[Union[SchemaType, List[SchemaType]]], + dedupe: bool = True, +) -> List[SchemaType]: + flat_list: List[SchemaType] = [] + + for type_def in types: + if isinstance(type_def, list): + flat_list += flatten_schema_types(type_def, dedupe=False) + elif issubclass(type_def, GraphQLType): + flat_list += type_def.__get_graphql_types__() + elif get_graphql_type_name(type_def): + flat_list.append(type_def) + + if not dedupe: + return flat_list + + unique_list: List[SchemaType] = [] + for type_def in flat_list: + if type_def not in unique_list: + unique_list.append(type_def) + + return unique_list + + +def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: + if issubclass(type_def, Enum): + return type_def.__name__ + + if issubclass(type_def, GraphQLModel): + return type_def.__get_graphql_name__() + + return None + + +def assert_types_unique(type_defs: List[SchemaType]): + types_names: Dict[str, Any] = {} + for type_def in type_defs: + type_name = get_graphql_type_name(type_def) + if type_name in types_names: + raise ValueError( + f"Types '{type_def.__name__}' and '{types_names[type_name]}' both define " + f"GraphQL type with name '{type_name}'." + ) + + types_names[type_name] = type_def + + +def assert_types_not_abstract(type_defs: List[SchemaType]): + for type_def in type_defs: + if issubclass(type_def, GraphQLType) and getattr(type_def, "__abstract__", None): + raise ValueError( + f"Type '{type_def.__name__}' is an abstract type and can't be used " + "for schema creation." + ) \ No newline at end of file diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py new file mode 100644 index 0000000..f41ebf8 --- /dev/null +++ b/ariadne_graphql_modules/next/objecttype.py @@ -0,0 +1,75 @@ +from typing import Iterable, Optional, Tuple, Type + +from ariadne.types import Resolver +from graphql import ObjectTypeDefinitionNode + +from .base import GraphQLModel, GraphQLType +from .convert_name import convert_python_name_to_graphql + + +class GraphQLObject(GraphQLType): + __schema__: Optional[str] + __requires__: Optional[Iterable[GraphQLType]] + + __abstract__: bool = True + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + validate_object_type_with_schema(cls) + else: + validate_object_type(cls) + + @classmethod + def __get_graphql_model__(cls) -> "GraphQLModel": + return GraphQLObjectModel( + name=cls.__get_graphql_name__(), + fields=tuple( + field for field in dir(cls).values() + if isinstance(field, GraphQLObjectField) + ) + ) + + +def validate_object_type(graphql_type: Type[GraphQLObject]): + pass + + +def validate_object_type_with_schema(graphql_type: Type[GraphQLObject]): + pass + + +class GraphQLObjectField: + def __init__(self, *, resolver: Resolver, name: str): + self.resolver = resolver + self.name = str + + +def object_field(f: Optional[Resolver] = None, *, name: Optional[str] = None): + def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: + return GraphQLObjectField( + resolver=f, + name=name or convert_python_name_to_graphql(f.__name__), + ) + + if f is not None: + if name is not None: + raise ValueError( + "'object_field' decorator was called with both function argument " + "and the options." + ) + + return object_field_factory(f) + + return object_field_factory + + +class GraphQLObjectModel(GraphQLModel): + ast_type = ObjectTypeDefinitionNode + fields: Tuple[GraphQLObjectField] diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py new file mode 100644 index 0000000..e69de29 diff --git a/tests_next/__init__.py b/tests_next/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests_next/conftest.py b/tests_next/conftest.py new file mode 100644 index 0000000..0c054a8 --- /dev/null +++ b/tests_next/conftest.py @@ -0,0 +1,15 @@ +from textwrap import dedent + +import pytest +from graphql import GraphQLSchema, print_schema + + +@pytest.fixture +def assert_schema_equals(): + def schema_equals_assertion(schema: GraphQLSchema, target: str): + schema_str = print_schema(schema) + print(schema_str) + + assert schema_str == dedent(target).strip() + + return schema_equals_assertion diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py new file mode 100644 index 0000000..6e3a532 --- /dev/null +++ b/tests_next/test_object_type.py @@ -0,0 +1,28 @@ +from graphql import graphql_sync + +from ariadne import make_executable_schema + +from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema, object_field + + +def test_minimal_object_type(assert_schema_equals): + class QueryType(GraphQLObject): + @object_field + def hello(obj) -> str: + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} diff --git a/tests_next/test_object_type_with_schema.py b/tests_next/test_object_type_with_schema.py new file mode 100644 index 0000000..c6e242b --- /dev/null +++ b/tests_next/test_object_type_with_schema.py @@ -0,0 +1,2 @@ +def test_something(): + assert 1 + 2 == 3 From 89a0b9640fe35323855dbedbb2ec6c5e830d6ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Wed, 2 Aug 2023 16:07:04 +0200 Subject: [PATCH 02/63] Minimal working object type --- ariadne_graphql_modules/next/base.py | 13 +-- .../next/executable_schema.py | 25 ++++- ariadne_graphql_modules/next/objecttype.py | 101 +++++++++++++++--- tests_next/test_object_type.py | 8 +- 4 files changed, 120 insertions(+), 27 deletions(-) diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 3682b31..925545a 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -1,4 +1,4 @@ -from typing import Iterable, Optional +from typing import Iterable, Optional, Type from graphql import GraphQLSchema, TypeDefinitionNode @@ -38,15 +38,12 @@ def __get_graphql_types__(cls) -> Iterable["GraphQLType"]: class GraphQLModel: name: str - ast_type: TypeDefinitionNode + ast: TypeDefinitionNode + ast_type: Type[TypeDefinitionNode] - def __init__(self, name: str): + def __init__(self, name: str, ast: TypeDefinitionNode): self.name = name - - def get_ast(self) -> TypeDefinitionNode: - raise NotImplementedError( - "Subclasses of 'GraphQLModel' need to define a 'get_ast' method." - ) + self.ast = ast def bind_to_schema(self, schema: GraphQLSchema): pass diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index ef39115..fbacdca 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -2,7 +2,12 @@ from typing import Any, Dict, List, Optional, Sequence, Type, Union from ariadne import SchemaBindable, SchemaDirectiveVisitor, SchemaNameConverter -from graphql import GraphQLSchema +from graphql import ( + DocumentNode, + GraphQLSchema, + assert_valid_schema, + build_ast_schema, +) from .base import GraphQLModel, GraphQLType @@ -29,6 +34,22 @@ def make_executable_schema( # Convert unique types list to models/bindables list # Deal with deferred types + schema_models: List[GraphQLModel] = [ + type_def.__get_graphql_model__() for type_def in types_list + ] + + document_node = DocumentNode( + definitions=tuple(schema_model.ast for schema_model in schema_models), + ) + + schema = build_ast_schema(document_node) + assert_valid_schema(schema) + + for schema_model in schema_models: + schema_model.bind_to_schema(schema) + + return schema + def find_type_defs(types: Sequence[SchemaType]) -> List[str]: type_defs: List[str] = [] @@ -52,7 +73,7 @@ def flatten_types(types: Sequence[Union[SchemaType, List[SchemaType]]]) -> List[ if getattr(type_def, "__abstract__", None): raise ValueError( - f"Type '{type_def.__name__}' is an abstract type and can't be used " + f"Type '{type_name}' is an abstract type and can't be used " "for schema creation." ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index f41ebf8..483eed8 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,7 +1,15 @@ -from typing import Iterable, Optional, Tuple, Type +from typing import Dict, Iterable, List, Optional, Type +from ariadne import ObjectType as ObjectTypeBindable from ariadne.types import Resolver -from graphql import ObjectTypeDefinitionNode +from graphql import ( + FieldDefinitionNode, + GraphQLSchema, + NameNode, + NamedTypeNode, + NonNullTypeNode, + ObjectTypeDefinitionNode, +) from .base import GraphQLModel, GraphQLType from .convert_name import convert_python_name_to_graphql @@ -28,13 +36,48 @@ def __init_subclass__(cls) -> None: @classmethod def __get_graphql_model__(cls) -> "GraphQLModel": - return GraphQLObjectModel( - name=cls.__get_graphql_name__(), - fields=tuple( - field for field in dir(cls).values() - if isinstance(field, GraphQLObjectField) + name = cls.__get_graphql_name__() + fields_ast: List[FieldDefinitionNode] = [] + resolvers: Dict[str, Resolver] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for field in cls.__dict__.values(): + if not isinstance(field, GraphQLObjectField): + continue + + if field.resolver: + resolvers[field.name] = field.resolver + + fields_ast.append( + FieldDefinitionNode( + name=NameNode(value=field.name), + type=NonNullTypeNode( + type=NamedTypeNode( + name=NameNode(value="String"), + ), + ), + ) ) + + return GraphQLObjectModel( + name=name, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=name), + fields=tuple(fields_ast), + ), + resolvers=resolvers, + out_names=out_names, ) + + @staticmethod + def field( + f: Optional[Resolver] = None, + *, + name: Optional[str] = None, + type: Optional[Type[GraphQLType]] = None, + ): + """Shortcut for using object_field() separately""" + return object_field(f, name=name) def validate_object_type(graphql_type: Type[GraphQLObject]): @@ -46,12 +89,22 @@ def validate_object_type_with_schema(graphql_type: Type[GraphQLObject]): class GraphQLObjectField: - def __init__(self, *, resolver: Resolver, name: str): + def __init__( + self, + *, + name: str, + resolver: Optional[Resolver] = None, + ): + self.name = name self.resolver = resolver - self.name = str -def object_field(f: Optional[Resolver] = None, *, name: Optional[str] = None): +def object_field( + f: Optional[Resolver] = None, + *, + name: Optional[str] = None, + type: Optional[Type[GraphQLType]] = None, +): def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: return GraphQLObjectField( resolver=f, @@ -59,9 +112,9 @@ def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: ) if f is not None: - if name is not None: + if any((name, type)): raise ValueError( - "'object_field' decorator was called with both function argument " + "'object_field' decorator was called with function argument " "and the options." ) @@ -72,4 +125,26 @@ def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: class GraphQLObjectModel(GraphQLModel): ast_type = ObjectTypeDefinitionNode - fields: Tuple[GraphQLObjectField] + resolvers: Dict[str, Resolver] + out_names: Dict[str, Dict[str, str]] + + def __init__( + self, + name: str, + ast: ObjectTypeDefinitionNode, + resolvers: Dict[str, Resolver], + out_names: Dict[str, Dict[str, str]], + ): + self.name = name + self.ast = ast + self.resolvers = resolvers + self.out_names = out_names + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = ObjectTypeBindable(self.name) + + for field, resolver in self.resolvers.items(): + bindable.set_field(field, resolver) + + bindable.bind_to_schema(schema) + diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index 6e3a532..c52bba7 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -2,13 +2,13 @@ from ariadne import make_executable_schema -from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema, object_field +from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema def test_minimal_object_type(assert_schema_equals): class QueryType(GraphQLObject): - @object_field - def hello(obj) -> str: + @GraphQLObject.field + def hello(obj, info) -> str: return "Hello World!" schema = make_executable_schema(QueryType) @@ -22,7 +22,7 @@ def hello(obj) -> str: """, ) - result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + result = graphql_sync(schema, "{ hello }") assert not result.errors assert result.data == {"hello": "Hello World!"} From 67702eba56c29c300579e0e291a54eeeb776bc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Wed, 2 Aug 2023 19:00:27 +0200 Subject: [PATCH 03/63] WIP convert type hint to GraphQL type --- ariadne_graphql_modules/next/objecttype.py | 55 ++++++++++++----- ariadne_graphql_modules/next/typing.py | 69 ++++++++++++++++++++++ tests_next/test_typing.py | 66 +++++++++++++++++++++ 3 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 tests_next/test_typing.py diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 483eed8..ec9e5da 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,4 +1,4 @@ -from typing import Dict, Iterable, List, Optional, Type +from typing import Any, Dict, Iterable, List, Optional, Type, get_origin, get_type_hints from ariadne import ObjectType as ObjectTypeBindable from ariadne.types import Resolver @@ -9,10 +9,12 @@ NamedTypeNode, NonNullTypeNode, ObjectTypeDefinitionNode, + TypeNode, ) from .base import GraphQLModel, GraphQLType from .convert_name import convert_python_name_to_graphql +from .typing import get_type_node class GraphQLObject(GraphQLType): @@ -48,16 +50,7 @@ def __get_graphql_model__(cls) -> "GraphQLModel": if field.resolver: resolvers[field.name] = field.resolver - fields_ast.append( - FieldDefinitionNode( - name=NameNode(value=field.name), - type=NonNullTypeNode( - type=NamedTypeNode( - name=NameNode(value="String"), - ), - ), - ) - ) + fields_ast.append(get_field_node(field)) return GraphQLObjectModel( name=name, @@ -74,7 +67,7 @@ def field( f: Optional[Resolver] = None, *, name: Optional[str] = None, - type: Optional[Type[GraphQLType]] = None, + type: Optional[Any] = None, ): """Shortcut for using object_field() separately""" return object_field(f, name=name) @@ -93,9 +86,11 @@ def __init__( self, *, name: str, + type: Any, resolver: Optional[Resolver] = None, ): self.name = name + self.type = type self.resolver = resolver @@ -103,12 +98,28 @@ def object_field( f: Optional[Resolver] = None, *, name: Optional[str] = None, - type: Optional[Type[GraphQLType]] = None, + type: Optional[Any] = None, ): def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: + field_type: Any = None + field_name = name or convert_python_name_to_graphql(f.__name__) + + if type: + field_type = type + elif f: + field_type = get_field_type_from_resolver(f) + + if field_type is None: + raise ValueError( + f"Unable to find return type for field '{field_name}'. Either add " + "return type annotation on it's resolver or specify return type " + "explicitly via 'type=...' option." + ) + return GraphQLObjectField( resolver=f, - name=name or convert_python_name_to_graphql(f.__name__), + name=field_name, + type=field_type, ) if f is not None: @@ -123,6 +134,16 @@ def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: return object_field_factory +def get_field_type_from_resolver(resolver: Resolver): + return_hint = get_type_hints(resolver).get("return") + if not return_hint: + raise ValueError( + f"Resolver function '{resolver}' is missing return type's annotation." + ) + + return return_hint + + class GraphQLObjectModel(GraphQLModel): ast_type = ObjectTypeDefinitionNode resolvers: Dict[str, Resolver] @@ -148,3 +169,9 @@ def bind_to_schema(self, schema: GraphQLSchema): bindable.bind_to_schema(schema) + +def get_field_node(field: GraphQLObjectField) -> FieldDefinitionNode: + return FieldDefinitionNode( + name=NameNode(value=field.name), + type=get_type_node(field.type), + ) diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index e69de29..bbfbdde 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -0,0 +1,69 @@ +from inspect import isclass +from types import UnionType +from typing import Any, List, Union, get_args, get_origin + +from graphql import ( + ListTypeNode, + NameNode, + NamedTypeNode, + NonNullTypeNode, + TypeNode, +) + +from .base import GraphQLType + + +def get_type_node(type_hint: Any) -> TypeNode: + if is_nullable(type_hint): + nullable = True + type_hint = unwrap_type(type_hint) + else: + nullable = False + + if is_list(type_hint): + list_item_type_hint = unwrap_type(type_hint) + type_node = ListTypeNode(type=get_type_node(list_item_type_hint)) + elif type_hint == str: + type_node = NamedTypeNode(name=NameNode(value="String")) + elif type_hint == int: + type_node = NamedTypeNode(name=NameNode(value="Int")) + elif type_hint == float: + type_node = NamedTypeNode(name=NameNode(value="Float")) + elif type_hint == bool: + type_node = NamedTypeNode(name=NameNode(value="Boolean")) + elif isclass(type_hint) and issubclass(type_hint, GraphQLType): + type_node = NamedTypeNode( + name=NameNode(value=type_hint.__get_graphql_name__()), + ) + else: + raise ValueError(f"Can't create a GraphQL return type for '{type_hint}'") + + if nullable: + return type_node + + return NonNullTypeNode(type=type_node) + + + +def is_list(type_hint: Any) -> bool: + return get_origin(type_hint) == list + + +def is_nullable(type_hint: Any) -> bool: + origin = get_origin(type_hint) + if origin in (UnionType, Union): + return type(None) in get_args(type_hint) + + return False + + +def unwrap_type(type_hint: Any) -> Any: + args = list(get_args(type_hint)) + if type(None) in args: + args.remove(type(None)) + if len(args) != 1: + raise ValueError( + f"Type {type_hint} is a wrapper type for multiple other " + "types and can't be unwrapped." + ) + return args[0] diff --git a/tests_next/test_typing.py b/tests_next/test_typing.py new file mode 100644 index 0000000..a2a8726 --- /dev/null +++ b/tests_next/test_typing.py @@ -0,0 +1,66 @@ +from typing import List, Optional, Union + +from graphql import ListTypeNode, NameNode, NamedTypeNode, NonNullTypeNode + +from ariadne_graphql_modules.next import GraphQLObject +from ariadne_graphql_modules.next.typing import get_type_node + + +def assert_non_null_type(type_node, name: str): + assert isinstance(type_node, NonNullTypeNode) + assert_named_type(type_node.type, name) + + +def assert_non_null_list_type(type_node, name: str): + assert isinstance(type_node, NonNullTypeNode) + assert isinstance(type_node.type, ListTypeNode) + assert isinstance(type_node.type.type, NonNullTypeNode) + assert_named_type(type_node.type.type.type, name) + + +def assert_list_type(type_node, name: str): + assert isinstance(type_node, ListTypeNode) + assert isinstance(type_node.type, NonNullTypeNode) + assert_named_type(type_node.type.type, name) + + +def assert_named_type(type_node, name: str): + assert isinstance(type_node, NamedTypeNode) + assert isinstance(type_node.name, NameNode) + assert type_node.name.value == name + + +def test_get_graphql_type_node_from_python_builtin_type(): + assert_named_type(get_type_node(Optional[str]), "String") + assert_named_type(get_type_node(Union[int, None]), "Int") + assert_named_type(get_type_node(float | None), "Float") + assert_named_type(get_type_node(Optional[bool]), "Boolean") + + +def test_get_non_null_graphql_type_node_from_python_builtin_type(): + assert_non_null_type(get_type_node(str), "String") + assert_non_null_type(get_type_node(int), "Int") + assert_non_null_type(get_type_node(float), "Float") + assert_non_null_type(get_type_node(bool), "Boolean") + + +def test_get_graphql_type_node_from_graphql_type(): + class UserType(GraphQLObject): + ... + + assert_non_null_type(get_type_node(UserType), "User") + assert_named_type(get_type_node(Optional[UserType]), "User") + + +def test_get_graphql_list_type_node_from_python_builtin_type(): + assert_list_type(get_type_node(Optional[List[str]]), "String") + assert_list_type(get_type_node(Union[List[int], None]), "Int") + assert_list_type(get_type_node(List[float] | None), "Float") + assert_list_type(get_type_node(Optional[List[bool]]), "Boolean") + + +def test_get_non_null_graphql_list_type_node_from_python_builtin_type(): + assert_non_null_list_type(get_type_node(List[str]), "String") + assert_non_null_list_type(get_type_node(List[int]), "Int") + assert_non_null_list_type(get_type_node(List[float]), "Float") + assert_non_null_list_type(get_type_node(List[bool]), "Boolean") From 2fa687e3654933de54fc2cf1161be62f7d05f5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Thu, 24 Aug 2023 11:13:59 +0200 Subject: [PATCH 04/63] Basic object and scalar type support --- ariadne_graphql_modules/next/__init__.py | 5 +- ariadne_graphql_modules/next/base.py | 18 +- ariadne_graphql_modules/next/convert_name.py | 2 +- .../next/executable_schema.py | 47 +++- ariadne_graphql_modules/next/objecttype.py | 260 +++++++++++++++--- ariadne_graphql_modules/next/scalartype.py | 139 ++++++++++ ariadne_graphql_modules/next/typing.py | 15 +- tests_next/snapshots/__init__.py | 0 .../snap_test_object_type_validation.py | 14 + .../snap_test_scalar_type_validation.py | 12 + tests_next/test_object_type.py | 251 ++++++++++++++++- tests_next/test_object_type_validation.py | 45 +++ tests_next/test_scalar_type.py | 111 ++++++++ tests_next/test_scalar_type_validation.py | 25 ++ 14 files changed, 881 insertions(+), 63 deletions(-) create mode 100644 ariadne_graphql_modules/next/scalartype.py create mode 100644 tests_next/snapshots/__init__.py create mode 100644 tests_next/snapshots/snap_test_object_type_validation.py create mode 100644 tests_next/snapshots/snap_test_scalar_type_validation.py create mode 100644 tests_next/test_object_type_validation.py create mode 100644 tests_next/test_scalar_type.py create mode 100644 tests_next/test_scalar_type_validation.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 3fec79f..98107eb 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -1,12 +1,15 @@ from .base import GraphQLModel, GraphQLType from .executable_schema import make_executable_schema from .objecttype import GraphQLObject, GraphQLObjectModel, object_field +from .scalartype import GraphQLScalar, GraphQScalarModel __all__ = [ "GraphQLModel", "GraphQLObject", "GraphQLObjectModel", + "GraphQLScalar", + "GraphQScalarModel", "GraphQLType", "make_executable_schema", "object_field", -] \ No newline at end of file +] diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 925545a..0403c62 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -1,6 +1,6 @@ from typing import Iterable, Optional, Type -from graphql import GraphQLSchema, TypeDefinitionNode +from graphql import GraphQLSchema, TypeDefinitionNode, parse class GraphQLType: @@ -12,6 +12,9 @@ def __get_graphql_name__(cls) -> "str": if getattr(cls, "__graphql_name__", None): return cls.__graphql_name__ + if getattr(cls, "__schema__", None): + return cls.__get_graphql_name_from_schema__() + name = cls.__name__ if name.endswith("GraphQLType"): # 'UserGraphQLType' will produce the 'User' name @@ -19,11 +22,24 @@ def __get_graphql_name__(cls) -> "str": if name.endswith("Type"): # 'UserType' will produce the 'User' name return name[:-4] or name + if name.endswith("GraphQLScalar"): + # 'DateTimeGraphQLScalar' will produce the 'DateTime' name + return name[:-13] or name + if name.endswith("Scalar"): + # 'DateTimeLScalar' will produce the 'DateTime' name + return name[:-6] or name if name.endswith("GraphQL"): # 'UserGraphQL' will produce the 'User' name return name[:-7] or name + return name + @classmethod + def __get_graphql_name_from_schema__(cls) -> "str": + # Todo: cache this in future... + document = parse(cls.__schema__) + return document.definitions[0].name.value + @classmethod def __get_graphql_model__(cls) -> "GraphQLModel": raise NotImplementedError( diff --git a/ariadne_graphql_modules/next/convert_name.py b/ariadne_graphql_modules/next/convert_name.py index 41072e4..471585e 100644 --- a/ariadne_graphql_modules/next/convert_name.py +++ b/ariadne_graphql_modules/next/convert_name.py @@ -11,4 +11,4 @@ def convert_python_name_to_graphql(python_name: str) -> str: else: final_name += c.lower() - return final_name \ No newline at end of file + return final_name diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index fbacdca..c510286 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -10,6 +10,8 @@ ) from .base import GraphQLModel, GraphQLType +from .objecttype import GraphQLObjectModel +from .scalartype import GraphQScalarModel SchemaType = Union[ str, @@ -30,18 +32,14 @@ def make_executable_schema( assert_types_unique(types_list) assert_types_not_abstract(types_list) - # TODO: - # Convert unique types list to models/bindables list - # Deal with deferred types - - schema_models: List[GraphQLModel] = [ - type_def.__get_graphql_model__() for type_def in types_list - ] + schema_models: List[GraphQLModel] = sort_models( + [type_def.__get_graphql_model__() for type_def in types_list] + ) document_node = DocumentNode( definitions=tuple(schema_model.ast for schema_model in schema_models), ) - + schema = build_ast_schema(document_node) assert_valid_schema(schema) @@ -63,7 +61,9 @@ def find_type_defs(types: Sequence[SchemaType]) -> List[str]: return type_defs -def flatten_types(types: Sequence[Union[SchemaType, List[SchemaType]]]) -> List[Union[Enum, SchemaBindable, GraphQLModel]]: +def flatten_types( + types: Sequence[Union[SchemaType, List[SchemaType]]] +) -> List[Union[Enum, SchemaBindable, GraphQLModel]]: flat_schema_types_list: List[SchemaType] = flatten_schema_types(types) types_list: List[Union[Enum, SchemaBindable, GraphQLModel]] = [] @@ -114,7 +114,7 @@ def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: if issubclass(type_def, Enum): return type_def.__name__ - if issubclass(type_def, GraphQLModel): + if issubclass(type_def, GraphQLType): return type_def.__get_graphql_name__() return None @@ -129,14 +129,35 @@ def assert_types_unique(type_defs: List[SchemaType]): f"Types '{type_def.__name__}' and '{types_names[type_name]}' both define " f"GraphQL type with name '{type_name}'." ) - + types_names[type_name] = type_def def assert_types_not_abstract(type_defs: List[SchemaType]): for type_def in type_defs: - if issubclass(type_def, GraphQLType) and getattr(type_def, "__abstract__", None): + if issubclass(type_def, GraphQLType) and getattr( + type_def, "__abstract__", None + ): raise ValueError( f"Type '{type_def.__name__}' is an abstract type and can't be used " "for schema creation." - ) \ No newline at end of file + ) + + +def sort_models(schema_models: List[GraphQLModel]) -> List[GraphQLModel]: + sorted_models: List[GraphQLModel] = [] + sorted_models += sort_models_by_type(GraphQScalarModel, schema_models) + sorted_models += [ + model for model in schema_models if isinstance(model, GraphQLObjectModel) + ] + return sorted_models + + +def sort_models_by_type( + model_type: Type[GraphQLModel], + schema_models: List[GraphQLModel], +) -> List[GraphQLModel]: + return sorted( + [model for model in schema_models if isinstance(model, model_type)], + key=lambda m: m.name, + ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index ec9e5da..6452ffc 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,4 +1,13 @@ -from typing import Any, Dict, Iterable, List, Optional, Type, get_origin, get_type_hints +from typing import ( + Any, + Dict, + Iterable, + List, + Optional, + Type, + cast, + get_type_hints, +) from ariadne import ObjectType as ObjectTypeBindable from ariadne.types import Resolver @@ -6,15 +15,13 @@ FieldDefinitionNode, GraphQLSchema, NameNode, - NamedTypeNode, - NonNullTypeNode, ObjectTypeDefinitionNode, - TypeNode, ) +from ..utils import parse_definition from .base import GraphQLModel, GraphQLType from .convert_name import convert_python_name_to_graphql -from .typing import get_type_node +from .typing import get_graphql_type, get_type_node class GraphQLObject(GraphQLType): @@ -39,18 +46,88 @@ def __init_subclass__(cls) -> None: @classmethod def __get_graphql_model__(cls) -> "GraphQLModel": name = cls.__get_graphql_name__() + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__(name) + + return cls.__get_graphql_model_without_schema__(name) + + @classmethod + def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": + definition = cast( + ObjectTypeDefinitionNode, + parse_definition(ObjectTypeDefinitionNode, cls.__schema__), + ) + + resolvers: Dict[str, Resolver] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers[cls_attr.field] = cls_attr.resolver + + return GraphQLObjectModel( + name=definition.name.value, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=definition.name.value), + fields=definition.fields, + ), + resolvers=resolvers, + out_names=out_names, + ) + + @classmethod + def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": + type_hints = get_type_hints(cls) + fields_resolvers: Dict[str, Resolver] = {} + fields_instances: Dict[str, GraphQLObjectField] = {} + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + fields_instances[attr_name] = cls_attr + if isinstance(cls_attr, GraphQLObjectResolver): + fields_resolvers[cls_attr.field] = cls_attr.resolver + fields_ast: List[FieldDefinitionNode] = [] resolvers: Dict[str, Resolver] = {} out_names: Dict[str, Dict[str, str]] = {} - for field in cls.__dict__.values(): - if not isinstance(field, GraphQLObjectField): + for hint_name, hint_type in type_hints.items(): + if hint_name.startswith("__"): continue - if field.resolver: - resolvers[field.name] = field.resolver + if hint_name in fields_instances: + continue + + hint_field_name = convert_python_name_to_graphql(hint_name) + + fields_ast.append(get_field_node_from_type_hint(hint_field_name, hint_type)) + + if hint_name in fields_resolvers: + resolvers[hint_field_name] = fields_resolvers[hint_name] + + for attr_name, attr_field in fields_instances.items(): + field_instance = GraphQLObjectField( + name=attr_field.name or convert_python_name_to_graphql(attr_name), + type=attr_field.type or type_hints.get(attr_name), + resolver=attr_field.resolver, + ) + + if field_instance.type is None: + raise ValueError( + f"Unable to find return type for field '{field_instance.name}'. " + "Either add a return type annotation on it's resolver or specify " + "return type explicitly via 'type=...' option." + ) + + if field_instance.resolver: + resolvers[field_instance.name] = field_instance.resolver + elif attr_name in fields_resolvers: + resolvers[field_instance.name] = fields_resolvers[attr_name] - fields_ast.append(get_field_node(field)) + fields_ast.append(get_field_node_from_obj_field(field_instance)) return GraphQLObjectModel( name=name, @@ -61,7 +138,28 @@ def __get_graphql_model__(cls) -> "GraphQLModel": resolvers=resolvers, out_names=out_names, ) - + + @classmethod + def __get_graphql_types__(cls) -> Iterable["GraphQLType"]: + """Returns iterable with GraphQL types associated with this type""" + types: List[GraphQLType] = [cls] + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if cls_attr.type: + field_graphql_type = get_graphql_type(cls_attr.type) + if field_graphql_type and field_graphql_type not in types: + types.append(field_graphql_type) + + type_hints = get_type_hints(cls) + for hint_type in type_hints.values(): + hint_graphql_type = get_graphql_type(hint_type) + if hint_graphql_type and hint_graphql_type not in types: + types.append(hint_graphql_type) + + return types + @staticmethod def field( f: Optional[Resolver] = None, @@ -69,24 +167,70 @@ def field( name: Optional[str] = None, type: Optional[Any] = None, ): - """Shortcut for using object_field() separately""" - return object_field(f, name=name) + """Shortcut for object_field()""" + return object_field(f, name=name, type=type) + + @staticmethod + def resolver( + field: str, + type: Optional[Any] = None, + ): + """Shortcut for object_resolver()""" + return object_resolver(field=field, type=type) + + +def validate_object_type(cls: Type[GraphQLObject]): + attrs_names: List[str] = [ + attr_name for attr_name in get_type_hints(cls) if not attr_name.startswith("__") + ] + resolvers_names: List[str] = [] + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if cls_attr not in attrs_names: + attrs_names.append(cls_attr) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers_names.append(cls_attr.field) + + for resolver_for in resolvers_names: + if resolver_for not in attrs_names: + valid_fields: str = "', '".join(sorted(attrs_names)) + raise ValueError( + f"Class '{cls.__name__}' defines resolver for an undefined " + f"attribute '{resolver_for}'. (Valid attrs: '{valid_fields}')" + ) -def validate_object_type(graphql_type: Type[GraphQLObject]): - pass +def validate_object_type_with_schema(cls: Type[GraphQLObject]): + definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) + if not isinstance(definition, ObjectTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{ObjectTypeDefinitionNode.__name__}')" + ) -def validate_object_type_with_schema(graphql_type: Type[GraphQLObject]): - pass + field_names: List[str] = [f.name.value for f in definition.fields] + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.field not in field_names: + valid_fields: str = "', '".join(sorted(field_names)) + raise ValueError( + f"Class '{cls.__name__}' defines resolver for an undefined " + f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" + ) class GraphQLObjectField: def __init__( self, *, - name: str, - type: Any, + name: Optional[str] = None, + type: Optional[Any] = None, resolver: Optional[Resolver] = None, ): self.name = name @@ -101,21 +245,19 @@ def object_field( type: Optional[Any] = None, ): def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: + field_name: Optional[str] = None field_type: Any = None - field_name = name or convert_python_name_to_graphql(f.__name__) + + if name: + field_name = name + elif f.__name__ != "": + field_name = convert_python_name_to_graphql(f.__name__) if type: field_type = type elif f: field_type = get_field_type_from_resolver(f) - if field_type is None: - raise ValueError( - f"Unable to find return type for field '{field_name}'. Either add " - "return type annotation on it's resolver or specify return type " - "explicitly via 'type=...' option." - ) - return GraphQLObjectField( resolver=f, name=field_name, @@ -123,25 +265,52 @@ def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: ) if f is not None: - if any((name, type)): - raise ValueError( - "'object_field' decorator was called with function argument " - "and the options." - ) - return object_field_factory(f) - + return object_field_factory -def get_field_type_from_resolver(resolver: Resolver): - return_hint = get_type_hints(resolver).get("return") - if not return_hint: - raise ValueError( - f"Resolver function '{resolver}' is missing return type's annotation." +def get_field_type_from_resolver(resolver: Resolver) -> Any: + return get_type_hints(resolver).get("return") + + +class GraphQLObjectResolver: + def __init__( + self, + resolver: Resolver, + field: str, + *, + type: Optional[Any] = None, + ): + self.resolver = resolver + self.field = field + self.type = type + + +def object_resolver( + field: str, + type: Optional[Any] = None, +): + def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: + field_type: Any = None + + if field: + field_name = field + elif f.__name__ != "": + field_name = convert_python_name_to_graphql(f.__name__) + + if type: + field_type = type + else: + field_type = get_field_type_from_resolver(f) + + return GraphQLObjectResolver( + resolver=f, + field=field_name, + type=field_type, ) - return return_hint + return object_resolver_factory class GraphQLObjectModel(GraphQLModel): @@ -170,7 +339,16 @@ def bind_to_schema(self, schema: GraphQLSchema): bindable.bind_to_schema(schema) -def get_field_node(field: GraphQLObjectField) -> FieldDefinitionNode: +def get_field_node_from_type_hint( + field_name: str, field_type: Any +) -> FieldDefinitionNode: + return FieldDefinitionNode( + name=NameNode(value=field_name), + type=get_type_node(field_type), + ) + + +def get_field_node_from_obj_field(field: GraphQLObjectField) -> FieldDefinitionNode: return FieldDefinitionNode( name=NameNode(value=field.name), type=get_type_node(field.type), diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/scalartype.py new file mode 100644 index 0000000..5ec50b2 --- /dev/null +++ b/ariadne_graphql_modules/next/scalartype.py @@ -0,0 +1,139 @@ +from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, cast + +from ariadne import ScalarType as ScalarTypeBindable +from graphql import ( + GraphQLSchema, + NameNode, + ScalarTypeDefinitionNode, + ValueNode, + value_from_ast_untyped, +) + +from ..utils import parse_definition +from .base import GraphQLModel, GraphQLType + +T = TypeVar("T") + + +class GraphQLScalar(GraphQLType, Generic[T]): + __abstract__: bool = True + + wrapped_value: T + + def __init__(self, value: T): + self.wrapped_value = value + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + validate_scalar_type_with_schema(cls) + + @classmethod + def __get_graphql_model__(cls) -> "GraphQLModel": + name = cls.__get_graphql_name__() + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__(name) + + return cls.__get_graphql_model_without_schema__(name) + + @classmethod + def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": + definition = cast( + ScalarTypeDefinitionNode, + parse_definition(ScalarTypeDefinitionNode, cls.__schema__), + ) + + return GraphQScalarModel( + name=definition.name.value, + ast=definition, + serialize=cls.serialize, + parse_value=cls.parse_value, + parse_literal=cls.parse_literal, + ) + + @classmethod + def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": + return GraphQScalarModel( + name=name, + ast=ScalarTypeDefinitionNode( + name=NameNode(value=name), + ), + serialize=cls.serialize, + parse_value=cls.parse_value, + parse_literal=cls.parse_literal, + ) + + @classmethod + def serialize(cls, value: Any) -> Any: + if isinstance(value, cls): + return value.unwrap() + + return value + + @classmethod + def parse_value(cls, value: Any) -> Any: + return value + + @classmethod + def parse_literal( + cls, node: ValueNode, variables: Optional[Dict[str, Any]] = None + ) -> Any: + return cls.parse_value(value_from_ast_untyped(node, variables)) + + def unwrap(self) -> T: + return self.wrapped_value + + +def validate_scalar_type_with_schema(cls: Type[GraphQLScalar]): + definition = parse_definition(cls.__name__, cls.__schema__) + + if not isinstance(definition, ScalarTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{ScalarTypeDefinitionNode.__name__}')" + ) + + graphql_name = getattr(cls, "__graphql_name__", None) + if graphql_name and definition.name.value != definition: + raise ValueError( + f"Class '{cls.__name__}' defines both '__graphql_name__' and " + f"'__schema__' attributes, but scalar's names in those don't match. " + f"('{graphql_name}' != '{definition.name.value}')" + ) + + +class GraphQScalarModel(GraphQLModel): + ast_type = ScalarTypeDefinitionNode + + def __init__( + self, + name: str, + ast: ScalarTypeDefinitionNode, + serialize: Callable[[Any], Any], + parse_value: Callable[[Any], Any], + parse_literal: Callable[[ValueNode, Optional[Dict[str, Any]]], Any], + ): + self.name = name + self.ast = ast + self.serialize = serialize + self.parse_value = parse_value + self.parse_literal = parse_literal + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = ScalarTypeBindable( + self.name, + serializer=self.serialize, + value_parser=self.parse_value, + literal_parser=self.parse_literal, + ) + + bindable.bind_to_schema(schema) diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index bbfbdde..abf167d 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -1,6 +1,6 @@ from inspect import isclass from types import UnionType -from typing import Any, List, Union, get_args, get_origin +from typing import Any, Optional, Union, get_args, get_origin from graphql import ( ListTypeNode, @@ -23,7 +23,7 @@ def get_type_node(type_hint: Any) -> TypeNode: if is_list(type_hint): list_item_type_hint = unwrap_type(type_hint) type_node = ListTypeNode(type=get_type_node(list_item_type_hint)) - elif type_hint == str: + elif type_hint == str: type_node = NamedTypeNode(name=NameNode(value="String")) elif type_hint == int: type_node = NamedTypeNode(name=NameNode(value="Int")) @@ -44,7 +44,6 @@ def get_type_node(type_hint: Any) -> TypeNode: return NonNullTypeNode(type=type_node) - def is_list(type_hint: Any) -> bool: return get_origin(type_hint) == list @@ -67,3 +66,13 @@ def unwrap_type(type_hint: Any) -> Any: "types and can't be unwrapped." ) return args[0] + + +def get_graphql_type(type_hint: Any) -> Optional[GraphQLType]: + if not isclass(type_hint): + return None + + if issubclass(type_hint, GraphQLType): + return type_hint + + return None diff --git a/tests_next/snapshots/__init__.py b/tests_next/snapshots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py new file mode 100644 index 0000000..31787ee --- /dev/null +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined attribute 'other'. (Valid attrs: 'hello')" + +snapshots['test_object_type_validation_fails_for_undefined_field_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" + +snapshots['test_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" diff --git a/tests_next/snapshots/snap_test_scalar_type_validation.py b/tests_next/snapshots/snap_test_scalar_type_validation.py new file mode 100644 index 0000000..6aa0205 --- /dev/null +++ b/tests_next/snapshots/snap_test_scalar_type_validation.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode')" + +snapshots['test_scalar_type_validation_fails_for_name_mismatch_between_schema_and_attr 1'] = "Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, but scalar's names in those don't match. ('Date' != 'Custom')" diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index c52bba7..19e867d 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -1,18 +1,263 @@ from graphql import graphql_sync -from ariadne import make_executable_schema - +from ariadne_graphql_modules import gql from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema -def test_minimal_object_type(assert_schema_equals): +def test_object_type_with_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_field_resolver(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field + def hello(obj, info) -> str: + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_typed_field_instance(assert_schema_equals): + class QueryType(GraphQLObject): + hello = GraphQLObject.field(lambda *_: "Hello World!", type=str) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_annotated_field_instance(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str = GraphQLObject.field(lambda *_: "Hello World!") + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_typed_field_and_field_resolver(assert_schema_equals): class QueryType(GraphQLObject): + name: str + @GraphQLObject.field def hello(obj, info) -> str: return "Hello World!" schema = make_executable_schema(QueryType) + assert_schema_equals( + schema, + """ + type Query { + name: String! + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ name hello }", root_value={"name": "Ok"}) + + assert not result.errors + assert result.data == {"name": "Ok", "hello": "Hello World!"} + + +def test_object_type_with_schema(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_nested_types(assert_schema_equals): + class UserType(GraphQLObject): + name: str + + class PostType(GraphQLObject): + message: str + + class QueryType(GraphQLObject): + user: UserType + + @GraphQLObject.field(type=PostType) + def post(obj, info): + return {"message": "test"} + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + user: User! + post: Post! + } + + type Post { + message: String! + } + + type User { + name: String! + } + """, + ) + + result = graphql_sync( + schema, + "{ user { name } post { message } }", + root_value={"user": {"name": "Bob"}}, + ) + + assert not result.errors + assert result.data == { + "user": { + "name": "Bob", + }, + "post": {"message": "test"}, + } + + +def test_resolver_decorator_sets_resolver_for_type_hint_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_resolver_for_instance_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str = GraphQLObject.field() + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_resolver_for_field_in_schema(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + assert_schema_equals( schema, """ diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py new file mode 100644 index 0000000..5316423 --- /dev/null +++ b/tests_next/test_object_type_validation.py @@ -0,0 +1,45 @@ +import pytest + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import GraphQLObject + + +def test_scalar_type_validation_fails_for_invalid_type_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = gql("scalar Custom") + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_undefined_attr_resolver(snapshot): + with pytest.raises(ValueError) as exc_info: + + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("other") + def resolve_hello(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_undefined_field_resolver(snapshot): + with pytest.raises(ValueError) as exc_info: + + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + @GraphQLObject.resolver("other") + def resolve_hello(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_scalar_type.py b/tests_next/test_scalar_type.py new file mode 100644 index 0000000..6b0dd11 --- /dev/null +++ b/tests_next/test_scalar_type.py @@ -0,0 +1,111 @@ +from datetime import date + +from graphql import graphql_sync + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import ( + GraphQLObject, + GraphQLScalar, + make_executable_schema, +) + + +class DateScalar(GraphQLScalar[date]): + @classmethod + def serialize(cls, value): + if isinstance(value, cls): + return str(value.unwrap()) + + return str(value) + + +class SDLDateScalar(GraphQLScalar[date]): + __schema__ = "scalar Date" + + @classmethod + def serialize(cls, value): + if isinstance(value, cls): + return str(value.unwrap()) + + return str(value) + + +def test_scalar_field_returning_scalar_instance(assert_schema_equals): + class QueryType(GraphQLObject): + date: DateScalar + + @GraphQLObject.resolver("date") + def resolve_date(*_) -> DateScalar: + return DateScalar(date(1989, 10, 30)) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + scalar Date + + type Query { + date: Date! + } + """, + ) + + result = graphql_sync(schema, "{ date }") + + assert not result.errors + assert result.data == {"date": "1989-10-30"} + + +def test_scalar_field_returning_scalar_wrapped_type(assert_schema_equals): + class QueryType(GraphQLObject): + date: DateScalar + + @GraphQLObject.resolver("date", type=DateScalar) + def resolve_date(*_) -> date: + return date(1989, 10, 30) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + scalar Date + + type Query { + date: Date! + } + """, + ) + + result = graphql_sync(schema, "{ date }") + + assert not result.errors + assert result.data == {"date": "1989-10-30"} + + +def test_schema_first_scalar_type(assert_schema_equals): + class QueryType(GraphQLObject): + date: SDLDateScalar + + @GraphQLObject.resolver("date") + def resolve_date(*_) -> SDLDateScalar: + return SDLDateScalar(date(1989, 10, 30)) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + scalar Date + + type Query { + date: Date! + } + """, + ) + + result = graphql_sync(schema, "{ date }") + + assert not result.errors + assert result.data == {"date": "1989-10-30"} diff --git a/tests_next/test_scalar_type_validation.py b/tests_next/test_scalar_type_validation.py new file mode 100644 index 0000000..f23b543 --- /dev/null +++ b/tests_next/test_scalar_type_validation.py @@ -0,0 +1,25 @@ +import pytest + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import GraphQLScalar + + +def test_scalar_type_validation_fails_for_invalid_type_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomScalar(GraphQLScalar[str]): + __schema__ = gql("type Custom") + + snapshot.assert_match(str(exc_info.value)) + + +def test_scalar_type_validation_fails_for_name_mismatch_between_schema_and_attr( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomScalar(GraphQLScalar[str]): + __graphql_name__ = "Date" + __schema__ = gql("scalar Custom") + + snapshot.assert_match(str(exc_info.value)) From 5bc9fd726da5d14224d1df2d8f619c9207d72403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Fri, 8 Sep 2023 18:38:40 +0200 Subject: [PATCH 05/63] More API experiments --- ariadne_graphql_modules/next/base.py | 1 + ariadne_graphql_modules/next/objecttype.py | 255 ++++++++++++++++-- ariadne_graphql_modules/next/scalartype.py | 4 + ariadne_graphql_modules/next/validators.py | 11 + tests_next/conftest.py | 2 - .../snap_test_object_type_validation.py | 6 +- .../snap_test_scalar_type_validation.py | 2 + tests_next/snapshots/snap_test_validators.py | 10 + .../test_get_field_args_from_resolver.py | 154 +++++++++++ tests_next/test_object_type.py | 99 +++++++ tests_next/test_object_type_field_args.py | 146 ++++++++++ tests_next/test_object_type_validation.py | 19 +- tests_next/test_scalar_type_validation.py | 15 ++ tests_next/test_validators.py | 38 +++ 14 files changed, 741 insertions(+), 21 deletions(-) create mode 100644 ariadne_graphql_modules/next/validators.py create mode 100644 tests_next/snapshots/snap_test_validators.py create mode 100644 tests_next/test_get_field_args_from_resolver.py create mode 100644 tests_next/test_object_type_field_args.py create mode 100644 tests_next/test_validators.py diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 0403c62..e6f2c8a 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -5,6 +5,7 @@ class GraphQLType: __graphql_name__: Optional[str] + __description__: Optional[str] __abstract__: bool = True @classmethod diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 6452ffc..152fc18 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,9 +1,11 @@ +from inspect import signature from typing import ( Any, Dict, Iterable, List, Optional, + Tuple, Type, cast, get_type_hints, @@ -14,21 +16,24 @@ from graphql import ( FieldDefinitionNode, GraphQLSchema, + InputValueDefinitionNode, NameNode, ObjectTypeDefinitionNode, + StringValueNode, ) from ..utils import parse_definition from .base import GraphQLModel, GraphQLType from .convert_name import convert_python_name_to_graphql from .typing import get_graphql_type, get_type_node +from .validators import validate_description class GraphQLObject(GraphQLType): - __schema__: Optional[str] - __requires__: Optional[Iterable[GraphQLType]] - __abstract__: bool = True + __schema__: Optional[str] + __description__: Optional[str] + __aliases__: Optional[Dict[str, str]] def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -74,12 +79,15 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": fields=definition.fields, ), resolvers=resolvers, + aliases=getattr(cls, "__aliases__", {}), out_names=out_names, ) @classmethod def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": type_hints = get_type_hints(cls) + fields_args_options: Dict[str, dict] = {} + fields_descriptions: Dict[str, str] = {} fields_resolvers: Dict[str, Resolver] = {} fields_instances: Dict[str, GraphQLObjectField] = {} @@ -87,8 +95,16 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectField): fields_instances[attr_name] = cls_attr + if cls_attr.args: + fields_args_options[attr_name] = cls_attr.args + if cls_attr.description: + fields_descriptions[attr_name] = cls_attr.description if isinstance(cls_attr, GraphQLObjectResolver): fields_resolvers[cls_attr.field] = cls_attr.resolver + if cls_attr.args: + fields_args_options[cls_attr.field] = cls_attr.args + if cls_attr.description: + fields_descriptions[cls_attr.field] = cls_attr.description fields_ast: List[FieldDefinitionNode] = [] resolvers: Dict[str, Resolver] = {} @@ -103,14 +119,26 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": hint_field_name = convert_python_name_to_graphql(hint_name) - fields_ast.append(get_field_node_from_type_hint(hint_field_name, hint_type)) - if hint_name in fields_resolvers: resolvers[hint_field_name] = fields_resolvers[hint_name] + field_args = get_field_args_from_resolver(resolvers[hint_field_name]) + else: + field_args = {} + + fields_ast.append( + get_field_node_from_type_hint( + hint_field_name, + hint_type, + field_args, + fields_descriptions.get(hint_name), + ) + ) for attr_name, attr_field in fields_instances.items(): field_instance = GraphQLObjectField( name=attr_field.name or convert_python_name_to_graphql(attr_name), + description=attr_field.description + or fields_descriptions.get(attr_name), type=attr_field.type or type_hints.get(attr_name), resolver=attr_field.resolver, ) @@ -122,20 +150,38 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": "return type explicitly via 'type=...' option." ) + field_resolver: Optional[Resolver] = None if field_instance.resolver: - resolvers[field_instance.name] = field_instance.resolver + field_resolver = field_instance.resolver elif attr_name in fields_resolvers: - resolvers[field_instance.name] = fields_resolvers[attr_name] - - fields_ast.append(get_field_node_from_obj_field(field_instance)) + field_resolver = fields_resolvers[attr_name] + + field_args = {} + if field_resolver: + resolvers[field_instance.name] = field_resolver + field_args = get_field_args_from_resolver(field_resolver) + if field_instance.name in fields_args_options: + update_resolver_args( + field_args, fields_args_options[field_instance.name] + ) + + fields_ast.append(get_field_node_from_obj_field(field_instance, field_args)) + + object_description = getattr(cls, "__description__", None) + if object_description: + description = StringValueNode(value=object_description) + else: + description = None return GraphQLObjectModel( name=name, ast=ObjectTypeDefinitionNode( name=NameNode(value=name), + description=description, fields=tuple(fields_ast), ), resolvers=resolvers, + aliases=getattr(cls, "__aliases__", {}), out_names=out_names, ) @@ -166,17 +212,44 @@ def field( *, name: Optional[str] = None, type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, + description: Optional[str] = None, ): """Shortcut for object_field()""" - return object_field(f, name=name, type=type) + return object_field( + f, + args=args, + name=name, + type=type, + description=description, + ) @staticmethod def resolver( field: str, type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, + description: Optional[str] = None, ): """Shortcut for object_resolver()""" - return object_resolver(field=field, type=type) + return object_resolver( + args=args, + field=field, + type=type, + description=description, + ) + + @staticmethod + def argument( + description: Optional[str] = None, + type: Optional[Any] = None, + ) -> dict: + options: dict = {} + if description: + options["description"] = description + if type: + options["type"] = type + return options def validate_object_type(cls: Type[GraphQLObject]): @@ -224,24 +297,38 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]): f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" ) + validate_description(cls, definition) + class GraphQLObjectField: + name: Optional[str] + description: Optional[str] + type: Optional[Any] + args: Optional[Dict[str, dict]] + resolver: Optional[Resolver] + def __init__( self, *, name: Optional[str] = None, + description: Optional[str] = None, type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, resolver: Optional[Resolver] = None, ): self.name = name + self.description = description self.type = type + self.args = args self.resolver = resolver def object_field( f: Optional[Resolver] = None, *, + args: Optional[Dict[str, dict]] = None, name: Optional[str] = None, + description: Optional[str] = None, type: Optional[Any] = None, ): def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: @@ -250,8 +337,6 @@ def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: if name: field_name = name - elif f.__name__ != "": - field_name = convert_python_name_to_graphql(f.__name__) if type: field_type = type @@ -259,9 +344,11 @@ def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: field_type = get_field_type_from_resolver(f) return GraphQLObjectField( - resolver=f, name=field_name, + description=description, type=field_type, + args=args, + resolver=f, ) if f is not None: @@ -275,13 +362,23 @@ def get_field_type_from_resolver(resolver: Resolver) -> Any: class GraphQLObjectResolver: + resolver: Resolver + field: str + description: Optional[str] + args: Optional[Dict[str, dict]] + type: Optional[Any] + def __init__( self, resolver: Resolver, field: str, *, + description: Optional[str] = None, + args: Optional[Dict[str, dict]] = None, type: Optional[Any] = None, ): + self.args = args + self.description = description self.resolver = resolver self.field = field self.type = type @@ -290,6 +387,8 @@ def __init__( def object_resolver( field: str, type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, + description: Optional[str] = None, ): def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: field_type: Any = None @@ -305,6 +404,8 @@ def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: field_type = get_field_type_from_resolver(f) return GraphQLObjectResolver( + args=args, + description=description, resolver=f, field=field_name, type=field_type, @@ -316,6 +417,7 @@ def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: class GraphQLObjectModel(GraphQLModel): ast_type = ObjectTypeDefinitionNode resolvers: Dict[str, Resolver] + aliases: Dict[str, str] out_names: Dict[str, Dict[str, str]] def __init__( @@ -323,11 +425,13 @@ def __init__( name: str, ast: ObjectTypeDefinitionNode, resolvers: Dict[str, Resolver], + aliases: Dict[str, str], out_names: Dict[str, Dict[str, str]], ): self.name = name self.ast = ast self.resolvers = resolvers + self.aliases = aliases self.out_names = out_names def bind_to_schema(self, schema: GraphQLSchema): @@ -335,21 +439,140 @@ def bind_to_schema(self, schema: GraphQLSchema): for field, resolver in self.resolvers.items(): bindable.set_field(field, resolver) + for alias, target in self.aliases.items(): + bindable.set_alias(alias, target) bindable.bind_to_schema(schema) def get_field_node_from_type_hint( - field_name: str, field_type: Any + field_name: str, + field_type: Any, + field_args: Any, + field_description: Optional[str] = None, ) -> FieldDefinitionNode: + if field_description: + description = StringValueNode(value=field_description) + else: + description = None + return FieldDefinitionNode( + description=description, name=NameNode(value=field_name), type=get_type_node(field_type), + arguments=get_field_args_nodes_from_obj_field_arg(field_args), ) -def get_field_node_from_obj_field(field: GraphQLObjectField) -> FieldDefinitionNode: +def get_field_node_from_obj_field( + field: GraphQLObjectField, + field_args: Any, +) -> FieldDefinitionNode: + if field.description: + description = StringValueNode(value=field.description) + else: + description = None + return FieldDefinitionNode( + description=description, name=NameNode(value=field.name), type=get_type_node(field.type), + arguments=get_field_args_nodes_from_obj_field_arg(field_args), + ) + + +class GraphQLObjectFieldArg: + name: Optional[str] + out_name: Optional[str] + description: Optional[str] + type: Optional[Any] + + def __init__( + self, + *, + name: Optional[str] = None, + out_name: Optional[str] = None, + description: Optional[str] = None, + type: Optional[Any] = None, + ): + self.name = name + self.out_name = out_name + self.description = description + self.type = type + + +def get_field_args_from_resolver( + resolver: Resolver, +) -> Dict[str, GraphQLObjectFieldArg]: + resolver_signature = signature(resolver) + type_hints = get_type_hints(resolver) + type_hints.pop("return", None) + + field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args_start = 0 + + # Fist pass: (arg, *_, something, something) or (arg, *, something, something): + for i, param in enumerate(resolver_signature.parameters.values()): + param_repr = str(param) + if param_repr.startswith("*") and not param_repr.startswith("**"): + field_args_start = i + 1 + break + else: + if len(resolver_signature.parameters) < 2: + raise TypeError( + f"Resolver function '{resolver_signature}' should accept at least 'obj' and 'info' positional arguments." + ) + + field_args_start = 2 + + args_parameters = tuple(resolver_signature.parameters.items())[field_args_start:] + if not args_parameters: + return field_args + + for param_name, param in args_parameters: + field_args[param_name] = GraphQLObjectFieldArg( + name=convert_python_name_to_graphql(param_name), + out_name=param_name, + type=type_hints.get(param_name), + ) + + return field_args + + +def get_field_args_nodes_from_obj_field_arg( + field_args: Dict[str, GraphQLObjectFieldArg] +) -> Tuple[InputValueDefinitionNode]: + return tuple( + get_field_arg_node_from_obj_field_arg(field_arg) + for field_arg in field_args.values() ) + + +def get_field_arg_node_from_obj_field_arg( + field_arg: GraphQLObjectFieldArg, +) -> InputValueDefinitionNode: + if field_arg.description: + description = StringValueNode(value=field_arg.description) + else: + description = None + + return InputValueDefinitionNode( + description=description, + name=NameNode(value=field_arg.name), + type=get_type_node(field_arg.type), + ) + + +def update_resolver_args( + resolver_args: Dict[str, GraphQLObjectFieldArg], + args_options: Optional[Dict[str, dict]], +): + if not args_options: + return + + for arg_name, arg_options in args_options.items(): + resolver_arg = resolver_args[arg_name] + if arg_options.get("description"): + resolver_arg.description = arg_options["description"] + if arg_options.get("type"): + resolver_arg.type = arg_options["type"] diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/scalartype.py index 5ec50b2..a2ade94 100644 --- a/ariadne_graphql_modules/next/scalartype.py +++ b/ariadne_graphql_modules/next/scalartype.py @@ -5,12 +5,14 @@ GraphQLSchema, NameNode, ScalarTypeDefinitionNode, + StringValueNode, ValueNode, value_from_ast_untyped, ) from ..utils import parse_definition from .base import GraphQLModel, GraphQLType +from .validators import validate_description T = TypeVar("T") @@ -110,6 +112,8 @@ def validate_scalar_type_with_schema(cls: Type[GraphQLScalar]): f"('{graphql_name}' != '{definition.name.value}')" ) + validate_description(cls, definition) + class GraphQScalarModel(GraphQLModel): ast_type = ScalarTypeDefinitionNode diff --git a/ariadne_graphql_modules/next/validators.py b/ariadne_graphql_modules/next/validators.py new file mode 100644 index 0000000..374023c --- /dev/null +++ b/ariadne_graphql_modules/next/validators.py @@ -0,0 +1,11 @@ +from typing import Any, Type + +from .objecttype import GraphQLType + + +def validate_description(cls: Type[GraphQLType], definition: Any): + if getattr(cls, "__description__", None) and definition.description: + raise ValueError( + f"Class '{cls.__name__}' defines description in both " + "'__description__' and '__schema__' attributes." + ) diff --git a/tests_next/conftest.py b/tests_next/conftest.py index 0c054a8..05d91f4 100644 --- a/tests_next/conftest.py +++ b/tests_next/conftest.py @@ -8,8 +8,6 @@ def assert_schema_equals(): def schema_equals_assertion(schema: GraphQLSchema, target: str): schema_str = print_schema(schema) - print(schema_str) - assert schema_str == dedent(target).strip() return schema_equals_assertion diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py index 31787ee..dcc7396 100644 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -7,8 +7,10 @@ snapshots = Snapshot() +snapshots['test_object_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" + +snapshots['test_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." + snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined attribute 'other'. (Valid attrs: 'hello')" snapshots['test_object_type_validation_fails_for_undefined_field_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" - -snapshots['test_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" diff --git a/tests_next/snapshots/snap_test_scalar_type_validation.py b/tests_next/snapshots/snap_test_scalar_type_validation.py index 6aa0205..2960d8d 100644 --- a/tests_next/snapshots/snap_test_scalar_type_validation.py +++ b/tests_next/snapshots/snap_test_scalar_type_validation.py @@ -10,3 +10,5 @@ snapshots['test_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode')" snapshots['test_scalar_type_validation_fails_for_name_mismatch_between_schema_and_attr 1'] = "Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, but scalar's names in those don't match. ('Date' != 'Custom')" + +snapshots['test_scalar_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomScalar' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_validators.py b/tests_next/snapshots/snap_test_validators.py new file mode 100644 index 0000000..057e09c --- /dev/null +++ b/tests_next/snapshots/snap_test_validators.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_description_validator_raises_error_for_type_with_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/test_get_field_args_from_resolver.py b/tests_next/test_get_field_args_from_resolver.py new file mode 100644 index 0000000..054fd99 --- /dev/null +++ b/tests_next/test_get_field_args_from_resolver.py @@ -0,0 +1,154 @@ +from ariadne_graphql_modules.next.objecttype import get_field_args_from_resolver + + +def test_field_has_no_args_after_obj_and_info_args(): + def field_resolver(obj, info): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert not field_args + + +def test_field_has_no_args_in_resolver_with_catch_all_args_list(): + def field_resolver(*_): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert not field_args + + +def test_field_has_arg_after_excess_positional_args(): + def field_resolver(*_, name): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert len(field_args) == 1 + + field_arg = field_args["name"] + assert field_arg.name == "name" + assert field_arg.out_name == "name" + assert field_arg.type is None + + +def test_field_has_arg_after_positional_args_separator(): + def field_resolver(obj, info, *, name): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert len(field_args) == 1 + + field_arg = field_args["name"] + assert field_arg.name == "name" + assert field_arg.out_name == "name" + assert field_arg.type is None + + +def test_field_has_arg_after_obj_and_info_args(): + def field_resolver(obj, info, name): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert len(field_args) == 1 + + field_arg = field_args["name"] + assert field_arg.name == "name" + assert field_arg.out_name == "name" + assert field_arg.type is None + + +def test_field_has_multiple_args_after_excess_positional_args(): + def field_resolver(*_, name, age_cutoff: int): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert len(field_args) == 2 + + name_arg = field_args["name"] + assert name_arg.name == "name" + assert name_arg.out_name == "name" + assert name_arg.type is None + + age_arg = field_args["age_cutoff"] + assert age_arg.name == "ageCutoff" + assert age_arg.out_name == "age_cutoff" + assert age_arg.type is int + + +def test_field_has_multiple_args_after_positional_args_separator(): + def field_resolver(obj, info, *, name, age_cutoff: int): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert len(field_args) == 2 + + name_arg = field_args["name"] + assert name_arg.name == "name" + assert name_arg.out_name == "name" + assert name_arg.type is None + + age_arg = field_args["age_cutoff"] + assert age_arg.name == "ageCutoff" + assert age_arg.out_name == "age_cutoff" + assert age_arg.type is int + + +def test_field_has_multiple_args_after_obj_and_info_args(): + def field_resolver(obj, info, name, age_cutoff: int): + pass + + field_args = get_field_args_from_resolver(field_resolver) + assert len(field_args) == 2 + + name_arg = field_args["name"] + assert name_arg.name == "name" + assert name_arg.out_name == "name" + assert name_arg.type is None + + age_arg = field_args["age_cutoff"] + assert age_arg.name == "ageCutoff" + assert age_arg.out_name == "age_cutoff" + assert age_arg.type is int + + +def test_field_has_arg_after_obj_and_info_args_on_class_function(): + class CustomObject: + def field_resolver(obj, info, name): + pass + + field_args = get_field_args_from_resolver(CustomObject.field_resolver) + assert len(field_args) == 1 + + field_arg = field_args["name"] + assert field_arg.name == "name" + assert field_arg.out_name == "name" + assert field_arg.type is None + + +def test_field_has_arg_after_obj_and_info_args_on_class_method(): + class CustomObject: + @classmethod + def field_resolver(cls, obj, info, name): + pass + + field_args = get_field_args_from_resolver(CustomObject.field_resolver) + assert len(field_args) == 1 + + field_arg = field_args["name"] + assert field_arg.name == "name" + assert field_arg.out_name == "name" + assert field_arg.type is None + + +def test_field_has_arg_after_obj_and_info_args_on_static_method(): + class CustomObject: + @staticmethod + def field_resolver(obj, info, name): + pass + + field_args = get_field_args_from_resolver(CustomObject.field_resolver) + assert len(field_args) == 1 + + field_arg = field_args["name"] + assert field_arg.name == "name" + assert field_arg.out_name == "name" + assert field_arg.type is None diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index 19e867d..aafee26 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -25,6 +25,31 @@ class QueryType(GraphQLObject): assert result.data == {"hello": "Hello World!"} +def test_object_type_with_alias(assert_schema_equals): + class QueryType(GraphQLObject): + __aliases__ = { + "hello": "welcome" + } + + hello: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }", root_value={"welcome": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + def test_object_type_with_field_resolver(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field @@ -271,3 +296,77 @@ def resolve_hello(*_): assert not result.errors assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_description(assert_schema_equals): + class QueryType(GraphQLObject): + __description__ = "Lorem ipsum." + + hello: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + \"\"\"Lorem ipsum.\"\"\" + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_field_decorator_sets_description_for_field(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field(description="Lorem ipsum.") + def hello(obj, info) -> str: + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + \"\"\"Lorem ipsum.\"\"\" + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_description_for_type_hint_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello", description="Lorem ipsum.") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + \"\"\"Lorem ipsum.\"\"\" + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} diff --git a/tests_next/test_object_type_field_args.py b/tests_next/test_object_type_field_args.py new file mode 100644 index 0000000..8480ec9 --- /dev/null +++ b/tests_next/test_object_type_field_args.py @@ -0,0 +1,146 @@ +from graphql import GraphQLResolveInfo, graphql_sync + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema + + +def test_object_type_field_resolver_with_scalar_arg(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field + def hello(obj, info: GraphQLResolveInfo, *, name: str) -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String!): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_object_type_field_instance_with_scalar_arg(assert_schema_equals): + def hello_resolver(*_, name: str) -> str: + return f"Hello {name}!" + + class QueryType(GraphQLObject): + hello = GraphQLObject.field(hello_resolver) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String!): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_resolver_decorator_sets_resolver_with_arg_for_type_hint_field( + assert_schema_equals, +): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + def resolve_hello(*_, name: str): + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String!): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_object_type_field_instance_with_argument_description(assert_schema_equals): + def hello_resolver(*_, name: str) -> str: + return f"Hello {name}!" + + class QueryType(GraphQLObject): + hello = GraphQLObject.field( + hello_resolver, + args={ + "name": GraphQLObject.argument( + description="Lorem ipsum dolor met!", + ), + }, + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum dolor met!\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_object_type_field_instance_with_description(assert_schema_equals): + def hello_resolver(*_, name: str) -> str: + return f"Hello {name}!" + + class QueryType(GraphQLObject): + hello = GraphQLObject.field( + hello_resolver, + args={ + "name": GraphQLObject.argument( + description="Lorem ipsum dolor met!", + ), + }, + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum dolor met!\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index 5316423..d6270df 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -4,7 +4,7 @@ from ariadne_graphql_modules.next import GraphQLObject -def test_scalar_type_validation_fails_for_invalid_type_schema(snapshot): +def test_object_type_validation_fails_for_invalid_type_schema(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -43,3 +43,20 @@ def resolve_hello(*_): return "Hello World!" snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_two_descriptions(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __description__ = "Hello world!" + __schema__ = gql( + """ + \"\"\"Other description\"\"\" + type Query { + hello: String! + } + """ + ) + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_scalar_type_validation.py b/tests_next/test_scalar_type_validation.py index f23b543..14095f0 100644 --- a/tests_next/test_scalar_type_validation.py +++ b/tests_next/test_scalar_type_validation.py @@ -23,3 +23,18 @@ class CustomScalar(GraphQLScalar[str]): __schema__ = gql("scalar Custom") snapshot.assert_match(str(exc_info.value)) + + +def test_scalar_type_validation_fails_for_two_descriptions(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomScalar(GraphQLScalar[str]): + __description__ = "Hello world!" + __schema__ = gql( + """ + \"\"\"Other description\"\"\" + scalar Lorem + """ + ) + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_validators.py b/tests_next/test_validators.py new file mode 100644 index 0000000..9570729 --- /dev/null +++ b/tests_next/test_validators.py @@ -0,0 +1,38 @@ +import pytest +from graphql import parse + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next.validators import validate_description + + +def test_description_validator_passes_type_without_description(): + class CustomType: + pass + + validate_description(CustomType, parse("scalar Custom").definitions[0]) + + +def test_description_validator_passes_type_with_description_attr(): + class CustomType: + __description__ = "Example scalar" + + validate_description(CustomType, parse("scalar Custom").definitions[0]) + + +def test_description_validator_raises_error_for_type_with_two_descriptions(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType: + __description__ = "Example scalar" + + validate_description( + CustomType, + parse( + """ + \"\"\"Lorem ipsum\"\"\" + scalar Custom + """ + ).definitions[0], + ) + + snapshot.assert_match(str(exc_info.value)) From 32ba3606cb443a892775c687b4efe9fd6c0da2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Mon, 11 Sep 2023 14:10:40 +0200 Subject: [PATCH 06/63] Field args names and outnames --- ariadne_graphql_modules/next/objecttype.py | 42 ++++++- tests_next/test_object_type.py | 4 +- tests_next/test_object_type_field_args.py | 126 ++++++++++++++++++--- 3 files changed, 150 insertions(+), 22 deletions(-) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 152fc18..2ecfc5a 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -15,6 +15,8 @@ from ariadne.types import Resolver from graphql import ( FieldDefinitionNode, + GraphQLField, + GraphQLObjectType, GraphQLSchema, InputValueDefinitionNode, NameNode, @@ -122,6 +124,13 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": if hint_name in fields_resolvers: resolvers[hint_field_name] = fields_resolvers[hint_name] field_args = get_field_args_from_resolver(resolvers[hint_field_name]) + if field_args: + if fields_args_options.get(hint_field_name): + update_args_options( + field_args, fields_args_options[hint_field_name] + ) + + out_names[hint_field_name] = get_field_args_out_names(field_args) else: field_args = {} @@ -161,12 +170,15 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": resolvers[field_instance.name] = field_resolver field_args = get_field_args_from_resolver(field_resolver) if field_instance.name in fields_args_options: - update_resolver_args( + update_args_options( field_args, fields_args_options[field_instance.name] ) fields_ast.append(get_field_node_from_obj_field(field_instance, field_args)) + if field_args: + out_names[field_instance.name] = get_field_args_out_names(field_args) + object_description = getattr(cls, "__description__", None) if object_description: description = StringValueNode(value=object_description) @@ -241,10 +253,13 @@ def resolver( @staticmethod def argument( + name: Optional[str] = None, description: Optional[str] = None, type: Optional[Any] = None, ) -> dict: options: dict = {} + if name: + options["name"] = name if description: options["description"] = description if type: @@ -444,6 +459,12 @@ def bind_to_schema(self, schema: GraphQLSchema): bindable.bind_to_schema(schema) + graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) + for field_name, field_out_names in self.out_names.items(): + graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) + for arg_name, out_name in field_out_names.items(): + graphql_field.args[arg_name].out_name = out_name + def get_field_node_from_type_hint( field_name: str, @@ -460,7 +481,7 @@ def get_field_node_from_type_hint( description=description, name=NameNode(value=field_name), type=get_type_node(field_type), - arguments=get_field_args_nodes_from_obj_field_arg(field_args), + arguments=get_field_args_nodes_from_obj_field_args(field_args), ) @@ -477,7 +498,7 @@ def get_field_node_from_obj_field( description=description, name=NameNode(value=field.name), type=get_type_node(field.type), - arguments=get_field_args_nodes_from_obj_field_arg(field_args), + arguments=get_field_args_nodes_from_obj_field_args(field_args), ) @@ -539,7 +560,16 @@ def get_field_args_from_resolver( return field_args -def get_field_args_nodes_from_obj_field_arg( +def get_field_args_out_names( + field_args: Dict[str, GraphQLObjectFieldArg] +) -> Dict[str, str]: + out_names: Dict[str, str] = {} + for field_arg in field_args.values(): + out_names[field_arg.name] = field_arg.out_name + return out_names + + +def get_field_args_nodes_from_obj_field_args( field_args: Dict[str, GraphQLObjectFieldArg] ) -> Tuple[InputValueDefinitionNode]: return tuple( @@ -563,7 +593,7 @@ def get_field_arg_node_from_obj_field_arg( ) -def update_resolver_args( +def update_args_options( resolver_args: Dict[str, GraphQLObjectFieldArg], args_options: Optional[Dict[str, dict]], ): @@ -572,6 +602,8 @@ def update_resolver_args( for arg_name, arg_options in args_options.items(): resolver_arg = resolver_args[arg_name] + if arg_options.get("name"): + resolver_arg.name = arg_options["name"] if arg_options.get("description"): resolver_arg.description = arg_options["description"] if arg_options.get("type"): diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index aafee26..7b9602f 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -27,9 +27,7 @@ class QueryType(GraphQLObject): def test_object_type_with_alias(assert_schema_equals): class QueryType(GraphQLObject): - __aliases__ = { - "hello": "welcome" - } + __aliases__ = {"hello": "welcome"} hello: str diff --git a/tests_next/test_object_type_field_args.py b/tests_next/test_object_type_field_args.py index 8480ec9..3b81621 100644 --- a/tests_next/test_object_type_field_args.py +++ b/tests_next/test_object_type_field_args.py @@ -51,6 +51,40 @@ class QueryType(GraphQLObject): assert result.data == {"hello": "Hello Bob!"} +def test_object_type_field_instance_with_description(assert_schema_equals): + def hello_resolver(*_, name: str) -> str: + return f"Hello {name}!" + + class QueryType(GraphQLObject): + hello = GraphQLObject.field( + hello_resolver, + args={ + "name": GraphQLObject.argument( + description="Lorem ipsum dolor met!", + ), + }, + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum dolor met!\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + def test_resolver_decorator_sets_resolver_with_arg_for_type_hint_field( assert_schema_equals, ): @@ -112,19 +146,63 @@ class QueryType(GraphQLObject): assert result.data == {"hello": "Hello Bob!"} -def test_object_type_field_instance_with_description(assert_schema_equals): - def hello_resolver(*_, name: str) -> str: - return f"Hello {name}!" +def test_object_type_field_resolver_instance_arg_with_out_name(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + def resolve_hello(*_, first_name: str) -> str: + return f"Hello {first_name}!" + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(firstName: String!): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(firstName: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_object_type_field_instance_arg_with_out_name(assert_schema_equals): class QueryType(GraphQLObject): - hello = GraphQLObject.field( - hello_resolver, - args={ - "name": GraphQLObject.argument( - description="Lorem ipsum dolor met!", - ), - }, + @GraphQLObject.field + def hello(*_, first_name: str) -> str: + return f"Hello {first_name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(firstName: String!): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(firstName: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_object_type_field_resolver_instance_arg_with_custom_name(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver( + "hello", args={"first_name": GraphQLObject.argument(name="name")} ) + def resolve_hello(*_, first_name: str) -> str: + return f"Hello {first_name}!" schema = make_executable_schema(QueryType) @@ -132,10 +210,30 @@ class QueryType(GraphQLObject): schema, """ type Query { - hello( - \"\"\"Lorem ipsum dolor met!\"\"\" - name: String! - ): String! + hello(name: String!): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_object_type_field_instance_arg_with_custom_name(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field(args={"first_name": GraphQLObject.argument(name="name")}) + def hello(*_, first_name: str) -> str: + return f"Hello {first_name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String!): String! } """, ) From 372428fca8b30e4add16df680c0089daad2e50bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Mon, 11 Sep 2023 14:58:38 +0200 Subject: [PATCH 07/63] Validate graphql name matches schema contents --- ariadne_graphql_modules/next/base.py | 9 ------ ariadne_graphql_modules/next/objecttype.py | 7 +++-- ariadne_graphql_modules/next/scalartype.py | 12 ++------ ariadne_graphql_modules/next/validators.py | 13 ++++++++ .../snap_test_scalar_type_validation.py | 2 +- tests_next/snapshots/snap_test_validators.py | 2 ++ tests_next/test_validators.py | 30 ++++++++++++++++++- 7 files changed, 51 insertions(+), 24 deletions(-) diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index e6f2c8a..7baa2cc 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -13,9 +13,6 @@ def __get_graphql_name__(cls) -> "str": if getattr(cls, "__graphql_name__", None): return cls.__graphql_name__ - if getattr(cls, "__schema__", None): - return cls.__get_graphql_name_from_schema__() - name = cls.__name__ if name.endswith("GraphQLType"): # 'UserGraphQLType' will produce the 'User' name @@ -35,12 +32,6 @@ def __get_graphql_name__(cls) -> "str": return name - @classmethod - def __get_graphql_name_from_schema__(cls) -> "str": - # Todo: cache this in future... - document = parse(cls.__schema__) - return document.definitions[0].name.value - @classmethod def __get_graphql_model__(cls) -> "GraphQLModel": raise NotImplementedError( diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 2ecfc5a..61ea6a1 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -28,7 +28,7 @@ from .base import GraphQLModel, GraphQLType from .convert_name import convert_python_name_to_graphql from .typing import get_graphql_type, get_type_node -from .validators import validate_description +from .validators import validate_description, validate_name class GraphQLObject(GraphQLType): @@ -301,6 +301,9 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]): f"'{ObjectTypeDefinitionNode.__name__}')" ) + validate_name(cls, definition) + validate_description(cls, definition) + field_names: List[str] = [f.name.value for f in definition.fields] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -312,8 +315,6 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]): f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" ) - validate_description(cls, definition) - class GraphQLObjectField: name: Optional[str] diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/scalartype.py index a2ade94..2977077 100644 --- a/ariadne_graphql_modules/next/scalartype.py +++ b/ariadne_graphql_modules/next/scalartype.py @@ -5,14 +5,13 @@ GraphQLSchema, NameNode, ScalarTypeDefinitionNode, - StringValueNode, ValueNode, value_from_ast_untyped, ) from ..utils import parse_definition from .base import GraphQLModel, GraphQLType -from .validators import validate_description +from .validators import validate_description, validate_name T = TypeVar("T") @@ -104,14 +103,7 @@ def validate_scalar_type_with_schema(cls: Type[GraphQLScalar]): f"'{ScalarTypeDefinitionNode.__name__}')" ) - graphql_name = getattr(cls, "__graphql_name__", None) - if graphql_name and definition.name.value != definition: - raise ValueError( - f"Class '{cls.__name__}' defines both '__graphql_name__' and " - f"'__schema__' attributes, but scalar's names in those don't match. " - f"('{graphql_name}' != '{definition.name.value}')" - ) - + validate_name(cls, definition) validate_description(cls, definition) diff --git a/ariadne_graphql_modules/next/validators.py b/ariadne_graphql_modules/next/validators.py index 374023c..a0332c1 100644 --- a/ariadne_graphql_modules/next/validators.py +++ b/ariadne_graphql_modules/next/validators.py @@ -3,6 +3,19 @@ from .objecttype import GraphQLType +def validate_name(cls: Type[GraphQLType], definition: Any): + graphql_name = getattr(cls, "__graphql_name__", None) + + if graphql_name and definition.name.value != graphql_name: + raise ValueError( + f"Class '{cls.__name__}' defines both '__graphql_name__' and " + f"'__schema__' attributes, but names in those don't match. " + f"('{graphql_name}' != '{definition.name.value}')" + ) + + setattr(cls, "__graphql_name__", definition.name.value) + + def validate_description(cls: Type[GraphQLType], definition: Any): if getattr(cls, "__description__", None) and definition.description: raise ValueError( diff --git a/tests_next/snapshots/snap_test_scalar_type_validation.py b/tests_next/snapshots/snap_test_scalar_type_validation.py index 2960d8d..db1f2a2 100644 --- a/tests_next/snapshots/snap_test_scalar_type_validation.py +++ b/tests_next/snapshots/snap_test_scalar_type_validation.py @@ -9,6 +9,6 @@ snapshots['test_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode')" -snapshots['test_scalar_type_validation_fails_for_name_mismatch_between_schema_and_attr 1'] = "Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, but scalar's names in those don't match. ('Date' != 'Custom')" +snapshots['test_scalar_type_validation_fails_for_name_mismatch_between_schema_and_attr 1'] = "Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Date' != 'Custom')" snapshots['test_scalar_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomScalar' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_validators.py b/tests_next/snapshots/snap_test_validators.py index 057e09c..ab766f3 100644 --- a/tests_next/snapshots/snap_test_validators.py +++ b/tests_next/snapshots/snap_test_validators.py @@ -8,3 +8,5 @@ snapshots = Snapshot() snapshots['test_description_validator_raises_error_for_type_with_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." + +snapshots['test_name_validator_raises_error_for_name_and_definition_mismatch 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Example' != 'Custom')" diff --git a/tests_next/test_validators.py b/tests_next/test_validators.py index 9570729..c39ce69 100644 --- a/tests_next/test_validators.py +++ b/tests_next/test_validators.py @@ -2,7 +2,7 @@ from graphql import parse from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next.validators import validate_description +from ariadne_graphql_modules.next.validators import validate_description, validate_name def test_description_validator_passes_type_without_description(): @@ -36,3 +36,31 @@ class CustomType: ) snapshot.assert_match(str(exc_info.value)) + + +def test_name_validator_passes_type_without_explicit_name(): + class CustomType: + pass + + validate_name(CustomType, parse("type Custom").definitions[0]) + + +def test_name_validator_passes_type_with_graphql_name_attr_matching_definition(): + class CustomType: + __graphql_name__ = "Custom" + + validate_name(CustomType, parse("type Custom").definitions[0]) + + +def test_name_validator_raises_error_for_name_and_definition_mismatch(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType: + __graphql_name__ = "Example" + + validate_name( + CustomType, + parse("type Custom").definitions[0], + ) + + snapshot.assert_match(str(exc_info.value)) From 2bb1f4d46f0ce37969d8e8997d97fb850c3aa53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Tue, 12 Sep 2023 15:14:57 +0200 Subject: [PATCH 08/63] WIP deferred fields types --- ariadne_graphql_modules/next/__init__.py | 2 ++ ariadne_graphql_modules/next/deferredtype.py | 24 +++++++++++++++ ariadne_graphql_modules/next/objecttype.py | 11 +++---- ariadne_graphql_modules/next/typing.py | 32 ++++++++++++++++++-- tests_next/test_deferred_type.py | 15 +++++++++ tests_next/test_typing.py | 28 +++++++++++++++-- tests_next/types.py | 5 +++ 7 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 ariadne_graphql_modules/next/deferredtype.py create mode 100644 tests_next/test_deferred_type.py create mode 100644 tests_next/types.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 98107eb..229ec19 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -1,4 +1,5 @@ from .base import GraphQLModel, GraphQLType +from .deferredtype import deferred from .executable_schema import make_executable_schema from .objecttype import GraphQLObject, GraphQLObjectModel, object_field from .scalartype import GraphQLScalar, GraphQScalarModel @@ -10,6 +11,7 @@ "GraphQLScalar", "GraphQScalarModel", "GraphQLType", + "deferred", "make_executable_schema", "object_field", ] diff --git a/ariadne_graphql_modules/next/deferredtype.py b/ariadne_graphql_modules/next/deferredtype.py new file mode 100644 index 0000000..95845db --- /dev/null +++ b/ariadne_graphql_modules/next/deferredtype.py @@ -0,0 +1,24 @@ +import sys +from dataclasses import dataclass +from typing import cast + + +@dataclass(frozen=True) +class DeferredType: + path: str + + +def deferred(module_path: str) -> DeferredType: + if not module_path.startswith("."): + return DeferredType(module_path) + + frame = sys._getframe(2) + if not frame: + raise RuntimeError( + "'deferred' can't be called outside of class's attribute's " + "definition context." + ) + + # TODO: Support up a level traversal for multiple levels + package = cast(str, frame.f_globals["__package__"]) + return DeferredType(f"{package}{module_path}") diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 61ea6a1..7903cb0 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -8,7 +8,6 @@ Tuple, Type, cast, - get_type_hints, ) from ariadne import ObjectType as ObjectTypeBindable @@ -87,7 +86,7 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": @classmethod def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": - type_hints = get_type_hints(cls) + type_hints = cls.__annotations__ fields_args_options: Dict[str, dict] = {} fields_descriptions: Dict[str, str] = {} fields_resolvers: Dict[str, Resolver] = {} @@ -210,7 +209,7 @@ def __get_graphql_types__(cls) -> Iterable["GraphQLType"]: if field_graphql_type and field_graphql_type not in types: types.append(field_graphql_type) - type_hints = get_type_hints(cls) + type_hints = cls.__annotations__ for hint_type in type_hints.values(): hint_graphql_type = get_graphql_type(hint_type) if hint_graphql_type and hint_graphql_type not in types: @@ -269,7 +268,7 @@ def argument( def validate_object_type(cls: Type[GraphQLObject]): attrs_names: List[str] = [ - attr_name for attr_name in get_type_hints(cls) if not attr_name.startswith("__") + attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") ] resolvers_names: List[str] = [] @@ -374,7 +373,7 @@ def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: def get_field_type_from_resolver(resolver: Resolver) -> Any: - return get_type_hints(resolver).get("return") + return resolver.__annotations__.get("return") class GraphQLObjectResolver: @@ -527,7 +526,7 @@ def get_field_args_from_resolver( resolver: Resolver, ) -> Dict[str, GraphQLObjectFieldArg]: resolver_signature = signature(resolver) - type_hints = get_type_hints(resolver) + type_hints = resolver.__annotations__ type_hints.pop("return", None) field_args: Dict[str, GraphQLObjectFieldArg] = {} diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index abf167d..5990c5b 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -1,6 +1,7 @@ +from importlib import import_module from inspect import isclass from types import UnionType -from typing import Any, Optional, Union, get_args, get_origin +from typing import Annotated, Any, ForwardRef, Optional, Union, get_args, get_origin from graphql import ( ListTypeNode, @@ -11,6 +12,7 @@ ) from .base import GraphQLType +from .deferredtype import DeferredType def get_type_node(type_hint: Any) -> TypeNode: @@ -31,12 +33,25 @@ def get_type_node(type_hint: Any) -> TypeNode: type_node = NamedTypeNode(name=NameNode(value="Float")) elif type_hint == bool: type_node = NamedTypeNode(name=NameNode(value="Boolean")) + elif get_origin(type_hint) is Annotated: + forward_ref, type_meta = get_args(type_hint) + if not type_meta or not isinstance(type_meta, DeferredType): + raise ValueError( + f"Can't create a GraphQL return type for '{type_hint}'. " + "Second argument of 'Annotated' is expected to be a return " + "value from the 'deferred()' utility." + ) + + deferred_type = get_deferred_type(type_hint, forward_ref, type_meta) + type_node = NamedTypeNode( + name=NameNode(value=deferred_type.__get_graphql_name__()), + ) elif isclass(type_hint) and issubclass(type_hint, GraphQLType): type_node = NamedTypeNode( name=NameNode(value=type_hint.__get_graphql_name__()), ) else: - raise ValueError(f"Can't create a GraphQL return type for '{type_hint}'") + raise ValueError(f"Can't create a GraphQL return type for '{type_hint}'.") if nullable: return type_node @@ -68,6 +83,19 @@ def unwrap_type(type_hint: Any) -> Any: return args[0] +def get_deferred_type( + type_hint: Any, forward_ref: ForwardRef, deferred_type: DeferredType +) -> Optional[GraphQLType]: + type_name = forward_ref.__forward_arg__ + module = import_module(deferred_type.path) + graphql_type = getattr(module, type_name) + + if not isclass(graphql_type) or not issubclass(graphql_type, GraphQLType): + raise ValueError(f"Can't create a GraphQL return type for '{type_hint}'.") + + return graphql_type + + def get_graphql_type(type_hint: Any) -> Optional[GraphQLType]: if not isclass(type_hint): return None diff --git a/tests_next/test_deferred_type.py b/tests_next/test_deferred_type.py new file mode 100644 index 0000000..2053a1a --- /dev/null +++ b/tests_next/test_deferred_type.py @@ -0,0 +1,15 @@ +import pytest + +from ariadne_graphql_modules.next import deferred + + +def test_deferred_returns_deferred_type_with_abs_path(): + deferred_type = deferred("tests_next.types") + assert deferred_type.path == "tests_next.types" + + +def test_deferred_returns_deferred_type_with_relative_path(): + class MockType: + deferred_type = deferred(".types") + + assert MockType.deferred_type.path == "tests_next.types" diff --git a/tests_next/test_typing.py b/tests_next/test_typing.py index a2a8726..c6056aa 100644 --- a/tests_next/test_typing.py +++ b/tests_next/test_typing.py @@ -1,10 +1,13 @@ -from typing import List, Optional, Union +from typing import TYPE_CHECKING, Annotated, List, Optional, Union, get_type_hints from graphql import ListTypeNode, NameNode, NamedTypeNode, NonNullTypeNode -from ariadne_graphql_modules.next import GraphQLObject +from ariadne_graphql_modules.next import GraphQLObject, deferred from ariadne_graphql_modules.next.typing import get_type_node +if TYPE_CHECKING: + from .types import ForwardScalar + def assert_non_null_type(type_node, name: str): assert isinstance(type_node, NonNullTypeNode) @@ -64,3 +67,24 @@ def test_get_non_null_graphql_list_type_node_from_python_builtin_type(): assert_non_null_list_type(get_type_node(List[int]), "Int") assert_non_null_list_type(get_type_node(List[float]), "Float") assert_non_null_list_type(get_type_node(List[bool]), "Boolean") + + +def test_get_graphql_type_node_from_annotated_type(): + class MockType(GraphQLObject): + field: Annotated["ForwardScalar", deferred("tests_next.types")] + + assert_non_null_type(get_type_node(MockType.__annotations__["field"]), "Forward") + + +def test_get_graphql_type_node_from_annotated_type_with_relative_path(): + class MockType(GraphQLObject): + field: Annotated["ForwardScalar", deferred(".types")] + + assert_non_null_type(get_type_node(MockType.__annotations__["field"]), "Forward") + + +def test_get_graphql_type_node_from_nullable_annotated_type(): + class MockType(GraphQLObject): + field: Optional[Annotated["ForwardScalar", deferred("tests_next.types")]] + + assert_named_type(get_type_node(MockType.__annotations__["field"]), "Forward") diff --git a/tests_next/types.py b/tests_next/types.py new file mode 100644 index 0000000..fbd549b --- /dev/null +++ b/tests_next/types.py @@ -0,0 +1,5 @@ +from ariadne_graphql_modules.next import GraphQLScalar + + +class ForwardScalar(GraphQLScalar): + __schema__ = "scalar Forward" From 64ecdda1d55d3313a6db3381ee4477c52196a3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Tue, 12 Sep 2023 15:42:19 +0200 Subject: [PATCH 09/63] Deferred type tests --- ariadne_graphql_modules/next/deferredtype.py | 17 +++++++++-- .../snapshots/snap_test_deferred_type.py | 10 +++++++ tests_next/test_deferred_type.py | 30 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests_next/snapshots/snap_test_deferred_type.py diff --git a/ariadne_graphql_modules/next/deferredtype.py b/ariadne_graphql_modules/next/deferredtype.py index 95845db..87c5c36 100644 --- a/ariadne_graphql_modules/next/deferredtype.py +++ b/ariadne_graphql_modules/next/deferredtype.py @@ -19,6 +19,17 @@ def deferred(module_path: str) -> DeferredType: "definition context." ) - # TODO: Support up a level traversal for multiple levels - package = cast(str, frame.f_globals["__package__"]) - return DeferredType(f"{package}{module_path}") + module_path_suffix = module_path[1:] # Remove initial dot + current_package = cast(str, frame.f_globals["__package__"]) + packages = current_package.split(".") + + while module_path_suffix.startswith(".") and packages: + module_path_suffix = module_path_suffix[1:] # Remove dot + packages = packages[:-1] + if not packages: + raise ValueError( + f"'{module_path}' points outside of the '{current_package}' package." + ) + + package = ".".join(packages) + return DeferredType(f"{package}.{module_path_suffix}") diff --git a/tests_next/snapshots/snap_test_deferred_type.py b/tests_next/snapshots/snap_test_deferred_type.py new file mode 100644 index 0000000..a3d83e4 --- /dev/null +++ b/tests_next/snapshots/snap_test_deferred_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_deferred_raises_error_for_invalid_relative_path 1'] = "'...types' points outside of the 'lorem' package." diff --git a/tests_next/test_deferred_type.py b/tests_next/test_deferred_type.py index 2053a1a..874aa99 100644 --- a/tests_next/test_deferred_type.py +++ b/tests_next/test_deferred_type.py @@ -1,3 +1,5 @@ +from unittest.mock import Mock + import pytest from ariadne_graphql_modules.next import deferred @@ -13,3 +15,31 @@ class MockType: deferred_type = deferred(".types") assert MockType.deferred_type.path == "tests_next.types" + + +def test_deferred_returns_deferred_type_with_higher_level_relative_path(monkeypatch): + frame_mock = Mock(f_globals={"__package__": "lorem.ipsum"}) + monkeypatch.setattr( + "ariadne_graphql_modules.next.deferredtype.sys._getframe", + Mock(return_value=frame_mock), + ) + + class MockType: + deferred_type = deferred("..types") + + assert MockType.deferred_type.path == "lorem.types" + + +def test_deferred_raises_error_for_invalid_relative_path(monkeypatch, snapshot): + frame_mock = Mock(f_globals={"__package__": "lorem"}) + monkeypatch.setattr( + "ariadne_graphql_modules.next.deferredtype.sys._getframe", + Mock(return_value=frame_mock), + ) + + with pytest.raises(ValueError) as exc_info: + + class MockType: + deferred_type = deferred("...types") + + snapshot.assert_match(str(exc_info.value)) From 83046b45a44e60f42fcd558b32892ff726e091f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Wed, 20 Sep 2023 18:07:58 +0200 Subject: [PATCH 10/63] Enums support --- ariadne_graphql_modules/next/__init__.py | 9 +- ariadne_graphql_modules/next/base.py | 35 +- ariadne_graphql_modules/next/description.py | 13 + ariadne_graphql_modules/next/enumtype.py | 120 +++++++ .../next/executable_schema.py | 10 +- ariadne_graphql_modules/next/objecttype.py | 196 +++++------ ariadne_graphql_modules/next/scalartype.py | 42 +-- ariadne_graphql_modules/next/typing.py | 25 +- tests_next/conftest.py | 18 +- .../snap_test_object_type_validation.py | 10 + .../snapshots/snap_test_standard_enum.py | 18 ++ tests_next/test_description_node.py | 35 ++ tests_next/test_metadata.py | 86 +++++ tests_next/test_object_type_validation.py | 62 ++++ tests_next/test_standard_enum.py | 306 ++++++++++++++++++ tests_next/test_typing.py | 102 ++++-- tests_next/types.py | 7 + 17 files changed, 925 insertions(+), 169 deletions(-) create mode 100644 ariadne_graphql_modules/next/description.py create mode 100644 ariadne_graphql_modules/next/enumtype.py create mode 100644 tests_next/snapshots/snap_test_standard_enum.py create mode 100644 tests_next/test_description_node.py create mode 100644 tests_next/test_metadata.py create mode 100644 tests_next/test_standard_enum.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 229ec19..b18858c 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -1,17 +1,24 @@ -from .base import GraphQLModel, GraphQLType +from .base import GraphQLMetadata, GraphQLModel, GraphQLType from .deferredtype import deferred +from .description import get_description_node +from .enumtype import GraphQLEnumModel, create_graphql_enum_model, graphql_enum from .executable_schema import make_executable_schema from .objecttype import GraphQLObject, GraphQLObjectModel, object_field from .scalartype import GraphQLScalar, GraphQScalarModel __all__ = [ + "GraphQLEnumModel", + "GraphQLMetadata", "GraphQLModel", "GraphQLObject", "GraphQLObjectModel", "GraphQLScalar", "GraphQScalarModel", "GraphQLType", + "create_graphql_enum_model", "deferred", + "get_description_node", + "graphql_enum", "make_executable_schema", "object_field", ] diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 7baa2cc..481932e 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -1,6 +1,8 @@ -from typing import Iterable, Optional, Type +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Iterable, Optional, Type, Union -from graphql import GraphQLSchema, TypeDefinitionNode, parse +from graphql import GraphQLSchema, TypeDefinitionNode class GraphQLType: @@ -33,7 +35,7 @@ def __get_graphql_name__(cls) -> "str": return name @classmethod - def __get_graphql_model__(cls) -> "GraphQLModel": + def __get_graphql_model__(cls, metadata: "Metadata") -> "GraphQLModel": raise NotImplementedError( "Subclasses of 'GraphQLType' must define '__get_graphql_model__'" ) @@ -44,6 +46,7 @@ def __get_graphql_types__(cls) -> Iterable["GraphQLType"]: return [cls] +@dataclass(frozen=True) class GraphQLModel: name: str ast: TypeDefinitionNode @@ -55,3 +58,29 @@ def __init__(self, name: str, ast: TypeDefinitionNode): def bind_to_schema(self, schema: GraphQLSchema): pass + + +@dataclass(frozen=True) +class GraphQLMetadata: + models: Dict[Union[Type[GraphQLType], Type[Enum]], GraphQLModel] = field( + default_factory=dict + ) + + def get_graphql_model( + self, type: Union[Type[GraphQLType], Type[Enum]] + ) -> GraphQLModel: + if type not in self.models: + if hasattr(type, "__get_graphql_model__"): + self.models[type] = type.__get_graphql_model__(self) + elif issubclass(type, Enum): + from .enumtype import create_graphql_enum_model + + self.models[type] = create_graphql_enum_model(type) + else: + raise ValueError(f"Can't retrieve GraphQL model for '{type}'.") + + return self.models[type] + + def get_graphql_name(self, type: Union[Type[GraphQLType], Type[Enum]]) -> str: + model = self.get_graphql_model(type) + return model.name diff --git a/ariadne_graphql_modules/next/description.py b/ariadne_graphql_modules/next/description.py new file mode 100644 index 0000000..ff7a8ba --- /dev/null +++ b/ariadne_graphql_modules/next/description.py @@ -0,0 +1,13 @@ +from textwrap import dedent +from typing import Optional + +from graphql import StringValueNode + + +def get_description_node(description: Optional[str]) -> Optional[StringValueNode]: + if not description: + return None + + return StringValueNode( + value=dedent(description).strip(), block="\n" in description.strip() + ) diff --git a/ariadne_graphql_modules/next/enumtype.py b/ariadne_graphql_modules/next/enumtype.py new file mode 100644 index 0000000..b45f52d --- /dev/null +++ b/ariadne_graphql_modules/next/enumtype.py @@ -0,0 +1,120 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, Iterable, Optional, Type, cast + +from ariadne import EnumType +from graphql import ( + EnumTypeDefinitionNode, + EnumValueDefinitionNode, + GraphQLSchema, + NameNode, +) + +from .base import GraphQLModel +from .description import get_description_node + + +def create_graphql_enum_model( + enum: Type[Enum], + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[Dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +) -> "GraphQLEnumModel": + if members_include and members_exclude: + raise ValueError( + "'members_include' and 'members_exclude' " "options are mutually exclusive." + ) + + if hasattr(enum, "__get_graphql_model__"): + return cast(GraphQLEnumModel, enum.__get_graphql_model__()) + + if not name: + if hasattr(enum, "__get_graphql_name__"): + name = cast("str", enum.__get_graphql_name__()) + else: + name = enum.__name__ + + members: Dict[str, Any] = {i.name: i for i in enum} + final_members: Dict[str, Any] = {} + + if members_include: + for key, value in members.items(): + if key in members_include: + final_members[key] = value + elif members_exclude: + for key, value in members.items(): + if key not in members_exclude: + final_members[key] = value + else: + final_members = members + + members_descriptions = members_descriptions or {} + for member in members_descriptions: + if member not in final_members: + raise ValueError( + f"Member description was specified for a member '{member}' " + "not present in final GraphQL enum." + ) + + return GraphQLEnumModel( + name=name, + members=final_members, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node(description), + values=tuple( + EnumValueDefinitionNode( + name=NameNode(value=value_name), + description=get_description_node( + members_descriptions.get(value_name) + ), + ) + for value_name in final_members + ), + ), + ) + + +@dataclass(frozen=True) +class GraphQLEnumModel(GraphQLModel): + members: Dict[str, Any] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = EnumType(self.name, values=self.members) + bindable.bind_to_schema(schema) + + +def graphql_enum( + cls: Optional[Type[Enum]] = None, + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[Dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +): + def graphql_enum_decorator(cls: Type[Enum]): + graphql_model = create_graphql_enum_model( + cls, + name=name, + description=description, + members_descriptions=members_descriptions, + members_include=members_include, + members_exclude=members_exclude, + ) + + @classmethod + def __get_graphql_model__(*_) -> GraphQLEnumModel: + return graphql_model + + setattr(cls, "__get_graphql_model__", __get_graphql_model__) + return cls + + if cls: + return graphql_enum_decorator(cls) + + return graphql_enum_decorator diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index c510286..f37f89d 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -9,7 +9,8 @@ build_ast_schema, ) -from .base import GraphQLModel, GraphQLType +from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .enumtype import GraphQLEnumModel from .objecttype import GraphQLObjectModel from .scalartype import GraphQScalarModel @@ -26,6 +27,7 @@ def make_executable_schema( directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, convert_names_case: Union[bool, SchemaNameConverter] = False, ) -> GraphQLSchema: + metadata = GraphQLMetadata() type_defs: List[str] = find_type_defs(types) types_list: List[SchemaType] = flatten_types(types) @@ -33,7 +35,7 @@ def make_executable_schema( assert_types_not_abstract(types_list) schema_models: List[GraphQLModel] = sort_models( - [type_def.__get_graphql_model__() for type_def in types_list] + [metadata.get_graphql_model(type_def) for type_def in types_list] ) document_node = DocumentNode( @@ -79,6 +81,9 @@ def flatten_types( types_list.append(type_def) + if issubclass(type_def, Enum): + types_list.append(type_def) + if isinstance(type_def, list): types_list += find_type_defs(type_def) @@ -146,6 +151,7 @@ def assert_types_not_abstract(type_defs: List[SchemaType]): def sort_models(schema_models: List[GraphQLModel]) -> List[GraphQLModel]: sorted_models: List[GraphQLModel] = [] + sorted_models += sort_models_by_type(GraphQLEnumModel, schema_models) sorted_models += sort_models_by_type(GraphQScalarModel, schema_models) sorted_models += [ model for model in schema_models if isinstance(model, GraphQLObjectModel) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 7903cb0..a29d668 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, replace from inspect import signature from typing import ( Any, @@ -20,12 +21,12 @@ InputValueDefinitionNode, NameNode, ObjectTypeDefinitionNode, - StringValueNode, ) from ..utils import parse_definition -from .base import GraphQLModel, GraphQLType +from .base import GraphQLMetadata, GraphQLModel, GraphQLType from .convert_name import convert_python_name_to_graphql +from .description import get_description_node from .typing import get_graphql_type, get_type_node from .validators import validate_description, validate_name @@ -50,16 +51,18 @@ def __init_subclass__(cls) -> None: validate_object_type(cls) @classmethod - def __get_graphql_model__(cls) -> "GraphQLModel": + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(name) + return cls.__get_graphql_model_with_schema__(metadata, name) - return cls.__get_graphql_model_without_schema__(name) + return cls.__get_graphql_model_without_schema__(metadata, name) @classmethod - def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": + def __get_graphql_model_with_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": definition = cast( ObjectTypeDefinitionNode, parse_definition(ObjectTypeDefinitionNode, cls.__schema__), @@ -75,6 +78,7 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": return GraphQLObjectModel( name=definition.name.value, + ast_type=ObjectTypeDefinitionNode, ast=ObjectTypeDefinitionNode( name=NameNode(value=definition.name.value), fields=definition.fields, @@ -85,7 +89,9 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": ) @classmethod - def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": type_hints = cls.__annotations__ fields_args_options: Dict[str, dict] = {} fields_descriptions: Dict[str, str] = {} @@ -125,7 +131,7 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": field_args = get_field_args_from_resolver(resolvers[hint_field_name]) if field_args: if fields_args_options.get(hint_field_name): - update_args_options( + field_args = update_field_args_options( field_args, fields_args_options[hint_field_name] ) @@ -135,6 +141,7 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": fields_ast.append( get_field_node_from_type_hint( + metadata, hint_field_name, hint_type, field_args, @@ -169,26 +176,25 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": resolvers[field_instance.name] = field_resolver field_args = get_field_args_from_resolver(field_resolver) if field_instance.name in fields_args_options: - update_args_options( + field_args = update_field_args_options( field_args, fields_args_options[field_instance.name] ) - fields_ast.append(get_field_node_from_obj_field(field_instance, field_args)) + fields_ast.append( + get_field_node_from_obj_field(metadata, field_instance, field_args) + ) if field_args: out_names[field_instance.name] = get_field_args_out_names(field_args) - object_description = getattr(cls, "__description__", None) - if object_description: - description = StringValueNode(value=object_description) - else: - description = None - return GraphQLObjectModel( name=name, + ast_type=ObjectTypeDefinitionNode, ast=ObjectTypeDefinitionNode( name=NameNode(value=name), - description=description, + description=get_description_node( + getattr(cls, "__description__", None), + ), fields=tuple(fields_ast), ), resolvers=resolvers, @@ -288,6 +294,35 @@ def validate_object_type(cls: Type[GraphQLObject]): f"attribute '{resolver_for}'. (Valid attrs: '{valid_fields}')" ) + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, (GraphQLObjectField, GraphQLObjectResolver)): + if not cls_attr.resolver or not cls_attr.args: + continue + + resolver_args = get_field_args_names_from_resolver(cls_attr.resolver) + if resolver_args: + error_help = "expected one of: '%s'" % ("', '".join(resolver_args)) + else: + error_help = "function accepts no extra arguments" + + for arg_name in cls_attr.args: + if arg_name not in resolver_args: + if isinstance(cls_attr, GraphQLObjectField): + raise ValueError( + f"Class '{cls.__name__}' defines '{attr_name}' field " + f"with extra configuration for '{arg_name}' argument " + "thats not defined on the resolver function. " + f"({error_help})" + ) + else: + raise ValueError( + f"Class '{cls.__name__}' defines '{attr_name}' resolver " + f"with extra configuration for '{arg_name}' argument" + "thats not defined on the resolver function. " + f"({error_help})" + ) + def validate_object_type_with_schema(cls: Type[GraphQLObject]): definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) @@ -376,27 +411,13 @@ def get_field_type_from_resolver(resolver: Resolver) -> Any: return resolver.__annotations__.get("return") +@dataclass(frozen=True) class GraphQLObjectResolver: resolver: Resolver field: str - description: Optional[str] - args: Optional[Dict[str, dict]] - type: Optional[Any] - - def __init__( - self, - resolver: Resolver, - field: str, - *, - description: Optional[str] = None, - args: Optional[Dict[str, dict]] = None, - type: Optional[Any] = None, - ): - self.args = args - self.description = description - self.resolver = resolver - self.field = field - self.type = type + description: Optional[str] = None + args: Optional[Dict[str, dict]] = None + type: Optional[Any] = None def object_resolver( @@ -429,26 +450,12 @@ def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: return object_resolver_factory +@dataclass(frozen=True) class GraphQLObjectModel(GraphQLModel): - ast_type = ObjectTypeDefinitionNode resolvers: Dict[str, Resolver] aliases: Dict[str, str] out_names: Dict[str, Dict[str, str]] - def __init__( - self, - name: str, - ast: ObjectTypeDefinitionNode, - resolvers: Dict[str, Resolver], - aliases: Dict[str, str], - out_names: Dict[str, Dict[str, str]], - ): - self.name = name - self.ast = ast - self.resolvers = resolvers - self.aliases = aliases - self.out_names = out_names - def bind_to_schema(self, schema: GraphQLSchema): bindable = ObjectTypeBindable(self.name) @@ -467,59 +474,39 @@ def bind_to_schema(self, schema: GraphQLSchema): def get_field_node_from_type_hint( + metadata: GraphQLMetadata, field_name: str, field_type: Any, field_args: Any, field_description: Optional[str] = None, ) -> FieldDefinitionNode: - if field_description: - description = StringValueNode(value=field_description) - else: - description = None - return FieldDefinitionNode( - description=description, + description=get_description_node(field_description), name=NameNode(value=field_name), - type=get_type_node(field_type), - arguments=get_field_args_nodes_from_obj_field_args(field_args), + type=get_type_node(metadata, field_type), + arguments=get_field_args_nodes_from_obj_field_args(metadata, field_args), ) def get_field_node_from_obj_field( + metadata: GraphQLMetadata, field: GraphQLObjectField, field_args: Any, ) -> FieldDefinitionNode: - if field.description: - description = StringValueNode(value=field.description) - else: - description = None - return FieldDefinitionNode( - description=description, + description=get_description_node(field.description), name=NameNode(value=field.name), - type=get_type_node(field.type), - arguments=get_field_args_nodes_from_obj_field_args(field_args), + type=get_type_node(metadata, field.type), + arguments=get_field_args_nodes_from_obj_field_args(metadata, field_args), ) +@dataclass(frozen=True) class GraphQLObjectFieldArg: name: Optional[str] out_name: Optional[str] - description: Optional[str] type: Optional[Any] - - def __init__( - self, - *, - name: Optional[str] = None, - out_name: Optional[str] = None, - description: Optional[str] = None, - type: Optional[Any] = None, - ): - self.name = name - self.out_name = out_name - self.description = description - self.type = type + description: Optional[str] = None def get_field_args_from_resolver( @@ -560,6 +547,10 @@ def get_field_args_from_resolver( return field_args +def get_field_args_names_from_resolver(resolver: Resolver) -> List[str]: + return list(get_field_args_from_resolver(resolver).keys()) + + def get_field_args_out_names( field_args: Dict[str, GraphQLObjectFieldArg] ) -> Dict[str, str]: @@ -570,41 +561,50 @@ def get_field_args_out_names( def get_field_args_nodes_from_obj_field_args( - field_args: Dict[str, GraphQLObjectFieldArg] + metadata: GraphQLMetadata, field_args: Dict[str, GraphQLObjectFieldArg] ) -> Tuple[InputValueDefinitionNode]: return tuple( - get_field_arg_node_from_obj_field_arg(field_arg) + get_field_arg_node_from_obj_field_arg(metadata, field_arg) for field_arg in field_args.values() ) def get_field_arg_node_from_obj_field_arg( + metadata: GraphQLMetadata, field_arg: GraphQLObjectFieldArg, ) -> InputValueDefinitionNode: - if field_arg.description: - description = StringValueNode(value=field_arg.description) - else: - description = None - return InputValueDefinitionNode( - description=description, + description=get_description_node(field_arg.description), name=NameNode(value=field_arg.name), - type=get_type_node(field_arg.type), + type=get_type_node(metadata, field_arg.type), ) -def update_args_options( - resolver_args: Dict[str, GraphQLObjectFieldArg], +def update_field_args_options( + field_args: Dict[str, GraphQLObjectFieldArg], args_options: Optional[Dict[str, dict]], -): +) -> Dict[str, GraphQLObjectFieldArg]: if not args_options: - return + field_args + + updated_args: Dict[str, GraphQLObjectFieldArg] = {} + for arg_name in field_args: + arg_options = args_options.get(arg_name) + if not arg_options: + updated_args[arg_name] = field_args[arg_name] + continue - for arg_name, arg_options in args_options.items(): - resolver_arg = resolver_args[arg_name] + args_update = {} if arg_options.get("name"): - resolver_arg.name = arg_options["name"] + args_update["name"] = arg_options["name"] if arg_options.get("description"): - resolver_arg.description = arg_options["description"] + args_update["description"] = arg_options["description"] if arg_options.get("type"): - resolver_arg.type = arg_options["type"] + args_update["type"] = arg_options["type"] + + if args_update: + updated_args[arg_name] = replace(field_args[arg_name], **args_update) + else: + updated_args[arg_name] = field_args[arg_name] + + return updated_args diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/scalartype.py index 2977077..5a2a134 100644 --- a/ariadne_graphql_modules/next/scalartype.py +++ b/ariadne_graphql_modules/next/scalartype.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, cast from ariadne import ScalarType as ScalarTypeBindable @@ -10,7 +11,8 @@ ) from ..utils import parse_definition -from .base import GraphQLModel, GraphQLType +from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .description import get_description_node from .validators import validate_description, validate_name T = TypeVar("T") @@ -36,16 +38,18 @@ def __init_subclass__(cls) -> None: validate_scalar_type_with_schema(cls) @classmethod - def __get_graphql_model__(cls) -> "GraphQLModel": + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(name) + return cls.__get_graphql_model_with_schema__(metadata, name) - return cls.__get_graphql_model_without_schema__(name) + return cls.__get_graphql_model_without_schema__(metadata, name) @classmethod - def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": + def __get_graphql_model_with_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": definition = cast( ScalarTypeDefinitionNode, parse_definition(ScalarTypeDefinitionNode, cls.__schema__), @@ -53,6 +57,7 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": return GraphQScalarModel( name=definition.name.value, + ast_type=ScalarTypeDefinitionNode, ast=definition, serialize=cls.serialize, parse_value=cls.parse_value, @@ -60,11 +65,17 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLModel": ) @classmethod - def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": return GraphQScalarModel( name=name, + ast_type=ScalarTypeDefinitionNode, ast=ScalarTypeDefinitionNode( name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), ), serialize=cls.serialize, parse_value=cls.parse_value, @@ -107,22 +118,11 @@ def validate_scalar_type_with_schema(cls: Type[GraphQLScalar]): validate_description(cls, definition) +@dataclass(frozen=True) class GraphQScalarModel(GraphQLModel): - ast_type = ScalarTypeDefinitionNode - - def __init__( - self, - name: str, - ast: ScalarTypeDefinitionNode, - serialize: Callable[[Any], Any], - parse_value: Callable[[Any], Any], - parse_literal: Callable[[ValueNode, Optional[Dict[str, Any]]], Any], - ): - self.name = name - self.ast = ast - self.serialize = serialize - self.parse_value = parse_value - self.parse_literal = parse_literal + serialize: Callable[[Any], Any] + parse_value: Callable[[Any], Any] + parse_literal: Callable[[ValueNode, Optional[Dict[str, Any]]], Any] def bind_to_schema(self, schema: GraphQLSchema): bindable = ScalarTypeBindable( diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index 5990c5b..dd1c2e2 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -1,3 +1,4 @@ +from enum import Enum from importlib import import_module from inspect import isclass from types import UnionType @@ -11,20 +12,21 @@ TypeNode, ) -from .base import GraphQLType +from .base import GraphQLMetadata, GraphQLType from .deferredtype import DeferredType -def get_type_node(type_hint: Any) -> TypeNode: +def get_type_node(metadata: GraphQLMetadata, type_hint: Any) -> TypeNode: if is_nullable(type_hint): nullable = True type_hint = unwrap_type(type_hint) else: nullable = False + type_node = None if is_list(type_hint): list_item_type_hint = unwrap_type(type_hint) - type_node = ListTypeNode(type=get_type_node(list_item_type_hint)) + type_node = ListTypeNode(type=get_type_node(metadata, list_item_type_hint)) elif type_hint == str: type_node = NamedTypeNode(name=NameNode(value="String")) elif type_hint == int: @@ -44,13 +46,14 @@ def get_type_node(type_hint: Any) -> TypeNode: deferred_type = get_deferred_type(type_hint, forward_ref, type_meta) type_node = NamedTypeNode( - name=NameNode(value=deferred_type.__get_graphql_name__()), + name=NameNode(value=metadata.get_graphql_name(deferred_type)), ) - elif isclass(type_hint) and issubclass(type_hint, GraphQLType): + elif isclass(type_hint) and issubclass(type_hint, (GraphQLType, Enum)): type_node = NamedTypeNode( - name=NameNode(value=type_hint.__get_graphql_name__()), + name=NameNode(value=metadata.get_graphql_name(type_hint)), ) - else: + + if not type_node: raise ValueError(f"Can't create a GraphQL return type for '{type_hint}'.") if nullable: @@ -85,22 +88,22 @@ def unwrap_type(type_hint: Any) -> Any: def get_deferred_type( type_hint: Any, forward_ref: ForwardRef, deferred_type: DeferredType -) -> Optional[GraphQLType]: +) -> Optional[Union[GraphQLType, Enum]]: type_name = forward_ref.__forward_arg__ module = import_module(deferred_type.path) graphql_type = getattr(module, type_name) - if not isclass(graphql_type) or not issubclass(graphql_type, GraphQLType): + if not isclass(graphql_type) or not issubclass(graphql_type, (GraphQLType, Enum)): raise ValueError(f"Can't create a GraphQL return type for '{type_hint}'.") return graphql_type -def get_graphql_type(type_hint: Any) -> Optional[GraphQLType]: +def get_graphql_type(type_hint: Any) -> Optional[Union[GraphQLType, Enum]]: if not isclass(type_hint): return None - if issubclass(type_hint, GraphQLType): + if issubclass(type_hint, (GraphQLType, Enum)): return type_hint return None diff --git a/tests_next/conftest.py b/tests_next/conftest.py index 05d91f4..19fa568 100644 --- a/tests_next/conftest.py +++ b/tests_next/conftest.py @@ -1,7 +1,9 @@ from textwrap import dedent import pytest -from graphql import GraphQLSchema, print_schema +from graphql import TypeDefinitionNode, GraphQLSchema, print_ast, print_schema + +from ariadne_graphql_modules.next import GraphQLMetadata @pytest.fixture @@ -11,3 +13,17 @@ def schema_equals_assertion(schema: GraphQLSchema, target: str): assert schema_str == dedent(target).strip() return schema_equals_assertion + + +@pytest.fixture +def assert_ast_equals(): + def ast_equals_assertion(ast: TypeDefinitionNode, target: str): + ast_str = print_ast(ast) + assert ast_str == dedent(target).strip() + + return ast_equals_assertion + + +@pytest.fixture +def metadata(): + return GraphQLMetadata() diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py index dcc7396..07c0bf9 100644 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -9,8 +9,18 @@ snapshots['test_object_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" +snapshots['test_object_type_validation_fails_for_missing_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (expected one of: 'name')" + +snapshots['test_object_type_validation_fails_for_missing_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argumentthats not defined on the resolver function. (expected one of: 'name')" + +snapshots['test_object_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" + snapshots['test_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined attribute 'other'. (Valid attrs: 'hello')" snapshots['test_object_type_validation_fails_for_undefined_field_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" + +snapshots['test_object_type_validation_fails_for_undefined_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" + +snapshots['test_object_type_validation_fails_for_undefined_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argumentthats not defined on the resolver function. (function accepts no extra arguments)" diff --git a/tests_next/snapshots/snap_test_standard_enum.py b/tests_next/snapshots/snap_test_standard_enum.py new file mode 100644 index 0000000..67240d1 --- /dev/null +++ b/tests_next/snapshots/snap_test_standard_enum.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_value_error_is_raised_if_exclude_and_include_members_are_combined 1'] = "'members_include' and 'members_exclude' options are mutually exclusive." + +snapshots['test_value_error_is_raised_if_member_description_is_set_for_excluded_item 1'] = "Member description was specified for a member 'ADMINISTRATOR' not present in final GraphQL enum." + +snapshots['test_value_error_is_raised_if_member_description_is_set_for_missing_item 1'] = "Member description was specified for a member 'MISSING' not present in final GraphQL enum." + +snapshots['test_value_error_is_raised_if_member_description_is_set_for_omitted_item 1'] = "Member description was specified for a member 'ADMINISTRATOR' not present in final GraphQL enum." + +snapshots['test_value_error_is_raised_if_no_name_was_set_for_dict 1'] = "'name' argument is required when 'enum' type is a 'dict'." diff --git a/tests_next/test_description_node.py b/tests_next/test_description_node.py new file mode 100644 index 0000000..e9f7101 --- /dev/null +++ b/tests_next/test_description_node.py @@ -0,0 +1,35 @@ +from ariadne_graphql_modules.next import get_description_node + + +def test_no_description_is_returned_for_none(): + description = get_description_node(None) + assert description is None + + +def test_no_description_is_returned_for_empty_str(): + description = get_description_node("") + assert description is None + + +def test_description_is_returned_for_str(): + description = get_description_node("Example string.") + assert description.value == "Example string." + assert description.block is False + + +def test_description_is_stripped_for_whitespace(): + description = get_description_node(" Example string.\n") + assert description.value == "Example string." + assert description.block is False + + +def test_block_description_is_returned_for_multiline_str(): + description = get_description_node("Example string.\nNext line.") + assert description.value == "Example string.\nNext line." + assert description.block is True + + +def test_block_description_is_dedented(): + description = get_description_node(" Example string.\n Next line.") + assert description.value == "Example string.\nNext line." + assert description.block is True diff --git a/tests_next/test_metadata.py b/tests_next/test_metadata.py new file mode 100644 index 0000000..20a086f --- /dev/null +++ b/tests_next/test_metadata.py @@ -0,0 +1,86 @@ +from enum import Enum + +from ariadne_graphql_modules.next import GraphQLObject, graphql_enum + + +class QueryType(GraphQLObject): + hello: str + + +def test_metadata_returns_model_for_type(assert_ast_equals, metadata): + model = metadata.get_graphql_model(QueryType) + assert model.name == "Query" + + assert_ast_equals( + model.ast, + ( + """ + type Query { + hello: String! + } + """ + ), + ) + + +def test_metadata_returns_graphql_name_for_type(assert_ast_equals, metadata): + assert metadata.get_graphql_name(QueryType) == "Query" + + +class UserLevel(Enum): + GUEST = 0 + MEMBER = 1 + MODERATOR = 2 + ADMINISTRATOR = 3 + + +def test_metadata_returns_model_for_standard_enum(assert_ast_equals, metadata): + model = metadata.get_graphql_model(UserLevel) + assert model.name == "UserLevel" + + assert_ast_equals( + model.ast, + ( + """ + enum UserLevel { + GUEST + MEMBER + MODERATOR + ADMINISTRATOR + } + """ + ), + ) + + +def test_metadata_returns_graphql_name_for_standard_enum(assert_ast_equals, metadata): + assert metadata.get_graphql_name(UserLevel) == "UserLevel" + + +@graphql_enum(name="SeverityEnum") +class SeverityLevel(Enum): + LOW = 0 + MEDIUM = 1 + HIGH = 2 + + +def test_metadata_returns_model_for_annotated_enum(assert_ast_equals, metadata): + model = metadata.get_graphql_model(SeverityLevel) + assert model.name == "SeverityEnum" + + assert_ast_equals( + model.ast, + ( + """ + enum SeverityEnum { + LOW + MEDIUM + HIGH + } + """ + ), + ) + + +def test_metadata_returns_graphql_name_for_annotated_enum(assert_ast_equals, metadata): + assert metadata.get_graphql_name(SeverityLevel) == "SeverityEnum" diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index d6270df..f9acf06 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -13,6 +13,16 @@ class CustomType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) +def test_object_type_validation_fails_for_names_not_matching(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __graphql_name__ = "Lorem" + __schema__ = gql("type Custom") + + snapshot.assert_match(str(exc_info.value)) + + def test_object_type_validation_fails_for_undefined_attr_resolver(snapshot): with pytest.raises(ValueError) as exc_info: @@ -60,3 +70,55 @@ class CustomType(GraphQLObject): ) snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_undefined_field_resolver_arg(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + @GraphQLObject.field(args={"invalid": GraphQLObject.argument(name="test")}) + def hello(*_) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_undefined_resolver_arg(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + hello: str + + @GraphQLObject.resolver( + "hello", args={"invalid": GraphQLObject.argument(name="test")} + ) + def resolve_hello(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_missing_field_resolver_arg(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + @GraphQLObject.field(args={"invalid": GraphQLObject.argument(name="test")}) + def hello(*_, name: str) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_missing_resolver_arg(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + hello: str + + @GraphQLObject.resolver( + "hello", args={"invalid": GraphQLObject.argument(name="test")} + ) + def resolve_hello(*_, name: str): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_standard_enum.py b/tests_next/test_standard_enum.py new file mode 100644 index 0000000..795f424 --- /dev/null +++ b/tests_next/test_standard_enum.py @@ -0,0 +1,306 @@ +from enum import Enum + +import pytest +from graphql import graphql_sync + +from ariadne_graphql_modules.next import ( + GraphQLObject, + create_graphql_enum_model, + graphql_enum, + make_executable_schema, +) + + +class UserLevel(Enum): + GUEST = 0 + MEMBER = 1 + MODERATOR = 2 + ADMINISTRATOR = 3 + + +def test_graphql_enum_model_is_created_from_python_enum(assert_ast_equals): + graphql_model = create_graphql_enum_model(UserLevel) + + assert graphql_model.name == "UserLevel" + assert graphql_model.members == { + "GUEST": UserLevel.GUEST, + "MEMBER": UserLevel.MEMBER, + "MODERATOR": UserLevel.MODERATOR, + "ADMINISTRATOR": UserLevel.ADMINISTRATOR, + } + + assert_ast_equals( + graphql_model.ast, + """ + enum UserLevel { + GUEST + MEMBER + MODERATOR + ADMINISTRATOR + } + """, + ) + + +def test_graphql_enum_model_is_created_with_custom_name(): + graphql_model = create_graphql_enum_model(UserLevel, name="CustomName") + assert graphql_model.name == "CustomName" + + +def test_graphql_enum_model_is_created_with_method(): + class UserLevel(Enum): + GUEST = 0 + MEMBER = 1 + MODERATOR = 2 + ADMINISTRATOR = 3 + + @staticmethod + def __get_graphql_model__(): + return "CustomModel" + + graphql_model = create_graphql_enum_model(UserLevel) + assert graphql_model == "CustomModel" + + +def test_graphql_enum_model_is_created_with_name_from_method(assert_ast_equals): + class UserLevel(Enum): + GUEST = 0 + MEMBER = 1 + MODERATOR = 2 + ADMINISTRATOR = 3 + + @staticmethod + def __get_graphql_name__(): + return "CustomName" + + graphql_model = create_graphql_enum_model(UserLevel) + assert graphql_model.name == "CustomName" + + +def test_graphql_enum_model_is_created_with_description(assert_ast_equals): + graphql_model = create_graphql_enum_model(UserLevel, description="Test enum.") + + assert graphql_model.name == "UserLevel" + assert graphql_model.members == { + "GUEST": UserLevel.GUEST, + "MEMBER": UserLevel.MEMBER, + "MODERATOR": UserLevel.MODERATOR, + "ADMINISTRATOR": UserLevel.ADMINISTRATOR, + } + + assert_ast_equals( + graphql_model.ast, + """ + "Test enum." + enum UserLevel { + GUEST + MEMBER + MODERATOR + ADMINISTRATOR + } + """, + ) + + +def test_graphql_enum_model_is_created_with_specified_members(assert_ast_equals): + graphql_model = create_graphql_enum_model( + UserLevel, + members_include=["GUEST", "MODERATOR"], + ) + + assert graphql_model.name == "UserLevel" + assert graphql_model.members == { + "GUEST": UserLevel.GUEST, + "MODERATOR": UserLevel.MODERATOR, + } + + assert_ast_equals( + graphql_model.ast, + """ + enum UserLevel { + GUEST + MODERATOR + } + """, + ) + + +def test_graphql_enum_model_is_created_without_specified_members(assert_ast_equals): + graphql_model = create_graphql_enum_model( + UserLevel, + members_exclude=["GUEST", "MODERATOR"], + ) + + assert graphql_model.name == "UserLevel" + assert graphql_model.members == { + "MEMBER": UserLevel.MEMBER, + "ADMINISTRATOR": UserLevel.ADMINISTRATOR, + } + + assert_ast_equals( + graphql_model.ast, + """ + enum UserLevel { + MEMBER + ADMINISTRATOR + } + """, + ) + + +def test_graphql_enum_model_is_created_with_members_descriptions(assert_ast_equals): + graphql_model = create_graphql_enum_model( + UserLevel, + members_descriptions={ + "GUEST": "Default role.", + "ADMINISTRATOR": "Can use admin panel.", + }, + ) + + assert graphql_model.name == "UserLevel" + assert graphql_model.members == { + "GUEST": UserLevel.GUEST, + "MEMBER": UserLevel.MEMBER, + "MODERATOR": UserLevel.MODERATOR, + "ADMINISTRATOR": UserLevel.ADMINISTRATOR, + } + + assert_ast_equals( + graphql_model.ast, + """ + enum UserLevel { + "Default role." + GUEST + MEMBER + MODERATOR + "Can use admin panel." + ADMINISTRATOR + } + """, + ) + + +def test_value_error_is_raised_if_exclude_and_include_members_are_combined(snapshot): + with pytest.raises(ValueError) as exc_info: + create_graphql_enum_model( + UserLevel, + members_exclude=["MEMBER"], + members_include=["ADMINISTRATOR"], + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_value_error_is_raised_if_member_description_is_set_for_missing_item(snapshot): + with pytest.raises(ValueError) as exc_info: + create_graphql_enum_model(UserLevel, members_descriptions={"MISSING": "Hello!"}) + + snapshot.assert_match(str(exc_info.value)) + + +def test_value_error_is_raised_if_member_description_is_set_for_omitted_item(snapshot): + with pytest.raises(ValueError) as exc_info: + create_graphql_enum_model( + UserLevel, + members_include=["GUEST"], + members_descriptions={"ADMINISTRATOR": "Hello!"}, + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_value_error_is_raised_if_member_description_is_set_for_excluded_item(snapshot): + with pytest.raises(ValueError) as exc_info: + create_graphql_enum_model( + UserLevel, + members_exclude=["ADMINISTRATOR"], + members_descriptions={"ADMINISTRATOR": "Hello!"}, + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_field_returning_enum_instance(assert_schema_equals): + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> UserLevel: + return UserLevel.MODERATOR + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + MEMBER + MODERATOR + ADMINISTRATOR + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "MODERATOR"} + + +def test_graphql_enum_decorator_without_options_sets_model_on_enum(assert_ast_equals): + @graphql_enum + class SeverityLevel(Enum): + LOW = 0 + MEDIUM = 1 + HIGH = 2 + + graphql_model = SeverityLevel.__get_graphql_model__() + + assert graphql_model.name == "SeverityLevel" + assert graphql_model.members == { + "LOW": SeverityLevel.LOW, + "MEDIUM": SeverityLevel.MEDIUM, + "HIGH": SeverityLevel.HIGH, + } + + assert_ast_equals( + graphql_model.ast, + """ + enum SeverityLevel { + LOW + MEDIUM + HIGH + } + """, + ) + + +def test_graphql_enum_decorator_with_options_sets_model_on_enum(assert_ast_equals): + @graphql_enum(name="SeverityEnum", members_exclude=["HIGH"]) + class SeverityLevel(Enum): + LOW = 0 + MEDIUM = 1 + HIGH = 2 + + graphql_model = SeverityLevel.__get_graphql_model__() + + assert graphql_model.name == "SeverityEnum" + assert graphql_model.members == { + "LOW": SeverityLevel.LOW, + "MEDIUM": SeverityLevel.MEDIUM, + } + + assert_ast_equals( + graphql_model.ast, + """ + enum SeverityEnum { + LOW + MEDIUM + } + """, + ) diff --git a/tests_next/test_typing.py b/tests_next/test_typing.py index c6056aa..f4b236a 100644 --- a/tests_next/test_typing.py +++ b/tests_next/test_typing.py @@ -1,12 +1,13 @@ -from typing import TYPE_CHECKING, Annotated, List, Optional, Union, get_type_hints +from enum import Enum +from typing import TYPE_CHECKING, Annotated, List, Optional, Union from graphql import ListTypeNode, NameNode, NamedTypeNode, NonNullTypeNode -from ariadne_graphql_modules.next import GraphQLObject, deferred +from ariadne_graphql_modules.next import GraphQLObject, deferred, graphql_enum from ariadne_graphql_modules.next.typing import get_type_node if TYPE_CHECKING: - from .types import ForwardScalar + from .types import ForwardEnum, ForwardScalar def assert_non_null_type(type_node, name: str): @@ -33,58 +34,95 @@ def assert_named_type(type_node, name: str): assert type_node.name.value == name -def test_get_graphql_type_node_from_python_builtin_type(): - assert_named_type(get_type_node(Optional[str]), "String") - assert_named_type(get_type_node(Union[int, None]), "Int") - assert_named_type(get_type_node(float | None), "Float") - assert_named_type(get_type_node(Optional[bool]), "Boolean") +def test_get_graphql_type_node_from_python_builtin_type(metadata): + assert_named_type(get_type_node(metadata, Optional[str]), "String") + assert_named_type(get_type_node(metadata, Union[int, None]), "Int") + assert_named_type(get_type_node(metadata, float | None), "Float") + assert_named_type(get_type_node(metadata, Optional[bool]), "Boolean") -def test_get_non_null_graphql_type_node_from_python_builtin_type(): - assert_non_null_type(get_type_node(str), "String") - assert_non_null_type(get_type_node(int), "Int") - assert_non_null_type(get_type_node(float), "Float") - assert_non_null_type(get_type_node(bool), "Boolean") +def test_get_non_null_graphql_type_node_from_python_builtin_type(metadata): + assert_non_null_type(get_type_node(metadata, str), "String") + assert_non_null_type(get_type_node(metadata, int), "Int") + assert_non_null_type(get_type_node(metadata, float), "Float") + assert_non_null_type(get_type_node(metadata, bool), "Boolean") -def test_get_graphql_type_node_from_graphql_type(): +def test_get_graphql_type_node_from_graphql_type(metadata): class UserType(GraphQLObject): ... - assert_non_null_type(get_type_node(UserType), "User") - assert_named_type(get_type_node(Optional[UserType]), "User") + assert_non_null_type(get_type_node(metadata, UserType), "User") + assert_named_type(get_type_node(metadata, Optional[UserType]), "User") -def test_get_graphql_list_type_node_from_python_builtin_type(): - assert_list_type(get_type_node(Optional[List[str]]), "String") - assert_list_type(get_type_node(Union[List[int], None]), "Int") - assert_list_type(get_type_node(List[float] | None), "Float") - assert_list_type(get_type_node(Optional[List[bool]]), "Boolean") +def test_get_graphql_list_type_node_from_python_builtin_type(metadata): + assert_list_type(get_type_node(metadata, Optional[List[str]]), "String") + assert_list_type(get_type_node(metadata, Union[List[int], None]), "Int") + assert_list_type(get_type_node(metadata, List[float] | None), "Float") + assert_list_type(get_type_node(metadata, Optional[List[bool]]), "Boolean") -def test_get_non_null_graphql_list_type_node_from_python_builtin_type(): - assert_non_null_list_type(get_type_node(List[str]), "String") - assert_non_null_list_type(get_type_node(List[int]), "Int") - assert_non_null_list_type(get_type_node(List[float]), "Float") - assert_non_null_list_type(get_type_node(List[bool]), "Boolean") +def test_get_non_null_graphql_list_type_node_from_python_builtin_type(metadata): + assert_non_null_list_type(get_type_node(metadata, List[str]), "String") + assert_non_null_list_type(get_type_node(metadata, List[int]), "Int") + assert_non_null_list_type(get_type_node(metadata, List[float]), "Float") + assert_non_null_list_type(get_type_node(metadata, List[bool]), "Boolean") -def test_get_graphql_type_node_from_annotated_type(): +def test_get_graphql_type_node_from_annotated_type(metadata): class MockType(GraphQLObject): field: Annotated["ForwardScalar", deferred("tests_next.types")] - assert_non_null_type(get_type_node(MockType.__annotations__["field"]), "Forward") + assert_non_null_type( + get_type_node(metadata, MockType.__annotations__["field"]), "Forward" + ) -def test_get_graphql_type_node_from_annotated_type_with_relative_path(): +def test_get_graphql_type_node_from_annotated_type_with_relative_path(metadata): class MockType(GraphQLObject): field: Annotated["ForwardScalar", deferred(".types")] - assert_non_null_type(get_type_node(MockType.__annotations__["field"]), "Forward") + assert_non_null_type( + get_type_node(metadata, MockType.__annotations__["field"]), "Forward" + ) -def test_get_graphql_type_node_from_nullable_annotated_type(): +def test_get_graphql_type_node_from_nullable_annotated_type(metadata): class MockType(GraphQLObject): field: Optional[Annotated["ForwardScalar", deferred("tests_next.types")]] - assert_named_type(get_type_node(MockType.__annotations__["field"]), "Forward") + assert_named_type( + get_type_node(metadata, MockType.__annotations__["field"]), "Forward" + ) + + +def test_get_graphql_type_node_from_annotated_enum(metadata): + class MockType(GraphQLObject): + field: Annotated["ForwardEnum", deferred("tests_next.types")] + + assert_non_null_type( + get_type_node(metadata, MockType.__annotations__["field"]), "ForwardEnum" + ) + + +def test_get_graphql_type_node_from_enum_type(metadata): + class UserLevel(Enum): + GUEST = 0 + MEMBER = 1 + MODERATOR = 2 + ADMINISTRATOR = 3 + + assert_non_null_type(get_type_node(metadata, UserLevel), "UserLevel") + assert_named_type(get_type_node(metadata, Optional[UserLevel]), "UserLevel") + + +def test_get_graphql_type_node_from_annotated_enum_type(metadata): + @graphql_enum(name="SeverityEnum") + class SeverityLevel(Enum): + LOW = 0 + MEDIUM = 1 + HIGH = 2 + + assert_non_null_type(get_type_node(metadata, SeverityLevel), "SeverityEnum") + assert_named_type(get_type_node(metadata, Optional[SeverityLevel]), "SeverityEnum") diff --git a/tests_next/types.py b/tests_next/types.py index fbd549b..02761c7 100644 --- a/tests_next/types.py +++ b/tests_next/types.py @@ -1,5 +1,12 @@ +from enum import Enum + from ariadne_graphql_modules.next import GraphQLScalar class ForwardScalar(GraphQLScalar): __schema__ = "scalar Forward" + + +class ForwardEnum(Enum): + RED = "RED" + BLU = "BLU" From 82e5f47c3e3338de5d1f3bda45fe41e58f0b4f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Fri, 22 Sep 2023 18:45:17 +0200 Subject: [PATCH 11/63] New GraphQLEnum --- ariadne_graphql_modules/next/__init__.py | 8 +- ariadne_graphql_modules/next/enumtype.py | 235 +++++++- ariadne_graphql_modules/next/objecttype.py | 56 +- .../snap_test_enum_type_validation.py | 30 + .../snapshots/snap_test_standard_enum.py | 2 - tests_next/test_enum_type.py | 522 ++++++++++++++++++ tests_next/test_enum_type_validation.py | 176 ++++++ tests_next/test_scalar_type.py | 2 +- 8 files changed, 997 insertions(+), 34 deletions(-) create mode 100644 tests_next/snapshots/snap_test_enum_type_validation.py create mode 100644 tests_next/test_enum_type.py create mode 100644 tests_next/test_enum_type_validation.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index b18858c..66dbe3b 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -1,12 +1,18 @@ from .base import GraphQLMetadata, GraphQLModel, GraphQLType from .deferredtype import deferred from .description import get_description_node -from .enumtype import GraphQLEnumModel, create_graphql_enum_model, graphql_enum +from .enumtype import ( + GraphQLEnum, + GraphQLEnumModel, + create_graphql_enum_model, + graphql_enum, +) from .executable_schema import make_executable_schema from .objecttype import GraphQLObject, GraphQLObjectModel, object_field from .scalartype import GraphQLScalar, GraphQScalarModel __all__ = [ + "GraphQLEnum", "GraphQLEnumModel", "GraphQLMetadata", "GraphQLModel", diff --git a/ariadne_graphql_modules/next/enumtype.py b/ariadne_graphql_modules/next/enumtype.py index b45f52d..58a5321 100644 --- a/ariadne_graphql_modules/next/enumtype.py +++ b/ariadne_graphql_modules/next/enumtype.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Iterable, Optional, Type, cast +from inspect import isclass +from typing import Any, Dict, Iterable, List, Optional, Set, Type, Union, cast from ariadne import EnumType from graphql import ( @@ -10,10 +11,240 @@ NameNode, ) -from .base import GraphQLModel +from ..utils import parse_definition +from .base import GraphQLMetadata, GraphQLModel, GraphQLType from .description import get_description_node +class GraphQLEnum(GraphQLType): + __abstract__: bool = True + __schema__: Optional[str] + __description__: Optional[str] + __members__: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] + __members_descriptions__: Optional[Dict[str, str]] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + validate_enum_type_with_schema(cls) + else: + validate_enum_type(cls) + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": + name = cls.__get_graphql_name__() + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__(metadata, name) + + return cls.__get_graphql_model_without_schema__(metadata, name) + + @classmethod + def __get_graphql_model_with_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLEnumModel": + definition = cast( + EnumTypeDefinitionNode, + parse_definition(EnumTypeDefinitionNode, cls.__schema__), + ) + + members = getattr(cls, "__members__", None) + if isinstance(members, dict): + members_values = {key: value for key, value in members.items()} + elif isclass(members) and issubclass(members, Enum): + members_values = {i.name: i for i in members} + else: + members_values = {i.name.value: i.name.value for i in definition.values} + + members_descriptions = getattr(cls, "__members_descriptions__", {}) + + return GraphQLEnumModel( + name=name, + members=members_values, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + directives=definition.directives, + description=definition.description + or (get_description_node(getattr(cls, "__description__", None))), + values=tuple( + EnumValueDefinitionNode( + name=value.name, + directives=value.directives, + description=value.description + or ( + get_description_node( + members_descriptions.get(value.name.value), + ) + ), + ) + for value in definition.values + ), + ), + ) + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLEnumModel": + members = getattr(cls, "__members__", None) + + if isinstance(members, dict): + members_values = {key: value for key, value in members.items()} + elif isclass(members) and issubclass(members, Enum): + members_values = {i.name: i for i in members} + elif isinstance(members, list): + members_values = {kv: kv for kv in members} + + members_descriptions = getattr(cls, "__members_descriptions__", {}) + + return GraphQLEnumModel( + name=name, + members=members_values, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + values=tuple( + EnumValueDefinitionNode( + name=NameNode(value=value_name), + description=get_description_node( + members_descriptions.get(value_name) + ), + ) + for value_name in members_values + ), + ), + ) + + +def validate_enum_type_with_schema(cls: Type[GraphQLEnum]): + from .validators import validate_description, validate_name + + definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) + + if not isinstance(definition, EnumTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{EnumTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + members_names = set(value.name.value for value in definition.values) + if not members_names: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "that doesn't declare any enum members." + ) + + members_values = getattr(cls, "__members__", None) + if members_values: + if isinstance(members_values, list): + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute " + "can't be a list when used together with '__schema__'." + ) + + missing_members: Optional[List[str]] = None + if isinstance(members_values, dict): + missing_members = members_names - set(members_values) + if isclass(members_values) and issubclass(members_values, Enum): + missing_members = members_names - set( + value.name for value in members_values + ) + + if missing_members: + missing_members_str = "', '".join(missing_members) + raise ValueError( + f"Class '{cls.__name__}' '__members__' is missing values " + "for enum members defined in '__schema__'. " + f"(missing items: '{missing_members_str}')" + ) + + members_descriptions = getattr(cls, "__members_descriptions__", {}) + validate_enum_members_descriptions(cls, members_names, members_descriptions) + + duplicate_descriptions: List[str] = [] + for ast_member in definition.values: + member_name = ast_member.name.value + if ( + ast_member.description + and ast_member.description.value + and members_descriptions.get(member_name) + ): + duplicate_descriptions.append(member_name) + + if duplicate_descriptions: + duplicate_descriptions_str = "', '".join(duplicate_descriptions) + raise ValueError( + f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " + f"descriptions for enum members that also have description in '__schema__' " + f"attribute. (members: '{duplicate_descriptions_str}')" + ) + + +def validate_enum_type(cls: Type[GraphQLEnum]): + members_values = getattr(cls, "__members__", None) + if not members_values: + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute is either missing or " + "empty. Either define it or provide full SDL for this enum using " + "the '__schema__' attribute." + ) + + if not ( + isinstance(members_values, dict) + or isinstance(members_values, list) + or (isclass(members_values) and issubclass(members_values, Enum)) + ): + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute is of unsupported type. " + f"Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. " + f"(found: '{type(members_values)}')" + ) + + members_names = get_members_set(members_values) + members_descriptions = getattr(cls, "__members_descriptions__", {}) + validate_enum_members_descriptions(cls, members_names, members_descriptions) + + +def validate_enum_members_descriptions( + cls: Type[GraphQLEnum], members: Set[str], members_descriptions: dict +): + invalid_descriptions = set(members_descriptions) - members + if invalid_descriptions: + invalid_descriptions_str = "', '".join(invalid_descriptions) + raise ValueError( + f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " + f"descriptions for undefined enum members. " + f"(undefined members: '{invalid_descriptions_str}')" + ) + + +def get_members_set( + members: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] +) -> Set[str]: + if isinstance(members, dict): + return set(members.keys()) + + if isclass(members) and issubclass(members, Enum): + return set(member.name for member in members) + + return set(members) + + def create_graphql_enum_model( enum: Type[Enum], *, diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index a29d668..d27d583 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -62,7 +62,7 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": @classmethod def __get_graphql_model_with_schema__( cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLModel": + ) -> "GraphQLObjectModel": definition = cast( ObjectTypeDefinitionNode, parse_definition(ObjectTypeDefinitionNode, cls.__schema__), @@ -91,7 +91,7 @@ def __get_graphql_model_with_schema__( @classmethod def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLModel": + ) -> "GraphQLObjectModel": type_hints = cls.__annotations__ fields_args_options: Dict[str, dict] = {} fields_descriptions: Dict[str, str] = {} @@ -272,6 +272,32 @@ def argument( return options +def validate_object_type_with_schema(cls: Type[GraphQLObject]): + definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) + + if not isinstance(definition, ObjectTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{ObjectTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + field_names: List[str] = [f.name.value for f in definition.fields] + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.field not in field_names: + valid_fields: str = "', '".join(sorted(field_names)) + raise ValueError( + f"Class '{cls.__name__}' defines resolver for an undefined " + f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" + ) + + def validate_object_type(cls: Type[GraphQLObject]): attrs_names: List[str] = [ attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") @@ -324,32 +350,6 @@ def validate_object_type(cls: Type[GraphQLObject]): ) -def validate_object_type_with_schema(cls: Type[GraphQLObject]): - definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) - - if not isinstance(definition, ObjectTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{ObjectTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - field_names: List[str] = [f.name.value for f in definition.fields] - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.field not in field_names: - valid_fields: str = "', '".join(sorted(field_names)) - raise ValueError( - f"Class '{cls.__name__}' defines resolver for an undefined " - f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" - ) - - class GraphQLObjectField: name: Optional[str] description: Optional[str] diff --git a/tests_next/snapshots/snap_test_enum_type_validation.py b/tests_next/snapshots/snap_test_enum_type_validation.py new file mode 100644 index 0000000..e306759 --- /dev/null +++ b/tests_next/snapshots/snap_test_enum_type_validation.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_enum_type_validation_fails_for_empty_enum 1'] = "Class 'UserLevel' defines '__schema__' attribute that doesn't declare any enum members." + +snapshots['test_enum_type_validation_fails_for_invalid_members 1'] = "Class 'UserLevel' '__members__' attribute is of unsupported type. Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. (found: '')" + +snapshots['test_enum_type_validation_fails_for_invalid_type_schema 1'] = "Class 'UserLevel' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'EnumTypeDefinitionNode')" + +snapshots['test_enum_type_validation_fails_for_missing_members 1'] = "Class 'UserLevel' '__members__' attribute is either missing or empty. Either define it or provide full SDL for this enum using the '__schema__' attribute." + +snapshots['test_enum_type_validation_fails_for_name_mismatch_between_schema_and_attr 1'] = "Class 'UserLevel' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('UserRank' != 'Custom')" + +snapshots['test_enum_type_validation_fails_for_schema_and_members_dict_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MEMBER')" + +snapshots['test_enum_type_validation_fails_for_schema_and_members_duplicated_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for enum members that also have description in '__schema__' attribute. (members: 'MEMBER')" + +snapshots['test_enum_type_validation_fails_for_schema_and_members_enum_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MODERATOR')" + +snapshots['test_enum_type_validation_fails_for_schema_and_members_list 1'] = "Class 'UserLevel' '__members__' attribute can't be a list when used together with '__schema__'." + +snapshots['test_enum_type_validation_fails_for_schema_invalid_members_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for undefined enum members. (undefined members: 'INVALID')" + +snapshots['test_enum_type_validation_fails_for_two_descriptions 1'] = "Class 'UserLevel' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_standard_enum.py b/tests_next/snapshots/snap_test_standard_enum.py index 67240d1..bf78283 100644 --- a/tests_next/snapshots/snap_test_standard_enum.py +++ b/tests_next/snapshots/snap_test_standard_enum.py @@ -14,5 +14,3 @@ snapshots['test_value_error_is_raised_if_member_description_is_set_for_missing_item 1'] = "Member description was specified for a member 'MISSING' not present in final GraphQL enum." snapshots['test_value_error_is_raised_if_member_description_is_set_for_omitted_item 1'] = "Member description was specified for a member 'ADMINISTRATOR' not present in final GraphQL enum." - -snapshots['test_value_error_is_raised_if_no_name_was_set_for_dict 1'] = "'name' argument is required when 'enum' type is a 'dict'." diff --git a/tests_next/test_enum_type.py b/tests_next/test_enum_type.py new file mode 100644 index 0000000..4d5b79c --- /dev/null +++ b/tests_next/test_enum_type.py @@ -0,0 +1,522 @@ +from enum import Enum + +from graphql import graphql_sync + +from ariadne_graphql_modules.next import ( + GraphQLEnum, + GraphQLObject, + make_executable_schema, +) + + +class UserLevelEnum(Enum): + GUEST = 0 + MEMBER = 1 + ADMIN = 2 + + +def test_enum_field_returning_enum_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> UserLevelEnum: + return UserLevelEnum.MEMBER + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "MEMBER"} + + +def test_enum_field_returning_dict_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> dict: + return 0 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "GUEST"} + + +def test_enum_field_returning_str_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = [ + "GUEST", + "MEMBER", + "ADMIN", + ] + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> str: + return "ADMIN" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_enum_type_with_custom_name(assert_schema_equals): + class UserLevel(GraphQLEnum): + __graphql_name__ = "UserLevelEnum" + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + level: UserLevel + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevelEnum { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevelEnum! + } + """, + ) + + +def test_enum_type_with_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __description__ = "Hello world." + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + level: UserLevel + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + +def test_enum_type_with_member_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = UserLevelEnum + __members_descriptions__ = {"MEMBER": "Hello world."} + + class QueryType(GraphQLObject): + level: UserLevel + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + +def test_sdl_enum_field_returning_enum_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """ + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> UserLevelEnum: + return UserLevelEnum.MEMBER + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "MEMBER"} + + +def test_sdl_enum_field_returning_dict_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_sdl_enum_field_returning_str_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """ + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> str: + return "GUEST" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "GUEST"} + + +def test_sdl_enum_with_description_attr(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + __description__ = "Hello world." + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_sdl_enum_with_schema_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_sdl_enum_with_member_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + __members_descriptions__ = {"MEMBER": "Hello world."} + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_sdl_enum_with_member_schema_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN + } + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + enum UserLevel { + GUEST + + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN + } + + type Query { + level: UserLevel! + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} diff --git a/tests_next/test_enum_type_validation.py b/tests_next/test_enum_type_validation.py new file mode 100644 index 0000000..5c23156 --- /dev/null +++ b/tests_next/test_enum_type_validation.py @@ -0,0 +1,176 @@ +from enum import Enum + +import pytest + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import GraphQLEnum + + +def test_enum_type_validation_fails_for_invalid_type_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __schema__ = gql("scalar Custom") + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_name_mismatch_between_schema_and_attr( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __graphql_name__ = "UserRank" + __schema__ = gql( + """ + enum Custom { + GUEST + MEMBER + } + """ + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_empty_enum(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __schema__ = gql("enum UserLevel") + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_two_descriptions(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __description__ = "Hello world!" + __schema__ = gql( + """ + \"\"\"Other description\"\"\" + enum Custom { + GUEST + MEMBER + } + """ + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_schema_and_members_list(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __schema__ = gql( + """ + enum Custom { + GUEST + MEMBER + } + """ + ) + __members__ = ["GUEST", "MEMBER"] + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_schema_and_members_dict_mismatch(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __schema__ = gql( + """ + enum Custom { + GUEST + MEMBER + } + """ + ) + __members__ = { + "GUEST": 0, + "MODERATOR": 1, + } + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_schema_and_members_enum_mismatch(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevelEnum(Enum): + GUEST = 0 + MEMBER = 1 + ADMIN = 2 + + class UserLevel(GraphQLEnum): + __schema__ = gql( + """ + enum Custom { + GUEST + MEMBER + MODERATOR + } + """ + ) + __members__ = UserLevelEnum + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_schema_and_members_duplicated_descriptions( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __schema__ = gql( + """ + enum Custom { + GUEST + \"Lorem ipsum.\" + MEMBER + } + """ + ) + __members_descriptions__ = {"MEMBER": "Other description."} + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_schema_invalid_members_descriptions(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __schema__ = gql( + """ + enum Custom { + GUEST + MEMBER + } + """ + ) + __members_descriptions__ = {"INVALID": "Other description."} + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_missing_members(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + pass + + snapshot.assert_match(str(exc_info.value)) + + +def test_enum_type_validation_fails_for_invalid_members(snapshot): + with pytest.raises(ValueError) as exc_info: + + class UserLevel(GraphQLEnum): + __members__ = "INVALID" + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_scalar_type.py b/tests_next/test_scalar_type.py index 6b0dd11..a26b295 100644 --- a/tests_next/test_scalar_type.py +++ b/tests_next/test_scalar_type.py @@ -84,7 +84,7 @@ def resolve_date(*_) -> date: assert result.data == {"date": "1989-10-30"} -def test_schema_first_scalar_type(assert_schema_equals): +def test_sdl_scalar_field_returning_scalar_instance(assert_schema_equals): class QueryType(GraphQLObject): date: SDLDateScalar From 1081c29c4b187f0335b493efa94c77e111e491a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Fri, 22 Sep 2023 18:49:33 +0200 Subject: [PATCH 12/63] Tweak default graphql name from class name for enums --- ariadne_graphql_modules/next/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 481932e..5e85929 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -22,6 +22,9 @@ def __get_graphql_name__(cls) -> "str": if name.endswith("Type"): # 'UserType' will produce the 'User' name return name[:-4] or name + if name.endswith("GraphQLEnum"): + # 'UserLevelGraphQLEnum' will produce the 'UserLevelEnum' name + return f"{name[:-11]}Enum" or name if name.endswith("GraphQLScalar"): # 'DateTimeGraphQLScalar' will produce the 'DateTime' name return name[:-13] or name From dfd2f51ea26e5a2b944e27d2039c571aa92450ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Fri, 13 Oct 2023 16:57:36 +0200 Subject: [PATCH 13/63] More improvements to ObjectType, initial InputType --- ariadne_graphql_modules/next/__init__.py | 3 + ariadne_graphql_modules/next/base.py | 34 +- ariadne_graphql_modules/next/deferredtype.py | 8 +- ariadne_graphql_modules/next/enumtype.py | 3 +- .../next/executable_schema.py | 20 +- ariadne_graphql_modules/next/inputtype.py | 190 +++++++ ariadne_graphql_modules/next/objecttype.py | 528 ++++++++++++------ ariadne_graphql_modules/next/typing.py | 29 +- ariadne_graphql_modules/next/validators.py | 2 +- tests_next/snapshots/snap_test_metadata.py | 10 + .../snap_test_object_type_validation.py | 38 +- tests_next/test_input_type.py | 44 ++ tests_next/test_metadata.py | 14 + tests_next/test_object_type.py | 175 +++++- tests_next/test_object_type_validation.py | 312 +++++++++++ tests_next/test_object_type_with_schema.py | 2 - tests_next/test_typing.py | 32 +- 17 files changed, 1233 insertions(+), 211 deletions(-) create mode 100644 ariadne_graphql_modules/next/inputtype.py create mode 100644 tests_next/snapshots/snap_test_metadata.py create mode 100644 tests_next/test_input_type.py delete mode 100644 tests_next/test_object_type_with_schema.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 66dbe3b..da65989 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -8,12 +8,15 @@ graphql_enum, ) from .executable_schema import make_executable_schema +from .inputtype import GraphQLInput, GraphQLInputModel from .objecttype import GraphQLObject, GraphQLObjectModel, object_field from .scalartype import GraphQLScalar, GraphQScalarModel __all__ = [ "GraphQLEnum", "GraphQLEnumModel", + "GraphQLInput", + "GraphQLInputModel", "GraphQLMetadata", "GraphQLModel", "GraphQLObject", diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 5e85929..79b6c61 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Dict, Iterable, Optional, Type, Union +from typing import Any, Dict, Iterable, Optional, Type, Union from graphql import GraphQLSchema, TypeDefinitionNode @@ -16,15 +16,12 @@ def __get_graphql_name__(cls) -> "str": return cls.__graphql_name__ name = cls.__name__ - if name.endswith("GraphQLType"): - # 'UserGraphQLType' will produce the 'User' name - return name[:-11] or name - if name.endswith("Type"): - # 'UserType' will produce the 'User' name - return name[:-4] or name if name.endswith("GraphQLEnum"): # 'UserLevelGraphQLEnum' will produce the 'UserLevelEnum' name return f"{name[:-11]}Enum" or name + if name.endswith("GraphQLInput"): + # 'UserGraphQLInput' will produce the 'UserInput' name + return f"{name[:-11]}Input" or name if name.endswith("GraphQLScalar"): # 'DateTimeGraphQLScalar' will produce the 'DateTime' name return name[:-13] or name @@ -34,17 +31,25 @@ def __get_graphql_name__(cls) -> "str": if name.endswith("GraphQL"): # 'UserGraphQL' will produce the 'User' name return name[:-7] or name + if name.endswith("Type"): + # 'UserType' will produce the 'User' name + return name[:-4] or name + if name.endswith("GraphQLType"): + # 'UserGraphQLType' will produce the 'User' name + return name[:-11] or name return name @classmethod - def __get_graphql_model__(cls, metadata: "Metadata") -> "GraphQLModel": + def __get_graphql_model__(cls, metadata: "GraphQLMetadata") -> "GraphQLModel": raise NotImplementedError( "Subclasses of 'GraphQLType' must define '__get_graphql_model__'" ) @classmethod - def __get_graphql_types__(cls) -> Iterable["GraphQLType"]: + def __get_graphql_types__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: """Returns iterable with GraphQL types associated with this type""" return [cls] @@ -65,10 +70,21 @@ def bind_to_schema(self, schema: GraphQLSchema): @dataclass(frozen=True) class GraphQLMetadata: + data: Dict[Union[Type[GraphQLType], Type[Enum]], Any] = field(default_factory=dict) models: Dict[Union[Type[GraphQLType], Type[Enum]], GraphQLModel] = field( default_factory=dict ) + def get_data(self, type: Union[Type[GraphQLType], Type[Enum]]) -> Any: + try: + return self.data[type] + except KeyError as e: + raise KeyError(f"No data is set for '{type}'.") from e + + def set_data(self, type: Union[Type[GraphQLType], Type[Enum]], data: Any) -> Any: + self.data[type] = data + return data + def get_graphql_model( self, type: Union[Type[GraphQLType], Type[Enum]] ) -> GraphQLModel: diff --git a/ariadne_graphql_modules/next/deferredtype.py b/ariadne_graphql_modules/next/deferredtype.py index 87c5c36..31f1aeb 100644 --- a/ariadne_graphql_modules/next/deferredtype.py +++ b/ariadne_graphql_modules/next/deferredtype.py @@ -4,13 +4,13 @@ @dataclass(frozen=True) -class DeferredType: +class DeferredTypeData: path: str -def deferred(module_path: str) -> DeferredType: +def deferred(module_path: str) -> DeferredTypeData: if not module_path.startswith("."): - return DeferredType(module_path) + return DeferredTypeData(module_path) frame = sys._getframe(2) if not frame: @@ -32,4 +32,4 @@ def deferred(module_path: str) -> DeferredType: ) package = ".".join(packages) - return DeferredType(f"{package}.{module_path_suffix}") + return DeferredTypeData(f"{package}.{module_path_suffix}") diff --git a/ariadne_graphql_modules/next/enumtype.py b/ariadne_graphql_modules/next/enumtype.py index 58a5321..19b3fd4 100644 --- a/ariadne_graphql_modules/next/enumtype.py +++ b/ariadne_graphql_modules/next/enumtype.py @@ -14,6 +14,7 @@ from ..utils import parse_definition from .base import GraphQLMetadata, GraphQLModel, GraphQLType from .description import get_description_node +from .validators import validate_description, validate_name class GraphQLEnum(GraphQLType): @@ -127,8 +128,6 @@ def __get_graphql_model_without_schema__( def validate_enum_type_with_schema(cls: Type[GraphQLEnum]): - from .validators import validate_description, validate_name - definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) if not isinstance(definition, EnumTypeDefinitionNode): diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index f37f89d..e088a66 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -11,6 +11,7 @@ from .base import GraphQLMetadata, GraphQLModel, GraphQLType from .enumtype import GraphQLEnumModel +from .inputtype import GraphQLInputModel from .objecttype import GraphQLObjectModel from .scalartype import GraphQScalarModel @@ -29,7 +30,7 @@ def make_executable_schema( ) -> GraphQLSchema: metadata = GraphQLMetadata() type_defs: List[str] = find_type_defs(types) - types_list: List[SchemaType] = flatten_types(types) + types_list: List[SchemaType] = flatten_types(types, metadata) assert_types_unique(types_list) assert_types_not_abstract(types_list) @@ -64,9 +65,12 @@ def find_type_defs(types: Sequence[SchemaType]) -> List[str]: def flatten_types( - types: Sequence[Union[SchemaType, List[SchemaType]]] + types: Sequence[Union[SchemaType, List[SchemaType]]], + metadata: GraphQLMetadata, ) -> List[Union[Enum, SchemaBindable, GraphQLModel]]: - flat_schema_types_list: List[SchemaType] = flatten_schema_types(types) + flat_schema_types_list: List[SchemaType] = flatten_schema_types( + types, metadata, dedupe=True + ) types_list: List[Union[Enum, SchemaBindable, GraphQLModel]] = [] for type_def in flat_schema_types_list: @@ -92,15 +96,16 @@ def flatten_types( def flatten_schema_types( types: Sequence[Union[SchemaType, List[SchemaType]]], - dedupe: bool = True, + metadata: GraphQLMetadata, + dedupe: bool, ) -> List[SchemaType]: flat_list: List[SchemaType] = [] for type_def in types: if isinstance(type_def, list): - flat_list += flatten_schema_types(type_def, dedupe=False) + flat_list += flatten_schema_types(type_def, metadata, dedupe=False) elif issubclass(type_def, GraphQLType): - flat_list += type_def.__get_graphql_types__() + flat_list += type_def.__get_graphql_types__(metadata) elif get_graphql_type_name(type_def): flat_list.append(type_def) @@ -151,11 +156,12 @@ def assert_types_not_abstract(type_defs: List[SchemaType]): def sort_models(schema_models: List[GraphQLModel]) -> List[GraphQLModel]: sorted_models: List[GraphQLModel] = [] - sorted_models += sort_models_by_type(GraphQLEnumModel, schema_models) sorted_models += sort_models_by_type(GraphQScalarModel, schema_models) + sorted_models += sort_models_by_type(GraphQLEnumModel, schema_models) sorted_models += [ model for model in schema_models if isinstance(model, GraphQLObjectModel) ] + sorted_models += sort_models_by_type(GraphQLInputModel, schema_models) return sorted_models diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py new file mode 100644 index 0000000..4352458 --- /dev/null +++ b/ariadne_graphql_modules/next/inputtype.py @@ -0,0 +1,190 @@ +from dataclasses import dataclass +from typing import Any, Dict, Iterable, List, Optional, Type + +from ariadne import InputType as InputTypeBindable +from graphql import ( + GraphQLSchema, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + NameNode, +) + +from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .convert_name import convert_python_name_to_graphql +from .description import get_description_node +from .typing import get_graphql_type, get_type_node +from .validators import validate_description, validate_name + + +class GraphQLInput(GraphQLType): + def __init__(self, data: dict): + raise NotImplementedError("GraphQLType.__init__() is not implemented") + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + validate_input_type_with_schema(cls) + else: + validate_input_type(cls) + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": + name = cls.__get_graphql_name__() + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__(metadata, name) + + return cls.__get_graphql_model_without_schema__(metadata, name) + + @classmethod + def __get_graphql_model_with_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLInputModel": + pass + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLInputModel": + type_hints = cls.__annotations__ + fields_instances: Dict[str, GraphQLInputField] = {} + fields_descriptions: Dict[str, str] = {} + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLInputField): + fields_instances[attr_name] = cls_attr + if cls_attr.description: + fields_descriptions[attr_name] = cls_attr.description + + fields_ast: List[InputValueDefinitionNode] = [] + out_names: Dict[str, str] = {} + + for hint_name, hint_type in type_hints.items(): + if hint_name.startswith("__"): + continue + + if hint_name in fields_instances: + continue + + hint_field_name = convert_python_name_to_graphql(hint_name) + fields_ast.append( + get_field_node_from_type_hint( + metadata, + hint_field_name, + hint_type, + fields_descriptions.get(hint_name), + ) + ) + + return GraphQLInputModel( + name=name, + ast_type=InputObjectTypeDefinitionNode, + ast=InputObjectTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + fields=tuple(fields_ast), + ), + out_type=cls, + out_names=out_names, + ) + + @classmethod + def __get_graphql_types__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: + """Returns iterable with GraphQL types associated with this type""" + types: List[GraphQLType] = [cls] + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLInputField): + if cls_attr.type: + field_graphql_type = get_graphql_type(cls_attr.type) + if field_graphql_type and field_graphql_type not in types: + types.append(field_graphql_type) + + type_hints = cls.__annotations__ + for hint_name, hint_type in type_hints.values(): + if hint_name.startswith("__"): + continue + + hint_graphql_type = get_graphql_type(hint_type) + if hint_graphql_type and hint_graphql_type not in types: + types.append(hint_graphql_type) + + return types + + @staticmethod + def field( + *, + name: Optional[str] = None, + type: Optional[Any] = None, + description: Optional[str] = None, + ): + """Shortcut for GraphQLInputField()""" + return GraphQLInputField( + name=name, + type=type, + description=description, + ) + + +def validate_input_type_with_schema(cls: Type[GraphQLInput]): + pass + + +def validate_input_type(cls: Type[GraphQLInput]): + pass + + +class GraphQLInputField: + name: Optional[str] + description: Optional[str] + type: Optional[Any] + default_value: Optional[Any] + + def __init__( + self, + *, + name: Optional[str] = None, + description: Optional[str] = None, + type: Optional[Any] = None, + default_value: Optional[Any] = None, + ): + self.name = name + self.description = description + self.type = type + self.default_value = default_value + + +@dataclass(frozen=True) +class GraphQLInputModel(GraphQLModel): + out_type: Any + out_names: Dict[str, str] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = InputTypeBindable(self.name, self.out_type, self.out_names) + bindable.bind_to_schema(schema) + + +def get_field_node_from_type_hint( + metadata: GraphQLMetadata, + field_name: str, + field_type: Any, + field_description: Optional[str] = None, +) -> InputValueDefinitionNode: + return InputValueDefinitionNode( + description=get_description_node(field_description), + name=NameNode(value=field_name), + type=get_type_node(metadata, field_type), + default_value=None, + ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index d27d583..d672b7a 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, replace +from enum import Enum from inspect import signature from typing import ( Any, @@ -8,6 +9,7 @@ Optional, Tuple, Type, + Union, cast, ) @@ -21,6 +23,7 @@ InputValueDefinitionNode, NameNode, ObjectTypeDefinitionNode, + StringValueNode, ) from ..utils import parse_definition @@ -36,6 +39,7 @@ class GraphQLObject(GraphQLType): __schema__: Optional[str] __description__: Optional[str] __aliases__: Optional[Dict[str, str]] + __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -68,6 +72,8 @@ def __get_graphql_model_with_schema__( parse_definition(ObjectTypeDefinitionNode, cls.__schema__), ) + descriptions: Dict[str, str] = {} + args_descriptions: Dict[str, Dict[str, str]] = {} resolvers: Dict[str, Resolver] = {} out_names: Dict[str, Dict[str, str]] = {} @@ -75,13 +81,58 @@ def __get_graphql_model_with_schema__( cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectResolver): resolvers[cls_attr.field] = cls_attr.resolver + if cls_attr.description: + descriptions[cls_attr.field] = get_description_node( + cls_attr.description + ) + + if cls_attr.args: + args_descriptions[cls_attr.field] = {} + for arg_name, arg_options in cls_attr.args.items(): + arg_description = get_description_node( + arg_options.get("description") + ) + if arg_description: + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description + + fields: List[FieldDefinitionNode] = [] + for field in definition.fields: + field_args_descriptions = args_descriptions.get(field.name.value, {}) + + args: List[InputValueDefinitionNode] = [] + for arg in field.arguments: + args.append( + InputValueDefinitionNode( + description=arg.description + or field_args_descriptions.get(arg.name.value), + name=arg.name, + directives=arg.directives, + type=arg.type, + default_value=arg.default_value, + ) + ) + + fields.append( + FieldDefinitionNode( + name=field.name, + description=( + field.description or descriptions.get(field.name.value) + ), + directives=field.directives, + arguments=tuple(args), + type=field.type, + ) + ) + print(descriptions) return GraphQLObjectModel( name=definition.name.value, ast_type=ObjectTypeDefinitionNode, ast=ObjectTypeDefinitionNode( name=NameNode(value=definition.name.value), - fields=definition.fields, + fields=tuple(fields), ), resolvers=resolvers, aliases=getattr(cls, "__aliases__", {}), @@ -92,100 +143,20 @@ def __get_graphql_model_with_schema__( def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLObjectModel": - type_hints = cls.__annotations__ - fields_args_options: Dict[str, dict] = {} - fields_descriptions: Dict[str, str] = {} - fields_resolvers: Dict[str, Resolver] = {} - fields_instances: Dict[str, GraphQLObjectField] = {} - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - fields_instances[attr_name] = cls_attr - if cls_attr.args: - fields_args_options[attr_name] = cls_attr.args - if cls_attr.description: - fields_descriptions[attr_name] = cls_attr.description - if isinstance(cls_attr, GraphQLObjectResolver): - fields_resolvers[cls_attr.field] = cls_attr.resolver - if cls_attr.args: - fields_args_options[cls_attr.field] = cls_attr.args - if cls_attr.description: - fields_descriptions[cls_attr.field] = cls_attr.description + type_data = get_graphql_object_data(metadata, cls) fields_ast: List[FieldDefinitionNode] = [] resolvers: Dict[str, Resolver] = {} out_names: Dict[str, Dict[str, str]] = {} - for hint_name, hint_type in type_hints.items(): - if hint_name.startswith("__"): - continue + for field in type_data.fields.values(): + fields_ast.append(get_field_node_from_obj_field(metadata, field)) - if hint_name in fields_instances: - continue + if field.resolver: + resolvers[field.name] = field.resolver - hint_field_name = convert_python_name_to_graphql(hint_name) - - if hint_name in fields_resolvers: - resolvers[hint_field_name] = fields_resolvers[hint_name] - field_args = get_field_args_from_resolver(resolvers[hint_field_name]) - if field_args: - if fields_args_options.get(hint_field_name): - field_args = update_field_args_options( - field_args, fields_args_options[hint_field_name] - ) - - out_names[hint_field_name] = get_field_args_out_names(field_args) - else: - field_args = {} - - fields_ast.append( - get_field_node_from_type_hint( - metadata, - hint_field_name, - hint_type, - field_args, - fields_descriptions.get(hint_name), - ) - ) - - for attr_name, attr_field in fields_instances.items(): - field_instance = GraphQLObjectField( - name=attr_field.name or convert_python_name_to_graphql(attr_name), - description=attr_field.description - or fields_descriptions.get(attr_name), - type=attr_field.type or type_hints.get(attr_name), - resolver=attr_field.resolver, - ) - - if field_instance.type is None: - raise ValueError( - f"Unable to find return type for field '{field_instance.name}'. " - "Either add a return type annotation on it's resolver or specify " - "return type explicitly via 'type=...' option." - ) - - field_resolver: Optional[Resolver] = None - if field_instance.resolver: - field_resolver = field_instance.resolver - elif attr_name in fields_resolvers: - field_resolver = fields_resolvers[attr_name] - - field_args = {} - if field_resolver: - resolvers[field_instance.name] = field_resolver - field_args = get_field_args_from_resolver(field_resolver) - if field_instance.name in fields_args_options: - field_args = update_field_args_options( - field_args, fields_args_options[field_instance.name] - ) - - fields_ast.append( - get_field_node_from_obj_field(metadata, field_instance, field_args) - ) - - if field_args: - out_names[field_instance.name] = get_field_args_out_names(field_args) + if field.args: + out_names[field.name] = get_field_args_out_names(field.args) return GraphQLObjectModel( name=name, @@ -203,23 +174,40 @@ def __get_graphql_model_without_schema__( ) @classmethod - def __get_graphql_types__(cls) -> Iterable["GraphQLType"]: + def __get_graphql_types__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: """Returns iterable with GraphQL types associated with this type""" + if getattr(cls, "__schema__", None): + return cls.__get_graphql_types_with_schema__(metadata) + + return cls.__get_graphql_types_without_schema__(metadata) + + @classmethod + def __get_graphql_types_with_schema__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: types: List[GraphQLType] = [cls] + types.extend(getattr(cls, "__requires__", [])) + return types - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - if cls_attr.type: - field_graphql_type = get_graphql_type(cls_attr.type) - if field_graphql_type and field_graphql_type not in types: - types.append(field_graphql_type) - - type_hints = cls.__annotations__ - for hint_type in type_hints.values(): - hint_graphql_type = get_graphql_type(hint_type) - if hint_graphql_type and hint_graphql_type not in types: - types.append(hint_graphql_type) + @classmethod + def __get_graphql_types_without_schema__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: + types: List[GraphQLType] = [cls] + type_data = get_graphql_object_data(metadata, cls) + + for field in type_data.fields.values(): + field_type = get_graphql_type(field.type) + if field_type and field_type not in types: + types.append(field_type) + + if field.args: + for field_arg in field.args.values(): + field_arg_type = get_graphql_type(field_arg.type) + if field_arg_type and field_arg_type not in types: + types.append(field_arg_type) return types @@ -287,8 +275,20 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]): validate_description(cls, definition) field_names: List[str] = [f.name.value for f in definition.fields] + field_definitions: Dict[str, FieldDefinitionNode] = { + f.name.value: f for f in definition.fields + } + + resolvers_names: List[str] = [] + for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + raise ValueError( + f"Class '{cls.__name__}' defines 'GraphQLObjectField' instance. " + "This is not supported for types defining '__schema__'." + ) + if isinstance(cls_attr, GraphQLObjectResolver): if cls_attr.field not in field_names: valid_fields: str = "', '".join(sorted(field_names)) @@ -297,21 +297,123 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]): f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" ) + if cls_attr.field in resolvers_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple resolvers for field " + f"'{cls_attr.field}'." + ) + + if cls_attr.description and field_definitions[cls_attr.field].description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{cls_attr.field}'." + ) + + if cls_attr.args: + field_args = { + arg.name.value: arg + for arg in field_definitions[cls_attr.field].arguments + } + + for arg_name, arg_options in cls_attr.args.items(): + if arg_name not in field_args: + raise ValueError( + f"Class '{cls.__name__}' defines options for '{arg_name}' " + f"argument of the '{cls_attr.field}' field " + "that doesn't exist." + ) + + if arg_options.get("name"): + raise ValueError( + f"Class '{cls.__name__}' defines 'name' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options.get("type"): + raise ValueError( + f"Class '{cls.__name__}' defines 'type' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options["description"] and field_args[arg_name].description: + raise ValueError( + f"Class '{cls.__name__}' defines duplicate descriptions " + f"for '{arg_name}' argument " + f"of the '{cls_attr.field}' field." + ) + + resolvers_names.append(cls_attr.field) + + validate_object_aliases(cls, field_names, resolvers_names) + def validate_object_type(cls: Type[GraphQLObject]): attrs_names: List[str] = [ attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") ] + + fields_instances: Dict[str, GraphQLObjectField] = {} + fields_names: List[str] = [] resolvers_names: List[str] = [] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectField): + if cls_attr.name in fields_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with GraphQL " + f"name '{cls_attr.name}'." + ) + + fields_names.append(cls_attr.name) + if cls_attr not in attrs_names: - attrs_names.append(cls_attr) + attrs_names.append(attr_name) + if cls_attr.resolver: + resolvers_names.append(attr_name) + + fields_instances[attr_name] = cls_attr + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.field in resolvers_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple resolvers for field " + f"'{cls_attr.field}'." + ) + resolvers_names.append(cls_attr.field) + field_instance = fields_instances.get(cls_attr.field) + if field_instance: + if field_instance.description and cls_attr.description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{cls_attr.field}'." + ) + if field_instance.args and cls_attr.args: + raise ValueError( + f"Class '{cls.__name__}' defines multiple arguments options " + f"('args') for field '{cls_attr.field}'." + ) + + graphql_names: List[str] = [] + + for attr_name in attrs_names: + if getattr(cls, attr_name, None) is None: + attr_graphql_name = convert_python_name_to_graphql(attr_name) + + if attr_graphql_name in graphql_names or attr_graphql_name in fields_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with GraphQL " + f"name '{attr_graphql_name}'." + ) + + graphql_names.append(attr_graphql_name) + for resolver_for in resolvers_names: if resolver_for not in attrs_names: valid_fields: str = "', '".join(sorted(attrs_names)) @@ -341,13 +443,130 @@ def validate_object_type(cls: Type[GraphQLObject]): "thats not defined on the resolver function. " f"({error_help})" ) - else: - raise ValueError( - f"Class '{cls.__name__}' defines '{attr_name}' resolver " - f"with extra configuration for '{arg_name}' argument" - "thats not defined on the resolver function. " - f"({error_help})" - ) + + raise ValueError( + f"Class '{cls.__name__}' defines '{attr_name}' resolver " + f"with extra configuration for '{arg_name}' argument " + "thats not defined on the resolver function. " + f"({error_help})" + ) + + validate_object_aliases(cls, attrs_names, resolvers_names) + + +def validate_object_aliases( + cls: Type[GraphQLObject], fields_names: List[str], resolvers_names: List[str] +): + aliases = getattr(cls, "__aliases__", None) or {} + for alias in aliases: + if alias not in fields_names: + valid_fields: str = "', '".join(sorted(fields_names)) + raise ValueError( + f"Class '{cls.__name__}' defines an alias for an undefined " + f"field '{alias}'. (Valid fields: '{valid_fields}')" + ) + + if alias in resolvers_names: + raise ValueError( + f"Class '{cls.__name__}' defines an alias for a field " + f"'{alias}' that already has a custom resolver." + ) + + +@dataclass(frozen=True) +class GraphQLObjectData: + fields: Dict[str, "GraphQLObjectField"] + + +def get_graphql_object_data( + metadata: GraphQLMetadata, cls: Type[GraphQLObject] +) -> GraphQLObjectData: + try: + return metadata.get_data(cls) + except KeyError: + if getattr(cls, "__schema__", None): + raise NotImplementedError( + "'get_graphql_object_data' is not supported for " + "objects with '__schema__'." + ) + else: + object_data = create_graphql_object_data_without_schema(cls) + + metadata.set_data(cls, object_data) + return object_data + + +def create_graphql_object_data_without_schema( + cls: Type[GraphQLObject], +) -> GraphQLObjectData: + fields_types: Dict[str, str] = {} + fields_names: Dict[str, str] = {} + fields_descriptions: Dict[str, str] = {} + fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} + fields_resolvers: Dict[str, Resolver] = {} + + type_hints = cls.__annotations__ + + fields_order: List[str] = [] + + for attr_name, attr_type in type_hints.items(): + if attr_name.startswith("__"): + continue + + fields_order.append(attr_name) + + fields_names[attr_name] = convert_python_name_to_graphql(attr_name) + fields_types[attr_name] = attr_type + + for attr_name in dir(cls): + if attr_name.startswith("__"): + continue + + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if attr_name not in fields_order: + fields_order.append(attr_name) + + fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( + attr_name + ) + + if cls_attr.type and attr_name not in fields_types: + fields_types[attr_name] = cls_attr.type + if cls_attr.description: + fields_descriptions[attr_name] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[attr_name] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[attr_name] = update_field_args_options( + field_args, cls_attr.args + ) + + if isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.type + if cls_attr.description: + fields_descriptions[cls_attr.field] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[cls_attr.field] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + + fields: Dict[str, "GraphQLObjectField"] = {} + for field_name in fields_order: + fields[field_name] = GraphQLObjectField( + name=fields_names[field_name], + description=fields_descriptions.get(field_name), + type=fields_types[field_name], + args=fields_args.get(field_name), + resolver=fields_resolvers.get(field_name), + ) + + return GraphQLObjectData(fields=fields) class GraphQLObjectField: @@ -372,39 +591,33 @@ def __init__( self.args = args self.resolver = resolver + def __call__(self, resolver: Resolver): + """Makes GraphQLObjectField instances work as decorators.""" + self.resolver = resolver + if not self.type: + self.type = get_field_type_from_resolver(resolver) + return self + def object_field( - f: Optional[Resolver] = None, + resolver: Optional[Resolver] = None, *, args: Optional[Dict[str, dict]] = None, name: Optional[str] = None, description: Optional[str] = None, type: Optional[Any] = None, -): - def object_field_factory(f: Optional[Resolver]) -> GraphQLObjectField: - field_name: Optional[str] = None - field_type: Any = None - - if name: - field_name = name - - if type: - field_type = type - elif f: - field_type = get_field_type_from_resolver(f) - - return GraphQLObjectField( - name=field_name, - description=description, - type=field_type, - args=args, - resolver=f, - ) - - if f is not None: - return object_field_factory(f) - - return object_field_factory +) -> GraphQLObjectField: + field_type: Any = type + if not type and resolver: + field_type = get_field_type_from_resolver(resolver) + + return GraphQLObjectField( + name=name, + description=description, + type=field_type, + args=args, + resolver=resolver, + ) def get_field_type_from_resolver(resolver: Resolver) -> Any: @@ -427,24 +640,12 @@ def object_resolver( description: Optional[str] = None, ): def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: - field_type: Any = None - - if field: - field_name = field - elif f.__name__ != "": - field_name = convert_python_name_to_graphql(f.__name__) - - if type: - field_type = type - else: - field_type = get_field_type_from_resolver(f) - return GraphQLObjectResolver( args=args, description=description, resolver=f, - field=field_name, - type=field_type, + field=field, + type=type or get_field_type_from_resolver(f), ) return object_resolver_factory @@ -473,31 +674,15 @@ def bind_to_schema(self, schema: GraphQLSchema): graphql_field.args[arg_name].out_name = out_name -def get_field_node_from_type_hint( - metadata: GraphQLMetadata, - field_name: str, - field_type: Any, - field_args: Any, - field_description: Optional[str] = None, -) -> FieldDefinitionNode: - return FieldDefinitionNode( - description=get_description_node(field_description), - name=NameNode(value=field_name), - type=get_type_node(metadata, field_type), - arguments=get_field_args_nodes_from_obj_field_args(metadata, field_args), - ) - - def get_field_node_from_obj_field( metadata: GraphQLMetadata, field: GraphQLObjectField, - field_args: Any, ) -> FieldDefinitionNode: return FieldDefinitionNode( description=get_description_node(field.description), name=NameNode(value=field.name), type=get_type_node(metadata, field.type), - arguments=get_field_args_nodes_from_obj_field_args(metadata, field_args), + arguments=get_field_args_nodes_from_obj_field_args(metadata, field.args), ) @@ -561,8 +746,11 @@ def get_field_args_out_names( def get_field_args_nodes_from_obj_field_args( - metadata: GraphQLMetadata, field_args: Dict[str, GraphQLObjectFieldArg] -) -> Tuple[InputValueDefinitionNode]: + metadata: GraphQLMetadata, field_args: Optional[Dict[str, GraphQLObjectFieldArg]] +) -> Optional[Tuple[InputValueDefinitionNode]]: + if not field_args: + return None + return tuple( get_field_arg_node_from_obj_field_arg(metadata, field_arg) for field_arg in field_args.values() @@ -585,7 +773,7 @@ def update_field_args_options( args_options: Optional[Dict[str, dict]], ) -> Dict[str, GraphQLObjectFieldArg]: if not args_options: - field_args + return field_args updated_args: Dict[str, GraphQLObjectFieldArg] = {} for arg_name in field_args: diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index dd1c2e2..30c8d7d 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -2,7 +2,16 @@ from importlib import import_module from inspect import isclass from types import UnionType -from typing import Annotated, Any, ForwardRef, Optional, Union, get_args, get_origin +from typing import ( + Annotated, + Any, + ForwardRef, + Optional, + Type, + Union, + get_args, + get_origin, +) from graphql import ( ListTypeNode, @@ -13,7 +22,7 @@ ) from .base import GraphQLMetadata, GraphQLType -from .deferredtype import DeferredType +from .deferredtype import DeferredTypeData def get_type_node(metadata: GraphQLMetadata, type_hint: Any) -> TypeNode: @@ -37,7 +46,7 @@ def get_type_node(metadata: GraphQLMetadata, type_hint: Any) -> TypeNode: type_node = NamedTypeNode(name=NameNode(value="Boolean")) elif get_origin(type_hint) is Annotated: forward_ref, type_meta = get_args(type_hint) - if not type_meta or not isinstance(type_meta, DeferredType): + if not type_meta or not isinstance(type_meta, DeferredTypeData): raise ValueError( f"Can't create a GraphQL return type for '{type_hint}'. " "Second argument of 'Annotated' is expected to be a return " @@ -87,7 +96,7 @@ def unwrap_type(type_hint: Any) -> Any: def get_deferred_type( - type_hint: Any, forward_ref: ForwardRef, deferred_type: DeferredType + type_hint: Any, forward_ref: ForwardRef, deferred_type: DeferredTypeData ) -> Optional[Union[GraphQLType, Enum]]: type_name = forward_ref.__forward_arg__ module = import_module(deferred_type.path) @@ -99,11 +108,15 @@ def get_deferred_type( return graphql_type -def get_graphql_type(type_hint: Any) -> Optional[Union[GraphQLType, Enum]]: - if not isclass(type_hint): +def get_graphql_type(annotation: Any) -> Optional[Union[Type[GraphQLType], Type[Enum]]]: + """Utility that extracts GraphQL type from type annotation""" + if is_nullable(annotation) or is_list(annotation): + return get_graphql_type(unwrap_type(annotation)) + + if not isclass(annotation): return None - if issubclass(type_hint, (GraphQLType, Enum)): - return type_hint + if issubclass(annotation, (GraphQLType, Enum)): + return annotation return None diff --git a/ariadne_graphql_modules/next/validators.py b/ariadne_graphql_modules/next/validators.py index a0332c1..fee52f2 100644 --- a/ariadne_graphql_modules/next/validators.py +++ b/ariadne_graphql_modules/next/validators.py @@ -1,6 +1,6 @@ from typing import Any, Type -from .objecttype import GraphQLType +from .base import GraphQLType def validate_name(cls: Type[GraphQLType], definition: Any): diff --git a/tests_next/snapshots/snap_test_metadata.py b/tests_next/snapshots/snap_test_metadata.py new file mode 100644 index 0000000..4f30414 --- /dev/null +++ b/tests_next/snapshots/snap_test_metadata.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_metadata_raises_key_error_for_unset_data 1'] = '"No data is set for \'\'."' diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py index 07c0bf9..a575c49 100644 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -7,14 +7,48 @@ snapshots = Snapshot() +snapshots['test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'userId'." + +snapshots['test_object_type_validation_fails_for_field_with_multiple_args 1'] = "Class 'CustomType' defines multiple resolvers for field 'lorem'." + +snapshots['test_object_type_validation_fails_for_field_with_multiple_descriptions 1'] = "Class 'CustomType' defines multiple descriptions for field 'hello'." + +snapshots['test_object_type_validation_fails_for_invalid_alias 1'] = "Class 'CustomType' defines an alias for an undefined field 'invalid'. (Valid fields: 'hello')" + +snapshots['test_object_type_validation_fails_for_invalid_schema_alias 1'] = "Class 'CustomType' defines an alias for an undefined field 'invalid'. (Valid fields: 'hello')" + snapshots['test_object_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" snapshots['test_object_type_validation_fails_for_missing_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (expected one of: 'name')" -snapshots['test_object_type_validation_fails_for_missing_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argumentthats not defined on the resolver function. (expected one of: 'name')" +snapshots['test_object_type_validation_fails_for_missing_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (expected one of: 'name')" + +snapshots['test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'userId'." + +snapshots['test_object_type_validation_fails_for_multiple_field_resolvers 1'] = "Class 'CustomType' defines multiple resolvers for field 'hello'." + +snapshots['test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'hello'." + +snapshots['test_object_type_validation_fails_for_multiple_schema_field_resolvers 1'] = "Class 'CustomType' defines multiple resolvers for field 'hello'." snapshots['test_object_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" +snapshots['test_object_type_validation_fails_for_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." + +snapshots['test_object_type_validation_fails_for_schema_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." + +snapshots['test_object_type_validation_fails_for_schema_field_with_arg_double_description 1'] = "Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' field." + +snapshots['test_object_type_validation_fails_for_schema_field_with_arg_name 1'] = "Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." + +snapshots['test_object_type_validation_fails_for_schema_field_with_arg_type 1'] = "Class 'CustomType' defines 'type' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." + +snapshots['test_object_type_validation_fails_for_schema_field_with_invalid_arg_name 1'] = "Class 'CustomType' defines options for 'other' argument of the 'hello' field that doesn't exist." + +snapshots['test_object_type_validation_fails_for_schema_field_with_multiple_descriptions 1'] = "Class 'CustomType' defines multiple descriptions for field 'hello'." + +snapshots['test_object_type_validation_fails_for_schema_with_separate_field 1'] = "Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for types defining '__schema__'." + snapshots['test_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined attribute 'other'. (Valid attrs: 'hello')" @@ -23,4 +57,4 @@ snapshots['test_object_type_validation_fails_for_undefined_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" -snapshots['test_object_type_validation_fails_for_undefined_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argumentthats not defined on the resolver function. (function accepts no extra arguments)" +snapshots['test_object_type_validation_fails_for_undefined_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py new file mode 100644 index 0000000..e78ec9a --- /dev/null +++ b/tests_next/test_input_type.py @@ -0,0 +1,44 @@ +from typing import Optional + +from graphql import graphql_sync + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import ( + GraphQLInput, + GraphQLObject, + make_executable_schema, +) + + +def test_field_with_input_type_arg(assert_schema_equals): + class SearchInput(GraphQLInput): + query: Optional[str] + age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + age: Int + } + """, + ) + + result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') + + assert not result.errors + assert result.data == {"hello": "Hello World!"} diff --git a/tests_next/test_metadata.py b/tests_next/test_metadata.py index 20a086f..b451ec6 100644 --- a/tests_next/test_metadata.py +++ b/tests_next/test_metadata.py @@ -1,5 +1,7 @@ from enum import Enum +import pytest + from ariadne_graphql_modules.next import GraphQLObject, graphql_enum @@ -7,6 +9,18 @@ class QueryType(GraphQLObject): hello: str +def test_metadata_returns_sets_and_returns_data_for_type(metadata): + assert metadata.set_data(QueryType, 42) == 42 + assert metadata.get_data(QueryType) == 42 + + +def test_metadata_raises_key_error_for_unset_data(snapshot, metadata): + with pytest.raises(KeyError) as exc_info: + metadata.get_data(QueryType) + + snapshot.assert_match(str(exc_info.value)) + + def test_metadata_returns_model_for_type(assert_ast_equals, metadata): model = metadata.get_graphql_model(QueryType) assert model.name == "Query" diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index 7b9602f..a94cdbd 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -190,13 +190,13 @@ def post(obj, info): post: Post! } - type Post { - message: String! - } - type User { name: String! } + + type Post { + message: String! + } """, ) @@ -242,7 +242,7 @@ def resolve_hello(*_): def test_resolver_decorator_sets_resolver_for_instance_field(assert_schema_equals): class QueryType(GraphQLObject): - hello: str = GraphQLObject.field() + hello: str = GraphQLObject.field(name="hello") @GraphQLObject.resolver("hello") def resolve_hello(*_): @@ -344,6 +344,34 @@ def hello(obj, info) -> str: assert result.data == {"hello": "Hello World!"} +def test_field_decorator_sets_description_for_field_arg(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field( + args={"name": GraphQLObject.argument(description="Lorem ipsum.")} + ) + def hello(obj, info, name: str) -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + def test_resolver_decorator_sets_description_for_type_hint_field(assert_schema_equals): class QueryType(GraphQLObject): hello: str @@ -368,3 +396,140 @@ def resolve_hello(*_): assert not result.errors assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_description_for_field_in_schema(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + @GraphQLObject.resolver("hello", description="Lorem ipsum.") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + \"\"\"Lorem ipsum.\"\"\" + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_description_for_field_arg(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(description="Lorem ipsum.")} + ) + def resolve_hello(obj, info, name: str) -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_schema_sets_description_for_field_arg(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! + } + """ + ) + + @GraphQLObject.resolver("hello") + def resolve_hello(*_, name: str): + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_resolver_decorator_sets_description_for_field_arg_in_schema( + assert_schema_equals, +): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello(name: String!): String! + } + """ + ) + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(description="Description")} + ) + def resolve_hello(*_, name: str): + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Description\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index f9acf06..9f5518b 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -72,6 +72,50 @@ class CustomType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) +def test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + user_id: str + user__id: str + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + user_id: str + + @GraphQLObject.field(name="userId") + def lorem(*_) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + @GraphQLObject.field(name="hello") + def lorem(*_) -> str: + return "Hello World!" + + @GraphQLObject.field(name="hello") + def ipsum(*_) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + def test_object_type_validation_fails_for_undefined_field_resolver_arg(snapshot): with pytest.raises(ValueError) as exc_info: @@ -122,3 +166,271 @@ def resolve_hello(*_, name: str): return "Hello World!" snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_multiple_field_resolvers(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + @GraphQLObject.resolver("hello") + def resolve_hello_other(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_multiple_schema_field_resolvers(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + @GraphQLObject.resolver("hello") + def resolve_hello_other(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_field_with_multiple_descriptions( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + hello: str = GraphQLObject.field(description="Description") + + @GraphQLObject.resolver("hello", description="Other") + def ipsum(*_) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_field_with_multiple_descriptions( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = """ + type Custom { + \"\"\"Description\"\"\" + hello: String! + } + """ + + @GraphQLObject.resolver("hello", description="Other") + def ipsum(*_) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_field_with_invalid_arg_name( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = """ + type Custom { + hello(name: String!): String! + } + """ + + @GraphQLObject.resolver( + "hello", args={"other": GraphQLObject.argument(description="Ok")} + ) + def ipsum(*_, name: str) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_field_with_arg_name( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = """ + type Custom { + hello(name: String!): String! + } + """ + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(name="Other")} + ) + def ipsum(*_, name: str) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_field_with_arg_type( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = """ + type Custom { + hello(name: String!): String! + } + """ + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(type=str)} + ) + def ipsum(*_, name: str) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_field_with_arg_double_description( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = """ + type Custom { + hello( + \"\"\"Description\"\"\" + name: String! + ): String! + } + """ + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(description="Other")} + ) + def ipsum(*_, name: str) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_field_with_multiple_args( + snapshot, +): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + @GraphQLObject.field(name="hello", args={""}) + def lorem(*_, a: int) -> str: + return "Hello World!" + + @GraphQLObject.resolver("lorem", description="Other") + def ipsum(*_) -> str: + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_invalid_alias(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + hello: str + + __aliases__ = { + "invalid": "ok", + } + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_invalid_schema_alias(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + __aliases__ = { + "invalid": "ok", + } + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_resolver_alias(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + hello: str + + __aliases__ = { + "hello": "ok", + } + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_alias(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + __aliases__ = { + "hello": "ok", + } + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_with_separate_field(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + hello = GraphQLObject.field(lambda *_: "noop") + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_object_type_with_schema.py b/tests_next/test_object_type_with_schema.py deleted file mode 100644 index c6e242b..0000000 --- a/tests_next/test_object_type_with_schema.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_something(): - assert 1 + 2 == 3 diff --git a/tests_next/test_typing.py b/tests_next/test_typing.py index f4b236a..c9fcc99 100644 --- a/tests_next/test_typing.py +++ b/tests_next/test_typing.py @@ -4,7 +4,7 @@ from graphql import ListTypeNode, NameNode, NamedTypeNode, NonNullTypeNode from ariadne_graphql_modules.next import GraphQLObject, deferred, graphql_enum -from ariadne_graphql_modules.next.typing import get_type_node +from ariadne_graphql_modules.next.typing import get_graphql_type, get_type_node if TYPE_CHECKING: from .types import ForwardEnum, ForwardScalar @@ -34,6 +34,36 @@ def assert_named_type(type_node, name: str): assert type_node.name.value == name +def test_get_graphql_type_from_python_builtin_type_returns_none(metadata): + assert get_graphql_type(Optional[str]) is None + assert get_graphql_type(Union[int, None]) is None + assert get_graphql_type(float | None) is None + assert get_graphql_type(Optional[bool]) is None + + +def test_get_graphql_type_from_graphql_type_subclass_returns_type(metadata): + class UserType(GraphQLObject): + ... + + assert get_graphql_type(UserType) == UserType + assert get_graphql_type(Optional[UserType]) == UserType + assert get_graphql_type(List[UserType]) == UserType + assert get_graphql_type(Optional[List[Optional[UserType]]]) == UserType + + +def test_get_graphql_type_from_enum_returns_type(metadata): + class UserLevel(Enum): + GUEST = 0 + MEMBER = 1 + MODERATOR = 2 + ADMINISTRATOR = 3 + + assert get_graphql_type(UserLevel) == UserLevel + assert get_graphql_type(Optional[UserLevel]) == UserLevel + assert get_graphql_type(List[UserLevel]) == UserLevel + assert get_graphql_type(Optional[List[Optional[UserLevel]]]) == UserLevel + + def test_get_graphql_type_node_from_python_builtin_type(metadata): assert_named_type(get_type_node(metadata, Optional[str]), "String") assert_named_type(get_type_node(metadata, Union[int, None]), "Int") From 35a91cc3eb4c72b65b675a5def6f3cbf1c67962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Mon, 23 Oct 2023 16:58:39 +0200 Subject: [PATCH 14/63] Input types --- ariadne_graphql_modules/next/__init__.py | 6 + ariadne_graphql_modules/next/convert_name.py | 13 ++ ariadne_graphql_modules/next/inputtype.py | 148 +++++++++++++++++- ariadne_graphql_modules/next/objecttype.py | 7 +- .../snap_test_object_type_validation.py | 6 +- tests_next/test_input_type.py | 120 +++++++++++++- tests_next/test_object_type_validation.py | 51 +++--- 7 files changed, 320 insertions(+), 31 deletions(-) diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index da65989..e14e6e1 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -1,4 +1,8 @@ from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .convert_name import ( + convert_graphql_name_to_python, + convert_python_name_to_graphql, +) from .deferredtype import deferred from .description import get_description_node from .enumtype import ( @@ -24,6 +28,8 @@ "GraphQLScalar", "GraphQScalarModel", "GraphQLType", + "convert_graphql_name_to_python", + "convert_python_name_to_graphql", "create_graphql_enum_model", "deferred", "get_description_node", diff --git a/ariadne_graphql_modules/next/convert_name.py b/ariadne_graphql_modules/next/convert_name.py index 471585e..0451fbd 100644 --- a/ariadne_graphql_modules/next/convert_name.py +++ b/ariadne_graphql_modules/next/convert_name.py @@ -12,3 +12,16 @@ def convert_python_name_to_graphql(python_name: str) -> str: final_name += c.lower() return final_name + + +def convert_graphql_name_to_python(graphql_name: str) -> str: + final_name = "" + for i, c in enumerate(graphql_name.lower()): + if not i: + final_name += c + else: + if final_name[-1] != "_" and (c != graphql_name[i] or c.isdigit()): + final_name += "_" + final_name += c + + return final_name diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py index 4352458..ebd7a2c 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/inputtype.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Dict, Iterable, List, Optional, Type +from typing import Any, Dict, Iterable, List, Optional, Type, cast from ariadne import InputType as InputTypeBindable from graphql import ( @@ -9,16 +9,33 @@ NameNode, ) +from ..utils import parse_definition from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .convert_name import convert_python_name_to_graphql +from .convert_name import ( + convert_graphql_name_to_python, + convert_python_name_to_graphql, +) from .description import get_description_node from .typing import get_graphql_type, get_type_node from .validators import validate_description, validate_name class GraphQLInput(GraphQLType): - def __init__(self, data: dict): - raise NotImplementedError("GraphQLType.__init__() is not implemented") + __kwargs__: List[str] + __out_names__: Optional[Dict[str, str]] = None + + def __init__(self, **kwargs: Any): + for kwarg in self.__kwargs__: + setattr(self, kwarg, kwargs.get(kwarg)) + + for kwarg in kwargs: + if kwarg not in self.__kwargs__: + valid_kwargs = "', '".join(self.__kwargs__) + raise TypeError( + f"{type(self).__name__}.__init__() got an unexpected " + f"keyword argument '{kwarg}'. " + f"Valid keyword arguments: '{valid_kwargs}'" + ) def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -33,6 +50,10 @@ def __init_subclass__(cls) -> None: else: validate_input_type(cls) + @classmethod + def create_from_data(cls, data: Dict[str, Any]) -> "GraphQLInput": + return cls(**data) + @classmethod def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() @@ -46,7 +67,40 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": def __get_graphql_model_with_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLInputModel": - pass + definition = cast( + InputObjectTypeDefinitionNode, + parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), + ) + + out_names: Dict[str, str] = {} + if getattr(cls, "__out_names__"): + out_names.update(cls.__out_names__) + + fields: List[InputValueDefinitionNode] = [] + for field in definition.fields: + fields.append( + InputValueDefinitionNode( + name=field.name, + description=(field.description), + directives=field.directives, + type=field.type, + ) + ) + + field_name = field.name.value + if field_name not in out_names: + out_names[field_name] = convert_graphql_name_to_python(field_name) + + return GraphQLInputModel( + name=definition.name.value, + ast_type=InputObjectTypeDefinitionNode, + ast=InputObjectTypeDefinitionNode( + name=NameNode(value=definition.name.value), + fields=tuple(fields), + ), + out_type=cls.create_from_data, + out_names=out_names, + ) @classmethod def __get_graphql_model_without_schema__( @@ -83,6 +137,8 @@ def __get_graphql_model_without_schema__( ) ) + out_names[hint_field_name] = hint_name + return GraphQLInputModel( name=name, ast_type=InputObjectTypeDefinitionNode, @@ -93,7 +149,7 @@ def __get_graphql_model_without_schema__( ), fields=tuple(fields_ast), ), - out_type=cls, + out_type=cls.create_from_data, out_names=out_names, ) @@ -139,11 +195,87 @@ def field( def validate_input_type_with_schema(cls: Type[GraphQLInput]): - pass + definition = cast( + InputObjectTypeDefinitionNode, + parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), + ) + + if not isinstance(definition, InputObjectTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{InputObjectTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + if not definition.fields: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an input type without any fields. " + ) + + fields_names: List[str] = [field.name.value for field in definition.fields] + used_out_names: List[str] = [] + + out_names: Dict[str, str] = getattr(cls, "__out_names__", {}) or {} + for field_name, out_name in out_names.items(): + if field_name not in fields_names: + raise ValueError( + f"Class '{cls.__name__}' defines an outname for '{field_name}' " + "field in it's '__out_names__' attribute which is not defined " + "in '__schema__'." + ) + + if out_name in used_out_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with an outname " + f"'{out_name}' in it's '__out_names__' attribute." + ) + + used_out_names.append(out_name) + + cls.__kwargs__ = get_input_type_with_schema_kwargs(cls, definition, out_names) + + +def get_input_type_with_schema_kwargs( + cls: Type[GraphQLInput], + definition: InputObjectTypeDefinitionNode, + out_names: Dict[str, str], +) -> List[str]: + kwargs: List[str] = [] + for field in definition.fields: + try: + kwargs.append(out_names[field.name.value]) + except KeyError: + kwargs.append(convert_graphql_name_to_python(field.name.value)) + return kwargs def validate_input_type(cls: Type[GraphQLInput]): - pass + if cls.__out_names__: + raise ValueError( + f"Class '{cls.__name__}' defines '__out_names__' attribute. " + "This is not supported for types not defining '__schema__'." + ) + + cls.__kwargs__ = get_input_type_kwargs(cls) + + +def get_input_type_kwargs(cls: Type[GraphQLInput]) -> List[str]: + fields: List[str] = [] + + for attr_name in cls.__annotations__: + if attr_name.startswith("__"): + continue + + attr_value = getattr(cls, attr_name, None) + if attr_value is None or isinstance(attr_value, GraphQLInputField): + fields.append(attr_name) + + return fields class GraphQLInputField: diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index d672b7a..cae61f0 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -125,7 +125,6 @@ def __get_graphql_model_with_schema__( type=field.type, ) ) - print(descriptions) return GraphQLObjectModel( name=definition.name.value, @@ -274,6 +273,12 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]): validate_name(cls, definition) validate_description(cls, definition) + if not definition.fields: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an object type without any fields. " + ) + field_names: List[str] = [f.name.value for f in definition.fields] field_definitions: Dict[str, FieldDefinitionNode] = { f.name.value: f for f in definition.fields diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py index a575c49..5d246be 100644 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -35,8 +35,6 @@ snapshots['test_object_type_validation_fails_for_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." -snapshots['test_object_type_validation_fails_for_schema_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." - snapshots['test_object_type_validation_fails_for_schema_field_with_arg_double_description 1'] = "Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' field." snapshots['test_object_type_validation_fails_for_schema_field_with_arg_name 1'] = "Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." @@ -47,6 +45,10 @@ snapshots['test_object_type_validation_fails_for_schema_field_with_multiple_descriptions 1'] = "Class 'CustomType' defines multiple descriptions for field 'hello'." +snapshots['test_object_type_validation_fails_for_schema_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an object type without any fields. " + +snapshots['test_object_type_validation_fails_for_schema_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." + snapshots['test_object_type_validation_fails_for_schema_with_separate_field 1'] = "Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for types defining '__schema__'." snapshots['test_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py index e78ec9a..dc94d55 100644 --- a/tests_next/test_input_type.py +++ b/tests_next/test_input_type.py @@ -10,7 +10,7 @@ ) -def test_field_with_input_type_arg(assert_schema_equals): +def test_input_type_arg(assert_schema_equals): class SearchInput(GraphQLInput): query: Optional[str] age: Optional[int] @@ -41,4 +41,120 @@ def resolve_search(*_, input: SearchInput) -> str: result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') assert not result.errors - assert result.data == {"hello": "Hello World!"} + assert result.data == {"search": "['Hello', None]"} + + +def test_schema_input_type_arg(assert_schema_equals): + class SearchInput(GraphQLInput): + __schema__ = """ + input SearchInput { + query: String + age: Int + } + """ + + query: Optional[str] + age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + age: Int + } + """, + ) + + result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') + + assert not result.errors + assert result.data == {"search": "['Hello', None]"} + + +def test_input_type_automatic_out_name_arg(assert_schema_equals): + class SearchInput(GraphQLInput): + query: Optional[str] + min_age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.min_age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + minAge: Int + } + """, + ) + + result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + + assert not result.errors + assert result.data == {"search": "[None, 21]"} + + +def test_schema_input_type_automatic_out_name_arg(assert_schema_equals): + class SearchInput(GraphQLInput): + __schema__ = """ + input SearchInput { + query: String + minAge: Int + } + """ + + query: Optional[str] + min_age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.min_age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + minAge: Int + } + """, + ) + + result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + + assert not result.errors + assert result.data == {"search": "[None, 21]"} diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index 9f5518b..a12db16 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -18,6 +18,38 @@ def test_object_type_validation_fails_for_names_not_matching(snapshot): class CustomType(GraphQLObject): __graphql_name__ = "Lorem" + __schema__ = gql( + """ + type Custom { + hello: String + } + """ + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_two_descriptions(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __description__ = "Hello world!" + __schema__ = gql( + """ + \"\"\"Other description\"\"\" + type Query { + hello: String! + } + """ + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_schema_missing_fields(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): __schema__ = gql("type Custom") snapshot.assert_match(str(exc_info.value)) @@ -55,23 +87,6 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_two_descriptions(snapshot): - with pytest.raises(ValueError) as exc_info: - - class CustomType(GraphQLObject): - __description__ = "Hello world!" - __schema__ = gql( - """ - \"\"\"Other description\"\"\" - type Query { - hello: String! - } - """ - ) - - snapshot.assert_match(str(exc_info.value)) - - def test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name( snapshot, ): @@ -396,7 +411,7 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_alias(snapshot): +def test_object_type_validation_fails_for_schema_resolver_alias(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): From c59e62dfe29d7a5e30e3454582299885593d5ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Tue, 24 Oct 2023 18:24:56 +0200 Subject: [PATCH 15/63] Recurrent types, ID type --- ariadne_graphql_modules/next/__init__.py | 2 + ariadne_graphql_modules/next/base.py | 11 +- ariadne_graphql_modules/next/idtype.py | 23 +++ ariadne_graphql_modules/next/inputtype.py | 5 +- ariadne_graphql_modules/next/objecttype.py | 6 +- ariadne_graphql_modules/next/typing.py | 26 ++- .../snap_test_input_type_validation.py | 22 +++ tests_next/test_id_type.py | 153 ++++++++++++++++++ tests_next/test_input_type.py | 90 +++++++++++ tests_next/test_input_type_validation.py | 108 +++++++++++++ tests_next/test_object_type.py | 52 ++++++ 11 files changed, 489 insertions(+), 9 deletions(-) create mode 100644 ariadne_graphql_modules/next/idtype.py create mode 100644 tests_next/snapshots/snap_test_input_type_validation.py create mode 100644 tests_next/test_id_type.py create mode 100644 tests_next/test_input_type_validation.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index e14e6e1..29afe1f 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -12,6 +12,7 @@ graphql_enum, ) from .executable_schema import make_executable_schema +from .idtype import GraphQLID from .inputtype import GraphQLInput, GraphQLInputModel from .objecttype import GraphQLObject, GraphQLObjectModel, object_field from .scalartype import GraphQLScalar, GraphQScalarModel @@ -19,6 +20,7 @@ __all__ = [ "GraphQLEnum", "GraphQLEnumModel", + "GraphQLID", "GraphQLInput", "GraphQLInputModel", "GraphQLMetadata", diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 79b6c61..e8d02dd 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -71,6 +71,7 @@ def bind_to_schema(self, schema: GraphQLSchema): @dataclass(frozen=True) class GraphQLMetadata: data: Dict[Union[Type[GraphQLType], Type[Enum]], Any] = field(default_factory=dict) + names: Dict[Union[Type[GraphQLType], Type[Enum]], str] = field(default_factory=dict) models: Dict[Union[Type[GraphQLType], Type[Enum]], GraphQLModel] = field( default_factory=dict ) @@ -100,6 +101,12 @@ def get_graphql_model( return self.models[type] + def set_graphql_name(self, type: Union[Type[GraphQLType], Type[Enum]], name: str): + self.names[type] = name + def get_graphql_name(self, type: Union[Type[GraphQLType], Type[Enum]]) -> str: - model = self.get_graphql_model(type) - return model.name + if type not in self.names: + model = self.get_graphql_model(type) + self.set_graphql_name(type, model.name) + + return self.names[type] diff --git a/ariadne_graphql_modules/next/idtype.py b/ariadne_graphql_modules/next/idtype.py new file mode 100644 index 0000000..4b8ac09 --- /dev/null +++ b/ariadne_graphql_modules/next/idtype.py @@ -0,0 +1,23 @@ +from typing import Any, Union + + +class GraphQLID: + __slots__ = ("value",) + + value: str + + def __init__(self, value: Union[int, str] = None): + self.value = str(value) + + def __eq__(self, value: Any) -> bool: + if isinstance(value, (str, int)): + return self.value == str(value) + if isinstance(value, GraphQLID): + return self.value == value.value + return False + + def __int__(self) -> int: + return int(self.value) + + def __str__(self) -> str: + return self.value diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py index ebd7a2c..81d445a 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/inputtype.py @@ -57,6 +57,7 @@ def create_from_data(cls, data: Dict[str, Any]) -> "GraphQLInput": @classmethod def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() + metadata.set_graphql_name(cls, name) if getattr(cls, "__schema__", None): return cls.__get_graphql_model_with_schema__(metadata, name) @@ -130,6 +131,7 @@ def __get_graphql_model_without_schema__( hint_field_name = convert_python_name_to_graphql(hint_name) fields_ast.append( get_field_node_from_type_hint( + cls, metadata, hint_field_name, hint_type, @@ -309,6 +311,7 @@ def bind_to_schema(self, schema: GraphQLSchema): def get_field_node_from_type_hint( + parent_type: GraphQLInput, metadata: GraphQLMetadata, field_name: str, field_type: Any, @@ -317,6 +320,6 @@ def get_field_node_from_type_hint( return InputValueDefinitionNode( description=get_description_node(field_description), name=NameNode(value=field_name), - type=get_type_node(metadata, field_type), + type=get_type_node(metadata, field_type, parent_type), default_value=None, ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index cae61f0..dd69292 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -57,6 +57,7 @@ def __init_subclass__(cls) -> None: @classmethod def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() + metadata.set_graphql_name(cls, name) if getattr(cls, "__schema__", None): return cls.__get_graphql_model_with_schema__(metadata, name) @@ -149,7 +150,7 @@ def __get_graphql_model_without_schema__( out_names: Dict[str, Dict[str, str]] = {} for field in type_data.fields.values(): - fields_ast.append(get_field_node_from_obj_field(metadata, field)) + fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) if field.resolver: resolvers[field.name] = field.resolver @@ -680,13 +681,14 @@ def bind_to_schema(self, schema: GraphQLSchema): def get_field_node_from_obj_field( + parent_type: GraphQLObject, metadata: GraphQLMetadata, field: GraphQLObjectField, ) -> FieldDefinitionNode: return FieldDefinitionNode( description=get_description_node(field.description), name=NameNode(value=field.name), - type=get_type_node(metadata, field.type), + type=get_type_node(metadata, field.type, parent_type), arguments=get_field_args_nodes_from_obj_field_args(metadata, field.args), ) diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index 30c8d7d..3c382fd 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -23,9 +23,14 @@ from .base import GraphQLMetadata, GraphQLType from .deferredtype import DeferredTypeData +from .idtype import GraphQLID -def get_type_node(metadata: GraphQLMetadata, type_hint: Any) -> TypeNode: +def get_type_node( + metadata: GraphQLMetadata, + type_hint: Any, + parent_type: Optional[GraphQLType] = None, +) -> TypeNode: if is_nullable(type_hint): nullable = True type_hint = unwrap_type(type_hint) @@ -35,7 +40,9 @@ def get_type_node(metadata: GraphQLMetadata, type_hint: Any) -> TypeNode: type_node = None if is_list(type_hint): list_item_type_hint = unwrap_type(type_hint) - type_node = ListTypeNode(type=get_type_node(metadata, list_item_type_hint)) + type_node = ListTypeNode( + type=get_type_node(metadata, list_item_type_hint, parent_type=parent_type) + ) elif type_hint == str: type_node = NamedTypeNode(name=NameNode(value="String")) elif type_hint == int: @@ -57,10 +64,21 @@ def get_type_node(metadata: GraphQLMetadata, type_hint: Any) -> TypeNode: type_node = NamedTypeNode( name=NameNode(value=metadata.get_graphql_name(deferred_type)), ) - elif isclass(type_hint) and issubclass(type_hint, (GraphQLType, Enum)): + elif isinstance(type_hint, ForwardRef): + type_name = type_hint.__forward_arg__ + if not parent_type or parent_type.__name__ != type_name: + ... + type_node = NamedTypeNode( - name=NameNode(value=metadata.get_graphql_name(type_hint)), + name=NameNode(value=metadata.get_graphql_name(parent_type)), ) + elif isclass(type_hint): + if issubclass(type_hint, GraphQLID): + type_node = NamedTypeNode(name=NameNode(value="ID")) + elif issubclass(type_hint, (GraphQLType, Enum)): + type_node = NamedTypeNode( + name=NameNode(value=metadata.get_graphql_name(type_hint)), + ) if not type_node: raise ValueError(f"Can't create a GraphQL return type for '{type_hint}'.") diff --git a/tests_next/snapshots/snap_test_input_type_validation.py b/tests_next/snapshots/snap_test_input_type_validation.py new file mode 100644 index 0000000..41f3745 --- /dev/null +++ b/tests_next/snapshots/snap_test_input_type_validation.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_input_type_validation_fails_for_duplicate_schema_out_name 1'] = "Class 'CustomType' defines multiple fields with an outname 'ok' in it's '__out_names__' attribute." + +snapshots['test_input_type_validation_fails_for_invalid_schema_out_name 1'] = "Class 'CustomType' defines an outname for 'invalid' field in it's '__out_names__' attribute which is not defined in '__schema__'." + +snapshots['test_input_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'InputObjectTypeDefinitionNode')" + +snapshots['test_input_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" + +snapshots['test_input_type_validation_fails_for_out_names_without_schema 1'] = "Class 'CustomType' defines '__out_names__' attribute. This is not supported for types not defining '__schema__'." + +snapshots['test_input_type_validation_fails_for_schema_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an input type without any fields. " + +snapshots['test_input_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/test_id_type.py b/tests_next/test_id_type.py new file mode 100644 index 0000000..1a4b2e1 --- /dev/null +++ b/tests_next/test_id_type.py @@ -0,0 +1,153 @@ +from graphql import graphql_sync + +from ariadne_graphql_modules.next import ( + GraphQLID, + GraphQLInput, + GraphQLObject, + make_executable_schema, +) + + +def test_graphql_id_instance_from_int_is_casted_to_str(): + gid = GraphQLID(123) + assert gid.value == "123" + + +def test_graphql_id_instance_from_str_remains_as_str(): + gid = GraphQLID("obj-id") + assert gid.value == "obj-id" + + +def test_graphql_id_can_be_cast_to_int(): + gid = GraphQLID(123) + assert int(gid) == 123 + + +def test_graphql_id_can_be_cast_to_str(): + gid = GraphQLID(123) + assert str(gid) == "123" + + +def test_graphql_id_can_be_compared_to_other_id(): + assert GraphQLID("123") == GraphQLID("123") + assert GraphQLID(123) == GraphQLID(123) + assert GraphQLID(123) == GraphQLID("123") + assert GraphQLID("123") == GraphQLID(123) + + assert GraphQLID("123") != GraphQLID("321") + assert GraphQLID(123) != GraphQLID(321) + assert GraphQLID(321) != GraphQLID("123") + assert GraphQLID("123") != GraphQLID(321) + + +def test_graphql_id_can_be_compared_to_str(): + assert GraphQLID("123") == "123" + assert GraphQLID(123) == "123" + + assert GraphQLID("123") != "321" + assert GraphQLID(123) != "321" + + +def test_graphql_id_can_be_compared_to_int(): + assert GraphQLID("123") == 123 + assert GraphQLID(123) == 123 + + assert GraphQLID("123") != 321 + assert GraphQLID(123) != 321 + + +def test_graphql_id_object_field_type_hint(assert_schema_equals): + class QueryType(GraphQLObject): + id: GraphQLID + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + id: ID! + } + """, + ) + + result = graphql_sync(schema, "{ id }", root_value={"id": 123}) + + assert not result.errors + assert result.data == {"id": "123"} + + +def test_graphql_id_object_field_instance(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field() + def id(*_) -> GraphQLID: + return GraphQLID(115) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + id: ID! + } + """, + ) + + result = graphql_sync(schema, "{ id }") + + assert not result.errors + assert result.data == {"id": "115"} + + +def test_graphql_id_object_field_instance_arg(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field() + def id(*_, arg: GraphQLID) -> str: + return str(arg) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + id(arg: ID!): String! + } + """, + ) + + result = graphql_sync(schema, '{ id(arg: "123") }') + + assert not result.errors + assert result.data == {"id": "123"} + + +def test_graphql_id_input_field(assert_schema_equals): + class ArgType(GraphQLInput): + id: GraphQLID + + class QueryType(GraphQLObject): + @GraphQLObject.field() + def id(*_, arg: ArgType) -> str: + return str(arg.id) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + id(arg: Arg!): String! + } + + input Arg { + id: ID! + } + """, + ) + + result = graphql_sync(schema, '{ id(arg: {id: "123"}) }') + + assert not result.errors + assert result.data == {"id": "123"} diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py index dc94d55..e182bfb 100644 --- a/tests_next/test_input_type.py +++ b/tests_next/test_input_type.py @@ -158,3 +158,93 @@ def resolve_search(*_, input: SearchInput) -> str: assert not result.errors assert result.data == {"search": "[None, 21]"} + + +def test_schema_input_type_explicit_out_name_arg(assert_schema_equals): + class SearchInput(GraphQLInput): + __schema__ = """ + input SearchInput { + query: String + minAge: Int + } + """ + __out_names__ = {"minAge": "age"} + + query: Optional[str] + age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + minAge: Int + } + """, + ) + + result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + + assert not result.errors + assert result.data == {"search": "[None, 21]"} + + +def test_input_type_self_reference(assert_schema_equals): + class SearchInput(GraphQLInput): + query: Optional[str] + extra: Optional["SearchInput"] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + if input.extra: + extra_repr = input.extra.query + else: + extra_repr = None + + return f"{repr([input.query, extra_repr])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + extra: SearchInput + } + """, + ) + + result = graphql_sync( + schema, + """ + { + search( + input: { query: "Hello", extra: { query: "Other" } } + ) + } + """, + ) + + assert not result.errors + assert result.data == {"search": "['Hello', 'Other']"} diff --git a/tests_next/test_input_type_validation.py b/tests_next/test_input_type_validation.py new file mode 100644 index 0000000..bdc879f --- /dev/null +++ b/tests_next/test_input_type_validation.py @@ -0,0 +1,108 @@ +import pytest + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import GraphQLInput + + +def test_input_type_validation_fails_for_invalid_type_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLInput): + __schema__ = gql("scalar Custom") + + snapshot.assert_match(str(exc_info.value)) + + +def test_input_type_validation_fails_for_names_not_matching(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLInput): + __graphql_name__ = "Lorem" + __schema__ = gql( + """ + input Custom { + hello: String! + } + """ + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_input_type_validation_fails_for_two_descriptions(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLInput): + __description__ = "Hello world!" + __schema__ = gql( + """ + \"\"\"Other description\"\"\" + input Custom { + hello: String! + } + """ + ) + + snapshot.assert_match(str(exc_info.value)) + + +def test_input_type_validation_fails_for_schema_missing_fields(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLInput): + __schema__ = gql("input Custom") + + snapshot.assert_match(str(exc_info.value)) + + +def test_input_type_validation_fails_for_out_names_without_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLInput): + hello: str + + __out_names__ = { + "hello": "ok", + } + + snapshot.assert_match(str(exc_info.value)) + + +def test_input_type_validation_fails_for_invalid_schema_out_name(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLInput): + __schema__ = gql( + """ + input Query { + hello: String! + } + """ + ) + + __out_names__ = { + "invalid": "ok", + } + + snapshot.assert_match(str(exc_info.value)) + + +def test_input_type_validation_fails_for_duplicate_schema_out_name(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLInput): + __schema__ = gql( + """ + input Query { + hello: String! + name: String! + } + """ + ) + + __out_names__ = { + "hello": "ok", + "name": "ok", + } + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index a94cdbd..6d184fb 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -1,3 +1,5 @@ +from typing import Optional + from graphql import graphql_sync from ariadne_graphql_modules import gql @@ -533,3 +535,53 @@ def resolve_hello(*_, name: str): assert not result.errors assert result.data == {"hello": "Hello Bob!"} + + +def test_object_type_self_reference( + assert_schema_equals, +): + class CategoryType(GraphQLObject): + name: str + parent: Optional["CategoryType"] + + class QueryType(GraphQLObject): + category: CategoryType + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + category: Category! + } + + type Category { + name: String! + parent: Category + } + """, + ) + + result = graphql_sync( + schema, + "{ category { name parent { name } } }", + root_value={ + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + }, + }, + ) + + assert not result.errors + assert result.data == { + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + }, + } From ffbbde3f9aea7ecce813cc3d88ff10f0f9a81a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Fri, 3 Nov 2023 12:25:41 +0100 Subject: [PATCH 16/63] Inputs and objects defaults --- ariadne_graphql_modules/next/__init__.py | 3 + ariadne_graphql_modules/next/inputtype.py | 124 +++++-- ariadne_graphql_modules/next/objecttype.py | 168 +++++++-- ariadne_graphql_modules/next/value.py | 88 +++++ tests_next/snapshots/snap_test_input_type.py | 12 + .../snap_test_input_type_validation.py | 4 + tests_next/snapshots/snap_test_object_type.py | 12 + .../snap_test_object_type_validation.py | 10 +- tests_next/test_input_type.py | 332 ++++++++++++++++++ tests_next/test_input_type_validation.py | 24 ++ tests_next/test_object_type.py | 152 ++++++++ tests_next/test_object_type_field_args.py | 199 ++++++++++- tests_next/test_object_type_validation.py | 77 +++- tests_next/test_value_node.py | 160 +++++++++ 14 files changed, 1303 insertions(+), 62 deletions(-) create mode 100644 ariadne_graphql_modules/next/value.py create mode 100644 tests_next/snapshots/snap_test_input_type.py create mode 100644 tests_next/snapshots/snap_test_object_type.py create mode 100644 tests_next/test_value_node.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 29afe1f..a01f98b 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -16,6 +16,7 @@ from .inputtype import GraphQLInput, GraphQLInputModel from .objecttype import GraphQLObject, GraphQLObjectModel, object_field from .scalartype import GraphQLScalar, GraphQScalarModel +from .value import get_value_from_node, get_value_node __all__ = [ "GraphQLEnum", @@ -35,6 +36,8 @@ "create_graphql_enum_model", "deferred", "get_description_node", + "get_value_from_node", + "get_value_node", "graphql_enum", "make_executable_schema", "object_field", diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py index 81d445a..4608b0a 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/inputtype.py @@ -1,3 +1,4 @@ +from copy import deepcopy from dataclasses import dataclass from typing import Any, Dict, Iterable, List, Optional, Type, cast @@ -18,16 +19,14 @@ from .description import get_description_node from .typing import get_graphql_type, get_type_node from .validators import validate_description, validate_name +from .value import get_value_from_node, get_value_node class GraphQLInput(GraphQLType): - __kwargs__: List[str] + __kwargs__: Dict[str, Any] __out_names__: Optional[Dict[str, str]] = None def __init__(self, **kwargs: Any): - for kwarg in self.__kwargs__: - setattr(self, kwarg, kwargs.get(kwarg)) - for kwarg in kwargs: if kwarg not in self.__kwargs__: valid_kwargs = "', '".join(self.__kwargs__) @@ -37,6 +36,9 @@ def __init__(self, **kwargs: Any): f"Valid keyword arguments: '{valid_kwargs}'" ) + for kwarg, default in self.__kwargs__.items(): + setattr(self, kwarg, kwargs.get(kwarg, deepcopy(default))) + def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -46,9 +48,9 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False if cls.__dict__.get("__schema__"): - validate_input_type_with_schema(cls) + cls.__kwargs__ = validate_input_type_with_schema(cls) else: - validate_input_type(cls) + cls.__kwargs__ = validate_input_type(cls) @classmethod def create_from_data(cls, data: Dict[str, Any]) -> "GraphQLInput": @@ -82,9 +84,10 @@ def __get_graphql_model_with_schema__( fields.append( InputValueDefinitionNode( name=field.name, - description=(field.description), + description=field.description, directives=field.directives, type=field.type, + default_value=field.default_value, ) ) @@ -109,14 +112,11 @@ def __get_graphql_model_without_schema__( ) -> "GraphQLInputModel": type_hints = cls.__annotations__ fields_instances: Dict[str, GraphQLInputField] = {} - fields_descriptions: Dict[str, str] = {} for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLInputField): fields_instances[attr_name] = cls_attr - if cls_attr.description: - fields_descriptions[attr_name] = cls_attr.description fields_ast: List[InputValueDefinitionNode] = [] out_names: Dict[str, str] = {} @@ -125,21 +125,47 @@ def __get_graphql_model_without_schema__( if hint_name.startswith("__"): continue - if hint_name in fields_instances: - continue + cls_attr = getattr(cls, hint_name, None) + default_name = convert_python_name_to_graphql(hint_name) + if isinstance(cls_attr, GraphQLInputField): + fields_ast.append( + get_field_node_from_type_hint( + cls, + metadata, + cls_attr.name or default_name, + cls_attr.type or hint_type, + cls_attr.description, + cls_attr.default_value, + ) + ) + out_names[cls_attr.name or default_name] = hint_name + fields_instances.pop(hint_name, None) + elif not callable(cls_attr): + fields_ast.append( + get_field_node_from_type_hint( + cls, + metadata, + default_name, + hint_type, + None, + cls_attr, + ) + ) + out_names[default_name] = hint_name - hint_field_name = convert_python_name_to_graphql(hint_name) + for attr_name, field_instance in fields_instances: + default_name = convert_python_name_to_graphql(hint_name) fields_ast.append( get_field_node_from_type_hint( cls, metadata, - hint_field_name, - hint_type, - fields_descriptions.get(hint_name), + field_instance.name or default_name, + field_instance.type, + field_instance.description, + field_instance.default_value, ) ) - - out_names[hint_field_name] = hint_name + out_names[cls_attr.name or default_name] = hint_name return GraphQLInputModel( name=name, @@ -187,16 +213,18 @@ def field( name: Optional[str] = None, type: Optional[Any] = None, description: Optional[str] = None, + default_value: Optional[Any] = None, ): """Shortcut for GraphQLInputField()""" return GraphQLInputField( name=name, type=type, description=description, + default_value=default_value, ) -def validate_input_type_with_schema(cls: Type[GraphQLInput]): +def validate_input_type_with_schema(cls: Type[GraphQLInput]) -> Dict[str, Any]: definition = cast( InputObjectTypeDefinitionNode, parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), @@ -239,45 +267,71 @@ def validate_input_type_with_schema(cls: Type[GraphQLInput]): used_out_names.append(out_name) - cls.__kwargs__ = get_input_type_with_schema_kwargs(cls, definition, out_names) + return get_input_type_with_schema_kwargs(cls, definition, out_names) def get_input_type_with_schema_kwargs( cls: Type[GraphQLInput], definition: InputObjectTypeDefinitionNode, out_names: Dict[str, str], -) -> List[str]: - kwargs: List[str] = [] +) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} for field in definition.fields: + if field.default_value: + default_value = get_value_from_node(field.default_value) + else: + default_value = None + try: - kwargs.append(out_names[field.name.value]) + kwargs[out_names[field.name.value]] = default_value except KeyError: - kwargs.append(convert_graphql_name_to_python(field.name.value)) + kwargs[convert_graphql_name_to_python(field.name.value)] = default_value + return kwargs -def validate_input_type(cls: Type[GraphQLInput]): +def validate_input_type(cls: Type[GraphQLInput]) -> Dict[str, Any]: if cls.__out_names__: raise ValueError( f"Class '{cls.__name__}' defines '__out_names__' attribute. " "This is not supported for types not defining '__schema__'." ) - cls.__kwargs__ = get_input_type_kwargs(cls) + return get_input_type_kwargs(cls) -def get_input_type_kwargs(cls: Type[GraphQLInput]) -> List[str]: - fields: List[str] = [] +def get_input_type_kwargs(cls: Type[GraphQLInput]) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} for attr_name in cls.__annotations__: if attr_name.startswith("__"): continue attr_value = getattr(cls, attr_name, None) - if attr_value is None or isinstance(attr_value, GraphQLInputField): - fields.append(attr_name) + if isinstance(attr_value, GraphQLInputField): + validate_field_default_value(cls, attr_name, attr_value.default_value) + kwargs[attr_name] = attr_value.default_value + elif not callable(attr_value): + validate_field_default_value(cls, attr_name, attr_value) + kwargs[attr_name] = attr_value + + return kwargs + - return fields +def validate_field_default_value( + cls: Type[GraphQLInput], field_name: str, default_value: Any +): + if default_value is None: + return + + try: + get_value_node(default_value) + except TypeError as e: + raise TypeError( + f"Class '{cls.__name__}' defines default value " + f"for the '{field_name}' field that can't be " + "represented in GraphQL schema." + ) from e class GraphQLInputField: @@ -316,10 +370,16 @@ def get_field_node_from_type_hint( field_name: str, field_type: Any, field_description: Optional[str] = None, + field_default_value: Optional[Any] = None, ) -> InputValueDefinitionNode: + if field_default_value is not None: + default_value = get_value_node(field_default_value) + else: + default_value = None + return InputValueDefinitionNode( description=get_description_node(field_description), name=NameNode(value=field_name), type=get_type_node(metadata, field_type, parent_type), - default_value=None, + default_value=default_value, ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index dd69292..29863c3 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -23,7 +23,6 @@ InputValueDefinitionNode, NameNode, ObjectTypeDefinitionNode, - StringValueNode, ) from ..utils import parse_definition @@ -32,15 +31,30 @@ from .description import get_description_node from .typing import get_graphql_type, get_type_node from .validators import validate_description, validate_name +from .value import get_value_node class GraphQLObject(GraphQLType): + __kwargs__: List[str] __abstract__: bool = True __schema__: Optional[str] __description__: Optional[str] __aliases__: Optional[Dict[str, str]] __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] + def __init__(self, **kwargs: Any): + for kwarg in kwargs: + if kwarg not in self.__kwargs__: + valid_kwargs = "', '".join(self.__kwargs__) + raise TypeError( + f"{type(self).__name__}.__init__() got an unexpected " + f"keyword argument '{kwarg}'. " + f"Valid keyword arguments: '{valid_kwargs}'" + ) + + for kwarg in self.__kwargs__: + setattr(self, kwarg, kwargs.get(kwarg)) + def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -50,9 +64,9 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False if cls.__dict__.get("__schema__"): - validate_object_type_with_schema(cls) + cls.__kwargs__ = validate_object_type_with_schema(cls) else: - validate_object_type(cls) + cls.__kwargs__ = validate_object_type(cls) @classmethod def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": @@ -75,6 +89,7 @@ def __get_graphql_model_with_schema__( descriptions: Dict[str, str] = {} args_descriptions: Dict[str, Dict[str, str]] = {} + args_defaults: Dict[str, Dict[str, Any]] = {} resolvers: Dict[str, Resolver] = {} out_names: Dict[str, Dict[str, str]] = {} @@ -87,31 +102,45 @@ def __get_graphql_model_with_schema__( cls_attr.description ) - if cls_attr.args: + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: args_descriptions[cls_attr.field] = {} - for arg_name, arg_options in cls_attr.args.items(): - arg_description = get_description_node( - arg_options.get("description") - ) + args_defaults[cls_attr.field] = {} + + final_args = update_field_args_options(field_args, cls_attr.args) + + for arg_name, arg_options in final_args.items(): + arg_description = get_description_node(arg_options.description) if arg_description: args_descriptions[cls_attr.field][ arg_name ] = arg_description + arg_default = arg_options.default_value + if arg_default is not None: + args_defaults[cls_attr.field][arg_name] = get_value_node( + arg_default + ) + fields: List[FieldDefinitionNode] = [] for field in definition.fields: field_args_descriptions = args_descriptions.get(field.name.value, {}) + field_args_defaults = args_defaults.get(field.name.value, {}) args: List[InputValueDefinitionNode] = [] for arg in field.arguments: + arg_name = arg.name.value args.append( InputValueDefinitionNode( - description=arg.description - or field_args_descriptions.get(arg.name.value), + description=( + arg.description or field_args_descriptions.get(arg_name) + ), name=arg.name, directives=arg.directives, type=arg.type, - default_value=arg.default_value, + default_value=( + arg.default_value or field_args_defaults.get(arg_name) + ), ) ) @@ -249,6 +278,7 @@ def argument( name: Optional[str] = None, description: Optional[str] = None, type: Optional[Any] = None, + default_value: Optional[Any] = None, ) -> dict: options: dict = {} if name: @@ -257,10 +287,12 @@ def argument( options["description"] = description if type: options["type"] = type + if default_value: + options["default_value"] = default_value return options -def validate_object_type_with_schema(cls: Type[GraphQLObject]): +def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> List[str]: definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) if not isinstance(definition, ObjectTypeDefinitionNode): @@ -343,19 +375,52 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]): "This is not supported for types defining '__schema__'." ) - if arg_options["description"] and field_args[arg_name].description: + if ( + arg_options.get("description") + and field_args[arg_name].description + ): raise ValueError( f"Class '{cls.__name__}' defines duplicate descriptions " f"for '{arg_name}' argument " f"of the '{cls_attr.field}' field." ) + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_options.get("default_value") + ) + + resolver_args = get_field_args_from_resolver(cls_attr.resolver) + for arg_name, arg_obj in resolver_args.items(): + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_obj.default_value + ) + resolvers_names.append(cls_attr.field) - validate_object_aliases(cls, field_names, resolvers_names) + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + validate_object_aliases(cls, aliases, field_names, resolvers_names) + + return [aliases.get(field_name, field_name) for field_name in field_names] + + +def validate_field_arg_default_value( + cls: Type[GraphQLObject], field_name: str, arg_name: str, default_value: Any +): + if default_value is None: + return + + try: + get_value_node(default_value) + except TypeError as e: + raise TypeError( + f"Class '{cls.__name__}' defines default value " + f"for '{arg_name}' argument " + f"of the '{field_name}' field that can't be " + "represented in GraphQL schema." + ) from e -def validate_object_type(cls: Type[GraphQLObject]): +def validate_object_type(cls: Type[GraphQLObject]) -> List[str]: attrs_names: List[str] = [ attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") ] @@ -431,17 +496,29 @@ def validate_object_type(cls: Type[GraphQLObject]): for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, (GraphQLObjectField, GraphQLObjectResolver)): - if not cls_attr.resolver or not cls_attr.args: + if not cls_attr.resolver: continue - resolver_args = get_field_args_names_from_resolver(cls_attr.resolver) + resolver_args = get_field_args_from_resolver(cls_attr.resolver) if resolver_args: - error_help = "expected one of: '%s'" % ("', '".join(resolver_args)) + for arg_name, arg_obj in resolver_args.items(): + validate_field_arg_default_value( + cls, attr_name, arg_name, arg_obj.default_value + ) + + if not cls_attr.args: + continue + + resolver_args_names = list(resolver_args.keys()) + if resolver_args_names: + error_help = "expected one of: '%s'" % ( + "', '".join(resolver_args_names) + ) else: error_help = "function accepts no extra arguments" - for arg_name in cls_attr.args: - if arg_name not in resolver_args: + for arg_name, arg_options in cls_attr.args.items(): + if arg_name not in resolver_args_names: if isinstance(cls_attr, GraphQLObjectField): raise ValueError( f"Class '{cls.__name__}' defines '{attr_name}' field " @@ -457,13 +534,22 @@ def validate_object_type(cls: Type[GraphQLObject]): f"({error_help})" ) - validate_object_aliases(cls, attrs_names, resolvers_names) + validate_field_arg_default_value( + cls, attr_name, arg_name, arg_options.get("default_value") + ) + + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + validate_object_aliases(cls, aliases, attrs_names, resolvers_names) + + return get_object_type_kwargs(cls, aliases) def validate_object_aliases( - cls: Type[GraphQLObject], fields_names: List[str], resolvers_names: List[str] + cls: Type[GraphQLObject], + aliases: Dict[str, str], + fields_names: List[str], + resolvers_names: List[str], ): - aliases = getattr(cls, "__aliases__", None) or {} for alias in aliases: if alias not in fields_names: valid_fields: str = "', '".join(sorted(fields_names)) @@ -479,6 +565,23 @@ def validate_object_aliases( ) +def get_object_type_kwargs( + cls: Type[GraphQLObject], + aliases: Dict[str, str], +) -> List[str]: + kwargs: List[str] = [] + + for attr_name in cls.__annotations__: + if attr_name.startswith("__"): + continue + + attr_value = getattr(cls, attr_name, None) + if attr_value is None or isinstance(attr_value, GraphQLObjectField): + kwargs.append(aliases.get(attr_name, attr_name)) + + return kwargs + + @dataclass(frozen=True) class GraphQLObjectData: fields: Dict[str, "GraphQLObjectField"] @@ -699,6 +802,7 @@ class GraphQLObjectFieldArg: out_name: Optional[str] type: Optional[Any] description: Optional[str] = None + default_value: Optional[Any] = None def get_field_args_from_resolver( @@ -730,19 +834,21 @@ def get_field_args_from_resolver( return field_args for param_name, param in args_parameters: + if param.default != param.empty: + param_default = param.default + else: + param_default = None + field_args[param_name] = GraphQLObjectFieldArg( name=convert_python_name_to_graphql(param_name), out_name=param_name, type=type_hints.get(param_name), + default_value=param_default, ) return field_args -def get_field_args_names_from_resolver(resolver: Resolver) -> List[str]: - return list(get_field_args_from_resolver(resolver).keys()) - - def get_field_args_out_names( field_args: Dict[str, GraphQLObjectFieldArg] ) -> Dict[str, str]: @@ -768,10 +874,16 @@ def get_field_arg_node_from_obj_field_arg( metadata: GraphQLMetadata, field_arg: GraphQLObjectFieldArg, ) -> InputValueDefinitionNode: + if field_arg.default_value is not None: + default_value = get_value_node(field_arg.default_value) + else: + default_value = None + return InputValueDefinitionNode( description=get_description_node(field_arg.description), name=NameNode(value=field_arg.name), type=get_type_node(metadata, field_arg.type), + default_value=default_value, ) @@ -794,6 +906,8 @@ def update_field_args_options( args_update["name"] = arg_options["name"] if arg_options.get("description"): args_update["description"] = arg_options["description"] + if arg_options.get("default_value") is not None: + args_update["default_value"] = arg_options["default_value"] if arg_options.get("type"): args_update["type"] = arg_options["type"] diff --git a/ariadne_graphql_modules/next/value.py b/ariadne_graphql_modules/next/value.py new file mode 100644 index 0000000..be4ee07 --- /dev/null +++ b/ariadne_graphql_modules/next/value.py @@ -0,0 +1,88 @@ +from decimal import Decimal +from enum import Enum +from typing import Any, Iterable, Mapping + +from graphql import ( + BooleanValueNode, + ConstListValueNode, + ConstObjectFieldNode, + ConstObjectValueNode, + ConstValueNode, + EnumValueNode, + FloatValueNode, + IntValueNode, + ListValueNode, + NameNode, + NullValueNode, + ObjectValueNode, + StringValueNode, +) + + +def get_value_node(value: Any): + if value is False or value is True: + return BooleanValueNode(value=value) + + if isinstance(value, Enum): + return EnumValueNode(value=value.name) + + if isinstance(value, (float, Decimal)): + return FloatValueNode(value=str(value)) + + if isinstance(value, int): + return IntValueNode(value=str(value)) + + if value is None: + return NullValueNode() + + if isinstance(value, str): + return StringValueNode(value=value, block="\n" in value) + + if isinstance(value, Mapping): + return ConstObjectValueNode( + fields=tuple( + ConstObjectFieldNode( + name=NameNode(value=str(name)), value=get_value_node(val) + ) + for name, val in value.items() + ), + ) + + if isinstance(value, Iterable): + return ConstListValueNode( + values=tuple(get_value_node(val) for val in value), + ) + + raise TypeError( + f"Python value '{repr(value)}' can't be represented as a GraphQL value node." + ) + + +def get_value_from_node(node: ConstValueNode) -> Any: + if isinstance(node, BooleanValueNode): + return node.value + + if isinstance(node, EnumValueNode): + return node.value + + if isinstance(node, FloatValueNode): + return Decimal(node.value) + + if isinstance(node, IntValueNode): + return int(node.value) + + if isinstance(node, NullValueNode): + return None + + if isinstance(node, StringValueNode): + return node.value + + if isinstance(node, (ConstObjectValueNode, ObjectValueNode)): + return {field.name.value: get_value_from_node(field) for field in node.fields} + + if isinstance(node, ListValueNode): + return [get_value_from_node(value) for value in node.values] + + raise TypeError( + f"GraphQL node '{repr(node)}' can't be represented as a Python value." + ) diff --git a/tests_next/snapshots/snap_test_input_type.py b/tests_next/snapshots/snap_test_input_type.py new file mode 100644 index 0000000..afc1d62 --- /dev/null +++ b/tests_next/snapshots/snap_test_input_type.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_input_type_instance_with_invalid_attrs_raising_error 1'] = "SearchInput.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'query', 'age'" + +snapshots['test_schema_input_type_instance_with_invalid_attrs_raising_error 1'] = "SearchInput.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'query', 'age'" diff --git a/tests_next/snapshots/snap_test_input_type_validation.py b/tests_next/snapshots/snap_test_input_type_validation.py index 41f3745..21fd659 100644 --- a/tests_next/snapshots/snap_test_input_type_validation.py +++ b/tests_next/snapshots/snap_test_input_type_validation.py @@ -20,3 +20,7 @@ snapshots['test_input_type_validation_fails_for_schema_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an input type without any fields. " snapshots['test_input_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." + +snapshots['test_input_type_validation_fails_for_unsupported_attr_default 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." + +snapshots['test_input_type_validation_fails_for_unsupported_field_default_option 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." diff --git a/tests_next/snapshots/snap_test_object_type.py b/tests_next/snapshots/snap_test_object_type.py new file mode 100644 index 0000000..71a39d8 --- /dev/null +++ b/tests_next/snapshots/snap_test_object_type.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_object_type_instance_with_invalid_attrs_raising_error 1'] = "CategoryType.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'name', 'posts'" + +snapshots['test_object_type_with_schema_instance_with_invalid_attrs_raising_error 1'] = "CategoryType.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'name', 'posts'" diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py index 5d246be..4435af3 100644 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -49,7 +49,7 @@ snapshots['test_object_type_validation_fails_for_schema_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." -snapshots['test_object_type_validation_fails_for_schema_with_separate_field 1'] = "Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for types defining '__schema__'." +snapshots['test_object_type_validation_fails_for_schema_with_field_instance 1'] = "Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for types defining '__schema__'." snapshots['test_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." @@ -60,3 +60,11 @@ snapshots['test_object_type_validation_fails_for_undefined_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" snapshots['test_object_type_validation_fails_for_undefined_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" + +snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." + +snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default_option 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." + +snapshots['test_object_type_validation_fails_for_unsupported_schema_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." + +snapshots['test_object_type_validation_fails_for_unsupported_schema_resolver_arg_option_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py index e182bfb..772431c 100644 --- a/tests_next/test_input_type.py +++ b/tests_next/test_input_type.py @@ -1,5 +1,6 @@ from typing import Optional +import pytest from graphql import graphql_sync from ariadne_graphql_modules import gql @@ -10,6 +11,163 @@ ) +def test_input_type_instance_with_all_attrs_values(): + class SearchInput(GraphQLInput): + query: str + age: int + + obj = SearchInput(query="search", age=20) + assert obj.query == "search" + assert obj.age == 20 + + +def test_input_type_instance_with_omitted_attrs_being_none(): + class SearchInput(GraphQLInput): + query: str + age: int + + obj = SearchInput(age=20) + assert obj.query is None + assert obj.age == 20 + + +def test_input_type_instance_with_default_attrs_values(): + class SearchInput(GraphQLInput): + query: str = "default" + age: int = 42 + + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 + + +def test_input_type_instance_with_all_fields_values(): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field() + age: int = GraphQLInput.field() + + obj = SearchInput(query="search", age=20) + assert obj.query == "search" + assert obj.age == 20 + + +def test_input_type_instance_with_all_fields_default_values(): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(default_value="default") + age: int = GraphQLInput.field(default_value=42) + + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 + + +def test_input_type_instance_with_invalid_attrs_raising_error(snapshot): + class SearchInput(GraphQLInput): + query: str + age: int + + with pytest.raises(TypeError) as exc_info: + SearchInput(age=20, invalid="Ok") + + snapshot.assert_match(str(exc_info.value)) + + +def test_schema_input_type_instance_with_all_attrs_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String + age: Int + } + """ + ) + + query: str + age: int + + obj = SearchInput(query="search", age=20) + assert obj.query == "search" + assert obj.age == 20 + + +def test_schema_input_type_instance_with_omitted_attrs_being_none(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String + age: Int + } + """ + ) + + query: str + age: int + + obj = SearchInput(age=20) + assert obj.query is None + assert obj.age == 20 + + +def test_schema_input_type_instance_with_default_attrs_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String = "default" + age: Int = 42 + } + """ + ) + + query: str + age: int + + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 + + +def test_schema_input_type_instance_with_all_attrs_default_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String = "default" + age: Int = 42 + } + """ + ) + + query: str + age: int + + obj = SearchInput() + assert obj.query == "default" + assert obj.age == 42 + + +def test_schema_input_type_instance_with_invalid_attrs_raising_error(snapshot): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String + age: Int + } + """ + ) + + query: str + age: int + + with pytest.raises(TypeError) as exc_info: + SearchInput(age=20, invalid="Ok") + + snapshot.assert_match(str(exc_info.value)) + + def test_input_type_arg(assert_schema_equals): class SearchInput(GraphQLInput): query: Optional[str] @@ -248,3 +406,177 @@ def resolve_search(*_, input: SearchInput) -> str: assert not result.errors assert result.data == {"search": "['Hello', 'Other']"} + + +def test_schema_input_type_with_default_value(assert_schema_equals): + class SearchInput(GraphQLInput): + __schema__ = """ + input SearchInput { + query: String = "Search" + age: Int = 42 + } + """ + + query: str + age: int + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String = "Search" + age: Int = 42 + } + """, + ) + + result = graphql_sync(schema, "{ search(input: {}) }") + + assert not result.errors + assert result.data == {"search": "['Search', 42]"} + + +def test_input_type_with_field_default_value(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = "default" + age: int = 42 + flag: bool = False + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age, input.flag])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String! = "default" + age: Int! = 42 + flag: Boolean! = false + } + """, + ) + + result = graphql_sync(schema, "{ search(input: {}) }") + + assert not result.errors + assert result.data == {"search": "['default', 42, False]"} + + +def test_input_type_with_field_instance_default_value(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(default_value="default") + age: int = GraphQLInput.field(default_value=42) + flag: bool = GraphQLInput.field(default_value=False) + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age, input.flag])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String! = "default" + age: Int! = 42 + flag: Boolean! = false + } + """, + ) + + result = graphql_sync(schema, "{ search(input: {}) }") + + assert not result.errors + assert result.data == {"search": "['default', 42, False]"} + + +def test_schema_input_type_with_field_description(assert_schema_equals): + class SearchInput(GraphQLInput): + __schema__ = """ + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + """ + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return str(input) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + """, + ) + + +def test_input_type_with_field_description(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(description="Hello world.") + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return str(input) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + """, + ) diff --git a/tests_next/test_input_type_validation.py b/tests_next/test_input_type_validation.py index bdc879f..d0b976e 100644 --- a/tests_next/test_input_type_validation.py +++ b/tests_next/test_input_type_validation.py @@ -106,3 +106,27 @@ class CustomType(GraphQLInput): } snapshot.assert_match(str(exc_info.value)) + + +class InvalidType: + pass + + +def test_input_type_validation_fails_for_unsupported_attr_default(snapshot): + with pytest.raises(TypeError) as exc_info: + + class QueryType(GraphQLInput): + attr: str = InvalidType() + + snapshot.assert_match(str(exc_info.value)) + + +def test_input_type_validation_fails_for_unsupported_field_default_option( + snapshot, +): + with pytest.raises(TypeError) as exc_info: + + class QueryType(GraphQLInput): + attr: str = GraphQLInput.field(default_value=InvalidType()) + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index 6d184fb..c5806cc 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -1,11 +1,121 @@ from typing import Optional +import pytest from graphql import graphql_sync from ariadne_graphql_modules import gql from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema +def test_object_type_instance_with_all_attrs_values(): + class CategoryType(GraphQLObject): + name: str + posts: int + + obj = CategoryType(name="Welcome", posts=20) + assert obj.name == "Welcome" + assert obj.posts == 20 + + +def test_object_type_instance_with_omitted_attrs_being_none(): + class CategoryType(GraphQLObject): + name: str + posts: int + + obj = CategoryType(posts=20) + assert obj.name is None + assert obj.posts == 20 + + +def test_object_type_instance_with_invalid_attrs_raising_error(snapshot): + class CategoryType(GraphQLObject): + name: str + posts: int + + with pytest.raises(TypeError) as exc_info: + CategoryType(name="Welcome", invalid="Ok") + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_with_schema_instance_with_all_attrs_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str + posts: int + + obj = CategoryType(name="Welcome", posts=20) + assert obj.name == "Welcome" + assert obj.posts == 20 + + +def test_object_type_with_schema_instance_with_omitted_attrs_being_none(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str + posts: int + + obj = CategoryType(posts=20) + assert obj.name is None + assert obj.posts == 20 + + +def test_object_type_with_schema_instance_with_invalid_attrs_raising_error(snapshot): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str + posts: int + + with pytest.raises(TypeError) as exc_info: + CategoryType(name="Welcome", invalid="Ok") + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_with_schema_instance_with_aliased_attr_value(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + __aliases__ = {"name": "title"} + + title: str + posts: int + + obj = CategoryType(title="Welcome", posts=20) + assert obj.title == "Welcome" + assert obj.posts == 20 + + def test_object_type_with_field(assert_schema_equals): class QueryType(GraphQLObject): hello: str @@ -585,3 +695,45 @@ class QueryType(GraphQLObject): }, }, } + + +def test_object_type_return_instance( + assert_schema_equals, +): + class CategoryType(GraphQLObject): + name: str + color: str + + class QueryType(GraphQLObject): + @GraphQLObject.field() + def category(*_) -> CategoryType: + return CategoryType( + name="Welcome", + color="#FF00FF", + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + category: Category! + } + + type Category { + name: String! + color: String! + } + """, + ) + + result = graphql_sync(schema, "{ category { name color } }") + + assert not result.errors + assert result.data == { + "category": { + "name": "Welcome", + "color": "#FF00FF", + }, + } diff --git a/tests_next/test_object_type_field_args.py b/tests_next/test_object_type_field_args.py index 3b81621..6e2fd3f 100644 --- a/tests_next/test_object_type_field_args.py +++ b/tests_next/test_object_type_field_args.py @@ -1,6 +1,5 @@ from graphql import GraphQLResolveInfo, graphql_sync -from ariadne_graphql_modules import gql from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema @@ -51,6 +50,204 @@ class QueryType(GraphQLObject): assert result.data == {"hello": "Hello Bob!"} +def test_object_type_field_resolver_with_arg_default_value(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field + def hello(obj, info: GraphQLResolveInfo, *, name: str = "Anon") -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String! = "Anon"): String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello Anon!"} + + +def test_object_type_field_instance_with_arg_default_value(assert_schema_equals): + def hello_resolver(*_, name: str = "Anon") -> str: + return f"Hello {name}!" + + class QueryType(GraphQLObject): + hello = GraphQLObject.field(hello_resolver) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String! = "Anon"): String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello Anon!"} + + +def test_object_type_field_resolver_with_arg_option_default_value( + assert_schema_equals, +): + class QueryType(GraphQLObject): + @GraphQLObject.field( + args={"name": GraphQLObject.argument(default_value="Anon")}, + ) + def hello(obj, info: GraphQLResolveInfo, *, name: str) -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String! = "Anon"): String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello Anon!"} + + +def test_object_type_field_instance_with_arg_option_default_value( + assert_schema_equals, +): + def hello_resolver(*_, name: str) -> str: + return f"Hello {name}!" + + class QueryType(GraphQLObject): + hello = GraphQLObject.field( + hello_resolver, + args={"name": GraphQLObject.argument(default_value="Anon")}, + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String! = "Anon"): String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello Anon!"} + + +def test_object_type_schema_field_with_arg_default_value( + assert_schema_equals, +): + class QueryType(GraphQLObject): + __schema__ = """ + type Query { + hello(name: String! = "Anon"): String! + } + """ + + @GraphQLObject.resolver("hello") + def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str) -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String! = "Anon"): String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello Anon!"} + + +def test_object_type_schema_field_with_arg_default_value_from_resolver_arg( + assert_schema_equals, +): + class QueryType(GraphQLObject): + __schema__ = """ + type Query { + hello(name: String!): String! + } + """ + + @GraphQLObject.resolver("hello") + def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str = "Anon") -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String! = "Anon"): String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello Anon!"} + + +def test_object_type_schema_field_with_arg_default_value_from_resolver_arg_option( + assert_schema_equals, +): + class QueryType(GraphQLObject): + __schema__ = """ + type Query { + hello(name: String!): String! + } + """ + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(default_value="Other")} + ) + def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str = "Anon") -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello(name: String! = "Other"): String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello Other!"} + + def test_object_type_field_instance_with_description(assert_schema_equals): def hello_resolver(*_, name: str) -> str: return f"Hello {name}!" diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index a12db16..aeb2b74 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -434,7 +434,7 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_with_separate_field(snapshot): +def test_object_type_validation_fails_for_schema_with_field_instance(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -449,3 +449,78 @@ class CustomType(GraphQLObject): hello = GraphQLObject.field(lambda *_: "noop") snapshot.assert_match(str(exc_info.value)) + + +class InvalidType: + pass + + +def test_object_type_validation_fails_for_unsupported_resolver_arg_default(snapshot): + with pytest.raises(TypeError) as exc_info: + + class QueryType(GraphQLObject): + @GraphQLObject.field + def hello(*_, name: str = InvalidType): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_unsupported_resolver_arg_default_option( + snapshot, +): + with pytest.raises(TypeError) as exc_info: + + class QueryType(GraphQLObject): + @GraphQLObject.field( + args={"name": GraphQLObject.argument(default_value=InvalidType)} + ) + def hello(*_, name: str): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_unsupported_schema_resolver_arg_default( + snapshot, +): + with pytest.raises(TypeError) as exc_info: + + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + @GraphQLObject.resolver("hello") + def resolve_hello(*_, name: str = InvalidType): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_object_type_validation_fails_for_unsupported_schema_resolver_arg_option_default( + snapshot, +): + with pytest.raises(TypeError) as exc_info: + + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello(name: String!): String! + } + """ + ) + + @GraphQLObject.resolver( + "hello", + args={"name": GraphQLObject.argument(default_value=InvalidType)}, + ) + def resolve_hello(*_, name: str): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_value_node.py b/tests_next/test_value_node.py new file mode 100644 index 0000000..48c754b --- /dev/null +++ b/tests_next/test_value_node.py @@ -0,0 +1,160 @@ +from decimal import Decimal +from enum import Enum + +import pytest +from graphql import ( + BooleanValueNode, + ConstListValueNode, + ConstObjectValueNode, + EnumValueNode, + FloatValueNode, + IntValueNode, + NullValueNode, + StringValueNode, + print_ast, +) + +from ariadne_graphql_modules.next import get_value_from_node, get_value_node + + +def test_get_false_value(): + node = get_value_node(False) + assert isinstance(node, BooleanValueNode) + assert print_ast(node) == "false" + + +def test_get_true_value(): + node = get_value_node(True) + assert isinstance(node, BooleanValueNode) + assert print_ast(node) == "true" + + +class PlainEnum(Enum): + VALUE = "ok" + + +class IntEnum(int, Enum): + VALUE = 21 + + +class StrEnum(str, Enum): + VALUE = "val" + + +def test_get_enum_value(): + node = get_value_node(PlainEnum.VALUE) + assert isinstance(node, EnumValueNode) + assert print_ast(node) == "VALUE" + + +def test_get_int_enum_value(): + node = get_value_node(IntEnum.VALUE) + assert isinstance(node, EnumValueNode) + assert print_ast(node) == "VALUE" + + +def test_get_str_enum_value(): + node = get_value_node(StrEnum.VALUE) + assert isinstance(node, EnumValueNode) + assert print_ast(node) == "VALUE" + + +def test_get_float_value(): + node = get_value_node(2.5) + assert isinstance(node, FloatValueNode) + assert print_ast(node) == "2.5" + + +def test_get_decimal_value(): + node = get_value_node(Decimal("2.33")) + assert isinstance(node, FloatValueNode) + assert print_ast(node) == "2.33" + + +def test_get_int_value(): + node = get_value_node(42) + assert isinstance(node, IntValueNode) + assert print_ast(node) == "42" + + +def test_get_str_value(): + node = get_value_node("Hello") + assert isinstance(node, StringValueNode) + assert print_ast(node) == '"Hello"' + + +def test_get_str_block_value(): + node = get_value_node("Hello\nWorld!") + assert isinstance(node, StringValueNode) + assert print_ast(node) == '"""\nHello\nWorld!\n"""' + + +def test_get_none_value(): + node = get_value_node(None) + assert isinstance(node, NullValueNode) + assert print_ast(node) == "null" + + +def test_get_list_value(): + node = get_value_node([1, 3, None]) + assert isinstance(node, ConstListValueNode) + assert print_ast(node) == "[1, 3, null]" + + +def test_get_tuple_value(): + node = get_value_node((1, 3, None)) + assert isinstance(node, ConstListValueNode) + assert print_ast(node) == "[1, 3, null]" + + +def test_get_dict_value(): + node = get_value_node({"a": 1, "c": 3, "d": None}) + assert isinstance(node, ConstObjectValueNode) + assert print_ast(node) == "{a: 1, c: 3, d: null}" + + +def test_type_error_is_raised_for_unsupported_python_value(): + class CustomType: + pass + + with pytest.raises(TypeError) as exc_info: + get_value_node(CustomType()) + + error_message = str(exc_info.value) + assert error_message.startswith("Python value '' can't be represented as a GraphQL value node.") + + +def test_get_false_value_from_node(): + value = get_value_from_node(BooleanValueNode(value=False)) + assert value is False + + +def test_get_true_value_from_node(): + value = get_value_from_node(BooleanValueNode(value=True)) + assert value is True + + +def test_get_enum_value_from_node(): + value = get_value_from_node(EnumValueNode(value="USER")) + assert value == "USER" + + +def test_get_float_value_from_node(): + value = get_value_from_node(FloatValueNode(value="2.5")) + assert value == Decimal("2.5") + + +def test_get_int_value_from_node(): + value = get_value_from_node(IntValueNode(value="42")) + assert value == 42 + + +def test_get_str_value_from_node(): + value = get_value_from_node(StringValueNode(value="Hello")) + assert value == "Hello" + + +def test_get_str_value_from_block_node(): + value = get_value_from_node(StringValueNode(value="Hello", block=True)) + assert value == "Hello" From 81d64c0158ee85c81ccb53e42e92f43af629ee32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Mon, 6 Nov 2023 16:50:28 +0100 Subject: [PATCH 17/63] Input and object instance types --- tests_next/test_input_type.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py index 772431c..cd7ccef 100644 --- a/tests_next/test_input_type.py +++ b/tests_next/test_input_type.py @@ -521,6 +521,33 @@ def resolve_search(*_, input: SearchInput) -> str: assert result.data == {"search": "['default', 42, False]"} +def test_input_type_with_field_type(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(type=int) + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return str(input) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: Int! + } + """, + ) + + def test_schema_input_type_with_field_description(assert_schema_equals): class SearchInput(GraphQLInput): __schema__ = """ From 2a19874fc28096d6cfc2eba7575a9f4f2bcf1ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Thu, 9 Nov 2023 18:50:47 +0100 Subject: [PATCH 18/63] Update tests --- ariadne_graphql_modules/next/enumtype.py | 212 +++--- ariadne_graphql_modules/next/inputtype.py | 102 +-- ariadne_graphql_modules/next/objecttype.py | 606 +++++++++--------- ariadne_graphql_modules/next/scalartype.py | 30 +- .../snap_test_enum_type_validation.py | 22 +- .../snap_test_input_type_validation.py | 18 +- .../snap_test_object_type_validation.py | 48 +- .../snap_test_scalar_type_validation.py | 6 +- tests_next/test_enum_type.py | 14 +- tests_next/test_enum_type_validation.py | 22 +- tests_next/test_input_type_validation.py | 12 +- tests_next/test_object_type_field_args.py | 6 +- tests_next/test_object_type_validation.py | 32 +- tests_next/test_scalar_type.py | 30 +- tests_next/test_scalar_type_validation.py | 6 +- 15 files changed, 598 insertions(+), 568 deletions(-) diff --git a/ariadne_graphql_modules/next/enumtype.py b/ariadne_graphql_modules/next/enumtype.py index 19b3fd4..15b4a64 100644 --- a/ariadne_graphql_modules/next/enumtype.py +++ b/ariadne_graphql_modules/next/enumtype.py @@ -127,6 +127,112 @@ def __get_graphql_model_without_schema__( ) +def create_graphql_enum_model( + enum: Type[Enum], + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[Dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +) -> "GraphQLEnumModel": + if members_include and members_exclude: + raise ValueError( + "'members_include' and 'members_exclude' " "options are mutually exclusive." + ) + + if hasattr(enum, "__get_graphql_model__"): + return cast(GraphQLEnumModel, enum.__get_graphql_model__()) + + if not name: + if hasattr(enum, "__get_graphql_name__"): + name = cast("str", enum.__get_graphql_name__()) + else: + name = enum.__name__ + + members: Dict[str, Any] = {i.name: i for i in enum} + final_members: Dict[str, Any] = {} + + if members_include: + for key, value in members.items(): + if key in members_include: + final_members[key] = value + elif members_exclude: + for key, value in members.items(): + if key not in members_exclude: + final_members[key] = value + else: + final_members = members + + members_descriptions = members_descriptions or {} + for member in members_descriptions: + if member not in final_members: + raise ValueError( + f"Member description was specified for a member '{member}' " + "not present in final GraphQL enum." + ) + + return GraphQLEnumModel( + name=name, + members=final_members, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node(description), + values=tuple( + EnumValueDefinitionNode( + name=NameNode(value=value_name), + description=get_description_node( + members_descriptions.get(value_name) + ), + ) + for value_name in final_members + ), + ), + ) + + +@dataclass(frozen=True) +class GraphQLEnumModel(GraphQLModel): + members: Dict[str, Any] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = EnumType(self.name, values=self.members) + bindable.bind_to_schema(schema) + + +def graphql_enum( + cls: Optional[Type[Enum]] = None, + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[Dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +): + def graphql_enum_decorator(cls: Type[Enum]): + graphql_model = create_graphql_enum_model( + cls, + name=name, + description=description, + members_descriptions=members_descriptions, + members_include=members_include, + members_exclude=members_exclude, + ) + + @classmethod + def __get_graphql_model__(*_) -> GraphQLEnumModel: + return graphql_model + + setattr(cls, "__get_graphql_model__", __get_graphql_model__) + return cls + + if cls: + return graphql_enum_decorator(cls) + + return graphql_enum_decorator + + def validate_enum_type_with_schema(cls: Type[GraphQLEnum]): definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) @@ -242,109 +348,3 @@ def get_members_set( return set(member.name for member in members) return set(members) - - -def create_graphql_enum_model( - enum: Type[Enum], - *, - name: Optional[str] = None, - description: Optional[str] = None, - members_descriptions: Optional[Dict[str, str]] = None, - members_include: Optional[Iterable[str]] = None, - members_exclude: Optional[Iterable[str]] = None, -) -> "GraphQLEnumModel": - if members_include and members_exclude: - raise ValueError( - "'members_include' and 'members_exclude' " "options are mutually exclusive." - ) - - if hasattr(enum, "__get_graphql_model__"): - return cast(GraphQLEnumModel, enum.__get_graphql_model__()) - - if not name: - if hasattr(enum, "__get_graphql_name__"): - name = cast("str", enum.__get_graphql_name__()) - else: - name = enum.__name__ - - members: Dict[str, Any] = {i.name: i for i in enum} - final_members: Dict[str, Any] = {} - - if members_include: - for key, value in members.items(): - if key in members_include: - final_members[key] = value - elif members_exclude: - for key, value in members.items(): - if key not in members_exclude: - final_members[key] = value - else: - final_members = members - - members_descriptions = members_descriptions or {} - for member in members_descriptions: - if member not in final_members: - raise ValueError( - f"Member description was specified for a member '{member}' " - "not present in final GraphQL enum." - ) - - return GraphQLEnumModel( - name=name, - members=final_members, - ast_type=EnumTypeDefinitionNode, - ast=EnumTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node(description), - values=tuple( - EnumValueDefinitionNode( - name=NameNode(value=value_name), - description=get_description_node( - members_descriptions.get(value_name) - ), - ) - for value_name in final_members - ), - ), - ) - - -@dataclass(frozen=True) -class GraphQLEnumModel(GraphQLModel): - members: Dict[str, Any] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = EnumType(self.name, values=self.members) - bindable.bind_to_schema(schema) - - -def graphql_enum( - cls: Optional[Type[Enum]] = None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - members_descriptions: Optional[Dict[str, str]] = None, - members_include: Optional[Iterable[str]] = None, - members_exclude: Optional[Iterable[str]] = None, -): - def graphql_enum_decorator(cls: Type[Enum]): - graphql_model = create_graphql_enum_model( - cls, - name=name, - description=description, - members_descriptions=members_descriptions, - members_include=members_include, - members_exclude=members_exclude, - ) - - @classmethod - def __get_graphql_model__(*_) -> GraphQLEnumModel: - return graphql_model - - setattr(cls, "__get_graphql_model__", __get_graphql_model__) - return cls - - if cls: - return graphql_enum_decorator(cls) - - return graphql_enum_decorator diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py index 4608b0a..520b883 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/inputtype.py @@ -224,6 +224,57 @@ def field( ) +class GraphQLInputField: + name: Optional[str] + description: Optional[str] + type: Optional[Any] + default_value: Optional[Any] + + def __init__( + self, + *, + name: Optional[str] = None, + description: Optional[str] = None, + type: Optional[Any] = None, + default_value: Optional[Any] = None, + ): + self.name = name + self.description = description + self.type = type + self.default_value = default_value + + +@dataclass(frozen=True) +class GraphQLInputModel(GraphQLModel): + out_type: Any + out_names: Dict[str, str] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = InputTypeBindable(self.name, self.out_type, self.out_names) + bindable.bind_to_schema(schema) + + +def get_field_node_from_type_hint( + parent_type: GraphQLInput, + metadata: GraphQLMetadata, + field_name: str, + field_type: Any, + field_description: Optional[str] = None, + field_default_value: Optional[Any] = None, +) -> InputValueDefinitionNode: + if field_default_value is not None: + default_value = get_value_node(field_default_value) + else: + default_value = None + + return InputValueDefinitionNode( + description=get_description_node(field_description), + name=NameNode(value=field_name), + type=get_type_node(metadata, field_type, parent_type), + default_value=default_value, + ) + + def validate_input_type_with_schema(cls: Type[GraphQLInput]) -> Dict[str, Any]: definition = cast( InputObjectTypeDefinitionNode, @@ -332,54 +383,3 @@ def validate_field_default_value( f"for the '{field_name}' field that can't be " "represented in GraphQL schema." ) from e - - -class GraphQLInputField: - name: Optional[str] - description: Optional[str] - type: Optional[Any] - default_value: Optional[Any] - - def __init__( - self, - *, - name: Optional[str] = None, - description: Optional[str] = None, - type: Optional[Any] = None, - default_value: Optional[Any] = None, - ): - self.name = name - self.description = description - self.type = type - self.default_value = default_value - - -@dataclass(frozen=True) -class GraphQLInputModel(GraphQLModel): - out_type: Any - out_names: Dict[str, str] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = InputTypeBindable(self.name, self.out_type, self.out_names) - bindable.bind_to_schema(schema) - - -def get_field_node_from_type_hint( - parent_type: GraphQLInput, - metadata: GraphQLMetadata, - field_name: str, - field_type: Any, - field_description: Optional[str] = None, - field_default_value: Optional[Any] = None, -) -> InputValueDefinitionNode: - if field_default_value is not None: - default_value = get_value_node(field_default_value) - else: - default_value = None - - return InputValueDefinitionNode( - description=get_description_node(field_description), - name=NameNode(value=field_name), - type=get_type_node(metadata, field_type, parent_type), - default_value=default_value, - ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 29863c3..18557c0 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -292,296 +292,6 @@ def argument( return options -def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> List[str]: - definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) - - if not isinstance(definition, ObjectTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{ObjectTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - if not definition.fields: - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an object type without any fields. " - ) - - field_names: List[str] = [f.name.value for f in definition.fields] - field_definitions: Dict[str, FieldDefinitionNode] = { - f.name.value: f for f in definition.fields - } - - resolvers_names: List[str] = [] - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - raise ValueError( - f"Class '{cls.__name__}' defines 'GraphQLObjectField' instance. " - "This is not supported for types defining '__schema__'." - ) - - if isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.field not in field_names: - valid_fields: str = "', '".join(sorted(field_names)) - raise ValueError( - f"Class '{cls.__name__}' defines resolver for an undefined " - f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" - ) - - if cls_attr.field in resolvers_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple resolvers for field " - f"'{cls_attr.field}'." - ) - - if cls_attr.description and field_definitions[cls_attr.field].description: - raise ValueError( - f"Class '{cls.__name__}' defines multiple descriptions " - f"for field '{cls_attr.field}'." - ) - - if cls_attr.args: - field_args = { - arg.name.value: arg - for arg in field_definitions[cls_attr.field].arguments - } - - for arg_name, arg_options in cls_attr.args.items(): - if arg_name not in field_args: - raise ValueError( - f"Class '{cls.__name__}' defines options for '{arg_name}' " - f"argument of the '{cls_attr.field}' field " - "that doesn't exist." - ) - - if arg_options.get("name"): - raise ValueError( - f"Class '{cls.__name__}' defines 'name' option for " - f"'{arg_name}' argument of the '{cls_attr.field}' field. " - "This is not supported for types defining '__schema__'." - ) - - if arg_options.get("type"): - raise ValueError( - f"Class '{cls.__name__}' defines 'type' option for " - f"'{arg_name}' argument of the '{cls_attr.field}' field. " - "This is not supported for types defining '__schema__'." - ) - - if ( - arg_options.get("description") - and field_args[arg_name].description - ): - raise ValueError( - f"Class '{cls.__name__}' defines duplicate descriptions " - f"for '{arg_name}' argument " - f"of the '{cls_attr.field}' field." - ) - - validate_field_arg_default_value( - cls, cls_attr.field, arg_name, arg_options.get("default_value") - ) - - resolver_args = get_field_args_from_resolver(cls_attr.resolver) - for arg_name, arg_obj in resolver_args.items(): - validate_field_arg_default_value( - cls, cls_attr.field, arg_name, arg_obj.default_value - ) - - resolvers_names.append(cls_attr.field) - - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - validate_object_aliases(cls, aliases, field_names, resolvers_names) - - return [aliases.get(field_name, field_name) for field_name in field_names] - - -def validate_field_arg_default_value( - cls: Type[GraphQLObject], field_name: str, arg_name: str, default_value: Any -): - if default_value is None: - return - - try: - get_value_node(default_value) - except TypeError as e: - raise TypeError( - f"Class '{cls.__name__}' defines default value " - f"for '{arg_name}' argument " - f"of the '{field_name}' field that can't be " - "represented in GraphQL schema." - ) from e - - -def validate_object_type(cls: Type[GraphQLObject]) -> List[str]: - attrs_names: List[str] = [ - attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") - ] - - fields_instances: Dict[str, GraphQLObjectField] = {} - fields_names: List[str] = [] - resolvers_names: List[str] = [] - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - if cls_attr.name in fields_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple fields with GraphQL " - f"name '{cls_attr.name}'." - ) - - fields_names.append(cls_attr.name) - - if cls_attr not in attrs_names: - attrs_names.append(attr_name) - if cls_attr.resolver: - resolvers_names.append(attr_name) - - fields_instances[attr_name] = cls_attr - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.field in resolvers_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple resolvers for field " - f"'{cls_attr.field}'." - ) - - resolvers_names.append(cls_attr.field) - - field_instance = fields_instances.get(cls_attr.field) - if field_instance: - if field_instance.description and cls_attr.description: - raise ValueError( - f"Class '{cls.__name__}' defines multiple descriptions " - f"for field '{cls_attr.field}'." - ) - if field_instance.args and cls_attr.args: - raise ValueError( - f"Class '{cls.__name__}' defines multiple arguments options " - f"('args') for field '{cls_attr.field}'." - ) - - graphql_names: List[str] = [] - - for attr_name in attrs_names: - if getattr(cls, attr_name, None) is None: - attr_graphql_name = convert_python_name_to_graphql(attr_name) - - if attr_graphql_name in graphql_names or attr_graphql_name in fields_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple fields with GraphQL " - f"name '{attr_graphql_name}'." - ) - - graphql_names.append(attr_graphql_name) - - for resolver_for in resolvers_names: - if resolver_for not in attrs_names: - valid_fields: str = "', '".join(sorted(attrs_names)) - raise ValueError( - f"Class '{cls.__name__}' defines resolver for an undefined " - f"attribute '{resolver_for}'. (Valid attrs: '{valid_fields}')" - ) - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, (GraphQLObjectField, GraphQLObjectResolver)): - if not cls_attr.resolver: - continue - - resolver_args = get_field_args_from_resolver(cls_attr.resolver) - if resolver_args: - for arg_name, arg_obj in resolver_args.items(): - validate_field_arg_default_value( - cls, attr_name, arg_name, arg_obj.default_value - ) - - if not cls_attr.args: - continue - - resolver_args_names = list(resolver_args.keys()) - if resolver_args_names: - error_help = "expected one of: '%s'" % ( - "', '".join(resolver_args_names) - ) - else: - error_help = "function accepts no extra arguments" - - for arg_name, arg_options in cls_attr.args.items(): - if arg_name not in resolver_args_names: - if isinstance(cls_attr, GraphQLObjectField): - raise ValueError( - f"Class '{cls.__name__}' defines '{attr_name}' field " - f"with extra configuration for '{arg_name}' argument " - "thats not defined on the resolver function. " - f"({error_help})" - ) - - raise ValueError( - f"Class '{cls.__name__}' defines '{attr_name}' resolver " - f"with extra configuration for '{arg_name}' argument " - "thats not defined on the resolver function. " - f"({error_help})" - ) - - validate_field_arg_default_value( - cls, attr_name, arg_name, arg_options.get("default_value") - ) - - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - validate_object_aliases(cls, aliases, attrs_names, resolvers_names) - - return get_object_type_kwargs(cls, aliases) - - -def validate_object_aliases( - cls: Type[GraphQLObject], - aliases: Dict[str, str], - fields_names: List[str], - resolvers_names: List[str], -): - for alias in aliases: - if alias not in fields_names: - valid_fields: str = "', '".join(sorted(fields_names)) - raise ValueError( - f"Class '{cls.__name__}' defines an alias for an undefined " - f"field '{alias}'. (Valid fields: '{valid_fields}')" - ) - - if alias in resolvers_names: - raise ValueError( - f"Class '{cls.__name__}' defines an alias for a field " - f"'{alias}' that already has a custom resolver." - ) - - -def get_object_type_kwargs( - cls: Type[GraphQLObject], - aliases: Dict[str, str], -) -> List[str]: - kwargs: List[str] = [] - - for attr_name in cls.__annotations__: - if attr_name.startswith("__"): - continue - - attr_value = getattr(cls, attr_name, None) - if attr_value is None or isinstance(attr_value, GraphQLObjectField): - kwargs.append(aliases.get(attr_name, attr_name)) - - return kwargs - - @dataclass(frozen=True) class GraphQLObjectData: fields: Dict[str, "GraphQLObjectField"] @@ -917,3 +627,319 @@ def update_field_args_options( updated_args[arg_name] = field_args[arg_name] return updated_args + + +def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> List[str]: + definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) + + if not isinstance(definition, ObjectTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{ObjectTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + if not definition.fields: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an object type without any fields. " + ) + + field_names: List[str] = [f.name.value for f in definition.fields] + field_definitions: Dict[str, FieldDefinitionNode] = { + f.name.value: f for f in definition.fields + } + + resolvers_names: List[str] = [] + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + raise ValueError( + f"Class '{cls.__name__}' defines 'GraphQLObjectField' instance. " + "This is not supported for types defining '__schema__'." + ) + + if isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.field not in field_names: + valid_fields: str = "', '".join(sorted(field_names)) + raise ValueError( + f"Class '{cls.__name__}' defines resolver for an undefined " + f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" + ) + + if cls_attr.field in resolvers_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple resolvers for field " + f"'{cls_attr.field}'." + ) + + if cls_attr.description and field_definitions[cls_attr.field].description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{cls_attr.field}'." + ) + + if cls_attr.args: + field_args = { + arg.name.value: arg + for arg in field_definitions[cls_attr.field].arguments + } + + for arg_name, arg_options in cls_attr.args.items(): + if arg_name not in field_args: + raise ValueError( + f"Class '{cls.__name__}' defines options for '{arg_name}' " + f"argument of the '{cls_attr.field}' field " + "that doesn't exist." + ) + + if arg_options.get("name"): + raise ValueError( + f"Class '{cls.__name__}' defines 'name' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options.get("type"): + raise ValueError( + f"Class '{cls.__name__}' defines 'type' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if ( + arg_options.get("description") + and field_args[arg_name].description + ): + raise ValueError( + f"Class '{cls.__name__}' defines duplicate descriptions " + f"for '{arg_name}' argument " + f"of the '{cls_attr.field}' field." + ) + + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_options.get("default_value") + ) + + resolver_args = get_field_args_from_resolver(cls_attr.resolver) + for arg_name, arg_obj in resolver_args.items(): + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_obj.default_value + ) + + resolvers_names.append(cls_attr.field) + + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + validate_object_aliases(cls, aliases, field_names, resolvers_names) + + return [aliases.get(field_name, field_name) for field_name in field_names] + + +def validate_field_arg_default_value( + cls: Type[GraphQLObject], field_name: str, arg_name: str, default_value: Any +): + if default_value is None: + return + + try: + get_value_node(default_value) + except TypeError as e: + raise TypeError( + f"Class '{cls.__name__}' defines default value " + f"for '{arg_name}' argument " + f"of the '{field_name}' field that can't be " + "represented in GraphQL schema." + ) from e + + +def validate_object_type(cls: Type[GraphQLObject]) -> List[str]: + attrs_names: List[str] = [ + attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") + ] + + fields_instances: Dict[str, GraphQLObjectField] = {} + resolvers_instances: Dict[str, GraphQLObjectResolver] = {} + fields_names: List[str] = [] + resolvers_names: List[str] = [] + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + fields_instances[attr_name] = cls_attr + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers_instances[attr_name] = cls_attr + + for attr_name, field_instance in fields_instances.items(): + if field_instance.name in fields_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with GraphQL " + f"name '{field_instance.name}'." + ) + + fields_names.append(field_instance.name) + + if attr_name not in attrs_names: + attrs_names.append(attr_name) + if field_instance.resolver: + resolvers_names.append(attr_name) + + for attr_name, resolver_instance in resolvers_instances.items(): + if resolver_instance.field in resolvers_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple resolvers for field " + f"'{resolver_instance.field}'." + ) + + resolvers_names.append(resolver_instance.field) + + field_instance = fields_instances.get(resolver_instance.field) + if field_instance: + if field_instance.description and resolver_instance.description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{resolver_instance.field}'." + ) + if field_instance.args and resolver_instance.args: + raise ValueError( + f"Class '{cls.__name__}' defines multiple arguments options " + f"('args') for field '{resolver_instance.field}'." + ) + + validate_object_unique_graphql_names(cls, attrs_names, fields_names) + validate_object_resolvers_fields(cls, attrs_names, resolvers_names) + validate_object_fields_args(cls) + + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + validate_object_aliases(cls, aliases, attrs_names, resolvers_names) + + return get_object_type_kwargs(cls, aliases) + + +def validate_object_unique_graphql_names( + cls: Type[GraphQLObject], + attrs_names: List[str], + fields_names: List[str], +): + graphql_names: List[str] = [] + + for attr_name in attrs_names: + if getattr(cls, attr_name, None) is None: + attr_graphql_name = convert_python_name_to_graphql(attr_name) + + if attr_graphql_name in graphql_names or attr_graphql_name in fields_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with GraphQL " + f"name '{attr_graphql_name}'." + ) + + graphql_names.append(attr_graphql_name) + + +def validate_object_resolvers_fields( + cls: Type[GraphQLObject], + attrs_names: List[str], + resolvers_names: List[str], +): + for resolver_for in resolvers_names: + if resolver_for not in attrs_names: + valid_fields: str = "', '".join(sorted(attrs_names)) + raise ValueError( + f"Class '{cls.__name__}' defines resolver for an undefined " + f"attribute '{resolver_for}'. (Valid attrs: '{valid_fields}')" + ) + + +def validate_object_fields_args(cls: Type[GraphQLObject]): + for field_name in dir(cls): + field_instance = getattr(cls, field_name) + if ( + isinstance(field_instance, (GraphQLObjectField, GraphQLObjectResolver)) + and field_instance.resolver + ): + validate_object_field_args(cls, field_name, field_instance) + + +def validate_object_field_args( + cls: Type[GraphQLObject], + field_name: str, + field_instance: Union["GraphQLObjectField", "GraphQLObjectResolver"], +): + resolver_args = get_field_args_from_resolver(field_instance.resolver) + if resolver_args: + for arg_name, arg_obj in resolver_args.items(): + validate_field_arg_default_value( + cls, field_name, arg_name, arg_obj.default_value + ) + + if not field_instance.args: + return # Skip extra logic for validating instance.args + + resolver_args_names = list(resolver_args.keys()) + if resolver_args_names: + error_help = "expected one of: '%s'" % ("', '".join(resolver_args_names)) + else: + error_help = "function accepts no extra arguments" + + for arg_name, arg_options in field_instance.args.items(): + if arg_name not in resolver_args_names: + if isinstance(field_instance, GraphQLObjectField): + raise ValueError( + f"Class '{cls.__name__}' defines '{field_name}' field " + f"with extra configuration for '{arg_name}' argument " + "thats not defined on the resolver function. " + f"({error_help})" + ) + + raise ValueError( + f"Class '{cls.__name__}' defines '{field_name}' resolver " + f"with extra configuration for '{arg_name}' argument " + "thats not defined on the resolver function. " + f"({error_help})" + ) + + validate_field_arg_default_value( + cls, field_name, arg_name, arg_options.get("default_value") + ) + + +def validate_object_aliases( + cls: Type[GraphQLObject], + aliases: Dict[str, str], + fields_names: List[str], + resolvers_names: List[str], +): + for alias in aliases: + if alias not in fields_names: + valid_fields: str = "', '".join(sorted(fields_names)) + raise ValueError( + f"Class '{cls.__name__}' defines an alias for an undefined " + f"field '{alias}'. (Valid fields: '{valid_fields}')" + ) + + if alias in resolvers_names: + raise ValueError( + f"Class '{cls.__name__}' defines an alias for a field " + f"'{alias}' that already has a custom resolver." + ) + + +def get_object_type_kwargs( + cls: Type[GraphQLObject], + aliases: Dict[str, str], +) -> List[str]: + kwargs: List[str] = [] + + for attr_name in cls.__annotations__: + if attr_name.startswith("__"): + continue + + attr_value = getattr(cls, attr_name, None) + if attr_value is None or isinstance(attr_value, GraphQLObjectField): + kwargs.append(aliases.get(attr_name, attr_name)) + + return kwargs diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/scalartype.py index 5a2a134..c3e55f2 100644 --- a/ariadne_graphql_modules/next/scalartype.py +++ b/ariadne_graphql_modules/next/scalartype.py @@ -103,21 +103,6 @@ def unwrap(self) -> T: return self.wrapped_value -def validate_scalar_type_with_schema(cls: Type[GraphQLScalar]): - definition = parse_definition(cls.__name__, cls.__schema__) - - if not isinstance(definition, ScalarTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{ScalarTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - @dataclass(frozen=True) class GraphQScalarModel(GraphQLModel): serialize: Callable[[Any], Any] @@ -133,3 +118,18 @@ def bind_to_schema(self, schema: GraphQLSchema): ) bindable.bind_to_schema(schema) + + +def validate_scalar_type_with_schema(cls: Type[GraphQLScalar]): + definition = parse_definition(cls.__name__, cls.__schema__) + + if not isinstance(definition, ScalarTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{ScalarTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) diff --git a/tests_next/snapshots/snap_test_enum_type_validation.py b/tests_next/snapshots/snap_test_enum_type_validation.py index e306759..18f5943 100644 --- a/tests_next/snapshots/snap_test_enum_type_validation.py +++ b/tests_next/snapshots/snap_test_enum_type_validation.py @@ -7,24 +7,24 @@ snapshots = Snapshot() -snapshots['test_enum_type_validation_fails_for_empty_enum 1'] = "Class 'UserLevel' defines '__schema__' attribute that doesn't declare any enum members." - snapshots['test_enum_type_validation_fails_for_invalid_members 1'] = "Class 'UserLevel' '__members__' attribute is of unsupported type. Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. (found: '')" -snapshots['test_enum_type_validation_fails_for_invalid_type_schema 1'] = "Class 'UserLevel' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'EnumTypeDefinitionNode')" - snapshots['test_enum_type_validation_fails_for_missing_members 1'] = "Class 'UserLevel' '__members__' attribute is either missing or empty. Either define it or provide full SDL for this enum using the '__schema__' attribute." -snapshots['test_enum_type_validation_fails_for_name_mismatch_between_schema_and_attr 1'] = "Class 'UserLevel' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('UserRank' != 'Custom')" +snapshots['test_schema_enum_type_validation_fails_for_duplicated_members_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for enum members that also have description in '__schema__' attribute. (members: 'MEMBER')" + +snapshots['test_schema_enum_type_validation_fails_for_empty_enum 1'] = "Class 'UserLevel' defines '__schema__' attribute that doesn't declare any enum members." + +snapshots['test_schema_enum_type_validation_fails_for_invalid_members_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for undefined enum members. (undefined members: 'INVALID')" -snapshots['test_enum_type_validation_fails_for_schema_and_members_dict_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MEMBER')" +snapshots['test_schema_enum_type_validation_fails_for_invalid_type_schema 1'] = "Class 'UserLevel' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'EnumTypeDefinitionNode')" -snapshots['test_enum_type_validation_fails_for_schema_and_members_duplicated_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for enum members that also have description in '__schema__' attribute. (members: 'MEMBER')" +snapshots['test_schema_enum_type_validation_fails_for_names_not_matching 1'] = "Class 'UserLevel' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('UserRank' != 'Custom')" -snapshots['test_enum_type_validation_fails_for_schema_and_members_enum_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MODERATOR')" +snapshots['test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MEMBER')" -snapshots['test_enum_type_validation_fails_for_schema_and_members_list 1'] = "Class 'UserLevel' '__members__' attribute can't be a list when used together with '__schema__'." +snapshots['test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MODERATOR')" -snapshots['test_enum_type_validation_fails_for_schema_invalid_members_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for undefined enum members. (undefined members: 'INVALID')" +snapshots['test_schema_enum_type_validation_fails_for_schema_and_members_list 1'] = "Class 'UserLevel' '__members__' attribute can't be a list when used together with '__schema__'." -snapshots['test_enum_type_validation_fails_for_two_descriptions 1'] = "Class 'UserLevel' defines description in both '__description__' and '__schema__' attributes." +snapshots['test_schema_enum_type_validation_fails_for_two_descriptions 1'] = "Class 'UserLevel' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_input_type_validation.py b/tests_next/snapshots/snap_test_input_type_validation.py index 21fd659..b45d388 100644 --- a/tests_next/snapshots/snap_test_input_type_validation.py +++ b/tests_next/snapshots/snap_test_input_type_validation.py @@ -7,20 +7,20 @@ snapshots = Snapshot() -snapshots['test_input_type_validation_fails_for_duplicate_schema_out_name 1'] = "Class 'CustomType' defines multiple fields with an outname 'ok' in it's '__out_names__' attribute." +snapshots['test_input_type_validation_fails_for_out_names_without_schema 1'] = "Class 'CustomType' defines '__out_names__' attribute. This is not supported for types not defining '__schema__'." -snapshots['test_input_type_validation_fails_for_invalid_schema_out_name 1'] = "Class 'CustomType' defines an outname for 'invalid' field in it's '__out_names__' attribute which is not defined in '__schema__'." +snapshots['test_input_type_validation_fails_for_unsupported_attr_default 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." -snapshots['test_input_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'InputObjectTypeDefinitionNode')" +snapshots['test_input_type_validation_fails_for_unsupported_field_default_option 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." -snapshots['test_input_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" +snapshots['test_schema_input_type_validation_fails_for_duplicate_out_name 1'] = "Class 'CustomType' defines multiple fields with an outname 'ok' in it's '__out_names__' attribute." -snapshots['test_input_type_validation_fails_for_out_names_without_schema 1'] = "Class 'CustomType' defines '__out_names__' attribute. This is not supported for types not defining '__schema__'." +snapshots['test_schema_input_type_validation_fails_for_invalid_out_name 1'] = "Class 'CustomType' defines an outname for 'invalid' field in it's '__out_names__' attribute which is not defined in '__schema__'." -snapshots['test_input_type_validation_fails_for_schema_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an input type without any fields. " +snapshots['test_schema_input_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'InputObjectTypeDefinitionNode')" -snapshots['test_input_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." +snapshots['test_schema_input_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" -snapshots['test_input_type_validation_fails_for_unsupported_attr_default 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." +snapshots['test_schema_input_type_validation_fails_for_schema_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an input type without any fields. " -snapshots['test_input_type_validation_fails_for_unsupported_field_default_option 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." +snapshots['test_schema_input_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py index 4435af3..f53d4f0 100644 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -15,10 +15,6 @@ snapshots['test_object_type_validation_fails_for_invalid_alias 1'] = "Class 'CustomType' defines an alias for an undefined field 'invalid'. (Valid fields: 'hello')" -snapshots['test_object_type_validation_fails_for_invalid_schema_alias 1'] = "Class 'CustomType' defines an alias for an undefined field 'invalid'. (Valid fields: 'hello')" - -snapshots['test_object_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" - snapshots['test_object_type_validation_fails_for_missing_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (expected one of: 'name')" snapshots['test_object_type_validation_fails_for_missing_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (expected one of: 'name')" @@ -29,42 +25,46 @@ snapshots['test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'hello'." -snapshots['test_object_type_validation_fails_for_multiple_schema_field_resolvers 1'] = "Class 'CustomType' defines multiple resolvers for field 'hello'." +snapshots['test_object_type_validation_fails_for_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." + +snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined attribute 'other'. (Valid attrs: 'hello')" -snapshots['test_object_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" +snapshots['test_object_type_validation_fails_for_undefined_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" -snapshots['test_object_type_validation_fails_for_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." +snapshots['test_object_type_validation_fails_for_undefined_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" -snapshots['test_object_type_validation_fails_for_schema_field_with_arg_double_description 1'] = "Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' field." +snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." -snapshots['test_object_type_validation_fails_for_schema_field_with_arg_name 1'] = "Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." +snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default_option 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." -snapshots['test_object_type_validation_fails_for_schema_field_with_arg_type 1'] = "Class 'CustomType' defines 'type' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." +snapshots['test_schema_object_type_validation_fails_for_arg_with_double_description 1'] = "Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' field." -snapshots['test_object_type_validation_fails_for_schema_field_with_invalid_arg_name 1'] = "Class 'CustomType' defines options for 'other' argument of the 'hello' field that doesn't exist." +snapshots['test_schema_object_type_validation_fails_for_arg_with_name_option 1'] = "Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." -snapshots['test_object_type_validation_fails_for_schema_field_with_multiple_descriptions 1'] = "Class 'CustomType' defines multiple descriptions for field 'hello'." +snapshots['test_schema_object_type_validation_fails_for_arg_with_type_option 1'] = "Class 'CustomType' defines 'type' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." -snapshots['test_object_type_validation_fails_for_schema_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an object type without any fields. " +snapshots['test_schema_object_type_validation_fails_for_field_instance 1'] = "Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for types defining '__schema__'." -snapshots['test_object_type_validation_fails_for_schema_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." +snapshots['test_schema_object_type_validation_fails_for_field_with_invalid_arg_name 1'] = "Class 'CustomType' defines options for 'other' argument of the 'hello' field that doesn't exist." -snapshots['test_object_type_validation_fails_for_schema_with_field_instance 1'] = "Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for types defining '__schema__'." +snapshots['test_schema_object_type_validation_fails_for_field_with_multiple_descriptions 1'] = "Class 'CustomType' defines multiple descriptions for field 'hello'." -snapshots['test_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." +snapshots['test_schema_object_type_validation_fails_for_invalid_alias 1'] = "Class 'CustomType' defines an alias for an undefined field 'invalid'. (Valid fields: 'hello')" -snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined attribute 'other'. (Valid attrs: 'hello')" +snapshots['test_schema_object_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" -snapshots['test_object_type_validation_fails_for_undefined_field_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" +snapshots['test_schema_object_type_validation_fails_for_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an object type without any fields. " -snapshots['test_object_type_validation_fails_for_undefined_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" +snapshots['test_schema_object_type_validation_fails_for_multiple_field_resolvers 1'] = "Class 'CustomType' defines multiple resolvers for field 'hello'." -snapshots['test_object_type_validation_fails_for_undefined_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" +snapshots['test_schema_object_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" -snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." +snapshots['test_schema_object_type_validation_fails_for_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." -snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default_option 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." +snapshots['test_schema_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." + +snapshots['test_schema_object_type_validation_fails_for_undefined_field_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" -snapshots['test_object_type_validation_fails_for_unsupported_schema_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." +snapshots['test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." -snapshots['test_object_type_validation_fails_for_unsupported_schema_resolver_arg_option_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." +snapshots['test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." diff --git a/tests_next/snapshots/snap_test_scalar_type_validation.py b/tests_next/snapshots/snap_test_scalar_type_validation.py index db1f2a2..e481060 100644 --- a/tests_next/snapshots/snap_test_scalar_type_validation.py +++ b/tests_next/snapshots/snap_test_scalar_type_validation.py @@ -7,8 +7,8 @@ snapshots = Snapshot() -snapshots['test_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode')" +snapshots['test_schema_scalar_type_validation_fails_for_different_names 1'] = "Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Date' != 'Custom')" -snapshots['test_scalar_type_validation_fails_for_name_mismatch_between_schema_and_attr 1'] = "Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Date' != 'Custom')" +snapshots['test_schema_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode')" -snapshots['test_scalar_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomScalar' defines description in both '__description__' and '__schema__' attributes." +snapshots['test_schema_scalar_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomScalar' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/test_enum_type.py b/tests_next/test_enum_type.py index 4d5b79c..ce343a8 100644 --- a/tests_next/test_enum_type.py +++ b/tests_next/test_enum_type.py @@ -206,7 +206,7 @@ class QueryType(GraphQLObject): ) -def test_sdl_enum_field_returning_enum_value(assert_schema_equals): +def test_schema_enum_field_returning_enum_value(assert_schema_equals): class UserLevel(GraphQLEnum): __schema__ = """ enum UserLevel { @@ -247,7 +247,7 @@ def resolve_level(*_) -> UserLevelEnum: assert result.data == {"level": "MEMBER"} -def test_sdl_enum_field_returning_dict_value(assert_schema_equals): +def test_schema_enum_field_returning_dict_value(assert_schema_equals): class UserLevel(GraphQLEnum): __schema__ = """ enum UserLevel { @@ -292,7 +292,7 @@ def resolve_level(*_) -> int: assert result.data == {"level": "ADMIN"} -def test_sdl_enum_field_returning_str_value(assert_schema_equals): +def test_schema_enum_field_returning_str_value(assert_schema_equals): class UserLevel(GraphQLEnum): __schema__ = """ enum UserLevel { @@ -332,7 +332,7 @@ def resolve_level(*_) -> str: assert result.data == {"level": "GUEST"} -def test_sdl_enum_with_description_attr(assert_schema_equals): +def test_schema_enum_with_description_attr(assert_schema_equals): class UserLevel(GraphQLEnum): __schema__ = """ enum UserLevel { @@ -379,7 +379,7 @@ def resolve_level(*_) -> int: assert result.data == {"level": "ADMIN"} -def test_sdl_enum_with_schema_description(assert_schema_equals): +def test_schema_enum_with_schema_description(assert_schema_equals): class UserLevel(GraphQLEnum): __schema__ = """ \"\"\"Hello world.\"\"\" @@ -426,7 +426,7 @@ def resolve_level(*_) -> int: assert result.data == {"level": "ADMIN"} -def test_sdl_enum_with_member_description(assert_schema_equals): +def test_schema_enum_with_member_description(assert_schema_equals): class UserLevel(GraphQLEnum): __schema__ = """ enum UserLevel { @@ -474,7 +474,7 @@ def resolve_level(*_) -> int: assert result.data == {"level": "ADMIN"} -def test_sdl_enum_with_member_schema_description(assert_schema_equals): +def test_schema_enum_with_member_schema_description(assert_schema_equals): class UserLevel(GraphQLEnum): __schema__ = """ enum UserLevel { diff --git a/tests_next/test_enum_type_validation.py b/tests_next/test_enum_type_validation.py index 5c23156..2bac52f 100644 --- a/tests_next/test_enum_type_validation.py +++ b/tests_next/test_enum_type_validation.py @@ -6,7 +6,7 @@ from ariadne_graphql_modules.next import GraphQLEnum -def test_enum_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_enum_type_validation_fails_for_invalid_type_schema(snapshot): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -15,7 +15,7 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_name_mismatch_between_schema_and_attr( +def test_schema_enum_type_validation_fails_for_names_not_matching( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -34,7 +34,7 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_empty_enum(snapshot): +def test_schema_enum_type_validation_fails_for_empty_enum(snapshot): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -43,7 +43,7 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_enum_type_validation_fails_for_two_descriptions(snapshot): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -61,7 +61,7 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_schema_and_members_list(snapshot): +def test_schema_enum_type_validation_fails_for_schema_and_members_list(snapshot): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -78,7 +78,9 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_schema_and_members_dict_mismatch(snapshot): +def test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch( + snapshot, +): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -98,7 +100,9 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_schema_and_members_enum_mismatch(snapshot): +def test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch( + snapshot, +): with pytest.raises(ValueError) as exc_info: class UserLevelEnum(Enum): @@ -121,7 +125,7 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_schema_and_members_duplicated_descriptions( +def test_schema_enum_type_validation_fails_for_duplicated_members_descriptions( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -141,7 +145,7 @@ class UserLevel(GraphQLEnum): snapshot.assert_match(str(exc_info.value)) -def test_enum_type_validation_fails_for_schema_invalid_members_descriptions(snapshot): +def test_schema_enum_type_validation_fails_for_invalid_members_descriptions(snapshot): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): diff --git a/tests_next/test_input_type_validation.py b/tests_next/test_input_type_validation.py index d0b976e..d8efe64 100644 --- a/tests_next/test_input_type_validation.py +++ b/tests_next/test_input_type_validation.py @@ -4,7 +4,7 @@ from ariadne_graphql_modules.next import GraphQLInput -def test_input_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_input_type_validation_fails_for_invalid_type_schema(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -13,7 +13,7 @@ class CustomType(GraphQLInput): snapshot.assert_match(str(exc_info.value)) -def test_input_type_validation_fails_for_names_not_matching(snapshot): +def test_schema_input_type_validation_fails_for_names_not_matching(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -29,7 +29,7 @@ class CustomType(GraphQLInput): snapshot.assert_match(str(exc_info.value)) -def test_input_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_input_type_validation_fails_for_two_descriptions(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -46,7 +46,7 @@ class CustomType(GraphQLInput): snapshot.assert_match(str(exc_info.value)) -def test_input_type_validation_fails_for_schema_missing_fields(snapshot): +def test_schema_input_type_validation_fails_for_schema_missing_fields(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -68,7 +68,7 @@ class CustomType(GraphQLInput): snapshot.assert_match(str(exc_info.value)) -def test_input_type_validation_fails_for_invalid_schema_out_name(snapshot): +def test_schema_input_type_validation_fails_for_invalid_out_name(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -87,7 +87,7 @@ class CustomType(GraphQLInput): snapshot.assert_match(str(exc_info.value)) -def test_input_type_validation_fails_for_duplicate_schema_out_name(snapshot): +def test_schema_input_type_validation_fails_for_duplicate_out_name(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): diff --git a/tests_next/test_object_type_field_args.py b/tests_next/test_object_type_field_args.py index 6e2fd3f..2a358cb 100644 --- a/tests_next/test_object_type_field_args.py +++ b/tests_next/test_object_type_field_args.py @@ -153,7 +153,7 @@ class QueryType(GraphQLObject): assert result.data == {"hello": "Hello Anon!"} -def test_object_type_schema_field_with_arg_default_value( +def test_schema_object_type_field_with_arg_default_value( assert_schema_equals, ): class QueryType(GraphQLObject): @@ -184,7 +184,7 @@ def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str) -> str: assert result.data == {"hello": "Hello Anon!"} -def test_object_type_schema_field_with_arg_default_value_from_resolver_arg( +def test_schema_object_type_field_with_arg_default_value_from_resolver_arg( assert_schema_equals, ): class QueryType(GraphQLObject): @@ -215,7 +215,7 @@ def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str = "Anon") -> str: assert result.data == {"hello": "Hello Anon!"} -def test_object_type_schema_field_with_arg_default_value_from_resolver_arg_option( +def test_schema_object_type_field_with_arg_default_value_from_resolver_arg_option( assert_schema_equals, ): class QueryType(GraphQLObject): diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index aeb2b74..32486d7 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -4,7 +4,7 @@ from ariadne_graphql_modules.next import GraphQLObject -def test_object_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_object_type_validation_fails_for_invalid_type_schema(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -13,7 +13,7 @@ class CustomType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_names_not_matching(snapshot): +def test_schema_object_type_validation_fails_for_names_not_matching(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -29,7 +29,7 @@ class CustomType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_object_type_validation_fails_for_two_descriptions(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -46,7 +46,7 @@ class CustomType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_missing_fields(snapshot): +def test_schema_object_type_validation_fails_for_missing_fields(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -68,7 +68,7 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_undefined_field_resolver(snapshot): +def test_schema_object_type_validation_fails_for_undefined_field_resolver(snapshot): with pytest.raises(ValueError) as exc_info: class QueryType(GraphQLObject): @@ -200,7 +200,7 @@ def resolve_hello_other(*_): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_multiple_schema_field_resolvers(snapshot): +def test_schema_object_type_validation_fails_for_multiple_field_resolvers(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -238,7 +238,7 @@ def ipsum(*_) -> str: snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_field_with_multiple_descriptions( +def test_schema_object_type_validation_fails_for_field_with_multiple_descriptions( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -258,7 +258,7 @@ def ipsum(*_) -> str: snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_field_with_invalid_arg_name( +def test_schema_object_type_validation_fails_for_field_with_invalid_arg_name( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -279,7 +279,7 @@ def ipsum(*_, name: str) -> str: snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_field_with_arg_name( +def test_schema_object_type_validation_fails_for_arg_with_name_option( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -300,7 +300,7 @@ def ipsum(*_, name: str) -> str: snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_field_with_arg_type( +def test_schema_object_type_validation_fails_for_arg_with_type_option( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -321,7 +321,7 @@ def ipsum(*_, name: str) -> str: snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_field_with_arg_double_description( +def test_schema_object_type_validation_fails_for_arg_with_double_description( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -375,7 +375,7 @@ class CustomType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_invalid_schema_alias(snapshot): +def test_schema_object_type_validation_fails_for_invalid_alias(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -411,7 +411,7 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_resolver_alias(snapshot): +def test_schema_object_type_validation_fails_for_resolver_alias(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -434,7 +434,7 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_schema_with_field_instance(snapshot): +def test_schema_object_type_validation_fails_for_field_instance(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -481,7 +481,7 @@ def hello(*_, name: str): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_unsupported_schema_resolver_arg_default( +def test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default( snapshot, ): with pytest.raises(TypeError) as exc_info: @@ -502,7 +502,7 @@ def resolve_hello(*_, name: str = InvalidType): snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_unsupported_schema_resolver_arg_option_default( +def test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default( snapshot, ): with pytest.raises(TypeError) as exc_info: diff --git a/tests_next/test_scalar_type.py b/tests_next/test_scalar_type.py index a26b295..79f2c77 100644 --- a/tests_next/test_scalar_type.py +++ b/tests_next/test_scalar_type.py @@ -19,17 +19,6 @@ def serialize(cls, value): return str(value) -class SDLDateScalar(GraphQLScalar[date]): - __schema__ = "scalar Date" - - @classmethod - def serialize(cls, value): - if isinstance(value, cls): - return str(value.unwrap()) - - return str(value) - - def test_scalar_field_returning_scalar_instance(assert_schema_equals): class QueryType(GraphQLObject): date: DateScalar @@ -84,13 +73,24 @@ def resolve_date(*_) -> date: assert result.data == {"date": "1989-10-30"} -def test_sdl_scalar_field_returning_scalar_instance(assert_schema_equals): +class SchemaDateScalar(GraphQLScalar[date]): + __schema__ = "scalar Date" + + @classmethod + def serialize(cls, value): + if isinstance(value, cls): + return str(value.unwrap()) + + return str(value) + + +def test_schema_scalar_field_returning_scalar_instance(assert_schema_equals): class QueryType(GraphQLObject): - date: SDLDateScalar + date: SchemaDateScalar @GraphQLObject.resolver("date") - def resolve_date(*_) -> SDLDateScalar: - return SDLDateScalar(date(1989, 10, 30)) + def resolve_date(*_) -> SchemaDateScalar: + return SchemaDateScalar(date(1989, 10, 30)) schema = make_executable_schema(QueryType) diff --git a/tests_next/test_scalar_type_validation.py b/tests_next/test_scalar_type_validation.py index 14095f0..35041dd 100644 --- a/tests_next/test_scalar_type_validation.py +++ b/tests_next/test_scalar_type_validation.py @@ -4,7 +4,7 @@ from ariadne_graphql_modules.next import GraphQLScalar -def test_scalar_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_scalar_type_validation_fails_for_invalid_type_schema(snapshot): with pytest.raises(ValueError) as exc_info: class CustomScalar(GraphQLScalar[str]): @@ -13,7 +13,7 @@ class CustomScalar(GraphQLScalar[str]): snapshot.assert_match(str(exc_info.value)) -def test_scalar_type_validation_fails_for_name_mismatch_between_schema_and_attr( +def test_schema_scalar_type_validation_fails_for_different_names( snapshot, ): with pytest.raises(ValueError) as exc_info: @@ -25,7 +25,7 @@ class CustomScalar(GraphQLScalar[str]): snapshot.assert_match(str(exc_info.value)) -def test_scalar_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_scalar_type_validation_fails_for_two_descriptions(snapshot): with pytest.raises(ValueError) as exc_info: class CustomScalar(GraphQLScalar[str]): From 07e5c91edfa209dccae8a7d6d2349c9c60564506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Tue, 21 Nov 2023 17:12:09 +0100 Subject: [PATCH 19/63] GraphQL object types --- ariadne_graphql_modules/next/inputtype.py | 15 +- ariadne_graphql_modules/next/objecttype.py | 303 ++++++++++++------ tests_next/snapshots/snap_test_object_type.py | 2 +- .../snap_test_object_type_validation.py | 14 +- tests_next/test_input_type.py | 19 ++ tests_next/test_object_type.py | 219 ++++++++++++- tests_next/test_object_type_validation.py | 53 ++- 7 files changed, 510 insertions(+), 115 deletions(-) diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py index 520b883..dd4178c 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/inputtype.py @@ -328,15 +328,20 @@ def get_input_type_with_schema_kwargs( ) -> Dict[str, Any]: kwargs: Dict[str, Any] = {} for field in definition.fields: - if field.default_value: + try: + python_name = out_names[field.name.value] + except KeyError: + python_name = convert_graphql_name_to_python(field.name.value) + + attr_default_value = getattr(cls, python_name, None) + if attr_default_value is not None and not callable(attr_default_value): + default_value = attr_default_value + elif field.default_value: default_value = get_value_from_node(field.default_value) else: default_value = None - try: - kwargs[out_names[field.name.value]] = default_value - except KeyError: - kwargs[convert_graphql_name_to_python(field.name.value)] = default_value + kwargs[python_name] = default_value return kwargs diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 18557c0..17a7700 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,3 +1,4 @@ +from copy import deepcopy from dataclasses import dataclass, replace from enum import Enum from inspect import signature @@ -35,7 +36,7 @@ class GraphQLObject(GraphQLType): - __kwargs__: List[str] + __kwargs__: Dict[str, Any] __abstract__: bool = True __schema__: Optional[str] __description__: Optional[str] @@ -52,8 +53,8 @@ def __init__(self, **kwargs: Any): f"Valid keyword arguments: '{valid_kwargs}'" ) - for kwarg in self.__kwargs__: - setattr(self, kwarg, kwargs.get(kwarg)) + for kwarg, default in self.__kwargs__.items(): + setattr(self, kwarg, kwargs.get(kwarg, deepcopy(default))) def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -66,7 +67,7 @@ def __init_subclass__(cls) -> None: if cls.__dict__.get("__schema__"): cls.__kwargs__ = validate_object_type_with_schema(cls) else: - cls.__kwargs__ = validate_object_type(cls) + cls.__kwargs__ = validate_object_type_without_schema(cls) @classmethod def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": @@ -173,14 +174,21 @@ def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLObjectModel": type_data = get_graphql_object_data(metadata, cls) + type_aliases = getattr(cls, "__aliases__", None) or {} fields_ast: List[FieldDefinitionNode] = [] resolvers: Dict[str, Resolver] = {} + aliases: Dict[str, str] = {} out_names: Dict[str, Dict[str, str]] = {} - for field in type_data.fields.values(): + for attr_name, field in type_data.fields.items(): fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) + if attr_name in type_aliases: + aliases[field.name] = type_aliases[attr_name] + elif attr_name != field.name: + aliases[field.name] = attr_name + if field.resolver: resolvers[field.name] = field.resolver @@ -198,7 +206,7 @@ def __get_graphql_model_without_schema__( fields=tuple(fields_ast), ), resolvers=resolvers, - aliases=getattr(cls, "__aliases__", {}), + aliases=aliases, out_names=out_names, ) @@ -248,6 +256,7 @@ def field( type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, description: Optional[str] = None, + default_value: Optional[Any] = None, ): """Shortcut for object_field()""" return object_field( @@ -256,6 +265,7 @@ def field( name=name, type=type, description=description, + default_value=default_value, ) @staticmethod @@ -323,15 +333,25 @@ def create_graphql_object_data_without_schema( fields_descriptions: Dict[str, str] = {} fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} fields_resolvers: Dict[str, Resolver] = {} + fields_defaults: Dict[str, Any] = {} + fields_order: List[str] = [] type_hints = cls.__annotations__ - fields_order: List[str] = [] + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + aliases_targets: List[str] = list(aliases.values()) for attr_name, attr_type in type_hints.items(): if attr_name.startswith("__"): continue + if attr_name in aliases_targets: + # Alias target is not included in schema + # unless its explicit field + cls_attr = getattr(cls, attr_name, None) + if not isinstance(cls_attr, GraphQLObjectField): + continue + fields_order.append(attr_name) fields_names[attr_name] = convert_python_name_to_graphql(attr_name) @@ -361,8 +381,10 @@ def create_graphql_object_data_without_schema( fields_args[attr_name] = update_field_args_options( field_args, cls_attr.args ) + if cls_attr.default_value: + fields_defaults[attr_name] = cls_attr.default_value - if isinstance(cls_attr, GraphQLObjectResolver): + elif isinstance(cls_attr, GraphQLObjectResolver): if cls_attr.type and cls_attr.field not in fields_types: fields_types[cls_attr.field] = cls_attr.type if cls_attr.description: @@ -375,6 +397,9 @@ def create_graphql_object_data_without_schema( field_args, cls_attr.args ) + elif attr_name not in aliases_targets and not callable(cls_attr): + fields_defaults[attr_name] = cls_attr + fields: Dict[str, "GraphQLObjectField"] = {} for field_name in fields_order: fields[field_name] = GraphQLObjectField( @@ -383,6 +408,7 @@ def create_graphql_object_data_without_schema( type=fields_types[field_name], args=fields_args.get(field_name), resolver=fields_resolvers.get(field_name), + default_value=fields_defaults.get(field_name), ) return GraphQLObjectData(fields=fields) @@ -394,6 +420,7 @@ class GraphQLObjectField: type: Optional[Any] args: Optional[Dict[str, dict]] resolver: Optional[Resolver] + default_value: Optional[Any] def __init__( self, @@ -403,12 +430,14 @@ def __init__( type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, resolver: Optional[Resolver] = None, + default_value: Optional[Any] = None, ): self.name = name self.description = description self.type = type self.args = args self.resolver = resolver + self.default_value = default_value def __call__(self, resolver: Resolver): """Makes GraphQLObjectField instances work as decorators.""" @@ -425,6 +454,7 @@ def object_field( name: Optional[str] = None, description: Optional[str] = None, type: Optional[Any] = None, + default_value: Optional[Any] = None, ) -> GraphQLObjectField: field_type: Any = type if not type and resolver: @@ -436,6 +466,7 @@ def object_field( type=field_type, args=args, resolver=resolver, + default_value=default_value, ) @@ -629,7 +660,7 @@ def update_field_args_options( return updated_args -def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> List[str]: +def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> Dict[str, Any]: definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) if not isinstance(definition, ObjectTypeDefinitionNode): @@ -654,7 +685,7 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> List[str]: f.name.value: f for f in definition.fields } - resolvers_names: List[str] = [] + fields_resolvers: List[str] = [] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -672,12 +703,14 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> List[str]: f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" ) - if cls_attr.field in resolvers_names: + if cls_attr.field in fields_resolvers: raise ValueError( f"Class '{cls.__name__}' defines multiple resolvers for field " f"'{cls_attr.field}'." ) + fields_resolvers.append(cls_attr.field) + if cls_attr.description and field_definitions[cls_attr.field].description: raise ValueError( f"Class '{cls.__name__}' defines multiple descriptions " @@ -732,12 +765,10 @@ def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> List[str]: cls, cls_attr.field, arg_name, arg_obj.default_value ) - resolvers_names.append(cls_attr.field) - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - validate_object_aliases(cls, aliases, field_names, resolvers_names) + validate_object_aliases(cls, aliases, field_names, fields_resolvers) - return [aliases.get(field_name, field_name) for field_name in field_names] + return get_object_type_with_schema_kwargs(cls, aliases, field_names) def validate_field_arg_default_value( @@ -757,102 +788,101 @@ def validate_field_arg_default_value( ) from e -def validate_object_type(cls: Type[GraphQLObject]) -> List[str]: - attrs_names: List[str] = [ - attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") - ] - - fields_instances: Dict[str, GraphQLObjectField] = {} - resolvers_instances: Dict[str, GraphQLObjectResolver] = {} - fields_names: List[str] = [] - resolvers_names: List[str] = [] - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - fields_instances[attr_name] = cls_attr - if isinstance(cls_attr, GraphQLObjectResolver): - resolvers_instances[attr_name] = cls_attr +def validate_object_type_without_schema(cls: Type[GraphQLObject]) -> Dict[str, Any]: + data = get_object_type_validation_data(cls) - for attr_name, field_instance in fields_instances.items(): - if field_instance.name in fields_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple fields with GraphQL " - f"name '{field_instance.name}'." - ) + # Alias target is not present in schema as a field if its not an + # explicit field (instance of GraphQLObjectField) + for alias_target in data.aliases.values(): + if ( + alias_target in data.fields_attrs + and alias_target not in data.fields_instances + ): + data.fields_attrs.remove(alias_target) - fields_names.append(field_instance.name) + # Validate GraphQL names for future type's fields and assert those are unique + validate_object_unique_graphql_names(cls, data.fields_attrs, data.fields_instances) + validate_object_resolvers( + cls, data.fields_attrs, data.fields_instances, data.resolvers_instances + ) + validate_object_fields_args(cls) - if attr_name not in attrs_names: - attrs_names.append(attr_name) + # Gather names of field attrs with defined resolver + fields_resolvers: List[str] = [] + for attr_name, field_instance in data.fields_instances.items(): if field_instance.resolver: - resolvers_names.append(attr_name) - - for attr_name, resolver_instance in resolvers_instances.items(): - if resolver_instance.field in resolvers_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple resolvers for field " - f"'{resolver_instance.field}'." - ) - - resolvers_names.append(resolver_instance.field) - - field_instance = fields_instances.get(resolver_instance.field) - if field_instance: - if field_instance.description and resolver_instance.description: - raise ValueError( - f"Class '{cls.__name__}' defines multiple descriptions " - f"for field '{resolver_instance.field}'." - ) - if field_instance.args and resolver_instance.args: - raise ValueError( - f"Class '{cls.__name__}' defines multiple arguments options " - f"('args') for field '{resolver_instance.field}'." - ) + fields_resolvers.append(attr_name) + for resolver_instance in data.resolvers_instances.values(): + fields_resolvers.append(resolver_instance.field) - validate_object_unique_graphql_names(cls, attrs_names, fields_names) - validate_object_resolvers_fields(cls, attrs_names, resolvers_names) - validate_object_fields_args(cls) - - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - validate_object_aliases(cls, aliases, attrs_names, resolvers_names) + validate_object_aliases(cls, data.aliases, data.fields_attrs, fields_resolvers) - return get_object_type_kwargs(cls, aliases) + return get_object_type_kwargs(cls, data.aliases) def validate_object_unique_graphql_names( cls: Type[GraphQLObject], - attrs_names: List[str], - fields_names: List[str], + fields_attrs: List[str], + fields_instances: Dict[str, GraphQLObjectField], ): graphql_names: List[str] = [] - - for attr_name in attrs_names: - if getattr(cls, attr_name, None) is None: + for attr_name in fields_attrs: + if attr_name in fields_instances and fields_instances[attr_name].name: + attr_graphql_name = fields_instances[attr_name].name + else: attr_graphql_name = convert_python_name_to_graphql(attr_name) - if attr_graphql_name in graphql_names or attr_graphql_name in fields_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple fields with GraphQL " - f"name '{attr_graphql_name}'." - ) + if attr_graphql_name in graphql_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with GraphQL " + f"name '{attr_graphql_name}'." + ) - graphql_names.append(attr_graphql_name) + graphql_names.append(attr_graphql_name) -def validate_object_resolvers_fields( +def validate_object_resolvers( cls: Type[GraphQLObject], - attrs_names: List[str], - resolvers_names: List[str], + fields_names: List[str], + fields_instances: Dict[str, GraphQLObjectField], + resolvers_instances: Dict[str, GraphQLObjectResolver], ): - for resolver_for in resolvers_names: - if resolver_for not in attrs_names: - valid_fields: str = "', '".join(sorted(attrs_names)) + resolvers_fields: List[str] = [] + + for field_attr, field_instance in fields_instances.items(): + if field_instance.resolver: + resolvers_fields.append(field_attr) + + for resolver in resolvers_instances.values(): + if resolver.field not in fields_names: + valid_fields: str = "', '".join(sorted(fields_names)) raise ValueError( f"Class '{cls.__name__}' defines resolver for an undefined " - f"attribute '{resolver_for}'. (Valid attrs: '{valid_fields}')" + f"field '{resolver.field}'. (Valid fields: '{valid_fields}')" + ) + + if resolver.field in resolvers_fields: + raise ValueError( + f"Class '{cls.__name__}' defines multiple resolvers for field " + f"'{resolver.field}'." ) + resolvers_fields.append(resolver.field) + + field_instance = fields_instances.get(resolver.field) + if field_instance: + if field_instance.description and resolver.description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{resolver.field}'." + ) + + if field_instance.args and resolver.args: + raise ValueError( + f"Class '{cls.__name__}' defines multiple arguments options " + f"('args') for field '{resolver.field}'." + ) + def validate_object_fields_args(cls: Type[GraphQLObject]): for field_name in dir(cls): @@ -911,7 +941,7 @@ def validate_object_aliases( cls: Type[GraphQLObject], aliases: Dict[str, str], fields_names: List[str], - resolvers_names: List[str], + fields_resolvers: List[str], ): for alias in aliases: if alias not in fields_names: @@ -921,25 +951,108 @@ def validate_object_aliases( f"field '{alias}'. (Valid fields: '{valid_fields}')" ) - if alias in resolvers_names: + if alias in fields_resolvers: raise ValueError( f"Class '{cls.__name__}' defines an alias for a field " f"'{alias}' that already has a custom resolver." ) +@dataclass +class GraphQLObjectValidationData: + aliases: Dict[str, str] + fields_attrs: List[str] + fields_instances: Dict[str, GraphQLObjectField] + resolvers_instances: Dict[str, GraphQLObjectResolver] + + +def get_object_type_validation_data( + cls: Type[GraphQLObject], +) -> GraphQLObjectValidationData: + fields_attrs: List[str] = [ + attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") + ] + + fields_instances: Dict[str, GraphQLObjectField] = {} + resolvers_instances: Dict[str, GraphQLObjectResolver] = {} + + for attr_name in dir(cls): + if attr_name.startswith("__"): + continue + + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers_instances[attr_name] = cls_attr + if attr_name in fields_attrs: + fields_attrs.remove(attr_name) + + elif isinstance(cls_attr, GraphQLObjectField): + fields_instances[attr_name] = cls_attr + + if attr_name not in fields_attrs: + fields_attrs.append(attr_name) + + elif callable(attr_name): + if attr_name in fields_attrs: + fields_attrs.remove(attr_name) + + return GraphQLObjectValidationData( + aliases=getattr(cls, "__aliases__", None) or {}, + fields_attrs=fields_attrs, + fields_instances=fields_instances, + resolvers_instances=resolvers_instances, + ) + + def get_object_type_kwargs( cls: Type[GraphQLObject], aliases: Dict[str, str], -) -> List[str]: - kwargs: List[str] = [] +) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} for attr_name in cls.__annotations__: if attr_name.startswith("__"): continue - attr_value = getattr(cls, attr_name, None) - if attr_value is None or isinstance(attr_value, GraphQLObjectField): - kwargs.append(aliases.get(attr_name, attr_name)) + kwarg_name = aliases.get(attr_name, attr_name) + kwarg_value = getattr(cls, kwarg_name, None) + if isinstance(kwarg_value, GraphQLObjectField): + kwargs[kwarg_name] = kwarg_value.default_value + elif isinstance(kwarg_value, GraphQLObjectResolver): + continue # Skip resolver instances + elif not callable(kwarg_value): + kwargs[kwarg_name] = kwarg_value + + for attr_name in dir(cls): + if attr_name.startswith("__") or attr_name in kwargs: + continue + + kwarg_name = aliases.get(attr_name, attr_name) + kwarg_value = getattr(cls, kwarg_name) + if isinstance(kwarg_value, GraphQLObjectField): + kwargs[kwarg_name] = kwarg_value.default_value + elif not callable(kwarg_value): + kwargs[kwarg_name] = kwarg_value + + return kwargs + + +def get_object_type_with_schema_kwargs( + cls: Type[GraphQLObject], + aliases: Dict[str, str], + field_names: List[str], +) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} + + for field_name in field_names: + final_name = aliases.get(field_name, field_name) + attr_value = getattr(cls, final_name, None) + + if isinstance(attr_value, GraphQLObjectField): + kwargs[final_name] = attr_value.default_value + elif not isinstance(attr_value, GraphQLObjectResolver) and not callable( + attr_value + ): + kwargs[final_name] = attr_value return kwargs diff --git a/tests_next/snapshots/snap_test_object_type.py b/tests_next/snapshots/snap_test_object_type.py index 71a39d8..0b3809f 100644 --- a/tests_next/snapshots/snap_test_object_type.py +++ b/tests_next/snapshots/snap_test_object_type.py @@ -9,4 +9,4 @@ snapshots['test_object_type_instance_with_invalid_attrs_raising_error 1'] = "CategoryType.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'name', 'posts'" -snapshots['test_object_type_with_schema_instance_with_invalid_attrs_raising_error 1'] = "CategoryType.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'name', 'posts'" +snapshots['test_schema_object_type_instance_with_invalid_attrs_raising_error 1'] = "CategoryType.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'name', 'posts'" diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py index f53d4f0..2041eaa 100644 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ b/tests_next/snapshots/snap_test_object_type_validation.py @@ -7,6 +7,10 @@ snapshots = Snapshot() +snapshots['test_object_type_validation_fails_for_alias_resolver 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." + +snapshots['test_object_type_validation_fails_for_alias_target_resolver 1'] = "Class 'CustomType' defines resolver for an undefined field 'welcome'. (Valid fields: 'hello')" + snapshots['test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'userId'." snapshots['test_object_type_validation_fails_for_field_with_multiple_args 1'] = "Class 'CustomType' defines multiple resolvers for field 'lorem'." @@ -25,9 +29,7 @@ snapshots['test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'hello'." -snapshots['test_object_type_validation_fails_for_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." - -snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined attribute 'other'. (Valid attrs: 'hello')" +snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" snapshots['test_object_type_validation_fails_for_undefined_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" @@ -37,6 +39,10 @@ snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default_option 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." +snapshots['test_schema_object_type_validation_fails_for_alias_resolver 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." + +snapshots['test_schema_object_type_validation_fails_for_alias_target_resolver 1'] = "Class 'CustomType' defines resolver for an undefined field 'ok'. (Valid fields: 'hello')" + snapshots['test_schema_object_type_validation_fails_for_arg_with_double_description 1'] = "Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' field." snapshots['test_schema_object_type_validation_fails_for_arg_with_name_option 1'] = "Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." @@ -59,8 +65,6 @@ snapshots['test_schema_object_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" -snapshots['test_schema_object_type_validation_fails_for_resolver_alias 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." - snapshots['test_schema_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." snapshots['test_schema_object_type_validation_fails_for_undefined_field_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py index cd7ccef..e0b3063 100644 --- a/tests_next/test_input_type.py +++ b/tests_next/test_input_type.py @@ -148,6 +148,25 @@ class SearchInput(GraphQLInput): assert obj.age == 42 +def test_schema_input_type_instance_with_default_attrs_python_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String + age: Int + } + """ + ) + + query: str = "default" + age: int = 42 + + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 + + def test_schema_input_type_instance_with_invalid_attrs_raising_error(snapshot): class SearchInput(GraphQLInput): __schema__ = gql( diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index c5806cc..f978c07 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -27,6 +27,40 @@ class CategoryType(GraphQLObject): assert obj.posts == 20 +def test_object_type_instance_with_aliased_attrs_values(): + class CategoryType(GraphQLObject): + name: str + posts: int + + __aliases__ = {"name": "title"} + + title: str + + obj = CategoryType(title="Welcome", posts=20) + assert obj.title == "Welcome" + assert obj.posts == 20 + + +def test_object_type_instance_with_omitted_attrs_being_default_values(): + class CategoryType(GraphQLObject): + name: str = "Hello" + posts: int = 42 + + obj = CategoryType(posts=20) + assert obj.name == "Hello" + assert obj.posts == 20 + + +def test_object_type_instance_with_all_attrs_being_default_values(): + class CategoryType(GraphQLObject): + name: str = "Hello" + posts: int = 42 + + obj = CategoryType() + assert obj.name == "Hello" + assert obj.posts == 42 + + def test_object_type_instance_with_invalid_attrs_raising_error(snapshot): class CategoryType(GraphQLObject): name: str @@ -38,7 +72,7 @@ class CategoryType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) -def test_object_type_with_schema_instance_with_all_attrs_values(): +def test_schema_object_type_instance_with_all_attrs_values(): class CategoryType(GraphQLObject): __schema__ = gql( """ @@ -57,7 +91,7 @@ class CategoryType(GraphQLObject): assert obj.posts == 20 -def test_object_type_with_schema_instance_with_omitted_attrs_being_none(): +def test_schema_object_type_instance_with_omitted_attrs_being_none(): class CategoryType(GraphQLObject): __schema__ = gql( """ @@ -76,7 +110,85 @@ class CategoryType(GraphQLObject): assert obj.posts == 20 -def test_object_type_with_schema_instance_with_invalid_attrs_raising_error(snapshot): +def test_schema_object_type_instance_with_omitted_attrs_being_default_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str = "Hello" + posts: int = 42 + + obj = CategoryType(posts=20) + assert obj.name == "Hello" + assert obj.posts == 20 + + +def test_schema_object_type_instance_with_all_attrs_being_default_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str = "Hello" + posts: int = 42 + + obj = CategoryType() + assert obj.name == "Hello" + assert obj.posts == 42 + + +def test_schema_object_type_instance_with_aliased_attrs_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + __aliases__ = {"name": "title"} + + title: str = "Hello" + posts: int = 42 + + obj = CategoryType(title="Ok") + assert obj.title == "Ok" + assert obj.posts == 42 + + +def test_schema_object_type_instance_with_aliased_attrs_default_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + __aliases__ = {"name": "title"} + + title: str = "Hello" + posts: int = 42 + + obj = CategoryType() + assert obj.title == "Hello" + assert obj.posts == 42 + + +def test_schema_object_type_instance_with_invalid_attrs_raising_error(snapshot): class CategoryType(GraphQLObject): __schema__ = gql( """ @@ -96,7 +208,7 @@ class CategoryType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) -def test_object_type_with_schema_instance_with_aliased_attr_value(): +def test_schema_object_type_instance_with_aliased_attr_value(): class CategoryType(GraphQLObject): __schema__ = gql( """ @@ -138,10 +250,36 @@ class QueryType(GraphQLObject): def test_object_type_with_alias(assert_schema_equals): + class QueryType(GraphQLObject): + __aliases__ = {"hello": "welcome_message"} + + hello: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync( + schema, "{ hello }", root_value={"welcome_message": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_alias_excludes_alias_targets(assert_schema_equals): class QueryType(GraphQLObject): __aliases__ = {"hello": "welcome"} hello: str + welcome: str schema = make_executable_schema(QueryType) @@ -160,6 +298,79 @@ class QueryType(GraphQLObject): assert result.data == {"hello": "Hello World!"} +def test_object_type_with_alias_includes_aliased_field_instances(assert_schema_equals): + class QueryType(GraphQLObject): + __aliases__ = {"hello": "welcome"} + + hello: str + welcome: str = GraphQLObject.field() + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + welcome: String! + } + """, + ) + + result = graphql_sync( + schema, "{ hello welcome }", root_value={"welcome": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"hello": "Hello World!", "welcome": "Hello World!"} + + +def test_object_type_with_attr_automatic_alias(assert_schema_equals): + class QueryType(GraphQLObject): + test_message: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + testMessage: String! + } + """, + ) + + result = graphql_sync( + schema, "{ testMessage }", root_value={"test_message": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"testMessage": "Hello World!"} + + +def test_object_type_with_field_instance_automatic_alias(assert_schema_equals): + class QueryType(GraphQLObject): + message: str = GraphQLObject.field(name="testMessage") + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + testMessage: String! + } + """, + ) + + result = graphql_sync( + schema, "{ testMessage }", root_value={"message": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"testMessage": "Hello World!"} + + def test_object_type_with_field_resolver(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index 32486d7..6b0158e 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -369,7 +369,7 @@ class CustomType(GraphQLObject): hello: str __aliases__ = { - "invalid": "ok", + "invalid": "target", } snapshot.assert_match(str(exc_info.value)) @@ -388,20 +388,20 @@ class CustomType(GraphQLObject): ) __aliases__ = { - "invalid": "ok", + "invalid": "welcome", } snapshot.assert_match(str(exc_info.value)) -def test_object_type_validation_fails_for_resolver_alias(snapshot): +def test_object_type_validation_fails_for_alias_resolver(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): hello: str __aliases__ = { - "hello": "ok", + "hello": "welcome", } @GraphQLObject.resolver("hello") @@ -411,7 +411,25 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_resolver_alias(snapshot): +def test_object_type_validation_fails_for_alias_target_resolver(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + hello: str + welcome: str + + __aliases__ = { + "hello": "welcome", + } + + @GraphQLObject.resolver("welcome") + def resolve_welcome(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + +def test_schema_object_type_validation_fails_for_alias_resolver(snapshot): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -434,6 +452,31 @@ def resolve_hello(*_): snapshot.assert_match(str(exc_info.value)) +def test_schema_object_type_validation_fails_for_alias_target_resolver(snapshot): + with pytest.raises(ValueError) as exc_info: + + class CustomType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + __aliases__ = { + "hello": "ok", + } + + ok: str + + @GraphQLObject.resolver("ok") + def resolve_hello(*_): + return "Hello World!" + + snapshot.assert_match(str(exc_info.value)) + + def test_schema_object_type_validation_fails_for_field_instance(snapshot): with pytest.raises(ValueError) as exc_info: From 26437c0196816023810e0561d1bbb1119bf836db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Mon, 8 Jan 2024 18:20:53 +0100 Subject: [PATCH 20/63] WIP finalize make_executable_schema --- ariadne_graphql_modules/next/__init__.py | 5 + .../next/executable_schema.py | 120 ++++++++----- ariadne_graphql_modules/next/roots.py | 83 +++++++++ ariadne_graphql_modules/next/sort.py | 127 ++++++++++++++ .../snap_test_make_executable_schema.py | 10 ++ tests_next/test_enum_type.py | 104 +++++------ tests_next/test_make_executable_schema.py | 165 ++++++++++++++++++ tests_next/test_standard_enum.py | 8 +- 8 files changed, 526 insertions(+), 96 deletions(-) create mode 100644 ariadne_graphql_modules/next/roots.py create mode 100644 ariadne_graphql_modules/next/sort.py create mode 100644 tests_next/snapshots/snap_test_make_executable_schema.py create mode 100644 tests_next/test_make_executable_schema.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index a01f98b..e8f3b41 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -15,7 +15,9 @@ from .idtype import GraphQLID from .inputtype import GraphQLInput, GraphQLInputModel from .objecttype import GraphQLObject, GraphQLObjectModel, object_field +from .roots import ROOTS_NAMES, merge_root_nodes from .scalartype import GraphQLScalar, GraphQScalarModel +from .sort import sort_schema_document from .value import get_value_from_node, get_value_node __all__ = [ @@ -31,6 +33,7 @@ "GraphQLScalar", "GraphQScalarModel", "GraphQLType", + "ROOTS_NAMES", "convert_graphql_name_to_python", "convert_python_name_to_graphql", "create_graphql_enum_model", @@ -40,5 +43,7 @@ "get_value_node", "graphql_enum", "make_executable_schema", + "merge_root_nodes", "object_field", + "sort_schema_document", ] diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index e088a66..e4d3d7f 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -1,19 +1,25 @@ from enum import Enum from typing import Any, Dict, List, Optional, Sequence, Type, Union -from ariadne import SchemaBindable, SchemaDirectiveVisitor, SchemaNameConverter +from ariadne import ( + SchemaBindable, + SchemaDirectiveVisitor, + SchemaNameConverter, + repair_schema_default_enum_values, + validate_schema_default_enum_values +) from graphql import ( DocumentNode, GraphQLSchema, assert_valid_schema, build_ast_schema, + parse, + concat_ast, ) from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .enumtype import GraphQLEnumModel -from .inputtype import GraphQLInputModel -from .objecttype import GraphQLObjectModel -from .scalartype import GraphQScalarModel +from .roots import ROOTS_NAMES, merge_root_nodes +from .sort import sort_schema_document SchemaType = Union[ str, @@ -27,27 +33,63 @@ def make_executable_schema( *types: Union[SchemaType, List[SchemaType]], directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, convert_names_case: Union[bool, SchemaNameConverter] = False, + merge_roots: bool = True, ) -> GraphQLSchema: metadata = GraphQLMetadata() type_defs: List[str] = find_type_defs(types) types_list: List[SchemaType] = flatten_types(types, metadata) - assert_types_unique(types_list) + assert_types_unique(types_list, merge_roots) assert_types_not_abstract(types_list) - schema_models: List[GraphQLModel] = sort_models( - [metadata.get_graphql_model(type_def) for type_def in types_list] - ) + schema_bindables: List[Union[SchemaBindable, GraphQLModel]] = [] + for type_def in types_list: + if isinstance(type_def, SchemaBindable): + schema_bindables.append(type_def) + else: + schema_bindables.append(metadata.get_graphql_model(type_def)) + + schema_models: List[GraphQLModel] = [ + type_def for type_def in schema_bindables if isinstance(type_def, GraphQLModel) + ] - document_node = DocumentNode( - definitions=tuple(schema_model.ast for schema_model in schema_models), - ) + models_document: Optional[DocumentNode] = None + type_defs_document: Optional[DocumentNode] = None + + if schema_models: + models_document = DocumentNode( + definitions=tuple(schema_model.ast for schema_model in schema_models), + ) + + if type_defs: + type_defs_document = parse("\n".join(type_defs)) + if models_document and type_defs_document: + document_node = concat_ast((models_document, type_defs_document)) + elif models_document: + document_node = models_document + elif type_defs_document: + document_node = type_defs_document + else: + raise ValueError( + "'make_executable_schema' was called without any GraphQL types." + ) + + if merge_roots: + document_node = merge_root_nodes(document_node) + + document_node = sort_schema_document(document_node) schema = build_ast_schema(document_node) + assert_valid_schema(schema) + validate_schema_default_enum_values(schema) + repair_schema_default_enum_values(schema) + + for schema_bindable in schema_bindables: + schema_bindable.bind_to_schema(schema) - for schema_model in schema_models: - schema_model.bind_to_schema(schema) + if convert_names_case: + pass return schema @@ -74,7 +116,10 @@ def flatten_types( types_list: List[Union[Enum, SchemaBindable, GraphQLModel]] = [] for type_def in flat_schema_types_list: - if issubclass(type_def, GraphQLType): + if isinstance(type_def, SchemaBindable): + types_list.append(type_def) + + elif issubclass(type_def, GraphQLType): type_name = type_def.__name__ if getattr(type_def, "__abstract__", None): @@ -85,10 +130,10 @@ def flatten_types( types_list.append(type_def) - if issubclass(type_def, Enum): + elif issubclass(type_def, Enum): types_list.append(type_def) - if isinstance(type_def, list): + elif isinstance(type_def, list): types_list += find_type_defs(type_def) return types_list @@ -102,8 +147,12 @@ def flatten_schema_types( flat_list: List[SchemaType] = [] for type_def in types: - if isinstance(type_def, list): + if isinstance(type_def, str): + continue + elif isinstance(type_def, list): flat_list += flatten_schema_types(type_def, metadata, dedupe=False) + elif isinstance(type_def, SchemaBindable): + flat_list.append(type_def) elif issubclass(type_def, GraphQLType): flat_list += type_def.__get_graphql_types__(metadata) elif get_graphql_type_name(type_def): @@ -121,6 +170,9 @@ def flatten_schema_types( def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: + if isinstance(type_def, SchemaBindable): + return None + if issubclass(type_def, Enum): return type_def.__name__ @@ -130,10 +182,16 @@ def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: return None -def assert_types_unique(type_defs: List[SchemaType]): +def assert_types_unique(type_defs: List[SchemaType], merge_roots: bool): types_names: Dict[str, Any] = {} for type_def in type_defs: type_name = get_graphql_type_name(type_def) + if not type_name: + continue + + if merge_roots and type_name in ROOTS_NAMES: + continue + if type_name in types_names: raise ValueError( f"Types '{type_def.__name__}' and '{types_names[type_name]}' both define " @@ -145,6 +203,9 @@ def assert_types_unique(type_defs: List[SchemaType]): def assert_types_not_abstract(type_defs: List[SchemaType]): for type_def in type_defs: + if isinstance(type_def, SchemaBindable): + continue + if issubclass(type_def, GraphQLType) and getattr( type_def, "__abstract__", None ): @@ -152,24 +213,3 @@ def assert_types_not_abstract(type_defs: List[SchemaType]): f"Type '{type_def.__name__}' is an abstract type and can't be used " "for schema creation." ) - - -def sort_models(schema_models: List[GraphQLModel]) -> List[GraphQLModel]: - sorted_models: List[GraphQLModel] = [] - sorted_models += sort_models_by_type(GraphQScalarModel, schema_models) - sorted_models += sort_models_by_type(GraphQLEnumModel, schema_models) - sorted_models += [ - model for model in schema_models if isinstance(model, GraphQLObjectModel) - ] - sorted_models += sort_models_by_type(GraphQLInputModel, schema_models) - return sorted_models - - -def sort_models_by_type( - model_type: Type[GraphQLModel], - schema_models: List[GraphQLModel], -) -> List[GraphQLModel]: - return sorted( - [model for model in schema_models if isinstance(model, model_type)], - key=lambda m: m.name, - ) diff --git a/ariadne_graphql_modules/next/roots.py b/ariadne_graphql_modules/next/roots.py new file mode 100644 index 0000000..d1d73ff --- /dev/null +++ b/ariadne_graphql_modules/next/roots.py @@ -0,0 +1,83 @@ +from typing import Dict, List, Optional, cast + +from graphql import ( + ConstDirectiveNode, + DocumentNode, + FieldDefinitionNode, + NamedTypeNode, + ObjectTypeDefinitionNode, + StringValueNode, + TypeDefinitionNode, +) + +DefinitionsList = List[TypeDefinitionNode] + +ROOTS_NAMES = ("Query", "Mutation", "Subscription") + + +def merge_root_nodes(document_node: DocumentNode) -> DocumentNode: + roots_definitions: Dict[str, List] = {root: [] for root in ROOTS_NAMES} + final_definitions: DefinitionsList = [] + + for node in document_node.definitions: + if not isinstance(node, TypeDefinitionNode): + raise ValueError( + "Only type definition nodes can be merged. " + f"Found unsupported node: '{node}'" + ) + + if node.name.value in roots_definitions: + roots_definitions[node.name.value].append(node) + else: + final_definitions.append(node) + + for definitions_to_merge in roots_definitions.values(): + if len(definitions_to_merge) > 1: + final_definitions.append(merge_nodes(definitions_to_merge)) + elif definitions_to_merge: + final_definitions.extend(definitions_to_merge) + + return DocumentNode(definitions=tuple(final_definitions)) + + +def merge_nodes(nodes: List[TypeDefinitionNode]) -> ObjectTypeDefinitionNode: + root_name = nodes[0].name.value + + description: Optional[StringValueNode] = None + interfaces: List[NamedTypeNode] = [] + directives: List[ConstDirectiveNode] = [] + fields: Dict[str, FieldDefinitionNode] = {} + + for node in nodes: + node = cast(ObjectTypeDefinitionNode, node) + if node.description: + if description: + raise ValueError( + f"Multiple {root_name} types are defining descriptions." + ) + + description = node.description + + if node.interfaces: + interfaces.extend(node.interfaces) + if node.directives: + directives.extend(node.directives) + + for field_node in node.fields: + field_name = field_node.name.value + if field_name in fields: + other_type_source = fields[field_name][0] + raise ValueError( + f"Multiple {root_name} types are defining same field " + f"'{field_name}': {other_type_source}, {field_node}" + ) + + fields[field_name] = field_node + + return ObjectTypeDefinitionNode( + name=nodes[0].name, + description=description, + interfaces=tuple(interfaces), + directives=tuple(directives), + fields=tuple(fields[field_name] for field_name in sorted(fields)), + ) diff --git a/ariadne_graphql_modules/next/sort.py b/ariadne_graphql_modules/next/sort.py new file mode 100644 index 0000000..ae9b68c --- /dev/null +++ b/ariadne_graphql_modules/next/sort.py @@ -0,0 +1,127 @@ +from typing import Dict, List, Optional, Union, cast + +from graphql import ( + DirectiveNode, + DocumentNode, + InputObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + ListTypeNode, + NonNullTypeNode, + ObjectTypeDefinitionNode, + ScalarTypeDefinitionNode, + TypeDefinitionNode, + TypeNode, +) + +from .roots import ROOTS_NAMES + + +def sort_schema_document(document: DocumentNode) -> DocumentNode: + unsorted_nodes: Dict[str, TypeDefinitionNode] = {} + sorted_nodes: List[TypeDefinitionNode] = [] + + for node in document.definitions: + cast_node = cast(TypeDefinitionNode, node) + unsorted_nodes[cast_node.name.value] = cast_node + + # Start schema from directives and scalars + sorted_nodes += get_sorted_directives(unsorted_nodes) + sorted_nodes += get_sorted_scalars(unsorted_nodes) + + # Next, include Query, Mutation and Subscription branches + for root in ROOTS_NAMES: + sorted_nodes += get_sorted_type(root, unsorted_nodes) + + # Finally include unused types + sorted_nodes += list(unsorted_nodes.values()) + + return DocumentNode(definitions=tuple(sorted_nodes)) + + +def get_sorted_directives( + unsorted_nodes: Dict[str, TypeDefinitionNode] +) -> List[DirectiveNode]: + directives: List[DirectiveNode] = [] + for name, model in tuple(unsorted_nodes.items()): + if isinstance(model, DirectiveNode): + directives.append(unsorted_nodes.pop(name)) + + return sorted(directives, key=lambda m: m.name.value) + + +def get_sorted_scalars( + unsorted_nodes: Dict[str, TypeDefinitionNode] +) -> List[ScalarTypeDefinitionNode]: + scalars: List[ScalarTypeDefinitionNode] = [] + for name, model in tuple(unsorted_nodes.items()): + if isinstance(model, ScalarTypeDefinitionNode): + scalars.append(unsorted_nodes.pop(name)) + + return sorted(scalars, key=lambda m: m.name.value) + + +def get_sorted_type( + root: str, + unsorted_nodes: Dict[str, TypeDefinitionNode], +) -> List[TypeDefinitionNode]: + sorted_nodes: List[TypeDefinitionNode] = [] + if root not in unsorted_nodes: + return sorted_nodes + + root_node = unsorted_nodes.pop(root) + sorted_nodes.append(root_node) + + if isinstance(root_node, (ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode)): + sorted_nodes += get_sorted_object_dependencies(root_node, unsorted_nodes) + elif isinstance(root_node, InputObjectTypeDefinitionNode): + pass + + return sorted_nodes + + +def get_sorted_object_dependencies( + root_node: Union[ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode], + unsorted_nodes: Dict[str, TypeDefinitionNode], +) -> List[TypeDefinitionNode]: + sorted_nodes: List[TypeDefinitionNode] = [] + + if root_node.interfaces: + for interface in root_node.interfaces: + interface_name = interface.name.value + interface_node = unsorted_nodes.pop(interface_name, None) + if interface_node: + sorted_nodes.append(interface_node) + sorted_nodes += get_sorted_object_dependencies( + interface_node, unsorted_nodes + ) + + for field in root_node.fields: + if field.arguments: + for argument in field.arguments: + argument_type = unwrap_type_name(argument.type) + sorted_nodes += get_sorted_type(argument_type, unsorted_nodes) + + field_type = unwrap_type_name(field.type) + sorted_nodes += get_sorted_type(field_type, unsorted_nodes) + + return sorted_nodes + + +def get_sorted_input_dependencies( + root_node: InputObjectTypeDefinitionNode, + unsorted_nodes: Dict[str, TypeDefinitionNode], +) -> List[TypeDefinitionNode]: + sorted_nodes: List[TypeDefinitionNode] = [] + + for field in root_node.fields: + field_type = unwrap_type_name(field.type) + sorted_nodes += get_sorted_type(field_type, unsorted_nodes) + + return sorted_nodes + + +def unwrap_type_name(type_node: TypeNode) -> str: + if isinstance(type_node, (ListTypeNode, NonNullTypeNode)): + return unwrap_type_name(type_node.type) + + return type_node.name.value diff --git a/tests_next/snapshots/snap_test_make_executable_schema.py b/tests_next/snapshots/snap_test_make_executable_schema.py new file mode 100644 index 0000000..9cfb3c1 --- /dev/null +++ b/tests_next/snapshots/snap_test_make_executable_schema.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_multiple_roots_fail_validation_if_merge_roots_is_disabled 1'] = "Types 'SecondRoot' and '.FirstRoot'>' both define GraphQL type with name 'Query'." diff --git a/tests_next/test_enum_type.py b/tests_next/test_enum_type.py index ce343a8..e4bf3c0 100644 --- a/tests_next/test_enum_type.py +++ b/tests_next/test_enum_type.py @@ -31,15 +31,15 @@ def resolve_level(*_) -> UserLevelEnum: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -69,15 +69,15 @@ def resolve_level(*_) -> dict: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -107,15 +107,15 @@ def resolve_level(*_) -> str: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -138,15 +138,15 @@ class QueryType(GraphQLObject): assert_schema_equals( schema, """ + type Query { + level: UserLevelEnum! + } + enum UserLevelEnum { GUEST MEMBER ADMIN } - - type Query { - level: UserLevelEnum! - } """, ) @@ -164,16 +164,16 @@ class QueryType(GraphQLObject): assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + \"\"\"Hello world.\"\"\" enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -191,6 +191,10 @@ class QueryType(GraphQLObject): assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST @@ -198,10 +202,6 @@ class QueryType(GraphQLObject): MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -229,15 +229,15 @@ def resolve_level(*_) -> UserLevelEnum: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -274,15 +274,15 @@ def resolve_level(*_) -> int: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -314,15 +314,15 @@ def resolve_level(*_) -> str: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -360,16 +360,16 @@ def resolve_level(*_) -> int: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + \"\"\"Hello world.\"\"\" enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -407,16 +407,16 @@ def resolve_level(*_) -> int: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + \"\"\"Hello world.\"\"\" enum UserLevel { GUEST MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -454,6 +454,10 @@ def resolve_level(*_) -> int: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST @@ -461,10 +465,6 @@ def resolve_level(*_) -> int: MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) @@ -502,6 +502,10 @@ def resolve_level(*_) -> int: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST @@ -509,10 +513,6 @@ def resolve_level(*_) -> int: MEMBER ADMIN } - - type Query { - level: UserLevel! - } """, ) diff --git a/tests_next/test_make_executable_schema.py b/tests_next/test_make_executable_schema.py new file mode 100644 index 0000000..4374458 --- /dev/null +++ b/tests_next/test_make_executable_schema.py @@ -0,0 +1,165 @@ +import pytest +from graphql import graphql_sync + +from ariadne import QueryType +from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema + + +def test_executable_schema_from_vanilla_schema_definition(assert_schema_equals): + query_type = QueryType() + query_type.set_field("message", lambda *_: "Hello world!") + + schema = make_executable_schema( + """ + type Query { + message: String! + } + """, + query_type + ) + + assert_schema_equals( + schema, + """ + type Query { + message: String! + } + """, + ) + + result = graphql_sync(schema, '{ message }') + + assert not result.errors + assert result.data == {"message": "Hello world!"} + + +def test_executable_schema_from_combined_vanilla_and_new_schema_definition(assert_schema_equals): + class UserType(GraphQLObject): + name: str + email: str + + query_type = QueryType() + query_type.set_field( + "user", lambda *_: UserType(name="Bob", email="test@example.com") + ) + + schema = make_executable_schema( + """ + type Query { + user: User! + } + """, + query_type, + UserType, + ) + + assert_schema_equals( + schema, + """ + type Query { + user: User! + } + + type User { + name: String! + email: String! + } + """, + ) + + result = graphql_sync(schema, '{ user { name email } }') + + assert not result.errors + assert result.data == { + "user": {"name": "Bob", "email": "test@example.com"}, + } + + +def test_executable_schema_with_merged_roots(assert_schema_equals): + class FirstRoot(GraphQLObject): + __graphql_name__ = "Query" + + name: str + surname: str + + class SecondRoot(GraphQLObject): + __graphql_name__ = "Query" + + message: str + + class ThirdRoot(GraphQLObject): + __graphql_name__ = "Query" + + score: int + + schema = make_executable_schema(FirstRoot, SecondRoot, ThirdRoot) + + assert_schema_equals( + schema, + """ + type Query { + message: String! + name: String! + score: Int! + surname: String! + } + """, + ) + + +def test_executable_schema_with_merged_object_and_vanilla_roots(assert_schema_equals): + class FirstRoot(GraphQLObject): + __graphql_name__ = "Query" + + name: str + surname: str + + class SecondRoot(GraphQLObject): + __graphql_name__ = "Query" + + message: str + + schema = make_executable_schema( + FirstRoot, + SecondRoot, + """ + type Query { + score: Int! + } + """, + ) + + assert_schema_equals( + schema, + """ + type Query { + message: String! + name: String! + score: Int! + surname: String! + } + """, + ) + + +def test_multiple_roots_fail_validation_if_merge_roots_is_disabled(snapshot): + class FirstRoot(GraphQLObject): + __graphql_name__ = "Query" + + name: str + surname: str + + class SecondRoot(GraphQLObject): + __graphql_name__ = "Query" + + message: str + + class ThirdRoot(GraphQLObject): + __graphql_name__ = "Query" + + score: int + + with pytest.raises(ValueError) as exc_info: + make_executable_schema(FirstRoot, SecondRoot, ThirdRoot, merge_roots=False) + + snapshot.assert_match(str(exc_info.value)) diff --git a/tests_next/test_standard_enum.py b/tests_next/test_standard_enum.py index 795f424..0e539a2 100644 --- a/tests_next/test_standard_enum.py +++ b/tests_next/test_standard_enum.py @@ -233,16 +233,16 @@ def resolve_level(*_) -> UserLevel: assert_schema_equals( schema, """ + type Query { + level: UserLevel! + } + enum UserLevel { GUEST MEMBER MODERATOR ADMINISTRATOR } - - type Query { - level: UserLevel! - } """, ) From e2e9052fc2f43a6b514ab3e287673e71bcd37333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Mon, 8 Jan 2024 18:27:42 +0100 Subject: [PATCH 21/63] Mute too many returns warning --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 02b186f..cd1fe1b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ ignore=snapshots load-plugins=pylint.extensions.bad_builtin, pylint.extensions.mccabe [MESSAGES CONTROL] -disable=C0103, C0111, C0209, C0412, I0011, R0101, R0801, R0901, R0902, R0903, R0912, R0913, R0914, R0915, R1260, W0231, W0621, W0703 +disable=C0103, C0111, C0209, C0412, I0011, R0101, R0801, R0901, R0902, R0903, R0911, R0912, R0913, R0914, R0915, R1260, W0231, W0621, W0703 [SIMILARITIES] ignore-imports=yes From 1c0fe66a91a47a089e5075f4fe26bdfd2addacf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Mon, 8 Jan 2024 18:46:23 +0100 Subject: [PATCH 22/63] Fix line too long error --- ariadne_graphql_modules/next/objecttype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 17a7700..62c2722 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -565,7 +565,8 @@ def get_field_args_from_resolver( else: if len(resolver_signature.parameters) < 2: raise TypeError( - f"Resolver function '{resolver_signature}' should accept at least 'obj' and 'info' positional arguments." + f"Resolver function '{resolver_signature}' should accept at least " + "'obj' and 'info' positional arguments." ) field_args_start = 2 From 77be4408131a3290b61b2e6f9fa39e3e369bc062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Tue, 16 Jan 2024 17:44:12 +0100 Subject: [PATCH 23/63] next.make_executable_schema --- .../next/executable_schema.py | 13 +- ariadne_graphql_modules/next/roots.py | 6 - ariadne_graphql_modules/next/typing.py | 4 + .../snap_test_make_executable_schema.py | 4 + tests_next/test_make_executable_schema.py | 137 +++++++++++++++++- 5 files changed, 148 insertions(+), 16 deletions(-) diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index e4d3d7f..6643d6a 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -5,8 +5,9 @@ SchemaBindable, SchemaDirectiveVisitor, SchemaNameConverter, + convert_schema_names, repair_schema_default_enum_values, - validate_schema_default_enum_values + validate_schema_default_enum_values, ) from graphql import ( DocumentNode, @@ -48,7 +49,7 @@ def make_executable_schema( schema_bindables.append(type_def) else: schema_bindables.append(metadata.get_graphql_model(type_def)) - + schema_models: List[GraphQLModel] = [ type_def for type_def in schema_bindables if isinstance(type_def, GraphQLModel) ] @@ -81,6 +82,9 @@ def make_executable_schema( document_node = sort_schema_document(document_node) schema = build_ast_schema(document_node) + if directives: + SchemaDirectiveVisitor.visit_schema_directives(schema, directives) + assert_valid_schema(schema) validate_schema_default_enum_values(schema) repair_schema_default_enum_values(schema) @@ -89,7 +93,10 @@ def make_executable_schema( schema_bindable.bind_to_schema(schema) if convert_names_case: - pass + convert_schema_names( + schema, + convert_names_case if callable(convert_names_case) else None, + ) return schema diff --git a/ariadne_graphql_modules/next/roots.py b/ariadne_graphql_modules/next/roots.py index d1d73ff..017510c 100644 --- a/ariadne_graphql_modules/next/roots.py +++ b/ariadne_graphql_modules/next/roots.py @@ -20,12 +20,6 @@ def merge_root_nodes(document_node: DocumentNode) -> DocumentNode: final_definitions: DefinitionsList = [] for node in document_node.definitions: - if not isinstance(node, TypeDefinitionNode): - raise ValueError( - "Only type definition nodes can be merged. " - f"Found unsupported node: '{node}'" - ) - if node.name.value in roots_definitions: roots_definitions[node.name.value].append(node) else: diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index 3c382fd..6ee7292 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -72,6 +72,10 @@ def get_type_node( type_node = NamedTypeNode( name=NameNode(value=metadata.get_graphql_name(parent_type)), ) + elif isinstance(type_hint, str): + type_node = NamedTypeNode( + name=NameNode(value=type_hint), + ) elif isclass(type_hint): if issubclass(type_hint, GraphQLID): type_node = NamedTypeNode(name=NameNode(value="ID")) diff --git a/tests_next/snapshots/snap_test_make_executable_schema.py b/tests_next/snapshots/snap_test_make_executable_schema.py index 9cfb3c1..df86f38 100644 --- a/tests_next/snapshots/snap_test_make_executable_schema.py +++ b/tests_next/snapshots/snap_test_make_executable_schema.py @@ -7,4 +7,8 @@ snapshots = Snapshot() +snapshots['test_make_executable_schema_raises_error_if_called_without_any_types 1'] = "'make_executable_schema' was called without any GraphQL types." + snapshots['test_multiple_roots_fail_validation_if_merge_roots_is_disabled 1'] = "Types 'SecondRoot' and '.FirstRoot'>' both define GraphQL type with name 'Query'." + +snapshots['test_schema_validation_fails_if_lazy_type_doesnt_exist 1'] = "Unknown type 'Missing'." diff --git a/tests_next/test_make_executable_schema.py b/tests_next/test_make_executable_schema.py index 4374458..51e51e5 100644 --- a/tests_next/test_make_executable_schema.py +++ b/tests_next/test_make_executable_schema.py @@ -1,7 +1,15 @@ -import pytest -from graphql import graphql_sync +from typing import Union -from ariadne import QueryType +import pytest +from graphql import ( + GraphQLField, + GraphQLInterfaceType, + GraphQLObjectType, + default_field_resolver, + graphql_sync, +) + +from ariadne import QueryType, SchemaDirectiveVisitor from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema @@ -15,7 +23,7 @@ def test_executable_schema_from_vanilla_schema_definition(assert_schema_equals): message: String! } """, - query_type + query_type, ) assert_schema_equals( @@ -27,13 +35,15 @@ def test_executable_schema_from_vanilla_schema_definition(assert_schema_equals): """, ) - result = graphql_sync(schema, '{ message }') + result = graphql_sync(schema, "{ message }") assert not result.errors assert result.data == {"message": "Hello world!"} -def test_executable_schema_from_combined_vanilla_and_new_schema_definition(assert_schema_equals): +def test_executable_schema_from_combined_vanilla_and_new_schema_definition( + assert_schema_equals, +): class UserType(GraphQLObject): name: str email: str @@ -67,7 +77,7 @@ class UserType(GraphQLObject): """, ) - result = graphql_sync(schema, '{ user { name email } }') + result = graphql_sync(schema, "{ user { name email } }") assert not result.errors assert result.data == { @@ -163,3 +173,116 @@ class ThirdRoot(GraphQLObject): make_executable_schema(FirstRoot, SecondRoot, ThirdRoot, merge_roots=False) snapshot.assert_match(str(exc_info.value)) + + +def test_schema_validation_fails_if_lazy_type_doesnt_exist(snapshot): + class QueryType(GraphQLObject): + @GraphQLObject.field(type=list["Missing"]) + def other(obj, info): + return None + + with pytest.raises(TypeError) as exc_info: + make_executable_schema(QueryType) + + snapshot.assert_match(str(exc_info.value)) + + +def test_schema_validation_passes_if_lazy_type_exists(): + class QueryType(GraphQLObject): + @GraphQLObject.field(type=list["Exists"]) + def other(obj, info): + return None + + type_def = """ + type Exists { + id: ID! + } + """ + + make_executable_schema(QueryType, type_def) + + +def test_make_executable_schema_raises_error_if_called_without_any_types(snapshot): + with pytest.raises(ValueError) as exc_info: + make_executable_schema(QueryType) + + snapshot.assert_match(str(exc_info.value)) + + +def test_make_executable_schema_raises_error_if_called_without_any_types(snapshot): + with pytest.raises(ValueError) as exc_info: + make_executable_schema(QueryType) + + snapshot.assert_match(str(exc_info.value)) + + +def test_make_executable_schema_doesnt_set_resolvers_if_convert_names_case_is_not_enabled(): + class QueryType(GraphQLObject): + other_field: str + + type_def = """ + type Query { + firstField: String! + } + """ + + schema = make_executable_schema(QueryType, type_def) + result = graphql_sync( + schema, + "{ firstField otherField }", + root_value={"firstField": "first", "other_field": "other"}, + ) + assert result.data == {"firstField": "first", "otherField": "other"} + + +def test_make_executable_schema_sets_resolvers_if_convert_names_case_is_enabled(): + class QueryType(GraphQLObject): + other_field: str + + type_def = """ + type Query { + firstField: String! + } + """ + + schema = make_executable_schema(QueryType, type_def, convert_names_case=True) + result = graphql_sync( + schema, + "{ firstField otherField }", + root_value={"first_field": "first", "other_field": "other"}, + ) + assert result.data == {"firstField": "first", "otherField": "other"} + + +class UpperDirective(SchemaDirectiveVisitor): + def visit_field_definition( + self, + field: GraphQLField, + object_type: Union[GraphQLObjectType, GraphQLInterfaceType], + ) -> GraphQLField: + resolver = field.resolve or default_field_resolver + + def resolve_upper(obj, info, **kwargs): + result = resolver(obj, info, **kwargs) + return result.upper() + + field.resolve = resolve_upper + return field + + +def test_make_executable_schema_supports_vanilla_directives(): + type_def = """ + directive @upper on FIELD_DEFINITION + + type Query { + field: String! @upper + } + """ + + schema = make_executable_schema(type_def, directives={"upper": UpperDirective}) + result = graphql_sync( + schema, + "{ field }", + root_value={"field": "first"}, + ) + assert result.data == {"field": "FIRST"} From d94a4f4af827b0e050b3ac2d41e371eedf1faa38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Fri, 29 Mar 2024 18:46:29 +0100 Subject: [PATCH 24/63] Barebones Union type implementation --- ariadne_graphql_modules/next/__init__.py | 7 +- .../next/executable_schema.py | 20 ++- ariadne_graphql_modules/next/mypy.py | 10 ++ ariadne_graphql_modules/next/scalartype.py | 6 +- ariadne_graphql_modules/next/uniontype.py | 142 ++++++++++++++++++ pyproject.toml | 3 + tests_next/test_object_type.py | 65 ++++++++ tests_next/test_scalar_type.py | 2 +- tests_next/test_typing.py | 6 +- tests_next/test_union_type.py | 84 +++++++++++ 10 files changed, 334 insertions(+), 11 deletions(-) create mode 100644 ariadne_graphql_modules/next/mypy.py create mode 100644 ariadne_graphql_modules/next/uniontype.py create mode 100644 tests_next/test_union_type.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index e8f3b41..69261ee 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -16,8 +16,9 @@ from .inputtype import GraphQLInput, GraphQLInputModel from .objecttype import GraphQLObject, GraphQLObjectModel, object_field from .roots import ROOTS_NAMES, merge_root_nodes -from .scalartype import GraphQLScalar, GraphQScalarModel +from .scalartype import GraphQLScalar, GraphQLScalarModel from .sort import sort_schema_document +from .uniontype import GraphQLUnion, GraphQLUnionModel from .value import get_value_from_node, get_value_node __all__ = [ @@ -31,8 +32,10 @@ "GraphQLObject", "GraphQLObjectModel", "GraphQLScalar", - "GraphQScalarModel", + "GraphQLScalarModel", "GraphQLType", + "GraphQLUnion", + "GraphQLUnionModel", "ROOTS_NAMES", "convert_graphql_name_to_python", "convert_python_name_to_graphql", diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index 6643d6a..fff489b 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -152,6 +152,7 @@ def flatten_schema_types( dedupe: bool, ) -> List[SchemaType]: flat_list: List[SchemaType] = [] + checked_types: List[Type[GraphQLType]] = [] for type_def in types: if isinstance(type_def, str): @@ -161,7 +162,7 @@ def flatten_schema_types( elif isinstance(type_def, SchemaBindable): flat_list.append(type_def) elif issubclass(type_def, GraphQLType): - flat_list += type_def.__get_graphql_types__(metadata) + add_graphql_type_to_flat_list(flat_list, checked_types, type_def, metadata) elif get_graphql_type_name(type_def): flat_list.append(type_def) @@ -176,6 +177,23 @@ def flatten_schema_types( return unique_list +def add_graphql_type_to_flat_list( + flat_list: List[SchemaType], + checked_types: List[Type[GraphQLType]], + type_def: GraphQLType, + metadata: GraphQLMetadata, +) -> List[SchemaType]: + if type_def in checked_types: + return + + checked_types.append(type_def) + + for child_type in type_def.__get_graphql_types__(metadata): + flat_list.append(child_type) + + add_graphql_type_to_flat_list(flat_list, checked_types, child_type, metadata) + + def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: if isinstance(type_def, SchemaBindable): return None diff --git a/ariadne_graphql_modules/next/mypy.py b/ariadne_graphql_modules/next/mypy.py new file mode 100644 index 0000000..0cee595 --- /dev/null +++ b/ariadne_graphql_modules/next/mypy.py @@ -0,0 +1,10 @@ +from mypy.plugin import Plugin + + +class AriadneGraphQLModulesPlugin(Plugin): + def get_type_analyze_hook(self, fullname: str): + print(fullname) + + +def plugin(version: str): + return AriadneGraphQLModulesPlugin diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/scalartype.py index c3e55f2..98327c5 100644 --- a/ariadne_graphql_modules/next/scalartype.py +++ b/ariadne_graphql_modules/next/scalartype.py @@ -55,7 +55,7 @@ def __get_graphql_model_with_schema__( parse_definition(ScalarTypeDefinitionNode, cls.__schema__), ) - return GraphQScalarModel( + return GraphQLScalarModel( name=definition.name.value, ast_type=ScalarTypeDefinitionNode, ast=definition, @@ -68,7 +68,7 @@ def __get_graphql_model_with_schema__( def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLModel": - return GraphQScalarModel( + return GraphQLScalarModel( name=name, ast_type=ScalarTypeDefinitionNode, ast=ScalarTypeDefinitionNode( @@ -104,7 +104,7 @@ def unwrap(self) -> T: @dataclass(frozen=True) -class GraphQScalarModel(GraphQLModel): +class GraphQLScalarModel(GraphQLModel): serialize: Callable[[Any], Any] parse_value: Callable[[Any], Any] parse_literal: Callable[[ValueNode, Optional[Dict[str, Any]]], Any] diff --git a/ariadne_graphql_modules/next/uniontype.py b/ariadne_graphql_modules/next/uniontype.py new file mode 100644 index 0000000..fb4e30d --- /dev/null +++ b/ariadne_graphql_modules/next/uniontype.py @@ -0,0 +1,142 @@ +from dataclasses import dataclass +from typing import ( + Any, + Callable, + Iterable, + List, + NoReturn, + Sequence, + Type, + cast, +) + +from ariadne import UnionType +from graphql import ( + GraphQLSchema, + NameNode, + NamedTypeNode, + UnionTypeDefinitionNode, +) + +from ..utils import parse_definition +from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .description import get_description_node +from .objecttype import GraphQLObject +from .validators import validate_description, validate_name + + +class GraphQLUnion(GraphQLType): + __types__: Sequence[Type[GraphQLType]] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + validate_union_type_with_schema(cls) + else: + validate_union_type(cls) + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": + name = cls.__get_graphql_name__() + metadata.set_graphql_name(cls, name) + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__(metadata, name) + + return cls.__get_graphql_model_without_schema__(metadata, name) + + @classmethod + def __get_graphql_model_with_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": + definition = cast( + UnionTypeDefinitionNode, + parse_definition(UnionTypeDefinitionNode, cls.__schema__), + ) + + return GraphQLUnionModel( + name=definition.name.value, + ast_type=UnionTypeDefinitionNode, + ast=definition, + resolve_type=cls.resolve_type, + ) + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": + return GraphQLUnionModel( + name=name, + ast_type=UnionTypeDefinitionNode, + ast=UnionTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + types=tuple( + NamedTypeNode(name=NameNode(value=t.__get_graphql_name__())) + for t in cls.__types__ + ), + ), + resolve_type=cls.resolve_type, + ) + + @classmethod + def __get_graphql_types__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: + """Returns iterable with GraphQL types associated with this type""" + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_types_with_schema__(metadata) + + return [cls] + cls.__types__ + + @staticmethod + def resolve_type(obj: Any, *args) -> str: + if isinstance(obj, GraphQLObject): + return obj.__get_graphql_name__() + + raise ValueError("TODO") + + +def validate_union_type(cls: Type[GraphQLUnion]) -> NoReturn: + types = getattr(cls, "__types__", None) + if not types: + raise ValueError( + f"Class '{cls.__name__}' is missing a '__types__' attribute " + "with list of types belonging to a union." + ) + + +def validate_union_type_with_schema(cls: Type[GraphQLUnion]) -> NoReturn: + definition = cast( + UnionTypeDefinitionNode, + parse_definition(UnionTypeDefinitionNode, cls.__schema__), + ) + + if not isinstance(definition, UnionTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines a '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{UnionTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + +@dataclass(frozen=True) +class GraphQLUnionModel(GraphQLModel): + resolve_type: Callable[[Any], Any] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = UnionType(self.name, self.resolve_type) + bindable.bind_to_schema(schema) diff --git a/pyproject.toml b/pyproject.toml index 363ba31..99f58df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,3 +77,6 @@ exclude = ''' [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "strict" + +[tool.mypy] +plugins = "ariadne_graphql_modules.next.mypy" \ No newline at end of file diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index f978c07..c7f76d3 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -948,3 +948,68 @@ def category(*_) -> CategoryType: "color": "#FF00FF", }, } + + +def test_object_type_nested_type( + assert_schema_equals, +): + class UserType(GraphQLObject): + username: str + + class CategoryType(GraphQLObject): + name: str + parent: Optional["CategoryType"] + owner: UserType + + class QueryType(GraphQLObject): + category: CategoryType + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + category: Category! + } + + type Category { + name: String! + parent: Category + owner: User! + } + + type User { + username: String! + } + """, + ) + + result = graphql_sync( + schema, + "{ category { name parent { name } owner { username } } }", + root_value={ + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + "owner": { + "username": "John", + }, + }, + }, + ) + + assert not result.errors + assert result.data == { + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + "owner": { + "username": "John", + }, + }, + } diff --git a/tests_next/test_scalar_type.py b/tests_next/test_scalar_type.py index 79f2c77..6ffa961 100644 --- a/tests_next/test_scalar_type.py +++ b/tests_next/test_scalar_type.py @@ -74,7 +74,7 @@ def resolve_date(*_) -> date: class SchemaDateScalar(GraphQLScalar[date]): - __schema__ = "scalar Date" + __schema__ = gql("scalar Date") @classmethod def serialize(cls, value): diff --git a/tests_next/test_typing.py b/tests_next/test_typing.py index c9fcc99..b93c11a 100644 --- a/tests_next/test_typing.py +++ b/tests_next/test_typing.py @@ -42,8 +42,7 @@ def test_get_graphql_type_from_python_builtin_type_returns_none(metadata): def test_get_graphql_type_from_graphql_type_subclass_returns_type(metadata): - class UserType(GraphQLObject): - ... + class UserType(GraphQLObject): ... assert get_graphql_type(UserType) == UserType assert get_graphql_type(Optional[UserType]) == UserType @@ -79,8 +78,7 @@ def test_get_non_null_graphql_type_node_from_python_builtin_type(metadata): def test_get_graphql_type_node_from_graphql_type(metadata): - class UserType(GraphQLObject): - ... + class UserType(GraphQLObject): ... assert_non_null_type(get_type_node(metadata, UserType), "User") assert_named_type(get_type_node(metadata, Optional[UserType]), "User") diff --git a/tests_next/test_union_type.py b/tests_next/test_union_type.py new file mode 100644 index 0000000..6ca0010 --- /dev/null +++ b/tests_next/test_union_type.py @@ -0,0 +1,84 @@ +from datetime import date +from typing import List + +from graphql import graphql_sync + +from ariadne_graphql_modules import gql +from ariadne_graphql_modules.next import ( + GraphQLID, + GraphQLObject, + GraphQLUnion, + make_executable_schema, +) + + +class UserType(GraphQLObject): + id: GraphQLID + username: str + + +class CommentType(GraphQLObject): + id: GraphQLID + content: str + + +def test_union_field_returning_object_instance(assert_schema_equals): + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=List[ResultType]) + def search(*_) -> List[UserType | CommentType]: + return [ + UserType(id=1, username="Bob"), + CommentType(id=2, content="Hello World!"), + ] + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } + + union Result = User | Comment + + type User { + id: ID! + username: String! + } + + type Comment { + id: ID! + content: String! + } + """, + ) + + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } + } + } + """, + ) + + assert not result.errors + assert result.data == { + "search": [ + {"id": "1", "username": "Bob"}, + {"id": "2", "content": "Hello World!"}, + ] + } From b71762b1d4db586d81e34c998bef29aefe4447f2 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Tue, 9 Apr 2024 08:43:29 +0200 Subject: [PATCH 25/63] added-uniont-type-validation --- ariadne_graphql_modules/next/uniontype.py | 24 ++- .../snap_test_union_type_validation.py | 12 ++ tests_next/test_union_type.py | 143 +++++++++++++++++- tests_next/test_union_type_validation.py | 56 +++++++ 4 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 tests_next/snapshots/snap_test_union_type_validation.py create mode 100644 tests_next/test_union_type_validation.py diff --git a/ariadne_graphql_modules/next/uniontype.py b/ariadne_graphql_modules/next/uniontype.py index fb4e30d..68ba60f 100644 --- a/ariadne_graphql_modules/next/uniontype.py +++ b/ariadne_graphql_modules/next/uniontype.py @@ -3,7 +3,6 @@ Any, Callable, Iterable, - List, NoReturn, Sequence, Type, @@ -92,10 +91,6 @@ def __get_graphql_types__( cls, metadata: "GraphQLMetadata" ) -> Iterable["GraphQLType"]: """Returns iterable with GraphQL types associated with this type""" - - if getattr(cls, "__schema__", None): - return cls.__get_graphql_types_with_schema__(metadata) - return [cls] + cls.__types__ @staticmethod @@ -103,7 +98,9 @@ def resolve_type(obj: Any, *args) -> str: if isinstance(obj, GraphQLObject): return obj.__get_graphql_name__() - raise ValueError("TODO") + raise ValueError( + f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." + ) def validate_union_type(cls: Type[GraphQLUnion]) -> NoReturn: @@ -132,6 +129,21 @@ def validate_union_type_with_schema(cls: Type[GraphQLUnion]) -> NoReturn: validate_name(cls, definition) validate_description(cls, definition) + schema_type_names = {type_node.name.value for type_node in definition.types} + + class_type_names = {t.__get_graphql_name__() for t in cls.__types__} + if not class_type_names.issubset(schema_type_names): + missing_in_schema = class_type_names - schema_type_names + raise ValueError( + f"Types {missing_in_schema} are defined in __types__ but not present in the __schema__." + ) + + if not schema_type_names.issubset(class_type_names): + missing_in_types = schema_type_names - class_type_names + raise ValueError( + f"Types {missing_in_types} are present in the __schema__ but not defined in __types__." + ) + @dataclass(frozen=True) class GraphQLUnionModel(GraphQLModel): diff --git a/tests_next/snapshots/snap_test_union_type_validation.py b/tests_next/snapshots/snap_test_union_type_validation.py new file mode 100644 index 0000000..dafa0ca --- /dev/null +++ b/tests_next/snapshots/snap_test_union_type_validation.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_missing_type_in_schema 1'] = "Types {'Comment'} are defined in __types__ but not present in the __schema__." + +snapshots['test_missing_type_in_types 1'] = "Types {'Comment'} are present in the __schema__ but not defined in __types__." diff --git a/tests_next/test_union_type.py b/tests_next/test_union_type.py index 6ca0010..93b42f3 100644 --- a/tests_next/test_union_type.py +++ b/tests_next/test_union_type.py @@ -1,9 +1,7 @@ -from datetime import date from typing import List from graphql import graphql_sync -from ariadne_graphql_modules import gql from ariadne_graphql_modules.next import ( GraphQLID, GraphQLObject, @@ -82,3 +80,144 @@ def search(*_) -> List[UserType | CommentType]: {"id": "2", "content": "Hello World!"}, ] } + + +def test_union_field_returning_empty_list(): + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=List[ResultType]) + def search(*_) -> List[UserType | CommentType]: + return [] + + schema = make_executable_schema(QueryType) + + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } + } + } + """, + ) + assert not result.errors + assert result.data == {"search": []} + + +def test_union_field_with_invalid_type_access(assert_schema_equals): + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=List[ResultType]) + def search(*_) -> List[UserType | CommentType]: + return [ + UserType(id=1, username="Bob"), + "InvalidType", + ] + + schema = make_executable_schema(QueryType) + + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } + } + } + """, + ) + assert result.errors + assert "InvalidType" in str(result.errors) + + +def test_serialization_error_handling(assert_schema_equals): + class InvalidType: + def __init__(self, value): + self.value = value + + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=List[ResultType]) + def search(*_) -> List[UserType | CommentType | InvalidType]: + return [InvalidType("This should cause an error")] + + schema = make_executable_schema(QueryType) + + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + } + } + """, + ) + assert result.errors + + +def test_union_with_schema_definition(assert_schema_equals): + class SearchResultUnion(GraphQLUnion): + __schema__ = """ + union SearchResult = User | Comment + """ + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=List[SearchResultUnion]) + def search(*_) -> List[UserType | CommentType]: + return [ + UserType(id="1", username="Alice"), + CommentType(id="2", content="Test post"), + ] + + schema = make_executable_schema([QueryType, SearchResultUnion]) + + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } + } + } + """, + ) + assert not result.errors + assert result.data == { + "search": [ + {"id": "1", "username": "Alice"}, + {"id": "2", "content": "Test post"}, + ] + } diff --git a/tests_next/test_union_type_validation.py b/tests_next/test_union_type_validation.py new file mode 100644 index 0000000..326b141 --- /dev/null +++ b/tests_next/test_union_type_validation.py @@ -0,0 +1,56 @@ +from ariadne_graphql_modules.next import ( + GraphQLID, + GraphQLObject, + GraphQLUnion, +) +from ariadne_graphql_modules.next.uniontype import validate_union_type_with_schema +import pytest + + +class UserType(GraphQLObject): + id: GraphQLID + username: str + + +class CommentType(GraphQLObject): + id: GraphQLID + content: str + + +def test_missing_type_in_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class MyUnion(GraphQLUnion): + __types__ = [UserType, CommentType] + __schema__ = """ + union MyUnion = User + """ + + validate_union_type_with_schema(MyUnion) + snapshot.assert_match(str(exc_info.value)) + + +def test_missing_type_in_types(snapshot): + with pytest.raises(ValueError) as exc_info: + + class MyUnion(GraphQLUnion): + __types__ = [UserType] + __schema__ = """ + union MyUnion = User | Comment + """ + + validate_union_type_with_schema(MyUnion) + snapshot.assert_match(str(exc_info.value)) + + +def test_all_types_present(): + class MyUnion(GraphQLUnion): + __types__ = [UserType, CommentType] + __schema__ = """ + union MyUnion = User | Comment + """ + + try: + validate_union_type_with_schema(MyUnion) + except ValueError as e: + pytest.fail(f"Unexpected ValueError raised: {e}") From 51d822538a39ac00b4cabc23edd3a7da32959530 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Tue, 9 Apr 2024 15:54:04 +0200 Subject: [PATCH 26/63] wrap-attributes-names-in-__attrname__-in-error-messages --- ariadne_graphql_modules/next/uniontype.py | 6 ++++-- tests_next/snapshots/snap_test_union_type_validation.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ariadne_graphql_modules/next/uniontype.py b/ariadne_graphql_modules/next/uniontype.py index 68ba60f..786beee 100644 --- a/ariadne_graphql_modules/next/uniontype.py +++ b/ariadne_graphql_modules/next/uniontype.py @@ -134,14 +134,16 @@ def validate_union_type_with_schema(cls: Type[GraphQLUnion]) -> NoReturn: class_type_names = {t.__get_graphql_name__() for t in cls.__types__} if not class_type_names.issubset(schema_type_names): missing_in_schema = class_type_names - schema_type_names + missing_in_schema_str = ", ".join(f"__{name}__" for name in missing_in_schema) raise ValueError( - f"Types {missing_in_schema} are defined in __types__ but not present in the __schema__." + f"Types {missing_in_schema_str} are defined in __types__ but not present in the __schema__." ) if not schema_type_names.issubset(class_type_names): missing_in_types = schema_type_names - class_type_names + missing_in_types_str = ", ".join(f"__{name}__" for name in missing_in_types) raise ValueError( - f"Types {missing_in_types} are present in the __schema__ but not defined in __types__." + f"Types {missing_in_types_str} are present in the __schema__ but not defined in __types__." ) diff --git a/tests_next/snapshots/snap_test_union_type_validation.py b/tests_next/snapshots/snap_test_union_type_validation.py index dafa0ca..2562f3b 100644 --- a/tests_next/snapshots/snap_test_union_type_validation.py +++ b/tests_next/snapshots/snap_test_union_type_validation.py @@ -7,6 +7,6 @@ snapshots = Snapshot() -snapshots['test_missing_type_in_schema 1'] = "Types {'Comment'} are defined in __types__ but not present in the __schema__." +snapshots['test_missing_type_in_schema 1'] = "Types __Comment__ are defined in __types__ but not present in the __schema__." -snapshots['test_missing_type_in_types 1'] = "Types {'Comment'} are present in the __schema__ but not defined in __types__." +snapshots['test_missing_type_in_types 1'] = "Types __Comment__ are present in the __schema__ but not defined in __types__." From 2fc3948c74375857f710860a5e5f5b806f34618c Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Tue, 9 Apr 2024 16:24:53 +0200 Subject: [PATCH 27/63] updated-error-message --- ariadne_graphql_modules/next/uniontype.py | 12 ++++++------ .../snapshots/snap_test_union_type_validation.py | 4 ++-- tests_next/test_union_type_validation.py | 7 ++++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/ariadne_graphql_modules/next/uniontype.py b/ariadne_graphql_modules/next/uniontype.py index 786beee..2546eb2 100644 --- a/ariadne_graphql_modules/next/uniontype.py +++ b/ariadne_graphql_modules/next/uniontype.py @@ -133,17 +133,17 @@ def validate_union_type_with_schema(cls: Type[GraphQLUnion]) -> NoReturn: class_type_names = {t.__get_graphql_name__() for t in cls.__types__} if not class_type_names.issubset(schema_type_names): - missing_in_schema = class_type_names - schema_type_names - missing_in_schema_str = ", ".join(f"__{name}__" for name in missing_in_schema) + missing_in_schema = sorted(class_type_names - schema_type_names) + missing_in_schema_str = "', '".join(missing_in_schema) raise ValueError( - f"Types {missing_in_schema_str} are defined in __types__ but not present in the __schema__." + f"Types '{missing_in_schema_str}' are in '__types__' but not in '__schema__'." ) if not schema_type_names.issubset(class_type_names): - missing_in_types = schema_type_names - class_type_names - missing_in_types_str = ", ".join(f"__{name}__" for name in missing_in_types) + missing_in_types = sorted(schema_type_names - class_type_names) + missing_in_types_str = "', '".join(missing_in_types) raise ValueError( - f"Types {missing_in_types_str} are present in the __schema__ but not defined in __types__." + f"Types '{missing_in_types_str}' are in '__schema__' but not in '__types__'." ) diff --git a/tests_next/snapshots/snap_test_union_type_validation.py b/tests_next/snapshots/snap_test_union_type_validation.py index 2562f3b..4abc204 100644 --- a/tests_next/snapshots/snap_test_union_type_validation.py +++ b/tests_next/snapshots/snap_test_union_type_validation.py @@ -7,6 +7,6 @@ snapshots = Snapshot() -snapshots['test_missing_type_in_schema 1'] = "Types __Comment__ are defined in __types__ but not present in the __schema__." +snapshots['test_missing_type_in_schema 1'] = "Types 'Comment', 'Post' are in '__types__' but not in '__schema__'." -snapshots['test_missing_type_in_types 1'] = "Types __Comment__ are present in the __schema__ but not defined in __types__." +snapshots['test_missing_type_in_types 1'] = "Types 'Comment' are in '__schema__' but not in '__types__'." diff --git a/tests_next/test_union_type_validation.py b/tests_next/test_union_type_validation.py index 326b141..b154d70 100644 --- a/tests_next/test_union_type_validation.py +++ b/tests_next/test_union_type_validation.py @@ -17,11 +17,16 @@ class CommentType(GraphQLObject): content: str +class PostType(GraphQLObject): + id: GraphQLID + content: str + + def test_missing_type_in_schema(snapshot): with pytest.raises(ValueError) as exc_info: class MyUnion(GraphQLUnion): - __types__ = [UserType, CommentType] + __types__ = [UserType, CommentType, PostType] __schema__ = """ union MyUnion = User """ From 6b65316b5c54da1520f22f5953c040332273186d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Tue, 23 Apr 2024 18:25:17 +0200 Subject: [PATCH 28/63] Fix tests suite --- ariadne_graphql_modules/next/executable_schema.py | 7 ++++++- ariadne_graphql_modules/next/inputtype.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index fff489b..660670d 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -161,6 +161,8 @@ def flatten_schema_types( flat_list += flatten_schema_types(type_def, metadata, dedupe=False) elif isinstance(type_def, SchemaBindable): flat_list.append(type_def) + elif issubclass(type_def, Enum): + flat_list.append(type_def) elif issubclass(type_def, GraphQLType): add_graphql_type_to_flat_list(flat_list, checked_types, type_def, metadata) elif get_graphql_type_name(type_def): @@ -191,7 +193,10 @@ def add_graphql_type_to_flat_list( for child_type in type_def.__get_graphql_types__(metadata): flat_list.append(child_type) - add_graphql_type_to_flat_list(flat_list, checked_types, child_type, metadata) + if issubclass(child_type, GraphQLType): + add_graphql_type_to_flat_list( + flat_list, checked_types, child_type, metadata + ) def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py index dd4178c..3cd5a8d 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/inputtype.py @@ -197,7 +197,7 @@ def __get_graphql_types__( types.append(field_graphql_type) type_hints = cls.__annotations__ - for hint_name, hint_type in type_hints.values(): + for hint_name, hint_type in type_hints.items(): if hint_name.startswith("__"): continue From 826bbc54faeffbcd6c73af6c17519535fcbdad5e Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 15 Apr 2024 14:08:39 +0200 Subject: [PATCH 29/63] Base implementation of interface with standarization methods from object type --- ariadne_graphql_modules/next/__init__.py | 3 + ariadne_graphql_modules/next/field.py | 50 +++ ariadne_graphql_modules/next/interfacetype.py | 215 ++++++++++ ariadne_graphql_modules/next/metadata.py | 123 ++++++ ariadne_graphql_modules/next/objectmixin.py | 196 +++++++++ ariadne_graphql_modules/next/objecttype.py | 372 ++---------------- ariadne_graphql_modules/next/resolver.py | 94 +++++ .../test_get_field_args_from_resolver.py | 2 +- tests_next/test_interface_type.py | 132 +++++++ 9 files changed, 853 insertions(+), 334 deletions(-) create mode 100644 ariadne_graphql_modules/next/field.py create mode 100644 ariadne_graphql_modules/next/interfacetype.py create mode 100644 ariadne_graphql_modules/next/metadata.py create mode 100644 ariadne_graphql_modules/next/objectmixin.py create mode 100644 ariadne_graphql_modules/next/resolver.py create mode 100644 tests_next/test_interface_type.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 69261ee..0e4c512 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -20,6 +20,7 @@ from .sort import sort_schema_document from .uniontype import GraphQLUnion, GraphQLUnionModel from .value import get_value_from_node, get_value_node +from .interfacetype import GraphQLInterface, GraphQLInterfaceModel __all__ = [ "GraphQLEnum", @@ -27,6 +28,8 @@ "GraphQLID", "GraphQLInput", "GraphQLInputModel", + "GraphQLInterface", + "GraphQLInterfaceModel", "GraphQLMetadata", "GraphQLModel", "GraphQLObject", diff --git a/ariadne_graphql_modules/next/field.py b/ariadne_graphql_modules/next/field.py new file mode 100644 index 0000000..01ae10d --- /dev/null +++ b/ariadne_graphql_modules/next/field.py @@ -0,0 +1,50 @@ +from dataclasses import dataclass +from typing import Any, Dict, Optional + +from ariadne.types import Resolver + + +@dataclass +class GraphQLObjectFieldArg: + name: str + out_name: str + type: Any + description: Optional[str] = None + default_value: Optional[Any] = None + + +class GraphQLObjectField: + name: Optional[str] + description: Optional[str] + type: Optional[Any] + args: Optional[Dict[str, dict]] + resolver: Optional[Resolver] + default_value: Optional[Any] + + def __init__( + self, + *, + name: Optional[str] = None, + description: Optional[str] = None, + type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, + resolver: Optional[Resolver] = None, + default_value: Optional[Any] = None, + ): + self.name = name + self.description = description + self.type = type + self.args = args + self.resolver = resolver + self.default_value = default_value + + def __call__(self, resolver: Resolver): + """Makes GraphQLObjectField instances work as decorators.""" + self.resolver = resolver + if not self.type: + self.type = get_field_type_from_resolver(resolver) + return self + + +def get_field_type_from_resolver(resolver: Resolver) -> Any: + return resolver.__annotations__.get("return") diff --git a/ariadne_graphql_modules/next/interfacetype.py b/ariadne_graphql_modules/next/interfacetype.py new file mode 100644 index 0000000..3f97f28 --- /dev/null +++ b/ariadne_graphql_modules/next/interfacetype.py @@ -0,0 +1,215 @@ +from dataclasses import dataclass +from enum import Enum +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Type, + Union, + cast, + Sequence, + NoReturn, +) + +from ariadne import InterfaceType +from ariadne.types import Resolver +from graphql import ( + FieldDefinitionNode, + GraphQLField, + GraphQLObjectType, + GraphQLSchema, + NameNode, + NamedTypeNode, + InterfaceTypeDefinitionNode, +) + +from .metadata import get_graphql_object_data +from .objectmixin import GraphQLModelHelpersMixin + +from ..utils import parse_definition +from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .description import get_description_node +from .typing import get_graphql_type +from .validators import validate_description, validate_name + + +class GraphQLInterface(GraphQLType, GraphQLModelHelpersMixin): + __types__: Sequence[Type[GraphQLType]] + __implements__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + validate_interface_type_with_schema(cls) + else: + validate_interface_type(cls) + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": + name = cls.__get_graphql_name__() + metadata.set_graphql_name(cls, name) + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__(metadata, name) + + return cls.__get_graphql_model_without_schema__(metadata, name) + + @classmethod + def __get_graphql_model_with_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLInterfaceModel": + definition = cast( + InterfaceTypeDefinitionNode, + parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), + ) + resolvers: Dict[str, Resolver] = cls.collect_resolvers_with_schema() + out_names: Dict[str, Dict[str, str]] = {} + fields: List[FieldDefinitionNode] = cls.gather_fields_with_schema(definition) + + return GraphQLInterfaceModel( + name=definition.name.value, + ast_type=InterfaceTypeDefinitionNode, + ast=InterfaceTypeDefinitionNode( + name=NameNode(value=definition.name.value), + fields=tuple(fields), + interfaces=definition.interfaces, + ), + resolve_type=cls.resolve_type, + resolvers=resolvers, + aliases=getattr(cls, "__aliases__", {}), + out_names=out_names, + ) + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLInterfaceModel": + type_data = get_graphql_object_data(metadata, cls) + + fields_ast: List[FieldDefinitionNode] = cls.gather_fields_without_schema( + metadata + ) + interfaces_ast: List[NamedTypeNode] = cls.gather_interfaces_without_schema( + type_data + ) + resolvers: Dict[str, Resolver] = cls.collect_resolvers_without_schema(type_data) + aliases: Dict[str, str] = cls.collect_aliases(type_data) + out_names: Dict[str, Dict[str, str]] = cls.collect_out_names(type_data) + + return GraphQLInterfaceModel( + name=name, + ast_type=InterfaceTypeDefinitionNode, + ast=InterfaceTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + fields=tuple(fields_ast), + interfaces=tuple(interfaces_ast), + ), + resolve_type=cls.resolve_type, + resolvers=resolvers, + aliases=aliases, + out_names=out_names, + ) + + @classmethod + def __get_graphql_types__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: + """Returns iterable with GraphQL types associated with this type""" + if getattr(cls, "__schema__", None): + return cls.__get_graphql_types_with_schema__(metadata) + + return cls.__get_graphql_types_without_schema__(metadata) + + @classmethod + def __get_graphql_types_with_schema__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: + types: List[GraphQLType] = [cls] + types.extend(getattr(cls, "__requires__", [])) + return types + + @classmethod + def __get_graphql_types_without_schema__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable["GraphQLType"]: + types: List[GraphQLType] = [cls] + type_data = get_graphql_object_data(metadata, cls) + + for field in type_data.fields.values(): + field_type = get_graphql_type(field.type) + if field_type and field_type not in types: + types.append(field_type) + + if field.args: + for field_arg in field.args.values(): + field_arg_type = get_graphql_type(field_arg.type) + if field_arg_type and field_arg_type not in types: + types.append(field_arg_type) + + return types + + @staticmethod + def resolve_type(obj: Any, *args) -> str: + if isinstance(obj, GraphQLInterface): + return obj.__get_graphql_name__() + + raise ValueError( + f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." + ) + + +def validate_interface_type(cls: Type[GraphQLInterface]) -> NoReturn: + pass + + +def validate_interface_type_with_schema(cls: Type[GraphQLInterface]) -> NoReturn: + definition = cast( + InterfaceTypeDefinitionNode, + parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), + ) + + if not isinstance(definition, InterfaceTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines a '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{InterfaceTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + +@dataclass(frozen=True) +class GraphQLInterfaceModel(GraphQLModel): + resolvers: Dict[str, Resolver] + resolve_type: Callable[[Any], Any] + out_names: Dict[str, Dict[str, str]] + aliases: Dict[str, str] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = InterfaceType(self.name, self.resolve_type) + for field, resolver in self.resolvers.items(): + bindable.set_field(field, resolver) + for alias, target in self.aliases.items(): + bindable.set_alias(alias, target) + + bindable.bind_to_schema(schema) + + graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) + for field_name, field_out_names in self.out_names.items(): + graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) + for arg_name, out_name in field_out_names.items(): + graphql_field.args[arg_name].out_name = out_name diff --git a/ariadne_graphql_modules/next/metadata.py b/ariadne_graphql_modules/next/metadata.py new file mode 100644 index 0000000..a507852 --- /dev/null +++ b/ariadne_graphql_modules/next/metadata.py @@ -0,0 +1,123 @@ +from dataclasses import dataclass +from typing import Any, Dict, List + +from .base import GraphQLMetadata +from .convert_name import convert_python_name_to_graphql +from .field import GraphQLObjectField, GraphQLObjectFieldArg +from .resolver import ( + GraphQLObjectResolver, + get_field_args_from_resolver, + update_field_args_options, +) +from ariadne.types import Resolver + + +@dataclass(frozen=True) +class GraphQLObjectData: + fields: Dict[str, GraphQLObjectField] + interfaces: List[str] + + +def get_graphql_object_data(metadata: GraphQLMetadata, cls): + try: + return metadata.get_data(cls) + except KeyError: + if getattr(cls, "__schema__", None): + raise NotImplementedError( + "'get_graphql_object_data' is not supported for objects with '__schema__'." + ) + else: + return create_graphql_object_data_without_schema(cls) + + +def create_graphql_object_data_without_schema( + cls, +) -> GraphQLObjectData: + fields_types: Dict[str, str] = {} + fields_names: Dict[str, str] = {} + fields_descriptions: Dict[str, str] = {} + fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} + fields_resolvers: Dict[str, Resolver] = {} + fields_defaults: Dict[str, Any] = {} + fields_order: List[str] = [] + + type_hints = cls.__annotations__ + + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + aliases_targets: List[str] = list(aliases.values()) + + interfaces: List[str] = [ + interface.__name__ for interface in getattr(cls, "__implements__", []) + ] + + for attr_name, attr_type in type_hints.items(): + if attr_name.startswith("__"): + continue + + if attr_name in aliases_targets: + # Alias target is not included in schema + # unless its explicit field + cls_attr = getattr(cls, attr_name, None) + if not isinstance(cls_attr, GraphQLObjectField): + continue + + fields_order.append(attr_name) + + fields_names[attr_name] = convert_python_name_to_graphql(attr_name) + fields_types[attr_name] = attr_type + + for attr_name in dir(cls): + if attr_name.startswith("__"): + continue + + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if attr_name not in fields_order: + fields_order.append(attr_name) + + fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( + attr_name + ) + + if cls_attr.type and attr_name not in fields_types: + fields_types[attr_name] = cls_attr.type + if cls_attr.description: + fields_descriptions[attr_name] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[attr_name] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[attr_name] = update_field_args_options( + field_args, cls_attr.args + ) + if cls_attr.default_value: + fields_defaults[attr_name] = cls_attr.default_value + + elif isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.type + if cls_attr.description: + fields_descriptions[cls_attr.field] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[cls_attr.field] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + + elif attr_name not in aliases_targets and not callable(cls_attr): + fields_defaults[attr_name] = cls_attr + + fields: Dict[str, "GraphQLObjectField"] = {} + for field_name in fields_order: + fields[field_name] = GraphQLObjectField( + name=fields_names[field_name], + description=fields_descriptions.get(field_name), + type=fields_types[field_name], + args=fields_args.get(field_name), + resolver=fields_resolvers.get(field_name), + default_value=fields_defaults.get(field_name), + ) + + return GraphQLObjectData(fields=fields, interfaces=interfaces) diff --git a/ariadne_graphql_modules/next/objectmixin.py b/ariadne_graphql_modules/next/objectmixin.py new file mode 100644 index 0000000..b0b650c --- /dev/null +++ b/ariadne_graphql_modules/next/objectmixin.py @@ -0,0 +1,196 @@ +from typing import Dict, List, Optional +from graphql import ( + FieldDefinitionNode, + InputValueDefinitionNode, + NameNode, + NamedTypeNode, +) + +from .base import GraphQLMetadata +from .description import get_description_node +from .field import GraphQLObjectField, GraphQLObjectFieldArg +from .metadata import ( + GraphQLObjectData, + get_graphql_object_data, +) +from .resolver import ( + GraphQLObjectResolver, + get_field_args_from_resolver, + update_field_args_options, +) +from .typing import get_type_node +from .value import get_value_node + + +class GraphQLModelHelpersMixin: + @classmethod + def gather_fields_without_schema(cls, metadata: GraphQLMetadata): + type_data = get_graphql_object_data(metadata, cls) + fields_ast = [ + cls.create_field_node(metadata, field) + for field in type_data.fields.values() + ] + return fields_ast + + @staticmethod + def collect_resolvers_without_schema(type_data: GraphQLObjectData): + return { + field.name: field.resolver + for field in type_data.fields.values() + if field.resolver + } + + @staticmethod + def collect_aliases(type_data: GraphQLObjectData): + aliases = {} + for field in type_data.fields.values(): + attr_name = field.name # Placeholder for actual attribute name logic + if attr_name != field.name: + aliases[field.name] = attr_name + return aliases + + @staticmethod + def collect_out_names(type_data: GraphQLObjectData): + out_names = {} + for field in type_data.fields.values(): + if field.args: + out_names[field.name] = { + arg.name: arg.out_name for arg in field.args.values() + } + return out_names + + @classmethod + def create_field_node(cls, metadata: GraphQLMetadata, field: GraphQLObjectField): + args_nodes = cls.get_field_args_nodes_from_obj_field_args(metadata, field.args) + return FieldDefinitionNode( + description=get_description_node(field.description), + name=NameNode(value=field.name), + type=get_type_node(metadata, field.type), + arguments=tuple(args_nodes) if args_nodes else None, + ) + + @staticmethod + def get_field_args_nodes_from_obj_field_args( + metadata: GraphQLMetadata, + field_args: Optional[Dict[str, GraphQLObjectFieldArg]], + ): + if not field_args: + return [] + + return [ + InputValueDefinitionNode( + description=get_description_node(arg.description), + name=NameNode(value=arg.name), + type=get_type_node(metadata, arg.type), + default_value=get_value_node(arg.default_value) + if arg.default_value is not None + else None, + ) + for arg in field_args.values() + ] + + @classmethod + def gather_interfaces_without_schema(cls, type_data: GraphQLObjectData): + interfaces_ast: List[NamedTypeNode] = [] + for interface_name in type_data.interfaces: + interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) + return interfaces_ast + + @classmethod + def gather_interfaces_with_schema(cls): + return [interface for interface in getattr(cls, "__implements__", [])] + + @classmethod + def gather_fields_with_schema(cls, definition): + descriptions, args_descriptions, args_defaults = ( + cls.collect_descriptions_and_defaults() + ) + + fields = [ + cls.create_field_definition_node( + field, descriptions, args_descriptions, args_defaults + ) + for field in definition.fields + ] + return fields + + @classmethod + def collect_resolvers_with_schema(cls): + resolvers = {} + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers[cls_attr.field] = cls_attr.resolver + return resolvers + + @classmethod + def collect_descriptions_and_defaults(cls): + descriptions = {} + args_descriptions = {} + args_defaults = {} + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.description: + descriptions[cls_attr.field] = get_description_node( + cls_attr.description + ) + + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + args_descriptions[cls_attr.field], args_defaults[cls_attr.field] = ( + cls.process_field_args(field_args, cls_attr.args) + ) + + return descriptions, args_descriptions, args_defaults + + @classmethod + def process_field_args(cls, field_args, resolver_args): + descriptions = {} + defaults = {} + final_args = update_field_args_options(field_args, resolver_args) + + for arg_name, arg_options in final_args.items(): + descriptions[arg_name] = ( + get_description_node(arg_options.description) + if arg_options.description + else None + ) + defaults[arg_name] = ( + get_value_node(arg_options.default_value) + if arg_options.default_value is not None + else None + ) + + return descriptions, defaults + + @classmethod + def create_field_definition_node( + cls, field, descriptions, args_descriptions, args_defaults + ): + field_name = field.name.value + field_args_descriptions = args_descriptions.get(field_name, {}) + field_args_defaults = args_defaults.get(field_name, {}) + + args = [ + InputValueDefinitionNode( + description=( + arg.description or field_args_descriptions.get(arg.name.value) + ), + name=arg.name, + directives=arg.directives, + type=arg.type, + default_value=( + arg.default_value or field_args_defaults.get(arg.name.value) + ), + ) + for arg in field.arguments + ] + + return FieldDefinitionNode( + name=field.name, + description=(field.description or descriptions.get(field_name)), + directives=field.directives, + arguments=tuple(args), + type=field.type, + ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 62c2722..88508c5 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,7 +1,6 @@ from copy import deepcopy -from dataclasses import dataclass, replace +from dataclasses import dataclass from enum import Enum -from inspect import signature from typing import ( Any, Dict, @@ -24,6 +23,21 @@ InputValueDefinitionNode, NameNode, ObjectTypeDefinitionNode, + NamedTypeNode, +) + +from .field import ( + GraphQLObjectField, + GraphQLObjectFieldArg, + get_field_type_from_resolver, +) +from .metadata import ( + get_graphql_object_data, +) +from .objectmixin import GraphQLModelHelpersMixin +from .resolver import ( + GraphQLObjectResolver, + get_field_args_from_resolver, ) from ..utils import parse_definition @@ -35,13 +49,14 @@ from .value import get_value_node -class GraphQLObject(GraphQLType): +class GraphQLObject(GraphQLType, GraphQLModelHelpersMixin): __kwargs__: Dict[str, Any] __abstract__: bool = True __schema__: Optional[str] __description__: Optional[str] __aliases__: Optional[Dict[str, str]] __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] + __implements__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] def __init__(self, **kwargs: Any): for kwarg in kwargs: @@ -63,6 +78,7 @@ def __init_subclass__(cls) -> None: return cls.__abstract__ = False + # cls.__validate_interfaces__() if cls.__dict__.get("__schema__"): cls.__kwargs__ = validate_object_type_with_schema(cls) @@ -88,74 +104,9 @@ def __get_graphql_model_with_schema__( parse_definition(ObjectTypeDefinitionNode, cls.__schema__), ) - descriptions: Dict[str, str] = {} - args_descriptions: Dict[str, Dict[str, str]] = {} - args_defaults: Dict[str, Dict[str, Any]] = {} - resolvers: Dict[str, Resolver] = {} + resolvers: Dict[str, Resolver] = cls.collect_resolvers_with_schema() out_names: Dict[str, Dict[str, str]] = {} - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - resolvers[cls_attr.field] = cls_attr.resolver - if cls_attr.description: - descriptions[cls_attr.field] = get_description_node( - cls_attr.description - ) - - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - args_descriptions[cls_attr.field] = {} - args_defaults[cls_attr.field] = {} - - final_args = update_field_args_options(field_args, cls_attr.args) - - for arg_name, arg_options in final_args.items(): - arg_description = get_description_node(arg_options.description) - if arg_description: - args_descriptions[cls_attr.field][ - arg_name - ] = arg_description - - arg_default = arg_options.default_value - if arg_default is not None: - args_defaults[cls_attr.field][arg_name] = get_value_node( - arg_default - ) - - fields: List[FieldDefinitionNode] = [] - for field in definition.fields: - field_args_descriptions = args_descriptions.get(field.name.value, {}) - field_args_defaults = args_defaults.get(field.name.value, {}) - - args: List[InputValueDefinitionNode] = [] - for arg in field.arguments: - arg_name = arg.name.value - args.append( - InputValueDefinitionNode( - description=( - arg.description or field_args_descriptions.get(arg_name) - ), - name=arg.name, - directives=arg.directives, - type=arg.type, - default_value=( - arg.default_value or field_args_defaults.get(arg_name) - ), - ) - ) - - fields.append( - FieldDefinitionNode( - name=field.name, - description=( - field.description or descriptions.get(field.name.value) - ), - directives=field.directives, - arguments=tuple(args), - type=field.type, - ) - ) + fields: List[FieldDefinitionNode] = cls.gather_fields_with_schema(definition) return GraphQLObjectModel( name=definition.name.value, @@ -163,6 +114,7 @@ def __get_graphql_model_with_schema__( ast=ObjectTypeDefinitionNode( name=NameNode(value=definition.name.value), fields=tuple(fields), + interfaces=definition.interfaces, ), resolvers=resolvers, aliases=getattr(cls, "__aliases__", {}), @@ -174,26 +126,16 @@ def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLObjectModel": type_data = get_graphql_object_data(metadata, cls) - type_aliases = getattr(cls, "__aliases__", None) or {} - fields_ast: List[FieldDefinitionNode] = [] - resolvers: Dict[str, Resolver] = {} - aliases: Dict[str, str] = {} - out_names: Dict[str, Dict[str, str]] = {} - - for attr_name, field in type_data.fields.items(): - fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) - - if attr_name in type_aliases: - aliases[field.name] = type_aliases[attr_name] - elif attr_name != field.name: - aliases[field.name] = attr_name - - if field.resolver: - resolvers[field.name] = field.resolver - - if field.args: - out_names[field.name] = get_field_args_out_names(field.args) + fields_ast: List[FieldDefinitionNode] = cls.gather_fields_without_schema( + metadata + ) + interfaces_ast: List[NamedTypeNode] = cls.gather_interfaces_without_schema( + type_data + ) + resolvers: Dict[str, Resolver] = cls.collect_resolvers_without_schema(type_data) + aliases: Dict[str, str] = cls.collect_aliases(type_data) + out_names: Dict[str, Dict[str, str]] = cls.collect_out_names(type_data) return GraphQLObjectModel( name=name, @@ -204,6 +146,7 @@ def __get_graphql_model_without_schema__( getattr(cls, "__description__", None), ), fields=tuple(fields_ast), + interfaces=tuple(interfaces_ast), ), resolvers=resolvers, aliases=aliases, @@ -301,150 +244,12 @@ def argument( options["default_value"] = default_value return options - -@dataclass(frozen=True) -class GraphQLObjectData: - fields: Dict[str, "GraphQLObjectField"] - - -def get_graphql_object_data( - metadata: GraphQLMetadata, cls: Type[GraphQLObject] -) -> GraphQLObjectData: - try: - return metadata.get_data(cls) - except KeyError: - if getattr(cls, "__schema__", None): - raise NotImplementedError( - "'get_graphql_object_data' is not supported for " - "objects with '__schema__'." - ) - else: - object_data = create_graphql_object_data_without_schema(cls) - - metadata.set_data(cls, object_data) - return object_data - - -def create_graphql_object_data_without_schema( - cls: Type[GraphQLObject], -) -> GraphQLObjectData: - fields_types: Dict[str, str] = {} - fields_names: Dict[str, str] = {} - fields_descriptions: Dict[str, str] = {} - fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} - fields_resolvers: Dict[str, Resolver] = {} - fields_defaults: Dict[str, Any] = {} - fields_order: List[str] = [] - - type_hints = cls.__annotations__ - - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - aliases_targets: List[str] = list(aliases.values()) - - for attr_name, attr_type in type_hints.items(): - if attr_name.startswith("__"): - continue - - if attr_name in aliases_targets: - # Alias target is not included in schema - # unless its explicit field - cls_attr = getattr(cls, attr_name, None) - if not isinstance(cls_attr, GraphQLObjectField): - continue - - fields_order.append(attr_name) - - fields_names[attr_name] = convert_python_name_to_graphql(attr_name) - fields_types[attr_name] = attr_type - - for attr_name in dir(cls): - if attr_name.startswith("__"): - continue - - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - if attr_name not in fields_order: - fields_order.append(attr_name) - - fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( - attr_name - ) - - if cls_attr.type and attr_name not in fields_types: - fields_types[attr_name] = cls_attr.type - if cls_attr.description: - fields_descriptions[attr_name] = cls_attr.description - if cls_attr.resolver: - fields_resolvers[attr_name] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - fields_args[attr_name] = update_field_args_options( - field_args, cls_attr.args - ) - if cls_attr.default_value: - fields_defaults[attr_name] = cls_attr.default_value - - elif isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.type - if cls_attr.description: - fields_descriptions[cls_attr.field] = cls_attr.description - if cls_attr.resolver: - fields_resolvers[cls_attr.field] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - fields_args[cls_attr.field] = update_field_args_options( - field_args, cls_attr.args - ) - - elif attr_name not in aliases_targets and not callable(cls_attr): - fields_defaults[attr_name] = cls_attr - - fields: Dict[str, "GraphQLObjectField"] = {} - for field_name in fields_order: - fields[field_name] = GraphQLObjectField( - name=fields_names[field_name], - description=fields_descriptions.get(field_name), - type=fields_types[field_name], - args=fields_args.get(field_name), - resolver=fields_resolvers.get(field_name), - default_value=fields_defaults.get(field_name), - ) - - return GraphQLObjectData(fields=fields) - - -class GraphQLObjectField: - name: Optional[str] - description: Optional[str] - type: Optional[Any] - args: Optional[Dict[str, dict]] - resolver: Optional[Resolver] - default_value: Optional[Any] - - def __init__( - self, - *, - name: Optional[str] = None, - description: Optional[str] = None, - type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, - resolver: Optional[Resolver] = None, - default_value: Optional[Any] = None, - ): - self.name = name - self.description = description - self.type = type - self.args = args - self.resolver = resolver - self.default_value = default_value - - def __call__(self, resolver: Resolver): - """Makes GraphQLObjectField instances work as decorators.""" - self.resolver = resolver - if not self.type: - self.type = get_field_type_from_resolver(resolver) - return self + @classmethod + def __validate_interfaces__(cls): + if getattr(cls, "__implements__", None): + for interface in cls.__implements__: + if not issubclass(interface, GraphQLType): + raise TypeError() def object_field( @@ -470,19 +275,6 @@ def object_field( ) -def get_field_type_from_resolver(resolver: Resolver) -> Any: - return resolver.__annotations__.get("return") - - -@dataclass(frozen=True) -class GraphQLObjectResolver: - resolver: Resolver - field: str - description: Optional[str] = None - args: Optional[Dict[str, dict]] = None - type: Optional[Any] = None - - def object_resolver( field: str, type: Optional[Any] = None, @@ -537,62 +329,8 @@ def get_field_node_from_obj_field( ) -@dataclass(frozen=True) -class GraphQLObjectFieldArg: - name: Optional[str] - out_name: Optional[str] - type: Optional[Any] - description: Optional[str] = None - default_value: Optional[Any] = None - - -def get_field_args_from_resolver( - resolver: Resolver, -) -> Dict[str, GraphQLObjectFieldArg]: - resolver_signature = signature(resolver) - type_hints = resolver.__annotations__ - type_hints.pop("return", None) - - field_args: Dict[str, GraphQLObjectFieldArg] = {} - field_args_start = 0 - - # Fist pass: (arg, *_, something, something) or (arg, *, something, something): - for i, param in enumerate(resolver_signature.parameters.values()): - param_repr = str(param) - if param_repr.startswith("*") and not param_repr.startswith("**"): - field_args_start = i + 1 - break - else: - if len(resolver_signature.parameters) < 2: - raise TypeError( - f"Resolver function '{resolver_signature}' should accept at least " - "'obj' and 'info' positional arguments." - ) - - field_args_start = 2 - - args_parameters = tuple(resolver_signature.parameters.items())[field_args_start:] - if not args_parameters: - return field_args - - for param_name, param in args_parameters: - if param.default != param.empty: - param_default = param.default - else: - param_default = None - - field_args[param_name] = GraphQLObjectFieldArg( - name=convert_python_name_to_graphql(param_name), - out_name=param_name, - type=type_hints.get(param_name), - default_value=param_default, - ) - - return field_args - - def get_field_args_out_names( - field_args: Dict[str, GraphQLObjectFieldArg] + field_args: Dict[str, GraphQLObjectFieldArg], ) -> Dict[str, str]: out_names: Dict[str, str] = {} for field_arg in field_args.values(): @@ -629,38 +367,6 @@ def get_field_arg_node_from_obj_field_arg( ) -def update_field_args_options( - field_args: Dict[str, GraphQLObjectFieldArg], - args_options: Optional[Dict[str, dict]], -) -> Dict[str, GraphQLObjectFieldArg]: - if not args_options: - return field_args - - updated_args: Dict[str, GraphQLObjectFieldArg] = {} - for arg_name in field_args: - arg_options = args_options.get(arg_name) - if not arg_options: - updated_args[arg_name] = field_args[arg_name] - continue - - args_update = {} - if arg_options.get("name"): - args_update["name"] = arg_options["name"] - if arg_options.get("description"): - args_update["description"] = arg_options["description"] - if arg_options.get("default_value") is not None: - args_update["default_value"] = arg_options["default_value"] - if arg_options.get("type"): - args_update["type"] = arg_options["type"] - - if args_update: - updated_args[arg_name] = replace(field_args[arg_name], **args_update) - else: - updated_args[arg_name] = field_args[arg_name] - - return updated_args - - def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> Dict[str, Any]: definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) diff --git a/ariadne_graphql_modules/next/resolver.py b/ariadne_graphql_modules/next/resolver.py new file mode 100644 index 0000000..0049793 --- /dev/null +++ b/ariadne_graphql_modules/next/resolver.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass, replace +from inspect import signature +from typing import Any, Dict, Optional + +from ariadne.types import Resolver + +from .convert_name import convert_python_name_to_graphql +from .field import GraphQLObjectFieldArg + + +@dataclass(frozen=True) +class GraphQLObjectResolver: + resolver: Resolver + field: str + description: Optional[str] = None + args: Optional[Dict[str, dict]] = None + type: Optional[Any] = None + + +def get_field_args_from_resolver( + resolver: Resolver, +) -> Dict[str, GraphQLObjectFieldArg]: + resolver_signature = signature(resolver) + type_hints = resolver.__annotations__ + type_hints.pop("return", None) + + field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args_start = 0 + + # Fist pass: (arg, *_, something, something) or (arg, *, something, something): + for i, param in enumerate(resolver_signature.parameters.values()): + param_repr = str(param) + if param_repr.startswith("*") and not param_repr.startswith("**"): + field_args_start = i + 1 + break + else: + if len(resolver_signature.parameters) < 2: + raise TypeError( + f"Resolver function '{resolver_signature}' should accept at least " + "'obj' and 'info' positional arguments." + ) + + field_args_start = 2 + + args_parameters = tuple(resolver_signature.parameters.items())[field_args_start:] + if not args_parameters: + return field_args + + for param_name, param in args_parameters: + if param.default != param.empty: + param_default = param.default + else: + param_default = None + + field_args[param_name] = GraphQLObjectFieldArg( + name=convert_python_name_to_graphql(param_name), + out_name=param_name, + type=type_hints.get(param_name), + default_value=param_default, + ) + + return field_args + + +def update_field_args_options( + field_args: Dict[str, GraphQLObjectFieldArg], + args_options: Optional[Dict[str, dict]], +) -> Dict[str, GraphQLObjectFieldArg]: + if not args_options: + return field_args + + updated_args: Dict[str, GraphQLObjectFieldArg] = {} + for arg_name in field_args: + arg_options = args_options.get(arg_name) + if not arg_options: + updated_args[arg_name] = field_args[arg_name] + continue + + args_update = {} + if arg_options.get("name"): + args_update["name"] = arg_options["name"] + if arg_options.get("description"): + args_update["description"] = arg_options["description"] + if arg_options.get("default_value") is not None: + args_update["default_value"] = arg_options["default_value"] + if arg_options.get("type"): + args_update["type"] = arg_options["type"] + + if args_update: + updated_args[arg_name] = replace(field_args[arg_name], **args_update) + else: + updated_args[arg_name] = field_args[arg_name] + + return updated_args diff --git a/tests_next/test_get_field_args_from_resolver.py b/tests_next/test_get_field_args_from_resolver.py index 054fd99..6e8174c 100644 --- a/tests_next/test_get_field_args_from_resolver.py +++ b/tests_next/test_get_field_args_from_resolver.py @@ -1,4 +1,4 @@ -from ariadne_graphql_modules.next.objecttype import get_field_args_from_resolver +from ariadne_graphql_modules.next.resolver import get_field_args_from_resolver def test_field_has_no_args_after_obj_and_info_args(): diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py new file mode 100644 index 0000000..940751d --- /dev/null +++ b/tests_next/test_interface_type.py @@ -0,0 +1,132 @@ +from typing import List + +from ariadne_graphql_modules.next import ( + GraphQLID, + GraphQLObject, + GraphQLInterface, + GraphQLUnion, + make_executable_schema, +) + + +class CommentType(GraphQLObject): + id: GraphQLID + content: str + + +def test_interface_without_schema(assert_schema_equals): + class UserInterface(GraphQLInterface): + summary: str + score: int + + class UserType(GraphQLObject): + name: str + summary: str + score: int + + __implements__ = [UserInterface] + + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=List[ResultType]) + def search(*_) -> List[UserType | CommentType]: + return [ + UserType(id=1, username="Bob"), + CommentType(id=2, content="Hello World!"), + ] + + schema = make_executable_schema(QueryType, UserInterface, UserType) + + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } + + union Result = User | Comment + + type User implements UserInterface { + name: String! + summary: String! + score: Int! + } + + type Comment { + id: ID! + content: String! + } + + interface UserInterface { + summary: String! + score: Int! + } + + """, + ) + + +def test_interface_with_schema(assert_schema_equals): + class UserInterface(GraphQLInterface): + __schema__ = """ + interface UserInterface { + summary: String! + score: Int! + } + """ + + class UserType(GraphQLObject): + __schema__ = """ + type User implements UserInterface { + id: ID! + name: String! + summary: String! + score: Int! + } + """ + + __implements__ = [UserInterface] + + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=List[ResultType]) + def search(*_) -> List[UserType | CommentType]: + return [ + UserType(id=1, username="Bob"), + CommentType(id=2, content="Hello World!"), + ] + + schema = make_executable_schema(QueryType, UserInterface, UserType) + + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } + + union Result = User | Comment + + type User implements UserInterface { + id: ID! + name: String! + summary: String! + score: Int! + } + + type Comment { + id: ID! + content: String! + } + + interface UserInterface { + summary: String! + score: Int! + } + + """, + ) From 6df97637539b47becc65b736dca8bfaf0af83f78 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Tue, 16 Apr 2024 11:32:06 +0200 Subject: [PATCH 30/63] Inherit from GraphQLObject in the Interface type --- ariadne_graphql_modules/next/field.py | 50 --- ariadne_graphql_modules/next/interfacetype.py | 205 ++++----- ariadne_graphql_modules/next/metadata.py | 123 ------ ariadne_graphql_modules/next/objectmixin.py | 196 --------- ariadne_graphql_modules/next/objecttype.py | 392 ++++++++++++++++-- ariadne_graphql_modules/next/resolver.py | 94 ----- .../test_get_field_args_from_resolver.py | 2 +- 7 files changed, 458 insertions(+), 604 deletions(-) delete mode 100644 ariadne_graphql_modules/next/field.py delete mode 100644 ariadne_graphql_modules/next/metadata.py delete mode 100644 ariadne_graphql_modules/next/objectmixin.py delete mode 100644 ariadne_graphql_modules/next/resolver.py diff --git a/ariadne_graphql_modules/next/field.py b/ariadne_graphql_modules/next/field.py deleted file mode 100644 index 01ae10d..0000000 --- a/ariadne_graphql_modules/next/field.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Dict, Optional - -from ariadne.types import Resolver - - -@dataclass -class GraphQLObjectFieldArg: - name: str - out_name: str - type: Any - description: Optional[str] = None - default_value: Optional[Any] = None - - -class GraphQLObjectField: - name: Optional[str] - description: Optional[str] - type: Optional[Any] - args: Optional[Dict[str, dict]] - resolver: Optional[Resolver] - default_value: Optional[Any] - - def __init__( - self, - *, - name: Optional[str] = None, - description: Optional[str] = None, - type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, - resolver: Optional[Resolver] = None, - default_value: Optional[Any] = None, - ): - self.name = name - self.description = description - self.type = type - self.args = args - self.resolver = resolver - self.default_value = default_value - - def __call__(self, resolver: Resolver): - """Makes GraphQLObjectField instances work as decorators.""" - self.resolver = resolver - if not self.type: - self.type = get_field_type_from_resolver(resolver) - return self - - -def get_field_type_from_resolver(resolver: Resolver) -> Any: - return resolver.__annotations__.get("return") diff --git a/ariadne_graphql_modules/next/interfacetype.py b/ariadne_graphql_modules/next/interfacetype.py index 3f97f28..be7beb5 100644 --- a/ariadne_graphql_modules/next/interfacetype.py +++ b/ariadne_graphql_modules/next/interfacetype.py @@ -11,7 +11,6 @@ Union, cast, Sequence, - NoReturn, ) from ariadne import InterfaceType @@ -21,47 +20,33 @@ GraphQLField, GraphQLObjectType, GraphQLSchema, + InputValueDefinitionNode, NameNode, NamedTypeNode, InterfaceTypeDefinitionNode, ) -from .metadata import get_graphql_object_data -from .objectmixin import GraphQLModelHelpersMixin +from .value import get_value_node + +from .objecttype import ( + GraphQLObject, + GraphQLObjectResolver, + get_field_args_from_resolver, + get_field_args_out_names, + get_field_node_from_obj_field, + get_graphql_object_data, + update_field_args_options, +) from ..utils import parse_definition from .base import GraphQLMetadata, GraphQLModel, GraphQLType from .description import get_description_node -from .typing import get_graphql_type -from .validators import validate_description, validate_name -class GraphQLInterface(GraphQLType, GraphQLModelHelpersMixin): +class GraphQLInterface(GraphQLObject): __types__: Sequence[Type[GraphQLType]] __implements__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.__dict__.get("__abstract__"): - return - - cls.__abstract__ = False - - if cls.__dict__.get("__schema__"): - validate_interface_type_with_schema(cls) - else: - validate_interface_type(cls) - - @classmethod - def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": - name = cls.__get_graphql_name__() - metadata.set_graphql_name(cls, name) - - if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(metadata, name) - - return cls.__get_graphql_model_without_schema__(metadata, name) + __valid_type__ = InterfaceTypeDefinitionNode @classmethod def __get_graphql_model_with_schema__( @@ -71,9 +56,75 @@ def __get_graphql_model_with_schema__( InterfaceTypeDefinitionNode, parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), ) - resolvers: Dict[str, Resolver] = cls.collect_resolvers_with_schema() + + descriptions: Dict[str, str] = {} + args_descriptions: Dict[str, Dict[str, str]] = {} + args_defaults: Dict[str, Dict[str, Any]] = {} + resolvers: Dict[str, Resolver] = {} out_names: Dict[str, Dict[str, str]] = {} - fields: List[FieldDefinitionNode] = cls.gather_fields_with_schema(definition) + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers[cls_attr.field] = cls_attr.resolver + if cls_attr.description: + descriptions[cls_attr.field] = get_description_node( + cls_attr.description + ) + + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + args_descriptions[cls_attr.field] = {} + args_defaults[cls_attr.field] = {} + + final_args = update_field_args_options(field_args, cls_attr.args) + + for arg_name, arg_options in final_args.items(): + arg_description = get_description_node(arg_options.description) + if arg_description: + args_descriptions[cls_attr.field][arg_name] = ( + arg_description + ) + + arg_default = arg_options.default_value + if arg_default is not None: + args_defaults[cls_attr.field][arg_name] = get_value_node( + arg_default + ) + + fields: List[FieldDefinitionNode] = [] + for field in definition.fields: + field_args_descriptions = args_descriptions.get(field.name.value, {}) + field_args_defaults = args_defaults.get(field.name.value, {}) + + args: List[InputValueDefinitionNode] = [] + for arg in field.arguments: + arg_name = arg.name.value + args.append( + InputValueDefinitionNode( + description=( + arg.description or field_args_descriptions.get(arg_name) + ), + name=arg.name, + directives=arg.directives, + type=arg.type, + default_value=( + arg.default_value or field_args_defaults.get(arg_name) + ), + ) + ) + + fields.append( + FieldDefinitionNode( + name=field.name, + description=( + field.description or descriptions.get(field.name.value) + ), + directives=field.directives, + arguments=tuple(args), + type=field.type, + ) + ) return GraphQLInterfaceModel( name=definition.name.value, @@ -94,16 +145,30 @@ def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLInterfaceModel": type_data = get_graphql_object_data(metadata, cls) + type_aliases = getattr(cls, "__aliases__", None) or {} - fields_ast: List[FieldDefinitionNode] = cls.gather_fields_without_schema( - metadata - ) - interfaces_ast: List[NamedTypeNode] = cls.gather_interfaces_without_schema( - type_data - ) - resolvers: Dict[str, Resolver] = cls.collect_resolvers_without_schema(type_data) - aliases: Dict[str, str] = cls.collect_aliases(type_data) - out_names: Dict[str, Dict[str, str]] = cls.collect_out_names(type_data) + fields_ast: List[FieldDefinitionNode] = [] + resolvers: Dict[str, Resolver] = {} + aliases: Dict[str, str] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for attr_name, field in type_data.fields.items(): + fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) + + if attr_name in type_aliases: + aliases[field.name] = type_aliases[attr_name] + elif attr_name != field.name: + aliases[field.name] = attr_name + + if field.resolver: + resolvers[field.name] = field.resolver + + if field.args: + out_names[field.name] = get_field_args_out_names(field.args) + + interfaces_ast: List[NamedTypeNode] = [] + for interface_name in type_data.interfaces: + interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) return GraphQLInterfaceModel( name=name, @@ -122,44 +187,6 @@ def __get_graphql_model_without_schema__( out_names=out_names, ) - @classmethod - def __get_graphql_types__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: - """Returns iterable with GraphQL types associated with this type""" - if getattr(cls, "__schema__", None): - return cls.__get_graphql_types_with_schema__(metadata) - - return cls.__get_graphql_types_without_schema__(metadata) - - @classmethod - def __get_graphql_types_with_schema__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: - types: List[GraphQLType] = [cls] - types.extend(getattr(cls, "__requires__", [])) - return types - - @classmethod - def __get_graphql_types_without_schema__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: - types: List[GraphQLType] = [cls] - type_data = get_graphql_object_data(metadata, cls) - - for field in type_data.fields.values(): - field_type = get_graphql_type(field.type) - if field_type and field_type not in types: - types.append(field_type) - - if field.args: - for field_arg in field.args.values(): - field_arg_type = get_graphql_type(field_arg.type) - if field_arg_type and field_arg_type not in types: - types.append(field_arg_type) - - return types - @staticmethod def resolve_type(obj: Any, *args) -> str: if isinstance(obj, GraphQLInterface): @@ -170,28 +197,6 @@ def resolve_type(obj: Any, *args) -> str: ) -def validate_interface_type(cls: Type[GraphQLInterface]) -> NoReturn: - pass - - -def validate_interface_type_with_schema(cls: Type[GraphQLInterface]) -> NoReturn: - definition = cast( - InterfaceTypeDefinitionNode, - parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), - ) - - if not isinstance(definition, InterfaceTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines a '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{InterfaceTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - @dataclass(frozen=True) class GraphQLInterfaceModel(GraphQLModel): resolvers: Dict[str, Resolver] diff --git a/ariadne_graphql_modules/next/metadata.py b/ariadne_graphql_modules/next/metadata.py deleted file mode 100644 index a507852..0000000 --- a/ariadne_graphql_modules/next/metadata.py +++ /dev/null @@ -1,123 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Dict, List - -from .base import GraphQLMetadata -from .convert_name import convert_python_name_to_graphql -from .field import GraphQLObjectField, GraphQLObjectFieldArg -from .resolver import ( - GraphQLObjectResolver, - get_field_args_from_resolver, - update_field_args_options, -) -from ariadne.types import Resolver - - -@dataclass(frozen=True) -class GraphQLObjectData: - fields: Dict[str, GraphQLObjectField] - interfaces: List[str] - - -def get_graphql_object_data(metadata: GraphQLMetadata, cls): - try: - return metadata.get_data(cls) - except KeyError: - if getattr(cls, "__schema__", None): - raise NotImplementedError( - "'get_graphql_object_data' is not supported for objects with '__schema__'." - ) - else: - return create_graphql_object_data_without_schema(cls) - - -def create_graphql_object_data_without_schema( - cls, -) -> GraphQLObjectData: - fields_types: Dict[str, str] = {} - fields_names: Dict[str, str] = {} - fields_descriptions: Dict[str, str] = {} - fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} - fields_resolvers: Dict[str, Resolver] = {} - fields_defaults: Dict[str, Any] = {} - fields_order: List[str] = [] - - type_hints = cls.__annotations__ - - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - aliases_targets: List[str] = list(aliases.values()) - - interfaces: List[str] = [ - interface.__name__ for interface in getattr(cls, "__implements__", []) - ] - - for attr_name, attr_type in type_hints.items(): - if attr_name.startswith("__"): - continue - - if attr_name in aliases_targets: - # Alias target is not included in schema - # unless its explicit field - cls_attr = getattr(cls, attr_name, None) - if not isinstance(cls_attr, GraphQLObjectField): - continue - - fields_order.append(attr_name) - - fields_names[attr_name] = convert_python_name_to_graphql(attr_name) - fields_types[attr_name] = attr_type - - for attr_name in dir(cls): - if attr_name.startswith("__"): - continue - - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - if attr_name not in fields_order: - fields_order.append(attr_name) - - fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( - attr_name - ) - - if cls_attr.type and attr_name not in fields_types: - fields_types[attr_name] = cls_attr.type - if cls_attr.description: - fields_descriptions[attr_name] = cls_attr.description - if cls_attr.resolver: - fields_resolvers[attr_name] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - fields_args[attr_name] = update_field_args_options( - field_args, cls_attr.args - ) - if cls_attr.default_value: - fields_defaults[attr_name] = cls_attr.default_value - - elif isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.type - if cls_attr.description: - fields_descriptions[cls_attr.field] = cls_attr.description - if cls_attr.resolver: - fields_resolvers[cls_attr.field] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - fields_args[cls_attr.field] = update_field_args_options( - field_args, cls_attr.args - ) - - elif attr_name not in aliases_targets and not callable(cls_attr): - fields_defaults[attr_name] = cls_attr - - fields: Dict[str, "GraphQLObjectField"] = {} - for field_name in fields_order: - fields[field_name] = GraphQLObjectField( - name=fields_names[field_name], - description=fields_descriptions.get(field_name), - type=fields_types[field_name], - args=fields_args.get(field_name), - resolver=fields_resolvers.get(field_name), - default_value=fields_defaults.get(field_name), - ) - - return GraphQLObjectData(fields=fields, interfaces=interfaces) diff --git a/ariadne_graphql_modules/next/objectmixin.py b/ariadne_graphql_modules/next/objectmixin.py deleted file mode 100644 index b0b650c..0000000 --- a/ariadne_graphql_modules/next/objectmixin.py +++ /dev/null @@ -1,196 +0,0 @@ -from typing import Dict, List, Optional -from graphql import ( - FieldDefinitionNode, - InputValueDefinitionNode, - NameNode, - NamedTypeNode, -) - -from .base import GraphQLMetadata -from .description import get_description_node -from .field import GraphQLObjectField, GraphQLObjectFieldArg -from .metadata import ( - GraphQLObjectData, - get_graphql_object_data, -) -from .resolver import ( - GraphQLObjectResolver, - get_field_args_from_resolver, - update_field_args_options, -) -from .typing import get_type_node -from .value import get_value_node - - -class GraphQLModelHelpersMixin: - @classmethod - def gather_fields_without_schema(cls, metadata: GraphQLMetadata): - type_data = get_graphql_object_data(metadata, cls) - fields_ast = [ - cls.create_field_node(metadata, field) - for field in type_data.fields.values() - ] - return fields_ast - - @staticmethod - def collect_resolvers_without_schema(type_data: GraphQLObjectData): - return { - field.name: field.resolver - for field in type_data.fields.values() - if field.resolver - } - - @staticmethod - def collect_aliases(type_data: GraphQLObjectData): - aliases = {} - for field in type_data.fields.values(): - attr_name = field.name # Placeholder for actual attribute name logic - if attr_name != field.name: - aliases[field.name] = attr_name - return aliases - - @staticmethod - def collect_out_names(type_data: GraphQLObjectData): - out_names = {} - for field in type_data.fields.values(): - if field.args: - out_names[field.name] = { - arg.name: arg.out_name for arg in field.args.values() - } - return out_names - - @classmethod - def create_field_node(cls, metadata: GraphQLMetadata, field: GraphQLObjectField): - args_nodes = cls.get_field_args_nodes_from_obj_field_args(metadata, field.args) - return FieldDefinitionNode( - description=get_description_node(field.description), - name=NameNode(value=field.name), - type=get_type_node(metadata, field.type), - arguments=tuple(args_nodes) if args_nodes else None, - ) - - @staticmethod - def get_field_args_nodes_from_obj_field_args( - metadata: GraphQLMetadata, - field_args: Optional[Dict[str, GraphQLObjectFieldArg]], - ): - if not field_args: - return [] - - return [ - InputValueDefinitionNode( - description=get_description_node(arg.description), - name=NameNode(value=arg.name), - type=get_type_node(metadata, arg.type), - default_value=get_value_node(arg.default_value) - if arg.default_value is not None - else None, - ) - for arg in field_args.values() - ] - - @classmethod - def gather_interfaces_without_schema(cls, type_data: GraphQLObjectData): - interfaces_ast: List[NamedTypeNode] = [] - for interface_name in type_data.interfaces: - interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) - return interfaces_ast - - @classmethod - def gather_interfaces_with_schema(cls): - return [interface for interface in getattr(cls, "__implements__", [])] - - @classmethod - def gather_fields_with_schema(cls, definition): - descriptions, args_descriptions, args_defaults = ( - cls.collect_descriptions_and_defaults() - ) - - fields = [ - cls.create_field_definition_node( - field, descriptions, args_descriptions, args_defaults - ) - for field in definition.fields - ] - return fields - - @classmethod - def collect_resolvers_with_schema(cls): - resolvers = {} - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - resolvers[cls_attr.field] = cls_attr.resolver - return resolvers - - @classmethod - def collect_descriptions_and_defaults(cls): - descriptions = {} - args_descriptions = {} - args_defaults = {} - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.description: - descriptions[cls_attr.field] = get_description_node( - cls_attr.description - ) - - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - args_descriptions[cls_attr.field], args_defaults[cls_attr.field] = ( - cls.process_field_args(field_args, cls_attr.args) - ) - - return descriptions, args_descriptions, args_defaults - - @classmethod - def process_field_args(cls, field_args, resolver_args): - descriptions = {} - defaults = {} - final_args = update_field_args_options(field_args, resolver_args) - - for arg_name, arg_options in final_args.items(): - descriptions[arg_name] = ( - get_description_node(arg_options.description) - if arg_options.description - else None - ) - defaults[arg_name] = ( - get_value_node(arg_options.default_value) - if arg_options.default_value is not None - else None - ) - - return descriptions, defaults - - @classmethod - def create_field_definition_node( - cls, field, descriptions, args_descriptions, args_defaults - ): - field_name = field.name.value - field_args_descriptions = args_descriptions.get(field_name, {}) - field_args_defaults = args_defaults.get(field_name, {}) - - args = [ - InputValueDefinitionNode( - description=( - arg.description or field_args_descriptions.get(arg.name.value) - ), - name=arg.name, - directives=arg.directives, - type=arg.type, - default_value=( - arg.default_value or field_args_defaults.get(arg.name.value) - ), - ) - for arg in field.arguments - ] - - return FieldDefinitionNode( - name=field.name, - description=(field.description or descriptions.get(field_name)), - directives=field.directives, - arguments=tuple(args), - type=field.type, - ) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 88508c5..ff803af 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,6 +1,7 @@ from copy import deepcopy -from dataclasses import dataclass +from dataclasses import dataclass, replace from enum import Enum +from inspect import signature from typing import ( Any, Dict, @@ -22,22 +23,9 @@ GraphQLSchema, InputValueDefinitionNode, NameNode, - ObjectTypeDefinitionNode, NamedTypeNode, -) - -from .field import ( - GraphQLObjectField, - GraphQLObjectFieldArg, - get_field_type_from_resolver, -) -from .metadata import ( - get_graphql_object_data, -) -from .objectmixin import GraphQLModelHelpersMixin -from .resolver import ( - GraphQLObjectResolver, - get_field_args_from_resolver, + ObjectTypeDefinitionNode, + TypeDefinitionNode, ) from ..utils import parse_definition @@ -49,7 +37,7 @@ from .value import get_value_node -class GraphQLObject(GraphQLType, GraphQLModelHelpersMixin): +class GraphQLObject(GraphQLType): __kwargs__: Dict[str, Any] __abstract__: bool = True __schema__: Optional[str] @@ -78,10 +66,10 @@ def __init_subclass__(cls) -> None: return cls.__abstract__ = False - # cls.__validate_interfaces__() if cls.__dict__.get("__schema__"): - cls.__kwargs__ = validate_object_type_with_schema(cls) + valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) + cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) else: cls.__kwargs__ = validate_object_type_without_schema(cls) @@ -104,9 +92,74 @@ def __get_graphql_model_with_schema__( parse_definition(ObjectTypeDefinitionNode, cls.__schema__), ) - resolvers: Dict[str, Resolver] = cls.collect_resolvers_with_schema() + descriptions: Dict[str, str] = {} + args_descriptions: Dict[str, Dict[str, str]] = {} + args_defaults: Dict[str, Dict[str, Any]] = {} + resolvers: Dict[str, Resolver] = {} out_names: Dict[str, Dict[str, str]] = {} - fields: List[FieldDefinitionNode] = cls.gather_fields_with_schema(definition) + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers[cls_attr.field] = cls_attr.resolver + if cls_attr.description: + descriptions[cls_attr.field] = get_description_node( + cls_attr.description + ) + + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + args_descriptions[cls_attr.field] = {} + args_defaults[cls_attr.field] = {} + + final_args = update_field_args_options(field_args, cls_attr.args) + + for arg_name, arg_options in final_args.items(): + arg_description = get_description_node(arg_options.description) + if arg_description: + args_descriptions[cls_attr.field][arg_name] = ( + arg_description + ) + + arg_default = arg_options.default_value + if arg_default is not None: + args_defaults[cls_attr.field][arg_name] = get_value_node( + arg_default + ) + + fields: List[FieldDefinitionNode] = [] + for field in definition.fields: + field_args_descriptions = args_descriptions.get(field.name.value, {}) + field_args_defaults = args_defaults.get(field.name.value, {}) + + args: List[InputValueDefinitionNode] = [] + for arg in field.arguments: + arg_name = arg.name.value + args.append( + InputValueDefinitionNode( + description=( + arg.description or field_args_descriptions.get(arg_name) + ), + name=arg.name, + directives=arg.directives, + type=arg.type, + default_value=( + arg.default_value or field_args_defaults.get(arg_name) + ), + ) + ) + + fields.append( + FieldDefinitionNode( + name=field.name, + description=( + field.description or descriptions.get(field.name.value) + ), + directives=field.directives, + arguments=tuple(args), + type=field.type, + ) + ) return GraphQLObjectModel( name=definition.name.value, @@ -126,16 +179,30 @@ def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLObjectModel": type_data = get_graphql_object_data(metadata, cls) + type_aliases = getattr(cls, "__aliases__", None) or {} - fields_ast: List[FieldDefinitionNode] = cls.gather_fields_without_schema( - metadata - ) - interfaces_ast: List[NamedTypeNode] = cls.gather_interfaces_without_schema( - type_data - ) - resolvers: Dict[str, Resolver] = cls.collect_resolvers_without_schema(type_data) - aliases: Dict[str, str] = cls.collect_aliases(type_data) - out_names: Dict[str, Dict[str, str]] = cls.collect_out_names(type_data) + fields_ast: List[FieldDefinitionNode] = [] + resolvers: Dict[str, Resolver] = {} + aliases: Dict[str, str] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for attr_name, field in type_data.fields.items(): + fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) + + if attr_name in type_aliases: + aliases[field.name] = type_aliases[attr_name] + elif attr_name != field.name: + aliases[field.name] = attr_name + + if field.resolver: + resolvers[field.name] = field.resolver + + if field.args: + out_names[field.name] = get_field_args_out_names(field.args) + + interfaces_ast: List[NamedTypeNode] = [] + for interface_name in type_data.interfaces: + interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) return GraphQLObjectModel( name=name, @@ -244,12 +311,155 @@ def argument( options["default_value"] = default_value return options - @classmethod - def __validate_interfaces__(cls): - if getattr(cls, "__implements__", None): - for interface in cls.__implements__: - if not issubclass(interface, GraphQLType): - raise TypeError() + +@dataclass(frozen=True) +class GraphQLObjectData: + fields: Dict[str, "GraphQLObjectField"] + interfaces: List[str] + + +def get_graphql_object_data( + metadata: GraphQLMetadata, cls: Type[GraphQLObject] +) -> GraphQLObjectData: + try: + return metadata.get_data(cls) + except KeyError: + if getattr(cls, "__schema__", None): + raise NotImplementedError( + "'get_graphql_object_data' is not supported for " + "objects with '__schema__'." + ) + else: + object_data = create_graphql_object_data_without_schema(cls) + + metadata.set_data(cls, object_data) + return object_data + + +def create_graphql_object_data_without_schema( + cls, +) -> GraphQLObjectData: + fields_types: Dict[str, str] = {} + fields_names: Dict[str, str] = {} + fields_descriptions: Dict[str, str] = {} + fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} + fields_resolvers: Dict[str, Resolver] = {} + fields_defaults: Dict[str, Any] = {} + fields_order: List[str] = [] + + type_hints = cls.__annotations__ + + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + aliases_targets: List[str] = list(aliases.values()) + + interfaces: List[str] = [ + interface.__name__ for interface in getattr(cls, "__implements__", []) + ] + + for attr_name, attr_type in type_hints.items(): + if attr_name.startswith("__"): + continue + + if attr_name in aliases_targets: + # Alias target is not included in schema + # unless its explicit field + cls_attr = getattr(cls, attr_name, None) + if not isinstance(cls_attr, GraphQLObjectField): + continue + + fields_order.append(attr_name) + + fields_names[attr_name] = convert_python_name_to_graphql(attr_name) + fields_types[attr_name] = attr_type + + for attr_name in dir(cls): + if attr_name.startswith("__"): + continue + + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if attr_name not in fields_order: + fields_order.append(attr_name) + + fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( + attr_name + ) + + if cls_attr.type and attr_name not in fields_types: + fields_types[attr_name] = cls_attr.type + if cls_attr.description: + fields_descriptions[attr_name] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[attr_name] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[attr_name] = update_field_args_options( + field_args, cls_attr.args + ) + if cls_attr.default_value: + fields_defaults[attr_name] = cls_attr.default_value + + elif isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.type + if cls_attr.description: + fields_descriptions[cls_attr.field] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[cls_attr.field] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + + elif attr_name not in aliases_targets and not callable(cls_attr): + fields_defaults[attr_name] = cls_attr + + fields: Dict[str, "GraphQLObjectField"] = {} + for field_name in fields_order: + fields[field_name] = GraphQLObjectField( + name=fields_names[field_name], + description=fields_descriptions.get(field_name), + type=fields_types[field_name], + args=fields_args.get(field_name), + resolver=fields_resolvers.get(field_name), + default_value=fields_defaults.get(field_name), + ) + + return GraphQLObjectData(fields=fields, interfaces=interfaces) + + +class GraphQLObjectField: + name: Optional[str] + description: Optional[str] + type: Optional[Any] + args: Optional[Dict[str, dict]] + resolver: Optional[Resolver] + default_value: Optional[Any] + + def __init__( + self, + *, + name: Optional[str] = None, + description: Optional[str] = None, + type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, + resolver: Optional[Resolver] = None, + default_value: Optional[Any] = None, + ): + self.name = name + self.description = description + self.type = type + self.args = args + self.resolver = resolver + self.default_value = default_value + + def __call__(self, resolver: Resolver): + """Makes GraphQLObjectField instances work as decorators.""" + self.resolver = resolver + if not self.type: + self.type = get_field_type_from_resolver(resolver) + return self def object_field( @@ -275,6 +485,19 @@ def object_field( ) +def get_field_type_from_resolver(resolver: Resolver) -> Any: + return resolver.__annotations__.get("return") + + +@dataclass(frozen=True) +class GraphQLObjectResolver: + resolver: Resolver + field: str + description: Optional[str] = None + args: Optional[Dict[str, dict]] = None + type: Optional[Any] = None + + def object_resolver( field: str, type: Optional[Any] = None, @@ -329,6 +552,60 @@ def get_field_node_from_obj_field( ) +@dataclass(frozen=True) +class GraphQLObjectFieldArg: + name: Optional[str] + out_name: Optional[str] + type: Optional[Any] + description: Optional[str] = None + default_value: Optional[Any] = None + + +def get_field_args_from_resolver( + resolver: Resolver, +) -> Dict[str, GraphQLObjectFieldArg]: + resolver_signature = signature(resolver) + type_hints = resolver.__annotations__ + type_hints.pop("return", None) + + field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args_start = 0 + + # Fist pass: (arg, *_, something, something) or (arg, *, something, something): + for i, param in enumerate(resolver_signature.parameters.values()): + param_repr = str(param) + if param_repr.startswith("*") and not param_repr.startswith("**"): + field_args_start = i + 1 + break + else: + if len(resolver_signature.parameters) < 2: + raise TypeError( + f"Resolver function '{resolver_signature}' should accept at least " + "'obj' and 'info' positional arguments." + ) + + field_args_start = 2 + + args_parameters = tuple(resolver_signature.parameters.items())[field_args_start:] + if not args_parameters: + return field_args + + for param_name, param in args_parameters: + if param.default != param.empty: + param_default = param.default + else: + param_default = None + + field_args[param_name] = GraphQLObjectFieldArg( + name=convert_python_name_to_graphql(param_name), + out_name=param_name, + type=type_hints.get(param_name), + default_value=param_default, + ) + + return field_args + + def get_field_args_out_names( field_args: Dict[str, GraphQLObjectFieldArg], ) -> Dict[str, str]: @@ -367,15 +644,50 @@ def get_field_arg_node_from_obj_field_arg( ) -def validate_object_type_with_schema(cls: Type[GraphQLObject]) -> Dict[str, Any]: - definition = parse_definition(ObjectTypeDefinitionNode, cls.__schema__) +def update_field_args_options( + field_args: Dict[str, GraphQLObjectFieldArg], + args_options: Optional[Dict[str, dict]], +) -> Dict[str, GraphQLObjectFieldArg]: + if not args_options: + return field_args + + updated_args: Dict[str, GraphQLObjectFieldArg] = {} + for arg_name in field_args: + arg_options = args_options.get(arg_name) + if not arg_options: + updated_args[arg_name] = field_args[arg_name] + continue + + args_update = {} + if arg_options.get("name"): + args_update["name"] = arg_options["name"] + if arg_options.get("description"): + args_update["description"] = arg_options["description"] + if arg_options.get("default_value") is not None: + args_update["default_value"] = arg_options["default_value"] + if arg_options.get("type"): + args_update["type"] = arg_options["type"] + + if args_update: + updated_args[arg_name] = replace(field_args[arg_name], **args_update) + else: + updated_args[arg_name] = field_args[arg_name] + + return updated_args + + +def validate_object_type_with_schema( + cls: Type[GraphQLObject], + valid_type: Type[TypeDefinitionNode] = ObjectTypeDefinitionNode, +) -> Dict[str, Any]: + definition = parse_definition(valid_type, cls.__schema__) - if not isinstance(definition, ObjectTypeDefinitionNode): + if not isinstance(definition, valid_type): raise ValueError( f"Class '{cls.__name__}' defines '__schema__' attribute " "with declaration for an invalid GraphQL type. " f"('{definition.__class__.__name__}' != " - f"'{ObjectTypeDefinitionNode.__name__}')" + f"'{valid_type.__name__}')" ) validate_name(cls, definition) diff --git a/ariadne_graphql_modules/next/resolver.py b/ariadne_graphql_modules/next/resolver.py deleted file mode 100644 index 0049793..0000000 --- a/ariadne_graphql_modules/next/resolver.py +++ /dev/null @@ -1,94 +0,0 @@ -from dataclasses import dataclass, replace -from inspect import signature -from typing import Any, Dict, Optional - -from ariadne.types import Resolver - -from .convert_name import convert_python_name_to_graphql -from .field import GraphQLObjectFieldArg - - -@dataclass(frozen=True) -class GraphQLObjectResolver: - resolver: Resolver - field: str - description: Optional[str] = None - args: Optional[Dict[str, dict]] = None - type: Optional[Any] = None - - -def get_field_args_from_resolver( - resolver: Resolver, -) -> Dict[str, GraphQLObjectFieldArg]: - resolver_signature = signature(resolver) - type_hints = resolver.__annotations__ - type_hints.pop("return", None) - - field_args: Dict[str, GraphQLObjectFieldArg] = {} - field_args_start = 0 - - # Fist pass: (arg, *_, something, something) or (arg, *, something, something): - for i, param in enumerate(resolver_signature.parameters.values()): - param_repr = str(param) - if param_repr.startswith("*") and not param_repr.startswith("**"): - field_args_start = i + 1 - break - else: - if len(resolver_signature.parameters) < 2: - raise TypeError( - f"Resolver function '{resolver_signature}' should accept at least " - "'obj' and 'info' positional arguments." - ) - - field_args_start = 2 - - args_parameters = tuple(resolver_signature.parameters.items())[field_args_start:] - if not args_parameters: - return field_args - - for param_name, param in args_parameters: - if param.default != param.empty: - param_default = param.default - else: - param_default = None - - field_args[param_name] = GraphQLObjectFieldArg( - name=convert_python_name_to_graphql(param_name), - out_name=param_name, - type=type_hints.get(param_name), - default_value=param_default, - ) - - return field_args - - -def update_field_args_options( - field_args: Dict[str, GraphQLObjectFieldArg], - args_options: Optional[Dict[str, dict]], -) -> Dict[str, GraphQLObjectFieldArg]: - if not args_options: - return field_args - - updated_args: Dict[str, GraphQLObjectFieldArg] = {} - for arg_name in field_args: - arg_options = args_options.get(arg_name) - if not arg_options: - updated_args[arg_name] = field_args[arg_name] - continue - - args_update = {} - if arg_options.get("name"): - args_update["name"] = arg_options["name"] - if arg_options.get("description"): - args_update["description"] = arg_options["description"] - if arg_options.get("default_value") is not None: - args_update["default_value"] = arg_options["default_value"] - if arg_options.get("type"): - args_update["type"] = arg_options["type"] - - if args_update: - updated_args[arg_name] = replace(field_args[arg_name], **args_update) - else: - updated_args[arg_name] = field_args[arg_name] - - return updated_args diff --git a/tests_next/test_get_field_args_from_resolver.py b/tests_next/test_get_field_args_from_resolver.py index 6e8174c..054fd99 100644 --- a/tests_next/test_get_field_args_from_resolver.py +++ b/tests_next/test_get_field_args_from_resolver.py @@ -1,4 +1,4 @@ -from ariadne_graphql_modules.next.resolver import get_field_args_from_resolver +from ariadne_graphql_modules.next.objecttype import get_field_args_from_resolver def test_field_has_no_args_after_obj_and_info_args(): From 638fe7bb2c56d626c91d1aad1b06766fadcb24cc Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Tue, 16 Apr 2024 11:34:03 +0200 Subject: [PATCH 31/63] Small fixes in typing --- ariadne_graphql_modules/next/objecttype.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index ff803af..580b4f7 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -337,7 +337,7 @@ def get_graphql_object_data( def create_graphql_object_data_without_schema( - cls, + cls: Type[GraphQLObject], ) -> GraphQLObjectData: fields_types: Dict[str, str] = {} fields_names: Dict[str, str] = {} @@ -607,7 +607,7 @@ def get_field_args_from_resolver( def get_field_args_out_names( - field_args: Dict[str, GraphQLObjectFieldArg], + field_args: Dict[str, GraphQLObjectFieldArg] ) -> Dict[str, str]: out_names: Dict[str, str] = {} for field_arg in field_args.values(): From be15b43b1a8d8cd31cc9da66f8de4b6bcbbb24b4 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Tue, 16 Apr 2024 14:02:11 +0200 Subject: [PATCH 32/63] Add more tests to the interface type --- .../snap_test_interface_type_validation.py | 18 +++ tests_next/test_interface_type.py | 146 ++++++++++++++++++ tests_next/test_interface_type_validation.py | 61 ++++++++ 3 files changed, 225 insertions(+) create mode 100644 tests_next/snapshots/snap_test_interface_type_validation.py create mode 100644 tests_next/test_interface_type_validation.py diff --git a/tests_next/snapshots/snap_test_interface_type_validation.py b/tests_next/snapshots/snap_test_interface_type_validation.py new file mode 100644 index 0000000..fc72186 --- /dev/null +++ b/tests_next/snapshots/snap_test_interface_type_validation.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_interface_no_interface_in_schema 1'] = "Unknown type 'BaseInterface'." + +snapshots['test_interface_with_different_types 1'] = '''Query root type must be provided. + +Interface field UserInterface.score expects type String! but User.score is type Int!.''' + +snapshots['test_missing_interface_implementation 1'] = '''Query root type must be provided. + +Interface field RequiredInterface.requiredField expected but Implementing does not provide it.''' diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py index 940751d..5b29321 100644 --- a/tests_next/test_interface_type.py +++ b/tests_next/test_interface_type.py @@ -1,5 +1,7 @@ from typing import List +from graphql import graphql_sync + from ariadne_graphql_modules.next import ( GraphQLID, GraphQLObject, @@ -130,3 +132,147 @@ def search(*_) -> List[UserType | CommentType]: """, ) + + +def test_interface_inheritance(assert_schema_equals): + class BaseEntityInterface(GraphQLInterface): + id: GraphQLID + + class UserInterface(GraphQLInterface): + id: GraphQLID + username: str + + __implements__ = [BaseEntityInterface] + + class UserType(GraphQLObject): + id: GraphQLID + username: str + + __implements__ = [UserInterface, BaseEntityInterface] + + class QueryType(GraphQLObject): + @GraphQLObject.field + def user(*_) -> UserType: + return UserType(id="1", username="test_user") + + schema = make_executable_schema( + QueryType, BaseEntityInterface, UserInterface, UserType + ) + + assert_schema_equals( + schema, + """ + type Query { + user: User! + } + + type User implements UserInterface & BaseEntityInterface { + id: ID! + username: String! + } + + interface UserInterface implements BaseEntityInterface { + id: ID! + username: String! + } + + interface BaseEntityInterface { + id: ID! + } + """, + ) + + +def test_interface_descriptions(assert_schema_equals): + class UserInterface(GraphQLInterface): + summary: str + score: int + + __description__ = "Lorem ipsum." + + class UserType(GraphQLObject): + id: GraphQLID + username: str + summary: str + score: int + + __implements__ = [UserInterface] + + class QueryType(GraphQLObject): + @GraphQLObject.field + def user(*_) -> UserType: + return UserType(id="1", username="test_user") + + schema = make_executable_schema(QueryType, UserType, UserInterface) + + assert_schema_equals( + schema, + """ + type Query { + user: User! + } + + type User implements UserInterface { + id: ID! + username: String! + summary: String! + score: Int! + } + + \"\"\"Lorem ipsum.\"\"\" + interface UserInterface { + summary: String! + score: Int! + } + """, + ) + + +def test_interface_resolvers_and_field_descriptions(assert_schema_equals): + class UserInterface(GraphQLInterface): + summary: str + score: int + + @GraphQLInterface.resolver("score", description="Lorem ipsum.") + def resolve_score(*_): + return 200 + + class UserType(GraphQLObject): + id: GraphQLID + summary: str + score: int + + __implements__ = [UserInterface] + + class QueryType(GraphQLObject): + @GraphQLObject.field + def user(*_) -> UserType: + return UserType(id="1") + + schema = make_executable_schema(QueryType, UserType, UserInterface) + + assert_schema_equals( + schema, + """ + type Query { + user: User! + } + + type User implements UserInterface { + id: ID! + summary: String! + score: Int! + } + + interface UserInterface { + summary: String! + + \"\"\"Lorem ipsum.\"\"\" + score: Int! + } + """, + ) + result = graphql_sync(schema, "{ user { score } }") + + assert not result.errors + assert result.data == {"user": {"score": 200}} diff --git a/tests_next/test_interface_type_validation.py b/tests_next/test_interface_type_validation.py new file mode 100644 index 0000000..bc2f414 --- /dev/null +++ b/tests_next/test_interface_type_validation.py @@ -0,0 +1,61 @@ +import pytest + +from ariadne_graphql_modules.next import ( + GraphQLID, + GraphQLObject, + GraphQLInterface, + make_executable_schema, +) + + +def test_interface_with_different_types(snapshot): + with pytest.raises(TypeError) as exc_info: + + class UserInterface(GraphQLInterface): + summary: str + score: str + + class UserType(GraphQLObject): + name: str + summary: str + score: int + + __implements__ = [UserInterface] + + make_executable_schema(UserType, UserInterface) + + snapshot.assert_match(str(exc_info.value)) + + +def test_missing_interface_implementation(snapshot): + with pytest.raises(TypeError) as exc_info: + + class RequiredInterface(GraphQLInterface): + required_field: str + + class ImplementingType(GraphQLObject): + optional_field: str + + __implements__ = [RequiredInterface] + + make_executable_schema(ImplementingType, RequiredInterface) + + snapshot.assert_match(str(exc_info.value)) + + +def test_interface_no_interface_in_schema(snapshot): + with pytest.raises(TypeError) as exc_info: + + class BaseInterface(GraphQLInterface): + id: GraphQLID + + class UserType(GraphQLObject): + id: GraphQLID + username: str + email: str + + __implements__ = [BaseInterface] + + make_executable_schema(UserType) + + snapshot.assert_match(str(exc_info.value)) From b58ac0531b2780d038ad5466ccb8a1f52f1c6174 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Tue, 16 Apr 2024 14:34:28 +0200 Subject: [PATCH 33/63] Small refactor on interface type --- ariadne_graphql_modules/next/interfacetype.py | 10 +--------- ariadne_graphql_modules/next/objecttype.py | 5 +++-- tests_next/test_interface_type.py | 12 ++++++------ 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ariadne_graphql_modules/next/interfacetype.py b/ariadne_graphql_modules/next/interfacetype.py index be7beb5..2772861 100644 --- a/ariadne_graphql_modules/next/interfacetype.py +++ b/ariadne_graphql_modules/next/interfacetype.py @@ -1,16 +1,10 @@ from dataclasses import dataclass -from enum import Enum from typing import ( Any, Callable, Dict, - Iterable, List, - Optional, - Type, - Union, cast, - Sequence, ) from ariadne import InterfaceType @@ -39,13 +33,11 @@ ) from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .base import GraphQLMetadata, GraphQLModel from .description import get_description_node class GraphQLInterface(GraphQLObject): - __types__: Sequence[Type[GraphQLType]] - __implements__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] __valid_type__ = InterfaceTypeDefinitionNode @classmethod diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 580b4f7..9dcb028 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -44,7 +44,7 @@ class GraphQLObject(GraphQLType): __description__: Optional[str] __aliases__: Optional[Dict[str, str]] __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] - __implements__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] + __implements__: Optional[Iterable[Type[GraphQLType]]] def __init__(self, **kwargs: Any): for kwarg in kwargs: @@ -236,6 +236,7 @@ def __get_graphql_types_with_schema__( ) -> Iterable["GraphQLType"]: types: List[GraphQLType] = [cls] types.extend(getattr(cls, "__requires__", [])) + types.extend(getattr(cls, "__implements__", [])) return types @classmethod @@ -607,7 +608,7 @@ def get_field_args_from_resolver( def get_field_args_out_names( - field_args: Dict[str, GraphQLObjectFieldArg] + field_args: Dict[str, GraphQLObjectFieldArg], ) -> Dict[str, str]: out_names: Dict[str, str] = {} for field_arg in field_args.values(): diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py index 5b29321..163ce19 100644 --- a/tests_next/test_interface_type.py +++ b/tests_next/test_interface_type.py @@ -102,7 +102,7 @@ def search(*_) -> List[UserType | CommentType]: CommentType(id=2, content="Hello World!"), ] - schema = make_executable_schema(QueryType, UserInterface, UserType) + schema = make_executable_schema(QueryType, UserType) assert_schema_equals( schema, @@ -119,17 +119,17 @@ def search(*_) -> List[UserType | CommentType]: summary: String! score: Int! } + + interface UserInterface { + summary: String! + score: Int! + } type Comment { id: ID! content: String! } - interface UserInterface { - summary: String! - score: Int! - } - """, ) From 4a442f731036ef741e4dcd9e4137404fcef33f8b Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Wed, 17 Apr 2024 08:18:06 +0200 Subject: [PATCH 34/63] Add __abstract__ attribute to the interface type --- ariadne_graphql_modules/next/interfacetype.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ariadne_graphql_modules/next/interfacetype.py b/ariadne_graphql_modules/next/interfacetype.py index 2772861..09fc6fb 100644 --- a/ariadne_graphql_modules/next/interfacetype.py +++ b/ariadne_graphql_modules/next/interfacetype.py @@ -38,6 +38,7 @@ class GraphQLInterface(GraphQLObject): + __abstract__: bool = True __valid_type__ = InterfaceTypeDefinitionNode @classmethod From 5b16e1273aca2d704e8b828afb0f48a61b247e4a Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 26 Jul 2024 09:54:11 +0200 Subject: [PATCH 35/63] added subscription model --- ariadne_graphql_modules/next/__init__.py | 3 + ariadne_graphql_modules/next/interfacetype.py | 8 +- ariadne_graphql_modules/next/objecttype.py | 222 +++++- .../next/subscriptiontype.py | 267 +++++++ pyproject.toml | 6 +- .../snap_test_subscription_type_validation.py | 34 + tests_next/test_subscription_type.py | 754 ++++++++++++++++++ .../test_subscription_type_validation.py | 304 +++++++ 8 files changed, 1585 insertions(+), 13 deletions(-) create mode 100644 ariadne_graphql_modules/next/subscriptiontype.py create mode 100644 tests_next/snapshots/snap_test_subscription_type_validation.py create mode 100644 tests_next/test_subscription_type.py create mode 100644 tests_next/test_subscription_type_validation.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 0e4c512..7b113a6 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -21,6 +21,7 @@ from .uniontype import GraphQLUnion, GraphQLUnionModel from .value import get_value_from_node, get_value_node from .interfacetype import GraphQLInterface, GraphQLInterfaceModel +from .subscriptiontype import GraphQLSubscription, GraphQLSubscriptionModel __all__ = [ "GraphQLEnum", @@ -30,6 +31,8 @@ "GraphQLInputModel", "GraphQLInterface", "GraphQLInterfaceModel", + "GraphQLSubscription", + "GraphQLSubscriptionModel", "GraphQLMetadata", "GraphQLModel", "GraphQLObject", diff --git a/ariadne_graphql_modules/next/interfacetype.py b/ariadne_graphql_modules/next/interfacetype.py index 09fc6fb..0d90a22 100644 --- a/ariadne_graphql_modules/next/interfacetype.py +++ b/ariadne_graphql_modules/next/interfacetype.py @@ -75,9 +75,9 @@ def __get_graphql_model_with_schema__( for arg_name, arg_options in final_args.items(): arg_description = get_description_node(arg_options.description) if arg_description: - args_descriptions[cls_attr.field][arg_name] = ( - arg_description - ) + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description arg_default = arg_options.default_value if arg_default is not None: @@ -150,7 +150,7 @@ def __get_graphql_model_without_schema__( if attr_name in type_aliases: aliases[field.name] = type_aliases[attr_name] - elif attr_name != field.name: + elif attr_name != field.name and not field.resolver: aliases[field.name] = attr_name if field.resolver: diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index 9dcb028..cd255f6 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -15,7 +15,7 @@ ) from ariadne import ObjectType as ObjectTypeBindable -from ariadne.types import Resolver +from ariadne.types import Resolver, Subscriber from graphql import ( FieldDefinitionNode, GraphQLField, @@ -117,9 +117,9 @@ def __get_graphql_model_with_schema__( for arg_name, arg_options in final_args.items(): arg_description = get_description_node(arg_options.description) if arg_description: - args_descriptions[cls_attr.field][arg_name] = ( - arg_description - ) + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description arg_default = arg_options.default_value if arg_default is not None: @@ -191,7 +191,7 @@ def __get_graphql_model_without_schema__( if attr_name in type_aliases: aliases[field.name] = type_aliases[attr_name] - elif attr_name != field.name: + elif attr_name != field.name and not field.resolver: aliases[field.name] = attr_name if field.resolver: @@ -345,6 +345,7 @@ def create_graphql_object_data_without_schema( fields_descriptions: Dict[str, str] = {} fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} fields_resolvers: Dict[str, Resolver] = {} + fields_subscribers: Dict[str, Subscriber] = {} fields_defaults: Dict[str, Any] = {} fields_order: List[str] = [] @@ -408,6 +409,18 @@ def create_graphql_object_data_without_schema( if cls_attr.resolver: fields_resolvers[cls_attr.field] = cls_attr.resolver field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args and not fields_args.get(cls_attr.field): + fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + elif isinstance(cls_attr, GraphQLObjectSource): + if cls_attr.type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.type + if cls_attr.description: + fields_descriptions[cls_attr.field] = cls_attr.description + if cls_attr.subscriber: + fields_subscribers[cls_attr.field] = cls_attr.subscriber + field_args = get_field_args_from_subscriber(cls_attr.subscriber) if field_args: fields_args[cls_attr.field] = update_field_args_options( field_args, cls_attr.args @@ -424,6 +437,7 @@ def create_graphql_object_data_without_schema( type=fields_types[field_name], args=fields_args.get(field_name), resolver=fields_resolvers.get(field_name), + subscriber=fields_subscribers.get(field_name), default_value=fields_defaults.get(field_name), ) @@ -436,6 +450,7 @@ class GraphQLObjectField: type: Optional[Any] args: Optional[Dict[str, dict]] resolver: Optional[Resolver] + subscriber: Optional[Subscriber] default_value: Optional[Any] def __init__( @@ -446,6 +461,7 @@ def __init__( type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, resolver: Optional[Resolver] = None, + subscriber: Optional[Subscriber] = None, default_value: Optional[Any] = None, ): self.name = name @@ -453,6 +469,7 @@ def __init__( self.type = type self.args = args self.resolver = resolver + self.subscriber = subscriber self.default_value = default_value def __call__(self, resolver: Resolver): @@ -517,6 +534,37 @@ def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: return object_resolver_factory +@dataclass(frozen=True) +class GraphQLObjectSource: + subscriber: Subscriber + field: str + description: Optional[str] = None + args: Optional[Dict[str, dict]] = None + type: Optional[Any] = None + + +def object_subscriber( + field: str, + type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, + description: Optional[str] = None, +): + def object_subscriber_factory(f: Optional[Subscriber]) -> GraphQLObjectSource: + return GraphQLObjectSource( + args=args, + description=description, + subscriber=f, + field=field, + type=type or get_field_type_from_subscriber(f), + ) + + return object_subscriber_factory + + +def get_field_type_from_subscriber(subscriber: Subscriber) -> Any: + return subscriber.__annotations__.get("return") + + @dataclass(frozen=True) class GraphQLObjectModel(GraphQLModel): resolvers: Dict[str, Resolver] @@ -607,6 +655,51 @@ def get_field_args_from_resolver( return field_args +def get_field_args_from_subscriber( + subscriber: Subscriber, +) -> Dict[str, GraphQLObjectFieldArg]: + subscriber_signature = signature(subscriber) + type_hints = subscriber.__annotations__ + type_hints.pop("return", None) + + field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args_start = 0 + + # Fist pass: (arg, *_, something, something) or (arg, *, something, something): + for i, param in enumerate(subscriber_signature.parameters.values()): + param_repr = str(param) + if param_repr.startswith("*") and not param_repr.startswith("**"): + field_args_start = i + 1 + break + else: + if len(subscriber_signature.parameters) < 2: + raise TypeError( + f"Subscriber function '{subscriber_signature}' should accept at least " + "'obj' and 'info' positional arguments." + ) + + field_args_start = 2 + + args_parameters = tuple(subscriber_signature.parameters.items())[field_args_start:] + if not args_parameters: + return field_args + + for param_name, param in args_parameters: + if param.default != param.empty: + param_default = param.default + else: + param_default = None + + field_args[param_name] = GraphQLObjectFieldArg( + name=convert_python_name_to_graphql(param_name), + out_name=param_name, + type=type_hints.get(param_name), + default_value=param_default, + ) + + return field_args + + def get_field_args_out_names( field_args: Dict[str, GraphQLObjectFieldArg], ) -> Dict[str, str]: @@ -658,7 +751,6 @@ def update_field_args_options( if not arg_options: updated_args[arg_name] = field_args[arg_name] continue - args_update = {} if arg_options.get("name"): args_update["name"] = arg_options["name"] @@ -706,6 +798,7 @@ def validate_object_type_with_schema( } fields_resolvers: List[str] = [] + source_fields: List[str] = [] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -784,6 +877,75 @@ def validate_object_type_with_schema( validate_field_arg_default_value( cls, cls_attr.field, arg_name, arg_obj.default_value ) + if isinstance(cls_attr, GraphQLObjectSource): + if cls_attr.field not in field_names: + valid_fields: str = "', '".join(sorted(field_names)) + raise ValueError( + f"Class '{cls.__name__}' defines source for an undefined " + f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" + ) + + if cls_attr.field in source_fields: + raise ValueError( + f"Class '{cls.__name__}' defines multiple sources for field " + f"'{cls_attr.field}'." + ) + + source_fields.append(cls_attr.field) + + if cls_attr.description and field_definitions[cls_attr.field].description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{cls_attr.field}'." + ) + + if cls_attr.args: + field_args = { + arg.name.value: arg + for arg in field_definitions[cls_attr.field].arguments + } + + for arg_name, arg_options in cls_attr.args.items(): + if arg_name not in field_args: + raise ValueError( + f"Class '{cls.__name__}' defines options for '{arg_name}' " + f"argument of the '{cls_attr.field}' field " + "that doesn't exist." + ) + + if arg_options.get("name"): + raise ValueError( + f"Class '{cls.__name__}' defines 'name' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options.get("type"): + raise ValueError( + f"Class '{cls.__name__}' defines 'type' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if ( + arg_options.get("description") + and field_args[arg_name].description + ): + raise ValueError( + f"Class '{cls.__name__}' defines duplicate descriptions " + f"for '{arg_name}' argument " + f"of the '{cls_attr.field}' field." + ) + + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_options.get("default_value") + ) + + subscriber_args = get_field_args_from_subscriber(cls_attr.subscriber) + for arg_name, arg_obj in subscriber_args.items(): + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_obj.default_value + ) aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} validate_object_aliases(cls, aliases, field_names, fields_resolvers) @@ -825,6 +987,7 @@ def validate_object_type_without_schema(cls: Type[GraphQLObject]) -> Dict[str, A validate_object_resolvers( cls, data.fields_attrs, data.fields_instances, data.resolvers_instances ) + validate_object_subscribers(cls, data.fields_attrs, data.sources_instances) validate_object_fields_args(cls) # Gather names of field attrs with defined resolver @@ -904,6 +1067,45 @@ def validate_object_resolvers( ) +def validate_object_subscribers( + cls: Type[GraphQLObject], + fields_names: List[str], + sources_instances: Dict[str, GraphQLObjectSource], +): + source_fields: List[str] = [] + + for key, source in sources_instances.items(): + if not isinstance(source.field, str): + raise ValueError(f"The field name for {key} must be a string.") + if source.field not in fields_names: + valid_fields: str = "', '".join(sorted(fields_names)) + raise ValueError( + f"Class '{cls.__name__}' defines source for an undefined " + f"field '{source.field}'. (Valid fields: '{valid_fields}')" + ) + if source.field in source_fields: + raise ValueError( + f"Class '{cls.__name__}' defines multiple sources for field " + f"'{source.field}'." + ) + + source_fields.append(source.field) + + if source.description is not None and not isinstance(source.description, str): + raise ValueError(f"The description for {key} must be a string if provided.") + + if source.args is not None: + if not isinstance(source.args, dict): + raise ValueError( + f"The args for {key} must be a dictionary if provided." + ) + for arg_name, arg_info in source.args.items(): + if not isinstance(arg_info, dict): + raise ValueError( + f"Argument {arg_name} for {key} must have a dict as its info." + ) + + def validate_object_fields_args(cls: Type[GraphQLObject]): for field_name in dir(cls): field_instance = getattr(cls, field_name) @@ -984,6 +1186,7 @@ class GraphQLObjectValidationData: fields_attrs: List[str] fields_instances: Dict[str, GraphQLObjectField] resolvers_instances: Dict[str, GraphQLObjectResolver] + sources_instances: Dict[str, GraphQLObjectSource] def get_object_type_validation_data( @@ -995,6 +1198,7 @@ def get_object_type_validation_data( fields_instances: Dict[str, GraphQLObjectField] = {} resolvers_instances: Dict[str, GraphQLObjectResolver] = {} + sources_instances: Dict[str, GraphQLObjectSource] = {} for attr_name in dir(cls): if attr_name.startswith("__"): @@ -1006,6 +1210,11 @@ def get_object_type_validation_data( if attr_name in fields_attrs: fields_attrs.remove(attr_name) + if isinstance(cls_attr, GraphQLObjectSource): + sources_instances[attr_name] = cls_attr + if attr_name in fields_attrs: + fields_attrs.remove(attr_name) + elif isinstance(cls_attr, GraphQLObjectField): fields_instances[attr_name] = cls_attr @@ -1021,6 +1230,7 @@ def get_object_type_validation_data( fields_attrs=fields_attrs, fields_instances=fields_instances, resolvers_instances=resolvers_instances, + sources_instances=sources_instances, ) diff --git a/ariadne_graphql_modules/next/subscriptiontype.py b/ariadne_graphql_modules/next/subscriptiontype.py new file mode 100644 index 0000000..e0f3481 --- /dev/null +++ b/ariadne_graphql_modules/next/subscriptiontype.py @@ -0,0 +1,267 @@ +from dataclasses import dataclass +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + cast, +) + +from ariadne import SubscriptionType +from ariadne.types import Resolver, Subscriber +from graphql import ( + FieldDefinitionNode, + GraphQLField, + GraphQLObjectType, + GraphQLSchema, + InputValueDefinitionNode, + NameNode, + NamedTypeNode, + ObjectTypeDefinitionNode, +) + + +from .value import get_value_node + +from .objecttype import ( + GraphQLObject, + GraphQLObjectResolver, + GraphQLObjectSource, + get_field_args_from_resolver, + get_field_args_from_subscriber, + get_field_args_out_names, + get_field_node_from_obj_field, + get_graphql_object_data, + object_subscriber, + update_field_args_options, +) + +from ..utils import parse_definition +from .base import GraphQLMetadata, GraphQLModel +from .description import get_description_node + + +class GraphQLSubscription(GraphQLObject): + __abstract__: bool = True + __valid_type__ = ObjectTypeDefinitionNode + + @classmethod + def __get_graphql_model_with_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLSubscriptionModel": + definition = cast( + ObjectTypeDefinitionNode, + parse_definition(ObjectTypeDefinitionNode, cls.__schema__), + ) + + descriptions: Dict[str, str] = {} + args_descriptions: Dict[str, Dict[str, str]] = {} + args_defaults: Dict[str, Dict[str, Any]] = {} + resolvers: Dict[str, Resolver] = {} + subscribers: Dict[str, Subscriber] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers[cls_attr.field] = cls_attr.resolver + if cls_attr.description: + descriptions[cls_attr.field] = get_description_node( + cls_attr.description + ) + + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + args_descriptions[cls_attr.field] = {} + args_defaults[cls_attr.field] = {} + + final_args = update_field_args_options(field_args, cls_attr.args) + + for arg_name, arg_options in final_args.items(): + arg_description = get_description_node(arg_options.description) + if arg_description: + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description + + arg_default = arg_options.default_value + if arg_default is not None: + args_defaults[cls_attr.field][arg_name] = get_value_node( + arg_default + ) + if isinstance(cls_attr, GraphQLObjectSource): + subscribers[cls_attr.field] = cls_attr.subscriber + if cls_attr.description: + descriptions[cls_attr.field] = get_description_node( + cls_attr.description + ) + + field_args = get_field_args_from_subscriber(cls_attr.subscriber) + if field_args: + args_descriptions[cls_attr.field] = {} + args_defaults[cls_attr.field] = {} + + final_args = update_field_args_options(field_args, cls_attr.args) + + for arg_name, arg_options in final_args.items(): + arg_description = get_description_node(arg_options.description) + if arg_description: + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description + + arg_default = arg_options.default_value + if arg_default is not None: + args_defaults[cls_attr.field][arg_name] = get_value_node( + arg_default + ) + + fields: List[FieldDefinitionNode] = [] + for field in definition.fields: + field_args_descriptions = args_descriptions.get(field.name.value, {}) + field_args_defaults = args_defaults.get(field.name.value, {}) + + args: List[InputValueDefinitionNode] = [] + for arg in field.arguments: + arg_name = arg.name.value + args.append( + InputValueDefinitionNode( + description=( + arg.description or field_args_descriptions.get(arg_name) + ), + name=arg.name, + directives=arg.directives, + type=arg.type, + default_value=( + arg.default_value or field_args_defaults.get(arg_name) + ), + ) + ) + + fields.append( + FieldDefinitionNode( + name=field.name, + description=( + field.description or descriptions.get(field.name.value) + ), + directives=field.directives, + arguments=tuple(args), + type=field.type, + ) + ) + + return GraphQLSubscriptionModel( + name=definition.name.value, + ast_type=ObjectTypeDefinitionNode, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=definition.name.value), + fields=tuple(fields), + interfaces=definition.interfaces, + ), + resolve_type=cls.resolve_type, + resolvers=resolvers, + subscribers=subscribers, + aliases=getattr(cls, "__aliases__", {}), + out_names=out_names, + ) + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLSubscriptionModel": + type_data = get_graphql_object_data(metadata, cls) + type_aliases = getattr(cls, "__aliases__", None) or {} + + fields_ast: List[FieldDefinitionNode] = [] + resolvers: Dict[str, Resolver] = {} + subscribers: Dict[str, Subscriber] = {} + aliases: Dict[str, str] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for attr_name, field in type_data.fields.items(): + fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) + if attr_name in type_aliases: + aliases[field.name] = type_aliases[attr_name] + elif attr_name != field.name and not field.resolver: + aliases[field.name] = attr_name + + if field.resolver: + resolvers[field.name] = field.resolver + + if field.subscriber: + subscribers[field.name] = field.subscriber + + if field.args: + out_names[field.name] = get_field_args_out_names(field.args) + + interfaces_ast: List[NamedTypeNode] = [] + for interface_name in type_data.interfaces: + interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) + + return GraphQLSubscriptionModel( + name=name, + ast_type=ObjectTypeDefinitionNode, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + fields=tuple(fields_ast), + interfaces=tuple(interfaces_ast), + ), + resolve_type=cls.resolve_type, + resolvers=resolvers, + aliases=aliases, + out_names=out_names, + subscribers=subscribers, + ) + + @staticmethod + def resolve_type(obj: Any, *args) -> str: + if isinstance(obj, GraphQLSubscription): + return obj.__get_graphql_name__() + + raise ValueError( + f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." + ) + + @staticmethod + def source( + field: str, + type: Optional[Any] = None, + args: Optional[Dict[str, dict]] = None, + description: Optional[str] = None, + ): + """Shortcut for object_resolver()""" + return object_subscriber( + args=args, + field=field, + type=type, + description=description, + ) + + +@dataclass(frozen=True) +class GraphQLSubscriptionModel(GraphQLModel): + resolvers: Dict[str, Resolver] + resolve_type: Callable[[Any], Any] + out_names: Dict[str, Dict[str, str]] + aliases: Dict[str, str] + subscribers: Dict[str, Subscriber] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = SubscriptionType() + for field, resolver in self.resolvers.items(): + bindable.set_field(field, resolver) + for alias, target in self.aliases.items(): + bindable.set_alias(alias, target) + for source, generator in self.subscribers.items(): + bindable.set_source(source, generator) + bindable.bind_to_schema(schema) + + graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) + for field_name, field_out_names in self.out_names.items(): + graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) + for arg_name, out_name in field_out_names.items(): + graphql_field.args[arg_name].out_name = out_name diff --git a/pyproject.toml b/pyproject.toml index 99f58df..8703bcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "ariadne>=0.22.0", + "ariadne==0.23.0", ] [project.optional-dependencies] @@ -75,8 +75,8 @@ exclude = ''' ''' [tool.pytest.ini_options] -testpaths = ["tests"] +testpaths = ["tests_next"] asyncio_mode = "strict" [tool.mypy] -plugins = "ariadne_graphql_modules.next.mypy" \ No newline at end of file +plugins = "ariadne_graphql_modules.next.mypy" diff --git a/tests_next/snapshots/snap_test_subscription_type_validation.py b/tests_next/snapshots/snap_test_subscription_type_validation.py new file mode 100644 index 0000000..e260522 --- /dev/null +++ b/tests_next/snapshots/snap_test_subscription_type_validation.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_arg_with_description_in_source_with_schema 1'] = "Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' field. This is not supported for types defining '__schema__'." + +snapshots['test_arg_with_name_in_source_with_schema 1'] = "Class 'SubscriptionType' defines 'name' option for 'channel' argument of the 'messageAdded' field. This is not supported for types defining '__schema__'." + +snapshots['test_arg_with_type_in_source_with_schema 1'] = "Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' field. This is not supported for types defining '__schema__'." + +snapshots['test_description_not_str_without_schema 1'] = 'The description for message_added_generator must be a string if provided.' + +snapshots['test_field_name_not_str_without_schema 1'] = 'The field name for message_added_generator must be a string.' + +snapshots['test_invalid_arg_name_in_source_with_schema 1'] = "Class 'SubscriptionType' defines options for 'channelID' argument of the 'messageAdded' field that doesn't exist." + +snapshots['test_multiple_descriptions_for_source_with_schema 1'] = "Class 'SubscriptionType' defines multiple descriptions for field 'messageAdded'." + +snapshots['test_multiple_sourced_for_field_with_schema 1'] = "Class 'SubscriptionType' defines multiple sources for field 'messageAdded'." + +snapshots['test_multiple_sources_without_schema 1'] = "Class 'SubscriptionType' defines multiple sources for field 'message_added'." + +snapshots['test_source_args_field_arg_not_dict_without_schema 1'] = 'Argument channel for message_added_generator must have a dict as its info.' + +snapshots['test_source_args_not_dict_without_schema 1'] = 'The args for message_added_generator must be a dictionary if provided.' + +snapshots['test_source_for_undefined_field_with_schema 1'] = "Class 'SubscriptionType' defines source for an undefined field 'message_added'. (Valid fields: 'messageAdded')" + +snapshots['test_undefined_name_without_schema 1'] = "Class 'SubscriptionType' defines source for an undefined field 'messageAdded'. (Valid fields: 'message_added')" diff --git a/tests_next/test_subscription_type.py b/tests_next/test_subscription_type.py new file mode 100644 index 0000000..73262e6 --- /dev/null +++ b/tests_next/test_subscription_type.py @@ -0,0 +1,754 @@ +from typing import List + +from ariadne import gql +from graphql import subscribe, parse +import pytest +from ariadne_graphql_modules.next import ( + GraphQLID, + GraphQLObject, + GraphQLSubscription, + GraphQLUnion, + make_executable_schema, +) + + +class Message(GraphQLObject): + id: GraphQLID + content: str + author: str + + +class User(GraphQLObject): + id: GraphQLID + username: str + + +class Notification(GraphQLUnion): + __types__ = [Message, User] + + +@pytest.mark.asyncio +async def test_basic_subscription_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source("message_added") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.resolver("message_added", type=Message) + async def resolve_message_added(message, info): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messageAdded: Message! + } + + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + + query = parse("subscription { messageAdded {id content author} }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message", "author": "Anon"} + } + + +@pytest.mark.asyncio +async def test_subscription_with_arguments_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source( + "message_added", + args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, + ) + async def message_added_generator(obj, info, channel: GraphQLID): + while True: + yield { + "id": "some_id", + "content": f"message_{channel}", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "message_added", + type=Message, + ) + async def resolve_message_added(message, *_, channel: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messageAdded( + \"\"\"Lorem ipsum.\"\"\" + channel: ID! + ): Message! + } + + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + + query = parse('subscription { messageAdded(channel: "123") {id content author} }') + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message_123", "author": "Anon"} + } + + +@pytest.mark.asyncio +async def test_multiple_supscriptions_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + message_added: Message + user_joined: User + + @GraphQLSubscription.source( + "message_added", + args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, + ) + async def message_added_generator(obj, info, channel: GraphQLID): + while True: + yield { + "id": "some_id", + "content": f"message_{channel}", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "message_added", + type=Message, + ) + async def resolve_message_added(message, *_, channel: GraphQLID): + return message + + @GraphQLSubscription.source( + "user_joined", + ) + async def user_joined_generator(obj, info): + while True: + yield { + "id": "some_id", + "username": "username", + } + + @GraphQLSubscription.resolver( + "user_joined", + type=Message, + ) + async def resolve_user_joined(user, *_): + return user + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messageAdded( + \"\"\"Lorem ipsum.\"\"\" + channel: ID! + ): Message! + userJoined: User! + } + + type Message { + id: ID! + content: String! + author: String! + } + + type User { + id: ID! + username: String! + } + """, + ) + + query = parse("subscription { userJoined {id username} }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == {"userJoined": {"id": "some_id", "username": "username"}} + + +@pytest.mark.asyncio +async def test_subscription_with_complex_data_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + messages_in_channel: List[Message] + + @GraphQLSubscription.source( + "messages_in_channel", + args={"channel_id": GraphQLObject.argument(description="Lorem ipsum.")}, + ) + async def message_added_generator(obj, info, channel_id: GraphQLID): + while True: + yield [ + { + "id": "some_id", + "content": f"message_{channel_id}", + "author": "Anon", + } + ] + + @GraphQLSubscription.resolver( + "messages_in_channel", + type=Message, + ) + async def resolve_message_added(message, *_, channel_id: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messagesInChannel( + \"\"\"Lorem ipsum.\"\"\" + channelId: ID! + ): [Message!]! + } + + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + + query = parse( + 'subscription { messagesInChannel(channelId: "123") {id content author} }' + ) + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messagesInChannel": [ + {"id": "some_id", "content": "message_123", "author": "Anon"} + ] + } + + +@pytest.mark.asyncio +async def test_subscription_with_union_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + notification_received: Notification + + @GraphQLSubscription.source( + "notification_received", + ) + async def message_added_generator(obj, info): + while True: + yield Message(id=1, content="content", author="anon") + + @GraphQLSubscription.resolver( + "notification_received", + ) + async def resolve_message_added(message, *_): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + notificationReceived: Notification! + } + + union Notification = Message | User + + type Message { + id: ID! + content: String! + author: String! + } + + type User { + id: ID! + username: String! + } + """, + ) + + query = parse("subscription { notificationReceived { ... on Message { id } } }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == {"notificationReceived": {"id": "1"}} + + +@pytest.mark.asyncio +async def test_basic_subscription_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded: Message! + } + """ + ) + + @GraphQLSubscription.source("messageAdded") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.resolver("messageAdded", type=Message) + async def resolve_message_added(message, info): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Message) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messageAdded: Message! + } + + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + + query = parse("subscription { messageAdded {id content author} }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message", "author": "Anon"} + } + + +@pytest.mark.asyncio +async def test_subscription_with_arguments_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded(channel: ID!): Message! + } + """ + ) + + @GraphQLSubscription.source( + "messageAdded", + ) + async def message_added_generator(obj, info, channel: GraphQLID): + while True: + yield { + "id": "some_id", + "content": f"message_{channel}", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "messageAdded", + type=Message, + ) + async def resolve_message_added(message, *_, channel: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Message) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messageAdded(channel: ID!): Message! + } + + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + + query = parse('subscription { messageAdded(channel: "123") {id content author} }') + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message_123", "author": "Anon"} + } + + +@pytest.mark.asyncio +async def test_multiple_supscriptions_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded: Message! + userJoined: User! + } + """ + ) + + @GraphQLSubscription.source( + "messageAdded", + ) + async def message_added_generator(obj, info): + while True: + yield { + "id": "some_id", + "content": "message", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "messageAdded", + ) + async def resolve_message_added(message, *_): + return message + + @GraphQLSubscription.source( + "userJoined", + ) + async def user_joined_generator(obj, info): + while True: + yield { + "id": "some_id", + "username": "username", + } + + @GraphQLSubscription.resolver( + "userJoined", + ) + async def resolve_user_joined(user, *_): + return user + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Message, User) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messageAdded: Message! + userJoined: User! + } + + type Message { + id: ID! + content: String! + author: String! + } + + type User { + id: ID! + username: String! + } + """, + ) + + query = parse("subscription { userJoined {id username} }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == {"userJoined": {"id": "some_id", "username": "username"}} + + +@pytest.mark.asyncio +async def test_subscription_with_complex_data_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messagesInChannel(channelId: ID!): [Message!]! + } + """ + ) + + @GraphQLSubscription.source( + "messagesInChannel", + ) + async def message_added_generator(obj, info, channelId: GraphQLID): + while True: + yield [ + { + "id": "some_id", + "content": f"message_{channelId}", + "author": "Anon", + } + ] + + @GraphQLSubscription.resolver( + "messagesInChannel", + ) + async def resolve_message_added(message, *_, channelId: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Message) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messagesInChannel(channelId: ID!): [Message!]! + } + + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + + query = parse( + 'subscription { messagesInChannel(channelId: "123") {id content author} }' + ) + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messagesInChannel": [ + {"id": "some_id", "content": "message_123", "author": "Anon"} + ] + } + + +@pytest.mark.asyncio +async def test_subscription_with_union_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + notificationReceived: Notification! + } + """ + ) + + @GraphQLSubscription.source( + "notificationReceived", + ) + async def message_added_generator(obj, info): + while True: + yield Message(id=1, content="content", author="anon") + + @GraphQLSubscription.resolver( + "notificationReceived", + ) + async def resolve_message_added(message, *_): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Notification) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + notificationReceived: Notification! + } + + union Notification = Message | User + + type Message { + id: ID! + content: String! + author: String! + } + + type User { + id: ID! + username: String! + } + """, + ) + + query = parse("subscription { notificationReceived { ... on Message { id } } }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == {"notificationReceived": {"id": "1"}} diff --git a/tests_next/test_subscription_type_validation.py b/tests_next/test_subscription_type_validation.py new file mode 100644 index 0000000..14a88e5 --- /dev/null +++ b/tests_next/test_subscription_type_validation.py @@ -0,0 +1,304 @@ +from ariadne import gql +import pytest +from ariadne_graphql_modules.next import ( + GraphQLID, + GraphQLObject, + GraphQLSubscription, + GraphQLUnion, +) + + +class Message(GraphQLObject): + id: GraphQLID + content: str + author: str + + +class User(GraphQLObject): + id: GraphQLID + username: str + + +class Notification(GraphQLUnion): + __types__ = [Message, User] + + +@pytest.mark.asyncio +async def test_undefined_name_without_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source("messageAdded") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_field_name_not_str_without_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source(23) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_multiple_sources_without_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source("message_added") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.source("message_added") + async def message_added_generator_2(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_description_not_str_without_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source("message_added", description=12) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_source_args_field_arg_not_dict_without_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source( + "message_added", + args={"channel": 123}, + ) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_source_args_not_dict_without_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source( + "message_added", + args=123, + ) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_source_for_undefined_field_with_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded: Message! + } + """ + ) + + @GraphQLSubscription.source("message_added") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_multiple_sourced_for_field_with_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded: Message! + } + """ + ) + + @GraphQLSubscription.source("messageAdded") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.source("messageAdded") + async def message_added_generator_2(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_multiple_descriptions_for_source_with_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + \"\"\"Hello!\"\"\" + messageAdded: Message! + } + """ + ) + + @GraphQLSubscription.source("messageAdded", description="hello") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_invalid_arg_name_in_source_with_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded(channel: ID!): Message! + } + """ + ) + + @GraphQLSubscription.source( + "messageAdded", + args={"channelID": GraphQLObject.argument(description="Lorem ipsum.")}, + ) + async def message_added_generator(obj, info, channel): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_arg_with_name_in_source_with_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded(channel: ID!): Message! + } + """ + ) + + @GraphQLSubscription.source( + "messageAdded", + args={ + "channel": GraphQLObject.argument( + name="channelID", description="Lorem ipsum." + ) + }, + ) + async def message_added_generator(obj, info, channel): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_arg_with_type_in_source_with_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded(channel: ID!): Message! + } + """ + ) + + @GraphQLSubscription.source( + "messageAdded", + args={ + "channel": GraphQLObject.argument( + type=str, description="Lorem ipsum." + ) + }, + ) + async def message_added_generator(obj, info, channel): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) + + +@pytest.mark.asyncio +async def test_arg_with_description_in_source_with_schema(snapshot): + with pytest.raises(ValueError) as exc_info: + + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded( + \"\"\"Lorem ipsum.\"\"\" + channel: ID! + ): Message! + } + """ + ) + + @GraphQLSubscription.source( + "messageAdded", + args={ + "channel": GraphQLObject.argument( + type=str, description="Lorem ipsum." + ) + }, + ) + async def message_added_generator(obj, info, channel): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + snapshot.assert_match(str(exc_info.value)) From 7cc8a54ed9afbf966f0b841c403d736f402345b8 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 12 Aug 2024 20:20:35 +0200 Subject: [PATCH 36/63] fixed-pylint-issues --- ariadne_graphql_modules/next/base.py | 60 +++++----- ariadne_graphql_modules/next/deferredtype.py | 2 +- ariadne_graphql_modules/next/enumtype.py | 105 +++++++++--------- .../next/executable_schema.py | 2 +- ariadne_graphql_modules/next/inputtype.py | 49 ++++---- ariadne_graphql_modules/next/interfacetype.py | 10 +- ariadne_graphql_modules/next/mypy.py | 27 ++++- ariadne_graphql_modules/next/objecttype.py | 90 ++++++++------- ariadne_graphql_modules/next/scalartype.py | 13 +-- ariadne_graphql_modules/next/sort.py | 2 +- .../next/subscriptiontype.py | 10 +- ariadne_graphql_modules/next/uniontype.py | 21 ++-- ariadne_graphql_modules/utils.py | 2 +- .../test_get_field_args_from_resolver.py | 24 ++-- tests_next/test_input_type.py | 2 +- tests_next/test_interface_type.py | 4 +- tests_next/test_make_executable_schema.py | 4 +- tests_next/test_object_type.py | 4 +- tests_next/test_object_type_validation.py | 2 +- tests_next/test_scalar_type.py | 10 +- tests_next/test_subscription_type.py | 34 +++--- .../test_subscription_type_validation.py | 4 +- tests_next/test_union_type.py | 10 +- 23 files changed, 247 insertions(+), 244 deletions(-) diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index e8d02dd..7710db8 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -47,9 +47,7 @@ def __get_graphql_model__(cls, metadata: "GraphQLMetadata") -> "GraphQLModel": ) @classmethod - def __get_graphql_types__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: + def __get_graphql_types__(cls, _: "GraphQLMetadata") -> Iterable["GraphQLType"]: """Returns iterable with GraphQL types associated with this type""" return [cls] @@ -60,10 +58,6 @@ class GraphQLModel: ast: TypeDefinitionNode ast_type: Type[TypeDefinitionNode] - def __init__(self, name: str, ast: TypeDefinitionNode): - self.name = name - self.ast = ast - def bind_to_schema(self, schema: GraphQLSchema): pass @@ -76,37 +70,45 @@ class GraphQLMetadata: default_factory=dict ) - def get_data(self, type: Union[Type[GraphQLType], Type[Enum]]) -> Any: + def get_data(self, graphql_type: Union[Type[GraphQLType], Type[Enum]]) -> Any: try: - return self.data[type] + return self.data[graphql_type] except KeyError as e: - raise KeyError(f"No data is set for '{type}'.") from e + raise KeyError(f"No data is set for '{graphql_type}'.") from e - def set_data(self, type: Union[Type[GraphQLType], Type[Enum]], data: Any) -> Any: - self.data[type] = data + def set_data( + self, graphql_type: Union[Type[GraphQLType], Type[Enum]], data: Any + ) -> Any: + self.data[graphql_type] = data return data def get_graphql_model( - self, type: Union[Type[GraphQLType], Type[Enum]] + self, graphql_type: Union[Type[GraphQLType], Type[Enum]] ) -> GraphQLModel: - if type not in self.models: - if hasattr(type, "__get_graphql_model__"): - self.models[type] = type.__get_graphql_model__(self) - elif issubclass(type, Enum): - from .enumtype import create_graphql_enum_model - - self.models[type] = create_graphql_enum_model(type) + if graphql_type not in self.models: + if hasattr(graphql_type, "__get_graphql_model__"): + self.models[graphql_type] = graphql_type.__get_graphql_model__(self) + elif issubclass(graphql_type, Enum): + from .enumtype import ( # pylint: disable=R0401,C0415 + create_graphql_enum_model, + ) + + self.models[graphql_type] = create_graphql_enum_model(graphql_type) else: - raise ValueError(f"Can't retrieve GraphQL model for '{type}'.") + raise ValueError(f"Can't retrieve GraphQL model for '{graphql_type}'.") - return self.models[type] + return self.models[graphql_type] - def set_graphql_name(self, type: Union[Type[GraphQLType], Type[Enum]], name: str): - self.names[type] = name + def set_graphql_name( + self, graphql_type: Union[Type[GraphQLType], Type[Enum]], name: str + ): + self.names[graphql_type] = name - def get_graphql_name(self, type: Union[Type[GraphQLType], Type[Enum]]) -> str: - if type not in self.names: - model = self.get_graphql_model(type) - self.set_graphql_name(type, model.name) + def get_graphql_name( + self, graphql_type: Union[Type[GraphQLType], Type[Enum]] + ) -> str: + if graphql_type not in self.names: + model = self.get_graphql_model(graphql_type) + self.set_graphql_name(graphql_type, model.name) - return self.names[type] + return self.names[graphql_type] diff --git a/ariadne_graphql_modules/next/deferredtype.py b/ariadne_graphql_modules/next/deferredtype.py index 31f1aeb..d9564d6 100644 --- a/ariadne_graphql_modules/next/deferredtype.py +++ b/ariadne_graphql_modules/next/deferredtype.py @@ -12,7 +12,7 @@ def deferred(module_path: str) -> DeferredTypeData: if not module_path.startswith("."): return DeferredTypeData(module_path) - frame = sys._getframe(2) + frame = sys._getframe(2) # pylint: disable=protected-access if not frame: raise RuntimeError( "'deferred' can't be called outside of class's attribute's " diff --git a/ariadne_graphql_modules/next/enumtype.py b/ariadne_graphql_modules/next/enumtype.py index 15b4a64..5e17403 100644 --- a/ariadne_graphql_modules/next/enumtype.py +++ b/ariadne_graphql_modules/next/enumtype.py @@ -42,22 +42,22 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(metadata, name) + return cls.__get_graphql_model_with_schema__(name) - return cls.__get_graphql_model_without_schema__(metadata, name) + return cls.__get_graphql_model_without_schema__(name) + # pylint: disable=E1101 @classmethod - def __get_graphql_model_with_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLEnumModel": + def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLEnumModel": definition = cast( EnumTypeDefinitionNode, parse_definition(EnumTypeDefinitionNode, cls.__schema__), ) - members = getattr(cls, "__members__", None) + members = getattr(cls, "__members__", []) + members_values: Dict[str, Any] = {} if isinstance(members, dict): - members_values = {key: value for key, value in members.items()} + members_values = dict(members.items()) elif isclass(members) and issubclass(members, Enum): members_values = {i.name: i for i in members} else: @@ -91,13 +91,11 @@ def __get_graphql_model_with_schema__( ) @classmethod - def __get_graphql_model_without_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLEnumModel": - members = getattr(cls, "__members__", None) - + def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLEnumModel": + members = getattr(cls, "__members__", []) + members_values = {} if isinstance(members, dict): - members_values = {key: value for key, value in members.items()} + members_values = dict(members.items()) elif isclass(members) and issubclass(members, Enum): members_values = {i.name: i for i in members} elif isinstance(members, list): @@ -138,7 +136,7 @@ def create_graphql_enum_model( ) -> "GraphQLEnumModel": if members_include and members_exclude: raise ValueError( - "'members_include' and 'members_exclude' " "options are mutually exclusive." + "'members_include' and 'members_exclude' options are mutually exclusive." ) if hasattr(enum, "__get_graphql_model__"): @@ -233,21 +231,21 @@ def __get_graphql_model__(*_) -> GraphQLEnumModel: return graphql_enum_decorator +# pylint: disable=E1101 def validate_enum_type_with_schema(cls: Type[GraphQLEnum]): definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) if not isinstance(definition, EnumTypeDefinitionNode): raise ValueError( f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{EnumTypeDefinitionNode.__name__}')" + f"with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != '{EnumTypeDefinitionNode.__name__}')" ) validate_name(cls, definition) validate_description(cls, definition) - members_names = set(value.name.value for value in definition.values) + members_names = {value.name.value for value in definition.values} if not members_names: raise ValueError( f"Class '{cls.__name__}' defines '__schema__' attribute " @@ -256,47 +254,45 @@ def validate_enum_type_with_schema(cls: Type[GraphQLEnum]): members_values = getattr(cls, "__members__", None) if members_values: - if isinstance(members_values, list): - raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute " - "can't be a list when used together with '__schema__'." - ) - - missing_members: Optional[List[str]] = None - if isinstance(members_values, dict): - missing_members = members_names - set(members_values) - if isclass(members_values) and issubclass(members_values, Enum): - missing_members = members_names - set( - value.name for value in members_values - ) - - if missing_members: - missing_members_str = "', '".join(missing_members) - raise ValueError( - f"Class '{cls.__name__}' '__members__' is missing values " - "for enum members defined in '__schema__'. " - f"(missing items: '{missing_members_str}')" - ) + validate_members_values(cls, members_values, members_names) members_descriptions = getattr(cls, "__members_descriptions__", {}) validate_enum_members_descriptions(cls, members_names, members_descriptions) - duplicate_descriptions: List[str] = [] - for ast_member in definition.values: - member_name = ast_member.name.value - if ( - ast_member.description - and ast_member.description.value - and members_descriptions.get(member_name) - ): - duplicate_descriptions.append(member_name) + duplicate_descriptions = [ + ast_member.name.value + for ast_member in definition.values + if ast_member.description + and ast_member.description.value + and members_descriptions.get(ast_member.name.value) + ] if duplicate_descriptions: - duplicate_descriptions_str = "', '".join(duplicate_descriptions) raise ValueError( f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " f"descriptions for enum members that also have description in '__schema__' " - f"attribute. (members: '{duplicate_descriptions_str}')" + f"attribute. (members: '{', '.join(duplicate_descriptions)}')" + ) + + +def validate_members_values(cls, members_values, members_names): + if isinstance(members_values, list): + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute " + "can't be a list when used together with '__schema__'." + ) + + missing_members = None + if isinstance(members_values, dict): + missing_members = members_names - set(members_values) + elif isclass(members_values) and issubclass(members_values, Enum): + missing_members = members_names - {value.name for value in members_values} + + if missing_members: + raise ValueError( + f"Class '{cls.__name__}' '__members__' is missing values " + f"for enum members defined in '__schema__'. " + f"(missing items: '{', '.join(missing_members)}')" ) @@ -309,10 +305,11 @@ def validate_enum_type(cls: Type[GraphQLEnum]): "the '__schema__' attribute." ) - if not ( - isinstance(members_values, dict) - or isinstance(members_values, list) - or (isclass(members_values) and issubclass(members_values, Enum)) + if not any( + [ + isinstance(members_values, (dict, list)), + isclass(members_values) and issubclass(members_values, Enum), + ] ): raise ValueError( f"Class '{cls.__name__}' '__members__' attribute is of unsupported type. " diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index 660670d..7444956 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -157,7 +157,7 @@ def flatten_schema_types( for type_def in types: if isinstance(type_def, str): continue - elif isinstance(type_def, list): + if isinstance(type_def, list): flat_list += flatten_schema_types(type_def, metadata, dedupe=False) elif isinstance(type_def, SchemaBindable): flat_list.append(type_def) diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/inputtype.py index 3cd5a8d..c91e326 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/inputtype.py @@ -24,6 +24,7 @@ class GraphQLInput(GraphQLType): __kwargs__: Dict[str, Any] + __schema__: Optional[str] __out_names__: Optional[Dict[str, str]] = None def __init__(self, **kwargs: Any): @@ -62,14 +63,12 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": metadata.set_graphql_name(cls, name) if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(metadata, name) + return cls.__get_graphql_model_with_schema__() return cls.__get_graphql_model_without_schema__(metadata, name) @classmethod - def __get_graphql_model_with_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLInputModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLInputModel": definition = cast( InputObjectTypeDefinitionNode, parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), @@ -111,12 +110,11 @@ def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLInputModel": type_hints = cls.__annotations__ - fields_instances: Dict[str, GraphQLInputField] = {} - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLInputField): - fields_instances[attr_name] = cls_attr + fields_instances: Dict[str, GraphQLInputField] = { + attr_name: getattr(cls, attr_name) + for attr_name in dir(cls) + if isinstance(getattr(cls, attr_name), GraphQLInputField) + } fields_ast: List[InputValueDefinitionNode] = [] out_names: Dict[str, str] = {} @@ -127,13 +125,14 @@ def __get_graphql_model_without_schema__( cls_attr = getattr(cls, hint_name, None) default_name = convert_python_name_to_graphql(hint_name) + if isinstance(cls_attr, GraphQLInputField): fields_ast.append( get_field_node_from_type_hint( cls, metadata, cls_attr.name or default_name, - cls_attr.type or hint_type, + cls_attr.graphql_type or hint_type, cls_attr.description, cls_attr.default_value, ) @@ -153,19 +152,19 @@ def __get_graphql_model_without_schema__( ) out_names[default_name] = hint_name - for attr_name, field_instance in fields_instances: - default_name = convert_python_name_to_graphql(hint_name) + for attr_name, field_instance in fields_instances.items(): + default_name = convert_python_name_to_graphql(attr_name) fields_ast.append( get_field_node_from_type_hint( cls, metadata, field_instance.name or default_name, - field_instance.type, + field_instance.graphql_type, field_instance.description, field_instance.default_value, ) ) - out_names[cls_attr.name or default_name] = hint_name + out_names[field_instance.name or default_name] = attr_name return GraphQLInputModel( name=name, @@ -182,17 +181,15 @@ def __get_graphql_model_without_schema__( ) @classmethod - def __get_graphql_types__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: + def __get_graphql_types__(cls, _: "GraphQLMetadata") -> Iterable["GraphQLType"]: """Returns iterable with GraphQL types associated with this type""" types: List[GraphQLType] = [cls] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLInputField): - if cls_attr.type: - field_graphql_type = get_graphql_type(cls_attr.type) + if cls_attr.graphql_type: + field_graphql_type = get_graphql_type(cls_attr.graphql_type) if field_graphql_type and field_graphql_type not in types: types.append(field_graphql_type) @@ -211,14 +208,14 @@ def __get_graphql_types__( def field( *, name: Optional[str] = None, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, description: Optional[str] = None, default_value: Optional[Any] = None, - ): + ) -> Any: """Shortcut for GraphQLInputField()""" return GraphQLInputField( name=name, - type=type, + graphql_type=graphql_type, description=description, default_value=default_value, ) @@ -227,7 +224,7 @@ def field( class GraphQLInputField: name: Optional[str] description: Optional[str] - type: Optional[Any] + graphql_type: Optional[Any] default_value: Optional[Any] def __init__( @@ -235,12 +232,12 @@ def __init__( *, name: Optional[str] = None, description: Optional[str] = None, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, default_value: Optional[Any] = None, ): self.name = name self.description = description - self.type = type + self.graphql_type = graphql_type self.default_value = default_value diff --git a/ariadne_graphql_modules/next/interfacetype.py b/ariadne_graphql_modules/next/interfacetype.py index 0d90a22..a7def02 100644 --- a/ariadne_graphql_modules/next/interfacetype.py +++ b/ariadne_graphql_modules/next/interfacetype.py @@ -42,9 +42,7 @@ class GraphQLInterface(GraphQLObject): __valid_type__ = InterfaceTypeDefinitionNode @classmethod - def __get_graphql_model_with_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLInterfaceModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLInterfaceModel": definition = cast( InterfaceTypeDefinitionNode, parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), @@ -153,10 +151,10 @@ def __get_graphql_model_without_schema__( elif attr_name != field.name and not field.resolver: aliases[field.name] = attr_name - if field.resolver: + if field.resolver and field.name: resolvers[field.name] = field.resolver - if field.args: + if field.args and field.name: out_names[field.name] = get_field_args_out_names(field.args) interfaces_ast: List[NamedTypeNode] = [] @@ -181,7 +179,7 @@ def __get_graphql_model_without_schema__( ) @staticmethod - def resolve_type(obj: Any, *args) -> str: + def resolve_type(obj: Any, *_) -> str: if isinstance(obj, GraphQLInterface): return obj.__get_graphql_name__() diff --git a/ariadne_graphql_modules/next/mypy.py b/ariadne_graphql_modules/next/mypy.py index 0cee595..bb96d64 100644 --- a/ariadne_graphql_modules/next/mypy.py +++ b/ariadne_graphql_modules/next/mypy.py @@ -1,10 +1,29 @@ -from mypy.plugin import Plugin +from mypy.plugin import Plugin, MethodContext # pylint: disable=E0611 +from mypy.types import Instance # pylint: disable=E0611 +from mypy.nodes import EllipsisExpr # pylint: disable=E0611 class AriadneGraphQLModulesPlugin(Plugin): - def get_type_analyze_hook(self, fullname: str): - print(fullname) + def get_method_hook(self, fullname: str): + if "GraphQL" in fullname and fullname.endswith(".field"): + return self.transform_graphql_object + return None + def transform_graphql_object(self, ctx: MethodContext): + default_any_type = ctx.default_return_type -def plugin(version: str): + default_type_arg = ctx.args[2] + if default_type_arg: + default_type = ctx.arg_types[2][0] + default_arg = default_type_arg[0] + + # Fallback to default Any type if the field is required + if not isinstance(default_arg, EllipsisExpr): + if isinstance(default_type, Instance): + return default_type + + return default_any_type + + +def plugin(_): return AriadneGraphQLModulesPlugin diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py index cd255f6..fedb7fa 100644 --- a/ariadne_graphql_modules/next/objecttype.py +++ b/ariadne_graphql_modules/next/objecttype.py @@ -1,3 +1,4 @@ +# pylint: disable=C0302 from copy import deepcopy from dataclasses import dataclass, replace from enum import Enum @@ -79,14 +80,12 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": metadata.set_graphql_name(cls, name) if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(metadata, name) + return cls.__get_graphql_model_with_schema__() return cls.__get_graphql_model_without_schema__(metadata, name) @classmethod - def __get_graphql_model_with_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLObjectModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLObjectModel": definition = cast( ObjectTypeDefinitionNode, parse_definition(ObjectTypeDefinitionNode, cls.__schema__), @@ -232,7 +231,7 @@ def __get_graphql_types__( @classmethod def __get_graphql_types_with_schema__( - cls, metadata: "GraphQLMetadata" + cls, _: "GraphQLMetadata" ) -> Iterable["GraphQLType"]: types: List[GraphQLType] = [cls] types.extend(getattr(cls, "__requires__", [])) @@ -247,13 +246,13 @@ def __get_graphql_types_without_schema__( type_data = get_graphql_object_data(metadata, cls) for field in type_data.fields.values(): - field_type = get_graphql_type(field.type) + field_type = get_graphql_type(field.field_type) if field_type and field_type not in types: types.append(field_type) if field.args: for field_arg in field.args.values(): - field_arg_type = get_graphql_type(field_arg.type) + field_arg_type = get_graphql_type(field_arg.field_type) if field_arg_type and field_arg_type not in types: types.append(field_arg_type) @@ -264,17 +263,17 @@ def field( f: Optional[Resolver] = None, *, name: Optional[str] = None, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, description: Optional[str] = None, default_value: Optional[Any] = None, - ): + ) -> Any: """Shortcut for object_field()""" return object_field( f, args=args, name=name, - type=type, + graphql_type=graphql_type, description=description, default_value=default_value, ) @@ -282,7 +281,7 @@ def field( @staticmethod def resolver( field: str, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, description: Optional[str] = None, ): @@ -290,7 +289,7 @@ def resolver( return object_resolver( args=args, field=field, - type=type, + graphql_type=graphql_type, description=description, ) @@ -298,7 +297,7 @@ def resolver( def argument( name: Optional[str] = None, description: Optional[str] = None, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, default_value: Optional[Any] = None, ) -> dict: options: dict = {} @@ -306,8 +305,8 @@ def argument( options["name"] = name if description: options["description"] = description - if type: - options["type"] = type + if graphql_type: + options["type"] = graphql_type if default_value: options["default_value"] = default_value return options @@ -324,14 +323,13 @@ def get_graphql_object_data( ) -> GraphQLObjectData: try: return metadata.get_data(cls) - except KeyError: + except KeyError as exc: if getattr(cls, "__schema__", None): raise NotImplementedError( "'get_graphql_object_data' is not supported for " "objects with '__schema__'." - ) - else: - object_data = create_graphql_object_data_without_schema(cls) + ) from exc + object_data = create_graphql_object_data_without_schema(cls) metadata.set_data(cls, object_data) return object_data @@ -387,8 +385,8 @@ def create_graphql_object_data_without_schema( attr_name ) - if cls_attr.type and attr_name not in fields_types: - fields_types[attr_name] = cls_attr.type + if cls_attr.field_type: + fields_types[attr_name] = cls_attr.field_type if cls_attr.description: fields_descriptions[attr_name] = cls_attr.description if cls_attr.resolver: @@ -402,8 +400,8 @@ def create_graphql_object_data_without_schema( fields_defaults[attr_name] = cls_attr.default_value elif isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.type + if cls_attr.field_type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.field_type if cls_attr.description: fields_descriptions[cls_attr.field] = cls_attr.description if cls_attr.resolver: @@ -414,8 +412,8 @@ def create_graphql_object_data_without_schema( field_args, cls_attr.args ) elif isinstance(cls_attr, GraphQLObjectSource): - if cls_attr.type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.type + if cls_attr.field_type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.field_type if cls_attr.description: fields_descriptions[cls_attr.field] = cls_attr.description if cls_attr.subscriber: @@ -434,7 +432,7 @@ def create_graphql_object_data_without_schema( fields[field_name] = GraphQLObjectField( name=fields_names[field_name], description=fields_descriptions.get(field_name), - type=fields_types[field_name], + field_type=fields_types[field_name], args=fields_args.get(field_name), resolver=fields_resolvers.get(field_name), subscriber=fields_subscribers.get(field_name), @@ -447,7 +445,7 @@ def create_graphql_object_data_without_schema( class GraphQLObjectField: name: Optional[str] description: Optional[str] - type: Optional[Any] + field_type: Optional[Any] args: Optional[Dict[str, dict]] resolver: Optional[Resolver] subscriber: Optional[Subscriber] @@ -458,7 +456,7 @@ def __init__( *, name: Optional[str] = None, description: Optional[str] = None, - type: Optional[Any] = None, + field_type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, resolver: Optional[Resolver] = None, subscriber: Optional[Subscriber] = None, @@ -466,7 +464,7 @@ def __init__( ): self.name = name self.description = description - self.type = type + self.field_type = field_type self.args = args self.resolver = resolver self.subscriber = subscriber @@ -475,8 +473,8 @@ def __init__( def __call__(self, resolver: Resolver): """Makes GraphQLObjectField instances work as decorators.""" self.resolver = resolver - if not self.type: - self.type = get_field_type_from_resolver(resolver) + if not self.field_type: + self.field_type = get_field_type_from_resolver(resolver) return self @@ -486,17 +484,17 @@ def object_field( args: Optional[Dict[str, dict]] = None, name: Optional[str] = None, description: Optional[str] = None, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, default_value: Optional[Any] = None, ) -> GraphQLObjectField: - field_type: Any = type - if not type and resolver: + field_type: Any = graphql_type + if not graphql_type and resolver: field_type = get_field_type_from_resolver(resolver) return GraphQLObjectField( name=name, description=description, - type=field_type, + field_type=field_type, args=args, resolver=resolver, default_value=default_value, @@ -513,12 +511,12 @@ class GraphQLObjectResolver: field: str description: Optional[str] = None args: Optional[Dict[str, dict]] = None - type: Optional[Any] = None + field_type: Optional[Any] = None def object_resolver( field: str, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, description: Optional[str] = None, ): @@ -528,7 +526,7 @@ def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: description=description, resolver=f, field=field, - type=type or get_field_type_from_resolver(f), + field_type=graphql_type or get_field_type_from_resolver(f), ) return object_resolver_factory @@ -540,12 +538,12 @@ class GraphQLObjectSource: field: str description: Optional[str] = None args: Optional[Dict[str, dict]] = None - type: Optional[Any] = None + field_type: Optional[Any] = None def object_subscriber( field: str, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, description: Optional[str] = None, ): @@ -555,7 +553,7 @@ def object_subscriber_factory(f: Optional[Subscriber]) -> GraphQLObjectSource: description=description, subscriber=f, field=field, - type=type or get_field_type_from_subscriber(f), + field_type=graphql_type or get_field_type_from_subscriber(f), ) return object_subscriber_factory @@ -596,7 +594,7 @@ def get_field_node_from_obj_field( return FieldDefinitionNode( description=get_description_node(field.description), name=NameNode(value=field.name), - type=get_type_node(metadata, field.type, parent_type), + type=get_type_node(metadata, field.field_type, parent_type), arguments=get_field_args_nodes_from_obj_field_args(metadata, field.args), ) @@ -605,7 +603,7 @@ def get_field_node_from_obj_field( class GraphQLObjectFieldArg: name: Optional[str] out_name: Optional[str] - type: Optional[Any] + field_type: Optional[Any] description: Optional[str] = None default_value: Optional[Any] = None @@ -648,7 +646,7 @@ def get_field_args_from_resolver( field_args[param_name] = GraphQLObjectFieldArg( name=convert_python_name_to_graphql(param_name), out_name=param_name, - type=type_hints.get(param_name), + field_type=type_hints.get(param_name), default_value=param_default, ) @@ -693,7 +691,7 @@ def get_field_args_from_subscriber( field_args[param_name] = GraphQLObjectFieldArg( name=convert_python_name_to_graphql(param_name), out_name=param_name, - type=type_hints.get(param_name), + field_type=type_hints.get(param_name), default_value=param_default, ) @@ -733,7 +731,7 @@ def get_field_arg_node_from_obj_field_arg( return InputValueDefinitionNode( description=get_description_node(field_arg.description), name=NameNode(value=field_arg.name), - type=get_type_node(metadata, field_arg.type), + type=get_type_node(metadata, field_arg.field_type), default_value=default_value, ) diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/scalartype.py index 98327c5..1db9eb5 100644 --- a/ariadne_graphql_modules/next/scalartype.py +++ b/ariadne_graphql_modules/next/scalartype.py @@ -20,6 +20,7 @@ class GraphQLScalar(GraphQLType, Generic[T]): __abstract__: bool = True + __schema__: Optional[str] wrapped_value: T @@ -42,14 +43,12 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(metadata, name) + return cls.__get_graphql_model_with_schema__() - return cls.__get_graphql_model_without_schema__(metadata, name) + return cls.__get_graphql_model_without_schema__(name) @classmethod - def __get_graphql_model_with_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": definition = cast( ScalarTypeDefinitionNode, parse_definition(ScalarTypeDefinitionNode, cls.__schema__), @@ -65,9 +64,7 @@ def __get_graphql_model_with_schema__( ) @classmethod - def __get_graphql_model_without_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLModel": + def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": return GraphQLScalarModel( name=name, ast_type=ScalarTypeDefinitionNode, diff --git a/ariadne_graphql_modules/next/sort.py b/ariadne_graphql_modules/next/sort.py index ae9b68c..ffe35a8 100644 --- a/ariadne_graphql_modules/next/sort.py +++ b/ariadne_graphql_modules/next/sort.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Union, cast +from typing import Dict, List, Union, cast from graphql import ( DirectiveNode, diff --git a/ariadne_graphql_modules/next/subscriptiontype.py b/ariadne_graphql_modules/next/subscriptiontype.py index e0f3481..99f339c 100644 --- a/ariadne_graphql_modules/next/subscriptiontype.py +++ b/ariadne_graphql_modules/next/subscriptiontype.py @@ -47,9 +47,7 @@ class GraphQLSubscription(GraphQLObject): __valid_type__ = ObjectTypeDefinitionNode @classmethod - def __get_graphql_model_with_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLSubscriptionModel": + def __get_graphql_model_with_schema__(cls, *_) -> "GraphQLSubscriptionModel": definition = cast( ObjectTypeDefinitionNode, parse_definition(ObjectTypeDefinitionNode, cls.__schema__), @@ -218,7 +216,7 @@ def __get_graphql_model_without_schema__( ) @staticmethod - def resolve_type(obj: Any, *args) -> str: + def resolve_type(obj: Any, *_) -> str: if isinstance(obj, GraphQLSubscription): return obj.__get_graphql_name__() @@ -229,7 +227,7 @@ def resolve_type(obj: Any, *args) -> str: @staticmethod def source( field: str, - type: Optional[Any] = None, + graphql_type: Optional[Any] = None, args: Optional[Dict[str, dict]] = None, description: Optional[str] = None, ): @@ -237,7 +235,7 @@ def source( return object_subscriber( args=args, field=field, - type=type, + graphql_type=graphql_type, description=description, ) diff --git a/ariadne_graphql_modules/next/uniontype.py b/ariadne_graphql_modules/next/uniontype.py index 2546eb2..f6da388 100644 --- a/ariadne_graphql_modules/next/uniontype.py +++ b/ariadne_graphql_modules/next/uniontype.py @@ -4,6 +4,7 @@ Callable, Iterable, NoReturn, + Optional, Sequence, Type, cast, @@ -26,6 +27,7 @@ class GraphQLUnion(GraphQLType): __types__: Sequence[Type[GraphQLType]] + __schema__: Optional[str] def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -46,14 +48,12 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": metadata.set_graphql_name(cls, name) if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(metadata, name) + return cls.__get_graphql_model_with_schema__() - return cls.__get_graphql_model_without_schema__(metadata, name) + return cls.__get_graphql_model_without_schema__(name) @classmethod - def __get_graphql_model_with_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": definition = cast( UnionTypeDefinitionNode, parse_definition(UnionTypeDefinitionNode, cls.__schema__), @@ -67,9 +67,7 @@ def __get_graphql_model_with_schema__( ) @classmethod - def __get_graphql_model_without_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLModel": + def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": return GraphQLUnionModel( name=name, ast_type=UnionTypeDefinitionNode, @@ -87,14 +85,12 @@ def __get_graphql_model_without_schema__( ) @classmethod - def __get_graphql_types__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: + def __get_graphql_types__(cls, _: "GraphQLMetadata") -> Iterable["GraphQLType"]: """Returns iterable with GraphQL types associated with this type""" return [cls] + cls.__types__ @staticmethod - def resolve_type(obj: Any, *args) -> str: + def resolve_type(obj: Any, *_) -> str: if isinstance(obj, GraphQLObject): return obj.__get_graphql_name__() @@ -112,6 +108,7 @@ def validate_union_type(cls: Type[GraphQLUnion]) -> NoReturn: ) +# pylint: disable=E1101 def validate_union_type_with_schema(cls: Type[GraphQLUnion]) -> NoReturn: definition = cast( UnionTypeDefinitionNode, diff --git a/ariadne_graphql_modules/utils.py b/ariadne_graphql_modules/utils.py index be29116..b321687 100644 --- a/ariadne_graphql_modules/utils.py +++ b/ariadne_graphql_modules/utils.py @@ -10,7 +10,7 @@ ) -def parse_definition(type_name: str, schema: Any) -> DefinitionNode: +def parse_definition(type_name: Any, schema: Any) -> DefinitionNode: if not isinstance(schema, str): raise TypeError( f"{type_name} class was defined with __schema__ of invalid type: " diff --git a/tests_next/test_get_field_args_from_resolver.py b/tests_next/test_get_field_args_from_resolver.py index 054fd99..cfd3733 100644 --- a/tests_next/test_get_field_args_from_resolver.py +++ b/tests_next/test_get_field_args_from_resolver.py @@ -27,7 +27,7 @@ def field_resolver(*_, name): field_arg = field_args["name"] assert field_arg.name == "name" assert field_arg.out_name == "name" - assert field_arg.type is None + assert field_arg.field_type is None def test_field_has_arg_after_positional_args_separator(): @@ -40,7 +40,7 @@ def field_resolver(obj, info, *, name): field_arg = field_args["name"] assert field_arg.name == "name" assert field_arg.out_name == "name" - assert field_arg.type is None + assert field_arg.field_type is None def test_field_has_arg_after_obj_and_info_args(): @@ -53,7 +53,7 @@ def field_resolver(obj, info, name): field_arg = field_args["name"] assert field_arg.name == "name" assert field_arg.out_name == "name" - assert field_arg.type is None + assert field_arg.field_type is None def test_field_has_multiple_args_after_excess_positional_args(): @@ -66,12 +66,12 @@ def field_resolver(*_, name, age_cutoff: int): name_arg = field_args["name"] assert name_arg.name == "name" assert name_arg.out_name == "name" - assert name_arg.type is None + assert name_arg.field_type is None age_arg = field_args["age_cutoff"] assert age_arg.name == "ageCutoff" assert age_arg.out_name == "age_cutoff" - assert age_arg.type is int + assert age_arg.field_type is int def test_field_has_multiple_args_after_positional_args_separator(): @@ -84,12 +84,12 @@ def field_resolver(obj, info, *, name, age_cutoff: int): name_arg = field_args["name"] assert name_arg.name == "name" assert name_arg.out_name == "name" - assert name_arg.type is None + assert name_arg.field_type is None age_arg = field_args["age_cutoff"] assert age_arg.name == "ageCutoff" assert age_arg.out_name == "age_cutoff" - assert age_arg.type is int + assert age_arg.field_type is int def test_field_has_multiple_args_after_obj_and_info_args(): @@ -102,12 +102,12 @@ def field_resolver(obj, info, name, age_cutoff: int): name_arg = field_args["name"] assert name_arg.name == "name" assert name_arg.out_name == "name" - assert name_arg.type is None + assert name_arg.field_type is None age_arg = field_args["age_cutoff"] assert age_arg.name == "ageCutoff" assert age_arg.out_name == "age_cutoff" - assert age_arg.type is int + assert age_arg.field_type is int def test_field_has_arg_after_obj_and_info_args_on_class_function(): @@ -121,7 +121,7 @@ def field_resolver(obj, info, name): field_arg = field_args["name"] assert field_arg.name == "name" assert field_arg.out_name == "name" - assert field_arg.type is None + assert field_arg.field_type is None def test_field_has_arg_after_obj_and_info_args_on_class_method(): @@ -136,7 +136,7 @@ def field_resolver(cls, obj, info, name): field_arg = field_args["name"] assert field_arg.name == "name" assert field_arg.out_name == "name" - assert field_arg.type is None + assert field_arg.field_type is None def test_field_has_arg_after_obj_and_info_args_on_static_method(): @@ -151,4 +151,4 @@ def field_resolver(obj, info, name): field_arg = field_args["name"] assert field_arg.name == "name" assert field_arg.out_name == "name" - assert field_arg.type is None + assert field_arg.field_type is None diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py index e0b3063..19ad1c6 100644 --- a/tests_next/test_input_type.py +++ b/tests_next/test_input_type.py @@ -542,7 +542,7 @@ def resolve_search(*_, input: SearchInput) -> str: def test_input_type_with_field_type(assert_schema_equals): class SearchInput(GraphQLInput): - query: str = GraphQLInput.field(type=int) + query: str = GraphQLInput.field(graphql_type=int) class QueryType(GraphQLObject): search: str diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py index 163ce19..303071f 100644 --- a/tests_next/test_interface_type.py +++ b/tests_next/test_interface_type.py @@ -32,7 +32,7 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(type=List[ResultType]) + @GraphQLObject.field(graphql_type=List[ResultType]) def search(*_) -> List[UserType | CommentType]: return [ UserType(id=1, username="Bob"), @@ -95,7 +95,7 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(type=List[ResultType]) + @GraphQLObject.field(graphql_type=List[ResultType]) def search(*_) -> List[UserType | CommentType]: return [ UserType(id=1, username="Bob"), diff --git a/tests_next/test_make_executable_schema.py b/tests_next/test_make_executable_schema.py index 51e51e5..6b4479e 100644 --- a/tests_next/test_make_executable_schema.py +++ b/tests_next/test_make_executable_schema.py @@ -177,7 +177,7 @@ class ThirdRoot(GraphQLObject): def test_schema_validation_fails_if_lazy_type_doesnt_exist(snapshot): class QueryType(GraphQLObject): - @GraphQLObject.field(type=list["Missing"]) + @GraphQLObject.field(graphql_type=list["Missing"]) def other(obj, info): return None @@ -189,7 +189,7 @@ def other(obj, info): def test_schema_validation_passes_if_lazy_type_exists(): class QueryType(GraphQLObject): - @GraphQLObject.field(type=list["Exists"]) + @GraphQLObject.field(graphql_type=list["Exists"]) def other(obj, info): return None diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index c7f76d3..95b66c7 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -396,7 +396,7 @@ def hello(obj, info) -> str: def test_object_type_with_typed_field_instance(assert_schema_equals): class QueryType(GraphQLObject): - hello = GraphQLObject.field(lambda *_: "Hello World!", type=str) + hello = GraphQLObject.field(lambda *_: "Hello World!", graphql_type=str) schema = make_executable_schema(QueryType) @@ -499,7 +499,7 @@ class PostType(GraphQLObject): class QueryType(GraphQLObject): user: UserType - @GraphQLObject.field(type=PostType) + @GraphQLObject.field(graphql_type=PostType) def post(obj, info): return {"message": "test"} diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index 6b0158e..3ddbdc6 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -313,7 +313,7 @@ class CustomType(GraphQLObject): """ @GraphQLObject.resolver( - "hello", args={"name": GraphQLObject.argument(type=str)} + "hello", args={"name": GraphQLObject.argument(graphql_type=str)} ) def ipsum(*_, name: str) -> str: return "Hello World!" diff --git a/tests_next/test_scalar_type.py b/tests_next/test_scalar_type.py index 6ffa961..2f578c6 100644 --- a/tests_next/test_scalar_type.py +++ b/tests_next/test_scalar_type.py @@ -48,9 +48,9 @@ def resolve_date(*_) -> DateScalar: def test_scalar_field_returning_scalar_wrapped_type(assert_schema_equals): class QueryType(GraphQLObject): - date: DateScalar + scalar_date: DateScalar - @GraphQLObject.resolver("date", type=DateScalar) + @GraphQLObject.resolver("scalar_date", graphql_type=DateScalar) def resolve_date(*_) -> date: return date(1989, 10, 30) @@ -62,15 +62,15 @@ def resolve_date(*_) -> date: scalar Date type Query { - date: Date! + scalarDate: Date! } """, ) - result = graphql_sync(schema, "{ date }") + result = graphql_sync(schema, "{ scalarDate }") assert not result.errors - assert result.data == {"date": "1989-10-30"} + assert result.data == {"scalarDate": "1989-10-30"} class SchemaDateScalar(GraphQLScalar[date]): diff --git a/tests_next/test_subscription_type.py b/tests_next/test_subscription_type.py index 73262e6..b8bd2a5 100644 --- a/tests_next/test_subscription_type.py +++ b/tests_next/test_subscription_type.py @@ -37,12 +37,12 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - @GraphQLSubscription.resolver("message_added", type=Message) + @GraphQLSubscription.resolver("message_added", graphql_type=Message) async def resolve_message_added(message, info): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -102,13 +102,13 @@ async def message_added_generator(obj, info, channel: GraphQLID): @GraphQLSubscription.resolver( "message_added", - type=Message, + graphql_type=Message, ) async def resolve_message_added(message, *_, channel: GraphQLID): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -172,7 +172,7 @@ async def message_added_generator(obj, info, channel: GraphQLID): @GraphQLSubscription.resolver( "message_added", - type=Message, + graphql_type=Message, ) async def resolve_message_added(message, *_, channel: GraphQLID): return message @@ -189,13 +189,13 @@ async def user_joined_generator(obj, info): @GraphQLSubscription.resolver( "user_joined", - type=Message, + graphql_type=Message, ) async def resolve_user_joined(user, *_): return user class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -264,13 +264,13 @@ async def message_added_generator(obj, info, channel_id: GraphQLID): @GraphQLSubscription.resolver( "messages_in_channel", - type=Message, + graphql_type=Message, ) async def resolve_message_added(message, *_, channel_id: GraphQLID): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -337,7 +337,7 @@ async def resolve_message_added(message, *_): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -399,12 +399,12 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - @GraphQLSubscription.resolver("messageAdded", type=Message) + @GraphQLSubscription.resolver("messageAdded", graphql_type=Message) async def resolve_message_added(message, info): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -469,13 +469,13 @@ async def message_added_generator(obj, info, channel: GraphQLID): @GraphQLSubscription.resolver( "messageAdded", - type=Message, + graphql_type=Message, ) async def resolve_message_added(message, *_, channel: GraphQLID): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -562,7 +562,7 @@ async def resolve_user_joined(user, *_): return user class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -638,7 +638,7 @@ async def resolve_message_added(message, *_, channelId: GraphQLID): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" @@ -708,7 +708,7 @@ async def resolve_message_added(message, *_): return message class QueryType(GraphQLObject): - @GraphQLObject.field(type=str) + @GraphQLObject.field(graphql_type=str) def search_sth(*_) -> str: return "search" diff --git a/tests_next/test_subscription_type_validation.py b/tests_next/test_subscription_type_validation.py index 14a88e5..e772d2e 100644 --- a/tests_next/test_subscription_type_validation.py +++ b/tests_next/test_subscription_type_validation.py @@ -262,7 +262,7 @@ class SubscriptionType(GraphQLSubscription): "messageAdded", args={ "channel": GraphQLObject.argument( - type=str, description="Lorem ipsum." + graphql_type=str, description="Lorem ipsum." ) }, ) @@ -293,7 +293,7 @@ class SubscriptionType(GraphQLSubscription): "messageAdded", args={ "channel": GraphQLObject.argument( - type=str, description="Lorem ipsum." + graphql_type=str, description="Lorem ipsum." ) }, ) diff --git a/tests_next/test_union_type.py b/tests_next/test_union_type.py index 93b42f3..cbd722e 100644 --- a/tests_next/test_union_type.py +++ b/tests_next/test_union_type.py @@ -25,7 +25,7 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(type=List[ResultType]) + @GraphQLObject.field(graphql_type=List[ResultType]) def search(*_) -> List[UserType | CommentType]: return [ UserType(id=1, username="Bob"), @@ -87,7 +87,7 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(type=List[ResultType]) + @GraphQLObject.field(graphql_type=List[ResultType]) def search(*_) -> List[UserType | CommentType]: return [] @@ -119,7 +119,7 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(type=List[ResultType]) + @GraphQLObject.field(graphql_type=List[ResultType]) def search(*_) -> List[UserType | CommentType]: return [ UserType(id=1, username="Bob"), @@ -158,7 +158,7 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(type=List[ResultType]) + @GraphQLObject.field(graphql_type=List[ResultType]) def search(*_) -> List[UserType | CommentType | InvalidType]: return [InvalidType("This should cause an error")] @@ -188,7 +188,7 @@ class SearchResultUnion(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(type=List[SearchResultUnion]) + @GraphQLObject.field(graphql_type=List[SearchResultUnion]) def search(*_) -> List[UserType | CommentType]: return [ UserType(id="1", username="Alice"), From ea01834f9e49039b4e28de9a5d17cd6023d1fe25 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 16 Aug 2024 15:42:09 +0200 Subject: [PATCH 37/63] Fix all mypy issues, Fix all pylint issue, Refactor code --- ariadne_graphql_modules/next/__init__.py | 14 +- ariadne_graphql_modules/next/base.py | 47 +- ariadne_graphql_modules/next/convert_name.py | 30 +- ariadne_graphql_modules/next/deferredtype.py | 26 +- .../next/executable_schema.py | 42 +- .../next/graphql_enum_type/__init__.py | 9 + .../next/graphql_enum_type/enum_model.py | 15 + .../enum_type.py} | 296 ++-- .../next/graphql_input/__init__.py | 8 + .../next/graphql_input/input_field.py | 21 + .../next/graphql_input/input_model.py | 17 + .../input_type.py} | 186 +-- .../next/graphql_input/validators.py | 126 ++ .../next/graphql_interface/__init__.py | 8 + .../next/graphql_interface/interface_model.py | 31 + .../interface_type.py} | 72 +- .../next/graphql_object/__init__.py | 32 + .../next/graphql_object/object_field.py | 124 ++ .../next/graphql_object/object_model.py | 31 + .../next/graphql_object/object_type.py | 449 ++++++ .../next/graphql_object/utils.py | 190 +++ .../next/graphql_object/validators.py | 547 +++++++ .../next/graphql_scalar/__init__.py | 8 + .../next/graphql_scalar/scalar_model.py | 23 + .../scalar_type.py} | 50 +- .../next/graphql_scalar/validators.py | 25 + .../next/graphql_subscription/__init__.py | 8 + .../subscription_model.py | 33 + .../subscription_type.py} | 89 +- .../next/graphql_union/__init__.py | 8 + .../next/graphql_union/union_model.py | 16 + .../next/graphql_union/union_type.py | 88 ++ .../next/graphql_union/validators.py | 57 + ariadne_graphql_modules/next/idtype.py | 2 +- ariadne_graphql_modules/next/objecttype.py | 1286 ----------------- ariadne_graphql_modules/next/roots.py | 15 +- ariadne_graphql_modules/next/sort.py | 27 +- ariadne_graphql_modules/next/typing.py | 21 +- ariadne_graphql_modules/next/uniontype.py | 153 -- ariadne_graphql_modules/next/value.py | 4 +- .../snap_test_subscription_type_validation.py | 2 +- .../test_get_field_args_from_resolver.py | 2 +- tests_next/test_union_type_validation.py | 4 +- 43 files changed, 2216 insertions(+), 2026 deletions(-) create mode 100644 ariadne_graphql_modules/next/graphql_enum_type/__init__.py create mode 100644 ariadne_graphql_modules/next/graphql_enum_type/enum_model.py rename ariadne_graphql_modules/next/{enumtype.py => graphql_enum_type/enum_type.py} (52%) create mode 100644 ariadne_graphql_modules/next/graphql_input/__init__.py create mode 100644 ariadne_graphql_modules/next/graphql_input/input_field.py create mode 100644 ariadne_graphql_modules/next/graphql_input/input_model.py rename ariadne_graphql_modules/next/{inputtype.py => graphql_input/input_type.py} (58%) create mode 100644 ariadne_graphql_modules/next/graphql_input/validators.py create mode 100644 ariadne_graphql_modules/next/graphql_interface/__init__.py create mode 100644 ariadne_graphql_modules/next/graphql_interface/interface_model.py rename ariadne_graphql_modules/next/{interfacetype.py => graphql_interface/interface_type.py} (76%) create mode 100644 ariadne_graphql_modules/next/graphql_object/__init__.py create mode 100644 ariadne_graphql_modules/next/graphql_object/object_field.py create mode 100644 ariadne_graphql_modules/next/graphql_object/object_model.py create mode 100644 ariadne_graphql_modules/next/graphql_object/object_type.py create mode 100644 ariadne_graphql_modules/next/graphql_object/utils.py create mode 100644 ariadne_graphql_modules/next/graphql_object/validators.py create mode 100644 ariadne_graphql_modules/next/graphql_scalar/__init__.py create mode 100644 ariadne_graphql_modules/next/graphql_scalar/scalar_model.py rename ariadne_graphql_modules/next/{scalartype.py => graphql_scalar/scalar_type.py} (63%) create mode 100644 ariadne_graphql_modules/next/graphql_scalar/validators.py create mode 100644 ariadne_graphql_modules/next/graphql_subscription/__init__.py create mode 100644 ariadne_graphql_modules/next/graphql_subscription/subscription_model.py rename ariadne_graphql_modules/next/{subscriptiontype.py => graphql_subscription/subscription_type.py} (76%) create mode 100644 ariadne_graphql_modules/next/graphql_union/__init__.py create mode 100644 ariadne_graphql_modules/next/graphql_union/union_model.py create mode 100644 ariadne_graphql_modules/next/graphql_union/union_type.py create mode 100644 ariadne_graphql_modules/next/graphql_union/validators.py delete mode 100644 ariadne_graphql_modules/next/objecttype.py delete mode 100644 ariadne_graphql_modules/next/uniontype.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py index 7b113a6..6aea1ab 100644 --- a/ariadne_graphql_modules/next/__init__.py +++ b/ariadne_graphql_modules/next/__init__.py @@ -5,7 +5,7 @@ ) from .deferredtype import deferred from .description import get_description_node -from .enumtype import ( +from .graphql_enum_type import ( GraphQLEnum, GraphQLEnumModel, create_graphql_enum_model, @@ -13,15 +13,15 @@ ) from .executable_schema import make_executable_schema from .idtype import GraphQLID -from .inputtype import GraphQLInput, GraphQLInputModel -from .objecttype import GraphQLObject, GraphQLObjectModel, object_field +from .graphql_input import GraphQLInput, GraphQLInputModel +from .graphql_object import GraphQLObject, GraphQLObjectModel, object_field from .roots import ROOTS_NAMES, merge_root_nodes -from .scalartype import GraphQLScalar, GraphQLScalarModel +from .graphql_scalar import GraphQLScalar, GraphQLScalarModel from .sort import sort_schema_document -from .uniontype import GraphQLUnion, GraphQLUnionModel +from .graphql_union import GraphQLUnion, GraphQLUnionModel from .value import get_value_from_node, get_value_node -from .interfacetype import GraphQLInterface, GraphQLInterfaceModel -from .subscriptiontype import GraphQLSubscription, GraphQLSubscriptionModel +from .graphql_interface import GraphQLInterface, GraphQLInterfaceModel +from .graphql_subscription import GraphQLSubscription, GraphQLSubscriptionModel __all__ = [ "GraphQLEnum", diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/next/base.py index 7710db8..fcf7991 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/next/base.py @@ -11,32 +11,25 @@ class GraphQLType: __abstract__: bool = True @classmethod - def __get_graphql_name__(cls) -> "str": - if getattr(cls, "__graphql_name__", None): - return cls.__graphql_name__ + def __get_graphql_name__(cls) -> str: + name = getattr(cls, "__graphql_name__", None) + if name: + return name + + name_mappings = [ + ("GraphQLEnum", "Enum"), + ("GraphQLInput", "Input"), + ("GraphQLScalar", ""), + ("Scalar", ""), + ("GraphQL", ""), + ("Type", ""), + ("GraphQLType", ""), + ] name = cls.__name__ - if name.endswith("GraphQLEnum"): - # 'UserLevelGraphQLEnum' will produce the 'UserLevelEnum' name - return f"{name[:-11]}Enum" or name - if name.endswith("GraphQLInput"): - # 'UserGraphQLInput' will produce the 'UserInput' name - return f"{name[:-11]}Input" or name - if name.endswith("GraphQLScalar"): - # 'DateTimeGraphQLScalar' will produce the 'DateTime' name - return name[:-13] or name - if name.endswith("Scalar"): - # 'DateTimeLScalar' will produce the 'DateTime' name - return name[:-6] or name - if name.endswith("GraphQL"): - # 'UserGraphQL' will produce the 'User' name - return name[:-7] or name - if name.endswith("Type"): - # 'UserType' will produce the 'User' name - return name[:-4] or name - if name.endswith("GraphQLType"): - # 'UserGraphQLType' will produce the 'User' name - return name[:-11] or name + for suffix, replacement in name_mappings: + if name.endswith(suffix): + return name[: -len(suffix)] + replacement return name @@ -47,7 +40,9 @@ def __get_graphql_model__(cls, metadata: "GraphQLMetadata") -> "GraphQLModel": ) @classmethod - def __get_graphql_types__(cls, _: "GraphQLMetadata") -> Iterable["GraphQLType"]: + def __get_graphql_types__( + cls, _: "GraphQLMetadata" + ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: """Returns iterable with GraphQL types associated with this type""" return [cls] @@ -89,7 +84,7 @@ def get_graphql_model( if hasattr(graphql_type, "__get_graphql_model__"): self.models[graphql_type] = graphql_type.__get_graphql_model__(self) elif issubclass(graphql_type, Enum): - from .enumtype import ( # pylint: disable=R0401,C0415 + from .graphql_enum_type import ( # pylint: disable=R0401,C0415 create_graphql_enum_model, ) diff --git a/ariadne_graphql_modules/next/convert_name.py b/ariadne_graphql_modules/next/convert_name.py index 0451fbd..107bac4 100644 --- a/ariadne_graphql_modules/next/convert_name.py +++ b/ariadne_graphql_modules/next/convert_name.py @@ -1,27 +1,13 @@ def convert_python_name_to_graphql(python_name: str) -> str: - final_name = "" - for i, c in enumerate(python_name): - if not i: - final_name += c.lower() - else: - if c == "_": - continue - if python_name[i - 1] == "_": - final_name += c.upper() - else: - final_name += c.lower() - - return final_name + components = python_name.split("_") + return components[0].lower() + "".join(x.capitalize() for x in components[1:]) def convert_graphql_name_to_python(graphql_name: str) -> str: - final_name = "" - for i, c in enumerate(graphql_name.lower()): - if not i: - final_name += c + python_name = "" + for c in graphql_name: + if c.isupper() or c.isdigit(): + python_name += "_" + c.lower() else: - if final_name[-1] != "_" and (c != graphql_name[i] or c.isdigit()): - final_name += "_" - final_name += c - - return final_name + python_name += c + return python_name.lstrip("_") diff --git a/ariadne_graphql_modules/next/deferredtype.py b/ariadne_graphql_modules/next/deferredtype.py index d9564d6..2011c04 100644 --- a/ariadne_graphql_modules/next/deferredtype.py +++ b/ariadne_graphql_modules/next/deferredtype.py @@ -12,24 +12,38 @@ def deferred(module_path: str) -> DeferredTypeData: if not module_path.startswith("."): return DeferredTypeData(module_path) + frame = _get_caller_frame() + current_package = cast(str, frame.f_globals["__package__"]) + + module_path_suffix = _resolve_module_path_suffix(module_path, current_package) + + return DeferredTypeData(module_path_suffix) + + +def _get_caller_frame(): + """Retrieve the caller's frame and ensure it's within a valid context.""" frame = sys._getframe(2) # pylint: disable=protected-access if not frame: raise RuntimeError( - "'deferred' can't be called outside of class's attribute's " - "definition context." + "'deferred' must be called within a class attribute definition context." ) + return frame + +def _resolve_module_path_suffix(module_path: str, current_package: str) -> str: + """Resolve the full module path by handling relative imports.""" module_path_suffix = module_path[1:] # Remove initial dot - current_package = cast(str, frame.f_globals["__package__"]) packages = current_package.split(".") while module_path_suffix.startswith(".") and packages: module_path_suffix = module_path_suffix[1:] # Remove dot - packages = packages[:-1] + packages.pop() + if not packages: raise ValueError( f"'{module_path}' points outside of the '{current_package}' package." ) - package = ".".join(packages) - return DeferredTypeData(f"{package}.{module_path_suffix}") + return ( + f"{'.'.join(packages)}.{module_path_suffix}" if packages else module_path_suffix + ) diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py index 7444956..79da59f 100644 --- a/ariadne_graphql_modules/next/executable_schema.py +++ b/ariadne_graphql_modules/next/executable_schema.py @@ -22,16 +22,11 @@ from .roots import ROOTS_NAMES, merge_root_nodes from .sort import sort_schema_document -SchemaType = Union[ - str, - Enum, - SchemaBindable, - Type[GraphQLType], -] +SchemaType = Union[str, Enum, SchemaBindable, Type[GraphQLType], Type[Enum]] def make_executable_schema( - *types: Union[SchemaType, List[SchemaType]], + *types: SchemaType, directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, convert_names_case: Union[bool, SchemaNameConverter] = False, merge_roots: bool = True, @@ -47,7 +42,7 @@ def make_executable_schema( for type_def in types_list: if isinstance(type_def, SchemaBindable): schema_bindables.append(type_def) - else: + elif isinstance(type_def, type) and issubclass(type_def, (GraphQLType, Enum)): schema_bindables.append(metadata.get_graphql_model(type_def)) schema_models: List[GraphQLModel] = [ @@ -114,19 +109,19 @@ def find_type_defs(types: Sequence[SchemaType]) -> List[str]: def flatten_types( - types: Sequence[Union[SchemaType, List[SchemaType]]], + types: Sequence[SchemaType], metadata: GraphQLMetadata, -) -> List[Union[Enum, SchemaBindable, GraphQLModel]]: +) -> List[SchemaType]: flat_schema_types_list: List[SchemaType] = flatten_schema_types( types, metadata, dedupe=True ) - types_list: List[Union[Enum, SchemaBindable, GraphQLModel]] = [] + types_list: List[SchemaType] = [] for type_def in flat_schema_types_list: if isinstance(type_def, SchemaBindable): types_list.append(type_def) - elif issubclass(type_def, GraphQLType): + elif isinstance(type_def, type) and issubclass(type_def, GraphQLType): type_name = type_def.__name__ if getattr(type_def, "__abstract__", None): @@ -137,7 +132,7 @@ def flatten_types( types_list.append(type_def) - elif issubclass(type_def, Enum): + elif isinstance(type_def, type) and issubclass(type_def, Enum): types_list.append(type_def) elif isinstance(type_def, list): @@ -161,9 +156,9 @@ def flatten_schema_types( flat_list += flatten_schema_types(type_def, metadata, dedupe=False) elif isinstance(type_def, SchemaBindable): flat_list.append(type_def) - elif issubclass(type_def, Enum): + elif isinstance(type_def, type) and issubclass(type_def, Enum): flat_list.append(type_def) - elif issubclass(type_def, GraphQLType): + elif isinstance(type_def, type) and issubclass(type_def, GraphQLType): add_graphql_type_to_flat_list(flat_list, checked_types, type_def, metadata) elif get_graphql_type_name(type_def): flat_list.append(type_def) @@ -182,9 +177,9 @@ def flatten_schema_types( def add_graphql_type_to_flat_list( flat_list: List[SchemaType], checked_types: List[Type[GraphQLType]], - type_def: GraphQLType, + type_def: Type[GraphQLType], metadata: GraphQLMetadata, -) -> List[SchemaType]: +) -> None: if type_def in checked_types: return @@ -203,10 +198,10 @@ def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: if isinstance(type_def, SchemaBindable): return None - if issubclass(type_def, Enum): + if isinstance(type_def, type) and issubclass(type_def, Enum): return type_def.__name__ - if issubclass(type_def, GraphQLType): + if isinstance(type_def, type) and issubclass(type_def, GraphQLType): return type_def.__get_graphql_name__() return None @@ -223,8 +218,9 @@ def assert_types_unique(type_defs: List[SchemaType], merge_roots: bool): continue if type_name in types_names: + type_def_name = getattr(type_def, "__name__") or type_def raise ValueError( - f"Types '{type_def.__name__}' and '{types_names[type_name]}' both define " + f"Types '{type_def_name}' and '{types_names[type_name]}' both define " f"GraphQL type with name '{type_name}'." ) @@ -236,8 +232,10 @@ def assert_types_not_abstract(type_defs: List[SchemaType]): if isinstance(type_def, SchemaBindable): continue - if issubclass(type_def, GraphQLType) and getattr( - type_def, "__abstract__", None + if ( + isinstance(type_def, type) + and issubclass(type_def, GraphQLType) + and getattr(type_def, "__abstract__", None) ): raise ValueError( f"Type '{type_def.__name__}' is an abstract type and can't be used " diff --git a/ariadne_graphql_modules/next/graphql_enum_type/__init__.py b/ariadne_graphql_modules/next/graphql_enum_type/__init__.py new file mode 100644 index 0000000..a4c8666 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_enum_type/__init__.py @@ -0,0 +1,9 @@ +from .enum_type import GraphQLEnum, graphql_enum, create_graphql_enum_model +from .enum_model import GraphQLEnumModel + +__all__ = [ + "GraphQLEnum", + "GraphQLEnumModel", + "create_graphql_enum_model", + "graphql_enum", +] diff --git a/ariadne_graphql_modules/next/graphql_enum_type/enum_model.py b/ariadne_graphql_modules/next/graphql_enum_type/enum_model.py new file mode 100644 index 0000000..b455784 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_enum_type/enum_model.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import Any, Dict +from ariadne import EnumType +from graphql import GraphQLSchema + +from ..base import GraphQLModel + + +@dataclass(frozen=True) +class GraphQLEnumModel(GraphQLModel): + members: Dict[str, Any] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = EnumType(self.name, values=self.members) + bindable.bind_to_schema(schema) diff --git a/ariadne_graphql_modules/next/enumtype.py b/ariadne_graphql_modules/next/graphql_enum_type/enum_type.py similarity index 52% rename from ariadne_graphql_modules/next/enumtype.py rename to ariadne_graphql_modules/next/graphql_enum_type/enum_type.py index 5e17403..84a12e8 100644 --- a/ariadne_graphql_modules/next/enumtype.py +++ b/ariadne_graphql_modules/next/graphql_enum_type/enum_type.py @@ -1,20 +1,13 @@ -from dataclasses import dataclass from enum import Enum from inspect import isclass from typing import Any, Dict, Iterable, List, Optional, Set, Type, Union, cast -from ariadne import EnumType -from graphql import ( - EnumTypeDefinitionNode, - EnumValueDefinitionNode, - GraphQLSchema, - NameNode, -) - -from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .description import get_description_node -from .validators import validate_description, validate_name +from graphql import EnumTypeDefinitionNode, EnumValueDefinitionNode, NameNode +from ..base import GraphQLMetadata, GraphQLModel, GraphQLType +from ..description import get_description_node +from ..graphql_enum_type.enum_model import GraphQLEnumModel +from ..validators import validate_description, validate_name +from ...utils import parse_definition class GraphQLEnum(GraphQLType): @@ -31,11 +24,7 @@ def __init_subclass__(cls) -> None: return cls.__abstract__ = False - - if cls.__dict__.get("__schema__"): - validate_enum_type_with_schema(cls) - else: - validate_enum_type(cls) + cls._validate() @classmethod def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": @@ -46,22 +35,25 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": return cls.__get_graphql_model_without_schema__(name) - # pylint: disable=E1101 @classmethod def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLEnumModel": - definition = cast( + definition: EnumTypeDefinitionNode = cast( EnumTypeDefinitionNode, parse_definition(EnumTypeDefinitionNode, cls.__schema__), ) members = getattr(cls, "__members__", []) members_values: Dict[str, Any] = {} + if isinstance(members, dict): members_values = dict(members.items()) elif isclass(members) and issubclass(members, Enum): - members_values = {i.name: i for i in members} + members_values = {member.name: member for member in members} else: - members_values = {i.name.value: i.name.value for i in definition.values} + members_values = { + value.name.value: value.name.value + for value in definition.values # pylint: disable=no-member + } members_descriptions = getattr(cls, "__members_descriptions__", {}) @@ -85,7 +77,7 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLEnumModel": ) ), ) - for value in definition.values + for value in definition.values # pylint: disable=no-member ), ), ) @@ -124,6 +116,136 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLEnumModel": ), ) + @classmethod + def _validate(cls): + if getattr(cls, "__schema__", None): + cls._validate_enum_type_with_schema() + else: + cls._validate_enum_type() + + @classmethod + def _validate_enum_type_with_schema(cls): + definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) + + if not isinstance(definition, EnumTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + f"with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != '{EnumTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + members_names = { + value.name.value for value in definition.values # pylint: disable=no-member + } + if not members_names: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "that doesn't declare any enum members." + ) + + members_values = getattr(cls, "__members__", None) + if members_values: + cls.validate_members_values(members_values, members_names) + + members_descriptions = getattr(cls, "__members_descriptions__", {}) + cls.validate_enum_members_descriptions(members_names, members_descriptions) + + duplicate_descriptions = [ + ast_member.name.value + for ast_member in definition.values # pylint: disable=no-member + if ast_member.description + and ast_member.description.value + and members_descriptions.get(ast_member.name.value) + ] + + if duplicate_descriptions: + raise ValueError( + f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " + f"descriptions for enum members that also have description in '__schema__' " + f"attribute. (members: '{', '.join(duplicate_descriptions)}')" + ) + + @classmethod + def validate_members_values(cls, members_values, members_names): + if isinstance(members_values, list): + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute " + "can't be a list when used together with '__schema__'." + ) + + missing_members = None + if isinstance(members_values, dict): + missing_members = members_names - set(members_values) + elif isclass(members_values) and issubclass(members_values, Enum): + missing_members = members_names - {value.name for value in members_values} + + if missing_members: + raise ValueError( + f"Class '{cls.__name__}' '__members__' is missing values " + f"for enum members defined in '__schema__'. " + f"(missing items: '{', '.join(missing_members)}')" + ) + + @classmethod + def _validate_enum_type(cls): + members_values = getattr(cls, "__members__", None) + if not members_values: + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute is either missing or " + "empty. Either define it or provide full SDL for this enum using " + "the '__schema__' attribute." + ) + + if not any( + [ + isinstance(members_values, (dict, list)), + isclass(members_values) and issubclass(members_values, Enum), + ] + ): + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute is of unsupported type. " + f"Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. " + f"(found: '{type(members_values)}')" + ) + + members_names = cls.get_members_set(members_values) + members_descriptions = getattr(cls, "__members_descriptions__", {}) + cls.validate_enum_members_descriptions(members_names, members_descriptions) + + @classmethod + def validate_enum_members_descriptions( + cls, members: Set[str], members_descriptions: dict + ): + invalid_descriptions = set(members_descriptions) - members + if invalid_descriptions: + invalid_descriptions_str = "', '".join(invalid_descriptions) + raise ValueError( + f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " + f"descriptions for undefined enum members. " + f"(undefined members: '{invalid_descriptions_str}')" + ) + + @staticmethod + def get_members_set( + members: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] + ) -> Set[str]: + if isinstance(members, dict): + return set(members.keys()) + + if isclass(members) and issubclass(members, Enum): + return set(member.name for member in members) + + if isinstance(members, list): + return set(members) + + raise TypeError( + f"Expected members to be of type Dict[str, Any], List[str], or Enum." + f"Got {type(members).__name__} instead." + ) + def create_graphql_enum_model( enum: Type[Enum], @@ -190,15 +312,6 @@ def create_graphql_enum_model( ) -@dataclass(frozen=True) -class GraphQLEnumModel(GraphQLModel): - members: Dict[str, Any] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = EnumType(self.name, values=self.members) - bindable.bind_to_schema(schema) - - def graphql_enum( cls: Optional[Type[Enum]] = None, *, @@ -218,130 +331,13 @@ def graphql_enum_decorator(cls: Type[Enum]): members_exclude=members_exclude, ) - @classmethod def __get_graphql_model__(*_) -> GraphQLEnumModel: return graphql_model - setattr(cls, "__get_graphql_model__", __get_graphql_model__) + setattr(cls, "__get_graphql_model__", classmethod(__get_graphql_model__)) return cls if cls: return graphql_enum_decorator(cls) return graphql_enum_decorator - - -# pylint: disable=E1101 -def validate_enum_type_with_schema(cls: Type[GraphQLEnum]): - definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) - - if not isinstance(definition, EnumTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - f"with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != '{EnumTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - members_names = {value.name.value for value in definition.values} - if not members_names: - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "that doesn't declare any enum members." - ) - - members_values = getattr(cls, "__members__", None) - if members_values: - validate_members_values(cls, members_values, members_names) - - members_descriptions = getattr(cls, "__members_descriptions__", {}) - validate_enum_members_descriptions(cls, members_names, members_descriptions) - - duplicate_descriptions = [ - ast_member.name.value - for ast_member in definition.values - if ast_member.description - and ast_member.description.value - and members_descriptions.get(ast_member.name.value) - ] - - if duplicate_descriptions: - raise ValueError( - f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " - f"descriptions for enum members that also have description in '__schema__' " - f"attribute. (members: '{', '.join(duplicate_descriptions)}')" - ) - - -def validate_members_values(cls, members_values, members_names): - if isinstance(members_values, list): - raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute " - "can't be a list when used together with '__schema__'." - ) - - missing_members = None - if isinstance(members_values, dict): - missing_members = members_names - set(members_values) - elif isclass(members_values) and issubclass(members_values, Enum): - missing_members = members_names - {value.name for value in members_values} - - if missing_members: - raise ValueError( - f"Class '{cls.__name__}' '__members__' is missing values " - f"for enum members defined in '__schema__'. " - f"(missing items: '{', '.join(missing_members)}')" - ) - - -def validate_enum_type(cls: Type[GraphQLEnum]): - members_values = getattr(cls, "__members__", None) - if not members_values: - raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute is either missing or " - "empty. Either define it or provide full SDL for this enum using " - "the '__schema__' attribute." - ) - - if not any( - [ - isinstance(members_values, (dict, list)), - isclass(members_values) and issubclass(members_values, Enum), - ] - ): - raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute is of unsupported type. " - f"Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. " - f"(found: '{type(members_values)}')" - ) - - members_names = get_members_set(members_values) - members_descriptions = getattr(cls, "__members_descriptions__", {}) - validate_enum_members_descriptions(cls, members_names, members_descriptions) - - -def validate_enum_members_descriptions( - cls: Type[GraphQLEnum], members: Set[str], members_descriptions: dict -): - invalid_descriptions = set(members_descriptions) - members - if invalid_descriptions: - invalid_descriptions_str = "', '".join(invalid_descriptions) - raise ValueError( - f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " - f"descriptions for undefined enum members. " - f"(undefined members: '{invalid_descriptions_str}')" - ) - - -def get_members_set( - members: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] -) -> Set[str]: - if isinstance(members, dict): - return set(members.keys()) - - if isclass(members) and issubclass(members, Enum): - return set(member.name for member in members) - - return set(members) diff --git a/ariadne_graphql_modules/next/graphql_input/__init__.py b/ariadne_graphql_modules/next/graphql_input/__init__.py new file mode 100644 index 0000000..5d1e0b5 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_input/__init__.py @@ -0,0 +1,8 @@ +from .input_type import GraphQLInput +from .input_model import GraphQLInputModel + + +__all__ = [ + "GraphQLInput", + "GraphQLInputModel", +] diff --git a/ariadne_graphql_modules/next/graphql_input/input_field.py b/ariadne_graphql_modules/next/graphql_input/input_field.py new file mode 100644 index 0000000..4cbbff7 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_input/input_field.py @@ -0,0 +1,21 @@ +from typing import Any, Optional + + +class GraphQLInputField: + name: Optional[str] + description: Optional[str] + graphql_type: Optional[Any] + default_value: Optional[Any] + + def __init__( + self, + *, + name: Optional[str] = None, + description: Optional[str] = None, + graphql_type: Optional[Any] = None, + default_value: Optional[Any] = None, + ): + self.name = name + self.description = description + self.graphql_type = graphql_type + self.default_value = default_value diff --git a/ariadne_graphql_modules/next/graphql_input/input_model.py b/ariadne_graphql_modules/next/graphql_input/input_model.py new file mode 100644 index 0000000..6510d11 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_input/input_model.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from typing import Any, Dict + +from ariadne import InputType as InputTypeBindable +from graphql import GraphQLSchema + +from ..base import GraphQLModel + + +@dataclass(frozen=True) +class GraphQLInputModel(GraphQLModel): + out_type: Any + out_names: Dict[str, str] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = InputTypeBindable(self.name, self.out_type, self.out_names) + bindable.bind_to_schema(schema) diff --git a/ariadne_graphql_modules/next/inputtype.py b/ariadne_graphql_modules/next/graphql_input/input_type.py similarity index 58% rename from ariadne_graphql_modules/next/inputtype.py rename to ariadne_graphql_modules/next/graphql_input/input_type.py index c91e326..023df84 100644 --- a/ariadne_graphql_modules/next/inputtype.py +++ b/ariadne_graphql_modules/next/graphql_input/input_type.py @@ -1,25 +1,24 @@ from copy import deepcopy -from dataclasses import dataclass +from enum import Enum from typing import Any, Dict, Iterable, List, Optional, Type, cast -from ariadne import InputType as InputTypeBindable -from graphql import ( - GraphQLSchema, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - NameNode, -) +from graphql import InputObjectTypeDefinitionNode, InputValueDefinitionNode, NameNode -from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .convert_name import ( +from .input_field import GraphQLInputField +from ..base import GraphQLMetadata, GraphQLModel, GraphQLType +from ..convert_name import ( convert_graphql_name_to_python, convert_python_name_to_graphql, ) -from .description import get_description_node -from .typing import get_graphql_type, get_type_node -from .validators import validate_description, validate_name -from .value import get_value_from_node, get_value_node +from ..description import get_description_node +from ..graphql_input.input_model import GraphQLInputModel +from ..graphql_input.validators import ( + validate_input_type, + validate_input_type_with_schema, +) +from ..typing import get_graphql_type, get_type_node +from ..value import get_value_node +from ...utils import parse_definition class GraphQLInput(GraphQLType): @@ -74,9 +73,7 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLInputModel": parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), ) - out_names: Dict[str, str] = {} - if getattr(cls, "__out_names__"): - out_names.update(cls.__out_names__) + out_names: Dict[str, str] = getattr(cls, "__out_names__") or {} fields: List[InputValueDefinitionNode] = [] for field in definition.fields: @@ -181,9 +178,11 @@ def __get_graphql_model_without_schema__( ) @classmethod - def __get_graphql_types__(cls, _: "GraphQLMetadata") -> Iterable["GraphQLType"]: + def __get_graphql_types__( + cls, _: "GraphQLMetadata" + ) -> Iterable[Type["GraphQLType"] | Type[Enum]]: """Returns iterable with GraphQL types associated with this type""" - types: List[GraphQLType] = [cls] + types: List[Type["GraphQLType"] | Type[Enum]] = [cls] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -221,38 +220,8 @@ def field( ) -class GraphQLInputField: - name: Optional[str] - description: Optional[str] - graphql_type: Optional[Any] - default_value: Optional[Any] - - def __init__( - self, - *, - name: Optional[str] = None, - description: Optional[str] = None, - graphql_type: Optional[Any] = None, - default_value: Optional[Any] = None, - ): - self.name = name - self.description = description - self.graphql_type = graphql_type - self.default_value = default_value - - -@dataclass(frozen=True) -class GraphQLInputModel(GraphQLModel): - out_type: Any - out_names: Dict[str, str] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = InputTypeBindable(self.name, self.out_type, self.out_names) - bindable.bind_to_schema(schema) - - def get_field_node_from_type_hint( - parent_type: GraphQLInput, + parent_type: Type[GraphQLInput], metadata: GraphQLMetadata, field_name: str, field_type: Any, @@ -270,118 +239,3 @@ def get_field_node_from_type_hint( type=get_type_node(metadata, field_type, parent_type), default_value=default_value, ) - - -def validate_input_type_with_schema(cls: Type[GraphQLInput]) -> Dict[str, Any]: - definition = cast( - InputObjectTypeDefinitionNode, - parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), - ) - - if not isinstance(definition, InputObjectTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{InputObjectTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - if not definition.fields: - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an input type without any fields. " - ) - - fields_names: List[str] = [field.name.value for field in definition.fields] - used_out_names: List[str] = [] - - out_names: Dict[str, str] = getattr(cls, "__out_names__", {}) or {} - for field_name, out_name in out_names.items(): - if field_name not in fields_names: - raise ValueError( - f"Class '{cls.__name__}' defines an outname for '{field_name}' " - "field in it's '__out_names__' attribute which is not defined " - "in '__schema__'." - ) - - if out_name in used_out_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple fields with an outname " - f"'{out_name}' in it's '__out_names__' attribute." - ) - - used_out_names.append(out_name) - - return get_input_type_with_schema_kwargs(cls, definition, out_names) - - -def get_input_type_with_schema_kwargs( - cls: Type[GraphQLInput], - definition: InputObjectTypeDefinitionNode, - out_names: Dict[str, str], -) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} - for field in definition.fields: - try: - python_name = out_names[field.name.value] - except KeyError: - python_name = convert_graphql_name_to_python(field.name.value) - - attr_default_value = getattr(cls, python_name, None) - if attr_default_value is not None and not callable(attr_default_value): - default_value = attr_default_value - elif field.default_value: - default_value = get_value_from_node(field.default_value) - else: - default_value = None - - kwargs[python_name] = default_value - - return kwargs - - -def validate_input_type(cls: Type[GraphQLInput]) -> Dict[str, Any]: - if cls.__out_names__: - raise ValueError( - f"Class '{cls.__name__}' defines '__out_names__' attribute. " - "This is not supported for types not defining '__schema__'." - ) - - return get_input_type_kwargs(cls) - - -def get_input_type_kwargs(cls: Type[GraphQLInput]) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} - - for attr_name in cls.__annotations__: - if attr_name.startswith("__"): - continue - - attr_value = getattr(cls, attr_name, None) - if isinstance(attr_value, GraphQLInputField): - validate_field_default_value(cls, attr_name, attr_value.default_value) - kwargs[attr_name] = attr_value.default_value - elif not callable(attr_value): - validate_field_default_value(cls, attr_name, attr_value) - kwargs[attr_name] = attr_value - - return kwargs - - -def validate_field_default_value( - cls: Type[GraphQLInput], field_name: str, default_value: Any -): - if default_value is None: - return - - try: - get_value_node(default_value) - except TypeError as e: - raise TypeError( - f"Class '{cls.__name__}' defines default value " - f"for the '{field_name}' field that can't be " - "represented in GraphQL schema." - ) from e diff --git a/ariadne_graphql_modules/next/graphql_input/validators.py b/ariadne_graphql_modules/next/graphql_input/validators.py new file mode 100644 index 0000000..852a778 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_input/validators.py @@ -0,0 +1,126 @@ +from typing import Any, Dict, List, Type, cast, TYPE_CHECKING + +from graphql import InputObjectTypeDefinitionNode +from ..convert_name import convert_graphql_name_to_python +from ..validators import validate_description, validate_name +from ..value import get_value_from_node, get_value_node +from ...utils import parse_definition +from .input_field import GraphQLInputField + +if TYPE_CHECKING: + from .input_type import GraphQLInput + + +def validate_input_type_with_schema(cls: Type["GraphQLInput"]) -> Dict[str, Any]: + definition = cast( + InputObjectTypeDefinitionNode, + parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), + ) + + if not isinstance(definition, InputObjectTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{InputObjectTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + if not definition.fields: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an input type without any fields. " + ) + + fields_names: List[str] = [field.name.value for field in definition.fields] + used_out_names: List[str] = [] + + out_names: Dict[str, str] = getattr(cls, "__out_names__", {}) or {} + for field_name, out_name in out_names.items(): + if field_name not in fields_names: + raise ValueError( + f"Class '{cls.__name__}' defines an outname for '{field_name}' " + "field in it's '__out_names__' attribute which is not defined " + "in '__schema__'." + ) + + if out_name in used_out_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with an outname " + f"'{out_name}' in it's '__out_names__' attribute." + ) + + used_out_names.append(out_name) + + return get_input_type_with_schema_kwargs(cls, definition, out_names) + + +def get_input_type_with_schema_kwargs( + cls: Type["GraphQLInput"], + definition: InputObjectTypeDefinitionNode, + out_names: Dict[str, str], +) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} + for field in definition.fields: + try: + python_name = out_names[field.name.value] + except KeyError: + python_name = convert_graphql_name_to_python(field.name.value) + + attr_default_value = getattr(cls, python_name, None) + if attr_default_value is not None and not callable(attr_default_value): + default_value = attr_default_value + elif field.default_value: + default_value = get_value_from_node(field.default_value) + else: + default_value = None + + kwargs[python_name] = default_value + + return kwargs + + +def validate_input_type(cls: Type["GraphQLInput"]) -> Dict[str, Any]: + if cls.__out_names__: + raise ValueError( + f"Class '{cls.__name__}' defines '__out_names__' attribute. " + "This is not supported for types not defining '__schema__'." + ) + + return get_input_type_kwargs(cls) + + +def get_input_type_kwargs(cls: Type["GraphQLInput"]) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} + + for attr_name in cls.__annotations__: + if attr_name.startswith("__"): + continue + + attr_value = getattr(cls, attr_name, None) + if isinstance(attr_value, GraphQLInputField): + validate_field_default_value(cls, attr_name, attr_value.default_value) + kwargs[attr_name] = attr_value.default_value + elif not callable(attr_value): + validate_field_default_value(cls, attr_name, attr_value) + kwargs[attr_name] = attr_value + + return kwargs + + +def validate_field_default_value( + cls: Type["GraphQLInput"], field_name: str, default_value: Any +): + if default_value is None: + return + + try: + get_value_node(default_value) + except TypeError as e: + raise TypeError( + f"Class '{cls.__name__}' defines default value " + f"for the '{field_name}' field that can't be " + "represented in GraphQL schema." + ) from e diff --git a/ariadne_graphql_modules/next/graphql_interface/__init__.py b/ariadne_graphql_modules/next/graphql_interface/__init__.py new file mode 100644 index 0000000..7ccd6d6 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_interface/__init__.py @@ -0,0 +1,8 @@ +from .interface_type import GraphQLInterface +from .interface_model import GraphQLInterfaceModel + + +__all__ = [ + "GraphQLInterface", + "GraphQLInterfaceModel", +] diff --git a/ariadne_graphql_modules/next/graphql_interface/interface_model.py b/ariadne_graphql_modules/next/graphql_interface/interface_model.py new file mode 100644 index 0000000..cd93a8f --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_interface/interface_model.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import Any, Callable, Dict, cast + +from ariadne import InterfaceType +from ariadne.types import Resolver +from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema + +from ..base import GraphQLModel + + +@dataclass(frozen=True) +class GraphQLInterfaceModel(GraphQLModel): + resolvers: Dict[str, Resolver] + resolve_type: Callable[[Any], Any] + out_names: Dict[str, Dict[str, str]] + aliases: Dict[str, str] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = InterfaceType(self.name, self.resolve_type) + for field, resolver in self.resolvers.items(): + bindable.set_field(field, resolver) + for alias, target in self.aliases.items(): + bindable.set_alias(alias, target) + + bindable.bind_to_schema(schema) + + graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) + for field_name, field_out_names in self.out_names.items(): + graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) + for arg_name, out_name in field_out_names.items(): + graphql_field.args[arg_name].out_name = out_name diff --git a/ariadne_graphql_modules/next/interfacetype.py b/ariadne_graphql_modules/next/graphql_interface/interface_type.py similarity index 76% rename from ariadne_graphql_modules/next/interfacetype.py rename to ariadne_graphql_modules/next/graphql_interface/interface_type.py index a7def02..435057c 100644 --- a/ariadne_graphql_modules/next/interfacetype.py +++ b/ariadne_graphql_modules/next/graphql_interface/interface_type.py @@ -1,40 +1,28 @@ -from dataclasses import dataclass -from typing import ( - Any, - Callable, - Dict, - List, - cast, -) +from typing import Any, Dict, List, cast -from ariadne import InterfaceType from ariadne.types import Resolver from graphql import ( FieldDefinitionNode, - GraphQLField, - GraphQLObjectType, - GraphQLSchema, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, NamedTypeNode, - InterfaceTypeDefinitionNode, + StringValueNode, ) -from .value import get_value_node - -from .objecttype import ( - GraphQLObject, - GraphQLObjectResolver, +from ...utils import parse_definition +from ..base import GraphQLMetadata +from ..description import get_description_node +from ..graphql_object import GraphQLObject, GraphQLObjectResolver +from ..graphql_object.object_type import get_graphql_object_data +from ..graphql_object.utils import ( get_field_args_from_resolver, get_field_args_out_names, get_field_node_from_obj_field, - get_graphql_object_data, update_field_args_options, ) - -from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel -from .description import get_description_node +from ..value import get_value_node +from .interface_model import GraphQLInterfaceModel class GraphQLInterface(GraphQLObject): @@ -48,8 +36,8 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLInterfaceModel": parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), ) - descriptions: Dict[str, str] = {} - args_descriptions: Dict[str, Dict[str, str]] = {} + descriptions: Dict[str, StringValueNode] = {} + args_descriptions: Dict[str, Dict[str, StringValueNode]] = {} args_defaults: Dict[str, Dict[str, Any]] = {} resolvers: Dict[str, Resolver] = {} out_names: Dict[str, Dict[str, str]] = {} @@ -58,10 +46,9 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLInterfaceModel": cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectResolver): resolvers[cls_attr.field] = cls_attr.resolver - if cls_attr.description: - descriptions[cls_attr.field] = get_description_node( - cls_attr.description - ) + description_node = get_description_node(cls_attr.description) + if description_node: + descriptions[cls_attr.field] = description_node field_args = get_field_args_from_resolver(cls_attr.resolver) if field_args: @@ -146,9 +133,9 @@ def __get_graphql_model_without_schema__( for attr_name, field in type_data.fields.items(): fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) - if attr_name in type_aliases: + if attr_name in type_aliases and field.name: aliases[field.name] = type_aliases[attr_name] - elif attr_name != field.name and not field.resolver: + elif field.name and attr_name != field.name and not field.resolver: aliases[field.name] = attr_name if field.resolver and field.name: @@ -186,26 +173,3 @@ def resolve_type(obj: Any, *_) -> str: raise ValueError( f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." ) - - -@dataclass(frozen=True) -class GraphQLInterfaceModel(GraphQLModel): - resolvers: Dict[str, Resolver] - resolve_type: Callable[[Any], Any] - out_names: Dict[str, Dict[str, str]] - aliases: Dict[str, str] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = InterfaceType(self.name, self.resolve_type) - for field, resolver in self.resolvers.items(): - bindable.set_field(field, resolver) - for alias, target in self.aliases.items(): - bindable.set_alias(alias, target) - - bindable.bind_to_schema(schema) - - graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) - for field_name, field_out_names in self.out_names.items(): - graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) - for arg_name, out_name in field_out_names.items(): - graphql_field.args[arg_name].out_name = out_name diff --git a/ariadne_graphql_modules/next/graphql_object/__init__.py b/ariadne_graphql_modules/next/graphql_object/__init__.py new file mode 100644 index 0000000..a2b2c67 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_object/__init__.py @@ -0,0 +1,32 @@ +from .object_field import ( + object_field, + GraphQLObjectResolver, + GraphQLObjectSource, + object_subscriber, + GraphQLObjectFieldArg, +) +from .object_type import GraphQLObject, get_graphql_object_data +from .object_model import GraphQLObjectModel +from .utils import ( + get_field_args_from_resolver, + get_field_args_out_names, + get_field_node_from_obj_field, + update_field_args_options, + get_field_args_from_subscriber, +) + +__all__ = [ + "GraphQLObject", + "object_field", + "GraphQLObjectModel", + "get_field_args_from_resolver", + "get_field_args_out_names", + "get_field_node_from_obj_field", + "update_field_args_options", + "GraphQLObjectResolver", + "get_graphql_object_data", + "GraphQLObjectSource", + "object_subscriber", + "get_field_args_from_subscriber", + "GraphQLObjectFieldArg", +] diff --git a/ariadne_graphql_modules/next/graphql_object/object_field.py b/ariadne_graphql_modules/next/graphql_object/object_field.py new file mode 100644 index 0000000..15b5491 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_object/object_field.py @@ -0,0 +1,124 @@ +from dataclasses import dataclass +from typing import Any, Dict, Optional +from ariadne.types import Resolver, Subscriber + + +@dataclass(frozen=True) +class GraphQLObjectFieldArg: + name: Optional[str] + out_name: Optional[str] + field_type: Optional[Any] + description: Optional[str] = None + default_value: Optional[Any] = None + + +class GraphQLObjectField: + def __init__( + self, + *, + name: Optional[str] = None, + description: Optional[str] = None, + field_type: Optional[Any] = None, + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + resolver: Optional[Resolver] = None, + subscriber: Optional[Subscriber] = None, + default_value: Optional[Any] = None, + ): + self.name = name + self.description = description + self.field_type = field_type + self.args = args + self.resolver = resolver + self.subscriber = subscriber + self.default_value = default_value + + def __call__(self, resolver: Resolver): + """Makes GraphQLObjectField instances work as decorators.""" + self.resolver = resolver + if not self.field_type: + self.field_type = get_field_type_from_resolver(resolver) + return self + + +@dataclass(frozen=True) +class GraphQLObjectResolver: + resolver: Resolver + field: str + description: Optional[str] = None + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None + field_type: Optional[Any] = None + + +@dataclass(frozen=True) +class GraphQLObjectSource: + subscriber: Subscriber + field: str + description: Optional[str] = None + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None + field_type: Optional[Any] = None + + +def object_field( + resolver: Optional[Resolver] = None, + *, + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + name: Optional[str] = None, + description: Optional[str] = None, + graphql_type: Optional[Any] = None, + default_value: Optional[Any] = None, +) -> GraphQLObjectField: + field_type: Any = graphql_type + if not graphql_type and resolver: + field_type = get_field_type_from_resolver(resolver) + return GraphQLObjectField( + name=name, + description=description, + field_type=field_type, + args=args, + resolver=resolver, + default_value=default_value, + ) + + +def get_field_type_from_resolver(resolver: Resolver) -> Any: + return resolver.__annotations__.get("return") + + +def get_field_type_from_subscriber(subscriber: Subscriber) -> Any: + return subscriber.__annotations__.get("return") + + +def object_resolver( + field: str, + graphql_type: Optional[Any] = None, + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + description: Optional[str] = None, +): + def object_resolver_factory(f: Resolver) -> GraphQLObjectResolver: + return GraphQLObjectResolver( + args=args, + description=description, + resolver=f, + field=field, + field_type=graphql_type or get_field_type_from_resolver(f), + ) + + return object_resolver_factory + + +def object_subscriber( + field: str, + graphql_type: Optional[Any] = None, + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + description: Optional[str] = None, +): + def object_subscriber_factory(f: Subscriber) -> GraphQLObjectSource: + return GraphQLObjectSource( + args=args, + description=description, + subscriber=f, + field=field, + field_type=graphql_type or get_field_type_from_subscriber(f), + ) + + return object_subscriber_factory diff --git a/ariadne_graphql_modules/next/graphql_object/object_model.py b/ariadne_graphql_modules/next/graphql_object/object_model.py new file mode 100644 index 0000000..7a75065 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_object/object_model.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import Dict, cast + +from ariadne import ObjectType as ObjectTypeBindable +from ariadne.types import Resolver +from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema + +from ariadne_graphql_modules.next.base import GraphQLModel + + +@dataclass(frozen=True) +class GraphQLObjectModel(GraphQLModel): + resolvers: Dict[str, Resolver] + aliases: Dict[str, str] + out_names: Dict[str, Dict[str, str]] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = ObjectTypeBindable(self.name) + + for field, resolver in self.resolvers.items(): + bindable.set_field(field, resolver) + for alias, target in self.aliases.items(): + bindable.set_alias(alias, target) + + bindable.bind_to_schema(schema) + + graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) + for field_name, field_out_names in self.out_names.items(): + graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) + for arg_name, out_name in field_out_names.items(): + graphql_field.args[arg_name].out_name = out_name diff --git a/ariadne_graphql_modules/next/graphql_object/object_type.py b/ariadne_graphql_modules/next/graphql_object/object_type.py new file mode 100644 index 0000000..d7cdcf0 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_object/object_type.py @@ -0,0 +1,449 @@ +from copy import deepcopy +from dataclasses import dataclass +from enum import Enum +from typing import ( + Any, + Dict, + Iterable, + List, + Optional, + Type, + Union, + cast, +) + +from ariadne.types import Resolver, Subscriber +from graphql import ( + FieldDefinitionNode, + InputValueDefinitionNode, + NameNode, + NamedTypeNode, + ObjectTypeDefinitionNode, + StringValueNode, +) + +from .object_field import ( + GraphQLObjectField, + GraphQLObjectFieldArg, + GraphQLObjectResolver, + GraphQLObjectSource, + object_field, + object_resolver, +) +from .object_model import GraphQLObjectModel +from .utils import ( + get_field_args_from_resolver, + get_field_args_from_subscriber, + get_field_args_out_names, + get_field_node_from_obj_field, + update_field_args_options, +) + +from ...utils import parse_definition +from ..base import GraphQLMetadata, GraphQLModel, GraphQLType +from ..convert_name import convert_python_name_to_graphql +from ..description import get_description_node +from ..typing import get_graphql_type +from .validators import ( + validate_object_type_with_schema, + validate_object_type_without_schema, +) +from ..value import get_value_node + + +@dataclass(frozen=True) +class GraphQLObjectData: + fields: Dict[str, "GraphQLObjectField"] + interfaces: List[str] + + +class GraphQLObject(GraphQLType): + __kwargs__: Dict[str, Any] + __abstract__: bool = True + __schema__: Optional[str] + __description__: Optional[str] + __aliases__: Optional[Dict[str, str]] + __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] + __implements__: Optional[Iterable[Type[GraphQLType]]] + + def __init__(self, **kwargs: Any): + for kwarg in kwargs: + if kwarg not in self.__kwargs__: + valid_kwargs = "', '".join(self.__kwargs__) + raise TypeError( + f"{type(self).__name__}.__init__() got an unexpected " + f"keyword argument '{kwarg}'. " + f"Valid keyword arguments: '{valid_kwargs}'" + ) + + for kwarg, default in self.__kwargs__.items(): + setattr(self, kwarg, kwargs.get(kwarg, deepcopy(default))) + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) + cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) + else: + cls.__kwargs__ = validate_object_type_without_schema(cls) + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": + name = cls.__get_graphql_name__() + metadata.set_graphql_name(cls, name) + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__() + + return cls.__get_graphql_model_without_schema__(metadata, name) + + @classmethod + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": + definition = cast( + ObjectTypeDefinitionNode, + parse_definition(ObjectTypeDefinitionNode, cls.__schema__), + ) + + descriptions: Dict[str, StringValueNode] = {} + args_descriptions: Dict[str, Dict[str, StringValueNode]] = {} + args_defaults: Dict[str, Dict[str, Any]] = {} + resolvers: Dict[str, Resolver] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers[cls_attr.field] = cls_attr.resolver + description_node = get_description_node(cls_attr.description) + if description_node: + descriptions[cls_attr.field] = description_node + + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + args_descriptions[cls_attr.field] = {} + args_defaults[cls_attr.field] = {} + + final_args = update_field_args_options(field_args, cls_attr.args) + + for arg_name, arg_options in final_args.items(): + arg_description = get_description_node(arg_options.description) + if arg_description: + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description + + arg_default = arg_options.default_value + if arg_default is not None: + args_defaults[cls_attr.field][arg_name] = get_value_node( + arg_default + ) + + fields: List[FieldDefinitionNode] = [] + for field in definition.fields: + field_args_descriptions = args_descriptions.get(field.name.value, {}) + field_args_defaults = args_defaults.get(field.name.value, {}) + + args: List[InputValueDefinitionNode] = [] + for arg in field.arguments: + arg_name = arg.name.value + args.append( + InputValueDefinitionNode( + description=( + arg.description or field_args_descriptions.get(arg_name) + ), + name=arg.name, + directives=arg.directives, + type=arg.type, + default_value=( + arg.default_value or field_args_defaults.get(arg_name) + ), + ) + ) + + fields.append( + FieldDefinitionNode( + name=field.name, + description=( + field.description or descriptions.get(field.name.value) + ), + directives=field.directives, + arguments=tuple(args), + type=field.type, + ) + ) + + return GraphQLObjectModel( + name=definition.name.value, + ast_type=ObjectTypeDefinitionNode, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=definition.name.value), + fields=tuple(fields), + interfaces=definition.interfaces, + ), + resolvers=resolvers, + aliases=getattr(cls, "__aliases__", {}), + out_names=out_names, + ) + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": + type_data = get_graphql_object_data(metadata, cls) + type_aliases = getattr(cls, "__aliases__", {}) + + fields_ast: List[FieldDefinitionNode] = [] + resolvers: Dict[str, Resolver] = {} + aliases: Dict[str, str] = {} + out_names: Dict[str, Dict[str, str]] = {} + + for attr_name, field in type_data.fields.items(): + fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) + + if attr_name in type_aliases and field.name: + aliases[field.name] = type_aliases[attr_name] + elif field.name and attr_name != field.name and not field.resolver: + aliases[field.name] = attr_name + + if field.resolver and field.name: + resolvers[field.name] = field.resolver + + if field.args and field.name: + out_names[field.name] = get_field_args_out_names(field.args) + + interfaces_ast: List[NamedTypeNode] = [] + for interface_name in type_data.interfaces: + interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) + + return GraphQLObjectModel( + name=name, + ast_type=ObjectTypeDefinitionNode, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + fields=tuple(fields_ast), + interfaces=tuple(interfaces_ast), + ), + resolvers=resolvers, + aliases=aliases, + out_names=out_names, + ) + + @classmethod + def __get_graphql_types__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: + """Returns iterable with GraphQL types associated with this type""" + if getattr(cls, "__schema__", None): + return cls.__get_graphql_types_with_schema__(metadata) + + return cls.__get_graphql_types_without_schema__(metadata) + + @classmethod + def __get_graphql_types_with_schema__( + cls, _: "GraphQLMetadata" + ) -> Iterable[Type["GraphQLType"]]: + types: List[Type["GraphQLType"]] = [cls] + types.extend(getattr(cls, "__requires__", [])) + types.extend(getattr(cls, "__implements__", [])) + return types + + @classmethod + def __get_graphql_types_without_schema__( + cls, metadata: "GraphQLMetadata" + ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: + types: List[Union[Type["GraphQLType"], Type[Enum]]] = [cls] + type_data = get_graphql_object_data(metadata, cls) + + for field in type_data.fields.values(): + field_type = get_graphql_type(field.field_type) + if field_type and field_type not in types: + types.append(field_type) + + if field.args: + for field_arg in field.args.values(): + field_arg_type = get_graphql_type(field_arg.field_type) + if field_arg_type and field_arg_type not in types: + types.append(field_arg_type) + + return types + + @staticmethod + def field( + f: Optional[Resolver] = None, + *, + name: Optional[str] = None, + graphql_type: Optional[Any] = None, + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + description: Optional[str] = None, + default_value: Optional[Any] = None, + ) -> Any: + """Shortcut for object_field()""" + return object_field( + f, + args=args, + name=name, + graphql_type=graphql_type, + description=description, + default_value=default_value, + ) + + @staticmethod + def resolver( + field: str, + graphql_type: Optional[Any] = None, + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + description: Optional[str] = None, + ): + """Shortcut for object_resolver()""" + return object_resolver( + args=args, + field=field, + graphql_type=graphql_type, + description=description, + ) + + @staticmethod + def argument( + name: Optional[str] = None, + description: Optional[str] = None, + graphql_type: Optional[Any] = None, + default_value: Optional[Any] = None, + ) -> GraphQLObjectFieldArg: + return GraphQLObjectFieldArg( + name=name, + out_name=None, + field_type=graphql_type, + description=description, + default_value=default_value, + ) + + +def get_graphql_object_data( + metadata: GraphQLMetadata, cls: Type[GraphQLObject] +) -> GraphQLObjectData: + try: + return metadata.get_data(cls) + except KeyError as exc: + if getattr(cls, "__schema__", None): + raise NotImplementedError( + "'get_graphql_object_data' is not supported for " + "objects with '__schema__'." + ) from exc + object_data = create_graphql_object_data_without_schema(cls) + + metadata.set_data(cls, object_data) + return object_data + + +def create_graphql_object_data_without_schema( + cls: Type["GraphQLObject"], +) -> GraphQLObjectData: + fields_types: Dict[str, str] = {} + fields_names: Dict[str, str] = {} + fields_descriptions: Dict[str, str] = {} + fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} + fields_resolvers: Dict[str, Resolver] = {} + fields_subscribers: Dict[str, Subscriber] = {} + fields_defaults: Dict[str, Any] = {} + fields_order: List[str] = [] + + type_hints = cls.__annotations__ + + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + aliases_targets: List[str] = list(aliases.values()) + + interfaces: List[str] = [ + interface.__name__ for interface in getattr(cls, "__implements__", []) + ] + + for attr_name, attr_type in type_hints.items(): + if attr_name.startswith("__"): + continue + + if attr_name in aliases_targets: + # Alias target is not included in schema + # unless its explicit field + cls_attr = getattr(cls, attr_name, None) + if not isinstance(cls_attr, GraphQLObjectField): + continue + + fields_order.append(attr_name) + + fields_names[attr_name] = convert_python_name_to_graphql(attr_name) + fields_types[attr_name] = attr_type + + for attr_name in dir(cls): + if attr_name.startswith("__"): + continue + + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if attr_name not in fields_order: + fields_order.append(attr_name) + + fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( + attr_name + ) + + if cls_attr.field_type: + fields_types[attr_name] = cls_attr.field_type + if cls_attr.description: + fields_descriptions[attr_name] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[attr_name] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[attr_name] = update_field_args_options( + field_args, cls_attr.args + ) + if cls_attr.default_value: + fields_defaults[attr_name] = cls_attr.default_value + + elif isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.field_type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.field_type + if cls_attr.description: + fields_descriptions[cls_attr.field] = cls_attr.description + fields_resolvers[cls_attr.field] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args and not fields_args.get(cls_attr.field): + fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + elif isinstance(cls_attr, GraphQLObjectSource): + if cls_attr.field_type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.field_type + if cls_attr.description: + fields_descriptions[cls_attr.field] = cls_attr.description + fields_subscribers[cls_attr.field] = cls_attr.subscriber + field_args = get_field_args_from_subscriber(cls_attr.subscriber) + if field_args: + fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + + elif attr_name not in aliases_targets and not callable(cls_attr): + fields_defaults[attr_name] = cls_attr + + fields: Dict[str, "GraphQLObjectField"] = {} + for field_name in fields_order: + fields[field_name] = GraphQLObjectField( + name=fields_names[field_name], + description=fields_descriptions.get(field_name), + field_type=fields_types[field_name], + args=fields_args.get(field_name), + resolver=fields_resolvers.get(field_name), + subscriber=fields_subscribers.get(field_name), + default_value=fields_defaults.get(field_name), + ) + + return GraphQLObjectData(fields=fields, interfaces=interfaces) diff --git a/ariadne_graphql_modules/next/graphql_object/utils.py b/ariadne_graphql_modules/next/graphql_object/utils.py new file mode 100644 index 0000000..e562287 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_object/utils.py @@ -0,0 +1,190 @@ +from dataclasses import replace +from inspect import signature +from typing import TYPE_CHECKING, Dict, Optional, Tuple, Type + +from ariadne.types import Resolver, Subscriber +from graphql import FieldDefinitionNode, InputValueDefinitionNode, NameNode + +from ..base import GraphQLMetadata +from ..convert_name import convert_python_name_to_graphql +from ..description import get_description_node +from ..typing import get_type_node +from ..value import get_value_node +from .object_field import GraphQLObjectField, GraphQLObjectFieldArg + +if TYPE_CHECKING: + from .object_type import GraphQLObject + + +def get_field_node_from_obj_field( + parent_type: Type["GraphQLObject"], + metadata: GraphQLMetadata, + field: GraphQLObjectField, +) -> FieldDefinitionNode: + return FieldDefinitionNode( + description=get_description_node(field.description), + name=NameNode(value=field.name), + type=get_type_node(metadata, field.field_type, parent_type), + arguments=get_field_args_nodes_from_obj_field_args(metadata, field.args), + ) + + +def get_field_args_from_resolver( + resolver: Resolver, +) -> Dict[str, GraphQLObjectFieldArg]: + resolver_signature = signature(resolver) + type_hints = resolver.__annotations__ + type_hints.pop("return", None) + + field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args_start = 0 + + # Fist pass: (arg, *_, something, something) or (arg, *, something, something): + for i, param in enumerate(resolver_signature.parameters.values()): + param_repr = str(param) + if param_repr.startswith("*") and not param_repr.startswith("**"): + field_args_start = i + 1 + break + else: + if len(resolver_signature.parameters) < 2: + raise TypeError( + f"Resolver function '{resolver_signature}' should accept at least " + "'obj' and 'info' positional arguments." + ) + + field_args_start = 2 + + args_parameters = tuple(resolver_signature.parameters.items())[field_args_start:] + if not args_parameters: + return field_args + + for param_name, param in args_parameters: + if param.default != param.empty: + param_default = param.default + else: + param_default = None + + field_args[param_name] = GraphQLObjectFieldArg( + name=convert_python_name_to_graphql(param_name), + out_name=param_name, + field_type=type_hints.get(param_name), + default_value=param_default, + ) + + return field_args + + +def get_field_args_from_subscriber( + subscriber: Subscriber, +) -> Dict[str, GraphQLObjectFieldArg]: + subscriber_signature = signature(subscriber) + type_hints = subscriber.__annotations__ + type_hints.pop("return", None) + + field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args_start = 0 + + # Fist pass: (arg, *_, something, something) or (arg, *, something, something): + for i, param in enumerate(subscriber_signature.parameters.values()): + param_repr = str(param) + if param_repr.startswith("*") and not param_repr.startswith("**"): + field_args_start = i + 1 + break + else: + if len(subscriber_signature.parameters) < 2: + raise TypeError( + f"Subscriber function '{subscriber_signature}' should accept at least " + "'obj' and 'info' positional arguments." + ) + + field_args_start = 2 + + args_parameters = tuple(subscriber_signature.parameters.items())[field_args_start:] + if not args_parameters: + return field_args + + for param_name, param in args_parameters: + if param.default != param.empty: + param_default = param.default + else: + param_default = None + + field_args[param_name] = GraphQLObjectFieldArg( + name=convert_python_name_to_graphql(param_name), + out_name=param_name, + field_type=type_hints.get(param_name), + default_value=param_default, + ) + + return field_args + + +def get_field_args_out_names( + field_args: Dict[str, GraphQLObjectFieldArg], +) -> Dict[str, str]: + out_names: Dict[str, str] = {} + for field_arg in field_args.values(): + if field_arg.name and field_arg.out_name: + out_names[field_arg.name] = field_arg.out_name + return out_names + + +def get_field_arg_node_from_obj_field_arg( + metadata: GraphQLMetadata, + field_arg: GraphQLObjectFieldArg, +) -> InputValueDefinitionNode: + if field_arg.default_value is not None: + default_value = get_value_node(field_arg.default_value) + else: + default_value = None + + return InputValueDefinitionNode( + description=get_description_node(field_arg.description), + name=NameNode(value=field_arg.name), + type=get_type_node(metadata, field_arg.field_type), + default_value=default_value, + ) + + +def get_field_args_nodes_from_obj_field_args( + metadata: GraphQLMetadata, + field_args: Optional[Dict[str, GraphQLObjectFieldArg]], +) -> Optional[Tuple[InputValueDefinitionNode, ...]]: + if not field_args: + return None + + return tuple( + get_field_arg_node_from_obj_field_arg(metadata, field_arg) + for field_arg in field_args.values() + ) + + +def update_field_args_options( + field_args: Dict[str, GraphQLObjectFieldArg], + args_options: Optional[Dict[str, GraphQLObjectFieldArg]], +) -> Dict[str, GraphQLObjectFieldArg]: + if not args_options: + return field_args + + updated_args: Dict[str, GraphQLObjectFieldArg] = {} + for arg_name in field_args: + arg_options = args_options.get(arg_name) + if not arg_options: + updated_args[arg_name] = field_args[arg_name] + continue + args_update = {} + if arg_options.name: + args_update["name"] = arg_options.name + if arg_options.description: + args_update["description"] = arg_options.description + if arg_options.default_value is not None: + args_update["default_value"] = arg_options.default_value + if arg_options.field_type: + args_update["field_type"] = arg_options.field_type + + if args_update: + updated_args[arg_name] = replace(field_args[arg_name], **args_update) + else: + updated_args[arg_name] = field_args[arg_name] + + return updated_args diff --git a/ariadne_graphql_modules/next/graphql_object/validators.py b/ariadne_graphql_modules/next/graphql_object/validators.py new file mode 100644 index 0000000..672ad98 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_object/validators.py @@ -0,0 +1,547 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union, cast + +from graphql import FieldDefinitionNode, ObjectTypeDefinitionNode, TypeDefinitionNode + +from ..convert_name import convert_python_name_to_graphql +from .object_field import ( + GraphQLObjectField, + GraphQLObjectFieldArg, + GraphQLObjectResolver, + GraphQLObjectSource, +) +from .utils import ( + get_field_args_from_resolver, + get_field_args_from_subscriber, +) +from ..validators import validate_description, validate_name +from ..value import get_value_node +from ...utils import parse_definition + +if TYPE_CHECKING: + from .object_type import GraphQLObject + + +@dataclass +class GraphQLObjectValidationData: + aliases: Dict[str, str] + fields_attrs: List[str] + fields_instances: Dict[str, GraphQLObjectField] + resolvers_instances: Dict[str, GraphQLObjectResolver] + sources_instances: Dict[str, GraphQLObjectSource] + + +def validate_object_type_with_schema( + cls: Type["GraphQLObject"], + valid_type: Type[TypeDefinitionNode] = ObjectTypeDefinitionNode, +) -> Dict[str, Any]: + definition = cast( + ObjectTypeDefinitionNode, parse_definition(valid_type, cls.__schema__) + ) + + if not isinstance(definition, valid_type): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{valid_type.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + if not definition.fields: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an object type without any fields. " + ) + + field_names: List[str] = [f.name.value for f in definition.fields] + field_definitions: Dict[str, FieldDefinitionNode] = { + f.name.value: f for f in definition.fields + } + + fields_resolvers: List[str] = [] + source_fields: List[str] = [] + valid_fields: str = "" + + for attr_name in dir(cls): + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + raise ValueError( + f"Class '{cls.__name__}' defines 'GraphQLObjectField' instance. " + "This is not supported for types defining '__schema__'." + ) + + if isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.field not in field_names: + valid_fields = "', '".join(sorted(field_names)) + raise ValueError( + f"Class '{cls.__name__}' defines resolver for an undefined " + f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" + ) + + if cls_attr.field in fields_resolvers: + raise ValueError( + f"Class '{cls.__name__}' defines multiple resolvers for field " + f"'{cls_attr.field}'." + ) + + fields_resolvers.append(cls_attr.field) + + if cls_attr.description and field_definitions[cls_attr.field].description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{cls_attr.field}'." + ) + + if cls_attr.args: + field_args = { + arg.name.value: arg + for arg in field_definitions[cls_attr.field].arguments + } + + for arg_name, arg_options in cls_attr.args.items(): + if arg_name not in field_args: + raise ValueError( + f"Class '{cls.__name__}' defines options for '{arg_name}' " + f"argument of the '{cls_attr.field}' field " + "that doesn't exist." + ) + + if arg_options.name: + raise ValueError( + f"Class '{cls.__name__}' defines 'name' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options.field_type: + raise ValueError( + f"Class '{cls.__name__}' defines 'type' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options.description and field_args[arg_name].description: + raise ValueError( + f"Class '{cls.__name__}' defines duplicate descriptions " + f"for '{arg_name}' argument " + f"of the '{cls_attr.field}' field." + ) + + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_options.default_value + ) + + resolver_args = get_field_args_from_resolver(cls_attr.resolver) + for arg_name, arg_obj in resolver_args.items(): + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_obj.default_value + ) + if isinstance(cls_attr, GraphQLObjectSource): + if cls_attr.field not in field_names: + valid_fields = "', '".join(sorted(field_names)) + raise ValueError( + f"Class '{cls.__name__}' defines source for an undefined " + f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" + ) + + if cls_attr.field in source_fields: + raise ValueError( + f"Class '{cls.__name__}' defines multiple sources for field " + f"'{cls_attr.field}'." + ) + + source_fields.append(cls_attr.field) + + if cls_attr.description and field_definitions[cls_attr.field].description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{cls_attr.field}'." + ) + + if cls_attr.args: + field_args = { + arg.name.value: arg + for arg in field_definitions[cls_attr.field].arguments + } + + for arg_name, arg_options in cls_attr.args.items(): + if arg_name not in field_args: + raise ValueError( + f"Class '{cls.__name__}' defines options for '{arg_name}' " + f"argument of the '{cls_attr.field}' field " + "that doesn't exist." + ) + + if arg_options.name: + raise ValueError( + f"Class '{cls.__name__}' defines 'name' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options.field_type: + raise ValueError( + f"Class '{cls.__name__}' defines 'type' option for " + f"'{arg_name}' argument of the '{cls_attr.field}' field. " + "This is not supported for types defining '__schema__'." + ) + + if arg_options.description and field_args[arg_name].description: + raise ValueError( + f"Class '{cls.__name__}' defines duplicate descriptions " + f"for '{arg_name}' argument " + f"of the '{cls_attr.field}' field." + ) + + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_options.default_value + ) + + subscriber_args = get_field_args_from_subscriber(cls_attr.subscriber) + for arg_name, arg_obj in subscriber_args.items(): + validate_field_arg_default_value( + cls, cls_attr.field, arg_name, arg_obj.default_value + ) + + aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + validate_object_aliases(cls, aliases, field_names, fields_resolvers) + + return get_object_type_with_schema_kwargs(cls, aliases, field_names) + + +def validate_object_type_without_schema(cls: Type["GraphQLObject"]) -> Dict[str, Any]: + data = get_object_type_validation_data(cls) + + # Alias target is not present in schema as a field if its not an + # explicit field (instance of GraphQLObjectField) + for alias_target in data.aliases.values(): + if ( + alias_target in data.fields_attrs + and alias_target not in data.fields_instances + ): + data.fields_attrs.remove(alias_target) + + # Validate GraphQL names for future type's fields and assert those are unique + validate_object_unique_graphql_names(cls, data.fields_attrs, data.fields_instances) + validate_object_resolvers( + cls, data.fields_attrs, data.fields_instances, data.resolvers_instances + ) + validate_object_subscribers(cls, data.fields_attrs, data.sources_instances) + validate_object_fields_args(cls) + + # Gather names of field attrs with defined resolver + fields_resolvers: List[str] = [] + for attr_name, field_instance in data.fields_instances.items(): + if field_instance.resolver: + fields_resolvers.append(attr_name) + for resolver_instance in data.resolvers_instances.values(): + fields_resolvers.append(resolver_instance.field) + + validate_object_aliases(cls, data.aliases, data.fields_attrs, fields_resolvers) + + return get_object_type_kwargs(cls, data.aliases) + + +def validate_object_unique_graphql_names( + cls: Type["GraphQLObject"], + fields_attrs: List[str], + fields_instances: Dict[str, GraphQLObjectField], +): + graphql_names: List[str] = [] + for attr_name in fields_attrs: + if attr_name in fields_instances and fields_instances[attr_name].name: + attr_graphql_name = fields_instances[attr_name].name + else: + attr_graphql_name = convert_python_name_to_graphql(attr_name) + + if not attr_graphql_name: + raise ValueError( + f"Field '{attr_name}' in class '{cls.__name__}' has " + "an invalid or empty GraphQL name." + ) + + if attr_graphql_name in graphql_names: + raise ValueError( + f"Class '{cls.__name__}' defines multiple fields with GraphQL " + f"name '{attr_graphql_name}'." + ) + graphql_names.append(attr_graphql_name) + + +def validate_object_resolvers( + cls: Type["GraphQLObject"], + fields_names: List[str], + fields_instances: Dict[str, GraphQLObjectField], + resolvers_instances: Dict[str, GraphQLObjectResolver], +): + resolvers_fields: List[str] = [] + + for field_attr, field in fields_instances.items(): + if field.resolver: + resolvers_fields.append(field_attr) + + for resolver in resolvers_instances.values(): + if resolver.field not in fields_names: + valid_fields: str = "', '".join(sorted(fields_names)) + raise ValueError( + f"Class '{cls.__name__}' defines resolver for an undefined " + f"field '{resolver.field}'. (Valid fields: '{valid_fields}')" + ) + + if resolver.field in resolvers_fields: + raise ValueError( + f"Class '{cls.__name__}' defines multiple resolvers for field " + f"'{resolver.field}'." + ) + + resolvers_fields.append(resolver.field) + + field_instance: Optional[GraphQLObjectField] = fields_instances.get( + resolver.field + ) + if field_instance: + if field_instance.description and resolver.description: + raise ValueError( + f"Class '{cls.__name__}' defines multiple descriptions " + f"for field '{resolver.field}'." + ) + + if field_instance.args and resolver.args: + raise ValueError( + f"Class '{cls.__name__}' defines multiple arguments options " + f"('args') for field '{resolver.field}'." + ) + + +def validate_object_subscribers( + cls: Type["GraphQLObject"], + fields_names: List[str], + sources_instances: Dict[str, GraphQLObjectSource], +): + source_fields: List[str] = [] + + for key, source in sources_instances.items(): + if not isinstance(source.field, str): + raise ValueError(f"The field name for {key} must be a string.") + if source.field not in fields_names: + valid_fields: str = "', '".join(sorted(fields_names)) + raise ValueError( + f"Class '{cls.__name__}' defines source for an undefined " + f"field '{source.field}'. (Valid fields: '{valid_fields}')" + ) + if source.field in source_fields: + raise ValueError( + f"Class '{cls.__name__}' defines multiple sources for field " + f"'{source.field}'." + ) + + source_fields.append(source.field) + + if source.description is not None and not isinstance(source.description, str): + raise ValueError(f"The description for {key} must be a string if provided.") + + if source.args is not None: + if not isinstance(source.args, dict): + raise ValueError( + f"The args for {key} must be a dictionary if provided." + ) + for arg_name, arg_info in source.args.items(): + if not isinstance(arg_info, GraphQLObjectFieldArg): + raise ValueError( + f"Argument {arg_name} for {key} must " + "have a GraphQLObjectFieldArg as its info." + ) + + +def validate_object_fields_args(cls: Type["GraphQLObject"]): + for field_name in dir(cls): + field_instance = getattr(cls, field_name) + if ( + isinstance(field_instance, (GraphQLObjectField, GraphQLObjectResolver)) + and field_instance.resolver + ): + validate_object_field_args(cls, field_name, field_instance) + + +def validate_object_field_args( + cls: Type["GraphQLObject"], + field_name: str, + field_instance: Union["GraphQLObjectField", "GraphQLObjectResolver"], +): + if field_instance.resolver: + resolver_args = get_field_args_from_resolver(field_instance.resolver) + if resolver_args: + for arg_name, arg_obj in resolver_args.items(): + validate_field_arg_default_value( + cls, field_name, arg_name, arg_obj.default_value + ) + + if not field_instance.args: + return # Skip extra logic for validating instance.args + + resolver_args_names = list(resolver_args.keys()) + if resolver_args_names: + error_help = "expected one of: '%s'" % ("', '".join(resolver_args_names)) + else: + error_help = "function accepts no extra arguments" + + for arg_name, arg_options in field_instance.args.items(): + if arg_name not in resolver_args_names: + if isinstance(field_instance, GraphQLObjectField): + raise ValueError( + f"Class '{cls.__name__}' defines '{field_name}' field " + f"with extra configuration for '{arg_name}' argument " + "thats not defined on the resolver function. " + f"({error_help})" + ) + + raise ValueError( + f"Class '{cls.__name__}' defines '{field_name}' resolver " + f"with extra configuration for '{arg_name}' argument " + "thats not defined on the resolver function. " + f"({error_help})" + ) + + validate_field_arg_default_value( + cls, field_name, arg_name, arg_options.default_value + ) + + +def validate_object_aliases( + cls: Type["GraphQLObject"], + aliases: Dict[str, str], + fields_names: List[str], + fields_resolvers: List[str], +): + for alias in aliases: + if alias not in fields_names: + valid_fields: str = "', '".join(sorted(fields_names)) + raise ValueError( + f"Class '{cls.__name__}' defines an alias for an undefined " + f"field '{alias}'. (Valid fields: '{valid_fields}')" + ) + + if alias in fields_resolvers: + raise ValueError( + f"Class '{cls.__name__}' defines an alias for a field " + f"'{alias}' that already has a custom resolver." + ) + + +def validate_field_arg_default_value( + cls: Type["GraphQLObject"], field_name: str, arg_name: str, default_value: Any +): + if default_value is None: + return + + try: + get_value_node(default_value) + except TypeError as e: + raise TypeError( + f"Class '{cls.__name__}' defines default value " + f"for '{arg_name}' argument " + f"of the '{field_name}' field that can't be " + "represented in GraphQL schema." + ) from e + + +def get_object_type_validation_data( + cls: Type["GraphQLObject"], +) -> GraphQLObjectValidationData: + fields_attrs: List[str] = [ + attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") + ] + + fields_instances: Dict[str, GraphQLObjectField] = {} + resolvers_instances: Dict[str, GraphQLObjectResolver] = {} + sources_instances: Dict[str, GraphQLObjectSource] = {} + + for attr_name in dir(cls): + if attr_name.startswith("__"): + continue + + cls_attr = getattr(cls, attr_name) + if isinstance(cls_attr, GraphQLObjectResolver): + resolvers_instances[attr_name] = cls_attr + if attr_name in fields_attrs: + fields_attrs.remove(attr_name) + + if isinstance(cls_attr, GraphQLObjectSource): + sources_instances[attr_name] = cls_attr + if attr_name in fields_attrs: + fields_attrs.remove(attr_name) + + elif isinstance(cls_attr, GraphQLObjectField): + fields_instances[attr_name] = cls_attr + + if attr_name not in fields_attrs: + fields_attrs.append(attr_name) + + elif callable(attr_name): + if attr_name in fields_attrs: + fields_attrs.remove(attr_name) + + return GraphQLObjectValidationData( + aliases=getattr(cls, "__aliases__", None) or {}, + fields_attrs=fields_attrs, + fields_instances=fields_instances, + resolvers_instances=resolvers_instances, + sources_instances=sources_instances, + ) + + +def get_object_type_kwargs( + cls: Type["GraphQLObject"], + aliases: Dict[str, str], +) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} + + for attr_name in cls.__annotations__: + if attr_name.startswith("__"): + continue + + kwarg_name = aliases.get(attr_name, attr_name) + kwarg_value = getattr(cls, kwarg_name, None) + if isinstance(kwarg_value, GraphQLObjectField): + kwargs[kwarg_name] = kwarg_value.default_value + elif isinstance(kwarg_value, GraphQLObjectResolver): + continue # Skip resolver instances + elif not callable(kwarg_value): + kwargs[kwarg_name] = kwarg_value + + for attr_name in dir(cls): + if attr_name.startswith("__") or attr_name in kwargs: + continue + + kwarg_name = aliases.get(attr_name, attr_name) + kwarg_value = getattr(cls, kwarg_name) + if isinstance(kwarg_value, GraphQLObjectField): + kwargs[kwarg_name] = kwarg_value.default_value + elif not callable(kwarg_value): + kwargs[kwarg_name] = kwarg_value + + return kwargs + + +def get_object_type_with_schema_kwargs( + cls: Type["GraphQLObject"], + aliases: Dict[str, str], + field_names: List[str], +) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} + + for field_name in field_names: + final_name = aliases.get(field_name, field_name) + attr_value = getattr(cls, final_name, None) + + if isinstance(attr_value, GraphQLObjectField): + kwargs[final_name] = attr_value.default_value + elif not isinstance(attr_value, GraphQLObjectResolver) and not callable( + attr_value + ): + kwargs[final_name] = attr_value + + return kwargs diff --git a/ariadne_graphql_modules/next/graphql_scalar/__init__.py b/ariadne_graphql_modules/next/graphql_scalar/__init__.py new file mode 100644 index 0000000..1c1d0d2 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_scalar/__init__.py @@ -0,0 +1,8 @@ +from .scalar_type import GraphQLScalar +from .scalar_model import GraphQLScalarModel + + +__all__ = [ + "GraphQLScalar", + "GraphQLScalarModel", +] diff --git a/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py b/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py new file mode 100644 index 0000000..e9048ce --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from typing import Any, Callable, Dict, Optional + +from graphql import GraphQLSchema, ValueNode +from ariadne import ScalarType as ScalarTypeBindable +from ..base import GraphQLModel + + +@dataclass(frozen=True) +class GraphQLScalarModel(GraphQLModel): + serialize: Callable[[Any], Any] + parse_value: Callable[[Any], Any] + parse_literal: Callable[[ValueNode, Optional[Dict[str, Any]]], Any] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = ScalarTypeBindable( + self.name, + serializer=self.serialize, + value_parser=self.parse_value, + literal_parser=self.parse_literal, + ) + + bindable.bind_to_schema(schema) diff --git a/ariadne_graphql_modules/next/scalartype.py b/ariadne_graphql_modules/next/graphql_scalar/scalar_type.py similarity index 63% rename from ariadne_graphql_modules/next/scalartype.py rename to ariadne_graphql_modules/next/graphql_scalar/scalar_type.py index 1db9eb5..75436c8 100644 --- a/ariadne_graphql_modules/next/scalartype.py +++ b/ariadne_graphql_modules/next/graphql_scalar/scalar_type.py @@ -1,19 +1,21 @@ -from dataclasses import dataclass -from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, cast +from typing import Any, Dict, Generic, Optional, TypeVar, cast -from ariadne import ScalarType as ScalarTypeBindable from graphql import ( - GraphQLSchema, NameNode, ScalarTypeDefinitionNode, ValueNode, value_from_ast_untyped, ) -from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .description import get_description_node -from .validators import validate_description, validate_name +from ..description import get_description_node +from .scalar_model import GraphQLScalarModel + +from ...utils import parse_definition + +from .validators import validate_scalar_type_with_schema + +from ..base import GraphQLMetadata, GraphQLModel, GraphQLType + T = TypeVar("T") @@ -98,35 +100,3 @@ def parse_literal( def unwrap(self) -> T: return self.wrapped_value - - -@dataclass(frozen=True) -class GraphQLScalarModel(GraphQLModel): - serialize: Callable[[Any], Any] - parse_value: Callable[[Any], Any] - parse_literal: Callable[[ValueNode, Optional[Dict[str, Any]]], Any] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = ScalarTypeBindable( - self.name, - serializer=self.serialize, - value_parser=self.parse_value, - literal_parser=self.parse_literal, - ) - - bindable.bind_to_schema(schema) - - -def validate_scalar_type_with_schema(cls: Type[GraphQLScalar]): - definition = parse_definition(cls.__name__, cls.__schema__) - - if not isinstance(definition, ScalarTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{ScalarTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) diff --git a/ariadne_graphql_modules/next/graphql_scalar/validators.py b/ariadne_graphql_modules/next/graphql_scalar/validators.py new file mode 100644 index 0000000..374c482 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_scalar/validators.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING, Type + +from graphql import ScalarTypeDefinitionNode + +from ..validators import validate_description, validate_name + +from ...utils import parse_definition + +if TYPE_CHECKING: + from .scalar_type import GraphQLScalar + + +def validate_scalar_type_with_schema(cls: Type["GraphQLScalar"]): + definition = parse_definition(cls.__name__, cls.__schema__) + + if not isinstance(definition, ScalarTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{ScalarTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) diff --git a/ariadne_graphql_modules/next/graphql_subscription/__init__.py b/ariadne_graphql_modules/next/graphql_subscription/__init__.py new file mode 100644 index 0000000..962d24e --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_subscription/__init__.py @@ -0,0 +1,8 @@ +from .subscription_type import GraphQLSubscription +from .subscription_model import GraphQLSubscriptionModel + + +__all__ = [ + "GraphQLSubscription", + "GraphQLSubscriptionModel", +] diff --git a/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py b/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py new file mode 100644 index 0000000..52dc3be --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass +from typing import Any, Callable, Dict, cast + +from ariadne import SubscriptionType +from ariadne.types import Resolver, Subscriber +from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema + +from ariadne_graphql_modules.next.base import GraphQLModel + + +@dataclass(frozen=True) +class GraphQLSubscriptionModel(GraphQLModel): + resolvers: Dict[str, Resolver] + resolve_type: Callable[[Any], Any] + out_names: Dict[str, Dict[str, str]] + aliases: Dict[str, str] + subscribers: Dict[str, Subscriber] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = SubscriptionType() + for field, resolver in self.resolvers.items(): + bindable.set_field(field, resolver) + for alias, target in self.aliases.items(): + bindable.set_alias(alias, target) + for source, generator in self.subscribers.items(): + bindable.set_source(source, generator) + bindable.bind_to_schema(schema) + + graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) + for field_name, field_out_names in self.out_names.items(): + graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) + for arg_name, out_name in field_out_names.items(): + graphql_field.args[arg_name].out_name = out_name diff --git a/ariadne_graphql_modules/next/subscriptiontype.py b/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py similarity index 76% rename from ariadne_graphql_modules/next/subscriptiontype.py rename to ariadne_graphql_modules/next/graphql_subscription/subscription_type.py index 99f339c..95757e1 100644 --- a/ariadne_graphql_modules/next/subscriptiontype.py +++ b/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py @@ -1,33 +1,25 @@ -from dataclasses import dataclass -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - cast, -) +from typing import Any, Dict, List, Optional, cast -from ariadne import SubscriptionType -from ariadne.types import Resolver, Subscriber from graphql import ( FieldDefinitionNode, - GraphQLField, - GraphQLObjectType, - GraphQLSchema, InputValueDefinitionNode, NameNode, NamedTypeNode, ObjectTypeDefinitionNode, + StringValueNode, ) +from ariadne.types import Resolver, Subscriber -from .value import get_value_node -from .objecttype import ( +from ...utils import parse_definition +from ..base import GraphQLMetadata, GraphQLModel +from ..description import get_description_node +from ..graphql_object import ( GraphQLObject, GraphQLObjectResolver, GraphQLObjectSource, + GraphQLObjectFieldArg, get_field_args_from_resolver, get_field_args_from_subscriber, get_field_args_out_names, @@ -36,10 +28,8 @@ object_subscriber, update_field_args_options, ) - -from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel -from .description import get_description_node +from ..value import get_value_node +from .subscription_model import GraphQLSubscriptionModel class GraphQLSubscription(GraphQLObject): @@ -47,14 +37,14 @@ class GraphQLSubscription(GraphQLObject): __valid_type__ = ObjectTypeDefinitionNode @classmethod - def __get_graphql_model_with_schema__(cls, *_) -> "GraphQLSubscriptionModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": definition = cast( ObjectTypeDefinitionNode, parse_definition(ObjectTypeDefinitionNode, cls.__schema__), ) - descriptions: Dict[str, str] = {} - args_descriptions: Dict[str, Dict[str, str]] = {} + descriptions: Dict[str, StringValueNode] = {} + args_descriptions: Dict[str, Dict[str, StringValueNode]] = {} args_defaults: Dict[str, Dict[str, Any]] = {} resolvers: Dict[str, Resolver] = {} subscribers: Dict[str, Subscriber] = {} @@ -64,10 +54,9 @@ def __get_graphql_model_with_schema__(cls, *_) -> "GraphQLSubscriptionModel": cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectResolver): resolvers[cls_attr.field] = cls_attr.resolver - if cls_attr.description: - descriptions[cls_attr.field] = get_description_node( - cls_attr.description - ) + description_node = get_description_node(cls_attr.description) + if description_node: + descriptions[cls_attr.field] = description_node field_args = get_field_args_from_resolver(cls_attr.resolver) if field_args: @@ -90,10 +79,9 @@ def __get_graphql_model_with_schema__(cls, *_) -> "GraphQLSubscriptionModel": ) if isinstance(cls_attr, GraphQLObjectSource): subscribers[cls_attr.field] = cls_attr.subscriber - if cls_attr.description: - descriptions[cls_attr.field] = get_description_node( - cls_attr.description - ) + description_node = get_description_node(cls_attr.description) + if description_node: + descriptions[cls_attr.field] = description_node field_args = get_field_args_from_subscriber(cls_attr.subscriber) if field_args: @@ -167,7 +155,7 @@ def __get_graphql_model_with_schema__(cls, *_) -> "GraphQLSubscriptionModel": @classmethod def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLSubscriptionModel": + ) -> "GraphQLModel": type_data = get_graphql_object_data(metadata, cls) type_aliases = getattr(cls, "__aliases__", None) or {} @@ -179,18 +167,18 @@ def __get_graphql_model_without_schema__( for attr_name, field in type_data.fields.items(): fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) - if attr_name in type_aliases: + if attr_name in type_aliases and field.name: aliases[field.name] = type_aliases[attr_name] - elif attr_name != field.name and not field.resolver: + elif field.name and attr_name != field.name and not field.resolver: aliases[field.name] = attr_name - if field.resolver: + if field.resolver and field.name: resolvers[field.name] = field.resolver - if field.subscriber: + if field.subscriber and field.name: subscribers[field.name] = field.subscriber - if field.args: + if field.args and field.name: out_names[field.name] = get_field_args_out_names(field.args) interfaces_ast: List[NamedTypeNode] = [] @@ -228,7 +216,7 @@ def resolve_type(obj: Any, *_) -> str: def source( field: str, graphql_type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, description: Optional[str] = None, ): """Shortcut for object_resolver()""" @@ -238,28 +226,3 @@ def source( graphql_type=graphql_type, description=description, ) - - -@dataclass(frozen=True) -class GraphQLSubscriptionModel(GraphQLModel): - resolvers: Dict[str, Resolver] - resolve_type: Callable[[Any], Any] - out_names: Dict[str, Dict[str, str]] - aliases: Dict[str, str] - subscribers: Dict[str, Subscriber] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = SubscriptionType() - for field, resolver in self.resolvers.items(): - bindable.set_field(field, resolver) - for alias, target in self.aliases.items(): - bindable.set_alias(alias, target) - for source, generator in self.subscribers.items(): - bindable.set_source(source, generator) - bindable.bind_to_schema(schema) - - graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) - for field_name, field_out_names in self.out_names.items(): - graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) - for arg_name, out_name in field_out_names.items(): - graphql_field.args[arg_name].out_name = out_name diff --git a/ariadne_graphql_modules/next/graphql_union/__init__.py b/ariadne_graphql_modules/next/graphql_union/__init__.py new file mode 100644 index 0000000..7e9462d --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_union/__init__.py @@ -0,0 +1,8 @@ +from .union_type import GraphQLUnion +from .union_model import GraphQLUnionModel + + +__all__ = [ + "GraphQLUnion", + "GraphQLUnionModel", +] diff --git a/ariadne_graphql_modules/next/graphql_union/union_model.py b/ariadne_graphql_modules/next/graphql_union/union_model.py new file mode 100644 index 0000000..48f0628 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_union/union_model.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Any, Callable + +from ariadne import UnionType +from graphql import GraphQLSchema + +from ..base import GraphQLModel + + +@dataclass(frozen=True) +class GraphQLUnionModel(GraphQLModel): + resolve_type: Callable[[Any], Any] + + def bind_to_schema(self, schema: GraphQLSchema): + bindable = UnionType(self.name, self.resolve_type) + bindable.bind_to_schema(schema) diff --git a/ariadne_graphql_modules/next/graphql_union/union_type.py b/ariadne_graphql_modules/next/graphql_union/union_type.py new file mode 100644 index 0000000..d74c796 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_union/union_type.py @@ -0,0 +1,88 @@ +from typing import Any, Iterable, Optional, Sequence, Type, cast + +from graphql import NameNode, NamedTypeNode, UnionTypeDefinitionNode +from ..base import GraphQLMetadata, GraphQLModel, GraphQLType +from ..description import get_description_node +from ..graphql_object.object_type import GraphQLObject +from .union_model import GraphQLUnionModel +from .validators import ( + validate_union_type, + validate_union_type_with_schema, +) +from ...utils import parse_definition + + +class GraphQLUnion(GraphQLType): + __types__: Sequence[Type[GraphQLType]] + __schema__: Optional[str] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + validate_union_type_with_schema(cls) + else: + validate_union_type(cls) + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": + name = cls.__get_graphql_name__() + metadata.set_graphql_name(cls, name) + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__() + + return cls.__get_graphql_model_without_schema__(name) + + @classmethod + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": + definition = cast( + UnionTypeDefinitionNode, + parse_definition(UnionTypeDefinitionNode, cls.__schema__), + ) + + return GraphQLUnionModel( + name=definition.name.value, + ast_type=UnionTypeDefinitionNode, + ast=definition, + resolve_type=cls.resolve_type, + ) + + @classmethod + def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": + return GraphQLUnionModel( + name=name, + ast_type=UnionTypeDefinitionNode, + ast=UnionTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + types=tuple( + NamedTypeNode(name=NameNode(value=t.__get_graphql_name__())) + for t in cls.__types__ + ), + ), + resolve_type=cls.resolve_type, + ) + + @classmethod + def __get_graphql_types__( + cls, _: "GraphQLMetadata" + ) -> Iterable[Type["GraphQLType"]]: + """Returns iterable with GraphQL types associated with this type""" + return [cls, *cls.__types__] + + @staticmethod + def resolve_type(obj: Any, *_) -> str: + if isinstance(obj, GraphQLObject): + return obj.__get_graphql_name__() + + raise ValueError( + f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." + ) diff --git a/ariadne_graphql_modules/next/graphql_union/validators.py b/ariadne_graphql_modules/next/graphql_union/validators.py new file mode 100644 index 0000000..3d7f280 --- /dev/null +++ b/ariadne_graphql_modules/next/graphql_union/validators.py @@ -0,0 +1,57 @@ +from typing import Type, cast, TYPE_CHECKING + +from graphql import UnionTypeDefinitionNode + + +from ..validators import validate_description, validate_name +from ...utils import parse_definition + +if TYPE_CHECKING: + from .union_type import GraphQLUnion + + +def validate_union_type(cls: Type["GraphQLUnion"]) -> None: + types = getattr(cls, "__types__", None) + if not types: + raise ValueError( + f"Class '{cls.__name__}' is missing a '__types__' attribute " + "with list of types belonging to a union." + ) + + +def validate_union_type_with_schema(cls: Type["GraphQLUnion"]) -> None: + definition = cast( + UnionTypeDefinitionNode, + parse_definition(UnionTypeDefinitionNode, cls.__schema__), + ) + + if not isinstance(definition, UnionTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines a '__schema__' attribute " + "with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != " + f"'{UnionTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + schema_type_names = { + type_node.name.value + for type_node in definition.types # pylint: disable=no-member + } + + class_type_names = {t.__get_graphql_name__() for t in cls.__types__} + if not class_type_names.issubset(schema_type_names): + missing_in_schema = sorted(class_type_names - schema_type_names) + missing_in_schema_str = "', '".join(missing_in_schema) + raise ValueError( + f"Types '{missing_in_schema_str}' are in '__types__' but not in '__schema__'." + ) + + if not schema_type_names.issubset(class_type_names): + missing_in_types = sorted(schema_type_names - class_type_names) + missing_in_types_str = "', '".join(missing_in_types) + raise ValueError( + f"Types '{missing_in_types_str}' are in '__schema__' but not in '__types__'." + ) diff --git a/ariadne_graphql_modules/next/idtype.py b/ariadne_graphql_modules/next/idtype.py index 4b8ac09..0b2af7c 100644 --- a/ariadne_graphql_modules/next/idtype.py +++ b/ariadne_graphql_modules/next/idtype.py @@ -6,7 +6,7 @@ class GraphQLID: value: str - def __init__(self, value: Union[int, str] = None): + def __init__(self, value: Union[int, str]): self.value = str(value) def __eq__(self, value: Any) -> bool: diff --git a/ariadne_graphql_modules/next/objecttype.py b/ariadne_graphql_modules/next/objecttype.py deleted file mode 100644 index fedb7fa..0000000 --- a/ariadne_graphql_modules/next/objecttype.py +++ /dev/null @@ -1,1286 +0,0 @@ -# pylint: disable=C0302 -from copy import deepcopy -from dataclasses import dataclass, replace -from enum import Enum -from inspect import signature -from typing import ( - Any, - Dict, - Iterable, - List, - Optional, - Tuple, - Type, - Union, - cast, -) - -from ariadne import ObjectType as ObjectTypeBindable -from ariadne.types import Resolver, Subscriber -from graphql import ( - FieldDefinitionNode, - GraphQLField, - GraphQLObjectType, - GraphQLSchema, - InputValueDefinitionNode, - NameNode, - NamedTypeNode, - ObjectTypeDefinitionNode, - TypeDefinitionNode, -) - -from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .convert_name import convert_python_name_to_graphql -from .description import get_description_node -from .typing import get_graphql_type, get_type_node -from .validators import validate_description, validate_name -from .value import get_value_node - - -class GraphQLObject(GraphQLType): - __kwargs__: Dict[str, Any] - __abstract__: bool = True - __schema__: Optional[str] - __description__: Optional[str] - __aliases__: Optional[Dict[str, str]] - __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] - __implements__: Optional[Iterable[Type[GraphQLType]]] - - def __init__(self, **kwargs: Any): - for kwarg in kwargs: - if kwarg not in self.__kwargs__: - valid_kwargs = "', '".join(self.__kwargs__) - raise TypeError( - f"{type(self).__name__}.__init__() got an unexpected " - f"keyword argument '{kwarg}'. " - f"Valid keyword arguments: '{valid_kwargs}'" - ) - - for kwarg, default in self.__kwargs__.items(): - setattr(self, kwarg, kwargs.get(kwarg, deepcopy(default))) - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.__dict__.get("__abstract__"): - return - - cls.__abstract__ = False - - if cls.__dict__.get("__schema__"): - valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) - cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) - else: - cls.__kwargs__ = validate_object_type_without_schema(cls) - - @classmethod - def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": - name = cls.__get_graphql_name__() - metadata.set_graphql_name(cls, name) - - if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__() - - return cls.__get_graphql_model_without_schema__(metadata, name) - - @classmethod - def __get_graphql_model_with_schema__(cls) -> "GraphQLObjectModel": - definition = cast( - ObjectTypeDefinitionNode, - parse_definition(ObjectTypeDefinitionNode, cls.__schema__), - ) - - descriptions: Dict[str, str] = {} - args_descriptions: Dict[str, Dict[str, str]] = {} - args_defaults: Dict[str, Dict[str, Any]] = {} - resolvers: Dict[str, Resolver] = {} - out_names: Dict[str, Dict[str, str]] = {} - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - resolvers[cls_attr.field] = cls_attr.resolver - if cls_attr.description: - descriptions[cls_attr.field] = get_description_node( - cls_attr.description - ) - - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - args_descriptions[cls_attr.field] = {} - args_defaults[cls_attr.field] = {} - - final_args = update_field_args_options(field_args, cls_attr.args) - - for arg_name, arg_options in final_args.items(): - arg_description = get_description_node(arg_options.description) - if arg_description: - args_descriptions[cls_attr.field][ - arg_name - ] = arg_description - - arg_default = arg_options.default_value - if arg_default is not None: - args_defaults[cls_attr.field][arg_name] = get_value_node( - arg_default - ) - - fields: List[FieldDefinitionNode] = [] - for field in definition.fields: - field_args_descriptions = args_descriptions.get(field.name.value, {}) - field_args_defaults = args_defaults.get(field.name.value, {}) - - args: List[InputValueDefinitionNode] = [] - for arg in field.arguments: - arg_name = arg.name.value - args.append( - InputValueDefinitionNode( - description=( - arg.description or field_args_descriptions.get(arg_name) - ), - name=arg.name, - directives=arg.directives, - type=arg.type, - default_value=( - arg.default_value or field_args_defaults.get(arg_name) - ), - ) - ) - - fields.append( - FieldDefinitionNode( - name=field.name, - description=( - field.description or descriptions.get(field.name.value) - ), - directives=field.directives, - arguments=tuple(args), - type=field.type, - ) - ) - - return GraphQLObjectModel( - name=definition.name.value, - ast_type=ObjectTypeDefinitionNode, - ast=ObjectTypeDefinitionNode( - name=NameNode(value=definition.name.value), - fields=tuple(fields), - interfaces=definition.interfaces, - ), - resolvers=resolvers, - aliases=getattr(cls, "__aliases__", {}), - out_names=out_names, - ) - - @classmethod - def __get_graphql_model_without_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLObjectModel": - type_data = get_graphql_object_data(metadata, cls) - type_aliases = getattr(cls, "__aliases__", None) or {} - - fields_ast: List[FieldDefinitionNode] = [] - resolvers: Dict[str, Resolver] = {} - aliases: Dict[str, str] = {} - out_names: Dict[str, Dict[str, str]] = {} - - for attr_name, field in type_data.fields.items(): - fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) - - if attr_name in type_aliases: - aliases[field.name] = type_aliases[attr_name] - elif attr_name != field.name and not field.resolver: - aliases[field.name] = attr_name - - if field.resolver: - resolvers[field.name] = field.resolver - - if field.args: - out_names[field.name] = get_field_args_out_names(field.args) - - interfaces_ast: List[NamedTypeNode] = [] - for interface_name in type_data.interfaces: - interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) - - return GraphQLObjectModel( - name=name, - ast_type=ObjectTypeDefinitionNode, - ast=ObjectTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node( - getattr(cls, "__description__", None), - ), - fields=tuple(fields_ast), - interfaces=tuple(interfaces_ast), - ), - resolvers=resolvers, - aliases=aliases, - out_names=out_names, - ) - - @classmethod - def __get_graphql_types__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: - """Returns iterable with GraphQL types associated with this type""" - if getattr(cls, "__schema__", None): - return cls.__get_graphql_types_with_schema__(metadata) - - return cls.__get_graphql_types_without_schema__(metadata) - - @classmethod - def __get_graphql_types_with_schema__( - cls, _: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: - types: List[GraphQLType] = [cls] - types.extend(getattr(cls, "__requires__", [])) - types.extend(getattr(cls, "__implements__", [])) - return types - - @classmethod - def __get_graphql_types_without_schema__( - cls, metadata: "GraphQLMetadata" - ) -> Iterable["GraphQLType"]: - types: List[GraphQLType] = [cls] - type_data = get_graphql_object_data(metadata, cls) - - for field in type_data.fields.values(): - field_type = get_graphql_type(field.field_type) - if field_type and field_type not in types: - types.append(field_type) - - if field.args: - for field_arg in field.args.values(): - field_arg_type = get_graphql_type(field_arg.field_type) - if field_arg_type and field_arg_type not in types: - types.append(field_arg_type) - - return types - - @staticmethod - def field( - f: Optional[Resolver] = None, - *, - name: Optional[str] = None, - graphql_type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, - description: Optional[str] = None, - default_value: Optional[Any] = None, - ) -> Any: - """Shortcut for object_field()""" - return object_field( - f, - args=args, - name=name, - graphql_type=graphql_type, - description=description, - default_value=default_value, - ) - - @staticmethod - def resolver( - field: str, - graphql_type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, - description: Optional[str] = None, - ): - """Shortcut for object_resolver()""" - return object_resolver( - args=args, - field=field, - graphql_type=graphql_type, - description=description, - ) - - @staticmethod - def argument( - name: Optional[str] = None, - description: Optional[str] = None, - graphql_type: Optional[Any] = None, - default_value: Optional[Any] = None, - ) -> dict: - options: dict = {} - if name: - options["name"] = name - if description: - options["description"] = description - if graphql_type: - options["type"] = graphql_type - if default_value: - options["default_value"] = default_value - return options - - -@dataclass(frozen=True) -class GraphQLObjectData: - fields: Dict[str, "GraphQLObjectField"] - interfaces: List[str] - - -def get_graphql_object_data( - metadata: GraphQLMetadata, cls: Type[GraphQLObject] -) -> GraphQLObjectData: - try: - return metadata.get_data(cls) - except KeyError as exc: - if getattr(cls, "__schema__", None): - raise NotImplementedError( - "'get_graphql_object_data' is not supported for " - "objects with '__schema__'." - ) from exc - object_data = create_graphql_object_data_without_schema(cls) - - metadata.set_data(cls, object_data) - return object_data - - -def create_graphql_object_data_without_schema( - cls: Type[GraphQLObject], -) -> GraphQLObjectData: - fields_types: Dict[str, str] = {} - fields_names: Dict[str, str] = {} - fields_descriptions: Dict[str, str] = {} - fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} - fields_resolvers: Dict[str, Resolver] = {} - fields_subscribers: Dict[str, Subscriber] = {} - fields_defaults: Dict[str, Any] = {} - fields_order: List[str] = [] - - type_hints = cls.__annotations__ - - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - aliases_targets: List[str] = list(aliases.values()) - - interfaces: List[str] = [ - interface.__name__ for interface in getattr(cls, "__implements__", []) - ] - - for attr_name, attr_type in type_hints.items(): - if attr_name.startswith("__"): - continue - - if attr_name in aliases_targets: - # Alias target is not included in schema - # unless its explicit field - cls_attr = getattr(cls, attr_name, None) - if not isinstance(cls_attr, GraphQLObjectField): - continue - - fields_order.append(attr_name) - - fields_names[attr_name] = convert_python_name_to_graphql(attr_name) - fields_types[attr_name] = attr_type - - for attr_name in dir(cls): - if attr_name.startswith("__"): - continue - - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - if attr_name not in fields_order: - fields_order.append(attr_name) - - fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( - attr_name - ) - - if cls_attr.field_type: - fields_types[attr_name] = cls_attr.field_type - if cls_attr.description: - fields_descriptions[attr_name] = cls_attr.description - if cls_attr.resolver: - fields_resolvers[attr_name] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - fields_args[attr_name] = update_field_args_options( - field_args, cls_attr.args - ) - if cls_attr.default_value: - fields_defaults[attr_name] = cls_attr.default_value - - elif isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.field_type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.field_type - if cls_attr.description: - fields_descriptions[cls_attr.field] = cls_attr.description - if cls_attr.resolver: - fields_resolvers[cls_attr.field] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args and not fields_args.get(cls_attr.field): - fields_args[cls_attr.field] = update_field_args_options( - field_args, cls_attr.args - ) - elif isinstance(cls_attr, GraphQLObjectSource): - if cls_attr.field_type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.field_type - if cls_attr.description: - fields_descriptions[cls_attr.field] = cls_attr.description - if cls_attr.subscriber: - fields_subscribers[cls_attr.field] = cls_attr.subscriber - field_args = get_field_args_from_subscriber(cls_attr.subscriber) - if field_args: - fields_args[cls_attr.field] = update_field_args_options( - field_args, cls_attr.args - ) - - elif attr_name not in aliases_targets and not callable(cls_attr): - fields_defaults[attr_name] = cls_attr - - fields: Dict[str, "GraphQLObjectField"] = {} - for field_name in fields_order: - fields[field_name] = GraphQLObjectField( - name=fields_names[field_name], - description=fields_descriptions.get(field_name), - field_type=fields_types[field_name], - args=fields_args.get(field_name), - resolver=fields_resolvers.get(field_name), - subscriber=fields_subscribers.get(field_name), - default_value=fields_defaults.get(field_name), - ) - - return GraphQLObjectData(fields=fields, interfaces=interfaces) - - -class GraphQLObjectField: - name: Optional[str] - description: Optional[str] - field_type: Optional[Any] - args: Optional[Dict[str, dict]] - resolver: Optional[Resolver] - subscriber: Optional[Subscriber] - default_value: Optional[Any] - - def __init__( - self, - *, - name: Optional[str] = None, - description: Optional[str] = None, - field_type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, - resolver: Optional[Resolver] = None, - subscriber: Optional[Subscriber] = None, - default_value: Optional[Any] = None, - ): - self.name = name - self.description = description - self.field_type = field_type - self.args = args - self.resolver = resolver - self.subscriber = subscriber - self.default_value = default_value - - def __call__(self, resolver: Resolver): - """Makes GraphQLObjectField instances work as decorators.""" - self.resolver = resolver - if not self.field_type: - self.field_type = get_field_type_from_resolver(resolver) - return self - - -def object_field( - resolver: Optional[Resolver] = None, - *, - args: Optional[Dict[str, dict]] = None, - name: Optional[str] = None, - description: Optional[str] = None, - graphql_type: Optional[Any] = None, - default_value: Optional[Any] = None, -) -> GraphQLObjectField: - field_type: Any = graphql_type - if not graphql_type and resolver: - field_type = get_field_type_from_resolver(resolver) - - return GraphQLObjectField( - name=name, - description=description, - field_type=field_type, - args=args, - resolver=resolver, - default_value=default_value, - ) - - -def get_field_type_from_resolver(resolver: Resolver) -> Any: - return resolver.__annotations__.get("return") - - -@dataclass(frozen=True) -class GraphQLObjectResolver: - resolver: Resolver - field: str - description: Optional[str] = None - args: Optional[Dict[str, dict]] = None - field_type: Optional[Any] = None - - -def object_resolver( - field: str, - graphql_type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, - description: Optional[str] = None, -): - def object_resolver_factory(f: Optional[Resolver]) -> GraphQLObjectResolver: - return GraphQLObjectResolver( - args=args, - description=description, - resolver=f, - field=field, - field_type=graphql_type or get_field_type_from_resolver(f), - ) - - return object_resolver_factory - - -@dataclass(frozen=True) -class GraphQLObjectSource: - subscriber: Subscriber - field: str - description: Optional[str] = None - args: Optional[Dict[str, dict]] = None - field_type: Optional[Any] = None - - -def object_subscriber( - field: str, - graphql_type: Optional[Any] = None, - args: Optional[Dict[str, dict]] = None, - description: Optional[str] = None, -): - def object_subscriber_factory(f: Optional[Subscriber]) -> GraphQLObjectSource: - return GraphQLObjectSource( - args=args, - description=description, - subscriber=f, - field=field, - field_type=graphql_type or get_field_type_from_subscriber(f), - ) - - return object_subscriber_factory - - -def get_field_type_from_subscriber(subscriber: Subscriber) -> Any: - return subscriber.__annotations__.get("return") - - -@dataclass(frozen=True) -class GraphQLObjectModel(GraphQLModel): - resolvers: Dict[str, Resolver] - aliases: Dict[str, str] - out_names: Dict[str, Dict[str, str]] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = ObjectTypeBindable(self.name) - - for field, resolver in self.resolvers.items(): - bindable.set_field(field, resolver) - for alias, target in self.aliases.items(): - bindable.set_alias(alias, target) - - bindable.bind_to_schema(schema) - - graphql_type = cast(GraphQLObjectType, schema.get_type(self.name)) - for field_name, field_out_names in self.out_names.items(): - graphql_field = cast(GraphQLField, graphql_type.fields[field_name]) - for arg_name, out_name in field_out_names.items(): - graphql_field.args[arg_name].out_name = out_name - - -def get_field_node_from_obj_field( - parent_type: GraphQLObject, - metadata: GraphQLMetadata, - field: GraphQLObjectField, -) -> FieldDefinitionNode: - return FieldDefinitionNode( - description=get_description_node(field.description), - name=NameNode(value=field.name), - type=get_type_node(metadata, field.field_type, parent_type), - arguments=get_field_args_nodes_from_obj_field_args(metadata, field.args), - ) - - -@dataclass(frozen=True) -class GraphQLObjectFieldArg: - name: Optional[str] - out_name: Optional[str] - field_type: Optional[Any] - description: Optional[str] = None - default_value: Optional[Any] = None - - -def get_field_args_from_resolver( - resolver: Resolver, -) -> Dict[str, GraphQLObjectFieldArg]: - resolver_signature = signature(resolver) - type_hints = resolver.__annotations__ - type_hints.pop("return", None) - - field_args: Dict[str, GraphQLObjectFieldArg] = {} - field_args_start = 0 - - # Fist pass: (arg, *_, something, something) or (arg, *, something, something): - for i, param in enumerate(resolver_signature.parameters.values()): - param_repr = str(param) - if param_repr.startswith("*") and not param_repr.startswith("**"): - field_args_start = i + 1 - break - else: - if len(resolver_signature.parameters) < 2: - raise TypeError( - f"Resolver function '{resolver_signature}' should accept at least " - "'obj' and 'info' positional arguments." - ) - - field_args_start = 2 - - args_parameters = tuple(resolver_signature.parameters.items())[field_args_start:] - if not args_parameters: - return field_args - - for param_name, param in args_parameters: - if param.default != param.empty: - param_default = param.default - else: - param_default = None - - field_args[param_name] = GraphQLObjectFieldArg( - name=convert_python_name_to_graphql(param_name), - out_name=param_name, - field_type=type_hints.get(param_name), - default_value=param_default, - ) - - return field_args - - -def get_field_args_from_subscriber( - subscriber: Subscriber, -) -> Dict[str, GraphQLObjectFieldArg]: - subscriber_signature = signature(subscriber) - type_hints = subscriber.__annotations__ - type_hints.pop("return", None) - - field_args: Dict[str, GraphQLObjectFieldArg] = {} - field_args_start = 0 - - # Fist pass: (arg, *_, something, something) or (arg, *, something, something): - for i, param in enumerate(subscriber_signature.parameters.values()): - param_repr = str(param) - if param_repr.startswith("*") and not param_repr.startswith("**"): - field_args_start = i + 1 - break - else: - if len(subscriber_signature.parameters) < 2: - raise TypeError( - f"Subscriber function '{subscriber_signature}' should accept at least " - "'obj' and 'info' positional arguments." - ) - - field_args_start = 2 - - args_parameters = tuple(subscriber_signature.parameters.items())[field_args_start:] - if not args_parameters: - return field_args - - for param_name, param in args_parameters: - if param.default != param.empty: - param_default = param.default - else: - param_default = None - - field_args[param_name] = GraphQLObjectFieldArg( - name=convert_python_name_to_graphql(param_name), - out_name=param_name, - field_type=type_hints.get(param_name), - default_value=param_default, - ) - - return field_args - - -def get_field_args_out_names( - field_args: Dict[str, GraphQLObjectFieldArg], -) -> Dict[str, str]: - out_names: Dict[str, str] = {} - for field_arg in field_args.values(): - out_names[field_arg.name] = field_arg.out_name - return out_names - - -def get_field_args_nodes_from_obj_field_args( - metadata: GraphQLMetadata, field_args: Optional[Dict[str, GraphQLObjectFieldArg]] -) -> Optional[Tuple[InputValueDefinitionNode]]: - if not field_args: - return None - - return tuple( - get_field_arg_node_from_obj_field_arg(metadata, field_arg) - for field_arg in field_args.values() - ) - - -def get_field_arg_node_from_obj_field_arg( - metadata: GraphQLMetadata, - field_arg: GraphQLObjectFieldArg, -) -> InputValueDefinitionNode: - if field_arg.default_value is not None: - default_value = get_value_node(field_arg.default_value) - else: - default_value = None - - return InputValueDefinitionNode( - description=get_description_node(field_arg.description), - name=NameNode(value=field_arg.name), - type=get_type_node(metadata, field_arg.field_type), - default_value=default_value, - ) - - -def update_field_args_options( - field_args: Dict[str, GraphQLObjectFieldArg], - args_options: Optional[Dict[str, dict]], -) -> Dict[str, GraphQLObjectFieldArg]: - if not args_options: - return field_args - - updated_args: Dict[str, GraphQLObjectFieldArg] = {} - for arg_name in field_args: - arg_options = args_options.get(arg_name) - if not arg_options: - updated_args[arg_name] = field_args[arg_name] - continue - args_update = {} - if arg_options.get("name"): - args_update["name"] = arg_options["name"] - if arg_options.get("description"): - args_update["description"] = arg_options["description"] - if arg_options.get("default_value") is not None: - args_update["default_value"] = arg_options["default_value"] - if arg_options.get("type"): - args_update["type"] = arg_options["type"] - - if args_update: - updated_args[arg_name] = replace(field_args[arg_name], **args_update) - else: - updated_args[arg_name] = field_args[arg_name] - - return updated_args - - -def validate_object_type_with_schema( - cls: Type[GraphQLObject], - valid_type: Type[TypeDefinitionNode] = ObjectTypeDefinitionNode, -) -> Dict[str, Any]: - definition = parse_definition(valid_type, cls.__schema__) - - if not isinstance(definition, valid_type): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{valid_type.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - if not definition.fields: - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "with declaration for an object type without any fields. " - ) - - field_names: List[str] = [f.name.value for f in definition.fields] - field_definitions: Dict[str, FieldDefinitionNode] = { - f.name.value: f for f in definition.fields - } - - fields_resolvers: List[str] = [] - source_fields: List[str] = [] - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - raise ValueError( - f"Class '{cls.__name__}' defines 'GraphQLObjectField' instance. " - "This is not supported for types defining '__schema__'." - ) - - if isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.field not in field_names: - valid_fields: str = "', '".join(sorted(field_names)) - raise ValueError( - f"Class '{cls.__name__}' defines resolver for an undefined " - f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" - ) - - if cls_attr.field in fields_resolvers: - raise ValueError( - f"Class '{cls.__name__}' defines multiple resolvers for field " - f"'{cls_attr.field}'." - ) - - fields_resolvers.append(cls_attr.field) - - if cls_attr.description and field_definitions[cls_attr.field].description: - raise ValueError( - f"Class '{cls.__name__}' defines multiple descriptions " - f"for field '{cls_attr.field}'." - ) - - if cls_attr.args: - field_args = { - arg.name.value: arg - for arg in field_definitions[cls_attr.field].arguments - } - - for arg_name, arg_options in cls_attr.args.items(): - if arg_name not in field_args: - raise ValueError( - f"Class '{cls.__name__}' defines options for '{arg_name}' " - f"argument of the '{cls_attr.field}' field " - "that doesn't exist." - ) - - if arg_options.get("name"): - raise ValueError( - f"Class '{cls.__name__}' defines 'name' option for " - f"'{arg_name}' argument of the '{cls_attr.field}' field. " - "This is not supported for types defining '__schema__'." - ) - - if arg_options.get("type"): - raise ValueError( - f"Class '{cls.__name__}' defines 'type' option for " - f"'{arg_name}' argument of the '{cls_attr.field}' field. " - "This is not supported for types defining '__schema__'." - ) - - if ( - arg_options.get("description") - and field_args[arg_name].description - ): - raise ValueError( - f"Class '{cls.__name__}' defines duplicate descriptions " - f"for '{arg_name}' argument " - f"of the '{cls_attr.field}' field." - ) - - validate_field_arg_default_value( - cls, cls_attr.field, arg_name, arg_options.get("default_value") - ) - - resolver_args = get_field_args_from_resolver(cls_attr.resolver) - for arg_name, arg_obj in resolver_args.items(): - validate_field_arg_default_value( - cls, cls_attr.field, arg_name, arg_obj.default_value - ) - if isinstance(cls_attr, GraphQLObjectSource): - if cls_attr.field not in field_names: - valid_fields: str = "', '".join(sorted(field_names)) - raise ValueError( - f"Class '{cls.__name__}' defines source for an undefined " - f"field '{cls_attr.field}'. (Valid fields: '{valid_fields}')" - ) - - if cls_attr.field in source_fields: - raise ValueError( - f"Class '{cls.__name__}' defines multiple sources for field " - f"'{cls_attr.field}'." - ) - - source_fields.append(cls_attr.field) - - if cls_attr.description and field_definitions[cls_attr.field].description: - raise ValueError( - f"Class '{cls.__name__}' defines multiple descriptions " - f"for field '{cls_attr.field}'." - ) - - if cls_attr.args: - field_args = { - arg.name.value: arg - for arg in field_definitions[cls_attr.field].arguments - } - - for arg_name, arg_options in cls_attr.args.items(): - if arg_name not in field_args: - raise ValueError( - f"Class '{cls.__name__}' defines options for '{arg_name}' " - f"argument of the '{cls_attr.field}' field " - "that doesn't exist." - ) - - if arg_options.get("name"): - raise ValueError( - f"Class '{cls.__name__}' defines 'name' option for " - f"'{arg_name}' argument of the '{cls_attr.field}' field. " - "This is not supported for types defining '__schema__'." - ) - - if arg_options.get("type"): - raise ValueError( - f"Class '{cls.__name__}' defines 'type' option for " - f"'{arg_name}' argument of the '{cls_attr.field}' field. " - "This is not supported for types defining '__schema__'." - ) - - if ( - arg_options.get("description") - and field_args[arg_name].description - ): - raise ValueError( - f"Class '{cls.__name__}' defines duplicate descriptions " - f"for '{arg_name}' argument " - f"of the '{cls_attr.field}' field." - ) - - validate_field_arg_default_value( - cls, cls_attr.field, arg_name, arg_options.get("default_value") - ) - - subscriber_args = get_field_args_from_subscriber(cls_attr.subscriber) - for arg_name, arg_obj in subscriber_args.items(): - validate_field_arg_default_value( - cls, cls_attr.field, arg_name, arg_obj.default_value - ) - - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - validate_object_aliases(cls, aliases, field_names, fields_resolvers) - - return get_object_type_with_schema_kwargs(cls, aliases, field_names) - - -def validate_field_arg_default_value( - cls: Type[GraphQLObject], field_name: str, arg_name: str, default_value: Any -): - if default_value is None: - return - - try: - get_value_node(default_value) - except TypeError as e: - raise TypeError( - f"Class '{cls.__name__}' defines default value " - f"for '{arg_name}' argument " - f"of the '{field_name}' field that can't be " - "represented in GraphQL schema." - ) from e - - -def validate_object_type_without_schema(cls: Type[GraphQLObject]) -> Dict[str, Any]: - data = get_object_type_validation_data(cls) - - # Alias target is not present in schema as a field if its not an - # explicit field (instance of GraphQLObjectField) - for alias_target in data.aliases.values(): - if ( - alias_target in data.fields_attrs - and alias_target not in data.fields_instances - ): - data.fields_attrs.remove(alias_target) - - # Validate GraphQL names for future type's fields and assert those are unique - validate_object_unique_graphql_names(cls, data.fields_attrs, data.fields_instances) - validate_object_resolvers( - cls, data.fields_attrs, data.fields_instances, data.resolvers_instances - ) - validate_object_subscribers(cls, data.fields_attrs, data.sources_instances) - validate_object_fields_args(cls) - - # Gather names of field attrs with defined resolver - fields_resolvers: List[str] = [] - for attr_name, field_instance in data.fields_instances.items(): - if field_instance.resolver: - fields_resolvers.append(attr_name) - for resolver_instance in data.resolvers_instances.values(): - fields_resolvers.append(resolver_instance.field) - - validate_object_aliases(cls, data.aliases, data.fields_attrs, fields_resolvers) - - return get_object_type_kwargs(cls, data.aliases) - - -def validate_object_unique_graphql_names( - cls: Type[GraphQLObject], - fields_attrs: List[str], - fields_instances: Dict[str, GraphQLObjectField], -): - graphql_names: List[str] = [] - for attr_name in fields_attrs: - if attr_name in fields_instances and fields_instances[attr_name].name: - attr_graphql_name = fields_instances[attr_name].name - else: - attr_graphql_name = convert_python_name_to_graphql(attr_name) - - if attr_graphql_name in graphql_names: - raise ValueError( - f"Class '{cls.__name__}' defines multiple fields with GraphQL " - f"name '{attr_graphql_name}'." - ) - - graphql_names.append(attr_graphql_name) - - -def validate_object_resolvers( - cls: Type[GraphQLObject], - fields_names: List[str], - fields_instances: Dict[str, GraphQLObjectField], - resolvers_instances: Dict[str, GraphQLObjectResolver], -): - resolvers_fields: List[str] = [] - - for field_attr, field_instance in fields_instances.items(): - if field_instance.resolver: - resolvers_fields.append(field_attr) - - for resolver in resolvers_instances.values(): - if resolver.field not in fields_names: - valid_fields: str = "', '".join(sorted(fields_names)) - raise ValueError( - f"Class '{cls.__name__}' defines resolver for an undefined " - f"field '{resolver.field}'. (Valid fields: '{valid_fields}')" - ) - - if resolver.field in resolvers_fields: - raise ValueError( - f"Class '{cls.__name__}' defines multiple resolvers for field " - f"'{resolver.field}'." - ) - - resolvers_fields.append(resolver.field) - - field_instance = fields_instances.get(resolver.field) - if field_instance: - if field_instance.description and resolver.description: - raise ValueError( - f"Class '{cls.__name__}' defines multiple descriptions " - f"for field '{resolver.field}'." - ) - - if field_instance.args and resolver.args: - raise ValueError( - f"Class '{cls.__name__}' defines multiple arguments options " - f"('args') for field '{resolver.field}'." - ) - - -def validate_object_subscribers( - cls: Type[GraphQLObject], - fields_names: List[str], - sources_instances: Dict[str, GraphQLObjectSource], -): - source_fields: List[str] = [] - - for key, source in sources_instances.items(): - if not isinstance(source.field, str): - raise ValueError(f"The field name for {key} must be a string.") - if source.field not in fields_names: - valid_fields: str = "', '".join(sorted(fields_names)) - raise ValueError( - f"Class '{cls.__name__}' defines source for an undefined " - f"field '{source.field}'. (Valid fields: '{valid_fields}')" - ) - if source.field in source_fields: - raise ValueError( - f"Class '{cls.__name__}' defines multiple sources for field " - f"'{source.field}'." - ) - - source_fields.append(source.field) - - if source.description is not None and not isinstance(source.description, str): - raise ValueError(f"The description for {key} must be a string if provided.") - - if source.args is not None: - if not isinstance(source.args, dict): - raise ValueError( - f"The args for {key} must be a dictionary if provided." - ) - for arg_name, arg_info in source.args.items(): - if not isinstance(arg_info, dict): - raise ValueError( - f"Argument {arg_name} for {key} must have a dict as its info." - ) - - -def validate_object_fields_args(cls: Type[GraphQLObject]): - for field_name in dir(cls): - field_instance = getattr(cls, field_name) - if ( - isinstance(field_instance, (GraphQLObjectField, GraphQLObjectResolver)) - and field_instance.resolver - ): - validate_object_field_args(cls, field_name, field_instance) - - -def validate_object_field_args( - cls: Type[GraphQLObject], - field_name: str, - field_instance: Union["GraphQLObjectField", "GraphQLObjectResolver"], -): - resolver_args = get_field_args_from_resolver(field_instance.resolver) - if resolver_args: - for arg_name, arg_obj in resolver_args.items(): - validate_field_arg_default_value( - cls, field_name, arg_name, arg_obj.default_value - ) - - if not field_instance.args: - return # Skip extra logic for validating instance.args - - resolver_args_names = list(resolver_args.keys()) - if resolver_args_names: - error_help = "expected one of: '%s'" % ("', '".join(resolver_args_names)) - else: - error_help = "function accepts no extra arguments" - - for arg_name, arg_options in field_instance.args.items(): - if arg_name not in resolver_args_names: - if isinstance(field_instance, GraphQLObjectField): - raise ValueError( - f"Class '{cls.__name__}' defines '{field_name}' field " - f"with extra configuration for '{arg_name}' argument " - "thats not defined on the resolver function. " - f"({error_help})" - ) - - raise ValueError( - f"Class '{cls.__name__}' defines '{field_name}' resolver " - f"with extra configuration for '{arg_name}' argument " - "thats not defined on the resolver function. " - f"({error_help})" - ) - - validate_field_arg_default_value( - cls, field_name, arg_name, arg_options.get("default_value") - ) - - -def validate_object_aliases( - cls: Type[GraphQLObject], - aliases: Dict[str, str], - fields_names: List[str], - fields_resolvers: List[str], -): - for alias in aliases: - if alias not in fields_names: - valid_fields: str = "', '".join(sorted(fields_names)) - raise ValueError( - f"Class '{cls.__name__}' defines an alias for an undefined " - f"field '{alias}'. (Valid fields: '{valid_fields}')" - ) - - if alias in fields_resolvers: - raise ValueError( - f"Class '{cls.__name__}' defines an alias for a field " - f"'{alias}' that already has a custom resolver." - ) - - -@dataclass -class GraphQLObjectValidationData: - aliases: Dict[str, str] - fields_attrs: List[str] - fields_instances: Dict[str, GraphQLObjectField] - resolvers_instances: Dict[str, GraphQLObjectResolver] - sources_instances: Dict[str, GraphQLObjectSource] - - -def get_object_type_validation_data( - cls: Type[GraphQLObject], -) -> GraphQLObjectValidationData: - fields_attrs: List[str] = [ - attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") - ] - - fields_instances: Dict[str, GraphQLObjectField] = {} - resolvers_instances: Dict[str, GraphQLObjectResolver] = {} - sources_instances: Dict[str, GraphQLObjectSource] = {} - - for attr_name in dir(cls): - if attr_name.startswith("__"): - continue - - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - resolvers_instances[attr_name] = cls_attr - if attr_name in fields_attrs: - fields_attrs.remove(attr_name) - - if isinstance(cls_attr, GraphQLObjectSource): - sources_instances[attr_name] = cls_attr - if attr_name in fields_attrs: - fields_attrs.remove(attr_name) - - elif isinstance(cls_attr, GraphQLObjectField): - fields_instances[attr_name] = cls_attr - - if attr_name not in fields_attrs: - fields_attrs.append(attr_name) - - elif callable(attr_name): - if attr_name in fields_attrs: - fields_attrs.remove(attr_name) - - return GraphQLObjectValidationData( - aliases=getattr(cls, "__aliases__", None) or {}, - fields_attrs=fields_attrs, - fields_instances=fields_instances, - resolvers_instances=resolvers_instances, - sources_instances=sources_instances, - ) - - -def get_object_type_kwargs( - cls: Type[GraphQLObject], - aliases: Dict[str, str], -) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} - - for attr_name in cls.__annotations__: - if attr_name.startswith("__"): - continue - - kwarg_name = aliases.get(attr_name, attr_name) - kwarg_value = getattr(cls, kwarg_name, None) - if isinstance(kwarg_value, GraphQLObjectField): - kwargs[kwarg_name] = kwarg_value.default_value - elif isinstance(kwarg_value, GraphQLObjectResolver): - continue # Skip resolver instances - elif not callable(kwarg_value): - kwargs[kwarg_name] = kwarg_value - - for attr_name in dir(cls): - if attr_name.startswith("__") or attr_name in kwargs: - continue - - kwarg_name = aliases.get(attr_name, attr_name) - kwarg_value = getattr(cls, kwarg_name) - if isinstance(kwarg_value, GraphQLObjectField): - kwargs[kwarg_name] = kwarg_value.default_value - elif not callable(kwarg_value): - kwargs[kwarg_name] = kwarg_value - - return kwargs - - -def get_object_type_with_schema_kwargs( - cls: Type[GraphQLObject], - aliases: Dict[str, str], - field_names: List[str], -) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} - - for field_name in field_names: - final_name = aliases.get(field_name, field_name) - attr_value = getattr(cls, final_name, None) - - if isinstance(attr_value, GraphQLObjectField): - kwargs[final_name] = attr_value.default_value - elif not isinstance(attr_value, GraphQLObjectResolver) and not callable( - attr_value - ): - kwargs[final_name] = attr_value - - return kwargs diff --git a/ariadne_graphql_modules/next/roots.py b/ariadne_graphql_modules/next/roots.py index 017510c..6e52563 100644 --- a/ariadne_graphql_modules/next/roots.py +++ b/ariadne_graphql_modules/next/roots.py @@ -2,6 +2,7 @@ from graphql import ( ConstDirectiveNode, + DefinitionNode, DocumentNode, FieldDefinitionNode, NamedTypeNode, @@ -10,17 +11,22 @@ TypeDefinitionNode, ) -DefinitionsList = List[TypeDefinitionNode] +DefinitionsList = List[DefinitionNode] ROOTS_NAMES = ("Query", "Mutation", "Subscription") def merge_root_nodes(document_node: DocumentNode) -> DocumentNode: - roots_definitions: Dict[str, List] = {root: [] for root in ROOTS_NAMES} + roots_definitions: Dict[str, List[TypeDefinitionNode]] = { + root: [] for root in ROOTS_NAMES + } final_definitions: DefinitionsList = [] for node in document_node.definitions: - if node.name.value in roots_definitions: + if ( + isinstance(node, TypeDefinitionNode) + and node.name.value in roots_definitions + ): roots_definitions[node.name.value].append(node) else: final_definitions.append(node) @@ -60,12 +66,11 @@ def merge_nodes(nodes: List[TypeDefinitionNode]) -> ObjectTypeDefinitionNode: for field_node in node.fields: field_name = field_node.name.value if field_name in fields: - other_type_source = fields[field_name][0] + other_type_source = fields[field_name] raise ValueError( f"Multiple {root_name} types are defining same field " f"'{field_name}': {other_type_source}, {field_node}" ) - fields[field_name] = field_node return ObjectTypeDefinitionNode( diff --git a/ariadne_graphql_modules/next/sort.py b/ariadne_graphql_modules/next/sort.py index ffe35a8..939d152 100644 --- a/ariadne_graphql_modules/next/sort.py +++ b/ariadne_graphql_modules/next/sort.py @@ -1,11 +1,13 @@ -from typing import Dict, List, Union, cast +from typing import Any, Dict, List, Union, cast from graphql import ( - DirectiveNode, + DefinitionNode, + DirectiveDefinitionNode, DocumentNode, InputObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, ListTypeNode, + NamedTypeNode, NonNullTypeNode, ObjectTypeDefinitionNode, ScalarTypeDefinitionNode, @@ -18,7 +20,7 @@ def sort_schema_document(document: DocumentNode) -> DocumentNode: unsorted_nodes: Dict[str, TypeDefinitionNode] = {} - sorted_nodes: List[TypeDefinitionNode] = [] + sorted_nodes: List[Union[TypeDefinitionNode, DefinitionNode]] = [] for node in document.definitions: cast_node = cast(TypeDefinitionNode, node) @@ -39,18 +41,17 @@ def sort_schema_document(document: DocumentNode) -> DocumentNode: def get_sorted_directives( - unsorted_nodes: Dict[str, TypeDefinitionNode] -) -> List[DirectiveNode]: - directives: List[DirectiveNode] = [] + unsorted_nodes: Dict[str, Any] +) -> List[DirectiveDefinitionNode]: + directives: List[DirectiveDefinitionNode] = [] for name, model in tuple(unsorted_nodes.items()): - if isinstance(model, DirectiveNode): + if isinstance(model, DirectiveDefinitionNode): directives.append(unsorted_nodes.pop(name)) - return sorted(directives, key=lambda m: m.name.value) def get_sorted_scalars( - unsorted_nodes: Dict[str, TypeDefinitionNode] + unsorted_nodes: Dict[str, Any] ) -> List[ScalarTypeDefinitionNode]: scalars: List[ScalarTypeDefinitionNode] = [] for name, model in tuple(unsorted_nodes.items()): @@ -89,7 +90,8 @@ def get_sorted_object_dependencies( for interface in root_node.interfaces: interface_name = interface.name.value interface_node = unsorted_nodes.pop(interface_name, None) - if interface_node: + + if isinstance(interface_node, InterfaceTypeDefinitionNode): sorted_nodes.append(interface_node) sorted_nodes += get_sorted_object_dependencies( interface_node, unsorted_nodes @@ -123,5 +125,6 @@ def get_sorted_input_dependencies( def unwrap_type_name(type_node: TypeNode) -> str: if isinstance(type_node, (ListTypeNode, NonNullTypeNode)): return unwrap_type_name(type_node.type) - - return type_node.name.value + if isinstance(type_node, NamedTypeNode): + return type_node.name.value + raise ValueError("Unexpected type node encountered.") diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index 6ee7292..b6f9e08 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -29,7 +29,7 @@ def get_type_node( metadata: GraphQLMetadata, type_hint: Any, - parent_type: Optional[GraphQLType] = None, + parent_type: Optional[Type[GraphQLType]] = None, ) -> TypeNode: if is_nullable(type_hint): nullable = True @@ -37,19 +37,19 @@ def get_type_node( else: nullable = False - type_node = None + type_node: Optional[TypeNode] = None if is_list(type_hint): list_item_type_hint = unwrap_type(type_hint) type_node = ListTypeNode( type=get_type_node(metadata, list_item_type_hint, parent_type=parent_type) ) - elif type_hint == str: + elif type_hint is str: type_node = NamedTypeNode(name=NameNode(value="String")) - elif type_hint == int: + elif type_hint is int: type_node = NamedTypeNode(name=NameNode(value="Int")) - elif type_hint == float: + elif type_hint is float: type_node = NamedTypeNode(name=NameNode(value="Float")) - elif type_hint == bool: + elif type_hint is bool: type_node = NamedTypeNode(name=NameNode(value="Boolean")) elif get_origin(type_hint) is Annotated: forward_ref, type_meta = get_args(type_hint) @@ -67,7 +67,10 @@ def get_type_node( elif isinstance(type_hint, ForwardRef): type_name = type_hint.__forward_arg__ if not parent_type or parent_type.__name__ != type_name: - ... + raise ValueError( + "Can't create a GraphQL return type" + f"for forward reference '{type_name}'." + ) type_node = NamedTypeNode( name=NameNode(value=metadata.get_graphql_name(parent_type)), @@ -94,7 +97,7 @@ def get_type_node( def is_list(type_hint: Any) -> bool: - return get_origin(type_hint) == list + return get_origin(type_hint) is list def is_nullable(type_hint: Any) -> bool: @@ -119,7 +122,7 @@ def unwrap_type(type_hint: Any) -> Any: def get_deferred_type( type_hint: Any, forward_ref: ForwardRef, deferred_type: DeferredTypeData -) -> Optional[Union[GraphQLType, Enum]]: +) -> Union[Type[GraphQLType], Type[Enum]]: type_name = forward_ref.__forward_arg__ module = import_module(deferred_type.path) graphql_type = getattr(module, type_name) diff --git a/ariadne_graphql_modules/next/uniontype.py b/ariadne_graphql_modules/next/uniontype.py deleted file mode 100644 index f6da388..0000000 --- a/ariadne_graphql_modules/next/uniontype.py +++ /dev/null @@ -1,153 +0,0 @@ -from dataclasses import dataclass -from typing import ( - Any, - Callable, - Iterable, - NoReturn, - Optional, - Sequence, - Type, - cast, -) - -from ariadne import UnionType -from graphql import ( - GraphQLSchema, - NameNode, - NamedTypeNode, - UnionTypeDefinitionNode, -) - -from ..utils import parse_definition -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .description import get_description_node -from .objecttype import GraphQLObject -from .validators import validate_description, validate_name - - -class GraphQLUnion(GraphQLType): - __types__: Sequence[Type[GraphQLType]] - __schema__: Optional[str] - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.__dict__.get("__abstract__"): - return - - cls.__abstract__ = False - - if cls.__dict__.get("__schema__"): - validate_union_type_with_schema(cls) - else: - validate_union_type(cls) - - @classmethod - def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": - name = cls.__get_graphql_name__() - metadata.set_graphql_name(cls, name) - - if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__() - - return cls.__get_graphql_model_without_schema__(name) - - @classmethod - def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": - definition = cast( - UnionTypeDefinitionNode, - parse_definition(UnionTypeDefinitionNode, cls.__schema__), - ) - - return GraphQLUnionModel( - name=definition.name.value, - ast_type=UnionTypeDefinitionNode, - ast=definition, - resolve_type=cls.resolve_type, - ) - - @classmethod - def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": - return GraphQLUnionModel( - name=name, - ast_type=UnionTypeDefinitionNode, - ast=UnionTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node( - getattr(cls, "__description__", None), - ), - types=tuple( - NamedTypeNode(name=NameNode(value=t.__get_graphql_name__())) - for t in cls.__types__ - ), - ), - resolve_type=cls.resolve_type, - ) - - @classmethod - def __get_graphql_types__(cls, _: "GraphQLMetadata") -> Iterable["GraphQLType"]: - """Returns iterable with GraphQL types associated with this type""" - return [cls] + cls.__types__ - - @staticmethod - def resolve_type(obj: Any, *_) -> str: - if isinstance(obj, GraphQLObject): - return obj.__get_graphql_name__() - - raise ValueError( - f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." - ) - - -def validate_union_type(cls: Type[GraphQLUnion]) -> NoReturn: - types = getattr(cls, "__types__", None) - if not types: - raise ValueError( - f"Class '{cls.__name__}' is missing a '__types__' attribute " - "with list of types belonging to a union." - ) - - -# pylint: disable=E1101 -def validate_union_type_with_schema(cls: Type[GraphQLUnion]) -> NoReturn: - definition = cast( - UnionTypeDefinitionNode, - parse_definition(UnionTypeDefinitionNode, cls.__schema__), - ) - - if not isinstance(definition, UnionTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines a '__schema__' attribute " - "with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != " - f"'{UnionTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - schema_type_names = {type_node.name.value for type_node in definition.types} - - class_type_names = {t.__get_graphql_name__() for t in cls.__types__} - if not class_type_names.issubset(schema_type_names): - missing_in_schema = sorted(class_type_names - schema_type_names) - missing_in_schema_str = "', '".join(missing_in_schema) - raise ValueError( - f"Types '{missing_in_schema_str}' are in '__types__' but not in '__schema__'." - ) - - if not schema_type_names.issubset(class_type_names): - missing_in_types = sorted(schema_type_names - class_type_names) - missing_in_types_str = "', '".join(missing_in_types) - raise ValueError( - f"Types '{missing_in_types_str}' are in '__schema__' but not in '__types__'." - ) - - -@dataclass(frozen=True) -class GraphQLUnionModel(GraphQLModel): - resolve_type: Callable[[Any], Any] - - def bind_to_schema(self, schema: GraphQLSchema): - bindable = UnionType(self.name, self.resolve_type) - bindable.bind_to_schema(schema) diff --git a/ariadne_graphql_modules/next/value.py b/ariadne_graphql_modules/next/value.py index be4ee07..5e11d90 100644 --- a/ariadne_graphql_modules/next/value.py +++ b/ariadne_graphql_modules/next/value.py @@ -78,7 +78,9 @@ def get_value_from_node(node: ConstValueNode) -> Any: return node.value if isinstance(node, (ConstObjectValueNode, ObjectValueNode)): - return {field.name.value: get_value_from_node(field) for field in node.fields} + return { + field.name.value: get_value_from_node(field.value) for field in node.fields + } if isinstance(node, ListValueNode): return [get_value_from_node(value) for value in node.values] diff --git a/tests_next/snapshots/snap_test_subscription_type_validation.py b/tests_next/snapshots/snap_test_subscription_type_validation.py index e260522..0901764 100644 --- a/tests_next/snapshots/snap_test_subscription_type_validation.py +++ b/tests_next/snapshots/snap_test_subscription_type_validation.py @@ -25,7 +25,7 @@ snapshots['test_multiple_sources_without_schema 1'] = "Class 'SubscriptionType' defines multiple sources for field 'message_added'." -snapshots['test_source_args_field_arg_not_dict_without_schema 1'] = 'Argument channel for message_added_generator must have a dict as its info.' +snapshots['test_source_args_field_arg_not_dict_without_schema 1'] = 'Argument channel for message_added_generator must have a GraphQLObjectFieldArg as its info.' snapshots['test_source_args_not_dict_without_schema 1'] = 'The args for message_added_generator must be a dictionary if provided.' diff --git a/tests_next/test_get_field_args_from_resolver.py b/tests_next/test_get_field_args_from_resolver.py index cfd3733..c7e0924 100644 --- a/tests_next/test_get_field_args_from_resolver.py +++ b/tests_next/test_get_field_args_from_resolver.py @@ -1,4 +1,4 @@ -from ariadne_graphql_modules.next.objecttype import get_field_args_from_resolver +from ariadne_graphql_modules.next.graphql_object import get_field_args_from_resolver def test_field_has_no_args_after_obj_and_info_args(): diff --git a/tests_next/test_union_type_validation.py b/tests_next/test_union_type_validation.py index b154d70..29415a7 100644 --- a/tests_next/test_union_type_validation.py +++ b/tests_next/test_union_type_validation.py @@ -3,7 +3,9 @@ GraphQLObject, GraphQLUnion, ) -from ariadne_graphql_modules.next.uniontype import validate_union_type_with_schema +from ariadne_graphql_modules.next.graphql_union.validators import ( + validate_union_type_with_schema, +) import pytest From f50a23cc2219041bc6f27d484fedc972c1c170d5 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 19 Aug 2024 10:51:49 +0200 Subject: [PATCH 38/63] Remove mypy plugin --- ariadne_graphql_modules/next/mypy.py | 29 ---------------------------- pyproject.toml | 3 --- 2 files changed, 32 deletions(-) delete mode 100644 ariadne_graphql_modules/next/mypy.py diff --git a/ariadne_graphql_modules/next/mypy.py b/ariadne_graphql_modules/next/mypy.py deleted file mode 100644 index bb96d64..0000000 --- a/ariadne_graphql_modules/next/mypy.py +++ /dev/null @@ -1,29 +0,0 @@ -from mypy.plugin import Plugin, MethodContext # pylint: disable=E0611 -from mypy.types import Instance # pylint: disable=E0611 -from mypy.nodes import EllipsisExpr # pylint: disable=E0611 - - -class AriadneGraphQLModulesPlugin(Plugin): - def get_method_hook(self, fullname: str): - if "GraphQL" in fullname and fullname.endswith(".field"): - return self.transform_graphql_object - return None - - def transform_graphql_object(self, ctx: MethodContext): - default_any_type = ctx.default_return_type - - default_type_arg = ctx.args[2] - if default_type_arg: - default_type = ctx.arg_types[2][0] - default_arg = default_type_arg[0] - - # Fallback to default Any type if the field is required - if not isinstance(default_arg, EllipsisExpr): - if isinstance(default_type, Instance): - return default_type - - return default_any_type - - -def plugin(_): - return AriadneGraphQLModulesPlugin diff --git a/pyproject.toml b/pyproject.toml index 8703bcf..2b7b98d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,3 @@ exclude = ''' [tool.pytest.ini_options] testpaths = ["tests_next"] asyncio_mode = "strict" - -[tool.mypy] -plugins = "ariadne_graphql_modules.next.mypy" From 3cfe1fafd77ec23accdbc7958a2eb32fbada9d94 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 19 Aug 2024 10:52:13 +0200 Subject: [PATCH 39/63] update-interface-model --- .../next/graphql_object/object_type.py | 125 ++++++++++-------- .../snap_test_interface_type_validation.py | 4 - tests_next/test_interface_type.py | 95 +++++++++++-- tests_next/test_interface_type_validation.py | 16 --- 4 files changed, 152 insertions(+), 88 deletions(-) diff --git a/ariadne_graphql_modules/next/graphql_object/object_type.py b/ariadne_graphql_modules/next/graphql_object/object_type.py index d7cdcf0..80f8e67 100644 --- a/ariadne_graphql_modules/next/graphql_object/object_type.py +++ b/ariadne_graphql_modules/next/graphql_object/object_type.py @@ -67,16 +67,23 @@ class GraphQLObject(GraphQLType): __implements__: Optional[Iterable[Type[GraphQLType]]] def __init__(self, **kwargs: Any): + default_values: Dict[str, Any] = {} + for interface in getattr(self, "__implements__", []): + if hasattr(interface, "__kwargs__"): + default_values.update(interface.__kwargs__) + + default_values.update(self.__kwargs__) + for kwarg in kwargs: - if kwarg not in self.__kwargs__: - valid_kwargs = "', '".join(self.__kwargs__) + if kwarg not in default_values: + valid_kwargs = "', '".join(default_values) raise TypeError( f"{type(self).__name__}.__init__() got an unexpected " f"keyword argument '{kwarg}'. " f"Valid keyword arguments: '{valid_kwargs}'" ) - for kwarg, default in self.__kwargs__.items(): + for kwarg, default in default_values.items(): setattr(self, kwarg, kwargs.get(kwarg, deepcopy(default))) def __init_subclass__(cls) -> None: @@ -356,14 +363,18 @@ def create_graphql_object_data_without_schema( fields_defaults: Dict[str, Any] = {} fields_order: List[str] = [] - type_hints = cls.__annotations__ + interfaces = getattr(cls, "__implements__", []) + interfaces_names: List[str] = [interface.__name__ for interface in interfaces] + type_hints: dict[str, Any] = {} + aliases: Dict[str, str] = {} - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} - aliases_targets: List[str] = list(aliases.values()) + for interface in reversed(interfaces): + type_hints.update(interface.__annotations__) + aliases.update(getattr(interface, "__aliases__", {})) - interfaces: List[str] = [ - interface.__name__ for interface in getattr(cls, "__implements__", []) - ] + type_hints.update(cls.__annotations__) + aliases.update(getattr(cls, "__aliases__", None) or {}) + aliases_targets: List[str] = list(aliases.values()) for attr_name, attr_type in type_hints.items(): if attr_name.startswith("__"): @@ -381,59 +392,62 @@ def create_graphql_object_data_without_schema( fields_names[attr_name] = convert_python_name_to_graphql(attr_name) fields_types[attr_name] = attr_type - for attr_name in dir(cls): - if attr_name.startswith("__"): - continue - - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectField): - if attr_name not in fields_order: - fields_order.append(attr_name) + def process_class_attributes(target_cls): + for attr_name in dir(target_cls): + if attr_name.startswith("__"): + continue + cls_attr = getattr(target_cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if attr_name not in fields_order: + fields_order.append(attr_name) - fields_names[attr_name] = cls_attr.name or convert_python_name_to_graphql( - attr_name - ) + fields_names[attr_name] = ( + cls_attr.name or convert_python_name_to_graphql(attr_name) + ) - if cls_attr.field_type: - fields_types[attr_name] = cls_attr.field_type - if cls_attr.description: - fields_descriptions[attr_name] = cls_attr.description - if cls_attr.resolver: - fields_resolvers[attr_name] = cls_attr.resolver + if cls_attr.field_type: + fields_types[attr_name] = cls_attr.field_type + if cls_attr.description: + fields_descriptions[attr_name] = cls_attr.description + if cls_attr.resolver: + fields_resolvers[attr_name] = cls_attr.resolver + field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args: + fields_args[attr_name] = update_field_args_options( + field_args, cls_attr.args + ) + if cls_attr.default_value: + fields_defaults[attr_name] = cls_attr.default_value + elif isinstance(cls_attr, GraphQLObjectResolver): + if cls_attr.field_type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.field_type + if cls_attr.description and cls_attr.field not in fields_descriptions: + fields_descriptions[cls_attr.field] = cls_attr.description + fields_resolvers[cls_attr.field] = cls_attr.resolver field_args = get_field_args_from_resolver(cls_attr.resolver) + if field_args and not fields_args.get(cls_attr.field): + fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + elif isinstance(cls_attr, GraphQLObjectSource): + if cls_attr.field_type and cls_attr.field not in fields_types: + fields_types[cls_attr.field] = cls_attr.field_type + if cls_attr.description and cls_attr.field not in fields_descriptions: + fields_descriptions[cls_attr.field] = cls_attr.description + fields_subscribers[cls_attr.field] = cls_attr.subscriber + field_args = get_field_args_from_subscriber(cls_attr.subscriber) if field_args: - fields_args[attr_name] = update_field_args_options( + fields_args[cls_attr.field] = update_field_args_options( field_args, cls_attr.args ) - if cls_attr.default_value: - fields_defaults[attr_name] = cls_attr.default_value - - elif isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.field_type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.field_type - if cls_attr.description: - fields_descriptions[cls_attr.field] = cls_attr.description - fields_resolvers[cls_attr.field] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args and not fields_args.get(cls_attr.field): - fields_args[cls_attr.field] = update_field_args_options( - field_args, cls_attr.args - ) - elif isinstance(cls_attr, GraphQLObjectSource): - if cls_attr.field_type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.field_type - if cls_attr.description: - fields_descriptions[cls_attr.field] = cls_attr.description - fields_subscribers[cls_attr.field] = cls_attr.subscriber - field_args = get_field_args_from_subscriber(cls_attr.subscriber) - if field_args: - fields_args[cls_attr.field] = update_field_args_options( - field_args, cls_attr.args - ) - elif attr_name not in aliases_targets and not callable(cls_attr): - fields_defaults[attr_name] = cls_attr + elif attr_name not in aliases_targets and not callable(cls_attr): + fields_defaults[attr_name] = cls_attr + for interface in getattr(cls, "__implements__", []): + process_class_attributes(interface) + + process_class_attributes(cls) fields: Dict[str, "GraphQLObjectField"] = {} for field_name in fields_order: fields[field_name] = GraphQLObjectField( @@ -445,5 +459,4 @@ def create_graphql_object_data_without_schema( subscriber=fields_subscribers.get(field_name), default_value=fields_defaults.get(field_name), ) - - return GraphQLObjectData(fields=fields, interfaces=interfaces) + return GraphQLObjectData(fields=fields, interfaces=interfaces_names) diff --git a/tests_next/snapshots/snap_test_interface_type_validation.py b/tests_next/snapshots/snap_test_interface_type_validation.py index fc72186..ae606b0 100644 --- a/tests_next/snapshots/snap_test_interface_type_validation.py +++ b/tests_next/snapshots/snap_test_interface_type_validation.py @@ -12,7 +12,3 @@ snapshots['test_interface_with_different_types 1'] = '''Query root type must be provided. Interface field UserInterface.score expects type String! but User.score is type Int!.''' - -snapshots['test_missing_interface_implementation 1'] = '''Query root type must be provided. - -Interface field RequiredInterface.requiredField expected but Implementing does not provide it.''' diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py index 303071f..8393baa 100644 --- a/tests_next/test_interface_type.py +++ b/tests_next/test_interface_type.py @@ -51,9 +51,9 @@ def search(*_) -> List[UserType | CommentType]: union Result = User | Comment type User implements UserInterface { - name: String! summary: String! score: Int! + name: String! } type Comment { @@ -70,6 +70,82 @@ def search(*_) -> List[UserType | CommentType]: ) +def test_interface_inheritance_without_schema(assert_schema_equals): + def hello_resolver(*_, name: str) -> str: + return f"Hello {name}!" + + class UserInterface(GraphQLInterface): + summary: str + score: str = GraphQLInterface.field( + hello_resolver, + name="better_score", + graphql_type=str, + args={"name": GraphQLInterface.argument(name="json")}, + description="desc", + default_value="my_json", + ) + + class UserType(GraphQLObject): + name: str = GraphQLInterface.field( + name="name", + graphql_type=str, + args={"name": GraphQLInterface.argument(name="json")}, + default_value="my_json", + ) + + __implements__ = [UserInterface] + + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[UserType | CommentType]: + return [ + UserType(), + CommentType(id=2, content="Hello World!"), + ] + + schema = make_executable_schema(QueryType, UserInterface, UserType) + + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } + + union Result = User | Comment + + type User implements UserInterface { + summary: String! + + \"\"\"desc\"\"\" + better_score(json: String!): String! + name: String! + } + + type Comment { + id: ID! + content: String! + } + + interface UserInterface { + summary: String! + + \"\"\"desc\"\"\" + better_score(json: String!): String! + } + + """, + ) + + result = graphql_sync(schema, '{ search { ... on User{ better_score(json: "test") } } }') + + assert not result.errors + assert result.data == {"search": [{"better_score": "Hello test!"}, {}]} + + def test_interface_with_schema(assert_schema_equals): class UserInterface(GraphQLInterface): __schema__ = """ @@ -119,7 +195,7 @@ def search(*_) -> List[UserType | CommentType]: summary: String! score: Int! } - + interface UserInterface { summary: String! score: Int! @@ -139,14 +215,11 @@ class BaseEntityInterface(GraphQLInterface): id: GraphQLID class UserInterface(GraphQLInterface): - id: GraphQLID username: str __implements__ = [BaseEntityInterface] class UserType(GraphQLObject): - id: GraphQLID - username: str __implements__ = [UserInterface, BaseEntityInterface] @@ -193,8 +266,6 @@ class UserInterface(GraphQLInterface): class UserType(GraphQLObject): id: GraphQLID username: str - summary: str - score: int __implements__ = [UserInterface] @@ -213,10 +284,10 @@ def user(*_) -> UserType: } type User implements UserInterface { - id: ID! - username: String! summary: String! score: Int! + id: ID! + username: String! } \"\"\"Lorem ipsum.\"\"\" @@ -239,8 +310,6 @@ def resolve_score(*_): class UserType(GraphQLObject): id: GraphQLID - summary: str - score: int __implements__ = [UserInterface] @@ -259,9 +328,11 @@ def user(*_) -> UserType: } type User implements UserInterface { - id: ID! summary: String! + + \"\"\"Lorem ipsum.\"\"\" score: Int! + id: ID! } interface UserInterface { diff --git a/tests_next/test_interface_type_validation.py b/tests_next/test_interface_type_validation.py index bc2f414..d7e194f 100644 --- a/tests_next/test_interface_type_validation.py +++ b/tests_next/test_interface_type_validation.py @@ -27,22 +27,6 @@ class UserType(GraphQLObject): snapshot.assert_match(str(exc_info.value)) -def test_missing_interface_implementation(snapshot): - with pytest.raises(TypeError) as exc_info: - - class RequiredInterface(GraphQLInterface): - required_field: str - - class ImplementingType(GraphQLObject): - optional_field: str - - __implements__ = [RequiredInterface] - - make_executable_schema(ImplementingType, RequiredInterface) - - snapshot.assert_match(str(exc_info.value)) - - def test_interface_no_interface_in_schema(snapshot): with pytest.raises(TypeError) as exc_info: From ca685edcb0e15be1335ff3199ecb6aa592ef43e6 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 19 Aug 2024 10:59:17 +0200 Subject: [PATCH 40/63] run black --- tests_next/test_interface_type.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py index 8393baa..760df46 100644 --- a/tests_next/test_interface_type.py +++ b/tests_next/test_interface_type.py @@ -140,7 +140,9 @@ def search(*_) -> List[UserType | CommentType]: """, ) - result = graphql_sync(schema, '{ search { ... on User{ better_score(json: "test") } } }') + result = graphql_sync( + schema, '{ search { ... on User{ better_score(json: "test") } } }' + ) assert not result.errors assert result.data == {"search": [{"better_score": "Hello test!"}, {}]} From b0cc7474778955462851db30b752a6865ea6ecc3 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 19 Aug 2024 11:06:40 +0200 Subject: [PATCH 41/63] fix-UnionTypes-for-python-3.8-3.9 --- ariadne_graphql_modules/next/typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index b6f9e08..8a6b2ff 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -1,7 +1,6 @@ from enum import Enum from importlib import import_module from inspect import isclass -from types import UnionType from typing import ( Annotated, Any, @@ -25,6 +24,8 @@ from .deferredtype import DeferredTypeData from .idtype import GraphQLID +UnionType = type(int | str) + def get_type_node( metadata: GraphQLMetadata, From 39327a8dea8a6bb460a69c3489f74b901e90742a Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 19 Aug 2024 12:58:20 +0200 Subject: [PATCH 42/63] bump-python-to-3.12 --- .github/workflows/publish.yml | 4 +- .github/workflows/tests.yml | 2 +- .../next/graphql_input/input_type.py | 10 +- ariadne_graphql_modules/next/typing.py | 7 +- pyproject.toml | 19 ++- tests_next/conftest.py | 11 ++ tests_next/snapshots/__init__.py | 0 .../snapshots/snap_test_deferred_type.py | 10 -- .../snap_test_enum_type_validation.py | 30 ---- tests_next/snapshots/snap_test_input_type.py | 12 -- .../snap_test_input_type_validation.py | 26 ---- .../snap_test_interface_type_validation.py | 14 -- .../snap_test_make_executable_schema.py | 14 -- tests_next/snapshots/snap_test_metadata.py | 10 -- tests_next/snapshots/snap_test_object_type.py | 12 -- .../snap_test_object_type_validation.py | 74 ---------- .../snap_test_scalar_type_validation.py | 14 -- .../snapshots/snap_test_standard_enum.py | 16 --- .../snap_test_subscription_type_validation.py | 34 ----- .../snap_test_union_type_validation.py | 12 -- tests_next/snapshots/snap_test_validators.py | 12 -- ...ription_in_source_with_schema.obtained.yml | 3 + ...with_description_in_source_with_schema.yml | 3 + ...th_name_in_source_with_schema.obtained.yml | 3 + ...st_arg_with_name_in_source_with_schema.yml | 3 + ...th_type_in_source_with_schema.obtained.yml | 3 + ...st_arg_with_type_in_source_with_schema.yml | 3 + ...ror_for_invalid_relative_path.obtained.yml | 1 + ...raises_error_for_invalid_relative_path.yml | 1 + ...iption_not_str_without_schema.obtained.yml | 2 + ...est_description_not_str_without_schema.yml | 2 + ...or_type_with_two_descriptions.obtained.yml | 3 + ...s_error_for_type_with_two_descriptions.yml | 3 + ...ion_fails_for_invalid_members.obtained.yml | 2 + ...e_validation_fails_for_invalid_members.yml | 2 + ...ion_fails_for_missing_members.obtained.yml | 3 + ...e_validation_fails_for_missing_members.yml | 3 + ...d_name_not_str_without_schema.obtained.yml | 2 + ...test_field_name_not_str_without_schema.yml | 2 + ...h_invalid_attrs_raising_error.obtained.yml | 2 + ...tance_with_invalid_attrs_raising_error.yml | 2 + ..._for_out_names_without_schema.obtained.yml | 3 + ...ion_fails_for_out_names_without_schema.yml | 3 + ..._for_unsupported_attr_default.obtained.yml | 3 + ...ion_fails_for_unsupported_attr_default.yml | 3 + ...upported_field_default_option.obtained.yml | 3 + ...s_for_unsupported_field_default_option.yml | 3 + ...erface_no_interface_in_schema.obtained.yml | 2 + .../test_interface_no_interface_in_schema.yml | 2 + ...nterface_with_different_types.obtained.yml | 5 + .../test_interface_with_different_types.yml | 5 + ...rg_name_in_source_with_schema.obtained.yml | 3 + ...invalid_arg_name_in_source_with_schema.yml | 3 + ...r_if_called_without_any_types.obtained.yml | 1 + ...ises_error_if_called_without_any_types.yml | 1 + ...ises_key_error_for_unset_data.obtained.yml | 1 + ...tadata_raises_key_error_for_unset_data.yml | 1 + .../test_missing_type_in_schema.obtained.yml | 2 + .../snapshots/test_missing_type_in_schema.yml | 2 + .../test_missing_type_in_types.obtained.yml | 2 + .../snapshots/test_missing_type_in_types.yml | 2 + ...ptions_for_source_with_schema.obtained.yml | 2 + ...le_descriptions_for_source_with_schema.yml | 2 + ...on_if_merge_roots_is_disabled.obtained.yml | 3 + ..._validation_if_merge_roots_is_disabled.yml | 3 + ...sourced_for_field_with_schema.obtained.yml | 2 + ...multiple_sourced_for_field_with_schema.yml | 2 + ...ltiple_sources_without_schema.obtained.yml | 2 + .../test_multiple_sources_without_schema.yml | 2 + ..._name_and_definition_mismatch.obtained.yml | 3 + ...error_for_name_and_definition_mismatch.yml | 3 + ...h_invalid_attrs_raising_error.obtained.yml | 2 + ...tance_with_invalid_attrs_raising_error.yml | 2 + ...tion_fails_for_alias_resolver.obtained.yml | 3 + ...pe_validation_fails_for_alias_resolver.yml | 3 + ...ils_for_alias_target_resolver.obtained.yml | 2 + ...dation_fails_for_alias_target_resolver.yml | 2 + ..._field_with_same_graphql_name.obtained.yml | 2 + ..._attr_and_field_with_same_graphql_name.yml | 2 + ..._for_field_with_multiple_args.obtained.yml | 2 + ...ion_fails_for_field_with_multiple_args.yml | 2 + ...ld_with_multiple_descriptions.obtained.yml | 2 + ...s_for_field_with_multiple_descriptions.yml | 2 + ...ation_fails_for_invalid_alias.obtained.yml | 2 + ...ype_validation_fails_for_invalid_alias.yml | 2 + ...or_missing_field_resolver_arg.obtained.yml | 2 + ...n_fails_for_missing_field_resolver_arg.yml | 2 + ...ails_for_missing_resolver_arg.obtained.yml | 3 + ...idation_fails_for_missing_resolver_arg.yml | 3 + ..._attrs_with_same_graphql_name.obtained.yml | 2 + ..._multiple_attrs_with_same_graphql_name.yml | 2 + ..._for_multiple_field_resolvers.obtained.yml | 2 + ...ion_fails_for_multiple_field_resolvers.yml | 2 + ...fields_with_same_graphql_name.obtained.yml | 2 + ...multiple_fields_with_same_graphql_name.yml | 2 + ...s_for_undefined_attr_resolver.obtained.yml | 2 + ...tion_fails_for_undefined_attr_resolver.yml | 2 + ..._undefined_field_resolver_arg.obtained.yml | 3 + ...fails_for_undefined_field_resolver_arg.yml | 3 + ...ls_for_undefined_resolver_arg.obtained.yml | 4 + ...ation_fails_for_undefined_resolver_arg.yml | 4 + ...upported_resolver_arg_default.obtained.yml | 3 + ...s_for_unsupported_resolver_arg_default.yml | 3 + ...d_resolver_arg_default_option.obtained.yml | 3 + ...nsupported_resolver_arg_default_option.yml | 3 + ...plicated_members_descriptions.obtained.yml | 2 + ...ls_for_duplicated_members_descriptions.yml | 2 + ...lidation_fails_for_empty_enum.obtained.yml | 2 + ...m_type_validation_fails_for_empty_enum.yml | 2 + ..._invalid_members_descriptions.obtained.yml | 2 + ...fails_for_invalid_members_descriptions.yml | 2 + ...fails_for_invalid_type_schema.obtained.yml | 3 + ...lidation_fails_for_invalid_type_schema.yml | 3 + ..._fails_for_names_not_matching.obtained.yml | 3 + ...alidation_fails_for_names_not_matching.yml | 3 + ...ema_and_members_dict_mismatch.obtained.yml | 2 + ...s_for_schema_and_members_dict_mismatch.yml | 2 + ...ema_and_members_enum_mismatch.obtained.yml | 2 + ...s_for_schema_and_members_enum_mismatch.yml | 2 + ...s_for_schema_and_members_list.obtained.yml | 3 + ...tion_fails_for_schema_and_members_list.yml | 3 + ...on_fails_for_two_descriptions.obtained.yml | 2 + ..._validation_fails_for_two_descriptions.yml | 2 + ...h_invalid_attrs_raising_error.obtained.yml | 2 + ...tance_with_invalid_attrs_raising_error.yml | 2 + ..._fails_for_duplicate_out_name.obtained.yml | 3 + ...alidation_fails_for_duplicate_out_name.yml | 3 + ...on_fails_for_invalid_out_name.obtained.yml | 3 + ..._validation_fails_for_invalid_out_name.yml | 3 + ...fails_for_invalid_type_schema.obtained.yml | 3 + ...lidation_fails_for_invalid_type_schema.yml | 3 + ..._fails_for_names_not_matching.obtained.yml | 3 + ...alidation_fails_for_names_not_matching.yml | 3 + ...ils_for_schema_missing_fields.obtained.yml | 2 + ...dation_fails_for_schema_missing_fields.yml | 2 + ...on_fails_for_two_descriptions.obtained.yml | 3 + ..._validation_fails_for_two_descriptions.yml | 3 + ...h_invalid_attrs_raising_error.obtained.yml | 2 + ...tance_with_invalid_attrs_raising_error.yml | 2 + ...tion_fails_for_alias_resolver.obtained.yml | 3 + ...pe_validation_fails_for_alias_resolver.yml | 3 + ...ils_for_alias_target_resolver.obtained.yml | 2 + ...dation_fails_for_alias_target_resolver.yml | 2 + ...r_arg_with_double_description.obtained.yml | 3 + ..._fails_for_arg_with_double_description.yml | 3 + ...ails_for_arg_with_name_option.obtained.yml | 3 + ...idation_fails_for_arg_with_name_option.yml | 3 + ...ails_for_arg_with_type_option.obtained.yml | 3 + ...idation_fails_for_arg_with_type_option.yml | 3 + ...tion_fails_for_field_instance.obtained.yml | 3 + ...pe_validation_fails_for_field_instance.yml | 3 + ...r_field_with_invalid_arg_name.obtained.yml | 3 + ..._fails_for_field_with_invalid_arg_name.yml | 3 + ...ld_with_multiple_descriptions.obtained.yml | 2 + ...s_for_field_with_multiple_descriptions.yml | 2 + ...ation_fails_for_invalid_alias.obtained.yml | 2 + ...ype_validation_fails_for_invalid_alias.yml | 2 + ...fails_for_invalid_type_schema.obtained.yml | 3 + ...lidation_fails_for_invalid_type_schema.yml | 3 + ...tion_fails_for_missing_fields.obtained.yml | 2 + ...pe_validation_fails_for_missing_fields.yml | 2 + ..._for_multiple_field_resolvers.obtained.yml | 2 + ...ion_fails_for_multiple_field_resolvers.yml | 2 + ..._fails_for_names_not_matching.obtained.yml | 3 + ...alidation_fails_for_names_not_matching.yml | 3 + ...on_fails_for_two_descriptions.obtained.yml | 3 + ..._validation_fails_for_two_descriptions.yml | 3 + ..._for_undefined_field_resolver.obtained.yml | 2 + ...ion_fails_for_undefined_field_resolver.yml | 2 + ...upported_resolver_arg_default.obtained.yml | 3 + ...s_for_unsupported_resolver_arg_default.yml | 3 + ...d_resolver_arg_option_default.obtained.yml | 3 + ...nsupported_resolver_arg_option_default.yml | 3 + ...ion_fails_for_different_names.obtained.yml | 3 + ...e_validation_fails_for_different_names.yml | 3 + ...fails_for_invalid_type_schema.obtained.yml | 3 + ...lidation_fails_for_invalid_type_schema.yml | 3 + ...on_fails_for_two_descriptions.obtained.yml | 3 + ..._validation_fails_for_two_descriptions.yml | 3 + ...ils_if_lazy_type_doesnt_exist.obtained.yml | 2 + ...dation_fails_if_lazy_type_doesnt_exist.yml | 2 + ...d_arg_not_dict_without_schema.obtained.yml | 3 + ...args_field_arg_not_dict_without_schema.yml | 3 + ..._args_not_dict_without_schema.obtained.yml | 2 + ...st_source_args_not_dict_without_schema.yml | 2 + ...r_undefined_field_with_schema.obtained.yml | 2 + ...source_for_undefined_field_with_schema.yml | 2 + ...undefined_name_without_schema.obtained.yml | 2 + .../test_undefined_name_without_schema.yml | 2 + ..._include_members_are_combined.obtained.yml | 1 + ...clude_and_include_members_are_combined.yml | 1 + ...tion_is_set_for_excluded_item.obtained.yml | 3 + ...r_description_is_set_for_excluded_item.yml | 3 + ...ption_is_set_for_missing_item.obtained.yml | 3 + ...er_description_is_set_for_missing_item.yml | 3 + ...ption_is_set_for_omitted_item.obtained.yml | 3 + ...er_description_is_set_for_omitted_item.yml | 3 + tests_next/test_deferred_type.py | 4 +- tests_next/test_enum_type_validation.py | 44 +++--- tests_next/test_input_type.py | 8 +- tests_next/test_input_type_validation.py | 36 ++--- tests_next/test_interface_type.py | 8 +- tests_next/test_interface_type_validation.py | 8 +- tests_next/test_make_executable_schema.py | 16 +-- tests_next/test_metadata.py | 4 +- tests_next/test_object_type.py | 8 +- tests_next/test_object_type_validation.py | 132 +++++++++--------- tests_next/test_scalar_type_validation.py | 12 +- tests_next/test_standard_enum.py | 16 +-- .../test_subscription_type_validation.py | 52 +++---- tests_next/test_typing.py | 30 +++- tests_next/test_union_type.py | 14 +- tests_next/test_union_type_validation.py | 8 +- tests_next/test_validators.py | 10 +- 214 files changed, 689 insertions(+), 502 deletions(-) delete mode 100644 tests_next/snapshots/__init__.py delete mode 100644 tests_next/snapshots/snap_test_deferred_type.py delete mode 100644 tests_next/snapshots/snap_test_enum_type_validation.py delete mode 100644 tests_next/snapshots/snap_test_input_type.py delete mode 100644 tests_next/snapshots/snap_test_input_type_validation.py delete mode 100644 tests_next/snapshots/snap_test_interface_type_validation.py delete mode 100644 tests_next/snapshots/snap_test_make_executable_schema.py delete mode 100644 tests_next/snapshots/snap_test_metadata.py delete mode 100644 tests_next/snapshots/snap_test_object_type.py delete mode 100644 tests_next/snapshots/snap_test_object_type_validation.py delete mode 100644 tests_next/snapshots/snap_test_scalar_type_validation.py delete mode 100644 tests_next/snapshots/snap_test_standard_enum.py delete mode 100644 tests_next/snapshots/snap_test_subscription_type_validation.py delete mode 100644 tests_next/snapshots/snap_test_union_type_validation.py delete mode 100644 tests_next/snapshots/snap_test_validators.py create mode 100644 tests_next/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml create mode 100644 tests_next/snapshots/test_arg_with_description_in_source_with_schema.yml create mode 100644 tests_next/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml create mode 100644 tests_next/snapshots/test_arg_with_name_in_source_with_schema.yml create mode 100644 tests_next/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml create mode 100644 tests_next/snapshots/test_arg_with_type_in_source_with_schema.yml create mode 100644 tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml create mode 100644 tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml create mode 100644 tests_next/snapshots/test_description_not_str_without_schema.obtained.yml create mode 100644 tests_next/snapshots/test_description_not_str_without_schema.yml create mode 100644 tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml create mode 100644 tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml create mode 100644 tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.yml create mode 100644 tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml create mode 100644 tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.yml create mode 100644 tests_next/snapshots/test_field_name_not_str_without_schema.obtained.yml create mode 100644 tests_next/snapshots/test_field_name_not_str_without_schema.yml create mode 100644 tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml create mode 100644 tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml create mode 100644 tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml create mode 100644 tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml create mode 100644 tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml create mode 100644 tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml create mode 100644 tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml create mode 100644 tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml create mode 100644 tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml create mode 100644 tests_next/snapshots/test_interface_no_interface_in_schema.yml create mode 100644 tests_next/snapshots/test_interface_with_different_types.obtained.yml create mode 100644 tests_next/snapshots/test_interface_with_different_types.yml create mode 100644 tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml create mode 100644 tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.yml create mode 100644 tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml create mode 100644 tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml create mode 100644 tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml create mode 100644 tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml create mode 100644 tests_next/snapshots/test_missing_type_in_schema.obtained.yml create mode 100644 tests_next/snapshots/test_missing_type_in_schema.yml create mode 100644 tests_next/snapshots/test_missing_type_in_types.obtained.yml create mode 100644 tests_next/snapshots/test_missing_type_in_types.yml create mode 100644 tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml create mode 100644 tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.yml create mode 100644 tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml create mode 100644 tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml create mode 100644 tests_next/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml create mode 100644 tests_next/snapshots/test_multiple_sourced_for_field_with_schema.yml create mode 100644 tests_next/snapshots/test_multiple_sources_without_schema.obtained.yml create mode 100644 tests_next/snapshots/test_multiple_sources_without_schema.yml create mode 100644 tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml create mode 100644 tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml create mode 100644 tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml create mode 100644 tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml create mode 100644 tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml create mode 100644 tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml create mode 100644 tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml create mode 100644 tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml create mode 100644 tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml create mode 100644 tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml create mode 100644 tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml create mode 100644 tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml create mode 100644 tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml create mode 100644 tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml create mode 100644 tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml create mode 100644 tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml create mode 100644 tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml create mode 100644 tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.yml create mode 100644 tests_next/snapshots/test_source_args_not_dict_without_schema.obtained.yml create mode 100644 tests_next/snapshots/test_source_args_not_dict_without_schema.yml create mode 100644 tests_next/snapshots/test_source_for_undefined_field_with_schema.obtained.yml create mode 100644 tests_next/snapshots/test_source_for_undefined_field_with_schema.yml create mode 100644 tests_next/snapshots/test_undefined_name_without_schema.obtained.yml create mode 100644 tests_next/snapshots/test_undefined_name_without_schema.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml create mode 100644 tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 22ffa67..b5a6ad7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - - name: Set up Python 3.11 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Install pypa/build run: >- python -m diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2fe4eb9..9b5d3f4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 diff --git a/ariadne_graphql_modules/next/graphql_input/input_type.py b/ariadne_graphql_modules/next/graphql_input/input_type.py index 023df84..6d03a05 100644 --- a/ariadne_graphql_modules/next/graphql_input/input_type.py +++ b/ariadne_graphql_modules/next/graphql_input/input_type.py @@ -1,6 +1,6 @@ from copy import deepcopy from enum import Enum -from typing import Any, Dict, Iterable, List, Optional, Type, cast +from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast from graphql import InputObjectTypeDefinitionNode, InputValueDefinitionNode, NameNode @@ -106,7 +106,7 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLInputModel": def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLInputModel": - type_hints = cls.__annotations__ + type_hints = cls.__annotations__ # pylint: disable=no-member fields_instances: Dict[str, GraphQLInputField] = { attr_name: getattr(cls, attr_name) for attr_name in dir(cls) @@ -180,9 +180,9 @@ def __get_graphql_model_without_schema__( @classmethod def __get_graphql_types__( cls, _: "GraphQLMetadata" - ) -> Iterable[Type["GraphQLType"] | Type[Enum]]: + ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: """Returns iterable with GraphQL types associated with this type""" - types: List[Type["GraphQLType"] | Type[Enum]] = [cls] + types: List[Union[Type["GraphQLType"], Type[Enum]]] = [cls] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -192,7 +192,7 @@ def __get_graphql_types__( if field_graphql_type and field_graphql_type not in types: types.append(field_graphql_type) - type_hints = cls.__annotations__ + type_hints = cls.__annotations__ # pylint: disable=no-member for hint_name, hint_type in type_hints.items(): if hint_name.startswith("__"): continue diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/next/typing.py index 8a6b2ff..a482c0d 100644 --- a/ariadne_graphql_modules/next/typing.py +++ b/ariadne_graphql_modules/next/typing.py @@ -1,6 +1,7 @@ from enum import Enum from importlib import import_module from inspect import isclass +import sys from typing import ( Annotated, Any, @@ -19,12 +20,14 @@ NonNullTypeNode, TypeNode, ) - from .base import GraphQLMetadata, GraphQLType from .deferredtype import DeferredTypeData from .idtype import GraphQLID -UnionType = type(int | str) +if sys.version_info >= (3, 10): + from types import UnionType +else: + UnionType = Union def get_type_node( diff --git a/pyproject.toml b/pyproject.toml index 2b7b98d..bb2236b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ariadne-graphql-modules" -version = "0.8.0" +version = "1.0.0" description = "Ariadne toolkit for defining GraphQL schemas in modular fashion." authors = [{ name = "Mirumee Software", email = "hello@mirumee.com" }] readme = "README.md" @@ -15,15 +15,14 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", ] -dependencies = [ - "ariadne==0.23.0", -] + +dependencies = ["ariadne==0.23.0"] [project.optional-dependencies] test = [ @@ -32,7 +31,8 @@ test = [ "pylint", "pytest", "pytest-asyncio", - "snapshottest", + "pytest-regressions", + "pytest-datadir", ] [project.urls] @@ -47,16 +47,15 @@ include = [ "ariadne_graphql_modules/**/*.py", "ariadne_graphql_modules/py.typed", ] -exclude = [ - "tests", -] +exclude = ["tests"] [tool.hatch.envs.default] features = ["test"] +python = "3.12" [tool.black] line-length = 88 -target-version = ["py38", "py39", "py310", "py311"] +target-version = ["py39", "py310", "py311", "py312"] include = '\.pyi?$' exclude = ''' /( diff --git a/tests_next/conftest.py b/tests_next/conftest.py index 19fa568..46da232 100644 --- a/tests_next/conftest.py +++ b/tests_next/conftest.py @@ -1,3 +1,4 @@ +from pathlib import Path from textwrap import dedent import pytest @@ -27,3 +28,13 @@ def ast_equals_assertion(ast: TypeDefinitionNode, target: str): @pytest.fixture def metadata(): return GraphQLMetadata() + + +@pytest.fixture(scope="session") +def datadir() -> Path: + return Path(__file__).parent / "snapshots" + + +@pytest.fixture(scope="session") +def original_datadir() -> Path: + return Path(__file__).parent / "snapshots" diff --git a/tests_next/snapshots/__init__.py b/tests_next/snapshots/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests_next/snapshots/snap_test_deferred_type.py b/tests_next/snapshots/snap_test_deferred_type.py deleted file mode 100644 index a3d83e4..0000000 --- a/tests_next/snapshots/snap_test_deferred_type.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_deferred_raises_error_for_invalid_relative_path 1'] = "'...types' points outside of the 'lorem' package." diff --git a/tests_next/snapshots/snap_test_enum_type_validation.py b/tests_next/snapshots/snap_test_enum_type_validation.py deleted file mode 100644 index 18f5943..0000000 --- a/tests_next/snapshots/snap_test_enum_type_validation.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_enum_type_validation_fails_for_invalid_members 1'] = "Class 'UserLevel' '__members__' attribute is of unsupported type. Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. (found: '')" - -snapshots['test_enum_type_validation_fails_for_missing_members 1'] = "Class 'UserLevel' '__members__' attribute is either missing or empty. Either define it or provide full SDL for this enum using the '__schema__' attribute." - -snapshots['test_schema_enum_type_validation_fails_for_duplicated_members_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for enum members that also have description in '__schema__' attribute. (members: 'MEMBER')" - -snapshots['test_schema_enum_type_validation_fails_for_empty_enum 1'] = "Class 'UserLevel' defines '__schema__' attribute that doesn't declare any enum members." - -snapshots['test_schema_enum_type_validation_fails_for_invalid_members_descriptions 1'] = "Class 'UserLevel' '__members_descriptions__' attribute defines descriptions for undefined enum members. (undefined members: 'INVALID')" - -snapshots['test_schema_enum_type_validation_fails_for_invalid_type_schema 1'] = "Class 'UserLevel' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'EnumTypeDefinitionNode')" - -snapshots['test_schema_enum_type_validation_fails_for_names_not_matching 1'] = "Class 'UserLevel' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('UserRank' != 'Custom')" - -snapshots['test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MEMBER')" - -snapshots['test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch 1'] = "Class 'UserLevel' '__members__' is missing values for enum members defined in '__schema__'. (missing items: 'MODERATOR')" - -snapshots['test_schema_enum_type_validation_fails_for_schema_and_members_list 1'] = "Class 'UserLevel' '__members__' attribute can't be a list when used together with '__schema__'." - -snapshots['test_schema_enum_type_validation_fails_for_two_descriptions 1'] = "Class 'UserLevel' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_input_type.py b/tests_next/snapshots/snap_test_input_type.py deleted file mode 100644 index afc1d62..0000000 --- a/tests_next/snapshots/snap_test_input_type.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_input_type_instance_with_invalid_attrs_raising_error 1'] = "SearchInput.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'query', 'age'" - -snapshots['test_schema_input_type_instance_with_invalid_attrs_raising_error 1'] = "SearchInput.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'query', 'age'" diff --git a/tests_next/snapshots/snap_test_input_type_validation.py b/tests_next/snapshots/snap_test_input_type_validation.py deleted file mode 100644 index b45d388..0000000 --- a/tests_next/snapshots/snap_test_input_type_validation.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_input_type_validation_fails_for_out_names_without_schema 1'] = "Class 'CustomType' defines '__out_names__' attribute. This is not supported for types not defining '__schema__'." - -snapshots['test_input_type_validation_fails_for_unsupported_attr_default 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." - -snapshots['test_input_type_validation_fails_for_unsupported_field_default_option 1'] = "Class 'QueryType' defines default value for the 'attr' field that can't be represented in GraphQL schema." - -snapshots['test_schema_input_type_validation_fails_for_duplicate_out_name 1'] = "Class 'CustomType' defines multiple fields with an outname 'ok' in it's '__out_names__' attribute." - -snapshots['test_schema_input_type_validation_fails_for_invalid_out_name 1'] = "Class 'CustomType' defines an outname for 'invalid' field in it's '__out_names__' attribute which is not defined in '__schema__'." - -snapshots['test_schema_input_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'InputObjectTypeDefinitionNode')" - -snapshots['test_schema_input_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" - -snapshots['test_schema_input_type_validation_fails_for_schema_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an input type without any fields. " - -snapshots['test_schema_input_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_interface_type_validation.py b/tests_next/snapshots/snap_test_interface_type_validation.py deleted file mode 100644 index ae606b0..0000000 --- a/tests_next/snapshots/snap_test_interface_type_validation.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_interface_no_interface_in_schema 1'] = "Unknown type 'BaseInterface'." - -snapshots['test_interface_with_different_types 1'] = '''Query root type must be provided. - -Interface field UserInterface.score expects type String! but User.score is type Int!.''' diff --git a/tests_next/snapshots/snap_test_make_executable_schema.py b/tests_next/snapshots/snap_test_make_executable_schema.py deleted file mode 100644 index df86f38..0000000 --- a/tests_next/snapshots/snap_test_make_executable_schema.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_make_executable_schema_raises_error_if_called_without_any_types 1'] = "'make_executable_schema' was called without any GraphQL types." - -snapshots['test_multiple_roots_fail_validation_if_merge_roots_is_disabled 1'] = "Types 'SecondRoot' and '.FirstRoot'>' both define GraphQL type with name 'Query'." - -snapshots['test_schema_validation_fails_if_lazy_type_doesnt_exist 1'] = "Unknown type 'Missing'." diff --git a/tests_next/snapshots/snap_test_metadata.py b/tests_next/snapshots/snap_test_metadata.py deleted file mode 100644 index 4f30414..0000000 --- a/tests_next/snapshots/snap_test_metadata.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_metadata_raises_key_error_for_unset_data 1'] = '"No data is set for \'\'."' diff --git a/tests_next/snapshots/snap_test_object_type.py b/tests_next/snapshots/snap_test_object_type.py deleted file mode 100644 index 0b3809f..0000000 --- a/tests_next/snapshots/snap_test_object_type.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_object_type_instance_with_invalid_attrs_raising_error 1'] = "CategoryType.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'name', 'posts'" - -snapshots['test_schema_object_type_instance_with_invalid_attrs_raising_error 1'] = "CategoryType.__init__() got an unexpected keyword argument 'invalid'. Valid keyword arguments: 'name', 'posts'" diff --git a/tests_next/snapshots/snap_test_object_type_validation.py b/tests_next/snapshots/snap_test_object_type_validation.py deleted file mode 100644 index 2041eaa..0000000 --- a/tests_next/snapshots/snap_test_object_type_validation.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_object_type_validation_fails_for_alias_resolver 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." - -snapshots['test_object_type_validation_fails_for_alias_target_resolver 1'] = "Class 'CustomType' defines resolver for an undefined field 'welcome'. (Valid fields: 'hello')" - -snapshots['test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'userId'." - -snapshots['test_object_type_validation_fails_for_field_with_multiple_args 1'] = "Class 'CustomType' defines multiple resolvers for field 'lorem'." - -snapshots['test_object_type_validation_fails_for_field_with_multiple_descriptions 1'] = "Class 'CustomType' defines multiple descriptions for field 'hello'." - -snapshots['test_object_type_validation_fails_for_invalid_alias 1'] = "Class 'CustomType' defines an alias for an undefined field 'invalid'. (Valid fields: 'hello')" - -snapshots['test_object_type_validation_fails_for_missing_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (expected one of: 'name')" - -snapshots['test_object_type_validation_fails_for_missing_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (expected one of: 'name')" - -snapshots['test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'userId'." - -snapshots['test_object_type_validation_fails_for_multiple_field_resolvers 1'] = "Class 'CustomType' defines multiple resolvers for field 'hello'." - -snapshots['test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name 1'] = "Class 'CustomType' defines multiple fields with GraphQL name 'hello'." - -snapshots['test_object_type_validation_fails_for_undefined_attr_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" - -snapshots['test_object_type_validation_fails_for_undefined_field_resolver_arg 1'] = "Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" - -snapshots['test_object_type_validation_fails_for_undefined_resolver_arg 1'] = "Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' argument thats not defined on the resolver function. (function accepts no extra arguments)" - -snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." - -snapshots['test_object_type_validation_fails_for_unsupported_resolver_arg_default_option 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." - -snapshots['test_schema_object_type_validation_fails_for_alias_resolver 1'] = "Class 'CustomType' defines an alias for a field 'hello' that already has a custom resolver." - -snapshots['test_schema_object_type_validation_fails_for_alias_target_resolver 1'] = "Class 'CustomType' defines resolver for an undefined field 'ok'. (Valid fields: 'hello')" - -snapshots['test_schema_object_type_validation_fails_for_arg_with_double_description 1'] = "Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' field." - -snapshots['test_schema_object_type_validation_fails_for_arg_with_name_option 1'] = "Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." - -snapshots['test_schema_object_type_validation_fails_for_arg_with_type_option 1'] = "Class 'CustomType' defines 'type' option for 'name' argument of the 'hello' field. This is not supported for types defining '__schema__'." - -snapshots['test_schema_object_type_validation_fails_for_field_instance 1'] = "Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for types defining '__schema__'." - -snapshots['test_schema_object_type_validation_fails_for_field_with_invalid_arg_name 1'] = "Class 'CustomType' defines options for 'other' argument of the 'hello' field that doesn't exist." - -snapshots['test_schema_object_type_validation_fails_for_field_with_multiple_descriptions 1'] = "Class 'CustomType' defines multiple descriptions for field 'hello'." - -snapshots['test_schema_object_type_validation_fails_for_invalid_alias 1'] = "Class 'CustomType' defines an alias for an undefined field 'invalid'. (Valid fields: 'hello')" - -snapshots['test_schema_object_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode')" - -snapshots['test_schema_object_type_validation_fails_for_missing_fields 1'] = "Class 'CustomType' defines '__schema__' attribute with declaration for an object type without any fields. " - -snapshots['test_schema_object_type_validation_fails_for_multiple_field_resolvers 1'] = "Class 'CustomType' defines multiple resolvers for field 'hello'." - -snapshots['test_schema_object_type_validation_fails_for_names_not_matching 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Lorem' != 'Custom')" - -snapshots['test_schema_object_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." - -snapshots['test_schema_object_type_validation_fails_for_undefined_field_resolver 1'] = "Class 'QueryType' defines resolver for an undefined field 'other'. (Valid fields: 'hello')" - -snapshots['test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." - -snapshots['test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default 1'] = "Class 'QueryType' defines default value for 'name' argument of the 'hello' field that can't be represented in GraphQL schema." diff --git a/tests_next/snapshots/snap_test_scalar_type_validation.py b/tests_next/snapshots/snap_test_scalar_type_validation.py deleted file mode 100644 index e481060..0000000 --- a/tests_next/snapshots/snap_test_scalar_type_validation.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_schema_scalar_type_validation_fails_for_different_names 1'] = "Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Date' != 'Custom')" - -snapshots['test_schema_scalar_type_validation_fails_for_invalid_type_schema 1'] = "Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode')" - -snapshots['test_schema_scalar_type_validation_fails_for_two_descriptions 1'] = "Class 'CustomScalar' defines description in both '__description__' and '__schema__' attributes." diff --git a/tests_next/snapshots/snap_test_standard_enum.py b/tests_next/snapshots/snap_test_standard_enum.py deleted file mode 100644 index bf78283..0000000 --- a/tests_next/snapshots/snap_test_standard_enum.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_value_error_is_raised_if_exclude_and_include_members_are_combined 1'] = "'members_include' and 'members_exclude' options are mutually exclusive." - -snapshots['test_value_error_is_raised_if_member_description_is_set_for_excluded_item 1'] = "Member description was specified for a member 'ADMINISTRATOR' not present in final GraphQL enum." - -snapshots['test_value_error_is_raised_if_member_description_is_set_for_missing_item 1'] = "Member description was specified for a member 'MISSING' not present in final GraphQL enum." - -snapshots['test_value_error_is_raised_if_member_description_is_set_for_omitted_item 1'] = "Member description was specified for a member 'ADMINISTRATOR' not present in final GraphQL enum." diff --git a/tests_next/snapshots/snap_test_subscription_type_validation.py b/tests_next/snapshots/snap_test_subscription_type_validation.py deleted file mode 100644 index 0901764..0000000 --- a/tests_next/snapshots/snap_test_subscription_type_validation.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_arg_with_description_in_source_with_schema 1'] = "Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' field. This is not supported for types defining '__schema__'." - -snapshots['test_arg_with_name_in_source_with_schema 1'] = "Class 'SubscriptionType' defines 'name' option for 'channel' argument of the 'messageAdded' field. This is not supported for types defining '__schema__'." - -snapshots['test_arg_with_type_in_source_with_schema 1'] = "Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' field. This is not supported for types defining '__schema__'." - -snapshots['test_description_not_str_without_schema 1'] = 'The description for message_added_generator must be a string if provided.' - -snapshots['test_field_name_not_str_without_schema 1'] = 'The field name for message_added_generator must be a string.' - -snapshots['test_invalid_arg_name_in_source_with_schema 1'] = "Class 'SubscriptionType' defines options for 'channelID' argument of the 'messageAdded' field that doesn't exist." - -snapshots['test_multiple_descriptions_for_source_with_schema 1'] = "Class 'SubscriptionType' defines multiple descriptions for field 'messageAdded'." - -snapshots['test_multiple_sourced_for_field_with_schema 1'] = "Class 'SubscriptionType' defines multiple sources for field 'messageAdded'." - -snapshots['test_multiple_sources_without_schema 1'] = "Class 'SubscriptionType' defines multiple sources for field 'message_added'." - -snapshots['test_source_args_field_arg_not_dict_without_schema 1'] = 'Argument channel for message_added_generator must have a GraphQLObjectFieldArg as its info.' - -snapshots['test_source_args_not_dict_without_schema 1'] = 'The args for message_added_generator must be a dictionary if provided.' - -snapshots['test_source_for_undefined_field_with_schema 1'] = "Class 'SubscriptionType' defines source for an undefined field 'message_added'. (Valid fields: 'messageAdded')" - -snapshots['test_undefined_name_without_schema 1'] = "Class 'SubscriptionType' defines source for an undefined field 'messageAdded'. (Valid fields: 'message_added')" diff --git a/tests_next/snapshots/snap_test_union_type_validation.py b/tests_next/snapshots/snap_test_union_type_validation.py deleted file mode 100644 index 4abc204..0000000 --- a/tests_next/snapshots/snap_test_union_type_validation.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_missing_type_in_schema 1'] = "Types 'Comment', 'Post' are in '__types__' but not in '__schema__'." - -snapshots['test_missing_type_in_types 1'] = "Types 'Comment' are in '__schema__' but not in '__types__'." diff --git a/tests_next/snapshots/snap_test_validators.py b/tests_next/snapshots/snap_test_validators.py deleted file mode 100644 index ab766f3..0000000 --- a/tests_next/snapshots/snap_test_validators.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_description_validator_raises_error_for_type_with_two_descriptions 1'] = "Class 'CustomType' defines description in both '__description__' and '__schema__' attributes." - -snapshots['test_name_validator_raises_error_for_name_and_definition_mismatch 1'] = "Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but names in those don't match. ('Example' != 'Custom')" diff --git a/tests_next/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml b/tests_next/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml new file mode 100644 index 0000000..37ea424 --- /dev/null +++ b/tests_next/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' + field. This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_arg_with_description_in_source_with_schema.yml b/tests_next/snapshots/test_arg_with_description_in_source_with_schema.yml new file mode 100644 index 0000000..37ea424 --- /dev/null +++ b/tests_next/snapshots/test_arg_with_description_in_source_with_schema.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' + field. This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml b/tests_next/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml new file mode 100644 index 0000000..5e93ffd --- /dev/null +++ b/tests_next/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines 'name' option for 'channel' argument of the 'messageAdded' + field. This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_arg_with_name_in_source_with_schema.yml b/tests_next/snapshots/test_arg_with_name_in_source_with_schema.yml new file mode 100644 index 0000000..5e93ffd --- /dev/null +++ b/tests_next/snapshots/test_arg_with_name_in_source_with_schema.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines 'name' option for 'channel' argument of the 'messageAdded' + field. This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml b/tests_next/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml new file mode 100644 index 0000000..37ea424 --- /dev/null +++ b/tests_next/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' + field. This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_arg_with_type_in_source_with_schema.yml b/tests_next/snapshots/test_arg_with_type_in_source_with_schema.yml new file mode 100644 index 0000000..37ea424 --- /dev/null +++ b/tests_next/snapshots/test_arg_with_type_in_source_with_schema.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' + field. This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml b/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml new file mode 100644 index 0000000..82cb498 --- /dev/null +++ b/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml @@ -0,0 +1 @@ +'''...types'' points outside of the ''lorem'' package.' diff --git a/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml b/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml new file mode 100644 index 0000000..82cb498 --- /dev/null +++ b/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml @@ -0,0 +1 @@ +'''...types'' points outside of the ''lorem'' package.' diff --git a/tests_next/snapshots/test_description_not_str_without_schema.obtained.yml b/tests_next/snapshots/test_description_not_str_without_schema.obtained.yml new file mode 100644 index 0000000..b43461c --- /dev/null +++ b/tests_next/snapshots/test_description_not_str_without_schema.obtained.yml @@ -0,0 +1,2 @@ +The description for message_added_generator must be a string if provided. +... diff --git a/tests_next/snapshots/test_description_not_str_without_schema.yml b/tests_next/snapshots/test_description_not_str_without_schema.yml new file mode 100644 index 0000000..b43461c --- /dev/null +++ b/tests_next/snapshots/test_description_not_str_without_schema.yml @@ -0,0 +1,2 @@ +The description for message_added_generator must be a string if provided. +... diff --git a/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml b/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml new file mode 100644 index 0000000..ba71cfb --- /dev/null +++ b/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml b/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml new file mode 100644 index 0000000..ba71cfb --- /dev/null +++ b/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml b/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml new file mode 100644 index 0000000..f905b16 --- /dev/null +++ b/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members__'' attribute is of unsupported type. Expected ''Dict[str, + Any]'', ''Type[Enum]'' or List[str]. (found: '''')' diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.yml b/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.yml new file mode 100644 index 0000000..f905b16 --- /dev/null +++ b/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members__'' attribute is of unsupported type. Expected ''Dict[str, + Any]'', ''Type[Enum]'' or List[str]. (found: '''')' diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml b/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml new file mode 100644 index 0000000..1be2153 --- /dev/null +++ b/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' '__members__' attribute is either missing or empty. Either define + it or provide full SDL for this enum using the '__schema__' attribute. +... diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.yml b/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.yml new file mode 100644 index 0000000..1be2153 --- /dev/null +++ b/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' '__members__' attribute is either missing or empty. Either define + it or provide full SDL for this enum using the '__schema__' attribute. +... diff --git a/tests_next/snapshots/test_field_name_not_str_without_schema.obtained.yml b/tests_next/snapshots/test_field_name_not_str_without_schema.obtained.yml new file mode 100644 index 0000000..17a44f2 --- /dev/null +++ b/tests_next/snapshots/test_field_name_not_str_without_schema.obtained.yml @@ -0,0 +1,2 @@ +The field name for message_added_generator must be a string. +... diff --git a/tests_next/snapshots/test_field_name_not_str_without_schema.yml b/tests_next/snapshots/test_field_name_not_str_without_schema.yml new file mode 100644 index 0000000..17a44f2 --- /dev/null +++ b/tests_next/snapshots/test_field_name_not_str_without_schema.yml @@ -0,0 +1,2 @@ +The field name for message_added_generator must be a string. +... diff --git a/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml new file mode 100644 index 0000000..c51434c --- /dev/null +++ b/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml @@ -0,0 +1,2 @@ +'SearchInput.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''query'', ''age''' diff --git a/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml b/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml new file mode 100644 index 0000000..c51434c --- /dev/null +++ b/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml @@ -0,0 +1,2 @@ +'SearchInput.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''query'', ''age''' diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml b/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml new file mode 100644 index 0000000..2eeb09c --- /dev/null +++ b/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines '__out_names__' attribute. This is not supported for types + not defining '__schema__'. +... diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml b/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml new file mode 100644 index 0000000..2eeb09c --- /dev/null +++ b/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines '__out_names__' attribute. This is not supported for types + not defining '__schema__'. +... diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml new file mode 100644 index 0000000..87d2d4e --- /dev/null +++ b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for the 'attr' field that can't be represented + in GraphQL schema. +... diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml new file mode 100644 index 0000000..87d2d4e --- /dev/null +++ b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for the 'attr' field that can't be represented + in GraphQL schema. +... diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml new file mode 100644 index 0000000..87d2d4e --- /dev/null +++ b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for the 'attr' field that can't be represented + in GraphQL schema. +... diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml new file mode 100644 index 0000000..87d2d4e --- /dev/null +++ b/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for the 'attr' field that can't be represented + in GraphQL schema. +... diff --git a/tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml b/tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml new file mode 100644 index 0000000..63f9405 --- /dev/null +++ b/tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml @@ -0,0 +1,2 @@ +Unknown type 'BaseInterface'. +... diff --git a/tests_next/snapshots/test_interface_no_interface_in_schema.yml b/tests_next/snapshots/test_interface_no_interface_in_schema.yml new file mode 100644 index 0000000..63f9405 --- /dev/null +++ b/tests_next/snapshots/test_interface_no_interface_in_schema.yml @@ -0,0 +1,2 @@ +Unknown type 'BaseInterface'. +... diff --git a/tests_next/snapshots/test_interface_with_different_types.obtained.yml b/tests_next/snapshots/test_interface_with_different_types.obtained.yml new file mode 100644 index 0000000..f10dac9 --- /dev/null +++ b/tests_next/snapshots/test_interface_with_different_types.obtained.yml @@ -0,0 +1,5 @@ +'Query root type must be provided. + + + Interface field UserInterface.score expects type String! but User.score is type + Int!.' diff --git a/tests_next/snapshots/test_interface_with_different_types.yml b/tests_next/snapshots/test_interface_with_different_types.yml new file mode 100644 index 0000000..f10dac9 --- /dev/null +++ b/tests_next/snapshots/test_interface_with_different_types.yml @@ -0,0 +1,5 @@ +'Query root type must be provided. + + + Interface field UserInterface.score expects type String! but User.score is type + Int!.' diff --git a/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml b/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml new file mode 100644 index 0000000..78d9da1 --- /dev/null +++ b/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines options for 'channelID' argument of the 'messageAdded' + field that doesn't exist. +... diff --git a/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.yml b/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.yml new file mode 100644 index 0000000..78d9da1 --- /dev/null +++ b/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.yml @@ -0,0 +1,3 @@ +Class 'SubscriptionType' defines options for 'channelID' argument of the 'messageAdded' + field that doesn't exist. +... diff --git a/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml b/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml new file mode 100644 index 0000000..e939646 --- /dev/null +++ b/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml @@ -0,0 +1 @@ +'''make_executable_schema'' was called without any GraphQL types.' diff --git a/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml b/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml new file mode 100644 index 0000000..e939646 --- /dev/null +++ b/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml @@ -0,0 +1 @@ +'''make_executable_schema'' was called without any GraphQL types.' diff --git a/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml b/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml new file mode 100644 index 0000000..d7bf90b --- /dev/null +++ b/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml @@ -0,0 +1 @@ +'"No data is set for ''''."' diff --git a/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml b/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml new file mode 100644 index 0000000..d7bf90b --- /dev/null +++ b/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml @@ -0,0 +1 @@ +'"No data is set for ''''."' diff --git a/tests_next/snapshots/test_missing_type_in_schema.obtained.yml b/tests_next/snapshots/test_missing_type_in_schema.obtained.yml new file mode 100644 index 0000000..8c91f42 --- /dev/null +++ b/tests_next/snapshots/test_missing_type_in_schema.obtained.yml @@ -0,0 +1,2 @@ +Types 'Comment', 'Post' are in '__types__' but not in '__schema__'. +... diff --git a/tests_next/snapshots/test_missing_type_in_schema.yml b/tests_next/snapshots/test_missing_type_in_schema.yml new file mode 100644 index 0000000..8c91f42 --- /dev/null +++ b/tests_next/snapshots/test_missing_type_in_schema.yml @@ -0,0 +1,2 @@ +Types 'Comment', 'Post' are in '__types__' but not in '__schema__'. +... diff --git a/tests_next/snapshots/test_missing_type_in_types.obtained.yml b/tests_next/snapshots/test_missing_type_in_types.obtained.yml new file mode 100644 index 0000000..e504065 --- /dev/null +++ b/tests_next/snapshots/test_missing_type_in_types.obtained.yml @@ -0,0 +1,2 @@ +Types 'Comment' are in '__schema__' but not in '__types__'. +... diff --git a/tests_next/snapshots/test_missing_type_in_types.yml b/tests_next/snapshots/test_missing_type_in_types.yml new file mode 100644 index 0000000..e504065 --- /dev/null +++ b/tests_next/snapshots/test_missing_type_in_types.yml @@ -0,0 +1,2 @@ +Types 'Comment' are in '__schema__' but not in '__types__'. +... diff --git a/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml b/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml new file mode 100644 index 0000000..fb20bdf --- /dev/null +++ b/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml @@ -0,0 +1,2 @@ +Class 'SubscriptionType' defines multiple descriptions for field 'messageAdded'. +... diff --git a/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.yml b/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.yml new file mode 100644 index 0000000..fb20bdf --- /dev/null +++ b/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.yml @@ -0,0 +1,2 @@ +Class 'SubscriptionType' defines multiple descriptions for field 'messageAdded'. +... diff --git a/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml b/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml new file mode 100644 index 0000000..4eccfe9 --- /dev/null +++ b/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml @@ -0,0 +1,3 @@ +Types 'SecondRoot' and '.FirstRoot'>' + both define GraphQL type with name 'Query'. +... diff --git a/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml b/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml new file mode 100644 index 0000000..4eccfe9 --- /dev/null +++ b/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml @@ -0,0 +1,3 @@ +Types 'SecondRoot' and '.FirstRoot'>' + both define GraphQL type with name 'Query'. +... diff --git a/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml b/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml new file mode 100644 index 0000000..5fc64fd --- /dev/null +++ b/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml @@ -0,0 +1,2 @@ +Class 'SubscriptionType' defines multiple sources for field 'messageAdded'. +... diff --git a/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.yml b/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.yml new file mode 100644 index 0000000..5fc64fd --- /dev/null +++ b/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.yml @@ -0,0 +1,2 @@ +Class 'SubscriptionType' defines multiple sources for field 'messageAdded'. +... diff --git a/tests_next/snapshots/test_multiple_sources_without_schema.obtained.yml b/tests_next/snapshots/test_multiple_sources_without_schema.obtained.yml new file mode 100644 index 0000000..ba187af --- /dev/null +++ b/tests_next/snapshots/test_multiple_sources_without_schema.obtained.yml @@ -0,0 +1,2 @@ +Class 'SubscriptionType' defines multiple sources for field 'message_added'. +... diff --git a/tests_next/snapshots/test_multiple_sources_without_schema.yml b/tests_next/snapshots/test_multiple_sources_without_schema.yml new file mode 100644 index 0000000..ba187af --- /dev/null +++ b/tests_next/snapshots/test_multiple_sources_without_schema.yml @@ -0,0 +1,2 @@ +Class 'SubscriptionType' defines multiple sources for field 'message_added'. +... diff --git a/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml b/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml new file mode 100644 index 0000000..d649dfe --- /dev/null +++ b/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('Example' != 'Custom') +... diff --git a/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml b/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml new file mode 100644 index 0000000..d649dfe --- /dev/null +++ b/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('Example' != 'Custom') +... diff --git a/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml new file mode 100644 index 0000000..b5049ef --- /dev/null +++ b/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml @@ -0,0 +1,2 @@ +'CategoryType.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''name'', ''posts''' diff --git a/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml b/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml new file mode 100644 index 0000000..b5049ef --- /dev/null +++ b/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml @@ -0,0 +1,2 @@ +'CategoryType.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''name'', ''posts''' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml new file mode 100644 index 0000000..c38c902 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines an alias for a field 'hello' that already has a custom + resolver. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.yml b/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.yml new file mode 100644 index 0000000..c38c902 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines an alias for a field 'hello' that already has a custom + resolver. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml new file mode 100644 index 0000000..ed6cc30 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines resolver for an undefined field ''welcome''. (Valid + fields: ''hello'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml b/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml new file mode 100644 index 0000000..ed6cc30 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines resolver for an undefined field ''welcome''. (Valid + fields: ''hello'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml new file mode 100644 index 0000000..874b9cc --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple fields with GraphQL name 'userId'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml b/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml new file mode 100644 index 0000000..874b9cc --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple fields with GraphQL name 'userId'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml new file mode 100644 index 0000000..9868e1a --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple resolvers for field 'lorem'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml new file mode 100644 index 0000000..9868e1a --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple resolvers for field 'lorem'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml new file mode 100644 index 0000000..694fd36 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple descriptions for field 'hello'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml new file mode 100644 index 0000000..694fd36 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple descriptions for field 'hello'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml new file mode 100644 index 0000000..352c185 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines an alias for an undefined field ''invalid''. (Valid + fields: ''hello'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.yml b/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.yml new file mode 100644 index 0000000..352c185 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines an alias for an undefined field ''invalid''. (Valid + fields: ''hello'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml new file mode 100644 index 0000000..2741e54 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines ''hello'' field with extra configuration for ''invalid'' + argument thats not defined on the resolver function. (expected one of: ''name'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml b/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml new file mode 100644 index 0000000..2741e54 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines ''hello'' field with extra configuration for ''invalid'' + argument thats not defined on the resolver function. (expected one of: ''name'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml new file mode 100644 index 0000000..80d1f4b --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml @@ -0,0 +1,3 @@ +'Class ''CustomType'' defines ''resolve_hello'' resolver with extra configuration + for ''invalid'' argument thats not defined on the resolver function. (expected one + of: ''name'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml b/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml new file mode 100644 index 0000000..80d1f4b --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml @@ -0,0 +1,3 @@ +'Class ''CustomType'' defines ''resolve_hello'' resolver with extra configuration + for ''invalid'' argument thats not defined on the resolver function. (expected one + of: ''name'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml new file mode 100644 index 0000000..874b9cc --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple fields with GraphQL name 'userId'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml new file mode 100644 index 0000000..874b9cc --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple fields with GraphQL name 'userId'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml new file mode 100644 index 0000000..81b2a07 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple resolvers for field 'hello'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml new file mode 100644 index 0000000..81b2a07 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple resolvers for field 'hello'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml new file mode 100644 index 0000000..d792e40 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple fields with GraphQL name 'hello'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml new file mode 100644 index 0000000..d792e40 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple fields with GraphQL name 'hello'. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml new file mode 100644 index 0000000..bcb3882 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml @@ -0,0 +1,2 @@ +'Class ''QueryType'' defines resolver for an undefined field ''other''. (Valid fields: + ''hello'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml new file mode 100644 index 0000000..bcb3882 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml @@ -0,0 +1,2 @@ +'Class ''QueryType'' defines resolver for an undefined field ''other''. (Valid fields: + ''hello'')' diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml new file mode 100644 index 0000000..2de19e8 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument + thats not defined on the resolver function. (function accepts no extra arguments) +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml new file mode 100644 index 0000000..2de19e8 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument + thats not defined on the resolver function. (function accepts no extra arguments) +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml new file mode 100644 index 0000000..dc19fb7 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml @@ -0,0 +1,4 @@ +Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' + argument thats not defined on the resolver function. (function accepts no extra + arguments) +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml new file mode 100644 index 0000000..dc19fb7 --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml @@ -0,0 +1,4 @@ +Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' + argument thats not defined on the resolver function. (function accepts no extra + arguments) +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml new file mode 100644 index 0000000..96581ba --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members_descriptions__'' attribute defines descriptions for + enum members that also have description in ''__schema__'' attribute. (members: ''MEMBER'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml new file mode 100644 index 0000000..96581ba --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members_descriptions__'' attribute defines descriptions for + enum members that also have description in ''__schema__'' attribute. (members: ''MEMBER'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml new file mode 100644 index 0000000..f472c0f --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml @@ -0,0 +1,2 @@ +Class 'UserLevel' defines '__schema__' attribute that doesn't declare any enum members. +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml new file mode 100644 index 0000000..f472c0f --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml @@ -0,0 +1,2 @@ +Class 'UserLevel' defines '__schema__' attribute that doesn't declare any enum members. +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml new file mode 100644 index 0000000..69ed30b --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members_descriptions__'' attribute defines descriptions for + undefined enum members. (undefined members: ''INVALID'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml new file mode 100644 index 0000000..69ed30b --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members_descriptions__'' attribute defines descriptions for + undefined enum members. (undefined members: ''INVALID'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml new file mode 100644 index 0000000..ec84efc --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' defines '__schema__' attribute with declaration for an invalid GraphQL + type. ('ScalarTypeDefinitionNode' != 'EnumTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml new file mode 100644 index 0000000..ec84efc --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' defines '__schema__' attribute with declaration for an invalid GraphQL + type. ('ScalarTypeDefinitionNode' != 'EnumTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml new file mode 100644 index 0000000..76417dc --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('UserRank' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml new file mode 100644 index 0000000..76417dc --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('UserRank' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml new file mode 100644 index 0000000..7945146 --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members__'' is missing values for enum members defined in + ''__schema__''. (missing items: ''MEMBER'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml new file mode 100644 index 0000000..7945146 --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members__'' is missing values for enum members defined in + ''__schema__''. (missing items: ''MEMBER'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml new file mode 100644 index 0000000..9221777 --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members__'' is missing values for enum members defined in + ''__schema__''. (missing items: ''MODERATOR'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml new file mode 100644 index 0000000..9221777 --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml @@ -0,0 +1,2 @@ +'Class ''UserLevel'' ''__members__'' is missing values for enum members defined in + ''__schema__''. (missing items: ''MODERATOR'')' diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml new file mode 100644 index 0000000..ac227b0 --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' '__members__' attribute can't be a list when used together with + '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml new file mode 100644 index 0000000..ac227b0 --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml @@ -0,0 +1,3 @@ +Class 'UserLevel' '__members__' attribute can't be a list when used together with + '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml new file mode 100644 index 0000000..7e8271a --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml @@ -0,0 +1,2 @@ +Class 'UserLevel' defines description in both '__description__' and '__schema__' attributes. +... diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml new file mode 100644 index 0000000..7e8271a --- /dev/null +++ b/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml @@ -0,0 +1,2 @@ +Class 'UserLevel' defines description in both '__description__' and '__schema__' attributes. +... diff --git a/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml new file mode 100644 index 0000000..c51434c --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml @@ -0,0 +1,2 @@ +'SearchInput.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''query'', ''age''' diff --git a/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml b/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml new file mode 100644 index 0000000..c51434c --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml @@ -0,0 +1,2 @@ +'SearchInput.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''query'', ''age''' diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml new file mode 100644 index 0000000..58146cd --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines multiple fields with an outname 'ok' in it's '__out_names__' + attribute. +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml new file mode 100644 index 0000000..58146cd --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines multiple fields with an outname 'ok' in it's '__out_names__' + attribute. +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml new file mode 100644 index 0000000..3b496e3 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines an outname for 'invalid' field in it's '__out_names__' + attribute which is not defined in '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml new file mode 100644 index 0000000..3b496e3 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines an outname for 'invalid' field in it's '__out_names__' + attribute which is not defined in '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml new file mode 100644 index 0000000..c7632c5 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines '__schema__' attribute with declaration for an invalid + GraphQL type. ('ScalarTypeDefinitionNode' != 'InputObjectTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml new file mode 100644 index 0000000..c7632c5 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines '__schema__' attribute with declaration for an invalid + GraphQL type. ('ScalarTypeDefinitionNode' != 'InputObjectTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml new file mode 100644 index 0000000..5d0a017 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('Lorem' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml new file mode 100644 index 0000000..5d0a017 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('Lorem' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml new file mode 100644 index 0000000..7a624a4 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines ''__schema__'' attribute with declaration for an input + type without any fields. ' diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml new file mode 100644 index 0000000..7a624a4 --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines ''__schema__'' attribute with declaration for an input + type without any fields. ' diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml new file mode 100644 index 0000000..ba71cfb --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml b/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml new file mode 100644 index 0000000..ba71cfb --- /dev/null +++ b/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml new file mode 100644 index 0000000..b5049ef --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml @@ -0,0 +1,2 @@ +'CategoryType.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''name'', ''posts''' diff --git a/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml b/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml new file mode 100644 index 0000000..b5049ef --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml @@ -0,0 +1,2 @@ +'CategoryType.__init__() got an unexpected keyword argument ''invalid''. Valid keyword + arguments: ''name'', ''posts''' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml new file mode 100644 index 0000000..c38c902 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines an alias for a field 'hello' that already has a custom + resolver. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml new file mode 100644 index 0000000..c38c902 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines an alias for a field 'hello' that already has a custom + resolver. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml new file mode 100644 index 0000000..e114d51 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines resolver for an undefined field ''ok''. (Valid fields: + ''hello'')' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml new file mode 100644 index 0000000..e114d51 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines resolver for an undefined field ''ok''. (Valid fields: + ''hello'')' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml new file mode 100644 index 0000000..7fb95f5 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' + field. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml new file mode 100644 index 0000000..7fb95f5 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' + field. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml new file mode 100644 index 0000000..f9ac74d --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. + This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml new file mode 100644 index 0000000..f9ac74d --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. + This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml new file mode 100644 index 0000000..021b821 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'type' option for 'name' argument of the 'hello' field. + This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml new file mode 100644 index 0000000..021b821 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'type' option for 'name' argument of the 'hello' field. + This is not supported for types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml new file mode 100644 index 0000000..1fb66ce --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for + types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml new file mode 100644 index 0000000..1fb66ce --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for + types defining '__schema__'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml new file mode 100644 index 0000000..5c96002 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines options for 'other' argument of the 'hello' field that + doesn't exist. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml new file mode 100644 index 0000000..5c96002 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines options for 'other' argument of the 'hello' field that + doesn't exist. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml new file mode 100644 index 0000000..694fd36 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple descriptions for field 'hello'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml new file mode 100644 index 0000000..694fd36 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple descriptions for field 'hello'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml new file mode 100644 index 0000000..352c185 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines an alias for an undefined field ''invalid''. (Valid + fields: ''hello'')' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml new file mode 100644 index 0000000..352c185 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines an alias for an undefined field ''invalid''. (Valid + fields: ''hello'')' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml new file mode 100644 index 0000000..998c1f6 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines '__schema__' attribute with declaration for an invalid + GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml new file mode 100644 index 0000000..998c1f6 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines '__schema__' attribute with declaration for an invalid + GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml new file mode 100644 index 0000000..246403c --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines ''__schema__'' attribute with declaration for an object + type without any fields. ' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml new file mode 100644 index 0000000..246403c --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml @@ -0,0 +1,2 @@ +'Class ''CustomType'' defines ''__schema__'' attribute with declaration for an object + type without any fields. ' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml new file mode 100644 index 0000000..81b2a07 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple resolvers for field 'hello'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml new file mode 100644 index 0000000..81b2a07 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml @@ -0,0 +1,2 @@ +Class 'CustomType' defines multiple resolvers for field 'hello'. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml new file mode 100644 index 0000000..5d0a017 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('Lorem' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml new file mode 100644 index 0000000..5d0a017 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but + names in those don't match. ('Lorem' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml new file mode 100644 index 0000000..ba71cfb --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml new file mode 100644 index 0000000..ba71cfb --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml @@ -0,0 +1,3 @@ +Class 'CustomType' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml new file mode 100644 index 0000000..bcb3882 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml @@ -0,0 +1,2 @@ +'Class ''QueryType'' defines resolver for an undefined field ''other''. (Valid fields: + ''hello'')' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml new file mode 100644 index 0000000..bcb3882 --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml @@ -0,0 +1,2 @@ +'Class ''QueryType'' defines resolver for an undefined field ''other''. (Valid fields: + ''hello'')' diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml new file mode 100644 index 0000000..5d15013 --- /dev/null +++ b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, + but names in those don't match. ('Date' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml new file mode 100644 index 0000000..5d15013 --- /dev/null +++ b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml @@ -0,0 +1,3 @@ +Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, + but names in those don't match. ('Date' != 'Custom') +... diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml new file mode 100644 index 0000000..0eec05e --- /dev/null +++ b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid + GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml new file mode 100644 index 0000000..0eec05e --- /dev/null +++ b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml @@ -0,0 +1,3 @@ +Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid + GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode') +... diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml new file mode 100644 index 0000000..e10ef27 --- /dev/null +++ b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml @@ -0,0 +1,3 @@ +Class 'CustomScalar' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml new file mode 100644 index 0000000..e10ef27 --- /dev/null +++ b/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml @@ -0,0 +1,3 @@ +Class 'CustomScalar' defines description in both '__description__' and '__schema__' + attributes. +... diff --git a/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml b/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml new file mode 100644 index 0000000..f68971b --- /dev/null +++ b/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml @@ -0,0 +1,2 @@ +Unknown type 'Missing'. +... diff --git a/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml b/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml new file mode 100644 index 0000000..f68971b --- /dev/null +++ b/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml @@ -0,0 +1,2 @@ +Unknown type 'Missing'. +... diff --git a/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml b/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml new file mode 100644 index 0000000..51d3732 --- /dev/null +++ b/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml @@ -0,0 +1,3 @@ +Argument channel for message_added_generator must have a GraphQLObjectFieldArg as + its info. +... diff --git a/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.yml b/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.yml new file mode 100644 index 0000000..51d3732 --- /dev/null +++ b/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.yml @@ -0,0 +1,3 @@ +Argument channel for message_added_generator must have a GraphQLObjectFieldArg as + its info. +... diff --git a/tests_next/snapshots/test_source_args_not_dict_without_schema.obtained.yml b/tests_next/snapshots/test_source_args_not_dict_without_schema.obtained.yml new file mode 100644 index 0000000..d390e7e --- /dev/null +++ b/tests_next/snapshots/test_source_args_not_dict_without_schema.obtained.yml @@ -0,0 +1,2 @@ +The args for message_added_generator must be a dictionary if provided. +... diff --git a/tests_next/snapshots/test_source_args_not_dict_without_schema.yml b/tests_next/snapshots/test_source_args_not_dict_without_schema.yml new file mode 100644 index 0000000..d390e7e --- /dev/null +++ b/tests_next/snapshots/test_source_args_not_dict_without_schema.yml @@ -0,0 +1,2 @@ +The args for message_added_generator must be a dictionary if provided. +... diff --git a/tests_next/snapshots/test_source_for_undefined_field_with_schema.obtained.yml b/tests_next/snapshots/test_source_for_undefined_field_with_schema.obtained.yml new file mode 100644 index 0000000..d520895 --- /dev/null +++ b/tests_next/snapshots/test_source_for_undefined_field_with_schema.obtained.yml @@ -0,0 +1,2 @@ +'Class ''SubscriptionType'' defines source for an undefined field ''message_added''. + (Valid fields: ''messageAdded'')' diff --git a/tests_next/snapshots/test_source_for_undefined_field_with_schema.yml b/tests_next/snapshots/test_source_for_undefined_field_with_schema.yml new file mode 100644 index 0000000..d520895 --- /dev/null +++ b/tests_next/snapshots/test_source_for_undefined_field_with_schema.yml @@ -0,0 +1,2 @@ +'Class ''SubscriptionType'' defines source for an undefined field ''message_added''. + (Valid fields: ''messageAdded'')' diff --git a/tests_next/snapshots/test_undefined_name_without_schema.obtained.yml b/tests_next/snapshots/test_undefined_name_without_schema.obtained.yml new file mode 100644 index 0000000..c82b237 --- /dev/null +++ b/tests_next/snapshots/test_undefined_name_without_schema.obtained.yml @@ -0,0 +1,2 @@ +'Class ''SubscriptionType'' defines source for an undefined field ''messageAdded''. + (Valid fields: ''message_added'')' diff --git a/tests_next/snapshots/test_undefined_name_without_schema.yml b/tests_next/snapshots/test_undefined_name_without_schema.yml new file mode 100644 index 0000000..c82b237 --- /dev/null +++ b/tests_next/snapshots/test_undefined_name_without_schema.yml @@ -0,0 +1,2 @@ +'Class ''SubscriptionType'' defines source for an undefined field ''messageAdded''. + (Valid fields: ''message_added'')' diff --git a/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml b/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml new file mode 100644 index 0000000..393636c --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml @@ -0,0 +1 @@ +'''members_include'' and ''members_exclude'' options are mutually exclusive.' diff --git a/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml b/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml new file mode 100644 index 0000000..393636c --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml @@ -0,0 +1 @@ +'''members_include'' and ''members_exclude'' options are mutually exclusive.' diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml new file mode 100644 index 0000000..87f633f --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml @@ -0,0 +1,3 @@ +Member description was specified for a member 'ADMINISTRATOR' not present in final + GraphQL enum. +... diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml new file mode 100644 index 0000000..87f633f --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml @@ -0,0 +1,3 @@ +Member description was specified for a member 'ADMINISTRATOR' not present in final + GraphQL enum. +... diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml new file mode 100644 index 0000000..06e660c --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml @@ -0,0 +1,3 @@ +Member description was specified for a member 'MISSING' not present in final GraphQL + enum. +... diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml new file mode 100644 index 0000000..06e660c --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml @@ -0,0 +1,3 @@ +Member description was specified for a member 'MISSING' not present in final GraphQL + enum. +... diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml new file mode 100644 index 0000000..87f633f --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml @@ -0,0 +1,3 @@ +Member description was specified for a member 'ADMINISTRATOR' not present in final + GraphQL enum. +... diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml new file mode 100644 index 0000000..87f633f --- /dev/null +++ b/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml @@ -0,0 +1,3 @@ +Member description was specified for a member 'ADMINISTRATOR' not present in final + GraphQL enum. +... diff --git a/tests_next/test_deferred_type.py b/tests_next/test_deferred_type.py index 874aa99..964e1a3 100644 --- a/tests_next/test_deferred_type.py +++ b/tests_next/test_deferred_type.py @@ -30,7 +30,7 @@ class MockType: assert MockType.deferred_type.path == "lorem.types" -def test_deferred_raises_error_for_invalid_relative_path(monkeypatch, snapshot): +def test_deferred_raises_error_for_invalid_relative_path(monkeypatch, data_regression): frame_mock = Mock(f_globals={"__package__": "lorem"}) monkeypatch.setattr( "ariadne_graphql_modules.next.deferredtype.sys._getframe", @@ -42,4 +42,4 @@ def test_deferred_raises_error_for_invalid_relative_path(monkeypatch, snapshot): class MockType: deferred_type = deferred("...types") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_enum_type_validation.py b/tests_next/test_enum_type_validation.py index 2bac52f..b1c3949 100644 --- a/tests_next/test_enum_type_validation.py +++ b/tests_next/test_enum_type_validation.py @@ -6,17 +6,17 @@ from ariadne_graphql_modules.next import GraphQLEnum -def test_schema_enum_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_enum_type_validation_fails_for_invalid_type_schema(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): __schema__ = gql("scalar Custom") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_enum_type_validation_fails_for_names_not_matching( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -31,19 +31,19 @@ class UserLevel(GraphQLEnum): """ ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_enum_type_validation_fails_for_empty_enum(snapshot): +def test_schema_enum_type_validation_fails_for_empty_enum(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): __schema__ = gql("enum UserLevel") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_enum_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_enum_type_validation_fails_for_two_descriptions(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -58,10 +58,10 @@ class UserLevel(GraphQLEnum): """ ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_enum_type_validation_fails_for_schema_and_members_list(snapshot): +def test_schema_enum_type_validation_fails_for_schema_and_members_list(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -75,11 +75,11 @@ class UserLevel(GraphQLEnum): ) __members__ = ["GUEST", "MEMBER"] - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -97,11 +97,11 @@ class UserLevel(GraphQLEnum): "MODERATOR": 1, } - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -122,11 +122,11 @@ class UserLevel(GraphQLEnum): ) __members__ = UserLevelEnum - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_enum_type_validation_fails_for_duplicated_members_descriptions( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -142,10 +142,10 @@ class UserLevel(GraphQLEnum): ) __members_descriptions__ = {"MEMBER": "Other description."} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_enum_type_validation_fails_for_invalid_members_descriptions(snapshot): +def test_schema_enum_type_validation_fails_for_invalid_members_descriptions(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): @@ -159,22 +159,22 @@ class UserLevel(GraphQLEnum): ) __members_descriptions__ = {"INVALID": "Other description."} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_enum_type_validation_fails_for_missing_members(snapshot): +def test_enum_type_validation_fails_for_missing_members(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): pass - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_enum_type_validation_fails_for_invalid_members(snapshot): +def test_enum_type_validation_fails_for_invalid_members(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): __members__ = "INVALID" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py index 19ad1c6..977dfcb 100644 --- a/tests_next/test_input_type.py +++ b/tests_next/test_input_type.py @@ -61,7 +61,7 @@ class SearchInput(GraphQLInput): assert obj.age == 20 -def test_input_type_instance_with_invalid_attrs_raising_error(snapshot): +def test_input_type_instance_with_invalid_attrs_raising_error(data_regression): class SearchInput(GraphQLInput): query: str age: int @@ -69,7 +69,7 @@ class SearchInput(GraphQLInput): with pytest.raises(TypeError) as exc_info: SearchInput(age=20, invalid="Ok") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_input_type_instance_with_all_attrs_values(): @@ -167,7 +167,7 @@ class SearchInput(GraphQLInput): assert obj.age == 20 -def test_schema_input_type_instance_with_invalid_attrs_raising_error(snapshot): +def test_schema_input_type_instance_with_invalid_attrs_raising_error(data_regression): class SearchInput(GraphQLInput): __schema__ = gql( """ @@ -184,7 +184,7 @@ class SearchInput(GraphQLInput): with pytest.raises(TypeError) as exc_info: SearchInput(age=20, invalid="Ok") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_input_type_arg(assert_schema_equals): diff --git a/tests_next/test_input_type_validation.py b/tests_next/test_input_type_validation.py index d8efe64..f1e6273 100644 --- a/tests_next/test_input_type_validation.py +++ b/tests_next/test_input_type_validation.py @@ -4,16 +4,16 @@ from ariadne_graphql_modules.next import GraphQLInput -def test_schema_input_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_input_type_validation_fails_for_invalid_type_schema(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): __schema__ = gql("scalar Custom") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_input_type_validation_fails_for_names_not_matching(snapshot): +def test_schema_input_type_validation_fails_for_names_not_matching(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -26,10 +26,10 @@ class CustomType(GraphQLInput): """ ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_input_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_input_type_validation_fails_for_two_descriptions(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -43,19 +43,19 @@ class CustomType(GraphQLInput): """ ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_input_type_validation_fails_for_schema_missing_fields(snapshot): +def test_schema_input_type_validation_fails_for_schema_missing_fields(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): __schema__ = gql("input Custom") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_input_type_validation_fails_for_out_names_without_schema(snapshot): +def test_input_type_validation_fails_for_out_names_without_schema(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -65,10 +65,10 @@ class CustomType(GraphQLInput): "hello": "ok", } - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_input_type_validation_fails_for_invalid_out_name(snapshot): +def test_schema_input_type_validation_fails_for_invalid_out_name(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -84,10 +84,10 @@ class CustomType(GraphQLInput): "invalid": "ok", } - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_input_type_validation_fails_for_duplicate_out_name(snapshot): +def test_schema_input_type_validation_fails_for_duplicate_out_name(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLInput): @@ -105,28 +105,28 @@ class CustomType(GraphQLInput): "name": "ok", } - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) class InvalidType: pass -def test_input_type_validation_fails_for_unsupported_attr_default(snapshot): +def test_input_type_validation_fails_for_unsupported_attr_default(data_regression): with pytest.raises(TypeError) as exc_info: class QueryType(GraphQLInput): attr: str = InvalidType() - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_input_type_validation_fails_for_unsupported_field_default_option( - snapshot, + data_regression, ): with pytest.raises(TypeError) as exc_info: class QueryType(GraphQLInput): attr: str = GraphQLInput.field(default_value=InvalidType()) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py index 760df46..69c6bd9 100644 --- a/tests_next/test_interface_type.py +++ b/tests_next/test_interface_type.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from graphql import graphql_sync @@ -33,7 +33,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[UserType | CommentType]: + def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), CommentType(id=2, content="Hello World!"), @@ -100,7 +100,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[UserType | CommentType]: + def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(), CommentType(id=2, content="Hello World!"), @@ -174,7 +174,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[UserType | CommentType]: + def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), CommentType(id=2, content="Hello World!"), diff --git a/tests_next/test_interface_type_validation.py b/tests_next/test_interface_type_validation.py index d7e194f..0c96f8e 100644 --- a/tests_next/test_interface_type_validation.py +++ b/tests_next/test_interface_type_validation.py @@ -8,7 +8,7 @@ ) -def test_interface_with_different_types(snapshot): +def test_interface_with_different_types(data_regression): with pytest.raises(TypeError) as exc_info: class UserInterface(GraphQLInterface): @@ -24,10 +24,10 @@ class UserType(GraphQLObject): make_executable_schema(UserType, UserInterface) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_interface_no_interface_in_schema(snapshot): +def test_interface_no_interface_in_schema(data_regression): with pytest.raises(TypeError) as exc_info: class BaseInterface(GraphQLInterface): @@ -42,4 +42,4 @@ class UserType(GraphQLObject): make_executable_schema(UserType) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_make_executable_schema.py b/tests_next/test_make_executable_schema.py index 6b4479e..2559c7a 100644 --- a/tests_next/test_make_executable_schema.py +++ b/tests_next/test_make_executable_schema.py @@ -152,7 +152,7 @@ class SecondRoot(GraphQLObject): ) -def test_multiple_roots_fail_validation_if_merge_roots_is_disabled(snapshot): +def test_multiple_roots_fail_validation_if_merge_roots_is_disabled(data_regression): class FirstRoot(GraphQLObject): __graphql_name__ = "Query" @@ -172,10 +172,10 @@ class ThirdRoot(GraphQLObject): with pytest.raises(ValueError) as exc_info: make_executable_schema(FirstRoot, SecondRoot, ThirdRoot, merge_roots=False) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_validation_fails_if_lazy_type_doesnt_exist(snapshot): +def test_schema_validation_fails_if_lazy_type_doesnt_exist(data_regression): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=list["Missing"]) def other(obj, info): @@ -184,7 +184,7 @@ def other(obj, info): with pytest.raises(TypeError) as exc_info: make_executable_schema(QueryType) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_validation_passes_if_lazy_type_exists(): @@ -202,18 +202,18 @@ def other(obj, info): make_executable_schema(QueryType, type_def) -def test_make_executable_schema_raises_error_if_called_without_any_types(snapshot): +def test_make_executable_schema_raises_error_if_called_without_any_types(data_regression): with pytest.raises(ValueError) as exc_info: make_executable_schema(QueryType) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_make_executable_schema_raises_error_if_called_without_any_types(snapshot): +def test_make_executable_schema_raises_error_if_called_without_any_types(data_regression): with pytest.raises(ValueError) as exc_info: make_executable_schema(QueryType) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_make_executable_schema_doesnt_set_resolvers_if_convert_names_case_is_not_enabled(): diff --git a/tests_next/test_metadata.py b/tests_next/test_metadata.py index b451ec6..d02f5f6 100644 --- a/tests_next/test_metadata.py +++ b/tests_next/test_metadata.py @@ -14,11 +14,11 @@ def test_metadata_returns_sets_and_returns_data_for_type(metadata): assert metadata.get_data(QueryType) == 42 -def test_metadata_raises_key_error_for_unset_data(snapshot, metadata): +def test_metadata_raises_key_error_for_unset_data(data_regression, metadata): with pytest.raises(KeyError) as exc_info: metadata.get_data(QueryType) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_metadata_returns_model_for_type(assert_ast_equals, metadata): diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py index 95b66c7..6655bec 100644 --- a/tests_next/test_object_type.py +++ b/tests_next/test_object_type.py @@ -61,7 +61,7 @@ class CategoryType(GraphQLObject): assert obj.posts == 42 -def test_object_type_instance_with_invalid_attrs_raising_error(snapshot): +def test_object_type_instance_with_invalid_attrs_raising_error(data_regression): class CategoryType(GraphQLObject): name: str posts: int @@ -69,7 +69,7 @@ class CategoryType(GraphQLObject): with pytest.raises(TypeError) as exc_info: CategoryType(name="Welcome", invalid="Ok") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_instance_with_all_attrs_values(): @@ -188,7 +188,7 @@ class CategoryType(GraphQLObject): assert obj.posts == 42 -def test_schema_object_type_instance_with_invalid_attrs_raising_error(snapshot): +def test_schema_object_type_instance_with_invalid_attrs_raising_error(data_regression): class CategoryType(GraphQLObject): __schema__ = gql( """ @@ -205,7 +205,7 @@ class CategoryType(GraphQLObject): with pytest.raises(TypeError) as exc_info: CategoryType(name="Welcome", invalid="Ok") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_instance_with_aliased_attr_value(): diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index 3ddbdc6..308d6d0 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -4,16 +4,16 @@ from ariadne_graphql_modules.next import GraphQLObject -def test_schema_object_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_object_type_validation_fails_for_invalid_type_schema(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): __schema__ = gql("scalar Custom") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_names_not_matching(snapshot): +def test_schema_object_type_validation_fails_for_names_not_matching(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -26,10 +26,10 @@ class CustomType(GraphQLObject): """ ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_object_type_validation_fails_for_two_descriptions(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -43,19 +43,19 @@ class CustomType(GraphQLObject): """ ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_missing_fields(snapshot): +def test_schema_object_type_validation_fails_for_missing_fields(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): __schema__ = gql("type Custom") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_undefined_attr_resolver(snapshot): +def test_object_type_validation_fails_for_undefined_attr_resolver(data_regression): with pytest.raises(ValueError) as exc_info: class QueryType(GraphQLObject): @@ -65,10 +65,10 @@ class QueryType(GraphQLObject): def resolve_hello(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_undefined_field_resolver(snapshot): +def test_schema_object_type_validation_fails_for_undefined_field_resolver(data_regression): with pytest.raises(ValueError) as exc_info: class QueryType(GraphQLObject): @@ -84,11 +84,11 @@ class QueryType(GraphQLObject): def resolve_hello(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -96,11 +96,11 @@ class CustomType(GraphQLObject): user_id: str user__id: str - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -111,11 +111,11 @@ class CustomType(GraphQLObject): def lorem(*_) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -128,10 +128,10 @@ def lorem(*_) -> str: def ipsum(*_) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_undefined_field_resolver_arg(snapshot): +def test_object_type_validation_fails_for_undefined_field_resolver_arg(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -139,10 +139,10 @@ class CustomType(GraphQLObject): def hello(*_) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_undefined_resolver_arg(snapshot): +def test_object_type_validation_fails_for_undefined_resolver_arg(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -154,10 +154,10 @@ class CustomType(GraphQLObject): def resolve_hello(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_missing_field_resolver_arg(snapshot): +def test_object_type_validation_fails_for_missing_field_resolver_arg(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -165,10 +165,10 @@ class CustomType(GraphQLObject): def hello(*_, name: str) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_missing_resolver_arg(snapshot): +def test_object_type_validation_fails_for_missing_resolver_arg(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -180,10 +180,10 @@ class CustomType(GraphQLObject): def resolve_hello(*_, name: str): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_multiple_field_resolvers(snapshot): +def test_object_type_validation_fails_for_multiple_field_resolvers(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -197,10 +197,10 @@ def resolve_hello(*_): def resolve_hello_other(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_multiple_field_resolvers(snapshot): +def test_schema_object_type_validation_fails_for_multiple_field_resolvers(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -220,11 +220,11 @@ def resolve_hello(*_): def resolve_hello_other(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_object_type_validation_fails_for_field_with_multiple_descriptions( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -235,11 +235,11 @@ class CustomType(GraphQLObject): def ipsum(*_) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_validation_fails_for_field_with_multiple_descriptions( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -255,11 +255,11 @@ class CustomType(GraphQLObject): def ipsum(*_) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_validation_fails_for_field_with_invalid_arg_name( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -276,11 +276,11 @@ class CustomType(GraphQLObject): def ipsum(*_, name: str) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_validation_fails_for_arg_with_name_option( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -297,11 +297,11 @@ class CustomType(GraphQLObject): def ipsum(*_, name: str) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_validation_fails_for_arg_with_type_option( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -318,11 +318,11 @@ class CustomType(GraphQLObject): def ipsum(*_, name: str) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_validation_fails_for_arg_with_double_description( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -342,11 +342,11 @@ class CustomType(GraphQLObject): def ipsum(*_, name: str) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_object_type_validation_fails_for_field_with_multiple_args( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -359,10 +359,10 @@ def lorem(*_, a: int) -> str: def ipsum(*_) -> str: return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_invalid_alias(snapshot): +def test_object_type_validation_fails_for_invalid_alias(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -372,10 +372,10 @@ class CustomType(GraphQLObject): "invalid": "target", } - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_invalid_alias(snapshot): +def test_schema_object_type_validation_fails_for_invalid_alias(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -391,10 +391,10 @@ class CustomType(GraphQLObject): "invalid": "welcome", } - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_alias_resolver(snapshot): +def test_object_type_validation_fails_for_alias_resolver(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -408,10 +408,10 @@ class CustomType(GraphQLObject): def resolve_hello(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_object_type_validation_fails_for_alias_target_resolver(snapshot): +def test_object_type_validation_fails_for_alias_target_resolver(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -426,10 +426,10 @@ class CustomType(GraphQLObject): def resolve_welcome(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_alias_resolver(snapshot): +def test_schema_object_type_validation_fails_for_alias_resolver(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -449,10 +449,10 @@ class CustomType(GraphQLObject): def resolve_hello(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_alias_target_resolver(snapshot): +def test_schema_object_type_validation_fails_for_alias_target_resolver(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -474,10 +474,10 @@ class CustomType(GraphQLObject): def resolve_hello(*_): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_field_instance(snapshot): +def test_schema_object_type_validation_fails_for_field_instance(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -491,14 +491,14 @@ class CustomType(GraphQLObject): hello = GraphQLObject.field(lambda *_: "noop") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) class InvalidType: pass -def test_object_type_validation_fails_for_unsupported_resolver_arg_default(snapshot): +def test_object_type_validation_fails_for_unsupported_resolver_arg_default(data_regression): with pytest.raises(TypeError) as exc_info: class QueryType(GraphQLObject): @@ -506,11 +506,11 @@ class QueryType(GraphQLObject): def hello(*_, name: str = InvalidType): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_object_type_validation_fails_for_unsupported_resolver_arg_default_option( - snapshot, + data_regression, ): with pytest.raises(TypeError) as exc_info: @@ -521,11 +521,11 @@ class QueryType(GraphQLObject): def hello(*_, name: str): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default( - snapshot, + data_regression, ): with pytest.raises(TypeError) as exc_info: @@ -542,11 +542,11 @@ class QueryType(GraphQLObject): def resolve_hello(*_, name: str = InvalidType): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default( - snapshot, + data_regression, ): with pytest.raises(TypeError) as exc_info: @@ -566,4 +566,4 @@ class QueryType(GraphQLObject): def resolve_hello(*_, name: str): return "Hello World!" - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_scalar_type_validation.py b/tests_next/test_scalar_type_validation.py index 35041dd..d473cb7 100644 --- a/tests_next/test_scalar_type_validation.py +++ b/tests_next/test_scalar_type_validation.py @@ -4,17 +4,17 @@ from ariadne_graphql_modules.next import GraphQLScalar -def test_schema_scalar_type_validation_fails_for_invalid_type_schema(snapshot): +def test_schema_scalar_type_validation_fails_for_invalid_type_schema(data_regression): with pytest.raises(ValueError) as exc_info: class CustomScalar(GraphQLScalar[str]): __schema__ = gql("type Custom") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_schema_scalar_type_validation_fails_for_different_names( - snapshot, + data_regression, ): with pytest.raises(ValueError) as exc_info: @@ -22,10 +22,10 @@ class CustomScalar(GraphQLScalar[str]): __graphql_name__ = "Date" __schema__ = gql("scalar Custom") - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_schema_scalar_type_validation_fails_for_two_descriptions(snapshot): +def test_schema_scalar_type_validation_fails_for_two_descriptions(data_regression): with pytest.raises(ValueError) as exc_info: class CustomScalar(GraphQLScalar[str]): @@ -37,4 +37,4 @@ class CustomScalar(GraphQLScalar[str]): """ ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_standard_enum.py b/tests_next/test_standard_enum.py index 0e539a2..fa0f895 100644 --- a/tests_next/test_standard_enum.py +++ b/tests_next/test_standard_enum.py @@ -180,7 +180,7 @@ def test_graphql_enum_model_is_created_with_members_descriptions(assert_ast_equa ) -def test_value_error_is_raised_if_exclude_and_include_members_are_combined(snapshot): +def test_value_error_is_raised_if_exclude_and_include_members_are_combined(data_regression): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model( UserLevel, @@ -188,17 +188,17 @@ def test_value_error_is_raised_if_exclude_and_include_members_are_combined(snaps members_include=["ADMINISTRATOR"], ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_value_error_is_raised_if_member_description_is_set_for_missing_item(snapshot): +def test_value_error_is_raised_if_member_description_is_set_for_missing_item(data_regression): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model(UserLevel, members_descriptions={"MISSING": "Hello!"}) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_value_error_is_raised_if_member_description_is_set_for_omitted_item(snapshot): +def test_value_error_is_raised_if_member_description_is_set_for_omitted_item(data_regression): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model( UserLevel, @@ -206,10 +206,10 @@ def test_value_error_is_raised_if_member_description_is_set_for_omitted_item(sna members_descriptions={"ADMINISTRATOR": "Hello!"}, ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_value_error_is_raised_if_member_description_is_set_for_excluded_item(snapshot): +def test_value_error_is_raised_if_member_description_is_set_for_excluded_item(data_regression): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model( UserLevel, @@ -217,7 +217,7 @@ def test_value_error_is_raised_if_member_description_is_set_for_excluded_item(sn members_descriptions={"ADMINISTRATOR": "Hello!"}, ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_enum_field_returning_enum_instance(assert_schema_equals): diff --git a/tests_next/test_subscription_type_validation.py b/tests_next/test_subscription_type_validation.py index e772d2e..4b59cc7 100644 --- a/tests_next/test_subscription_type_validation.py +++ b/tests_next/test_subscription_type_validation.py @@ -24,7 +24,7 @@ class Notification(GraphQLUnion): @pytest.mark.asyncio -async def test_undefined_name_without_schema(snapshot): +async def test_undefined_name_without_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -35,11 +35,11 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_field_name_not_str_without_schema(snapshot): +async def test_field_name_not_str_without_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -50,11 +50,11 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_multiple_sources_without_schema(snapshot): +async def test_multiple_sources_without_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -70,11 +70,11 @@ async def message_added_generator_2(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_description_not_str_without_schema(snapshot): +async def test_description_not_str_without_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -85,11 +85,11 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_source_args_field_arg_not_dict_without_schema(snapshot): +async def test_source_args_field_arg_not_dict_without_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -103,11 +103,11 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_source_args_not_dict_without_schema(snapshot): +async def test_source_args_not_dict_without_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -121,11 +121,11 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_source_for_undefined_field_with_schema(snapshot): +async def test_source_for_undefined_field_with_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -142,11 +142,11 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_multiple_sourced_for_field_with_schema(snapshot): +async def test_multiple_sourced_for_field_with_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -168,11 +168,11 @@ async def message_added_generator_2(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_multiple_descriptions_for_source_with_schema(snapshot): +async def test_multiple_descriptions_for_source_with_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -190,11 +190,11 @@ async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_invalid_arg_name_in_source_with_schema(snapshot): +async def test_invalid_arg_name_in_source_with_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -214,11 +214,11 @@ async def message_added_generator(obj, info, channel): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_arg_with_name_in_source_with_schema(snapshot): +async def test_arg_with_name_in_source_with_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -242,11 +242,11 @@ async def message_added_generator(obj, info, channel): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_arg_with_type_in_source_with_schema(snapshot): +async def test_arg_with_type_in_source_with_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -270,11 +270,11 @@ async def message_added_generator(obj, info, channel): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) @pytest.mark.asyncio -async def test_arg_with_description_in_source_with_schema(snapshot): +async def test_arg_with_description_in_source_with_schema(data_regression): with pytest.raises(ValueError) as exc_info: class SubscriptionType(GraphQLSubscription): @@ -301,4 +301,4 @@ async def message_added_generator(obj, info, channel): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_typing.py b/tests_next/test_typing.py index b93c11a..9e4e3fb 100644 --- a/tests_next/test_typing.py +++ b/tests_next/test_typing.py @@ -1,7 +1,9 @@ from enum import Enum +import sys from typing import TYPE_CHECKING, Annotated, List, Optional, Union from graphql import ListTypeNode, NameNode, NamedTypeNode, NonNullTypeNode +import pytest from ariadne_graphql_modules.next import GraphQLObject, deferred, graphql_enum from ariadne_graphql_modules.next.typing import get_graphql_type, get_type_node @@ -37,10 +39,17 @@ def assert_named_type(type_node, name: str): def test_get_graphql_type_from_python_builtin_type_returns_none(metadata): assert get_graphql_type(Optional[str]) is None assert get_graphql_type(Union[int, None]) is None - assert get_graphql_type(float | None) is None assert get_graphql_type(Optional[bool]) is None +@pytest.mark.skipif( + sys.version_info >= (3, 9) and sys.version_info < (3, 10), + reason="Skip test for Python 3.9", +) +def test_get_graphql_type_from_python_builtin_type_returns_none_pipe_union(metadata): + assert get_graphql_type(float | None) is None + + def test_get_graphql_type_from_graphql_type_subclass_returns_type(metadata): class UserType(GraphQLObject): ... @@ -66,10 +75,17 @@ class UserLevel(Enum): def test_get_graphql_type_node_from_python_builtin_type(metadata): assert_named_type(get_type_node(metadata, Optional[str]), "String") assert_named_type(get_type_node(metadata, Union[int, None]), "Int") - assert_named_type(get_type_node(metadata, float | None), "Float") assert_named_type(get_type_node(metadata, Optional[bool]), "Boolean") +@pytest.mark.skipif( + sys.version_info >= (3, 9) and sys.version_info < (3, 10), + reason="Skip test for Python 3.9", +) +def test_get_graphql_type_node_from_python_builtin_type_pipe_union(metadata): + assert_named_type(get_type_node(metadata, float | None), "Float") + + def test_get_non_null_graphql_type_node_from_python_builtin_type(metadata): assert_non_null_type(get_type_node(metadata, str), "String") assert_non_null_type(get_type_node(metadata, int), "Int") @@ -87,10 +103,18 @@ class UserType(GraphQLObject): ... def test_get_graphql_list_type_node_from_python_builtin_type(metadata): assert_list_type(get_type_node(metadata, Optional[List[str]]), "String") assert_list_type(get_type_node(metadata, Union[List[int], None]), "Int") - assert_list_type(get_type_node(metadata, List[float] | None), "Float") + assert_list_type(get_type_node(metadata, Optional[List[bool]]), "Boolean") +@pytest.mark.skipif( + sys.version_info >= (3, 9) and sys.version_info < (3, 10), + reason="Skip test for Python 3.9", +) +def test_get_graphql_list_type_node_from_python_builtin_type_pipe_union(metadata): + assert_list_type(get_type_node(metadata, List[float] | None), "Float") + + def test_get_non_null_graphql_list_type_node_from_python_builtin_type(metadata): assert_non_null_list_type(get_type_node(metadata, List[str]), "String") assert_non_null_list_type(get_type_node(metadata, List[int]), "Int") diff --git a/tests_next/test_union_type.py b/tests_next/test_union_type.py index cbd722e..6d20267 100644 --- a/tests_next/test_union_type.py +++ b/tests_next/test_union_type.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from graphql import graphql_sync @@ -26,7 +26,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[UserType | CommentType]: + def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), CommentType(id=2, content="Hello World!"), @@ -88,7 +88,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[UserType | CommentType]: + def search(*_) -> List[Union[UserType, CommentType]]: return [] schema = make_executable_schema(QueryType) @@ -120,7 +120,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[UserType | CommentType]: + def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), "InvalidType", @@ -159,7 +159,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[UserType | CommentType | InvalidType]: + def search(*_) -> List[Union[UserType, CommentType, InvalidType]]: return [InvalidType("This should cause an error")] schema = make_executable_schema(QueryType) @@ -189,13 +189,13 @@ class SearchResultUnion(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[SearchResultUnion]) - def search(*_) -> List[UserType | CommentType]: + def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id="1", username="Alice"), CommentType(id="2", content="Test post"), ] - schema = make_executable_schema([QueryType, SearchResultUnion]) + schema = make_executable_schema(QueryType, SearchResultUnion) result = graphql_sync( schema, diff --git a/tests_next/test_union_type_validation.py b/tests_next/test_union_type_validation.py index 29415a7..f9930d4 100644 --- a/tests_next/test_union_type_validation.py +++ b/tests_next/test_union_type_validation.py @@ -24,7 +24,7 @@ class PostType(GraphQLObject): content: str -def test_missing_type_in_schema(snapshot): +def test_missing_type_in_schema(data_regression): with pytest.raises(ValueError) as exc_info: class MyUnion(GraphQLUnion): @@ -34,10 +34,10 @@ class MyUnion(GraphQLUnion): """ validate_union_type_with_schema(MyUnion) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) -def test_missing_type_in_types(snapshot): +def test_missing_type_in_types(data_regression): with pytest.raises(ValueError) as exc_info: class MyUnion(GraphQLUnion): @@ -47,7 +47,7 @@ class MyUnion(GraphQLUnion): """ validate_union_type_with_schema(MyUnion) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_all_types_present(): diff --git a/tests_next/test_validators.py b/tests_next/test_validators.py index c39ce69..45f4948 100644 --- a/tests_next/test_validators.py +++ b/tests_next/test_validators.py @@ -19,7 +19,9 @@ class CustomType: validate_description(CustomType, parse("scalar Custom").definitions[0]) -def test_description_validator_raises_error_for_type_with_two_descriptions(snapshot): +def test_description_validator_raises_error_for_type_with_two_descriptions( + data_regression, +): with pytest.raises(ValueError) as exc_info: class CustomType: @@ -35,7 +37,7 @@ class CustomType: ).definitions[0], ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) def test_name_validator_passes_type_without_explicit_name(): @@ -52,7 +54,7 @@ class CustomType: validate_name(CustomType, parse("type Custom").definitions[0]) -def test_name_validator_raises_error_for_name_and_definition_mismatch(snapshot): +def test_name_validator_raises_error_for_name_and_definition_mismatch(data_regression): with pytest.raises(ValueError) as exc_info: class CustomType: @@ -63,4 +65,4 @@ class CustomType: parse("type Custom").definitions[0], ) - snapshot.assert_match(str(exc_info.value)) + data_regression.check(str(exc_info.value)) From f4767f6fda517b2cb10e56940258b0b4f903b72a Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 19 Aug 2024 13:00:15 +0200 Subject: [PATCH 43/63] black reformat --- .github/workflows/tests.yml | 2 +- tests_next/test_enum_type_validation.py | 4 +++- tests_next/test_make_executable_schema.py | 8 ++++++-- tests_next/test_object_type_validation.py | 12 +++++++++--- tests_next/test_standard_enum.py | 16 ++++++++++++---- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b5d3f4..c421c2b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 diff --git a/tests_next/test_enum_type_validation.py b/tests_next/test_enum_type_validation.py index b1c3949..29e983a 100644 --- a/tests_next/test_enum_type_validation.py +++ b/tests_next/test_enum_type_validation.py @@ -145,7 +145,9 @@ class UserLevel(GraphQLEnum): data_regression.check(str(exc_info.value)) -def test_schema_enum_type_validation_fails_for_invalid_members_descriptions(data_regression): +def test_schema_enum_type_validation_fails_for_invalid_members_descriptions( + data_regression, +): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): diff --git a/tests_next/test_make_executable_schema.py b/tests_next/test_make_executable_schema.py index 2559c7a..b927649 100644 --- a/tests_next/test_make_executable_schema.py +++ b/tests_next/test_make_executable_schema.py @@ -202,14 +202,18 @@ def other(obj, info): make_executable_schema(QueryType, type_def) -def test_make_executable_schema_raises_error_if_called_without_any_types(data_regression): +def test_make_executable_schema_raises_error_if_called_without_any_types( + data_regression, +): with pytest.raises(ValueError) as exc_info: make_executable_schema(QueryType) data_regression.check(str(exc_info.value)) -def test_make_executable_schema_raises_error_if_called_without_any_types(data_regression): +def test_make_executable_schema_raises_error_if_called_without_any_types( + data_regression, +): with pytest.raises(ValueError) as exc_info: make_executable_schema(QueryType) diff --git a/tests_next/test_object_type_validation.py b/tests_next/test_object_type_validation.py index 308d6d0..2fcf813 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests_next/test_object_type_validation.py @@ -68,7 +68,9 @@ def resolve_hello(*_): data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_undefined_field_resolver(data_regression): +def test_schema_object_type_validation_fails_for_undefined_field_resolver( + data_regression, +): with pytest.raises(ValueError) as exc_info: class QueryType(GraphQLObject): @@ -200,7 +202,9 @@ def resolve_hello_other(*_): data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_multiple_field_resolvers(data_regression): +def test_schema_object_type_validation_fails_for_multiple_field_resolvers( + data_regression, +): with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): @@ -498,7 +502,9 @@ class InvalidType: pass -def test_object_type_validation_fails_for_unsupported_resolver_arg_default(data_regression): +def test_object_type_validation_fails_for_unsupported_resolver_arg_default( + data_regression, +): with pytest.raises(TypeError) as exc_info: class QueryType(GraphQLObject): diff --git a/tests_next/test_standard_enum.py b/tests_next/test_standard_enum.py index fa0f895..9f8eded 100644 --- a/tests_next/test_standard_enum.py +++ b/tests_next/test_standard_enum.py @@ -180,7 +180,9 @@ def test_graphql_enum_model_is_created_with_members_descriptions(assert_ast_equa ) -def test_value_error_is_raised_if_exclude_and_include_members_are_combined(data_regression): +def test_value_error_is_raised_if_exclude_and_include_members_are_combined( + data_regression, +): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model( UserLevel, @@ -191,14 +193,18 @@ def test_value_error_is_raised_if_exclude_and_include_members_are_combined(data_ data_regression.check(str(exc_info.value)) -def test_value_error_is_raised_if_member_description_is_set_for_missing_item(data_regression): +def test_value_error_is_raised_if_member_description_is_set_for_missing_item( + data_regression, +): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model(UserLevel, members_descriptions={"MISSING": "Hello!"}) data_regression.check(str(exc_info.value)) -def test_value_error_is_raised_if_member_description_is_set_for_omitted_item(data_regression): +def test_value_error_is_raised_if_member_description_is_set_for_omitted_item( + data_regression, +): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model( UserLevel, @@ -209,7 +215,9 @@ def test_value_error_is_raised_if_member_description_is_set_for_omitted_item(dat data_regression.check(str(exc_info.value)) -def test_value_error_is_raised_if_member_description_is_set_for_excluded_item(data_regression): +def test_value_error_is_raised_if_member_description_is_set_for_excluded_item( + data_regression, +): with pytest.raises(ValueError) as exc_info: create_graphql_enum_model( UserLevel, From 05cd2d99c6d87d4337596e8cf1e3f5ca46826d44 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 8 Aug 2024 12:48:28 +0200 Subject: [PATCH 44/63] add-compatibility-layer --- ariadne_graphql_modules/bases.py | 8 +- ariadne_graphql_modules/enum_type.py | 12 +- ariadne_graphql_modules/input_type.py | 15 +- ariadne_graphql_modules/interface_type.py | 14 +- .../next/compatibility_layer.py | 166 ++++++ ariadne_graphql_modules/object_type.py | 1 + ariadne_graphql_modules/resolvers_mixin.py | 5 +- ariadne_graphql_modules/scalar_type.py | 10 +- ariadne_graphql_modules/union_type.py | 12 +- tests_next/test_compatibility_layer.py | 480 ++++++++++++++++++ 10 files changed, 694 insertions(+), 29 deletions(-) create mode 100644 ariadne_graphql_modules/next/compatibility_layer.py create mode 100644 tests_next/test_compatibility_layer.py diff --git a/ariadne_graphql_modules/bases.py b/ariadne_graphql_modules/bases.py index c926c0b..22193b1 100644 --- a/ariadne_graphql_modules/bases.py +++ b/ariadne_graphql_modules/bases.py @@ -1,6 +1,11 @@ from typing import List, Type, Union -from graphql import DefinitionNode, GraphQLSchema, ObjectTypeDefinitionNode +from graphql import ( + DefinitionNode, + GraphQLSchema, + ObjectTypeDefinitionNode, + TypeDefinitionNode, +) from .dependencies import Dependencies from .types import RequirementsDict @@ -31,6 +36,7 @@ class DefinitionType(BaseType): graphql_name: str graphql_type: Type[DefinitionNode] + graphql_def: TypeDefinitionNode @classmethod def __get_requirements__(cls) -> RequirementsDict: diff --git a/ariadne_graphql_modules/enum_type.py b/ariadne_graphql_modules/enum_type.py index 9b6b044..c555b33 100644 --- a/ariadne_graphql_modules/enum_type.py +++ b/ariadne_graphql_modules/enum_type.py @@ -30,17 +30,19 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False - graphql_def = cls.__validate_schema__( + cls.graphql_def = cls.__validate_schema__( parse_definition(cls.__name__, cls.__schema__) ) - cls.graphql_name = graphql_def.name.value - cls.graphql_type = type(graphql_def) + cls.graphql_name = cls.graphql_def.name.value + cls.graphql_type = type(cls.graphql_def) requirements = cls.__get_requirements__() - cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) + cls.__validate_requirements_contain_extended_type__( + cls.graphql_def, requirements + ) - values = cls.__get_values__(graphql_def) + values = cls.__get_values__(cls.graphql_def) cls.__validate_values__(values) @classmethod diff --git a/ariadne_graphql_modules/input_type.py b/ariadne_graphql_modules/input_type.py index 282e78b..32295db 100644 --- a/ariadne_graphql_modules/input_type.py +++ b/ariadne_graphql_modules/input_type.py @@ -20,6 +20,7 @@ class InputType(BindableType): __args__: Optional[Union[Args, Callable[..., Args]]] = None graphql_fields: InputFieldsDict + graphql_def: InputNodeType def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -29,13 +30,13 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False - graphql_def = cls.__validate_schema__( + cls.graphql_def = cls.__validate_schema__( parse_definition(cls.__name__, cls.__schema__) ) - cls.graphql_name = graphql_def.name.value - cls.graphql_type = type(graphql_def) - cls.graphql_fields = cls.__get_fields__(graphql_def) + cls.graphql_name = cls.graphql_def.name.value + cls.graphql_type = type(cls.graphql_def) + cls.graphql_fields = cls.__get_fields__(cls.graphql_def) if callable(cls.__args__): # pylint: disable=not-callable @@ -44,9 +45,11 @@ def __init_subclass__(cls) -> None: cls.__validate_args__() requirements = cls.__get_requirements__() - cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) + cls.__validate_requirements_contain_extended_type__( + cls.graphql_def, requirements + ) - dependencies = cls.__get_dependencies__(graphql_def) + dependencies = cls.__get_dependencies__(cls.graphql_def) cls.__validate_requirements__(requirements, dependencies) @classmethod diff --git a/ariadne_graphql_modules/interface_type.py b/ariadne_graphql_modules/interface_type.py index a9b054a..ac34188 100644 --- a/ariadne_graphql_modules/interface_type.py +++ b/ariadne_graphql_modules/interface_type.py @@ -41,18 +41,20 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False - graphql_def = cls.__validate_schema__( + cls.graphql_def = cls.__validate_schema__( parse_definition(cls.__name__, cls.__schema__) ) - cls.graphql_name = graphql_def.name.value - cls.graphql_type = type(graphql_def) - cls.graphql_fields = cls.__get_fields__(graphql_def) + cls.graphql_name = cls.graphql_def.name.value + cls.graphql_type = type(cls.graphql_def) + cls.graphql_fields = cls.__get_fields__(cls.graphql_def) requirements = cls.__get_requirements__() - cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) + cls.__validate_requirements_contain_extended_type__( + cls.graphql_def, requirements + ) - dependencies = cls.__get_dependencies__(graphql_def) + dependencies = cls.__get_dependencies__(cls.graphql_def) cls.__validate_requirements__(requirements, dependencies) if callable(cls.__fields_args__): diff --git a/ariadne_graphql_modules/next/compatibility_layer.py b/ariadne_graphql_modules/next/compatibility_layer.py new file mode 100644 index 0000000..efb4107 --- /dev/null +++ b/ariadne_graphql_modules/next/compatibility_layer.py @@ -0,0 +1,166 @@ +from typing import List, Type + +from graphql import ( + EnumTypeDefinitionNode, + InputObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + NameNode, + ObjectTypeDefinitionNode, + ScalarTypeDefinitionNode, + UnionTypeDefinitionNode, +) + +from ariadne_graphql_modules.executable_schema import get_all_types +from ariadne_graphql_modules.next.inputtype import GraphQLInputModel +from ariadne_graphql_modules.next.interfacetype import ( + GraphQLInterfaceModel, +) +from ariadne_graphql_modules.next.scalartype import GraphQLScalarModel +from ariadne_graphql_modules.next.subscriptiontype import GraphQLSubscriptionModel +from ariadne_graphql_modules.next.uniontype import GraphQLUnionModel + +from ..directive_type import DirectiveType +from ..enum_type import EnumType +from ..input_type import InputType +from ..interface_type import InterfaceType +from ..mutation_type import MutationType +from ..scalar_type import ScalarType +from ..subscription_type import SubscriptionType +from ..union_type import UnionType + +from ..object_type import ObjectType + +from .description import get_description_node +from ..bases import BindableType +from .base import GraphQLModel, GraphQLType +from . import GraphQLObjectModel, GraphQLEnumModel + + +def wrap_legacy_types( + *bindable_types: Type[BindableType], +) -> List[Type["LegacyGraphQLType"]]: + all_types = get_all_types(bindable_types) + + return [ + type(f"Legacy{t.__name__}", (LegacyGraphQLType,), {"__base_type__": t}) + for t in all_types + ] + + +class LegacyGraphQLType(GraphQLType): + __base_type__: Type[BindableType] + __abstract__: bool = False + + @classmethod + def __get_graphql_model__(cls, *_) -> GraphQLModel: + if issubclass(cls.__base_type__, ObjectType): + return cls.construct_object_model(cls.__base_type__) + if issubclass(cls.__base_type__, EnumType): + return cls.construct_enum_model(cls.__base_type__) + if issubclass(cls.__base_type__, InputType): + return cls.construct_input_model(cls.__base_type__) + if issubclass(cls.__base_type__, InterfaceType): + return cls.construct_interface_model(cls.__base_type__) + if issubclass(cls.__base_type__, MutationType): + return cls.construct_object_model(cls.__base_type__) + if issubclass(cls.__base_type__, ScalarType): + return cls.construct_scalar_model(cls.__base_type__) + if issubclass(cls.__base_type__, SubscriptionType): + return cls.construct_subscription_model(cls.__base_type__) + if issubclass(cls.__base_type__, UnionType): + return cls.construct_union_model(cls.__base_type__) + else: + raise ValueError(f"Unsupported base_type {cls.__base_type__}") + + @classmethod + def construct_object_model( + cls, base_type: Type[ObjectType] + ) -> "GraphQLObjectModel": + name = base_type.graphql_name + description = base_type.__doc__ + + return GraphQLObjectModel( + name=name, + ast_type=ObjectTypeDefinitionNode, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node(description), + fields=tuple(base_type.graphql_fields.values()), + interfaces=base_type.interfaces, + ), + resolvers=base_type.resolvers, + aliases=base_type.__aliases__ or {}, + out_names=base_type.__fields_args__ or {}, + ) + + @classmethod + def construct_enum_model(cls, base_type: Type[EnumType]) -> GraphQLEnumModel: + return GraphQLEnumModel( + name=base_type.graphql_name, + members=base_type.__enum__ or {}, + ast_type=EnumTypeDefinitionNode, + ast=base_type.graphql_def, + ) + + @classmethod + def construct_directive_model(cls, base_type: Type[DirectiveType]) -> GraphQLModel: + """TODO: https://github.com/mirumee/ariadne-graphql-modules/issues/29""" + + @classmethod + def construct_input_model(cls, base_type: Type[InputType]) -> GraphQLInputModel: + return GraphQLInputModel( + name=base_type.graphql_name, + ast_type=InputObjectTypeDefinitionNode, + ast=base_type.graphql_def, # type: ignore + out_type=base_type.graphql_type, + out_names=base_type.graphql_fields or {}, # type: ignore + ) + + @classmethod + def construct_interface_model( + cls, base_type: Type[InterfaceType] + ) -> GraphQLInterfaceModel: + return GraphQLInterfaceModel( + name=base_type.graphql_name, + ast_type=InterfaceTypeDefinitionNode, + ast=base_type.graphql_def, + resolve_type=base_type.resolve_type, + resolvers=base_type.resolvers, + out_names={}, + aliases=base_type.__aliases__ or {}, + ) + + @classmethod + def construct_scalar_model(cls, base_type: Type[ScalarType]) -> GraphQLScalarModel: + return GraphQLScalarModel( + name=base_type.graphql_name, + ast_type=ScalarTypeDefinitionNode, + ast=base_type.graphql_def, + serialize=base_type.serialize, + parse_value=base_type.parse_value, + parse_literal=base_type.parse_literal, + ) + + @classmethod + def construct_subscription_model( + cls, base_type: Type[SubscriptionType] + ) -> GraphQLSubscriptionModel: + return GraphQLSubscriptionModel( + name=base_type.graphql_name, + ast_type=ObjectTypeDefinitionNode, + ast=base_type.graphql_def, + resolve_type=None, + resolvers=base_type.resolvers, + aliases=base_type.__aliases__ or {}, + out_names=base_type.__fields_args__ or {}, + subscribers=base_type.subscribers, + ) + + @classmethod + def construct_union_model(cls, base_type: Type[UnionType]) -> GraphQLUnionModel: + return GraphQLUnionModel( + name=base_type.graphql_name, + ast_type=UnionTypeDefinitionNode, + ast=base_type.graphql_def, + resolve_type=base_type.resolve_type, + ) diff --git a/ariadne_graphql_modules/object_type.py b/ariadne_graphql_modules/object_type.py index c252090..8bac4a9 100644 --- a/ariadne_graphql_modules/object_type.py +++ b/ariadne_graphql_modules/object_type.py @@ -39,6 +39,7 @@ def __init_subclass__(cls) -> None: cls.graphql_name = graphql_def.name.value cls.graphql_type = type(graphql_def) cls.graphql_fields = cls.__get_fields__(graphql_def) + cls.interfaces = graphql_def.interfaces requirements = cls.__get_requirements__() cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) diff --git a/ariadne_graphql_modules/resolvers_mixin.py b/ariadne_graphql_modules/resolvers_mixin.py index c91fee4..9d641dc 100644 --- a/ariadne_graphql_modules/resolvers_mixin.py +++ b/ariadne_graphql_modules/resolvers_mixin.py @@ -1,6 +1,6 @@ -from typing import Callable, Dict, Optional, Union +from typing import Callable, Dict, Optional, Tuple, Union -from graphql import GraphQLFieldResolver +from graphql import GraphQLFieldResolver, NamedTypeNode from .types import FieldsDict from .utils import create_alias_resolver @@ -19,6 +19,7 @@ class ResolversMixin: graphql_fields: FieldsDict resolvers: Dict[str, GraphQLFieldResolver] + interfaces: Tuple[NamedTypeNode, ...] @classmethod def __validate_aliases__(cls): diff --git a/ariadne_graphql_modules/scalar_type.py b/ariadne_graphql_modules/scalar_type.py index 1eeb3d7..a86388b 100644 --- a/ariadne_graphql_modules/scalar_type.py +++ b/ariadne_graphql_modules/scalar_type.py @@ -35,15 +35,17 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False - graphql_def = cls.__validate_schema__( + cls.graphql_def = cls.__validate_schema__( parse_definition(cls.__name__, cls.__schema__) ) - cls.graphql_name = graphql_def.name.value - cls.graphql_type = type(graphql_def) + cls.graphql_name = cls.graphql_def.name.value + cls.graphql_type = type(cls.graphql_def) requirements = cls.__get_requirements__() - cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) + cls.__validate_requirements_contain_extended_type__( + cls.graphql_def, requirements + ) @classmethod def __validate_schema__(cls, type_def: DefinitionNode) -> ScalarNodeType: diff --git a/ariadne_graphql_modules/union_type.py b/ariadne_graphql_modules/union_type.py index 6551c46..26f6a5c 100644 --- a/ariadne_graphql_modules/union_type.py +++ b/ariadne_graphql_modules/union_type.py @@ -31,17 +31,19 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False - graphql_def = cls.__validate_schema__( + cls.graphql_def = cls.__validate_schema__( parse_definition(cls.__name__, cls.__schema__) ) - cls.graphql_name = graphql_def.name.value - cls.graphql_type = type(graphql_def) + cls.graphql_name = cls.graphql_def.name.value + cls.graphql_type = type(cls.graphql_def) requirements = cls.__get_requirements__() - cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) + cls.__validate_requirements_contain_extended_type__( + cls.graphql_def, requirements + ) - dependencies = cls.__get_dependencies__(graphql_def) + dependencies = cls.__get_dependencies__(cls.graphql_def) cls.__validate_requirements__(requirements, dependencies) @classmethod diff --git a/tests_next/test_compatibility_layer.py b/tests_next/test_compatibility_layer.py new file mode 100644 index 0000000..57395f9 --- /dev/null +++ b/tests_next/test_compatibility_layer.py @@ -0,0 +1,480 @@ +from dataclasses import dataclass +from datetime import date, datetime + +from graphql import StringValueNode +from ariadne_graphql_modules.bases import DeferredType +from ariadne_graphql_modules.collection_type import CollectionType +from ariadne_graphql_modules.enum_type import EnumType +from ariadne_graphql_modules.input_type import InputType +from ariadne_graphql_modules.interface_type import InterfaceType +from ariadne_graphql_modules.next.compatibility_layer import wrap_legacy_types +from ariadne_graphql_modules.next.executable_schema import make_executable_schema +from ariadne_graphql_modules.object_type import ObjectType +from ariadne_graphql_modules.scalar_type import ScalarType +from ariadne_graphql_modules.subscription_type import SubscriptionType +from ariadne_graphql_modules.union_type import UnionType + + +def test_object_type( + assert_schema_equals, +): + # pylint: disable=unused-variable + class FancyObjectType(ObjectType): + __schema__ = """ + type FancyObject { + id: ID! + someInt: Int! + someFloat: Float! + someBoolean: Boolean! + someString: String! + } + """ + + class QueryType(ObjectType): + __schema__ = """ + type Query { + field: String! + other: String! + firstField: String! + secondField: String! + fieldWithArg(someArg: String): String! + } + """ + __aliases__ = { + "firstField": "first_field", + "secondField": "second_field", + "fieldWithArg": "field_with_arg", + } + __fields_args__ = {"fieldWithArg": {"someArg": "some_arg"}} + + @staticmethod + def resolve_other(*_): + return "Word Up!" + + @staticmethod + def resolve_second_field(obj, *_): + return "Obj: %s" % obj["secondField"] + + @staticmethod + def resolve_field_with_arg(*_, some_arg): + return some_arg + + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + + my_legacy_types = wrap_legacy_types(QueryType, UserRoleEnum, FancyObjectType) + schema = make_executable_schema(*my_legacy_types) + + assert_schema_equals( + schema, + """ + type Query { + field: String! + other: String! + firstField: String! + secondField: String! + fieldWithArg(someArg: String): String! + } + + enum UserRole { + USER + MOD + ADMIN + } + + type FancyObject { + id: ID! + someInt: Int! + someFloat: Float! + someBoolean: Boolean! + someString: String! + } + """, + ) + + +def test_collection_types_are_included_in_schema(assert_schema_equals): + class QueryType(ObjectType): + __schema__ = """ + type Query { + user: User + } + """ + __requires__ = [DeferredType("User")] + + class UserGroupType(ObjectType): + __schema__ = """ + type UserGroup { + id: ID! + } + """ + + class UserType(ObjectType): + __schema__ = """ + type User { + id: ID! + group: UserGroup! + } + """ + __requires__ = [UserGroupType] + + class UserTypes(CollectionType): + __types__ = [ + QueryType, + UserType, + ] + + my_legacy_types = wrap_legacy_types(UserTypes) + schema = make_executable_schema(*my_legacy_types) + + assert_schema_equals( + schema, + """ + type Query { + user: User + } + + type User { + id: ID! + group: UserGroup! + } + + type UserGroup { + id: ID! + } + """, + ) + + +def test_input_type(assert_schema_equals): + class UserInput(InputType): + __schema__ = """ + input UserInput { + id: ID! + fullName: String! + } + """ + __args__ = { + "fullName": "full_name", + } + + class GenericScalar(ScalarType): + __schema__ = "scalar Generic" + + class QueryType(ObjectType): + __schema__ = """ + type Query { + reprInput(input: UserInput): Generic! + } + """ + __aliases__ = {"reprInput": "repr_input"} + __requires__ = [GenericScalar, UserInput] + + @staticmethod + def resolve_repr_input(*_, input): # pylint: disable=redefined-builtin + return input + + my_legacy_types = wrap_legacy_types(QueryType) + schema = make_executable_schema(*my_legacy_types) + + assert_schema_equals( + schema, + """ + scalar Generic + + type Query { + reprInput(input: UserInput): Generic! + } + + input UserInput { + id: ID! + fullName: String! + } + """, + ) + + +def test_interface_type(assert_schema_equals): + @dataclass + class User: + id: int + name: str + summary: str + + @dataclass + class Comment: + id: int + message: str + summary: str + + class ResultInterface(InterfaceType): + __schema__ = """ + interface Result { + summary: String! + score: Int! + } + """ + + @staticmethod + def resolve_type(instance, *_): + if isinstance(instance, Comment): + return "Comment" + + if isinstance(instance, User): + return "User" + + return None + + class QueryType(ObjectType): + __schema__ = """ + type Query { + results: [Result!]! + } + """ + __requires__ = [ResultInterface] + + my_legacy_types = wrap_legacy_types(QueryType) + schema = make_executable_schema(*my_legacy_types) + + assert_schema_equals( + schema, + """ + type Query { + results: [Result!]! + } + + interface Result { + summary: String! + score: Int! + } + """, + ) + + +def test_scalar_type(assert_schema_equals): + TEST_DATE = date(2006, 9, 13) + + class DateReadOnlyScalar(ScalarType): + __schema__ = "scalar DateReadOnly" + + @staticmethod + def serialize(date): + return date.strftime("%Y-%m-%d") + + class DateInputScalar(ScalarType): + __schema__ = "scalar DateInput" + + @staticmethod + def parse_value(formatted_date): + parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") + return parsed_datetime.date() + + @staticmethod + def parse_literal(ast, variable_values=None): # pylint: disable=unused-argument + if not isinstance(ast, StringValueNode): + raise ValueError() + + formatted_date = ast.value + parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") + return parsed_datetime.date() + + class DefaultParserScalar(ScalarType): + __schema__ = "scalar DefaultParser" + + @staticmethod + def parse_value(value): + return type(value).__name__ + + class QueryType(ObjectType): + __schema__ = """ + type Query { + testSerialize: DateReadOnly! + testInput(value: DateInput!): Boolean! + testInputValueType(value: DefaultParser!): String! + } + """ + __requires__ = [ + DateReadOnlyScalar, + DateInputScalar, + DefaultParserScalar, + ] + __aliases__ = { + "testSerialize": "test_serialize", + "testInput": "test_input", + "testInputValueType": "test_input_value_type", + } + + @staticmethod + def resolve_test_serialize(*_): + return TEST_DATE + + @staticmethod + def resolve_test_input(*_, value): + assert value == TEST_DATE + return True + + @staticmethod + def resolve_test_input_value_type(*_, value): + return value + + my_legacy_types = wrap_legacy_types(QueryType) + schema = make_executable_schema(*my_legacy_types) + + assert_schema_equals( + schema, + """ + scalar DateInput + + scalar DateReadOnly + + scalar DefaultParser + + type Query { + testSerialize: DateReadOnly! + testInput(value: DateInput!): Boolean! + testInputValueType(value: DefaultParser!): String! + } + """, + ) + + +def test_subscription_type(assert_schema_equals): + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Interface { + threads: ID! + } + """ + + @staticmethod + def resolve_type(instance, *_): + return "Threads" + + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + + class ExtendChatSubscription(SubscriptionType): + __schema__ = """ + extend type Subscription implements Interface { + threads: ID! + } + """ + __requires__ = [ChatSubscription, ExampleInterface] + + class QueryType(ObjectType): + __schema__ = """ + type Query { + testSubscription: String! + } + """ + + my_legacy_types = wrap_legacy_types( + QueryType, + ExampleInterface, + ChatSubscription, + ExtendChatSubscription, + ) + + schema = make_executable_schema(*my_legacy_types) + + assert_schema_equals( + schema, + """ + type Query { + testSubscription: String! + } + + type Subscription implements Interface { + chat: ID! + threads: ID! + } + + interface Interface { + threads: ID! + } + """, + ) + + +def test_union_type(assert_schema_equals): + @dataclass + class User: + id: int + name: str + + @dataclass + class Comment: + id: int + message: str + + class UserType(ObjectType): + __schema__ = """ + type User { + id: ID! + name: String! + } + """ + + class CommentType(ObjectType): + __schema__ = """ + type Comment { + id: ID! + message: String! + } + """ + + class ExampleUnion(UnionType): + __schema__ = "union Result = User | Comment" + __requires__ = [UserType, CommentType] + + @staticmethod + def resolve_type(instance, *_): + if isinstance(instance, Comment): + return "Comment" + + if isinstance(instance, User): + return "User" + + return None + + class QueryType(ObjectType): + __schema__ = """ + type Query { + testUnion: String! + } + """ + + my_legacy_types = wrap_legacy_types(ExampleUnion, CommentType, QueryType) + + schema = make_executable_schema(*my_legacy_types) + + assert_schema_equals( + schema, + """ + type Query { + testUnion: String! + } + + union Result = User | Comment + + type User { + id: ID! + name: String! + } + + type Comment { + id: ID! + message: String! + } + """, + ) From 89a800202e54641f6ed568df463d622e35810c85 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 19 Aug 2024 16:10:56 +0200 Subject: [PATCH 45/63] adjust code to newer changes --- ariadne_graphql_modules/bases.py | 4 +- ariadne_graphql_modules/mutation_type.py | 14 +-- .../next/compatibility_layer.py | 86 ++++++++++--------- .../next/graphql_interface/interface_model.py | 6 +- .../next/graphql_interface/interface_type.py | 2 +- .../next/graphql_scalar/scalar_model.py | 15 ++-- .../subscription_model.py | 3 +- .../graphql_subscription/subscription_type.py | 11 --- .../next/graphql_union/union_model.py | 5 +- ariadne_graphql_modules/object_type.py | 15 ++-- ariadne_graphql_modules/union_type.py | 2 +- tests/conftest.py | 12 +++ tests/snapshots/__init__.py | 0 .../snapshots/snap_test_definition_parser.py | 14 --- tests/snapshots/snap_test_directive_type.py | 20 ----- tests/snapshots/snap_test_enum_type.py | 26 ------ .../snapshots/snap_test_executable_schema.py | 10 --- tests/snapshots/snap_test_input_type.py | 28 ------ tests/snapshots/snap_test_interface_type.py | 32 ------- tests/snapshots/snap_test_mutation_type.py | 32 ------- tests/snapshots/snap_test_object_type.py | 36 -------- tests/snapshots/snap_test_scalar_type.py | 18 ---- .../snapshots/snap_test_subscription_type.py | 34 -------- tests/snapshots/snap_test_union_type.py | 24 ------ ...a_str_contains_multiple_types.obtained.yml | 2 + ...ror_schema_str_contains_multiple_types.yml | 2 + ...schema_str_has_invalid_syntax.obtained.yml | 2 + ...ror_when_schema_str_has_invalid_syntax.yml | 2 + ...r_when_schema_type_is_invalid.obtained.yml | 1 + ...ises_error_when_schema_type_is_invalid.yml | 1 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ..._when_defined_without_visitor.obtained.yml | 2 + ...ute_error_when_defined_without_visitor.yml | 2 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...extra_items_not_in_definition.obtained.yml | 2 + ...ping_has_extra_items_not_in_definition.yml | 2 + ..._misses_items_from_definition.obtained.yml | 2 + ...t_mapping_misses_items_from_definition.yml | 2 + ...extra_items_not_in_definition.obtained.yml | 2 + ...ping_has_extra_items_not_in_definition.yml | 2 + ..._misses_items_from_definition.obtained.yml | 2 + ...m_mapping_misses_items_from_definition.yml | 2 + ...erged_types_define_same_field.obtained.yml | 1 + ...rror_if_merged_types_define_same_field.yml | 1 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...rgs_map_for_nonexisting_field.obtained.yml | 1 + ...ed_with_args_map_for_nonexisting_field.yml | 1 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...d_without_extended_dependency.obtained.yml | 3 + ...en_defined_without_extended_dependency.yml | 3 + ...without_field_type_dependency.obtained.yml | 2 + ..._defined_without_field_type_dependency.yml | 2 + ...r_when_defined_without_fields.obtained.yml | 2 + ...ises_error_when_defined_without_fields.yml | 2 + ...nded_dependency_is_wrong_type.obtained.yml | 3 + ...when_extended_dependency_is_wrong_type.yml | 3 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...h_alias_for_nonexisting_field.obtained.yml | 1 + ...fined_with_alias_for_nonexisting_field.yml | 1 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...esolver_for_nonexisting_field.obtained.yml | 2 + ...ed_with_resolver_for_nonexisting_field.yml | 2 + ...hout_argument_type_dependency.obtained.yml | 3 + ...fined_without_argument_type_dependency.yml | 3 + ...d_without_extended_dependency.obtained.yml | 2 + ...en_defined_without_extended_dependency.yml | 2 + ...r_when_defined_without_fields.obtained.yml | 3 + ...ises_error_when_defined_without_fields.yml | 3 + ...ithout_return_type_dependency.obtained.yml | 3 + ...defined_without_return_type_dependency.yml | 3 + ...nded_dependency_is_wrong_type.obtained.yml | 3 + ...when_extended_dependency_is_wrong_type.yml | 3 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...fined_for_different_type_name.obtained.yml | 3 + ...r_when_defined_for_different_type_name.yml | 3 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ..._defined_with_multiple_fields.obtained.yml | 3 + ...rror_when_defined_with_multiple_fields.yml | 3 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...defined_with_nonexistant_args.obtained.yml | 2 + ...ror_when_defined_with_nonexistant_args.yml | 2 + ...allable_resolve_mutation_attr.obtained.yml | 3 + ...without_callable_resolve_mutation_attr.yml | 3 + ...r_when_defined_without_fields.obtained.yml | 3 + ...ises_error_when_defined_without_fields.yml | 3 + ...without_resolve_mutation_attr.obtained.yml | 2 + ..._defined_without_resolve_mutation_attr.yml | 2 + ...ithout_return_type_dependency.obtained.yml | 3 + ...defined_without_return_type_dependency.yml | 3 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...h_alias_for_nonexisting_field.obtained.yml | 1 + ...fined_with_alias_for_nonexisting_field.yml | 1 + ...ield_args_for_nonexisting_arg.obtained.yml | 1 + ...ed_with_field_args_for_nonexisting_arg.yml | 1 + ...ld_args_for_nonexisting_field.obtained.yml | 2 + ..._with_field_args_for_nonexisting_field.yml | 2 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...esolver_for_nonexisting_field.obtained.yml | 1 + ...ed_with_resolver_for_nonexisting_field.yml | 1 + ...hout_argument_type_dependency.obtained.yml | 3 + ...fined_without_argument_type_dependency.yml | 3 + ...d_without_extended_dependency.obtained.yml | 3 + ...en_defined_without_extended_dependency.yml | 3 + ...r_when_defined_without_fields.obtained.yml | 2 + ...ises_error_when_defined_without_fields.yml | 2 + ...ithout_return_type_dependency.obtained.yml | 2 + ...defined_without_return_type_dependency.yml | 2 + ...nded_dependency_is_wrong_type.obtained.yml | 2 + ...when_extended_dependency_is_wrong_type.yml | 2 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...h_alias_for_nonexisting_field.obtained.yml | 1 + ...fined_with_alias_for_nonexisting_field.yml | 1 + ...ith_invalid_graphql_type_name.obtained.yml | 3 + ...defined_with_invalid_graphql_type_name.yml | 3 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...esolver_for_nonexisting_field.obtained.yml | 2 + ...ed_with_resolver_for_nonexisting_field.yml | 2 + ...ith_sub_for_nonexisting_field.obtained.yml | 2 + ...defined_with_sub_for_nonexisting_field.yml | 2 + ...hout_argument_type_dependency.obtained.yml | 3 + ...fined_without_argument_type_dependency.yml | 3 + ...d_without_extended_dependency.obtained.yml | 3 + ...en_defined_without_extended_dependency.yml | 3 + ...r_when_defined_without_fields.obtained.yml | 3 + ...ises_error_when_defined_without_fields.yml | 3 + ...ithout_return_type_dependency.obtained.yml | 3 + ...defined_without_return_type_dependency.yml | 3 + ...nded_dependency_is_wrong_type.obtained.yml | 3 + ...when_extended_dependency_is_wrong_type.yml | 3 + ...r_when_defined_without_schema.obtained.yml | 2 + ...bute_error_when_defined_without_schema.yml | 2 + ...h_invalid_graphql_type_schema.obtained.yml | 2 + ...fined_with_invalid_graphql_type_schema.yml | 2 + ...fined_with_invalid_schema_str.obtained.yml | 2 + ...r_when_defined_with_invalid_schema_str.yml | 2 + ...ined_with_invalid_schema_type.obtained.yml | 1 + ..._when_defined_with_invalid_schema_type.yml | 1 + ...ed_with_multiple_types_schema.obtained.yml | 2 + ...hen_defined_with_multiple_types_schema.yml | 2 + ...d_without_extended_dependency.obtained.yml | 3 + ...en_defined_without_extended_dependency.yml | 3 + ...ithout_member_type_dependency.obtained.yml | 3 + ...defined_without_member_type_dependency.yml | 3 + ...nded_dependency_is_wrong_type.obtained.yml | 3 + ...when_extended_dependency_is_wrong_type.yml | 3 + tests/test_definition_parser.py | 16 ++-- tests/test_directive_type.py | 34 +++++--- tests/test_enum_type.py | 38 ++++---- tests/test_executable_schema.py | 4 +- tests/test_input_type.py | 48 ++++++----- tests/test_interface_type.py | 60 +++++++------ tests/test_mutation_type.py | 56 ++++++------ tests/test_object_type.py | 68 +++++++++------ tests/test_scalar_type.py | 26 +++--- tests/test_subscription_type.py | 58 +++++++------ tests/test_union_type.py | 40 +++++---- tests_next/test_compatibility_layer.py | 11 +-- tests_next/test_interface_type.py | 31 +++++-- 221 files changed, 757 insertions(+), 560 deletions(-) create mode 100644 tests/conftest.py delete mode 100644 tests/snapshots/__init__.py delete mode 100644 tests/snapshots/snap_test_definition_parser.py delete mode 100644 tests/snapshots/snap_test_directive_type.py delete mode 100644 tests/snapshots/snap_test_enum_type.py delete mode 100644 tests/snapshots/snap_test_executable_schema.py delete mode 100644 tests/snapshots/snap_test_input_type.py delete mode 100644 tests/snapshots/snap_test_interface_type.py delete mode 100644 tests/snapshots/snap_test_mutation_type.py delete mode 100644 tests/snapshots/snap_test_object_type.py delete mode 100644 tests/snapshots/snap_test_scalar_type.py delete mode 100644 tests/snapshots/snap_test_subscription_type.py delete mode 100644 tests/snapshots/snap_test_union_type.py create mode 100644 tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml create mode 100644 tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml create mode 100644 tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml create mode 100644 tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml create mode 100644 tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml create mode 100644 tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml create mode 100644 tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml create mode 100644 tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml create mode 100644 tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml create mode 100644 tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml create mode 100644 tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml create mode 100644 tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_defined_without_fields.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml create mode 100644 tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml create mode 100644 tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml create mode 100644 tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml create mode 100644 tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml create mode 100644 tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_fields.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml create mode 100644 tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml create mode 100644 tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml create mode 100644 tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml create mode 100644 tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml create mode 100644 tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml diff --git a/ariadne_graphql_modules/bases.py b/ariadne_graphql_modules/bases.py index 22193b1..2ff42e2 100644 --- a/ariadne_graphql_modules/bases.py +++ b/ariadne_graphql_modules/bases.py @@ -4,7 +4,7 @@ DefinitionNode, GraphQLSchema, ObjectTypeDefinitionNode, - TypeDefinitionNode, + TypeSystemDefinitionNode, ) from .dependencies import Dependencies @@ -36,7 +36,7 @@ class DefinitionType(BaseType): graphql_name: str graphql_type: Type[DefinitionNode] - graphql_def: TypeDefinitionNode + graphql_def: TypeSystemDefinitionNode @classmethod def __get_requirements__(cls) -> RequirementsDict: diff --git a/ariadne_graphql_modules/mutation_type.py b/ariadne_graphql_modules/mutation_type.py index ba41e2b..d8e020d 100644 --- a/ariadne_graphql_modules/mutation_type.py +++ b/ariadne_graphql_modules/mutation_type.py @@ -35,20 +35,22 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False - graphql_def = cls.__validate_schema__( + cls.graphql_def = cls.__validate_schema__( parse_definition(cls.__name__, cls.__schema__) ) - cls.graphql_name = graphql_def.name.value - cls.graphql_type = type(graphql_def) + cls.graphql_name = cls.graphql_def.name.value + cls.graphql_type = type(cls.graphql_def) - field = cls.__get_field__(graphql_def) + field = cls.__get_field__(cls.graphql_def) cls.mutation_name = field.name.value requirements = cls.__get_requirements__() - cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) + cls.__validate_requirements_contain_extended_type__( + cls.graphql_def, requirements + ) - dependencies = cls.__get_dependencies__(graphql_def) + dependencies = cls.__get_dependencies__(cls.graphql_def) cls.__validate_requirements__(requirements, dependencies) if callable(cls.__args__): diff --git a/ariadne_graphql_modules/next/compatibility_layer.py b/ariadne_graphql_modules/next/compatibility_layer.py index efb4107..8106dc5 100644 --- a/ariadne_graphql_modules/next/compatibility_layer.py +++ b/ariadne_graphql_modules/next/compatibility_layer.py @@ -1,23 +1,18 @@ -from typing import List, Type +from enum import Enum +from inspect import isclass +from typing import Any, Dict, List, Type, Union, cast from graphql import ( EnumTypeDefinitionNode, InputObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, - NameNode, ObjectTypeDefinitionNode, ScalarTypeDefinitionNode, + TypeExtensionNode, UnionTypeDefinitionNode, ) -from ariadne_graphql_modules.executable_schema import get_all_types -from ariadne_graphql_modules.next.inputtype import GraphQLInputModel -from ariadne_graphql_modules.next.interfacetype import ( - GraphQLInterfaceModel, -) -from ariadne_graphql_modules.next.scalartype import GraphQLScalarModel -from ariadne_graphql_modules.next.subscriptiontype import GraphQLSubscriptionModel -from ariadne_graphql_modules.next.uniontype import GraphQLUnionModel +from ..executable_schema import get_all_types from ..directive_type import DirectiveType from ..enum_type import EnumType @@ -30,10 +25,17 @@ from ..object_type import ObjectType -from .description import get_description_node from ..bases import BindableType from .base import GraphQLModel, GraphQLType -from . import GraphQLObjectModel, GraphQLEnumModel +from . import ( + GraphQLObjectModel, + GraphQLEnumModel, + GraphQLInputModel, + GraphQLScalarModel, + GraphQLInterfaceModel, + GraphQLSubscriptionModel, + GraphQLUnionModel, +) def wrap_legacy_types( @@ -53,6 +55,8 @@ class LegacyGraphQLType(GraphQLType): @classmethod def __get_graphql_model__(cls, *_) -> GraphQLModel: + if issubclass(cls.__base_type__.graphql_type, TypeExtensionNode): + pass if issubclass(cls.__base_type__, ObjectType): return cls.construct_object_model(cls.__base_type__) if issubclass(cls.__base_type__, EnumType): @@ -69,41 +73,40 @@ def __get_graphql_model__(cls, *_) -> GraphQLModel: return cls.construct_subscription_model(cls.__base_type__) if issubclass(cls.__base_type__, UnionType): return cls.construct_union_model(cls.__base_type__) - else: - raise ValueError(f"Unsupported base_type {cls.__base_type__}") + raise ValueError(f"Unsupported base_type {cls.__base_type__}") @classmethod def construct_object_model( - cls, base_type: Type[ObjectType] - ) -> "GraphQLObjectModel": - name = base_type.graphql_name - description = base_type.__doc__ - + cls, base_type: Type[Union[ObjectType, MutationType]] + ) -> GraphQLObjectModel: return GraphQLObjectModel( - name=name, + name=base_type.graphql_name, ast_type=ObjectTypeDefinitionNode, - ast=ObjectTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node(description), - fields=tuple(base_type.graphql_fields.values()), - interfaces=base_type.interfaces, - ), - resolvers=base_type.resolvers, - aliases=base_type.__aliases__ or {}, - out_names=base_type.__fields_args__ or {}, + ast=cast(ObjectTypeDefinitionNode, base_type.graphql_def), + resolvers=base_type.resolvers, # type: ignore + aliases=base_type.__aliases__ or {}, # type: ignore + out_names={}, ) @classmethod def construct_enum_model(cls, base_type: Type[EnumType]) -> GraphQLEnumModel: + members = base_type.__enum__ or {} + members_values: Dict[str, Any] = {} + + if isinstance(members, dict): + members_values = dict(members.items()) + elif isclass(members) and issubclass(members, Enum): + members_values = {member.name: member for member in members} + return GraphQLEnumModel( name=base_type.graphql_name, - members=base_type.__enum__ or {}, + members=members_values, ast_type=EnumTypeDefinitionNode, - ast=base_type.graphql_def, + ast=cast(EnumTypeDefinitionNode, base_type.graphql_def), ) @classmethod - def construct_directive_model(cls, base_type: Type[DirectiveType]) -> GraphQLModel: + def construct_directive_model(cls, base_type: Type[DirectiveType]): """TODO: https://github.com/mirumee/ariadne-graphql-modules/issues/29""" @classmethod @@ -111,9 +114,9 @@ def construct_input_model(cls, base_type: Type[InputType]) -> GraphQLInputModel: return GraphQLInputModel( name=base_type.graphql_name, ast_type=InputObjectTypeDefinitionNode, - ast=base_type.graphql_def, # type: ignore + ast=cast(InputObjectTypeDefinitionNode, base_type.graphql_def), out_type=base_type.graphql_type, - out_names=base_type.graphql_fields or {}, # type: ignore + out_names={}, ) @classmethod @@ -123,11 +126,11 @@ def construct_interface_model( return GraphQLInterfaceModel( name=base_type.graphql_name, ast_type=InterfaceTypeDefinitionNode, - ast=base_type.graphql_def, + ast=cast(InterfaceTypeDefinitionNode, base_type.graphql_def), resolve_type=base_type.resolve_type, resolvers=base_type.resolvers, out_names={}, - aliases=base_type.__aliases__ or {}, + aliases=base_type.__aliases__ or {}, # type: ignore ) @classmethod @@ -135,7 +138,7 @@ def construct_scalar_model(cls, base_type: Type[ScalarType]) -> GraphQLScalarMod return GraphQLScalarModel( name=base_type.graphql_name, ast_type=ScalarTypeDefinitionNode, - ast=base_type.graphql_def, + ast=cast(ScalarTypeDefinitionNode, base_type.graphql_def), serialize=base_type.serialize, parse_value=base_type.parse_value, parse_literal=base_type.parse_literal, @@ -148,11 +151,10 @@ def construct_subscription_model( return GraphQLSubscriptionModel( name=base_type.graphql_name, ast_type=ObjectTypeDefinitionNode, - ast=base_type.graphql_def, - resolve_type=None, + ast=cast(ObjectTypeDefinitionNode, base_type.graphql_def), resolvers=base_type.resolvers, - aliases=base_type.__aliases__ or {}, - out_names=base_type.__fields_args__ or {}, + aliases=base_type.__aliases__ or {}, # type: ignore + out_names={}, subscribers=base_type.subscribers, ) @@ -161,6 +163,6 @@ def construct_union_model(cls, base_type: Type[UnionType]) -> GraphQLUnionModel: return GraphQLUnionModel( name=base_type.graphql_name, ast_type=UnionTypeDefinitionNode, - ast=base_type.graphql_def, + ast=cast(UnionTypeDefinitionNode, base_type.graphql_def), resolve_type=base_type.resolve_type, ) diff --git a/ariadne_graphql_modules/next/graphql_interface/interface_model.py b/ariadne_graphql_modules/next/graphql_interface/interface_model.py index cd93a8f..3dfbf51 100644 --- a/ariadne_graphql_modules/next/graphql_interface/interface_model.py +++ b/ariadne_graphql_modules/next/graphql_interface/interface_model.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from typing import Any, Callable, Dict, cast +from typing import Dict, cast from ariadne import InterfaceType from ariadne.types import Resolver -from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema +from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLTypeResolver from ..base import GraphQLModel @@ -11,7 +11,7 @@ @dataclass(frozen=True) class GraphQLInterfaceModel(GraphQLModel): resolvers: Dict[str, Resolver] - resolve_type: Callable[[Any], Any] + resolve_type: GraphQLTypeResolver out_names: Dict[str, Dict[str, str]] aliases: Dict[str, str] diff --git a/ariadne_graphql_modules/next/graphql_interface/interface_type.py b/ariadne_graphql_modules/next/graphql_interface/interface_type.py index 435057c..1a7832b 100644 --- a/ariadne_graphql_modules/next/graphql_interface/interface_type.py +++ b/ariadne_graphql_modules/next/graphql_interface/interface_type.py @@ -167,7 +167,7 @@ def __get_graphql_model_without_schema__( @staticmethod def resolve_type(obj: Any, *_) -> str: - if isinstance(obj, GraphQLInterface): + if isinstance(obj, GraphQLObject): return obj.__get_graphql_name__() raise ValueError( diff --git a/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py b/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py index e9048ce..f6ad88c 100644 --- a/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py +++ b/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py @@ -1,16 +1,21 @@ from dataclasses import dataclass -from typing import Any, Callable, Dict, Optional +from typing import Optional -from graphql import GraphQLSchema, ValueNode +from graphql import ( + GraphQLScalarLiteralParser, + GraphQLScalarSerializer, + GraphQLScalarValueParser, + GraphQLSchema, +) from ariadne import ScalarType as ScalarTypeBindable from ..base import GraphQLModel @dataclass(frozen=True) class GraphQLScalarModel(GraphQLModel): - serialize: Callable[[Any], Any] - parse_value: Callable[[Any], Any] - parse_literal: Callable[[ValueNode, Optional[Dict[str, Any]]], Any] + serialize: Optional[GraphQLScalarSerializer] + parse_value: Optional[GraphQLScalarValueParser] + parse_literal: Optional[GraphQLScalarLiteralParser] def bind_to_schema(self, schema: GraphQLSchema): bindable = ScalarTypeBindable( diff --git a/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py b/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py index 52dc3be..95bff2d 100644 --- a/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py +++ b/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Callable, Dict, cast +from typing import Dict, cast from ariadne import SubscriptionType from ariadne.types import Resolver, Subscriber @@ -11,7 +11,6 @@ @dataclass(frozen=True) class GraphQLSubscriptionModel(GraphQLModel): resolvers: Dict[str, Resolver] - resolve_type: Callable[[Any], Any] out_names: Dict[str, Dict[str, str]] aliases: Dict[str, str] subscribers: Dict[str, Subscriber] diff --git a/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py b/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py index 95757e1..41aef33 100644 --- a/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py +++ b/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py @@ -145,7 +145,6 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": fields=tuple(fields), interfaces=definition.interfaces, ), - resolve_type=cls.resolve_type, resolvers=resolvers, subscribers=subscribers, aliases=getattr(cls, "__aliases__", {}), @@ -196,22 +195,12 @@ def __get_graphql_model_without_schema__( fields=tuple(fields_ast), interfaces=tuple(interfaces_ast), ), - resolve_type=cls.resolve_type, resolvers=resolvers, aliases=aliases, out_names=out_names, subscribers=subscribers, ) - @staticmethod - def resolve_type(obj: Any, *_) -> str: - if isinstance(obj, GraphQLSubscription): - return obj.__get_graphql_name__() - - raise ValueError( - f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." - ) - @staticmethod def source( field: str, diff --git a/ariadne_graphql_modules/next/graphql_union/union_model.py b/ariadne_graphql_modules/next/graphql_union/union_model.py index 48f0628..a5e42dd 100644 --- a/ariadne_graphql_modules/next/graphql_union/union_model.py +++ b/ariadne_graphql_modules/next/graphql_union/union_model.py @@ -1,15 +1,14 @@ from dataclasses import dataclass -from typing import Any, Callable from ariadne import UnionType -from graphql import GraphQLSchema +from graphql import GraphQLSchema, GraphQLTypeResolver from ..base import GraphQLModel @dataclass(frozen=True) class GraphQLUnionModel(GraphQLModel): - resolve_type: Callable[[Any], Any] + resolve_type: GraphQLTypeResolver def bind_to_schema(self, schema: GraphQLSchema): bindable = UnionType(self.name, self.resolve_type) diff --git a/ariadne_graphql_modules/object_type.py b/ariadne_graphql_modules/object_type.py index 8bac4a9..bb54b4a 100644 --- a/ariadne_graphql_modules/object_type.py +++ b/ariadne_graphql_modules/object_type.py @@ -32,19 +32,20 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False - graphql_def = cls.__validate_schema__( + cls.graphql_def = cls.__validate_schema__( parse_definition(cls.__name__, cls.__schema__) ) - cls.graphql_name = graphql_def.name.value - cls.graphql_type = type(graphql_def) - cls.graphql_fields = cls.__get_fields__(graphql_def) - cls.interfaces = graphql_def.interfaces + cls.graphql_name = cls.graphql_def.name.value + cls.graphql_type = type(cls.graphql_def) + cls.graphql_fields = cls.__get_fields__(cls.graphql_def) requirements = cls.__get_requirements__() - cls.__validate_requirements_contain_extended_type__(graphql_def, requirements) + cls.__validate_requirements_contain_extended_type__( + cls.graphql_def, requirements + ) - dependencies = cls.__get_dependencies__(graphql_def) + dependencies = cls.__get_dependencies__(cls.graphql_def) cls.__validate_requirements__(requirements, dependencies) if callable(cls.__fields_args__): diff --git a/ariadne_graphql_modules/union_type.py b/ariadne_graphql_modules/union_type.py index 26f6a5c..932ba4f 100644 --- a/ariadne_graphql_modules/union_type.py +++ b/ariadne_graphql_modules/union_type.py @@ -36,7 +36,7 @@ def __init_subclass__(cls) -> None: ) cls.graphql_name = cls.graphql_def.name.value - cls.graphql_type = type(cls.graphql_def) + cls.graphql_type = type(cls.graphql_def) # type: ignore requirements = cls.__get_requirements__() cls.__validate_requirements_contain_extended_type__( diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0f16709 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +from pathlib import Path +import pytest + + +@pytest.fixture(scope="session") +def datadir() -> Path: + return Path(__file__).parent / "snapshots" + + +@pytest.fixture(scope="session") +def original_datadir() -> Path: + return Path(__file__).parent / "snapshots" diff --git a/tests/snapshots/__init__.py b/tests/snapshots/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/snapshots/snap_test_definition_parser.py b/tests/snapshots/snap_test_definition_parser.py deleted file mode 100644 index f78c1f4..0000000 --- a/tests/snapshots/snap_test_definition_parser.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_definition_parser_raises_error_schema_str_contains_multiple_types 1'] = GenericRepr("") - -snapshots['test_definition_parser_raises_error_when_schema_str_has_invalid_syntax 1'] = GenericRepr('') - -snapshots['test_definition_parser_raises_error_when_schema_type_is_invalid 1'] = GenericRepr("") diff --git a/tests/snapshots/snap_test_directive_type.py b/tests/snapshots/snap_test_directive_type.py deleted file mode 100644 index 4691c16..0000000 --- a/tests/snapshots/snap_test_directive_type.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_directive_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_directive_type_raises_attribute_error_when_defined_without_visitor 1'] = GenericRepr("") - -snapshots['test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_directive_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_directive_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_directive_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") diff --git a/tests/snapshots/snap_test_enum_type.py b/tests/snapshots/snap_test_enum_type.py deleted file mode 100644 index 0ee40db..0000000 --- a/tests/snapshots/snap_test_enum_type.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_enum_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_enum_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_enum_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_enum_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") - -snapshots['test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition 1'] = GenericRepr("") - -snapshots['test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition 1'] = GenericRepr("") - -snapshots['test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition 1'] = GenericRepr("") - -snapshots['test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition 1'] = GenericRepr("") diff --git a/tests/snapshots/snap_test_executable_schema.py b/tests/snapshots/snap_test_executable_schema.py deleted file mode 100644 index afca833..0000000 --- a/tests/snapshots/snap_test_executable_schema.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_executable_schema_raises_value_error_if_merged_types_define_same_field 1'] = GenericRepr('') diff --git a/tests/snapshots/snap_test_input_type.py b/tests/snapshots/snap_test_input_type.py deleted file mode 100644 index 8df4264..0000000 --- a/tests/snapshots/snap_test_input_type.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_input_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_input_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_input_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_input_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") - -snapshots['test_input_type_raises_error_when_defined_without_extended_dependency 1'] = GenericRepr('') - -snapshots['test_input_type_raises_error_when_defined_without_field_type_dependency 1'] = GenericRepr('') - -snapshots['test_input_type_raises_error_when_defined_without_fields 1'] = GenericRepr("") - -snapshots['test_input_type_raises_error_when_extended_dependency_is_wrong_type 1'] = GenericRepr('') diff --git a/tests/snapshots/snap_test_interface_type.py b/tests/snapshots/snap_test_interface_type.py deleted file mode 100644 index b21bd35..0000000 --- a/tests/snapshots/snap_test_interface_type.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_interface_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_interface_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_interface_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_interface_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") - -snapshots['test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_interface_type_raises_error_when_defined_without_argument_type_dependency 1'] = GenericRepr('') - -snapshots['test_interface_type_raises_error_when_defined_without_extended_dependency 1'] = GenericRepr("") - -snapshots['test_interface_type_raises_error_when_defined_without_fields 1'] = GenericRepr("") - -snapshots['test_interface_type_raises_error_when_defined_without_return_type_dependency 1'] = GenericRepr('') - -snapshots['test_interface_type_raises_error_when_extended_dependency_is_wrong_type 1'] = GenericRepr('') diff --git a/tests/snapshots/snap_test_mutation_type.py b/tests/snapshots/snap_test_mutation_type.py deleted file mode 100644 index f5d3262..0000000 --- a/tests/snapshots/snap_test_mutation_type.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_mutation_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_mutation_type_raises_error_when_defined_for_different_type_name 1'] = GenericRepr('') - -snapshots['test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_mutation_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_mutation_type_raises_error_when_defined_with_multiple_fields 1'] = GenericRepr('') - -snapshots['test_mutation_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") - -snapshots['test_mutation_type_raises_error_when_defined_with_nonexistant_args 1'] = GenericRepr('') - -snapshots['test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr 1'] = GenericRepr('') - -snapshots['test_mutation_type_raises_error_when_defined_without_fields 1'] = GenericRepr("") - -snapshots['test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr 1'] = GenericRepr('') - -snapshots['test_mutation_type_raises_error_when_defined_without_return_type_dependency 1'] = GenericRepr('') - -snapshots['test_object_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') diff --git a/tests/snapshots/snap_test_object_type.py b/tests/snapshots/snap_test_object_type.py deleted file mode 100644 index d5ed96c..0000000 --- a/tests/snapshots/snap_test_object_type.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_object_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg 1'] = GenericRepr('') - -snapshots['test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_object_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_object_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_object_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") - -snapshots['test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_object_type_raises_error_when_defined_without_argument_type_dependency 1'] = GenericRepr('') - -snapshots['test_object_type_raises_error_when_defined_without_extended_dependency 1'] = GenericRepr('') - -snapshots['test_object_type_raises_error_when_defined_without_fields 1'] = GenericRepr("") - -snapshots['test_object_type_raises_error_when_defined_without_return_type_dependency 1'] = GenericRepr('') - -snapshots['test_object_type_raises_error_when_extended_dependency_is_wrong_type 1'] = GenericRepr('') diff --git a/tests/snapshots/snap_test_scalar_type.py b/tests/snapshots/snap_test_scalar_type.py deleted file mode 100644 index 8de8a0e..0000000 --- a/tests/snapshots/snap_test_scalar_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_scalar_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_scalar_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_scalar_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_scalar_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") diff --git a/tests/snapshots/snap_test_subscription_type.py b/tests/snapshots/snap_test_subscription_type.py deleted file mode 100644 index 2b46bb1..0000000 --- a/tests/snapshots/snap_test_subscription_type.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_subscription_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name 1'] = GenericRepr('') - -snapshots['test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_subscription_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_subscription_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field 1'] = GenericRepr("") - -snapshots['test_subscription_type_raises_error_when_defined_without_argument_type_dependency 1'] = GenericRepr('') - -snapshots['test_subscription_type_raises_error_when_defined_without_extended_dependency 1'] = GenericRepr('') - -snapshots['test_subscription_type_raises_error_when_defined_without_fields 1'] = GenericRepr("") - -snapshots['test_subscription_type_raises_error_when_defined_without_return_type_dependency 1'] = GenericRepr('') - -snapshots['test_subscription_type_raises_error_when_extended_dependency_is_wrong_type 1'] = GenericRepr('') diff --git a/tests/snapshots/snap_test_union_type.py b/tests/snapshots/snap_test_union_type.py deleted file mode 100644 index 26fd0c5..0000000 --- a/tests/snapshots/snap_test_union_type.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_interface_type_raises_error_when_extended_dependency_is_wrong_type 1'] = GenericRepr('') - -snapshots['test_union_type_raises_attribute_error_when_defined_without_schema 1'] = GenericRepr('') - -snapshots['test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema 1'] = GenericRepr("") - -snapshots['test_union_type_raises_error_when_defined_with_invalid_schema_str 1'] = GenericRepr('') - -snapshots['test_union_type_raises_error_when_defined_with_invalid_schema_type 1'] = GenericRepr("") - -snapshots['test_union_type_raises_error_when_defined_with_multiple_types_schema 1'] = GenericRepr("") - -snapshots['test_union_type_raises_error_when_defined_without_extended_dependency 1'] = GenericRepr('') - -snapshots['test_union_type_raises_error_when_defined_without_member_type_dependency 1'] = GenericRepr('') diff --git a/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml b/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml new file mode 100644 index 0000000..1901e66 --- /dev/null +++ b/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml @@ -0,0 +1,2 @@ +'MyType class was defined with __schema__ containing more than one GraphQL definition + (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml b/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml new file mode 100644 index 0000000..1901e66 --- /dev/null +++ b/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml @@ -0,0 +1,2 @@ +'MyType class was defined with __schema__ containing more than one GraphQL definition + (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml b/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml new file mode 100644 index 0000000..d16bce5 --- /dev/null +++ b/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo User\n |\ + \ ^" diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml b/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml new file mode 100644 index 0000000..d16bce5 --- /dev/null +++ b/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo User\n |\ + \ ^" diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml b/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml new file mode 100644 index 0000000..e12dbfa --- /dev/null +++ b/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml @@ -0,0 +1 @@ +'MyType class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml b/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml new file mode 100644 index 0000000..e12dbfa --- /dev/null +++ b/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml @@ -0,0 +1 @@ +'MyType class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..4b6d226 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'ExampleDirective' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..4b6d226 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'ExampleDirective' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml new file mode 100644 index 0000000..ccef661 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml @@ -0,0 +1,2 @@ +ExampleDirective class was defined without __visitor__ attribute +... diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml new file mode 100644 index 0000000..ccef661 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml @@ -0,0 +1,2 @@ +ExampleDirective class was defined without __visitor__ attribute +... diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..f43a447 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +ExampleDirective class was defined with __schema__ without GraphQL directive +... diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..f43a447 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +ExampleDirective class was defined with __schema__ without GraphQL directive +... diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..2b09463 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'directivo'.\n\nGraphQL request:1:1\n1 | directivo\ + \ @example on FIELD_DEFINITION\n | ^" diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..2b09463 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'directivo'.\n\nGraphQL request:1:1\n1 | directivo\ + \ @example on FIELD_DEFINITION\n | ^" diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..b287e70 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'ExampleDirective class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..b287e70 --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'ExampleDirective class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..417c07b --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'ExampleDirective class was defined with __schema__ containing more than one GraphQL + definition (found: DirectiveDefinitionNode, DirectiveDefinitionNode)' diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..417c07b --- /dev/null +++ b/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'ExampleDirective class was defined with __schema__ containing more than one GraphQL + definition (found: DirectiveDefinitionNode, DirectiveDefinitionNode)' diff --git a/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..3a7b959 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'UserRoleEnum' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..3a7b959 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'UserRoleEnum' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..bc4de25 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +UserRoleEnum class was defined with __schema__ without GraphQL enum +... diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..bc4de25 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +UserRoleEnum class was defined with __schema__ without GraphQL enum +... diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..c01e216 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'enom'.\n\nGraphQL request:1:1\n1 | enom UserRole\n\ + \ | ^" diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..c01e216 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'enom'.\n\nGraphQL request:1:1\n1 | enom UserRole\n\ + \ | ^" diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..6ee07b7 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'UserRoleEnum class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..6ee07b7 --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'UserRoleEnum class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..f513b5e --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __schema__ containing more than one GraphQL definition + (found: EnumTypeDefinitionNode, EnumTypeDefinitionNode)' diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..f513b5e --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __schema__ containing more than one GraphQL definition + (found: EnumTypeDefinitionNode, EnumTypeDefinitionNode)' diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml new file mode 100644 index 0000000..e71b37e --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ containing extra items missing in GraphQL + definition: REVIEW' diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml new file mode 100644 index 0000000..e71b37e --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ containing extra items missing in GraphQL + definition: REVIEW' diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml new file mode 100644 index 0000000..afa4a5a --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ missing following items required by + GraphQL definition: MOD' diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml new file mode 100644 index 0000000..afa4a5a --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ missing following items required by + GraphQL definition: MOD' diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml new file mode 100644 index 0000000..e71b37e --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ containing extra items missing in GraphQL + definition: REVIEW' diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml new file mode 100644 index 0000000..e71b37e --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ containing extra items missing in GraphQL + definition: REVIEW' diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml new file mode 100644 index 0000000..afa4a5a --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ missing following items required by + GraphQL definition: MOD' diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml new file mode 100644 index 0000000..afa4a5a --- /dev/null +++ b/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml @@ -0,0 +1,2 @@ +'UserRoleEnum class was defined with __enum__ missing following items required by + GraphQL definition: MOD' diff --git a/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml b/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml new file mode 100644 index 0000000..15490e9 --- /dev/null +++ b/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml @@ -0,0 +1 @@ +'Multiple Query types are defining same field ''city'': CityQueryType, YearQueryType' diff --git a/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml b/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml new file mode 100644 index 0000000..15490e9 --- /dev/null +++ b/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml @@ -0,0 +1 @@ +'Multiple Query types are defining same field ''city'': CityQueryType, YearQueryType' diff --git a/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..e789b2a --- /dev/null +++ b/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'UserInput' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..e789b2a --- /dev/null +++ b/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'UserInput' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..e522523 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml @@ -0,0 +1 @@ +'UserInput class was defined with args for fields not in GraphQL input: fullName' diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml new file mode 100644 index 0000000..e522523 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml @@ -0,0 +1 @@ +'UserInput class was defined with args for fields not in GraphQL input: fullName' diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..755ecaa --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +UserInput class was defined with __schema__ without GraphQL input +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..755ecaa --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +UserInput class was defined with __schema__ without GraphQL input +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..b0e2a72 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'inpet'.\n\nGraphQL request:1:1\n1 | inpet UserInput\n\ + \ | ^" diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..b0e2a72 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'inpet'.\n\nGraphQL request:1:1\n1 | inpet UserInput\n\ + \ | ^" diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..4b5db00 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'UserInput class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..4b5db00 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'UserInput class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..77630d7 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'UserInput class was defined with __schema__ containing more than one GraphQL definition + (found: InputObjectTypeDefinitionNode, InputObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..77630d7 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'UserInput class was defined with __schema__ containing more than one GraphQL definition + (found: InputObjectTypeDefinitionNode, InputObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml new file mode 100644 index 0000000..5140f86 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml @@ -0,0 +1,3 @@ +ExtendUserInput graphql type was defined without required GraphQL type definition + for 'User' in __requires__ +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml b/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml new file mode 100644 index 0000000..5140f86 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml @@ -0,0 +1,3 @@ +ExtendUserInput graphql type was defined without required GraphQL type definition + for 'User' in __requires__ +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml new file mode 100644 index 0000000..97a9adf --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml @@ -0,0 +1,2 @@ +UserInput class was defined without required GraphQL definition for 'Role' in __requires__ +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml b/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml new file mode 100644 index 0000000..97a9adf --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml @@ -0,0 +1,2 @@ +UserInput class was defined without required GraphQL definition for 'Role' in __requires__ +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml new file mode 100644 index 0000000..3959a6b --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml @@ -0,0 +1,2 @@ +UserInput class was defined with __schema__ containing empty GraphQL input definition +... diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.yml b/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.yml new file mode 100644 index 0000000..3959a6b --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.yml @@ -0,0 +1,2 @@ +UserInput class was defined with __schema__ containing empty GraphQL input definition +... diff --git a/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml new file mode 100644 index 0000000..258b184 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml @@ -0,0 +1,3 @@ +ExtendUserInput requires 'User' to be GraphQL input but other type was provided in + '__requires__' +... diff --git a/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml new file mode 100644 index 0000000..258b184 --- /dev/null +++ b/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml @@ -0,0 +1,3 @@ +ExtendUserInput requires 'User' to be GraphQL input but other type was provided in + '__requires__' +... diff --git a/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..41480da --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'ExampleInterface' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..41480da --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'ExampleInterface' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..00161da --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml @@ -0,0 +1 @@ +'ExampleInterface class was defined with aliases for fields not in GraphQL type: joinedDate' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml new file mode 100644 index 0000000..00161da --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml @@ -0,0 +1 @@ +'ExampleInterface class was defined with aliases for fields not in GraphQL type: joinedDate' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..c03787f --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +ExampleInterface class was defined with __schema__ without GraphQL interface +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..c03787f --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +ExampleInterface class was defined with __schema__ without GraphQL interface +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..8217a60 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'interfaco'.\n\nGraphQL request:1:1\n1 | interfaco\ + \ Example\n | ^" diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..8217a60 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'interfaco'.\n\nGraphQL request:1:1\n1 | interfaco\ + \ Example\n | ^" diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..6efffd8 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'ExampleInterface class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..6efffd8 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'ExampleInterface class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..c4a1d3d --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'ExampleInterface class was defined with __schema__ containing more than one GraphQL + definition (found: InterfaceTypeDefinitionNode, InterfaceTypeDefinitionNode)' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..c4a1d3d --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'ExampleInterface class was defined with __schema__ containing more than one GraphQL + definition (found: InterfaceTypeDefinitionNode, InterfaceTypeDefinitionNode)' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..4b67ef2 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml @@ -0,0 +1,2 @@ +'ExampleInterface class was defined with resolvers for fields not in GraphQL type: + resolve_group' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml new file mode 100644 index 0000000..4b67ef2 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml @@ -0,0 +1,2 @@ +'ExampleInterface class was defined with resolvers for fields not in GraphQL type: + resolve_group' diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml new file mode 100644 index 0000000..c70e6fa --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml @@ -0,0 +1,3 @@ +ExampleInterface class was defined without required GraphQL definition for 'UserInput' + in __requires__ +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml new file mode 100644 index 0000000..c70e6fa --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml @@ -0,0 +1,3 @@ +ExampleInterface class was defined without required GraphQL definition for 'UserInput' + in __requires__ +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml new file mode 100644 index 0000000..8c4dc90 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml @@ -0,0 +1,2 @@ +ExtendExampleInterface class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml new file mode 100644 index 0000000..8c4dc90 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml @@ -0,0 +1,2 @@ +ExtendExampleInterface class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml new file mode 100644 index 0000000..5e0e878 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml @@ -0,0 +1,3 @@ +ExampleInterface class was defined with __schema__ containing empty GraphQL interface + definition +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml new file mode 100644 index 0000000..5e0e878 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml @@ -0,0 +1,3 @@ +ExampleInterface class was defined with __schema__ containing empty GraphQL interface + definition +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml new file mode 100644 index 0000000..8503826 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml @@ -0,0 +1,3 @@ +ExampleInterface class was defined without required GraphQL definition for 'Group' + in __requires__ +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml b/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml new file mode 100644 index 0000000..8503826 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml @@ -0,0 +1,3 @@ +ExampleInterface class was defined without required GraphQL definition for 'Group' + in __requires__ +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml new file mode 100644 index 0000000..0b64116 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml @@ -0,0 +1,3 @@ +ExampleInterface requires 'Example' to be GraphQL interface but other type was provided + in '__requires__' +... diff --git a/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml new file mode 100644 index 0000000..0b64116 --- /dev/null +++ b/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml @@ -0,0 +1,3 @@ +ExampleInterface requires 'Example' to be GraphQL interface but other type was provided + in '__requires__' +... diff --git a/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..c33b88b --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'UserCreateMutation' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..c33b88b --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'UserCreateMutation' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml new file mode 100644 index 0000000..c529e91 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined with __schema__ containing GraphQL definition + for 'type User' while 'type Mutation' was expected +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml new file mode 100644 index 0000000..c529e91 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined with __schema__ containing GraphQL definition + for 'type User' while 'type Mutation' was expected +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..04cd779 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +UserCreateMutation class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..04cd779 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +UserCreateMutation class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..769faab --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'UserCreateMutation class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..769faab --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'UserCreateMutation class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml new file mode 100644 index 0000000..1b4c4d3 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml @@ -0,0 +1,3 @@ +UserCreateMutation class subclasses 'MutationType' class which requires __schema__ + to define exactly one field +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml new file mode 100644 index 0000000..1b4c4d3 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml @@ -0,0 +1,3 @@ +UserCreateMutation class subclasses 'MutationType' class which requires __schema__ + to define exactly one field +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..686b110 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'UserCreateMutation class was defined with __schema__ containing more than one GraphQL + definition (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..686b110 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'UserCreateMutation class was defined with __schema__ containing more than one GraphQL + definition (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml new file mode 100644 index 0000000..141c105 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml @@ -0,0 +1,2 @@ +'UserCreateMutation class was defined with args not on ''userCreate'' GraphQL field: + realName' diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml new file mode 100644 index 0000000..141c105 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml @@ -0,0 +1,2 @@ +'UserCreateMutation class was defined with args not on ''userCreate'' GraphQL field: + realName' diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml new file mode 100644 index 0000000..52d3d13 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined with attribute 'resolve_mutation' but it's not + callable +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml new file mode 100644 index 0000000..52d3d13 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined with attribute 'resolve_mutation' but it's not + callable +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml new file mode 100644 index 0000000..b5d09d3 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined with __schema__ containing empty GraphQL type + definition +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml new file mode 100644 index 0000000..b5d09d3 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined with __schema__ containing empty GraphQL type + definition +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml new file mode 100644 index 0000000..ce96568 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml @@ -0,0 +1,2 @@ +UserCreateMutation class was defined without required 'resolve_mutation' attribute +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml new file mode 100644 index 0000000..ce96568 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml @@ -0,0 +1,2 @@ +UserCreateMutation class was defined without required 'resolve_mutation' attribute +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml new file mode 100644 index 0000000..bcb3e21 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined without required GraphQL definition for 'UserCreateResult' + in __requires__ +... diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml new file mode 100644 index 0000000..bcb3e21 --- /dev/null +++ b/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml @@ -0,0 +1,3 @@ +UserCreateMutation class was defined without required GraphQL definition for 'UserCreateResult' + in __requires__ +... diff --git a/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..ed1a122 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'UserType' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..ed1a122 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'UserType' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..a8109e2 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml @@ -0,0 +1 @@ +'UserType class was defined with aliases for fields not in GraphQL type: joinedDate' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml new file mode 100644 index 0000000..a8109e2 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml @@ -0,0 +1 @@ +'UserType class was defined with aliases for fields not in GraphQL type: joinedDate' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml new file mode 100644 index 0000000..52b6f4e --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml @@ -0,0 +1 @@ +'UserType class was defined with args mappings not in not in ''name'' field: arg' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml new file mode 100644 index 0000000..52b6f4e --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml @@ -0,0 +1 @@ +'UserType class was defined with args mappings not in not in ''name'' field: arg' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..db29360 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml @@ -0,0 +1,2 @@ +'UserType class was defined with fields args mappings for fields not in GraphQL type: + group' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml new file mode 100644 index 0000000..db29360 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml @@ -0,0 +1,2 @@ +'UserType class was defined with fields args mappings for fields not in GraphQL type: + group' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..8d5f64d --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +UserType class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..8d5f64d --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +UserType class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..d16bce5 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo User\n |\ + \ ^" diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..d16bce5 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo User\n |\ + \ ^" diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..a32b7b5 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'UserType class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..a32b7b5 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'UserType class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..54101dd --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'UserType class was defined with __schema__ containing more than one GraphQL definition + (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..54101dd --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'UserType class was defined with __schema__ containing more than one GraphQL definition + (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..59ac07b --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml @@ -0,0 +1 @@ +'UserType class was defined with resolvers for fields not in GraphQL type: resolve_group' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml b/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml new file mode 100644 index 0000000..59ac07b --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml @@ -0,0 +1 @@ +'UserType class was defined with resolvers for fields not in GraphQL type: resolve_group' diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml new file mode 100644 index 0000000..a26f534 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml @@ -0,0 +1,3 @@ +UserType class was defined without required GraphQL definition for 'UserInput' in + __requires__ +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml new file mode 100644 index 0000000..a26f534 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml @@ -0,0 +1,3 @@ +UserType class was defined without required GraphQL definition for 'UserInput' in + __requires__ +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml new file mode 100644 index 0000000..9b74478 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml @@ -0,0 +1,3 @@ +ExtendUserType graphql type was defined without required GraphQL type definition for + 'User' in __requires__ +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml new file mode 100644 index 0000000..9b74478 --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml @@ -0,0 +1,3 @@ +ExtendUserType graphql type was defined without required GraphQL type definition for + 'User' in __requires__ +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml new file mode 100644 index 0000000..d1ef4ac --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml @@ -0,0 +1,2 @@ +UserType class was defined with __schema__ containing empty GraphQL type definition +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.yml new file mode 100644 index 0000000..d1ef4ac --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.yml @@ -0,0 +1,2 @@ +UserType class was defined with __schema__ containing empty GraphQL type definition +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml new file mode 100644 index 0000000..19d2bbe --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml @@ -0,0 +1,2 @@ +UserType class was defined without required GraphQL definition for 'Group' in __requires__ +... diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml b/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml new file mode 100644 index 0000000..19d2bbe --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml @@ -0,0 +1,2 @@ +UserType class was defined without required GraphQL definition for 'Group' in __requires__ +... diff --git a/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml new file mode 100644 index 0000000..5222a0e --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml @@ -0,0 +1,2 @@ +ExampleType requires 'Example' to be GraphQL type but other type was provided in '__requires__' +... diff --git a/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml new file mode 100644 index 0000000..5222a0e --- /dev/null +++ b/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml @@ -0,0 +1,2 @@ +ExampleType requires 'Example' to be GraphQL type but other type was provided in '__requires__' +... diff --git a/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..343ade4 --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'DateScalar' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..343ade4 --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'DateScalar' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..1090fab --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +DateScalar class was defined with __schema__ without GraphQL scalar +... diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..1090fab --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +DateScalar class was defined with __schema__ without GraphQL scalar +... diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..8cb39c7 --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'scalor'.\n\nGraphQL request:1:1\n1 | scalor Date\n\ + \ | ^" diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..8cb39c7 --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'scalor'.\n\nGraphQL request:1:1\n1 | scalor Date\n\ + \ | ^" diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..b55ebcd --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'DateScalar class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..b55ebcd --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'DateScalar class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..1775123 --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'DateScalar class was defined with __schema__ containing more than one GraphQL definition + (found: ScalarTypeDefinitionNode, ScalarTypeDefinitionNode)' diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..1775123 --- /dev/null +++ b/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'DateScalar class was defined with __schema__ containing more than one GraphQL definition + (found: ScalarTypeDefinitionNode, ScalarTypeDefinitionNode)' diff --git a/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..8f88a6f --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'UsersSubscription' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..8f88a6f --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'UsersSubscription' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..d0e6232 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml @@ -0,0 +1 @@ +'ChatSubscription class was defined with aliases for fields not in GraphQL type: userAlerts' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml new file mode 100644 index 0000000..d0e6232 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml @@ -0,0 +1 @@ +'ChatSubscription class was defined with aliases for fields not in GraphQL type: userAlerts' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml new file mode 100644 index 0000000..c7df1c7 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml @@ -0,0 +1,3 @@ +UsersSubscription class was defined with __schema__ containing GraphQL definition + for 'type Other' (expected 'type Subscription') +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml new file mode 100644 index 0000000..c7df1c7 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml @@ -0,0 +1,3 @@ +UsersSubscription class was defined with __schema__ containing GraphQL definition + for 'type Other' (expected 'type Subscription') +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..5075396 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +UsersSubscription class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..5075396 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +UsersSubscription class was defined with __schema__ without GraphQL type +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..218344a --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo Subscription\n\ + \ | ^" diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..218344a --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo Subscription\n\ + \ | ^" diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..18f9545 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'UsersSubscription class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..18f9545 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'UsersSubscription class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..9bdf884 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml @@ -0,0 +1,2 @@ +'ChatSubscription class was defined with resolvers for fields not in GraphQL type: + resolve_group' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml new file mode 100644 index 0000000..9bdf884 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml @@ -0,0 +1,2 @@ +'ChatSubscription class was defined with resolvers for fields not in GraphQL type: + resolve_group' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml new file mode 100644 index 0000000..d8dd479 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml @@ -0,0 +1,2 @@ +'ChatSubscription class was defined with subscribers for fields not in GraphQL type: + resolve_group' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml new file mode 100644 index 0000000..d8dd479 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml @@ -0,0 +1,2 @@ +'ChatSubscription class was defined with subscribers for fields not in GraphQL type: + resolve_group' diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml new file mode 100644 index 0000000..822b19a --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml @@ -0,0 +1,3 @@ +ChatSubscription class was defined without required GraphQL definition for 'ChannelInput' + in __requires__ +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml new file mode 100644 index 0000000..822b19a --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml @@ -0,0 +1,3 @@ +ChatSubscription class was defined without required GraphQL definition for 'ChannelInput' + in __requires__ +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml new file mode 100644 index 0000000..cd1dd29 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml @@ -0,0 +1,3 @@ +ExtendChatSubscription graphql type was defined without required GraphQL type definition + for 'Subscription' in __requires__ +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml new file mode 100644 index 0000000..cd1dd29 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml @@ -0,0 +1,3 @@ +ExtendChatSubscription graphql type was defined without required GraphQL type definition + for 'Subscription' in __requires__ +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml new file mode 100644 index 0000000..aba35e1 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml @@ -0,0 +1,3 @@ +UsersSubscription class was defined with __schema__ containing empty GraphQL type + definition +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml new file mode 100644 index 0000000..aba35e1 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml @@ -0,0 +1,3 @@ +UsersSubscription class was defined with __schema__ containing empty GraphQL type + definition +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml new file mode 100644 index 0000000..2ed5d80 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml @@ -0,0 +1,3 @@ +ChatSubscription class was defined without required GraphQL definition for 'Chat' + in __requires__ +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml new file mode 100644 index 0000000..2ed5d80 --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml @@ -0,0 +1,3 @@ +ChatSubscription class was defined without required GraphQL definition for 'Chat' + in __requires__ +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml new file mode 100644 index 0000000..20aa4af --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml @@ -0,0 +1,3 @@ +ExtendChatSubscription requires 'Subscription' to be GraphQL type but other type was + provided in '__requires__' +... diff --git a/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml new file mode 100644 index 0000000..20aa4af --- /dev/null +++ b/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml @@ -0,0 +1,3 @@ +ExtendChatSubscription requires 'Subscription' to be GraphQL type but other type was + provided in '__requires__' +... diff --git a/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml new file mode 100644 index 0000000..dc8ba31 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml @@ -0,0 +1,2 @@ +type object 'ExampleUnion' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml b/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml new file mode 100644 index 0000000..dc8ba31 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml @@ -0,0 +1,2 @@ +type object 'ExampleUnion' has no attribute '__schema__' +... diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml new file mode 100644 index 0000000..7925073 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml @@ -0,0 +1,2 @@ +ExampleUnion class was defined with __schema__ without GraphQL union +... diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml new file mode 100644 index 0000000..7925073 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml @@ -0,0 +1,2 @@ +ExampleUnion class was defined with __schema__ without GraphQL union +... diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml new file mode 100644 index 0000000..5bf1156 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'unien'.\n\nGraphQL request:1:1\n1 | unien Example\ + \ = A | B\n | ^" diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml new file mode 100644 index 0000000..5bf1156 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml @@ -0,0 +1,2 @@ +"Syntax Error: Unexpected Name 'unien'.\n\nGraphQL request:1:1\n1 | unien Example\ + \ = A | B\n | ^" diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml new file mode 100644 index 0000000..6213798 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml @@ -0,0 +1 @@ +'ExampleUnion class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml new file mode 100644 index 0000000..6213798 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml @@ -0,0 +1 @@ +'ExampleUnion class was defined with __schema__ of invalid type: bool' diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml new file mode 100644 index 0000000..0613370 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml @@ -0,0 +1,2 @@ +'ExampleUnion class was defined with __schema__ containing more than one GraphQL definition + (found: UnionTypeDefinitionNode, UnionTypeDefinitionNode)' diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml new file mode 100644 index 0000000..0613370 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml @@ -0,0 +1,2 @@ +'ExampleUnion class was defined with __schema__ containing more than one GraphQL definition + (found: UnionTypeDefinitionNode, UnionTypeDefinitionNode)' diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml new file mode 100644 index 0000000..db77044 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml @@ -0,0 +1,3 @@ +ExtendExampleUnion class was defined without required GraphQL union definition for + 'Result' in __requires__ +... diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml b/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml new file mode 100644 index 0000000..db77044 --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml @@ -0,0 +1,3 @@ +ExtendExampleUnion class was defined without required GraphQL union definition for + 'Result' in __requires__ +... diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml b/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml new file mode 100644 index 0000000..b4673df --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml @@ -0,0 +1,3 @@ +ExampleUnion class was defined without required GraphQL definition for 'Comment' in + __requires__ +... diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml b/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml new file mode 100644 index 0000000..b4673df --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml @@ -0,0 +1,3 @@ +ExampleUnion class was defined without required GraphQL definition for 'Comment' in + __requires__ +... diff --git a/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml new file mode 100644 index 0000000..0ce118e --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml @@ -0,0 +1,3 @@ +ExtendExampleUnion requires 'Example' to be GraphQL union but other type was provided + in '__requires__' +... diff --git a/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml new file mode 100644 index 0000000..0ce118e --- /dev/null +++ b/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml @@ -0,0 +1,3 @@ +ExtendExampleUnion requires 'Example' to be GraphQL union but other type was provided + in '__requires__' +... diff --git a/tests/test_definition_parser.py b/tests/test_definition_parser.py index c5e49d3..ab5ab43 100644 --- a/tests/test_definition_parser.py +++ b/tests/test_definition_parser.py @@ -34,21 +34,25 @@ def test_definition_parser_parses_definition_with_description(): assert type_def.description.value == "Test user type" -def test_definition_parser_raises_error_when_schema_type_is_invalid(snapshot): +def test_definition_parser_raises_error_when_schema_type_is_invalid(data_regression): with pytest.raises(TypeError) as err: parse_definition("MyType", True) - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_definition_parser_raises_error_when_schema_str_has_invalid_syntax(snapshot): +def test_definition_parser_raises_error_when_schema_str_has_invalid_syntax( + data_regression, +): with pytest.raises(GraphQLError) as err: parse_definition("MyType", "typo User") - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_definition_parser_raises_error_schema_str_contains_multiple_types(snapshot): +def test_definition_parser_raises_error_schema_str_contains_multiple_types( + data_regression, +): with pytest.raises(ValueError) as err: parse_definition( "MyType", @@ -59,4 +63,4 @@ def test_definition_parser_raises_error_schema_str_contains_multiple_types(snaps """, ) - snapshot.assert_match(err) + data_regression.check(str(err.value)) diff --git a/tests/test_directive_type.py b/tests/test_directive_type.py index fcd3846..15116f1 100644 --- a/tests/test_directive_type.py +++ b/tests/test_directive_type.py @@ -5,45 +5,53 @@ from ariadne_graphql_modules import DirectiveType, ObjectType, make_executable_schema -def test_directive_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_directive_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class ExampleDirective(DirectiveType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_directive_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_directive_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class ExampleDirective(DirectiveType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_directive_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_directive_type_raises_error_when_defined_with_invalid_schema_str( + data_regression, +): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class ExampleDirective(DirectiveType): __schema__ = "directivo @example on FIELD_DEFINITION" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleDirective(DirectiveType): __schema__ = "scalar example" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_directive_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_directive_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleDirective(DirectiveType): @@ -53,16 +61,18 @@ class ExampleDirective(DirectiveType): directive @other on OBJECT """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_directive_type_raises_attribute_error_when_defined_without_visitor(snapshot): +def test_directive_type_raises_attribute_error_when_defined_without_visitor( + data_regression, +): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class ExampleDirective(DirectiveType): __schema__ = "directive @example on FIELD_DEFINITION" - snapshot.assert_match(err) + data_regression.check(str(err.value)) class ExampleSchemaVisitor(SchemaDirectiveVisitor): diff --git a/tests/test_enum_type.py b/tests/test_enum_type.py index 730eb8c..7ea4585 100644 --- a/tests/test_enum_type.py +++ b/tests/test_enum_type.py @@ -12,45 +12,47 @@ ) -def test_enum_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_enum_type_raises_attribute_error_when_defined_without_schema(data_regression): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class UserRoleEnum(EnumType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_enum_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_enum_type_raises_error_when_defined_with_invalid_schema_type(data_regression): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class UserRoleEnum(EnumType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_enum_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_enum_type_raises_error_when_defined_with_invalid_schema_str(data_regression): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class UserRoleEnum(EnumType): __schema__ = "enom UserRole" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserRoleEnum(EnumType): __schema__ = "scalar UserRole" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_enum_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_enum_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserRoleEnum(EnumType): @@ -67,7 +69,7 @@ class UserRoleEnum(EnumType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_enum_type_extracts_graphql_name(): @@ -178,7 +180,7 @@ class UserRoleEnum(EnumType): def test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -196,11 +198,11 @@ class UserRoleEnum(EnumType): "ADMIN": 2, } - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -219,7 +221,7 @@ class UserRoleEnum(EnumType): "ADMIN": 3, } - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_enum_type_can_be_defined_with_str_enum_mapping(): @@ -256,7 +258,7 @@ class UserRoleEnum(EnumType): def test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition( - snapshot, + data_regression, ): class RoleEnum(str, Enum): USER = "user" @@ -275,11 +277,11 @@ class UserRoleEnum(EnumType): """ __enum__ = RoleEnum - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition( - snapshot, + data_regression, ): class RoleEnum(str, Enum): USER = "user" @@ -299,4 +301,4 @@ class UserRoleEnum(EnumType): """ __enum__ = RoleEnum - snapshot.assert_match(err) + data_regression.check(str(err.value)) diff --git a/tests/test_executable_schema.py b/tests/test_executable_schema.py index f5dfe13..8a59ed3 100644 --- a/tests/test_executable_schema.py +++ b/tests/test_executable_schema.py @@ -67,7 +67,7 @@ def resolve_year(*_): def test_executable_schema_raises_value_error_if_merged_types_define_same_field( - snapshot, + data_regression, ): class CityQueryType(ObjectType): __schema__ = """ @@ -87,4 +87,4 @@ class YearQueryType(ObjectType): with pytest.raises(ValueError) as err: make_executable_schema(CityQueryType, YearQueryType) - snapshot.assert_match(err) + data_regression.check(str(err.value)) diff --git a/tests/test_input_type.py b/tests/test_input_type.py index ded8d94..daad5e0 100644 --- a/tests/test_input_type.py +++ b/tests/test_input_type.py @@ -14,35 +14,35 @@ ) -def test_input_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_input_type_raises_attribute_error_when_defined_without_schema(data_regression): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class UserInput(InputType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_input_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_input_type_raises_error_when_defined_with_invalid_schema_type(data_regression): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class UserInput(InputType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_input_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_input_type_raises_error_when_defined_with_invalid_schema_str(data_regression): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class UserInput(InputType): __schema__ = "inpet UserInput" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -53,10 +53,12 @@ class UserInput(InputType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_input_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_input_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserInput(InputType): @@ -66,16 +68,16 @@ class UserInput(InputType): input Group """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_input_type_raises_error_when_defined_without_fields(snapshot): +def test_input_type_raises_error_when_defined_without_fields(data_regression): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserInput(InputType): __schema__ = "input User" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_input_type_extracts_graphql_name(): @@ -89,7 +91,9 @@ class UserInput(InputType): assert UserInput.graphql_name == "User" -def test_input_type_raises_error_when_defined_without_field_type_dependency(snapshot): +def test_input_type_raises_error_when_defined_without_field_type_dependency( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserInput(InputType): @@ -100,7 +104,7 @@ class UserInput(InputType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_input_type_verifies_field_dependency(): @@ -193,7 +197,9 @@ class ExtendUserInput(InputType): __requires__ = [UserInput, ExampleDirective] -def test_input_type_raises_error_when_defined_without_extended_dependency(snapshot): +def test_input_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExtendUserInput(InputType): @@ -203,10 +209,12 @@ class ExtendUserInput(InputType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_input_type_raises_error_when_extended_dependency_is_wrong_type(snapshot): +def test_input_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): @@ -224,11 +232,11 @@ class ExtendUserInput(InputType): """ __requires__ = [ExampleInterface] - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -242,7 +250,7 @@ class UserInput(InputType): "fullName": "full_name", } - snapshot.assert_match(err) + data_regression.check(str(err.value)) class UserInput(InputType): diff --git a/tests/test_interface_type.py b/tests/test_interface_type.py index 4a064f4..be3637d 100644 --- a/tests/test_interface_type.py +++ b/tests/test_interface_type.py @@ -13,45 +13,53 @@ ) -def test_interface_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_interface_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_interface_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_interface_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_interface_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_interface_type_raises_error_when_defined_with_invalid_schema_str( + data_regression, +): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): __schema__ = "interfaco Example" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): __schema__ = "type Example" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_interface_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_interface_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): @@ -61,16 +69,16 @@ class ExampleInterface(InterfaceType): interface Other """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_interface_type_raises_error_when_defined_without_fields(snapshot): +def test_interface_type_raises_error_when_defined_without_fields(data_regression): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): __schema__ = "interface Example" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_interface_type_extracts_graphql_name(): @@ -85,7 +93,7 @@ class ExampleInterface(InterfaceType): def test_interface_type_raises_error_when_defined_without_return_type_dependency( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -97,7 +105,7 @@ class ExampleInterface(InterfaceType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_interface_type_verifies_field_dependency(): @@ -130,7 +138,7 @@ class ExampleInterface(InterfaceType): def test_interface_type_raises_error_when_defined_without_argument_type_dependency( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -141,7 +149,7 @@ class ExampleInterface(InterfaceType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_interface_type_verifies_circular_dependency_using_deferred_type(): @@ -225,7 +233,9 @@ class ExtendExampleInterface(InterfaceType): __requires__ = [ExampleInterface, OtherInterface] -def test_interface_type_raises_error_when_defined_without_extended_dependency(snapshot): +def test_interface_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExtendExampleInterface(ObjectType): @@ -235,10 +245,12 @@ class ExtendExampleInterface(ObjectType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_interface_type_raises_error_when_extended_dependency_is_wrong_type(snapshot): +def test_interface_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleType(ObjectType): @@ -256,11 +268,11 @@ class ExampleInterface(InterfaceType): """ __requires__ = [ExampleType] - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -274,11 +286,11 @@ class ExampleInterface(InterfaceType): "joinedDate": "joined_date", } - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -293,7 +305,7 @@ class ExampleInterface(InterfaceType): def resolve_group(*_): return None - snapshot.assert_match(err) + data_regression.check(str(err.value)) @dataclass diff --git a/tests/test_mutation_type.py b/tests/test_mutation_type.py index 2b73477..8b03c9e 100644 --- a/tests/test_mutation_type.py +++ b/tests/test_mutation_type.py @@ -8,45 +8,51 @@ ) -def test_mutation_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_mutation_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_mutation_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_mutation_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_object_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_object_type_raises_error_when_defined_with_invalid_schema_str(data_regression): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): __schema__ = "typo User" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): __schema__ = "scalar DateTime" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_mutation_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_mutation_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): @@ -56,10 +62,12 @@ class UserCreateMutation(MutationType): type Group """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_mutation_type_raises_error_when_defined_for_different_type_name(snapshot): +def test_mutation_type_raises_error_when_defined_for_different_type_name( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): @@ -69,10 +77,10 @@ class UserCreateMutation(MutationType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_mutation_type_raises_error_when_defined_without_fields(snapshot): +def test_mutation_type_raises_error_when_defined_without_fields(data_regression): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): @@ -80,10 +88,10 @@ class UserCreateMutation(MutationType): type Mutation """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_mutation_type_raises_error_when_defined_with_multiple_fields(snapshot): +def test_mutation_type_raises_error_when_defined_with_multiple_fields(data_regression): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserCreateMutation(MutationType): @@ -94,11 +102,11 @@ class UserCreateMutation(MutationType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr( - snapshot, + data_regression, ): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable @@ -109,11 +117,11 @@ class UserCreateMutation(MutationType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr( - snapshot, + data_regression, ): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable @@ -126,11 +134,11 @@ class UserCreateMutation(MutationType): resolve_mutation = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_mutation_type_raises_error_when_defined_without_return_type_dependency( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -145,7 +153,7 @@ class UserCreateMutation(MutationType): def resolve_mutation(*_args): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_mutation_type_verifies_field_dependency(): @@ -171,7 +179,7 @@ def resolve_mutation(*_args): def test_mutation_type_raises_error_when_defined_with_nonexistant_args( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -187,7 +195,7 @@ class UserCreateMutation(MutationType): def resolve_mutation(*_args): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) class QueryType(ObjectType): diff --git a/tests/test_object_type.py b/tests/test_object_type.py index b9ca048..8adf0e6 100644 --- a/tests/test_object_type.py +++ b/tests/test_object_type.py @@ -11,45 +11,51 @@ ) -def test_object_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_object_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class UserType(ObjectType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_object_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_object_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class UserType(ObjectType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_object_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_object_type_raises_error_when_defined_with_invalid_schema_str(data_regression): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class UserType(ObjectType): __schema__ = "typo User" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserType(ObjectType): __schema__ = "scalar DateTime" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_object_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_object_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserType(ObjectType): @@ -59,16 +65,16 @@ class UserType(ObjectType): type Group """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_object_type_raises_error_when_defined_without_fields(snapshot): +def test_object_type_raises_error_when_defined_without_fields(data_regression): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserType(ObjectType): __schema__ = "type User" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_extracts_graphql_name(): @@ -96,7 +102,9 @@ class FancyObjectType(ObjectType): """ -def test_object_type_raises_error_when_defined_without_return_type_dependency(snapshot): +def test_object_type_raises_error_when_defined_without_return_type_dependency( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UserType(ObjectType): @@ -107,7 +115,7 @@ class UserType(ObjectType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_verifies_field_dependency(): @@ -140,7 +148,7 @@ class UserType(ObjectType): def test_object_type_raises_error_when_defined_without_argument_type_dependency( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -151,7 +159,7 @@ class UserType(ObjectType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_verifies_circular_dependency_using_deferred_type(): @@ -235,7 +243,9 @@ class ExtendUserType(ObjectType): __requires__ = [UserType, ExampleInterface] -def test_object_type_raises_error_when_defined_without_extended_dependency(snapshot): +def test_object_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExtendUserType(ObjectType): @@ -245,10 +255,12 @@ class ExtendUserType(ObjectType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_object_type_raises_error_when_extended_dependency_is_wrong_type(snapshot): +def test_object_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleInterface(InterfaceType): @@ -266,11 +278,11 @@ class ExampleType(ObjectType): """ __requires__ = [ExampleInterface] - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -284,11 +296,11 @@ class UserType(ObjectType): "joinedDate": "joined_date", } - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -303,11 +315,11 @@ class UserType(ObjectType): def resolve_group(*_): return None - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -319,11 +331,11 @@ class UserType(ObjectType): """ __fields_args__ = {"group": {}} - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -335,7 +347,7 @@ class UserType(ObjectType): """ __fields_args__ = {"name": {"arg": "arg2"}} - snapshot.assert_match(err) + data_regression.check(str(err.value)) class QueryType(ObjectType): diff --git a/tests/test_scalar_type.py b/tests/test_scalar_type.py index ebdc18d..ec85340 100644 --- a/tests/test_scalar_type.py +++ b/tests/test_scalar_type.py @@ -12,45 +12,51 @@ ) -def test_scalar_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_scalar_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class DateScalar(ScalarType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_scalar_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_scalar_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class DateScalar(ScalarType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_scalar_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_scalar_type_raises_error_when_defined_with_invalid_schema_str(data_regression): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class DateScalar(ScalarType): __schema__ = "scalor Date" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class DateScalar(ScalarType): __schema__ = "type DateTime" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_scalar_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_scalar_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class DateScalar(ScalarType): @@ -60,7 +66,7 @@ class DateScalar(ScalarType): scalar DateTime """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_scalar_type_extracts_graphql_name(): diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index d870060..c0067cf 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -10,62 +10,68 @@ ) -def test_subscription_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_subscription_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class UsersSubscription(SubscriptionType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_subscription_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_subscription_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class UsersSubscription(SubscriptionType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_subscription_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_subscription_type_raises_error_when_defined_with_invalid_schema_str( + data_regression, +): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class UsersSubscription(SubscriptionType): __schema__ = "typo Subscription" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UsersSubscription(SubscriptionType): __schema__ = "scalar Subscription" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UsersSubscription(SubscriptionType): __schema__ = "type Other" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_subscription_type_raises_error_when_defined_without_fields(snapshot): +def test_subscription_type_raises_error_when_defined_without_fields(data_regression): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class UsersSubscription(SubscriptionType): __schema__ = "type Subscription" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_extracts_graphql_name(): @@ -80,7 +86,7 @@ class UsersSubscription(SubscriptionType): def test_subscription_type_raises_error_when_defined_without_return_type_dependency( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -92,7 +98,7 @@ class ChatSubscription(SubscriptionType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_verifies_field_dependency(): @@ -115,7 +121,7 @@ class ChatSubscription(SubscriptionType): def test_subscription_type_raises_error_when_defined_without_argument_type_dependency( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -126,7 +132,7 @@ class ChatSubscription(SubscriptionType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_can_be_extended_with_new_fields(): @@ -191,7 +197,7 @@ class ExtendChatSubscription(SubscriptionType): def test_subscription_type_raises_error_when_defined_without_extended_dependency( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -202,11 +208,11 @@ class ExtendChatSubscription(SubscriptionType): } """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_raises_error_when_extended_dependency_is_wrong_type( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -225,11 +231,11 @@ class ExtendChatSubscription(SubscriptionType): """ __requires__ = [ExampleInterface] - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -243,11 +249,11 @@ class ChatSubscription(SubscriptionType): "userAlerts": "user_alerts", } - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -262,11 +268,11 @@ class ChatSubscription(SubscriptionType): def resolve_group(*_): return None - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable @@ -281,7 +287,7 @@ class ChatSubscription(SubscriptionType): def subscribe_group(*_): return None - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_subscription_type_binds_resolver_and_subscriber_to_schema(): diff --git a/tests/test_union_type.py b/tests/test_union_type.py index 5b1f30f..61c8a3a 100644 --- a/tests/test_union_type.py +++ b/tests/test_union_type.py @@ -12,45 +12,47 @@ ) -def test_union_type_raises_attribute_error_when_defined_without_schema(snapshot): +def test_union_type_raises_attribute_error_when_defined_without_schema(data_regression): with pytest.raises(AttributeError) as err: # pylint: disable=unused-variable class ExampleUnion(UnionType): pass - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_union_type_raises_error_when_defined_with_invalid_schema_type(snapshot): +def test_union_type_raises_error_when_defined_with_invalid_schema_type(data_regression): with pytest.raises(TypeError) as err: # pylint: disable=unused-variable class ExampleUnion(UnionType): __schema__ = True - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_union_type_raises_error_when_defined_with_invalid_schema_str(snapshot): +def test_union_type_raises_error_when_defined_with_invalid_schema_str(data_regression): with pytest.raises(GraphQLError) as err: # pylint: disable=unused-variable class ExampleUnion(UnionType): __schema__ = "unien Example = A | B" - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema( - snapshot, + data_regression, ): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleUnion(UnionType): __schema__ = "scalar DateTime" - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_union_type_raises_error_when_defined_with_multiple_types_schema(snapshot): +def test_union_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleUnion(UnionType): @@ -60,7 +62,7 @@ class ExampleUnion(UnionType): union B = C | D """ - snapshot.assert_match(err) + data_regression.check(str(err.value)) @dataclass @@ -135,14 +137,16 @@ class ExampleUnion(UnionType): assert ExampleUnion.graphql_name == "Example" -def test_union_type_raises_error_when_defined_without_member_type_dependency(snapshot): +def test_union_type_raises_error_when_defined_without_member_type_dependency( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleUnion(UnionType): __schema__ = "union Example = User | Comment" __requires__ = [UserType] - snapshot.assert_match(err) + data_regression.check(str(err.value)) def test_interface_type_binds_type_resolver(): @@ -216,17 +220,21 @@ class ExtendExampleUnion(UnionType): __requires__ = [ExampleUnion, ExampleDirective] -def test_union_type_raises_error_when_defined_without_extended_dependency(snapshot): +def test_union_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExtendExampleUnion(UnionType): __schema__ = "extend union Result = User" __requires__ = [UserType] - snapshot.assert_match(err) + data_regression.check(str(err.value)) -def test_interface_type_raises_error_when_extended_dependency_is_wrong_type(snapshot): +def test_union_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): with pytest.raises(ValueError) as err: # pylint: disable=unused-variable class ExampleType(ObjectType): @@ -240,4 +248,4 @@ class ExtendExampleUnion(UnionType): __schema__ = "extend union Example = User" __requires__ = [ExampleType, UserType] - snapshot.assert_match(err) + data_regression.check(str(err.value)) diff --git a/tests_next/test_compatibility_layer.py b/tests_next/test_compatibility_layer.py index 57395f9..320bdc4 100644 --- a/tests_next/test_compatibility_layer.py +++ b/tests_next/test_compatibility_layer.py @@ -358,18 +358,12 @@ def resolve_type(instance, *_): class ChatSubscription(SubscriptionType): __schema__ = """ - type Subscription { + type Subscription implements Interface { chat: ID! - } - """ - - class ExtendChatSubscription(SubscriptionType): - __schema__ = """ - extend type Subscription implements Interface { threads: ID! } """ - __requires__ = [ChatSubscription, ExampleInterface] + __requires__ = [ExampleInterface] class QueryType(ObjectType): __schema__ = """ @@ -382,7 +376,6 @@ class QueryType(ObjectType): QueryType, ExampleInterface, ChatSubscription, - ExtendChatSubscription, ) schema = make_executable_schema(*my_legacy_types) diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py index 69c6bd9..74064ad 100644 --- a/tests_next/test_interface_type.py +++ b/tests_next/test_interface_type.py @@ -315,18 +315,31 @@ class UserType(GraphQLObject): __implements__ = [UserInterface] + class MyType(GraphQLObject): + id: GraphQLID + name: str + + __implements__ = [UserInterface] + class QueryType(GraphQLObject): - @GraphQLObject.field - def user(*_) -> UserType: - return UserType(id="1") + @GraphQLObject.field(graphql_type=List[UserInterface]) + def users(*_) -> List[Union[UserType, MyType]]: + return [MyType(id="2", name="old", summary="ss", score=22)] - schema = make_executable_schema(QueryType, UserType, UserInterface) + schema = make_executable_schema(QueryType, UserType, MyType, UserInterface) assert_schema_equals( schema, """ type Query { - user: User! + users: [UserInterface!]! + } + + interface UserInterface { + summary: String! + + \"\"\"Lorem ipsum.\"\"\" + score: Int! } type User implements UserInterface { @@ -337,15 +350,17 @@ def user(*_) -> UserType: id: ID! } - interface UserInterface { + type My implements UserInterface { summary: String! \"\"\"Lorem ipsum.\"\"\" score: Int! + id: ID! + name: String! } """, ) - result = graphql_sync(schema, "{ user { score } }") + result = graphql_sync(schema, "{ users { ... on My { __typename score } } }") assert not result.errors - assert result.data == {"user": {"score": 200}} + assert result.data == {"users": [{"__typename": "My", "score": 200}]} From d95f2e0103b0763b1336d6ff8dbf2ad2e71d8230 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 22 Aug 2024 15:11:03 +0200 Subject: [PATCH 46/63] restructure folder layout to make it easier to deploy --- .github/workflows/tests.yml | 35 +- ariadne_graphql_modules/__init__.py | 86 +- ariadne_graphql_modules/{next => }/base.py | 2 +- .../base_object_type/__init__.py | 15 + .../graphql_field.py} | 63 +- .../graphql_type.py} | 323 +++-- .../utils.py | 6 +- .../validators.py | 45 +- .../{next => }/compatibility_layer.py | 25 +- .../{next => }/convert_name.py | 0 .../{next => }/deferredtype.py | 0 .../{next => }/description.py | 0 ariadne_graphql_modules/enum_type/__init__.py | 9 + .../graphql_type.py} | 4 +- .../enum_model.py => enum_type/models.py} | 0 ariadne_graphql_modules/enum_type/type.py | 343 +++++ ariadne_graphql_modules/executable_schema.py | 341 ++--- ariadne_graphql_modules/{next => }/idtype.py | 0 .../input_type/__init__.py | 8 + .../graphql_field.py} | 0 .../graphql_type.py} | 8 +- .../input_model.py => input_type/models.py} | 0 .../validators.py | 6 +- .../interface_type/__init__.py | 8 + .../interface_type/graphql_type.py | 147 ++ .../models.py} | 0 ariadne_graphql_modules/next/__init__.py | 58 - .../next/executable_schema.py | 243 ---- .../next/graphql_enum_type/__init__.py | 9 - .../next/graphql_input/__init__.py | 8 - .../next/graphql_interface/__init__.py | 8 - .../next/graphql_interface/interface_type.py | 175 --- .../next/graphql_scalar/__init__.py | 8 - .../next/graphql_subscription/__init__.py | 8 - .../next/graphql_union/__init__.py | 8 - .../__init__.py | 9 +- .../object_type/graphql_type.py | 148 ++ .../object_model.py => object_type/models.py} | 2 +- ariadne_graphql_modules/{next => }/roots.py | 0 .../scalar_type/__init__.py | 8 + .../graphql_type.py} | 4 +- .../scalar_model.py => scalar_type/models.py} | 0 .../validators.py | 4 +- ariadne_graphql_modules/{next => }/sort.py | 0 .../subscription_type/__init__.py | 8 + .../graphql_type.py} | 74 +- .../models.py} | 2 +- ariadne_graphql_modules/types.py | 8 + ariadne_graphql_modules/{next => }/typing.py | 0 .../union_type/__init__.py | 8 + .../graphql_type.py} | 6 +- .../union_model.py => union_type/models.py} | 0 .../validators.py | 4 +- ariadne_graphql_modules/v1/__init__.py | 38 + ariadne_graphql_modules/{ => v1}/bases.py | 2 +- .../{ => v1}/collection_type.py | 0 .../{ => v1}/convert_case.py | 2 +- .../{ => v1}/dependencies.py | 2 +- .../{ => v1}/directive_type.py | 2 +- ariadne_graphql_modules/{ => v1}/enum_type.py | 4 +- .../v1/executable_schema.py | 236 ++++ .../{ => v1}/input_type.py | 4 +- .../{ => v1}/interface_type.py | 4 +- .../{ => v1}/mutation_type.py | 4 +- .../{ => v1}/object_type.py | 4 +- .../{ => v1}/resolvers_mixin.py | 4 +- .../{ => v1}/scalar_type.py | 4 +- .../{ => v1}/subscription_type.py | 0 .../{ => v1}/union_type.py | 4 +- .../{next => }/validators.py | 0 ariadne_graphql_modules/{next => }/value.py | 0 pyproject.toml | 4 +- tests/conftest.py | 28 + ...ription_in_source_with_schema.obtained.yml | 0 ...with_description_in_source_with_schema.yml | 0 ...th_name_in_source_with_schema.obtained.yml | 0 ...st_arg_with_name_in_source_with_schema.yml | 0 ...th_type_in_source_with_schema.obtained.yml | 0 ...st_arg_with_type_in_source_with_schema.yml | 0 ...ror_for_invalid_relative_path.obtained.yml | 0 ...raises_error_for_invalid_relative_path.yml | 0 ...iption_not_str_without_schema.obtained.yml | 0 ...est_description_not_str_without_schema.yml | 0 ...or_type_with_two_descriptions.obtained.yml | 0 ...s_error_for_type_with_two_descriptions.yml | 0 ...ion_fails_for_invalid_members.obtained.yml | 0 ...e_validation_fails_for_invalid_members.yml | 0 ...ion_fails_for_missing_members.obtained.yml | 0 ...e_validation_fails_for_missing_members.yml | 0 ...d_name_not_str_without_schema.obtained.yml | 0 ...test_field_name_not_str_without_schema.yml | 0 ...h_invalid_attrs_raising_error.obtained.yml | 0 ...tance_with_invalid_attrs_raising_error.yml | 0 ..._for_out_names_without_schema.obtained.yml | 0 ...ion_fails_for_out_names_without_schema.yml | 0 ..._for_unsupported_attr_default.obtained.yml | 0 ...ion_fails_for_unsupported_attr_default.yml | 0 ...upported_field_default_option.obtained.yml | 0 ...s_for_unsupported_field_default_option.yml | 0 ..._no_interface_in_schema.obtained.diff.html | 51 + ...erface_no_interface_in_schema.obtained.yml | 0 .../test_interface_no_interface_in_schema.yml | 0 ...ce_with_different_types.obtained.diff.html | 54 + ...nterface_with_different_types.obtained.yml | 2 + .../test_interface_with_different_types.yml | 0 ...rg_name_in_source_with_schema.obtained.yml | 0 ...invalid_arg_name_in_source_with_schema.yml | 0 ...r_if_called_without_any_types.obtained.yml | 0 ...ises_error_if_called_without_any_types.yml | 0 ...ey_error_for_unset_data.obtained.diff.html | 50 + ...ises_key_error_for_unset_data.obtained.yml | 1 + ...tadata_raises_key_error_for_unset_data.yml | 1 + .../test_missing_type_in_schema.obtained.yml | 0 .../snapshots/test_missing_type_in_schema.yml | 0 .../test_missing_type_in_types.obtained.yml | 0 .../snapshots/test_missing_type_in_types.yml | 0 ...ptions_for_source_with_schema.obtained.yml | 0 ...le_descriptions_for_source_with_schema.yml | 0 ...merge_roots_is_disabled.obtained.diff.html | 52 + ...on_if_merge_roots_is_disabled.obtained.yml | 3 + ..._validation_if_merge_roots_is_disabled.yml | 3 + ...sourced_for_field_with_schema.obtained.yml | 0 ...multiple_sourced_for_field_with_schema.yml | 0 ...ltiple_sources_without_schema.obtained.yml | 0 .../test_multiple_sources_without_schema.yml | 0 ..._name_and_definition_mismatch.obtained.yml | 0 ...error_for_name_and_definition_mismatch.yml | 0 ...h_invalid_attrs_raising_error.obtained.yml | 0 ...tance_with_invalid_attrs_raising_error.yml | 0 ...tion_fails_for_alias_resolver.obtained.yml | 0 ...pe_validation_fails_for_alias_resolver.yml | 0 ...ils_for_alias_target_resolver.obtained.yml | 0 ...dation_fails_for_alias_target_resolver.yml | 0 ..._field_with_same_graphql_name.obtained.yml | 0 ..._attr_and_field_with_same_graphql_name.yml | 0 ..._for_field_with_multiple_args.obtained.yml | 0 ...ion_fails_for_field_with_multiple_args.yml | 0 ...ld_with_multiple_descriptions.obtained.yml | 0 ...s_for_field_with_multiple_descriptions.yml | 0 ...ation_fails_for_invalid_alias.obtained.yml | 0 ...ype_validation_fails_for_invalid_alias.yml | 0 ...or_missing_field_resolver_arg.obtained.yml | 0 ...n_fails_for_missing_field_resolver_arg.yml | 0 ...ails_for_missing_resolver_arg.obtained.yml | 0 ...idation_fails_for_missing_resolver_arg.yml | 0 ..._attrs_with_same_graphql_name.obtained.yml | 0 ..._multiple_attrs_with_same_graphql_name.yml | 0 ..._for_multiple_field_resolvers.obtained.yml | 0 ...ion_fails_for_multiple_field_resolvers.yml | 0 ...fields_with_same_graphql_name.obtained.yml | 0 ...multiple_fields_with_same_graphql_name.yml | 0 ...s_for_undefined_attr_resolver.obtained.yml | 0 ...tion_fails_for_undefined_attr_resolver.yml | 0 ..._undefined_field_resolver_arg.obtained.yml | 0 ...fails_for_undefined_field_resolver_arg.yml | 0 ...ls_for_undefined_resolver_arg.obtained.yml | 0 ...ation_fails_for_undefined_resolver_arg.yml | 0 ...upported_resolver_arg_default.obtained.yml | 0 ...s_for_unsupported_resolver_arg_default.yml | 0 ...d_resolver_arg_default_option.obtained.yml | 0 ...nsupported_resolver_arg_default_option.yml | 0 ...plicated_members_descriptions.obtained.yml | 0 ...ls_for_duplicated_members_descriptions.yml | 0 ...lidation_fails_for_empty_enum.obtained.yml | 0 ...m_type_validation_fails_for_empty_enum.yml | 0 ..._invalid_members_descriptions.obtained.yml | 0 ...fails_for_invalid_members_descriptions.yml | 0 ...fails_for_invalid_type_schema.obtained.yml | 0 ...lidation_fails_for_invalid_type_schema.yml | 0 ..._fails_for_names_not_matching.obtained.yml | 0 ...alidation_fails_for_names_not_matching.yml | 0 ...ema_and_members_dict_mismatch.obtained.yml | 0 ...s_for_schema_and_members_dict_mismatch.yml | 0 ...ema_and_members_enum_mismatch.obtained.yml | 0 ...s_for_schema_and_members_enum_mismatch.yml | 0 ...s_for_schema_and_members_list.obtained.yml | 0 ...tion_fails_for_schema_and_members_list.yml | 0 ...on_fails_for_two_descriptions.obtained.yml | 0 ..._validation_fails_for_two_descriptions.yml | 0 ...h_invalid_attrs_raising_error.obtained.yml | 0 ...tance_with_invalid_attrs_raising_error.yml | 0 ..._fails_for_duplicate_out_name.obtained.yml | 0 ...alidation_fails_for_duplicate_out_name.yml | 0 ...on_fails_for_invalid_out_name.obtained.yml | 0 ..._validation_fails_for_invalid_out_name.yml | 0 ...fails_for_invalid_type_schema.obtained.yml | 0 ...lidation_fails_for_invalid_type_schema.yml | 0 ..._fails_for_names_not_matching.obtained.yml | 0 ...alidation_fails_for_names_not_matching.yml | 0 ...ils_for_schema_missing_fields.obtained.yml | 0 ...dation_fails_for_schema_missing_fields.yml | 0 ...on_fails_for_two_descriptions.obtained.yml | 0 ..._validation_fails_for_two_descriptions.yml | 0 ...h_invalid_attrs_raising_error.obtained.yml | 0 ...tance_with_invalid_attrs_raising_error.yml | 0 ...tion_fails_for_alias_resolver.obtained.yml | 0 ...pe_validation_fails_for_alias_resolver.yml | 0 ...ils_for_alias_target_resolver.obtained.yml | 0 ...dation_fails_for_alias_target_resolver.yml | 0 ...r_arg_with_double_description.obtained.yml | 0 ..._fails_for_arg_with_double_description.yml | 0 ...ails_for_arg_with_name_option.obtained.yml | 0 ...idation_fails_for_arg_with_name_option.yml | 0 ...ails_for_arg_with_type_option.obtained.yml | 0 ...idation_fails_for_arg_with_type_option.yml | 0 ...tion_fails_for_field_instance.obtained.yml | 0 ...pe_validation_fails_for_field_instance.yml | 0 ...r_field_with_invalid_arg_name.obtained.yml | 0 ..._fails_for_field_with_invalid_arg_name.yml | 0 ...ld_with_multiple_descriptions.obtained.yml | 0 ...s_for_field_with_multiple_descriptions.yml | 0 ...ation_fails_for_invalid_alias.obtained.yml | 0 ...ype_validation_fails_for_invalid_alias.yml | 0 ...fails_for_invalid_type_schema.obtained.yml | 0 ...lidation_fails_for_invalid_type_schema.yml | 0 ...tion_fails_for_missing_fields.obtained.yml | 0 ...pe_validation_fails_for_missing_fields.yml | 0 ..._for_multiple_field_resolvers.obtained.yml | 0 ...ion_fails_for_multiple_field_resolvers.yml | 0 ..._fails_for_names_not_matching.obtained.yml | 0 ...alidation_fails_for_names_not_matching.yml | 0 ...on_fails_for_two_descriptions.obtained.yml | 0 ..._validation_fails_for_two_descriptions.yml | 0 ..._for_undefined_field_resolver.obtained.yml | 0 ...ion_fails_for_undefined_field_resolver.yml | 0 ...upported_resolver_arg_default.obtained.yml | 0 ...s_for_unsupported_resolver_arg_default.yml | 0 ...d_resolver_arg_option_default.obtained.yml | 0 ...nsupported_resolver_arg_option_default.yml | 0 ...ion_fails_for_different_names.obtained.yml | 0 ...e_validation_fails_for_different_names.yml | 0 ...fails_for_invalid_type_schema.obtained.yml | 0 ...lidation_fails_for_invalid_type_schema.yml | 0 ...on_fails_for_two_descriptions.obtained.yml | 0 ..._validation_fails_for_two_descriptions.yml | 0 ...ils_if_lazy_type_doesnt_exist.obtained.yml | 0 ...dation_fails_if_lazy_type_doesnt_exist.yml | 0 ...d_arg_not_dict_without_schema.obtained.yml | 0 ...args_field_arg_not_dict_without_schema.yml | 0 ..._args_not_dict_without_schema.obtained.yml | 0 ...st_source_args_not_dict_without_schema.yml | 0 ...r_undefined_field_with_schema.obtained.yml | 0 ...source_for_undefined_field_with_schema.yml | 0 ...undefined_name_without_schema.obtained.yml | 0 .../test_undefined_name_without_schema.yml | 0 ..._include_members_are_combined.obtained.yml | 0 ...clude_and_include_members_are_combined.yml | 0 ...tion_is_set_for_excluded_item.obtained.yml | 0 ...r_description_is_set_for_excluded_item.yml | 0 ...ption_is_set_for_missing_item.obtained.yml | 0 ...er_description_is_set_for_missing_item.yml | 0 ...ption_is_set_for_omitted_item.obtained.yml | 0 ...er_description_is_set_for_omitted_item.yml | 0 .../test_compatibility_layer.py | 22 +- {tests_next => tests}/test_deferred_type.py | 12 +- .../test_description_node.py | 2 +- tests/test_enum_type.py | 676 ++++++---- .../test_enum_type_validation.py | 4 +- .../test_get_field_args_from_resolver.py | 2 +- {tests_next => tests}/test_id_type.py | 2 +- tests/test_input_type.py | 743 ++++++++--- .../test_input_type_validation.py | 4 +- tests/test_interface_type.py | 695 +++++----- tests/test_interface_type_validation.py | 23 + .../test_make_executable_schema.py | 2 +- {tests_next => tests}/test_metadata.py | 2 +- tests/test_object_type.py | 1185 +++++++++++++---- .../test_object_type_field_args.py | 2 +- .../test_object_type_validation.py | 4 +- tests/test_scalar_type.py | 322 +---- .../test_scalar_type_validation.py | 4 +- {tests_next => tests}/test_standard_enum.py | 2 +- tests/test_subscription_type.py | 893 +++++++++---- .../test_subscription_type_validation.py | 2 +- {tests_next => tests}/test_typing.py | 10 +- tests/test_union_type.py | 380 +++--- .../test_union_type_validation.py | 4 +- {tests_next => tests}/test_validators.py | 3 +- {tests_next => tests}/test_value_node.py | 4 +- {tests_next => tests}/types.py | 2 +- tests_next/conftest.py | 40 - ...nterface_with_different_types.obtained.yml | 5 - ...ises_key_error_for_unset_data.obtained.yml | 1 - ...tadata_raises_key_error_for_unset_data.yml | 1 - ...on_if_merge_roots_is_disabled.obtained.yml | 3 - ..._validation_if_merge_roots_is_disabled.yml | 3 - tests_next/test_enum_type.py | 522 -------- tests_next/test_input_type.py | 628 --------- tests_next/test_interface_type.py | 366 ----- tests_next/test_interface_type_validation.py | 45 - tests_next/test_object_type.py | 1015 -------------- tests_next/test_scalar_type.py | 111 -- tests_next/test_subscription_type.py | 754 ----------- tests_next/test_union_type.py | 223 ---- {tests_next => tests_v1}/__init__.py | 0 tests_v1/conftest.py | 12 + ...a_str_contains_multiple_types.obtained.yml | 0 ...ror_schema_str_contains_multiple_types.yml | 0 ...schema_str_has_invalid_syntax.obtained.yml | 0 ...ror_when_schema_str_has_invalid_syntax.yml | 0 ...r_when_schema_type_is_invalid.obtained.yml | 0 ...ises_error_when_schema_type_is_invalid.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ..._when_defined_without_visitor.obtained.yml | 0 ...ute_error_when_defined_without_visitor.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...extra_items_not_in_definition.obtained.yml | 0 ...ping_has_extra_items_not_in_definition.yml | 0 ..._misses_items_from_definition.obtained.yml | 0 ...t_mapping_misses_items_from_definition.yml | 0 ...extra_items_not_in_definition.obtained.yml | 0 ...ping_has_extra_items_not_in_definition.yml | 0 ..._misses_items_from_definition.obtained.yml | 0 ...m_mapping_misses_items_from_definition.yml | 0 ...erged_types_define_same_field.obtained.yml | 0 ...rror_if_merged_types_define_same_field.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...rgs_map_for_nonexisting_field.obtained.yml | 0 ...ed_with_args_map_for_nonexisting_field.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...d_without_extended_dependency.obtained.yml | 0 ...en_defined_without_extended_dependency.yml | 0 ...without_field_type_dependency.obtained.yml | 0 ..._defined_without_field_type_dependency.yml | 0 ...r_when_defined_without_fields.obtained.yml | 0 ...ises_error_when_defined_without_fields.yml | 0 ...nded_dependency_is_wrong_type.obtained.yml | 0 ...when_extended_dependency_is_wrong_type.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...h_alias_for_nonexisting_field.obtained.yml | 0 ...fined_with_alias_for_nonexisting_field.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...esolver_for_nonexisting_field.obtained.yml | 0 ...ed_with_resolver_for_nonexisting_field.yml | 0 ...hout_argument_type_dependency.obtained.yml | 0 ...fined_without_argument_type_dependency.yml | 0 ...d_without_extended_dependency.obtained.yml | 0 ...en_defined_without_extended_dependency.yml | 0 ...r_when_defined_without_fields.obtained.yml | 0 ...ises_error_when_defined_without_fields.yml | 0 ...ithout_return_type_dependency.obtained.yml | 0 ...defined_without_return_type_dependency.yml | 0 ...nded_dependency_is_wrong_type.obtained.yml | 0 ...when_extended_dependency_is_wrong_type.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...fined_for_different_type_name.obtained.yml | 0 ...r_when_defined_for_different_type_name.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ..._defined_with_multiple_fields.obtained.yml | 0 ...rror_when_defined_with_multiple_fields.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...defined_with_nonexistant_args.obtained.yml | 0 ...ror_when_defined_with_nonexistant_args.yml | 0 ...allable_resolve_mutation_attr.obtained.yml | 0 ...without_callable_resolve_mutation_attr.yml | 0 ...r_when_defined_without_fields.obtained.yml | 0 ...ises_error_when_defined_without_fields.yml | 0 ...without_resolve_mutation_attr.obtained.yml | 0 ..._defined_without_resolve_mutation_attr.yml | 0 ...ithout_return_type_dependency.obtained.yml | 0 ...defined_without_return_type_dependency.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...h_alias_for_nonexisting_field.obtained.yml | 0 ...fined_with_alias_for_nonexisting_field.yml | 0 ...ield_args_for_nonexisting_arg.obtained.yml | 0 ...ed_with_field_args_for_nonexisting_arg.yml | 0 ...ld_args_for_nonexisting_field.obtained.yml | 0 ..._with_field_args_for_nonexisting_field.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...esolver_for_nonexisting_field.obtained.yml | 0 ...ed_with_resolver_for_nonexisting_field.yml | 0 ...hout_argument_type_dependency.obtained.yml | 0 ...fined_without_argument_type_dependency.yml | 0 ...d_without_extended_dependency.obtained.yml | 0 ...en_defined_without_extended_dependency.yml | 0 ...r_when_defined_without_fields.obtained.yml | 0 ...ises_error_when_defined_without_fields.yml | 0 ...ithout_return_type_dependency.obtained.yml | 0 ...defined_without_return_type_dependency.yml | 0 ...nded_dependency_is_wrong_type.obtained.yml | 0 ...when_extended_dependency_is_wrong_type.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...h_alias_for_nonexisting_field.obtained.yml | 0 ...fined_with_alias_for_nonexisting_field.yml | 0 ...ith_invalid_graphql_type_name.obtained.yml | 0 ...defined_with_invalid_graphql_type_name.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...esolver_for_nonexisting_field.obtained.yml | 0 ...ed_with_resolver_for_nonexisting_field.yml | 0 ...ith_sub_for_nonexisting_field.obtained.yml | 0 ...defined_with_sub_for_nonexisting_field.yml | 0 ...hout_argument_type_dependency.obtained.yml | 0 ...fined_without_argument_type_dependency.yml | 0 ...d_without_extended_dependency.obtained.yml | 0 ...en_defined_without_extended_dependency.yml | 0 ...r_when_defined_without_fields.obtained.yml | 0 ...ises_error_when_defined_without_fields.yml | 0 ...ithout_return_type_dependency.obtained.yml | 0 ...defined_without_return_type_dependency.yml | 0 ...nded_dependency_is_wrong_type.obtained.yml | 0 ...when_extended_dependency_is_wrong_type.yml | 0 ...r_when_defined_without_schema.obtained.yml | 0 ...bute_error_when_defined_without_schema.yml | 0 ...h_invalid_graphql_type_schema.obtained.yml | 0 ...fined_with_invalid_graphql_type_schema.yml | 0 ...fined_with_invalid_schema_str.obtained.yml | 0 ...r_when_defined_with_invalid_schema_str.yml | 0 ...ined_with_invalid_schema_type.obtained.yml | 0 ..._when_defined_with_invalid_schema_type.yml | 0 ...ed_with_multiple_types_schema.obtained.yml | 0 ...hen_defined_with_multiple_types_schema.yml | 0 ...d_without_extended_dependency.obtained.yml | 0 ...en_defined_without_extended_dependency.yml | 0 ...ithout_member_type_dependency.obtained.yml | 0 ...defined_without_member_type_dependency.yml | 0 ...nded_dependency_is_wrong_type.obtained.yml | 0 ...when_extended_dependency_is_wrong_type.yml | 0 {tests => tests_v1}/test_collection_type.py | 0 {tests => tests_v1}/test_convert_case.py | 0 {tests => tests_v1}/test_definition_parser.py | 0 {tests => tests_v1}/test_directive_type.py | 0 tests_v1/test_enum_type.py | 304 +++++ {tests => tests_v1}/test_executable_schema.py | 0 .../test_executable_schema_compat.py | 0 tests_v1/test_input_type.py | 293 ++++ tests_v1/test_interface_type.py | 462 +++++++ {tests => tests_v1}/test_mutation_type.py | 0 tests_v1/test_object_type.py | 410 ++++++ tests_v1/test_scalar_type.py | 287 ++++ tests_v1/test_subscription_type.py | 325 +++++ tests_v1/test_union_type.py | 251 ++++ 494 files changed, 7323 insertions(+), 6591 deletions(-) rename ariadne_graphql_modules/{next => }/base.py (97%) create mode 100644 ariadne_graphql_modules/base_object_type/__init__.py rename ariadne_graphql_modules/{next/graphql_object/object_field.py => base_object_type/graphql_field.py} (78%) rename ariadne_graphql_modules/{next/graphql_object/object_type.py => base_object_type/graphql_type.py} (57%) rename ariadne_graphql_modules/{next/graphql_object => base_object_type}/utils.py (97%) rename ariadne_graphql_modules/{next/graphql_object => base_object_type}/validators.py (95%) rename ariadne_graphql_modules/{next => }/compatibility_layer.py (92%) rename ariadne_graphql_modules/{next => }/convert_name.py (100%) rename ariadne_graphql_modules/{next => }/deferredtype.py (100%) rename ariadne_graphql_modules/{next => }/description.py (100%) create mode 100644 ariadne_graphql_modules/enum_type/__init__.py rename ariadne_graphql_modules/{next/graphql_enum_type/enum_type.py => enum_type/graphql_type.py} (99%) rename ariadne_graphql_modules/{next/graphql_enum_type/enum_model.py => enum_type/models.py} (100%) create mode 100644 ariadne_graphql_modules/enum_type/type.py rename ariadne_graphql_modules/{next => }/idtype.py (100%) create mode 100644 ariadne_graphql_modules/input_type/__init__.py rename ariadne_graphql_modules/{next/graphql_input/input_field.py => input_type/graphql_field.py} (100%) rename ariadne_graphql_modules/{next/graphql_input/input_type.py => input_type/graphql_type.py} (97%) rename ariadne_graphql_modules/{next/graphql_input/input_model.py => input_type/models.py} (100%) rename ariadne_graphql_modules/{next/graphql_input => input_type}/validators.py (97%) create mode 100644 ariadne_graphql_modules/interface_type/__init__.py create mode 100644 ariadne_graphql_modules/interface_type/graphql_type.py rename ariadne_graphql_modules/{next/graphql_interface/interface_model.py => interface_type/models.py} (100%) delete mode 100644 ariadne_graphql_modules/next/__init__.py delete mode 100644 ariadne_graphql_modules/next/executable_schema.py delete mode 100644 ariadne_graphql_modules/next/graphql_enum_type/__init__.py delete mode 100644 ariadne_graphql_modules/next/graphql_input/__init__.py delete mode 100644 ariadne_graphql_modules/next/graphql_interface/__init__.py delete mode 100644 ariadne_graphql_modules/next/graphql_interface/interface_type.py delete mode 100644 ariadne_graphql_modules/next/graphql_scalar/__init__.py delete mode 100644 ariadne_graphql_modules/next/graphql_subscription/__init__.py delete mode 100644 ariadne_graphql_modules/next/graphql_union/__init__.py rename ariadne_graphql_modules/{next/graphql_object => object_type}/__init__.py (77%) create mode 100644 ariadne_graphql_modules/object_type/graphql_type.py rename ariadne_graphql_modules/{next/graphql_object/object_model.py => object_type/models.py} (94%) rename ariadne_graphql_modules/{next => }/roots.py (100%) create mode 100644 ariadne_graphql_modules/scalar_type/__init__.py rename ariadne_graphql_modules/{next/graphql_scalar/scalar_type.py => scalar_type/graphql_type.py} (97%) rename ariadne_graphql_modules/{next/graphql_scalar/scalar_model.py => scalar_type/models.py} (100%) rename ariadne_graphql_modules/{next/graphql_scalar => scalar_type}/validators.py (89%) rename ariadne_graphql_modules/{next => }/sort.py (100%) create mode 100644 ariadne_graphql_modules/subscription_type/__init__.py rename ariadne_graphql_modules/{next/graphql_subscription/subscription_type.py => subscription_type/graphql_type.py} (78%) rename ariadne_graphql_modules/{next/graphql_subscription/subscription_model.py => subscription_type/models.py} (95%) rename ariadne_graphql_modules/{next => }/typing.py (100%) create mode 100644 ariadne_graphql_modules/union_type/__init__.py rename ariadne_graphql_modules/{next/graphql_union/union_type.py => union_type/graphql_type.py} (95%) rename ariadne_graphql_modules/{next/graphql_union/union_model.py => union_type/models.py} (100%) rename ariadne_graphql_modules/{next/graphql_union => union_type}/validators.py (96%) create mode 100644 ariadne_graphql_modules/v1/__init__.py rename ariadne_graphql_modules/{ => v1}/bases.py (98%) rename ariadne_graphql_modules/{ => v1}/collection_type.py (100%) rename ariadne_graphql_modules/{ => v1}/convert_case.py (97%) rename ariadne_graphql_modules/{ => v1}/dependencies.py (99%) rename ariadne_graphql_modules/{ => v1}/directive_type.py (97%) rename ariadne_graphql_modules/{ => v1}/enum_type.py (98%) create mode 100644 ariadne_graphql_modules/v1/executable_schema.py rename ariadne_graphql_modules/{ => v1}/input_type.py (97%) rename ariadne_graphql_modules/{ => v1}/interface_type.py (98%) rename ariadne_graphql_modules/{ => v1}/mutation_type.py (98%) rename ariadne_graphql_modules/{ => v1}/object_type.py (98%) rename ariadne_graphql_modules/{ => v1}/resolvers_mixin.py (97%) rename ariadne_graphql_modules/{ => v1}/scalar_type.py (97%) rename ariadne_graphql_modules/{ => v1}/subscription_type.py (100%) rename ariadne_graphql_modules/{ => v1}/union_type.py (97%) rename ariadne_graphql_modules/{next => }/validators.py (100%) rename ariadne_graphql_modules/{next => }/value.py (100%) rename {tests_next => tests}/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_arg_with_description_in_source_with_schema.yml (100%) rename {tests_next => tests}/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_arg_with_name_in_source_with_schema.yml (100%) rename {tests_next => tests}/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_arg_with_type_in_source_with_schema.yml (100%) rename {tests_next => tests}/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml (100%) rename {tests_next => tests}/snapshots/test_description_not_str_without_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_description_not_str_without_schema.yml (100%) rename {tests_next => tests}/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_enum_type_validation_fails_for_invalid_members.yml (100%) rename {tests_next => tests}/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_enum_type_validation_fails_for_missing_members.yml (100%) rename {tests_next => tests}/snapshots/test_field_name_not_str_without_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_field_name_not_str_without_schema.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml (100%) create mode 100644 tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html rename {tests_next => tests}/snapshots/test_interface_no_interface_in_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_interface_no_interface_in_schema.yml (100%) create mode 100644 tests/snapshots/test_interface_with_different_types.obtained.diff.html create mode 100644 tests/snapshots/test_interface_with_different_types.obtained.yml rename {tests_next => tests}/snapshots/test_interface_with_different_types.yml (100%) rename {tests_next => tests}/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_invalid_arg_name_in_source_with_schema.yml (100%) rename {tests_next => tests}/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml (100%) create mode 100644 tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html create mode 100644 tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml create mode 100644 tests/snapshots/test_metadata_raises_key_error_for_unset_data.yml rename {tests_next => tests}/snapshots/test_missing_type_in_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_missing_type_in_schema.yml (100%) rename {tests_next => tests}/snapshots/test_missing_type_in_types.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_missing_type_in_types.yml (100%) rename {tests_next => tests}/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_multiple_descriptions_for_source_with_schema.yml (100%) create mode 100644 tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html create mode 100644 tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml create mode 100644 tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml rename {tests_next => tests}/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_multiple_sourced_for_field_with_schema.yml (100%) rename {tests_next => tests}/snapshots/test_multiple_sources_without_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_multiple_sources_without_schema.yml (100%) rename {tests_next => tests}/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_alias_resolver.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_invalid_alias.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml (100%) rename {tests_next => tests}/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml (100%) rename {tests_next => tests}/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml (100%) rename {tests_next => tests}/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml (100%) rename {tests_next => tests}/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml (100%) rename {tests_next => tests}/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_source_args_field_arg_not_dict_without_schema.yml (100%) rename {tests_next => tests}/snapshots/test_source_args_not_dict_without_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_source_args_not_dict_without_schema.yml (100%) rename {tests_next => tests}/snapshots/test_source_for_undefined_field_with_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_source_for_undefined_field_with_schema.yml (100%) rename {tests_next => tests}/snapshots/test_undefined_name_without_schema.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_undefined_name_without_schema.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml (100%) rename {tests_next => tests}/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml (100%) rename {tests_next => tests}/test_compatibility_layer.py (93%) rename {tests_next => tests}/test_deferred_type.py (73%) rename {tests_next => tests}/test_description_node.py (94%) rename {tests_next => tests}/test_enum_type_validation.py (98%) rename {tests_next => tests}/test_get_field_args_from_resolver.py (98%) rename {tests_next => tests}/test_id_type.py (98%) rename {tests_next => tests}/test_input_type_validation.py (97%) create mode 100644 tests/test_interface_type_validation.py rename {tests_next => tests}/test_make_executable_schema.py (98%) rename {tests_next => tests}/test_metadata.py (96%) rename {tests_next => tests}/test_object_type_field_args.py (99%) rename {tests_next => tests}/test_object_type_validation.py (99%) rename {tests_next => tests}/test_scalar_type_validation.py (91%) rename {tests_next => tests}/test_standard_enum.py (99%) rename {tests_next => tests}/test_subscription_type_validation.py (99%) rename {tests_next => tests}/test_typing.py (95%) rename {tests_next => tests}/test_union_type_validation.py (92%) rename {tests_next => tests}/test_validators.py (92%) rename {tests_next => tests}/test_value_node.py (96%) rename {tests_next => tests}/types.py (73%) delete mode 100644 tests_next/conftest.py delete mode 100644 tests_next/snapshots/test_interface_with_different_types.obtained.yml delete mode 100644 tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml delete mode 100644 tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml delete mode 100644 tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml delete mode 100644 tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml delete mode 100644 tests_next/test_enum_type.py delete mode 100644 tests_next/test_input_type.py delete mode 100644 tests_next/test_interface_type.py delete mode 100644 tests_next/test_interface_type_validation.py delete mode 100644 tests_next/test_object_type.py delete mode 100644 tests_next/test_scalar_type.py delete mode 100644 tests_next/test_subscription_type.py delete mode 100644 tests_next/test_union_type.py rename {tests_next => tests_v1}/__init__.py (100%) create mode 100644 tests_v1/conftest.py rename {tests => tests_v1}/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml (100%) rename {tests => tests_v1}/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml (100%) rename {tests => tests_v1}/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml (100%) rename {tests => tests_v1}/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_defined_without_fields.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_fields.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml (100%) rename {tests => tests_v1}/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml (100%) rename {tests => tests_v1}/test_collection_type.py (100%) rename {tests => tests_v1}/test_convert_case.py (100%) rename {tests => tests_v1}/test_definition_parser.py (100%) rename {tests => tests_v1}/test_directive_type.py (100%) create mode 100644 tests_v1/test_enum_type.py rename {tests => tests_v1}/test_executable_schema.py (100%) rename {tests => tests_v1}/test_executable_schema_compat.py (100%) create mode 100644 tests_v1/test_input_type.py create mode 100644 tests_v1/test_interface_type.py rename {tests => tests_v1}/test_mutation_type.py (100%) create mode 100644 tests_v1/test_object_type.py create mode 100644 tests_v1/test_scalar_type.py create mode 100644 tests_v1/test_subscription_type.py create mode 100644 tests_v1/test_union_type.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c421c2b..a555ccd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,6 @@ on: jobs: build: - runs-on: ubuntu-latest strategy: fail-fast: false @@ -16,20 +15,20 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e .[test] - - name: Pytest - run: | - pytest - - name: Linters - run: | - pylint ariadne_graphql_modules tests - mypy ariadne_graphql_modules --ignore-missing-imports - black --check . + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[test] + - name: Pytest + run: | + pytest + - name: Linters + run: | + pylint ariadne_graphql_modules + mypy ariadne_graphql_modules --ignore-missing-imports + black --check . diff --git a/ariadne_graphql_modules/__init__.py b/ariadne_graphql_modules/__init__.py index 789effe..c80762d 100644 --- a/ariadne_graphql_modules/__init__.py +++ b/ariadne_graphql_modules/__init__.py @@ -1,38 +1,58 @@ -from ariadne import gql - -from .bases import BaseType, BindableType, DeferredType, DefinitionType -from .collection_type import CollectionType -from .convert_case import convert_case -from .directive_type import DirectiveType -from .enum_type import EnumType +from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .convert_name import ( + convert_graphql_name_to_python, + convert_python_name_to_graphql, +) +from .deferredtype import deferred +from .description import get_description_node +from .enum_type import ( + GraphQLEnum, + GraphQLEnumModel, + create_graphql_enum_model, + graphql_enum, +) from .executable_schema import make_executable_schema -from .input_type import InputType -from .interface_type import InterfaceType -from .mutation_type import MutationType -from .object_type import ObjectType -from .scalar_type import ScalarType -from .subscription_type import SubscriptionType -from .union_type import UnionType -from .utils import create_alias_resolver, parse_definition +from .idtype import GraphQLID +from .input_type import GraphQLInput, GraphQLInputModel +from .object_type import GraphQLObject, GraphQLObjectModel, object_field +from .roots import ROOTS_NAMES, merge_root_nodes +from .scalar_type import GraphQLScalar, GraphQLScalarModel +from .sort import sort_schema_document +from .union_type import GraphQLUnion, GraphQLUnionModel +from .value import get_value_from_node, get_value_node +from .interface_type import GraphQLInterface, GraphQLInterfaceModel +from .subscription_type import GraphQLSubscription, GraphQLSubscriptionModel __all__ = [ - "BaseType", - "BindableType", - "CollectionType", - "DeferredType", - "DefinitionType", - "DirectiveType", - "EnumType", - "InputType", - "InterfaceType", - "MutationType", - "ObjectType", - "ScalarType", - "SubscriptionType", - "UnionType", - "convert_case", - "create_alias_resolver", - "gql", + "GraphQLEnum", + "GraphQLEnumModel", + "GraphQLID", + "GraphQLInput", + "GraphQLInputModel", + "GraphQLInterface", + "GraphQLInterfaceModel", + "GraphQLSubscription", + "GraphQLSubscriptionModel", + "GraphQLMetadata", + "GraphQLModel", + "GraphQLObject", + "GraphQLObjectModel", + "GraphQLScalar", + "GraphQLScalarModel", + "GraphQLType", + "GraphQLUnion", + "GraphQLUnionModel", + "ROOTS_NAMES", + "convert_graphql_name_to_python", + "convert_python_name_to_graphql", + "create_graphql_enum_model", + "deferred", + "get_description_node", + "get_value_from_node", + "get_value_node", + "graphql_enum", "make_executable_schema", - "parse_definition", + "merge_root_nodes", + "object_field", + "sort_schema_document", ] diff --git a/ariadne_graphql_modules/next/base.py b/ariadne_graphql_modules/base.py similarity index 97% rename from ariadne_graphql_modules/next/base.py rename to ariadne_graphql_modules/base.py index fcf7991..aa57f2a 100644 --- a/ariadne_graphql_modules/next/base.py +++ b/ariadne_graphql_modules/base.py @@ -84,7 +84,7 @@ def get_graphql_model( if hasattr(graphql_type, "__get_graphql_model__"): self.models[graphql_type] = graphql_type.__get_graphql_model__(self) elif issubclass(graphql_type, Enum): - from .graphql_enum_type import ( # pylint: disable=R0401,C0415 + from .enum_type import ( # pylint: disable=R0401,C0415 create_graphql_enum_model, ) diff --git a/ariadne_graphql_modules/base_object_type/__init__.py b/ariadne_graphql_modules/base_object_type/__init__.py new file mode 100644 index 0000000..6884aa9 --- /dev/null +++ b/ariadne_graphql_modules/base_object_type/__init__.py @@ -0,0 +1,15 @@ +from .graphql_type import GraphQLBaseObject +from .graphql_field import GraphQLFieldData, GraphQLObjectData +from .validators import ( + validate_object_type_with_schema, + validate_object_type_without_schema, +) + + +__all__ = [ + "GraphQLBaseObject", + "GraphQLObjectData", + "GraphQLFieldData", + "validate_object_type_with_schema", + "validate_object_type_without_schema", +] diff --git a/ariadne_graphql_modules/next/graphql_object/object_field.py b/ariadne_graphql_modules/base_object_type/graphql_field.py similarity index 78% rename from ariadne_graphql_modules/next/graphql_object/object_field.py rename to ariadne_graphql_modules/base_object_type/graphql_field.py index 15b5491..a7ced74 100644 --- a/ariadne_graphql_modules/next/graphql_object/object_field.py +++ b/ariadne_graphql_modules/base_object_type/graphql_field.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass -from typing import Any, Dict, Optional +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional from ariadne.types import Resolver, Subscriber @@ -12,6 +12,47 @@ class GraphQLObjectFieldArg: default_value: Optional[Any] = None +@dataclass(frozen=True) +class GraphQLObjectData: + fields: Dict[str, "GraphQLObjectField"] + interfaces: List[str] + + +@dataclass(frozen=True) +class GraphQLObjectResolver: + resolver: Resolver + field: str + description: Optional[str] = None + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None + field_type: Optional[Any] = None + + +@dataclass(frozen=True) +class GraphQLObjectSource: + subscriber: Subscriber + field: str + description: Optional[str] = None + args: Optional[Dict[str, GraphQLObjectFieldArg]] = None + field_type: Optional[Any] = None + + +@dataclass +class GraphQLFieldData: + fields_types: Dict[str, str] = field(default_factory=dict) + fields_names: Dict[str, str] = field(default_factory=dict) + fields_descriptions: Dict[str, str] = field(default_factory=dict) + fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = field( + default_factory=dict + ) + fields_resolvers: Dict[str, Resolver] = field(default_factory=dict) + fields_subscribers: Dict[str, Subscriber] = field(default_factory=dict) + fields_defaults: Dict[str, Any] = field(default_factory=dict) + fields_order: List[str] = field(default_factory=list) + type_hints: Dict[str, Any] = field(default_factory=dict) + aliases: Dict[str, str] = field(default_factory=dict) + aliases_targets: List[str] = field(default_factory=list) + + class GraphQLObjectField: def __init__( self, @@ -40,24 +81,6 @@ def __call__(self, resolver: Resolver): return self -@dataclass(frozen=True) -class GraphQLObjectResolver: - resolver: Resolver - field: str - description: Optional[str] = None - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None - field_type: Optional[Any] = None - - -@dataclass(frozen=True) -class GraphQLObjectSource: - subscriber: Subscriber - field: str - description: Optional[str] = None - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None - field_type: Optional[Any] = None - - def object_field( resolver: Optional[Resolver] = None, *, diff --git a/ariadne_graphql_modules/next/graphql_object/object_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py similarity index 57% rename from ariadne_graphql_modules/next/graphql_object/object_type.py rename to ariadne_graphql_modules/base_object_type/graphql_type.py index 80f8e67..6dc9b9d 100644 --- a/ariadne_graphql_modules/next/graphql_object/object_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -7,22 +7,24 @@ Iterable, List, Optional, + Tuple, Type, Union, - cast, ) -from ariadne.types import Resolver, Subscriber +from ariadne.types import Resolver from graphql import ( FieldDefinitionNode, InputValueDefinitionNode, - NameNode, - NamedTypeNode, ObjectTypeDefinitionNode, StringValueNode, ) -from .object_field import ( +from ..types import GraphQLClassType + +from .graphql_field import ( + GraphQLFieldData, + GraphQLObjectData, GraphQLObjectField, GraphQLObjectFieldArg, GraphQLObjectResolver, @@ -30,7 +32,6 @@ object_field, object_resolver, ) -from .object_model import GraphQLObjectModel from .utils import ( get_field_args_from_resolver, get_field_args_from_subscriber, @@ -39,7 +40,6 @@ update_field_args_options, ) -from ...utils import parse_definition from ..base import GraphQLMetadata, GraphQLModel, GraphQLType from ..convert_name import convert_python_name_to_graphql from ..description import get_description_node @@ -51,26 +51,21 @@ from ..value import get_value_node -@dataclass(frozen=True) -class GraphQLObjectData: - fields: Dict[str, "GraphQLObjectField"] - interfaces: List[str] - - -class GraphQLObject(GraphQLType): +class GraphQLBaseObject(GraphQLType): __kwargs__: Dict[str, Any] __abstract__: bool = True __schema__: Optional[str] __description__: Optional[str] __aliases__: Optional[Dict[str, str]] __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] - __implements__: Optional[Iterable[Type[GraphQLType]]] + __graphql_type__ = GraphQLClassType.BASE def __init__(self, **kwargs: Any): default_values: Dict[str, Any] = {} - for interface in getattr(self, "__implements__", []): - if hasattr(interface, "__kwargs__"): - default_values.update(interface.__kwargs__) + + for inherited_obj in self._collect_inherited_objects(): + if hasattr(inherited_obj, "__kwargs__"): + default_values.update(inherited_obj.__kwargs__) default_values.update(self.__kwargs__) @@ -112,16 +107,22 @@ def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": @classmethod def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": - definition = cast( - ObjectTypeDefinitionNode, - parse_definition(ObjectTypeDefinitionNode, cls.__schema__), - ) + raise NotImplementedError() + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": + raise NotImplementedError() + @classmethod + def _create_fields_and_resolvers_with_schema( + cls, definition_fields: Tuple["FieldDefinitionNode", ...] + ) -> Tuple[tuple[FieldDefinitionNode, ...], Dict[str, Resolver]]: descriptions: Dict[str, StringValueNode] = {} args_descriptions: Dict[str, Dict[str, StringValueNode]] = {} args_defaults: Dict[str, Dict[str, Any]] = {} resolvers: Dict[str, Resolver] = {} - out_names: Dict[str, Dict[str, str]] = {} for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -133,26 +134,24 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": field_args = get_field_args_from_resolver(cls_attr.resolver) if field_args: - args_descriptions[cls_attr.field] = {} - args_defaults[cls_attr.field] = {} + args_descriptions[cls_attr.field], args_defaults[cls_attr.field] = ( + {}, + {}, + ) final_args = update_field_args_options(field_args, cls_attr.args) - for arg_name, arg_options in final_args.items(): - arg_description = get_description_node(arg_options.description) - if arg_description: - args_descriptions[cls_attr.field][ - arg_name - ] = arg_description - - arg_default = arg_options.default_value - if arg_default is not None: + description_node = get_description_node(cls_attr.description) + if description_node: + descriptions[cls_attr.field] = description_node + + if arg_options.default_value is not None: args_defaults[cls_attr.field][arg_name] = get_value_node( - arg_default + arg_options.default_value ) fields: List[FieldDefinitionNode] = [] - for field in definition.fields: + for field in definition_fields: field_args_descriptions = args_descriptions.get(field.name.value, {}) field_args_defaults = args_defaults.get(field.name.value, {}) @@ -185,30 +184,21 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": ) ) - return GraphQLObjectModel( - name=definition.name.value, - ast_type=ObjectTypeDefinitionNode, - ast=ObjectTypeDefinitionNode( - name=NameNode(value=definition.name.value), - fields=tuple(fields), - interfaces=definition.interfaces, - ), - resolvers=resolvers, - aliases=getattr(cls, "__aliases__", {}), - out_names=out_names, - ) + return tuple(fields), resolvers @classmethod - def __get_graphql_model_without_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLModel": - type_data = get_graphql_object_data(metadata, cls) - type_aliases = getattr(cls, "__aliases__", {}) - - fields_ast: List[FieldDefinitionNode] = [] - resolvers: Dict[str, Resolver] = {} - aliases: Dict[str, str] = {} - out_names: Dict[str, Dict[str, str]] = {} + def _process_graphql_fields( + cls, metadata: GraphQLMetadata, type_data, type_aliases + ) -> Tuple[ + List[FieldDefinitionNode], + Dict[str, Resolver], + Dict[str, str], + Dict[str, Dict[str, str]], + ]: + fields_ast = [] + resolvers = {} + aliases = {} + out_names = {} for attr_name, field in type_data.fields.items(): fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) @@ -224,25 +214,7 @@ def __get_graphql_model_without_schema__( if field.args and field.name: out_names[field.name] = get_field_args_out_names(field.args) - interfaces_ast: List[NamedTypeNode] = [] - for interface_name in type_data.interfaces: - interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) - - return GraphQLObjectModel( - name=name, - ast_type=ObjectTypeDefinitionNode, - ast=ObjectTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node( - getattr(cls, "__description__", None), - ), - fields=tuple(fields_ast), - interfaces=tuple(interfaces_ast), - ), - resolvers=resolvers, - aliases=aliases, - out_names=out_names, - ) + return fields_ast, resolvers, aliases, out_names @classmethod def __get_graphql_types__( @@ -268,7 +240,7 @@ def __get_graphql_types_without_schema__( cls, metadata: "GraphQLMetadata" ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: types: List[Union[Type["GraphQLType"], Type[Enum]]] = [cls] - type_data = get_graphql_object_data(metadata, cls) + type_data = cls.get_graphql_object_data(metadata) for field in type_data.fields.values(): field_type = get_graphql_type(field.field_type) @@ -333,130 +305,135 @@ def argument( default_value=default_value, ) + @classmethod + def get_graphql_object_data( + cls, + metadata: GraphQLMetadata, + ) -> GraphQLObjectData: + try: + return metadata.get_data(cls) + except KeyError as exc: + if getattr(cls, "__schema__", None): + raise NotImplementedError( + "'get_graphql_object_data' is not supported for " + "objects with '__schema__'." + ) from exc + object_data = cls.create_graphql_object_data_without_schema() + + metadata.set_data(cls, object_data) + return object_data -def get_graphql_object_data( - metadata: GraphQLMetadata, cls: Type[GraphQLObject] -) -> GraphQLObjectData: - try: - return metadata.get_data(cls) - except KeyError as exc: - if getattr(cls, "__schema__", None): - raise NotImplementedError( - "'get_graphql_object_data' is not supported for " - "objects with '__schema__'." - ) from exc - object_data = create_graphql_object_data_without_schema(cls) - - metadata.set_data(cls, object_data) - return object_data - - -def create_graphql_object_data_without_schema( - cls: Type["GraphQLObject"], -) -> GraphQLObjectData: - fields_types: Dict[str, str] = {} - fields_names: Dict[str, str] = {} - fields_descriptions: Dict[str, str] = {} - fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = {} - fields_resolvers: Dict[str, Resolver] = {} - fields_subscribers: Dict[str, Subscriber] = {} - fields_defaults: Dict[str, Any] = {} - fields_order: List[str] = [] - - interfaces = getattr(cls, "__implements__", []) - interfaces_names: List[str] = [interface.__name__ for interface in interfaces] - type_hints: dict[str, Any] = {} - aliases: Dict[str, str] = {} - - for interface in reversed(interfaces): - type_hints.update(interface.__annotations__) - aliases.update(getattr(interface, "__aliases__", {})) - - type_hints.update(cls.__annotations__) - aliases.update(getattr(cls, "__aliases__", None) or {}) - aliases_targets: List[str] = list(aliases.values()) - - for attr_name, attr_type in type_hints.items(): - if attr_name.startswith("__"): - continue - - if attr_name in aliases_targets: - # Alias target is not included in schema - # unless its explicit field - cls_attr = getattr(cls, attr_name, None) - if not isinstance(cls_attr, GraphQLObjectField): + @classmethod + def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: + raise NotImplementedError() + + @staticmethod + def _build_fields(fields_data: GraphQLFieldData) -> Dict[str, "GraphQLObjectField"]: + fields = {} + for field_name in fields_data.fields_order: + fields[field_name] = GraphQLObjectField( + name=fields_data.fields_names[field_name], + description=fields_data.fields_descriptions.get(field_name), + field_type=fields_data.fields_types[field_name], + args=fields_data.fields_args.get(field_name), + resolver=fields_data.fields_resolvers.get(field_name), + subscriber=fields_data.fields_subscribers.get(field_name), + default_value=fields_data.fields_defaults.get(field_name), + ) + return fields + + @classmethod + def _process_type_hints_and_aliases(cls, fields_data: GraphQLFieldData): + fields_data.type_hints.update(cls.__annotations__) # pylint: disable=no-member + fields_data.aliases.update(getattr(cls, "__aliases__", None) or {}) + fields_data.aliases_targets = list(fields_data.aliases.values()) + + for attr_name, attr_type in fields_data.type_hints.items(): + if attr_name.startswith("__"): continue - fields_order.append(attr_name) + if attr_name in fields_data.aliases_targets: + cls_attr = getattr(cls, attr_name, None) + if not isinstance(cls_attr, GraphQLObjectField): + continue - fields_names[attr_name] = convert_python_name_to_graphql(attr_name) - fields_types[attr_name] = attr_type + fields_data.fields_order.append(attr_name) + fields_data.fields_names[attr_name] = convert_python_name_to_graphql( + attr_name + ) + fields_data.fields_types[attr_name] = attr_type - def process_class_attributes(target_cls): + @staticmethod + def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): for attr_name in dir(target_cls): if attr_name.startswith("__"): continue cls_attr = getattr(target_cls, attr_name) if isinstance(cls_attr, GraphQLObjectField): - if attr_name not in fields_order: - fields_order.append(attr_name) + if attr_name not in fields_data.fields_order: + fields_data.fields_order.append(attr_name) - fields_names[attr_name] = ( + fields_data.fields_names[attr_name] = ( cls_attr.name or convert_python_name_to_graphql(attr_name) ) if cls_attr.field_type: - fields_types[attr_name] = cls_attr.field_type + fields_data.fields_types[attr_name] = cls_attr.field_type if cls_attr.description: - fields_descriptions[attr_name] = cls_attr.description + fields_data.fields_descriptions[attr_name] = cls_attr.description if cls_attr.resolver: - fields_resolvers[attr_name] = cls_attr.resolver + fields_data.fields_resolvers[attr_name] = cls_attr.resolver field_args = get_field_args_from_resolver(cls_attr.resolver) if field_args: - fields_args[attr_name] = update_field_args_options( + fields_data.fields_args[attr_name] = update_field_args_options( field_args, cls_attr.args ) if cls_attr.default_value: - fields_defaults[attr_name] = cls_attr.default_value + fields_data.fields_defaults[attr_name] = cls_attr.default_value elif isinstance(cls_attr, GraphQLObjectResolver): - if cls_attr.field_type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.field_type - if cls_attr.description and cls_attr.field not in fields_descriptions: - fields_descriptions[cls_attr.field] = cls_attr.description - fields_resolvers[cls_attr.field] = cls_attr.resolver + if ( + cls_attr.field_type + and cls_attr.field not in fields_data.fields_types + ): + fields_data.fields_types[cls_attr.field] = cls_attr.field_type + if ( + cls_attr.description + and cls_attr.field not in fields_data.fields_descriptions + ): + fields_data.fields_descriptions[cls_attr.field] = ( + cls_attr.description + ) + fields_data.fields_resolvers[cls_attr.field] = cls_attr.resolver field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args and not fields_args.get(cls_attr.field): - fields_args[cls_attr.field] = update_field_args_options( + if field_args and not fields_data.fields_args.get(cls_attr.field): + fields_data.fields_args[cls_attr.field] = update_field_args_options( field_args, cls_attr.args ) elif isinstance(cls_attr, GraphQLObjectSource): - if cls_attr.field_type and cls_attr.field not in fields_types: - fields_types[cls_attr.field] = cls_attr.field_type - if cls_attr.description and cls_attr.field not in fields_descriptions: - fields_descriptions[cls_attr.field] = cls_attr.description - fields_subscribers[cls_attr.field] = cls_attr.subscriber + if ( + cls_attr.field_type + and cls_attr.field not in fields_data.fields_types + ): + fields_data.fields_types[cls_attr.field] = cls_attr.field_type + if ( + cls_attr.description + and cls_attr.field not in fields_data.fields_descriptions + ): + fields_data.fields_descriptions[cls_attr.field] = ( + cls_attr.description + ) + fields_data.fields_subscribers[cls_attr.field] = cls_attr.subscriber field_args = get_field_args_from_subscriber(cls_attr.subscriber) if field_args: - fields_args[cls_attr.field] = update_field_args_options( + fields_data.fields_args[cls_attr.field] = update_field_args_options( field_args, cls_attr.args ) - elif attr_name not in aliases_targets and not callable(cls_attr): - fields_defaults[attr_name] = cls_attr - - for interface in getattr(cls, "__implements__", []): - process_class_attributes(interface) - - process_class_attributes(cls) - fields: Dict[str, "GraphQLObjectField"] = {} - for field_name in fields_order: - fields[field_name] = GraphQLObjectField( - name=fields_names[field_name], - description=fields_descriptions.get(field_name), - field_type=fields_types[field_name], - args=fields_args.get(field_name), - resolver=fields_resolvers.get(field_name), - subscriber=fields_subscribers.get(field_name), - default_value=fields_defaults.get(field_name), - ) - return GraphQLObjectData(fields=fields, interfaces=interfaces_names) + elif attr_name not in fields_data.aliases_targets and not callable( + cls_attr + ): + fields_data.fields_defaults[attr_name] = cls_attr + + @classmethod + def _collect_inherited_objects(cls): + raise NotImplementedError diff --git a/ariadne_graphql_modules/next/graphql_object/utils.py b/ariadne_graphql_modules/base_object_type/utils.py similarity index 97% rename from ariadne_graphql_modules/next/graphql_object/utils.py rename to ariadne_graphql_modules/base_object_type/utils.py index e562287..a3d9025 100644 --- a/ariadne_graphql_modules/next/graphql_object/utils.py +++ b/ariadne_graphql_modules/base_object_type/utils.py @@ -10,14 +10,14 @@ from ..description import get_description_node from ..typing import get_type_node from ..value import get_value_node -from .object_field import GraphQLObjectField, GraphQLObjectFieldArg +from .graphql_field import GraphQLObjectField, GraphQLObjectFieldArg if TYPE_CHECKING: - from .object_type import GraphQLObject + from .graphql_type import GraphQLBaseObject def get_field_node_from_obj_field( - parent_type: Type["GraphQLObject"], + parent_type: Type["GraphQLBaseObject"], metadata: GraphQLMetadata, field: GraphQLObjectField, ) -> FieldDefinitionNode: diff --git a/ariadne_graphql_modules/next/graphql_object/validators.py b/ariadne_graphql_modules/base_object_type/validators.py similarity index 95% rename from ariadne_graphql_modules/next/graphql_object/validators.py rename to ariadne_graphql_modules/base_object_type/validators.py index 672ad98..f104655 100644 --- a/ariadne_graphql_modules/next/graphql_object/validators.py +++ b/ariadne_graphql_modules/base_object_type/validators.py @@ -4,7 +4,7 @@ from graphql import FieldDefinitionNode, ObjectTypeDefinitionNode, TypeDefinitionNode from ..convert_name import convert_python_name_to_graphql -from .object_field import ( +from .graphql_field import ( GraphQLObjectField, GraphQLObjectFieldArg, GraphQLObjectResolver, @@ -16,10 +16,10 @@ ) from ..validators import validate_description, validate_name from ..value import get_value_node -from ...utils import parse_definition +from ..utils import parse_definition if TYPE_CHECKING: - from .object_type import GraphQLObject + from .graphql_type import GraphQLBaseObject @dataclass @@ -31,8 +31,15 @@ class GraphQLObjectValidationData: sources_instances: Dict[str, GraphQLObjectSource] +def get_all_annotations(cls): + annotations = {} + for base_cls in reversed(cls.__mro__): + annotations.update(getattr(base_cls, "__annotations__", {})) + return annotations + + def validate_object_type_with_schema( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], valid_type: Type[TypeDefinitionNode] = ObjectTypeDefinitionNode, ) -> Dict[str, Any]: definition = cast( @@ -212,7 +219,9 @@ def validate_object_type_with_schema( return get_object_type_with_schema_kwargs(cls, aliases, field_names) -def validate_object_type_without_schema(cls: Type["GraphQLObject"]) -> Dict[str, Any]: +def validate_object_type_without_schema( + cls: Type["GraphQLBaseObject"], +) -> Dict[str, Any]: data = get_object_type_validation_data(cls) # Alias target is not present in schema as a field if its not an @@ -246,7 +255,7 @@ def validate_object_type_without_schema(cls: Type["GraphQLObject"]) -> Dict[str, def validate_object_unique_graphql_names( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], fields_attrs: List[str], fields_instances: Dict[str, GraphQLObjectField], ): @@ -272,7 +281,7 @@ def validate_object_unique_graphql_names( def validate_object_resolvers( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], fields_names: List[str], fields_instances: Dict[str, GraphQLObjectField], resolvers_instances: Dict[str, GraphQLObjectResolver], @@ -317,7 +326,7 @@ def validate_object_resolvers( def validate_object_subscribers( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], fields_names: List[str], sources_instances: Dict[str, GraphQLObjectSource], ): @@ -356,7 +365,7 @@ def validate_object_subscribers( ) -def validate_object_fields_args(cls: Type["GraphQLObject"]): +def validate_object_fields_args(cls: Type["GraphQLBaseObject"]): for field_name in dir(cls): field_instance = getattr(cls, field_name) if ( @@ -367,7 +376,7 @@ def validate_object_fields_args(cls: Type["GraphQLObject"]): def validate_object_field_args( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], field_name: str, field_instance: Union["GraphQLObjectField", "GraphQLObjectResolver"], ): @@ -411,7 +420,7 @@ def validate_object_field_args( def validate_object_aliases( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], aliases: Dict[str, str], fields_names: List[str], fields_resolvers: List[str], @@ -432,7 +441,7 @@ def validate_object_aliases( def validate_field_arg_default_value( - cls: Type["GraphQLObject"], field_name: str, arg_name: str, default_value: Any + cls: Type["GraphQLBaseObject"], field_name: str, arg_name: str, default_value: Any ): if default_value is None: return @@ -449,10 +458,12 @@ def validate_field_arg_default_value( def get_object_type_validation_data( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], ) -> GraphQLObjectValidationData: fields_attrs: List[str] = [ - attr_name for attr_name in cls.__annotations__ if not attr_name.startswith("__") + attr_name + for attr_name in get_all_annotations(cls) + if not attr_name.startswith("__") ] fields_instances: Dict[str, GraphQLObjectField] = {} @@ -494,12 +505,12 @@ def get_object_type_validation_data( def get_object_type_kwargs( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], aliases: Dict[str, str], ) -> Dict[str, Any]: kwargs: Dict[str, Any] = {} - for attr_name in cls.__annotations__: + for attr_name in get_all_annotations(cls): if attr_name.startswith("__"): continue @@ -527,7 +538,7 @@ def get_object_type_kwargs( def get_object_type_with_schema_kwargs( - cls: Type["GraphQLObject"], + cls: Type["GraphQLBaseObject"], aliases: Dict[str, str], field_names: List[str], ) -> Dict[str, Any]: diff --git a/ariadne_graphql_modules/next/compatibility_layer.py b/ariadne_graphql_modules/compatibility_layer.py similarity index 92% rename from ariadne_graphql_modules/next/compatibility_layer.py rename to ariadne_graphql_modules/compatibility_layer.py index 8106dc5..182e2a0 100644 --- a/ariadne_graphql_modules/next/compatibility_layer.py +++ b/ariadne_graphql_modules/compatibility_layer.py @@ -12,20 +12,19 @@ UnionTypeDefinitionNode, ) -from ..executable_schema import get_all_types +from .v1.executable_schema import get_all_types + +from .v1.directive_type import DirectiveType +from .v1.enum_type import EnumType +from .v1.input_type import InputType +from .v1.interface_type import InterfaceType +from .v1.mutation_type import MutationType +from .v1.scalar_type import ScalarType +from .v1.subscription_type import SubscriptionType +from .v1.union_type import UnionType +from .v1.object_type import ObjectType +from .v1.bases import BindableType -from ..directive_type import DirectiveType -from ..enum_type import EnumType -from ..input_type import InputType -from ..interface_type import InterfaceType -from ..mutation_type import MutationType -from ..scalar_type import ScalarType -from ..subscription_type import SubscriptionType -from ..union_type import UnionType - -from ..object_type import ObjectType - -from ..bases import BindableType from .base import GraphQLModel, GraphQLType from . import ( GraphQLObjectModel, diff --git a/ariadne_graphql_modules/next/convert_name.py b/ariadne_graphql_modules/convert_name.py similarity index 100% rename from ariadne_graphql_modules/next/convert_name.py rename to ariadne_graphql_modules/convert_name.py diff --git a/ariadne_graphql_modules/next/deferredtype.py b/ariadne_graphql_modules/deferredtype.py similarity index 100% rename from ariadne_graphql_modules/next/deferredtype.py rename to ariadne_graphql_modules/deferredtype.py diff --git a/ariadne_graphql_modules/next/description.py b/ariadne_graphql_modules/description.py similarity index 100% rename from ariadne_graphql_modules/next/description.py rename to ariadne_graphql_modules/description.py diff --git a/ariadne_graphql_modules/enum_type/__init__.py b/ariadne_graphql_modules/enum_type/__init__.py new file mode 100644 index 0000000..2782ef5 --- /dev/null +++ b/ariadne_graphql_modules/enum_type/__init__.py @@ -0,0 +1,9 @@ +from .graphql_type import GraphQLEnum, graphql_enum, create_graphql_enum_model +from .models import GraphQLEnumModel + +__all__ = [ + "GraphQLEnum", + "GraphQLEnumModel", + "create_graphql_enum_model", + "graphql_enum", +] diff --git a/ariadne_graphql_modules/next/graphql_enum_type/enum_type.py b/ariadne_graphql_modules/enum_type/graphql_type.py similarity index 99% rename from ariadne_graphql_modules/next/graphql_enum_type/enum_type.py rename to ariadne_graphql_modules/enum_type/graphql_type.py index 84a12e8..fc33745 100644 --- a/ariadne_graphql_modules/next/graphql_enum_type/enum_type.py +++ b/ariadne_graphql_modules/enum_type/graphql_type.py @@ -5,9 +5,9 @@ from graphql import EnumTypeDefinitionNode, EnumValueDefinitionNode, NameNode from ..base import GraphQLMetadata, GraphQLModel, GraphQLType from ..description import get_description_node -from ..graphql_enum_type.enum_model import GraphQLEnumModel +from .models import GraphQLEnumModel from ..validators import validate_description, validate_name -from ...utils import parse_definition +from ..utils import parse_definition class GraphQLEnum(GraphQLType): diff --git a/ariadne_graphql_modules/next/graphql_enum_type/enum_model.py b/ariadne_graphql_modules/enum_type/models.py similarity index 100% rename from ariadne_graphql_modules/next/graphql_enum_type/enum_model.py rename to ariadne_graphql_modules/enum_type/models.py diff --git a/ariadne_graphql_modules/enum_type/type.py b/ariadne_graphql_modules/enum_type/type.py new file mode 100644 index 0000000..fc33745 --- /dev/null +++ b/ariadne_graphql_modules/enum_type/type.py @@ -0,0 +1,343 @@ +from enum import Enum +from inspect import isclass +from typing import Any, Dict, Iterable, List, Optional, Set, Type, Union, cast + +from graphql import EnumTypeDefinitionNode, EnumValueDefinitionNode, NameNode +from ..base import GraphQLMetadata, GraphQLModel, GraphQLType +from ..description import get_description_node +from .models import GraphQLEnumModel +from ..validators import validate_description, validate_name +from ..utils import parse_definition + + +class GraphQLEnum(GraphQLType): + __abstract__: bool = True + __schema__: Optional[str] + __description__: Optional[str] + __members__: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] + __members_descriptions__: Optional[Dict[str, str]] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + cls._validate() + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": + name = cls.__get_graphql_name__() + + if getattr(cls, "__schema__", None): + return cls.__get_graphql_model_with_schema__(name) + + return cls.__get_graphql_model_without_schema__(name) + + @classmethod + def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLEnumModel": + definition: EnumTypeDefinitionNode = cast( + EnumTypeDefinitionNode, + parse_definition(EnumTypeDefinitionNode, cls.__schema__), + ) + + members = getattr(cls, "__members__", []) + members_values: Dict[str, Any] = {} + + if isinstance(members, dict): + members_values = dict(members.items()) + elif isclass(members) and issubclass(members, Enum): + members_values = {member.name: member for member in members} + else: + members_values = { + value.name.value: value.name.value + for value in definition.values # pylint: disable=no-member + } + + members_descriptions = getattr(cls, "__members_descriptions__", {}) + + return GraphQLEnumModel( + name=name, + members=members_values, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + directives=definition.directives, + description=definition.description + or (get_description_node(getattr(cls, "__description__", None))), + values=tuple( + EnumValueDefinitionNode( + name=value.name, + directives=value.directives, + description=value.description + or ( + get_description_node( + members_descriptions.get(value.name.value), + ) + ), + ) + for value in definition.values # pylint: disable=no-member + ), + ), + ) + + @classmethod + def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLEnumModel": + members = getattr(cls, "__members__", []) + members_values = {} + if isinstance(members, dict): + members_values = dict(members.items()) + elif isclass(members) and issubclass(members, Enum): + members_values = {i.name: i for i in members} + elif isinstance(members, list): + members_values = {kv: kv for kv in members} + + members_descriptions = getattr(cls, "__members_descriptions__", {}) + + return GraphQLEnumModel( + name=name, + members=members_values, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + values=tuple( + EnumValueDefinitionNode( + name=NameNode(value=value_name), + description=get_description_node( + members_descriptions.get(value_name) + ), + ) + for value_name in members_values + ), + ), + ) + + @classmethod + def _validate(cls): + if getattr(cls, "__schema__", None): + cls._validate_enum_type_with_schema() + else: + cls._validate_enum_type() + + @classmethod + def _validate_enum_type_with_schema(cls): + definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) + + if not isinstance(definition, EnumTypeDefinitionNode): + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + f"with declaration for an invalid GraphQL type. " + f"('{definition.__class__.__name__}' != '{EnumTypeDefinitionNode.__name__}')" + ) + + validate_name(cls, definition) + validate_description(cls, definition) + + members_names = { + value.name.value for value in definition.values # pylint: disable=no-member + } + if not members_names: + raise ValueError( + f"Class '{cls.__name__}' defines '__schema__' attribute " + "that doesn't declare any enum members." + ) + + members_values = getattr(cls, "__members__", None) + if members_values: + cls.validate_members_values(members_values, members_names) + + members_descriptions = getattr(cls, "__members_descriptions__", {}) + cls.validate_enum_members_descriptions(members_names, members_descriptions) + + duplicate_descriptions = [ + ast_member.name.value + for ast_member in definition.values # pylint: disable=no-member + if ast_member.description + and ast_member.description.value + and members_descriptions.get(ast_member.name.value) + ] + + if duplicate_descriptions: + raise ValueError( + f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " + f"descriptions for enum members that also have description in '__schema__' " + f"attribute. (members: '{', '.join(duplicate_descriptions)}')" + ) + + @classmethod + def validate_members_values(cls, members_values, members_names): + if isinstance(members_values, list): + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute " + "can't be a list when used together with '__schema__'." + ) + + missing_members = None + if isinstance(members_values, dict): + missing_members = members_names - set(members_values) + elif isclass(members_values) and issubclass(members_values, Enum): + missing_members = members_names - {value.name for value in members_values} + + if missing_members: + raise ValueError( + f"Class '{cls.__name__}' '__members__' is missing values " + f"for enum members defined in '__schema__'. " + f"(missing items: '{', '.join(missing_members)}')" + ) + + @classmethod + def _validate_enum_type(cls): + members_values = getattr(cls, "__members__", None) + if not members_values: + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute is either missing or " + "empty. Either define it or provide full SDL for this enum using " + "the '__schema__' attribute." + ) + + if not any( + [ + isinstance(members_values, (dict, list)), + isclass(members_values) and issubclass(members_values, Enum), + ] + ): + raise ValueError( + f"Class '{cls.__name__}' '__members__' attribute is of unsupported type. " + f"Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. " + f"(found: '{type(members_values)}')" + ) + + members_names = cls.get_members_set(members_values) + members_descriptions = getattr(cls, "__members_descriptions__", {}) + cls.validate_enum_members_descriptions(members_names, members_descriptions) + + @classmethod + def validate_enum_members_descriptions( + cls, members: Set[str], members_descriptions: dict + ): + invalid_descriptions = set(members_descriptions) - members + if invalid_descriptions: + invalid_descriptions_str = "', '".join(invalid_descriptions) + raise ValueError( + f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " + f"descriptions for undefined enum members. " + f"(undefined members: '{invalid_descriptions_str}')" + ) + + @staticmethod + def get_members_set( + members: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] + ) -> Set[str]: + if isinstance(members, dict): + return set(members.keys()) + + if isclass(members) and issubclass(members, Enum): + return set(member.name for member in members) + + if isinstance(members, list): + return set(members) + + raise TypeError( + f"Expected members to be of type Dict[str, Any], List[str], or Enum." + f"Got {type(members).__name__} instead." + ) + + +def create_graphql_enum_model( + enum: Type[Enum], + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[Dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +) -> "GraphQLEnumModel": + if members_include and members_exclude: + raise ValueError( + "'members_include' and 'members_exclude' options are mutually exclusive." + ) + + if hasattr(enum, "__get_graphql_model__"): + return cast(GraphQLEnumModel, enum.__get_graphql_model__()) + + if not name: + if hasattr(enum, "__get_graphql_name__"): + name = cast("str", enum.__get_graphql_name__()) + else: + name = enum.__name__ + + members: Dict[str, Any] = {i.name: i for i in enum} + final_members: Dict[str, Any] = {} + + if members_include: + for key, value in members.items(): + if key in members_include: + final_members[key] = value + elif members_exclude: + for key, value in members.items(): + if key not in members_exclude: + final_members[key] = value + else: + final_members = members + + members_descriptions = members_descriptions or {} + for member in members_descriptions: + if member not in final_members: + raise ValueError( + f"Member description was specified for a member '{member}' " + "not present in final GraphQL enum." + ) + + return GraphQLEnumModel( + name=name, + members=final_members, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node(description), + values=tuple( + EnumValueDefinitionNode( + name=NameNode(value=value_name), + description=get_description_node( + members_descriptions.get(value_name) + ), + ) + for value_name in final_members + ), + ), + ) + + +def graphql_enum( + cls: Optional[Type[Enum]] = None, + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[Dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +): + def graphql_enum_decorator(cls: Type[Enum]): + graphql_model = create_graphql_enum_model( + cls, + name=name, + description=description, + members_descriptions=members_descriptions, + members_include=members_include, + members_exclude=members_exclude, + ) + + def __get_graphql_model__(*_) -> GraphQLEnumModel: + return graphql_model + + setattr(cls, "__get_graphql_model__", classmethod(__get_graphql_model__)) + return cls + + if cls: + return graphql_enum_decorator(cls) + + return graphql_enum_decorator diff --git a/ariadne_graphql_modules/executable_schema.py b/ariadne_graphql_modules/executable_schema.py index 1914742..79da59f 100644 --- a/ariadne_graphql_modules/executable_schema.py +++ b/ariadne_graphql_modules/executable_schema.py @@ -1,236 +1,243 @@ -from typing import ( - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, - cast, -) +from enum import Enum +from typing import Any, Dict, List, Optional, Sequence, Type, Union from ariadne import ( SchemaBindable, SchemaDirectiveVisitor, + SchemaNameConverter, + convert_schema_names, repair_schema_default_enum_values, validate_schema_default_enum_values, ) from graphql import ( - ConstDirectiveNode, DocumentNode, - FieldDefinitionNode, GraphQLSchema, - NamedTypeNode, - ObjectTypeDefinitionNode, - TypeDefinitionNode, assert_valid_schema, build_ast_schema, - concat_ast, parse, + concat_ast, ) -from graphql.language import ast -from .bases import BaseType, BindableType, DeferredType, DefinitionType -from .enum_type import EnumType +from .base import GraphQLMetadata, GraphQLModel, GraphQLType +from .roots import ROOTS_NAMES, merge_root_nodes +from .sort import sort_schema_document -ROOT_TYPES = ["Query", "Mutation", "Subscription"] +SchemaType = Union[str, Enum, SchemaBindable, Type[GraphQLType], Type[Enum]] def make_executable_schema( - *args: Union[Type[BaseType], SchemaBindable, str], + *types: SchemaType, + directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, + convert_names_case: Union[bool, SchemaNameConverter] = False, merge_roots: bool = True, - extra_directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, -): - all_types = get_all_types(args) - extra_defs = parse_extra_sdl(args) - extra_bindables: List[SchemaBindable] = [ - arg for arg in args if isinstance(arg, SchemaBindable) +) -> GraphQLSchema: + metadata = GraphQLMetadata() + type_defs: List[str] = find_type_defs(types) + types_list: List[SchemaType] = flatten_types(types, metadata) + + assert_types_unique(types_list, merge_roots) + assert_types_not_abstract(types_list) + + schema_bindables: List[Union[SchemaBindable, GraphQLModel]] = [] + for type_def in types_list: + if isinstance(type_def, SchemaBindable): + schema_bindables.append(type_def) + elif isinstance(type_def, type) and issubclass(type_def, (GraphQLType, Enum)): + schema_bindables.append(metadata.get_graphql_model(type_def)) + + schema_models: List[GraphQLModel] = [ + type_def for type_def in schema_bindables if isinstance(type_def, GraphQLModel) ] - type_defs: List[Type[DefinitionType]] = [] - for type_ in all_types: - if issubclass(type_, DefinitionType): - type_defs.append(type_) + models_document: Optional[DocumentNode] = None + type_defs_document: Optional[DocumentNode] = None - validate_no_missing_definitions(all_types, type_defs, extra_defs) + if schema_models: + models_document = DocumentNode( + definitions=tuple(schema_model.ast for schema_model in schema_models), + ) + + if type_defs: + type_defs_document = parse("\n".join(type_defs)) + + if models_document and type_defs_document: + document_node = concat_ast((models_document, type_defs_document)) + elif models_document: + document_node = models_document + elif type_defs_document: + document_node = type_defs_document + else: + raise ValueError( + "'make_executable_schema' was called without any GraphQL types." + ) - schema = build_schema(type_defs, extra_defs, merge_roots) + if merge_roots: + document_node = merge_root_nodes(document_node) - if extra_bindables: - for bindable in extra_bindables: - bindable.bind_to_schema(schema) + document_node = sort_schema_document(document_node) + schema = build_ast_schema(document_node) - if extra_directives: - SchemaDirectiveVisitor.visit_schema_directives(schema, extra_directives) + if directives: + SchemaDirectiveVisitor.visit_schema_directives(schema, directives) assert_valid_schema(schema) validate_schema_default_enum_values(schema) repair_schema_default_enum_values(schema) - add_directives_to_schema(schema, type_defs) + for schema_bindable in schema_bindables: + schema_bindable.bind_to_schema(schema) + + if convert_names_case: + convert_schema_names( + schema, + convert_names_case if callable(convert_names_case) else None, + ) return schema -def get_all_types( - args: Sequence[Union[Type[BaseType], SchemaBindable, str]] -) -> List[Type[BaseType]]: - all_types: List[Type[BaseType]] = [] - for arg in args: - if isinstance(arg, (str, SchemaBindable)): - continue # Skip args of unsupported types +def find_type_defs(types: Sequence[SchemaType]) -> List[str]: + type_defs: List[str] = [] - for child_type in arg.__get_types__(): - if child_type not in all_types: - all_types.append(child_type) - return all_types + for type_def in types: + if isinstance(type_def, str): + type_defs.append(type_def) + elif isinstance(type_def, list): + type_defs += find_type_defs(type_def) + return type_defs -def parse_extra_sdl( - args: Sequence[Union[Type[BaseType], SchemaBindable, str]] -) -> List[TypeDefinitionNode]: - sdl_strings: List[str] = [cast(str, arg) for arg in args if isinstance(arg, str)] - if not sdl_strings: - return [] - extra_sdl = "\n\n".join(sdl_strings) - return cast( - List[TypeDefinitionNode], - list(parse(extra_sdl).definitions), +def flatten_types( + types: Sequence[SchemaType], + metadata: GraphQLMetadata, +) -> List[SchemaType]: + flat_schema_types_list: List[SchemaType] = flatten_schema_types( + types, metadata, dedupe=True ) + types_list: List[SchemaType] = [] + for type_def in flat_schema_types_list: + if isinstance(type_def, SchemaBindable): + types_list.append(type_def) -def validate_no_missing_definitions( - all_types: List[Type[BaseType]], - type_defs: List[Type[DefinitionType]], - extra_defs: List[TypeDefinitionNode], -): - deferred_names: List[str] = [] - for type_ in all_types: - if isinstance(type_, DeferredType): - deferred_names.append(type_.graphql_name) - - real_names = [type_.graphql_name for type_ in type_defs] - real_names += [definition.name.value for definition in extra_defs] - - missing_names = set(deferred_names) - set(real_names) - if missing_names: - raise ValueError( - "Following types are defined as deferred and are missing " - f"from schema: {', '.join(missing_names)}" - ) + elif isinstance(type_def, type) and issubclass(type_def, GraphQLType): + type_name = type_def.__name__ + if getattr(type_def, "__abstract__", None): + raise ValueError( + f"Type '{type_name}' is an abstract type and can't be used " + "for schema creation." + ) -def build_schema( - type_defs: List[Type[DefinitionType]], - extra_defs: List[TypeDefinitionNode], - merge_roots: bool = True, -) -> GraphQLSchema: - schema_definitions: List[ast.DocumentNode] = [] - if merge_roots: - schema_definitions.append(build_root_schema(type_defs, extra_defs)) - for type_ in type_defs: - if type_.graphql_name not in ROOT_TYPES or not merge_roots: - schema_definitions.append(parse(type_.__schema__)) - for extra_type_def in extra_defs: - if extra_type_def.name.value not in ROOT_TYPES or not merge_roots: - schema_definitions.append(DocumentNode(definitions=(extra_type_def,))) + types_list.append(type_def) - ast_document = concat_ast(schema_definitions) - schema = build_ast_schema(ast_document) + elif isinstance(type_def, type) and issubclass(type_def, Enum): + types_list.append(type_def) - for type_ in type_defs: - if issubclass(type_, BindableType): - type_.__bind_to_schema__(schema) + elif isinstance(type_def, list): + types_list += find_type_defs(type_def) - return schema + return types_list -RootTypeDef = Tuple[str, DocumentNode] +def flatten_schema_types( + types: Sequence[Union[SchemaType, List[SchemaType]]], + metadata: GraphQLMetadata, + dedupe: bool, +) -> List[SchemaType]: + flat_list: List[SchemaType] = [] + checked_types: List[Type[GraphQLType]] = [] + for type_def in types: + if isinstance(type_def, str): + continue + if isinstance(type_def, list): + flat_list += flatten_schema_types(type_def, metadata, dedupe=False) + elif isinstance(type_def, SchemaBindable): + flat_list.append(type_def) + elif isinstance(type_def, type) and issubclass(type_def, Enum): + flat_list.append(type_def) + elif isinstance(type_def, type) and issubclass(type_def, GraphQLType): + add_graphql_type_to_flat_list(flat_list, checked_types, type_def, metadata) + elif get_graphql_type_name(type_def): + flat_list.append(type_def) -def build_root_schema( - type_defs: List[Type[DefinitionType]], - extra_defs: List[TypeDefinitionNode], -) -> DocumentNode: - root_types: Dict[str, List[RootTypeDef]] = { - "Query": [], - "Mutation": [], - "Subscription": [], - } + if not dedupe: + return flat_list - for type_def in type_defs: - if type_def.graphql_name in root_types: - root_types[type_def.graphql_name].append( - (type_def.__name__, parse(type_def.__schema__)) - ) + unique_list: List[SchemaType] = [] + for type_def in flat_list: + if type_def not in unique_list: + unique_list.append(type_def) - for extra_type_def in extra_defs: - if extra_type_def.name.value in root_types: - root_types[extra_type_def.name.value].append( - ("extra_sdl", DocumentNode(definitions=(extra_type_def,))) - ) + return unique_list - schema: List[DocumentNode] = [] - for root_name, root_type_defs in root_types.items(): - if len(root_type_defs) == 1: - schema.append(root_type_defs[0][1]) - elif root_type_defs: - schema.append(merge_root_types(root_name, root_type_defs)) - return concat_ast(schema) +def add_graphql_type_to_flat_list( + flat_list: List[SchemaType], + checked_types: List[Type[GraphQLType]], + type_def: Type[GraphQLType], + metadata: GraphQLMetadata, +) -> None: + if type_def in checked_types: + return + checked_types.append(type_def) -def merge_root_types(root_name: str, type_defs: List[RootTypeDef]) -> DocumentNode: - interfaces: List[NamedTypeNode] = [] - directives: List[ConstDirectiveNode] = [] - fields: Dict[str, Tuple[str, FieldDefinitionNode]] = {} + for child_type in type_def.__get_graphql_types__(metadata): + flat_list.append(child_type) - for type_source, type_def in type_defs: - type_definition = cast(ObjectTypeDefinitionNode, type_def.definitions[0]) - interfaces.extend(type_definition.interfaces) - directives.extend(type_definition.directives) + if issubclass(child_type, GraphQLType): + add_graphql_type_to_flat_list( + flat_list, checked_types, child_type, metadata + ) - for field_def in type_definition.fields: - field_name = field_def.name.value - if field_name in fields: - other_type_source = fields[field_name][0] - raise ValueError( - f"Multiple {root_name} types are defining same field " - f"'{field_name}': {other_type_source}, {type_source}" - ) - fields[field_name] = (type_source, field_def) +def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: + if isinstance(type_def, SchemaBindable): + return None - merged_definition = ast.ObjectTypeDefinitionNode() - merged_definition.name = ast.NameNode() - merged_definition.name.value = root_name - merged_definition.interfaces = tuple(interfaces) - merged_definition.directives = tuple(directives) - merged_definition.fields = tuple( - fields[field_name][1] for field_name in sorted(fields) - ) + if isinstance(type_def, type) and issubclass(type_def, Enum): + return type_def.__name__ - merged_document = DocumentNode() - merged_document.definitions = (merged_definition,) + if isinstance(type_def, type) and issubclass(type_def, GraphQLType): + return type_def.__get_graphql_name__() - return merged_document + return None -def add_directives_to_schema( - schema: GraphQLSchema, type_defs: List[Type[DefinitionType]] -): - directives: Dict[str, Type[SchemaDirectiveVisitor]] = {} +def assert_types_unique(type_defs: List[SchemaType], merge_roots: bool): + types_names: Dict[str, Any] = {} for type_def in type_defs: - visitor = getattr(type_def, "__visitor__", None) - if visitor and issubclass(visitor, SchemaDirectiveVisitor): - directives[type_def.graphql_name] = visitor + type_name = get_graphql_type_name(type_def) + if not type_name: + continue + + if merge_roots and type_name in ROOTS_NAMES: + continue + + if type_name in types_names: + type_def_name = getattr(type_def, "__name__") or type_def + raise ValueError( + f"Types '{type_def_name}' and '{types_names[type_name]}' both define " + f"GraphQL type with name '{type_name}'." + ) - if directives: - SchemaDirectiveVisitor.visit_schema_directives(schema, directives) + types_names[type_name] = type_def -def repair_default_enum_values(schema, types_list: List[Type[DefinitionType]]) -> None: - for type_ in types_list: - if issubclass(type_, EnumType): - type_.__bind_to_default_values__(schema) +def assert_types_not_abstract(type_defs: List[SchemaType]): + for type_def in type_defs: + if isinstance(type_def, SchemaBindable): + continue + + if ( + isinstance(type_def, type) + and issubclass(type_def, GraphQLType) + and getattr(type_def, "__abstract__", None) + ): + raise ValueError( + f"Type '{type_def.__name__}' is an abstract type and can't be used " + "for schema creation." + ) diff --git a/ariadne_graphql_modules/next/idtype.py b/ariadne_graphql_modules/idtype.py similarity index 100% rename from ariadne_graphql_modules/next/idtype.py rename to ariadne_graphql_modules/idtype.py diff --git a/ariadne_graphql_modules/input_type/__init__.py b/ariadne_graphql_modules/input_type/__init__.py new file mode 100644 index 0000000..23a8e12 --- /dev/null +++ b/ariadne_graphql_modules/input_type/__init__.py @@ -0,0 +1,8 @@ +from .graphql_type import GraphQLInput +from .models import GraphQLInputModel + + +__all__ = [ + "GraphQLInput", + "GraphQLInputModel", +] diff --git a/ariadne_graphql_modules/next/graphql_input/input_field.py b/ariadne_graphql_modules/input_type/graphql_field.py similarity index 100% rename from ariadne_graphql_modules/next/graphql_input/input_field.py rename to ariadne_graphql_modules/input_type/graphql_field.py diff --git a/ariadne_graphql_modules/next/graphql_input/input_type.py b/ariadne_graphql_modules/input_type/graphql_type.py similarity index 97% rename from ariadne_graphql_modules/next/graphql_input/input_type.py rename to ariadne_graphql_modules/input_type/graphql_type.py index 6d03a05..9670ea4 100644 --- a/ariadne_graphql_modules/next/graphql_input/input_type.py +++ b/ariadne_graphql_modules/input_type/graphql_type.py @@ -4,21 +4,21 @@ from graphql import InputObjectTypeDefinitionNode, InputValueDefinitionNode, NameNode -from .input_field import GraphQLInputField +from .graphql_field import GraphQLInputField from ..base import GraphQLMetadata, GraphQLModel, GraphQLType from ..convert_name import ( convert_graphql_name_to_python, convert_python_name_to_graphql, ) from ..description import get_description_node -from ..graphql_input.input_model import GraphQLInputModel -from ..graphql_input.validators import ( +from .models import GraphQLInputModel +from .validators import ( validate_input_type, validate_input_type_with_schema, ) from ..typing import get_graphql_type, get_type_node from ..value import get_value_node -from ...utils import parse_definition +from ..utils import parse_definition class GraphQLInput(GraphQLType): diff --git a/ariadne_graphql_modules/next/graphql_input/input_model.py b/ariadne_graphql_modules/input_type/models.py similarity index 100% rename from ariadne_graphql_modules/next/graphql_input/input_model.py rename to ariadne_graphql_modules/input_type/models.py diff --git a/ariadne_graphql_modules/next/graphql_input/validators.py b/ariadne_graphql_modules/input_type/validators.py similarity index 97% rename from ariadne_graphql_modules/next/graphql_input/validators.py rename to ariadne_graphql_modules/input_type/validators.py index 852a778..68432b8 100644 --- a/ariadne_graphql_modules/next/graphql_input/validators.py +++ b/ariadne_graphql_modules/input_type/validators.py @@ -4,11 +4,11 @@ from ..convert_name import convert_graphql_name_to_python from ..validators import validate_description, validate_name from ..value import get_value_from_node, get_value_node -from ...utils import parse_definition -from .input_field import GraphQLInputField +from ..utils import parse_definition +from .graphql_field import GraphQLInputField if TYPE_CHECKING: - from .input_type import GraphQLInput + from .graphql_type import GraphQLInput def validate_input_type_with_schema(cls: Type["GraphQLInput"]) -> Dict[str, Any]: diff --git a/ariadne_graphql_modules/interface_type/__init__.py b/ariadne_graphql_modules/interface_type/__init__.py new file mode 100644 index 0000000..0ec79d8 --- /dev/null +++ b/ariadne_graphql_modules/interface_type/__init__.py @@ -0,0 +1,8 @@ +from .graphql_type import GraphQLInterface +from .models import GraphQLInterfaceModel + + +__all__ = [ + "GraphQLInterface", + "GraphQLInterfaceModel", +] diff --git a/ariadne_graphql_modules/interface_type/graphql_type.py b/ariadne_graphql_modules/interface_type/graphql_type.py new file mode 100644 index 0000000..9a33c14 --- /dev/null +++ b/ariadne_graphql_modules/interface_type/graphql_type.py @@ -0,0 +1,147 @@ +from typing import Any, Dict, List, Optional, Tuple, cast + +from ariadne.types import Resolver +from graphql import ( + FieldDefinitionNode, + InterfaceTypeDefinitionNode, + NameNode, + NamedTypeNode, +) + +from ..base_object_type import ( + GraphQLFieldData, + GraphQLBaseObject, + GraphQLObjectData, + validate_object_type_with_schema, + validate_object_type_without_schema, +) +from ..types import GraphQLClassType + +from ..utils import parse_definition +from ..base import GraphQLMetadata +from ..description import get_description_node +from ..object_type import GraphQLObject +from .models import GraphQLInterfaceModel + + +class GraphQLInterface(GraphQLBaseObject): + __valid_type__ = InterfaceTypeDefinitionNode + __graphql_type__ = GraphQLClassType.INTERFACE + __abstract__ = True + __description__: Optional[str] = None + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + valid_type = getattr(cls, "__valid_type__", InterfaceTypeDefinitionNode) + cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) + else: + cls.__kwargs__ = validate_object_type_without_schema(cls) + + @classmethod + def __get_graphql_model_with_schema__(cls) -> "GraphQLInterfaceModel": + definition = cast( + InterfaceTypeDefinitionNode, + parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), + ) + + resolvers: Dict[str, Resolver] = {} + fields: Tuple[FieldDefinitionNode, ...] = tuple() + fields, resolvers = cls._create_fields_and_resolvers_with_schema( + definition.fields + ) + + return GraphQLInterfaceModel( + name=definition.name.value, + ast_type=InterfaceTypeDefinitionNode, + ast=InterfaceTypeDefinitionNode( + name=NameNode(value=definition.name.value), + fields=tuple(fields), + interfaces=definition.interfaces, + ), + resolve_type=cls.resolve_type, + resolvers=resolvers, + aliases=getattr(cls, "__aliases__", {}), + out_names={}, + ) + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLInterfaceModel": + type_data = cls.get_graphql_object_data(metadata) + type_aliases = getattr(cls, "__aliases__", None) or {} + + fields_ast: List[FieldDefinitionNode] = [] + resolvers: Dict[str, Resolver] = {} + aliases: Dict[str, str] = {} + out_names: Dict[str, Dict[str, str]] = {} + + fields_ast, resolvers, aliases, out_names = cls._process_graphql_fields( + metadata, type_data, type_aliases + ) + + interfaces_ast: List[NamedTypeNode] = [] + for interface_name in type_data.interfaces: + interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) + + return GraphQLInterfaceModel( + name=name, + ast_type=InterfaceTypeDefinitionNode, + ast=InterfaceTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + fields=tuple(fields_ast), + interfaces=tuple(interfaces_ast), + ), + resolve_type=cls.resolve_type, + resolvers=resolvers, + aliases=aliases, + out_names=out_names, + ) + + @staticmethod + def resolve_type(obj: Any, *_) -> str: + if isinstance(obj, GraphQLObject): + return obj.__get_graphql_name__() + + raise ValueError( + f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." + ) + + @classmethod + def _collect_inherited_objects(cls): + return [ + inherited_obj + for inherited_obj in cls.__mro__[1:] + if getattr(inherited_obj, "__graphql_type__", None) + == GraphQLClassType.INTERFACE + and not getattr(inherited_obj, "__abstract__", True) + ] + + @classmethod + def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: + fields_data = GraphQLFieldData() + inherited_objects = list(reversed(cls._collect_inherited_objects())) + + for inherited_obj in inherited_objects: + fields_data.type_hints.update(inherited_obj.__annotations__) + fields_data.aliases.update(getattr(inherited_obj, "__aliases__", {})) + + cls._process_type_hints_and_aliases(fields_data) + for inherited_obj in inherited_objects: + cls._process_class_attributes(inherited_obj, fields_data) + cls._process_class_attributes(cls, fields_data) + + return GraphQLObjectData( + fields=cls._build_fields(fields_data=fields_data), + interfaces=[], + ) diff --git a/ariadne_graphql_modules/next/graphql_interface/interface_model.py b/ariadne_graphql_modules/interface_type/models.py similarity index 100% rename from ariadne_graphql_modules/next/graphql_interface/interface_model.py rename to ariadne_graphql_modules/interface_type/models.py diff --git a/ariadne_graphql_modules/next/__init__.py b/ariadne_graphql_modules/next/__init__.py deleted file mode 100644 index 6aea1ab..0000000 --- a/ariadne_graphql_modules/next/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .convert_name import ( - convert_graphql_name_to_python, - convert_python_name_to_graphql, -) -from .deferredtype import deferred -from .description import get_description_node -from .graphql_enum_type import ( - GraphQLEnum, - GraphQLEnumModel, - create_graphql_enum_model, - graphql_enum, -) -from .executable_schema import make_executable_schema -from .idtype import GraphQLID -from .graphql_input import GraphQLInput, GraphQLInputModel -from .graphql_object import GraphQLObject, GraphQLObjectModel, object_field -from .roots import ROOTS_NAMES, merge_root_nodes -from .graphql_scalar import GraphQLScalar, GraphQLScalarModel -from .sort import sort_schema_document -from .graphql_union import GraphQLUnion, GraphQLUnionModel -from .value import get_value_from_node, get_value_node -from .graphql_interface import GraphQLInterface, GraphQLInterfaceModel -from .graphql_subscription import GraphQLSubscription, GraphQLSubscriptionModel - -__all__ = [ - "GraphQLEnum", - "GraphQLEnumModel", - "GraphQLID", - "GraphQLInput", - "GraphQLInputModel", - "GraphQLInterface", - "GraphQLInterfaceModel", - "GraphQLSubscription", - "GraphQLSubscriptionModel", - "GraphQLMetadata", - "GraphQLModel", - "GraphQLObject", - "GraphQLObjectModel", - "GraphQLScalar", - "GraphQLScalarModel", - "GraphQLType", - "GraphQLUnion", - "GraphQLUnionModel", - "ROOTS_NAMES", - "convert_graphql_name_to_python", - "convert_python_name_to_graphql", - "create_graphql_enum_model", - "deferred", - "get_description_node", - "get_value_from_node", - "get_value_node", - "graphql_enum", - "make_executable_schema", - "merge_root_nodes", - "object_field", - "sort_schema_document", -] diff --git a/ariadne_graphql_modules/next/executable_schema.py b/ariadne_graphql_modules/next/executable_schema.py deleted file mode 100644 index 79da59f..0000000 --- a/ariadne_graphql_modules/next/executable_schema.py +++ /dev/null @@ -1,243 +0,0 @@ -from enum import Enum -from typing import Any, Dict, List, Optional, Sequence, Type, Union - -from ariadne import ( - SchemaBindable, - SchemaDirectiveVisitor, - SchemaNameConverter, - convert_schema_names, - repair_schema_default_enum_values, - validate_schema_default_enum_values, -) -from graphql import ( - DocumentNode, - GraphQLSchema, - assert_valid_schema, - build_ast_schema, - parse, - concat_ast, -) - -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .roots import ROOTS_NAMES, merge_root_nodes -from .sort import sort_schema_document - -SchemaType = Union[str, Enum, SchemaBindable, Type[GraphQLType], Type[Enum]] - - -def make_executable_schema( - *types: SchemaType, - directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, - convert_names_case: Union[bool, SchemaNameConverter] = False, - merge_roots: bool = True, -) -> GraphQLSchema: - metadata = GraphQLMetadata() - type_defs: List[str] = find_type_defs(types) - types_list: List[SchemaType] = flatten_types(types, metadata) - - assert_types_unique(types_list, merge_roots) - assert_types_not_abstract(types_list) - - schema_bindables: List[Union[SchemaBindable, GraphQLModel]] = [] - for type_def in types_list: - if isinstance(type_def, SchemaBindable): - schema_bindables.append(type_def) - elif isinstance(type_def, type) and issubclass(type_def, (GraphQLType, Enum)): - schema_bindables.append(metadata.get_graphql_model(type_def)) - - schema_models: List[GraphQLModel] = [ - type_def for type_def in schema_bindables if isinstance(type_def, GraphQLModel) - ] - - models_document: Optional[DocumentNode] = None - type_defs_document: Optional[DocumentNode] = None - - if schema_models: - models_document = DocumentNode( - definitions=tuple(schema_model.ast for schema_model in schema_models), - ) - - if type_defs: - type_defs_document = parse("\n".join(type_defs)) - - if models_document and type_defs_document: - document_node = concat_ast((models_document, type_defs_document)) - elif models_document: - document_node = models_document - elif type_defs_document: - document_node = type_defs_document - else: - raise ValueError( - "'make_executable_schema' was called without any GraphQL types." - ) - - if merge_roots: - document_node = merge_root_nodes(document_node) - - document_node = sort_schema_document(document_node) - schema = build_ast_schema(document_node) - - if directives: - SchemaDirectiveVisitor.visit_schema_directives(schema, directives) - - assert_valid_schema(schema) - validate_schema_default_enum_values(schema) - repair_schema_default_enum_values(schema) - - for schema_bindable in schema_bindables: - schema_bindable.bind_to_schema(schema) - - if convert_names_case: - convert_schema_names( - schema, - convert_names_case if callable(convert_names_case) else None, - ) - - return schema - - -def find_type_defs(types: Sequence[SchemaType]) -> List[str]: - type_defs: List[str] = [] - - for type_def in types: - if isinstance(type_def, str): - type_defs.append(type_def) - elif isinstance(type_def, list): - type_defs += find_type_defs(type_def) - - return type_defs - - -def flatten_types( - types: Sequence[SchemaType], - metadata: GraphQLMetadata, -) -> List[SchemaType]: - flat_schema_types_list: List[SchemaType] = flatten_schema_types( - types, metadata, dedupe=True - ) - - types_list: List[SchemaType] = [] - for type_def in flat_schema_types_list: - if isinstance(type_def, SchemaBindable): - types_list.append(type_def) - - elif isinstance(type_def, type) and issubclass(type_def, GraphQLType): - type_name = type_def.__name__ - - if getattr(type_def, "__abstract__", None): - raise ValueError( - f"Type '{type_name}' is an abstract type and can't be used " - "for schema creation." - ) - - types_list.append(type_def) - - elif isinstance(type_def, type) and issubclass(type_def, Enum): - types_list.append(type_def) - - elif isinstance(type_def, list): - types_list += find_type_defs(type_def) - - return types_list - - -def flatten_schema_types( - types: Sequence[Union[SchemaType, List[SchemaType]]], - metadata: GraphQLMetadata, - dedupe: bool, -) -> List[SchemaType]: - flat_list: List[SchemaType] = [] - checked_types: List[Type[GraphQLType]] = [] - - for type_def in types: - if isinstance(type_def, str): - continue - if isinstance(type_def, list): - flat_list += flatten_schema_types(type_def, metadata, dedupe=False) - elif isinstance(type_def, SchemaBindable): - flat_list.append(type_def) - elif isinstance(type_def, type) and issubclass(type_def, Enum): - flat_list.append(type_def) - elif isinstance(type_def, type) and issubclass(type_def, GraphQLType): - add_graphql_type_to_flat_list(flat_list, checked_types, type_def, metadata) - elif get_graphql_type_name(type_def): - flat_list.append(type_def) - - if not dedupe: - return flat_list - - unique_list: List[SchemaType] = [] - for type_def in flat_list: - if type_def not in unique_list: - unique_list.append(type_def) - - return unique_list - - -def add_graphql_type_to_flat_list( - flat_list: List[SchemaType], - checked_types: List[Type[GraphQLType]], - type_def: Type[GraphQLType], - metadata: GraphQLMetadata, -) -> None: - if type_def in checked_types: - return - - checked_types.append(type_def) - - for child_type in type_def.__get_graphql_types__(metadata): - flat_list.append(child_type) - - if issubclass(child_type, GraphQLType): - add_graphql_type_to_flat_list( - flat_list, checked_types, child_type, metadata - ) - - -def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: - if isinstance(type_def, SchemaBindable): - return None - - if isinstance(type_def, type) and issubclass(type_def, Enum): - return type_def.__name__ - - if isinstance(type_def, type) and issubclass(type_def, GraphQLType): - return type_def.__get_graphql_name__() - - return None - - -def assert_types_unique(type_defs: List[SchemaType], merge_roots: bool): - types_names: Dict[str, Any] = {} - for type_def in type_defs: - type_name = get_graphql_type_name(type_def) - if not type_name: - continue - - if merge_roots and type_name in ROOTS_NAMES: - continue - - if type_name in types_names: - type_def_name = getattr(type_def, "__name__") or type_def - raise ValueError( - f"Types '{type_def_name}' and '{types_names[type_name]}' both define " - f"GraphQL type with name '{type_name}'." - ) - - types_names[type_name] = type_def - - -def assert_types_not_abstract(type_defs: List[SchemaType]): - for type_def in type_defs: - if isinstance(type_def, SchemaBindable): - continue - - if ( - isinstance(type_def, type) - and issubclass(type_def, GraphQLType) - and getattr(type_def, "__abstract__", None) - ): - raise ValueError( - f"Type '{type_def.__name__}' is an abstract type and can't be used " - "for schema creation." - ) diff --git a/ariadne_graphql_modules/next/graphql_enum_type/__init__.py b/ariadne_graphql_modules/next/graphql_enum_type/__init__.py deleted file mode 100644 index a4c8666..0000000 --- a/ariadne_graphql_modules/next/graphql_enum_type/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .enum_type import GraphQLEnum, graphql_enum, create_graphql_enum_model -from .enum_model import GraphQLEnumModel - -__all__ = [ - "GraphQLEnum", - "GraphQLEnumModel", - "create_graphql_enum_model", - "graphql_enum", -] diff --git a/ariadne_graphql_modules/next/graphql_input/__init__.py b/ariadne_graphql_modules/next/graphql_input/__init__.py deleted file mode 100644 index 5d1e0b5..0000000 --- a/ariadne_graphql_modules/next/graphql_input/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .input_type import GraphQLInput -from .input_model import GraphQLInputModel - - -__all__ = [ - "GraphQLInput", - "GraphQLInputModel", -] diff --git a/ariadne_graphql_modules/next/graphql_interface/__init__.py b/ariadne_graphql_modules/next/graphql_interface/__init__.py deleted file mode 100644 index 7ccd6d6..0000000 --- a/ariadne_graphql_modules/next/graphql_interface/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .interface_type import GraphQLInterface -from .interface_model import GraphQLInterfaceModel - - -__all__ = [ - "GraphQLInterface", - "GraphQLInterfaceModel", -] diff --git a/ariadne_graphql_modules/next/graphql_interface/interface_type.py b/ariadne_graphql_modules/next/graphql_interface/interface_type.py deleted file mode 100644 index 1a7832b..0000000 --- a/ariadne_graphql_modules/next/graphql_interface/interface_type.py +++ /dev/null @@ -1,175 +0,0 @@ -from typing import Any, Dict, List, cast - -from ariadne.types import Resolver -from graphql import ( - FieldDefinitionNode, - InputValueDefinitionNode, - InterfaceTypeDefinitionNode, - NameNode, - NamedTypeNode, - StringValueNode, -) - -from ...utils import parse_definition -from ..base import GraphQLMetadata -from ..description import get_description_node -from ..graphql_object import GraphQLObject, GraphQLObjectResolver -from ..graphql_object.object_type import get_graphql_object_data -from ..graphql_object.utils import ( - get_field_args_from_resolver, - get_field_args_out_names, - get_field_node_from_obj_field, - update_field_args_options, -) -from ..value import get_value_node -from .interface_model import GraphQLInterfaceModel - - -class GraphQLInterface(GraphQLObject): - __abstract__: bool = True - __valid_type__ = InterfaceTypeDefinitionNode - - @classmethod - def __get_graphql_model_with_schema__(cls) -> "GraphQLInterfaceModel": - definition = cast( - InterfaceTypeDefinitionNode, - parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), - ) - - descriptions: Dict[str, StringValueNode] = {} - args_descriptions: Dict[str, Dict[str, StringValueNode]] = {} - args_defaults: Dict[str, Dict[str, Any]] = {} - resolvers: Dict[str, Resolver] = {} - out_names: Dict[str, Dict[str, str]] = {} - - for attr_name in dir(cls): - cls_attr = getattr(cls, attr_name) - if isinstance(cls_attr, GraphQLObjectResolver): - resolvers[cls_attr.field] = cls_attr.resolver - description_node = get_description_node(cls_attr.description) - if description_node: - descriptions[cls_attr.field] = description_node - - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - args_descriptions[cls_attr.field] = {} - args_defaults[cls_attr.field] = {} - - final_args = update_field_args_options(field_args, cls_attr.args) - - for arg_name, arg_options in final_args.items(): - arg_description = get_description_node(arg_options.description) - if arg_description: - args_descriptions[cls_attr.field][ - arg_name - ] = arg_description - - arg_default = arg_options.default_value - if arg_default is not None: - args_defaults[cls_attr.field][arg_name] = get_value_node( - arg_default - ) - - fields: List[FieldDefinitionNode] = [] - for field in definition.fields: - field_args_descriptions = args_descriptions.get(field.name.value, {}) - field_args_defaults = args_defaults.get(field.name.value, {}) - - args: List[InputValueDefinitionNode] = [] - for arg in field.arguments: - arg_name = arg.name.value - args.append( - InputValueDefinitionNode( - description=( - arg.description or field_args_descriptions.get(arg_name) - ), - name=arg.name, - directives=arg.directives, - type=arg.type, - default_value=( - arg.default_value or field_args_defaults.get(arg_name) - ), - ) - ) - - fields.append( - FieldDefinitionNode( - name=field.name, - description=( - field.description or descriptions.get(field.name.value) - ), - directives=field.directives, - arguments=tuple(args), - type=field.type, - ) - ) - - return GraphQLInterfaceModel( - name=definition.name.value, - ast_type=InterfaceTypeDefinitionNode, - ast=InterfaceTypeDefinitionNode( - name=NameNode(value=definition.name.value), - fields=tuple(fields), - interfaces=definition.interfaces, - ), - resolve_type=cls.resolve_type, - resolvers=resolvers, - aliases=getattr(cls, "__aliases__", {}), - out_names=out_names, - ) - - @classmethod - def __get_graphql_model_without_schema__( - cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLInterfaceModel": - type_data = get_graphql_object_data(metadata, cls) - type_aliases = getattr(cls, "__aliases__", None) or {} - - fields_ast: List[FieldDefinitionNode] = [] - resolvers: Dict[str, Resolver] = {} - aliases: Dict[str, str] = {} - out_names: Dict[str, Dict[str, str]] = {} - - for attr_name, field in type_data.fields.items(): - fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) - - if attr_name in type_aliases and field.name: - aliases[field.name] = type_aliases[attr_name] - elif field.name and attr_name != field.name and not field.resolver: - aliases[field.name] = attr_name - - if field.resolver and field.name: - resolvers[field.name] = field.resolver - - if field.args and field.name: - out_names[field.name] = get_field_args_out_names(field.args) - - interfaces_ast: List[NamedTypeNode] = [] - for interface_name in type_data.interfaces: - interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) - - return GraphQLInterfaceModel( - name=name, - ast_type=InterfaceTypeDefinitionNode, - ast=InterfaceTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node( - getattr(cls, "__description__", None), - ), - fields=tuple(fields_ast), - interfaces=tuple(interfaces_ast), - ), - resolve_type=cls.resolve_type, - resolvers=resolvers, - aliases=aliases, - out_names=out_names, - ) - - @staticmethod - def resolve_type(obj: Any, *_) -> str: - if isinstance(obj, GraphQLObject): - return obj.__get_graphql_name__() - - raise ValueError( - f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." - ) diff --git a/ariadne_graphql_modules/next/graphql_scalar/__init__.py b/ariadne_graphql_modules/next/graphql_scalar/__init__.py deleted file mode 100644 index 1c1d0d2..0000000 --- a/ariadne_graphql_modules/next/graphql_scalar/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .scalar_type import GraphQLScalar -from .scalar_model import GraphQLScalarModel - - -__all__ = [ - "GraphQLScalar", - "GraphQLScalarModel", -] diff --git a/ariadne_graphql_modules/next/graphql_subscription/__init__.py b/ariadne_graphql_modules/next/graphql_subscription/__init__.py deleted file mode 100644 index 962d24e..0000000 --- a/ariadne_graphql_modules/next/graphql_subscription/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .subscription_type import GraphQLSubscription -from .subscription_model import GraphQLSubscriptionModel - - -__all__ = [ - "GraphQLSubscription", - "GraphQLSubscriptionModel", -] diff --git a/ariadne_graphql_modules/next/graphql_union/__init__.py b/ariadne_graphql_modules/next/graphql_union/__init__.py deleted file mode 100644 index 7e9462d..0000000 --- a/ariadne_graphql_modules/next/graphql_union/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .union_type import GraphQLUnion -from .union_model import GraphQLUnionModel - - -__all__ = [ - "GraphQLUnion", - "GraphQLUnionModel", -] diff --git a/ariadne_graphql_modules/next/graphql_object/__init__.py b/ariadne_graphql_modules/object_type/__init__.py similarity index 77% rename from ariadne_graphql_modules/next/graphql_object/__init__.py rename to ariadne_graphql_modules/object_type/__init__.py index a2b2c67..03db715 100644 --- a/ariadne_graphql_modules/next/graphql_object/__init__.py +++ b/ariadne_graphql_modules/object_type/__init__.py @@ -1,13 +1,13 @@ -from .object_field import ( +from ..base_object_type.graphql_field import ( object_field, GraphQLObjectResolver, GraphQLObjectSource, object_subscriber, GraphQLObjectFieldArg, ) -from .object_type import GraphQLObject, get_graphql_object_data -from .object_model import GraphQLObjectModel -from .utils import ( +from .graphql_type import GraphQLObject +from .models import GraphQLObjectModel +from ..base_object_type.utils import ( get_field_args_from_resolver, get_field_args_out_names, get_field_node_from_obj_field, @@ -24,7 +24,6 @@ "get_field_node_from_obj_field", "update_field_args_options", "GraphQLObjectResolver", - "get_graphql_object_data", "GraphQLObjectSource", "object_subscriber", "get_field_args_from_subscriber", diff --git a/ariadne_graphql_modules/object_type/graphql_type.py b/ariadne_graphql_modules/object_type/graphql_type.py new file mode 100644 index 0000000..89e0227 --- /dev/null +++ b/ariadne_graphql_modules/object_type/graphql_type.py @@ -0,0 +1,148 @@ +from typing import ( + Dict, + List, + Optional, + Tuple, + cast, +) + +from ariadne.types import Resolver +from graphql import ( + FieldDefinitionNode, + NameNode, + NamedTypeNode, + ObjectTypeDefinitionNode, +) + +from ..types import GraphQLClassType + +from ..base_object_type import ( + GraphQLFieldData, + GraphQLBaseObject, + GraphQLObjectData, + validate_object_type_with_schema, + validate_object_type_without_schema, +) +from .models import GraphQLObjectModel + + +from ..utils import parse_definition +from ..base import GraphQLMetadata, GraphQLModel +from ..description import get_description_node + + +class GraphQLObject(GraphQLBaseObject): + __valid_type__ = ObjectTypeDefinitionNode + __graphql_type__ = GraphQLClassType.OBJECT + __abstract__ = True + __description__: Optional[str] = None + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) + cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) + else: + cls.__kwargs__ = validate_object_type_without_schema(cls) + + @classmethod + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": + definition = cast( + ObjectTypeDefinitionNode, + parse_definition(ObjectTypeDefinitionNode, cls.__schema__), + ) + + resolvers: Dict[str, Resolver] = {} + fields: Tuple[FieldDefinitionNode, ...] = tuple() + fields, resolvers = cls._create_fields_and_resolvers_with_schema( + definition.fields + ) + + return GraphQLObjectModel( + name=definition.name.value, + ast_type=ObjectTypeDefinitionNode, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=definition.name.value), + fields=tuple(fields), + interfaces=definition.interfaces, + ), + resolvers=resolvers, + aliases=getattr(cls, "__aliases__", {}), + out_names={}, + ) + + @classmethod + def __get_graphql_model_without_schema__( + cls, metadata: GraphQLMetadata, name: str + ) -> "GraphQLModel": + type_data = cls.get_graphql_object_data(metadata) + type_aliases = getattr(cls, "__aliases__", {}) + + fields_ast: List[FieldDefinitionNode] = [] + resolvers: Dict[str, Resolver] = {} + aliases: Dict[str, str] = {} + out_names: Dict[str, Dict[str, str]] = {} + + fields_ast, resolvers, aliases, out_names = cls._process_graphql_fields( + metadata, type_data, type_aliases + ) + + interfaces_ast: List[NamedTypeNode] = [] + for interface_name in type_data.interfaces: + interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) + + return GraphQLObjectModel( + name=name, + ast_type=ObjectTypeDefinitionNode, + ast=ObjectTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node( + getattr(cls, "__description__", None), + ), + fields=tuple(fields_ast), + interfaces=tuple(interfaces_ast), + ), + resolvers=resolvers, + aliases=aliases, + out_names=out_names, + ) + + @classmethod + def _collect_inherited_objects(cls): + return [ + inherited_obj + for inherited_obj in cls.__mro__[1:] + if getattr(inherited_obj, "__graphql_type__", None) + in (GraphQLClassType.INTERFACE, GraphQLClassType.OBJECT) + and not getattr(inherited_obj, "__abstract__", True) + ] + + @classmethod + def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: + fields_data = GraphQLFieldData() + inherited_objects = list(reversed(cls._collect_inherited_objects())) + + for inherited_obj in inherited_objects: + fields_data.type_hints.update(inherited_obj.__annotations__) + fields_data.aliases.update(getattr(inherited_obj, "__aliases__", {})) + + cls._process_type_hints_and_aliases(fields_data) + + for inherited_obj in inherited_objects: + cls._process_class_attributes(inherited_obj, fields_data) + cls._process_class_attributes(cls, fields_data) + return GraphQLObjectData( + fields=cls._build_fields(fields_data=fields_data), + interfaces=[ + interface.__name__ + for interface in inherited_objects + if getattr(interface, "__graphql_type__", None) + == GraphQLClassType.INTERFACE + ], + ) diff --git a/ariadne_graphql_modules/next/graphql_object/object_model.py b/ariadne_graphql_modules/object_type/models.py similarity index 94% rename from ariadne_graphql_modules/next/graphql_object/object_model.py rename to ariadne_graphql_modules/object_type/models.py index 7a75065..1241188 100644 --- a/ariadne_graphql_modules/next/graphql_object/object_model.py +++ b/ariadne_graphql_modules/object_type/models.py @@ -5,7 +5,7 @@ from ariadne.types import Resolver from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema -from ariadne_graphql_modules.next.base import GraphQLModel +from ..base import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/next/roots.py b/ariadne_graphql_modules/roots.py similarity index 100% rename from ariadne_graphql_modules/next/roots.py rename to ariadne_graphql_modules/roots.py diff --git a/ariadne_graphql_modules/scalar_type/__init__.py b/ariadne_graphql_modules/scalar_type/__init__.py new file mode 100644 index 0000000..82909c7 --- /dev/null +++ b/ariadne_graphql_modules/scalar_type/__init__.py @@ -0,0 +1,8 @@ +from .graphql_type import GraphQLScalar +from .models import GraphQLScalarModel + + +__all__ = [ + "GraphQLScalar", + "GraphQLScalarModel", +] diff --git a/ariadne_graphql_modules/next/graphql_scalar/scalar_type.py b/ariadne_graphql_modules/scalar_type/graphql_type.py similarity index 97% rename from ariadne_graphql_modules/next/graphql_scalar/scalar_type.py rename to ariadne_graphql_modules/scalar_type/graphql_type.py index 75436c8..60f27f3 100644 --- a/ariadne_graphql_modules/next/graphql_scalar/scalar_type.py +++ b/ariadne_graphql_modules/scalar_type/graphql_type.py @@ -8,9 +8,9 @@ ) from ..description import get_description_node -from .scalar_model import GraphQLScalarModel +from .models import GraphQLScalarModel -from ...utils import parse_definition +from ..utils import parse_definition from .validators import validate_scalar_type_with_schema diff --git a/ariadne_graphql_modules/next/graphql_scalar/scalar_model.py b/ariadne_graphql_modules/scalar_type/models.py similarity index 100% rename from ariadne_graphql_modules/next/graphql_scalar/scalar_model.py rename to ariadne_graphql_modules/scalar_type/models.py diff --git a/ariadne_graphql_modules/next/graphql_scalar/validators.py b/ariadne_graphql_modules/scalar_type/validators.py similarity index 89% rename from ariadne_graphql_modules/next/graphql_scalar/validators.py rename to ariadne_graphql_modules/scalar_type/validators.py index 374c482..ddf6512 100644 --- a/ariadne_graphql_modules/next/graphql_scalar/validators.py +++ b/ariadne_graphql_modules/scalar_type/validators.py @@ -4,10 +4,10 @@ from ..validators import validate_description, validate_name -from ...utils import parse_definition +from ..utils import parse_definition if TYPE_CHECKING: - from .scalar_type import GraphQLScalar + from .graphql_type import GraphQLScalar def validate_scalar_type_with_schema(cls: Type["GraphQLScalar"]): diff --git a/ariadne_graphql_modules/next/sort.py b/ariadne_graphql_modules/sort.py similarity index 100% rename from ariadne_graphql_modules/next/sort.py rename to ariadne_graphql_modules/sort.py diff --git a/ariadne_graphql_modules/subscription_type/__init__.py b/ariadne_graphql_modules/subscription_type/__init__.py new file mode 100644 index 0000000..c271e40 --- /dev/null +++ b/ariadne_graphql_modules/subscription_type/__init__.py @@ -0,0 +1,8 @@ +from .graphql_type import GraphQLSubscription +from .models import GraphQLSubscriptionModel + + +__all__ = [ + "GraphQLSubscription", + "GraphQLSubscriptionModel", +] diff --git a/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py b/ariadne_graphql_modules/subscription_type/graphql_type.py similarity index 78% rename from ariadne_graphql_modules/next/graphql_subscription/subscription_type.py rename to ariadne_graphql_modules/subscription_type/graphql_type.py index 41aef33..90fd7e7 100644 --- a/ariadne_graphql_modules/next/graphql_subscription/subscription_type.py +++ b/ariadne_graphql_modules/subscription_type/graphql_type.py @@ -11,12 +11,22 @@ from ariadne.types import Resolver, Subscriber +from ..base_object_type import ( + GraphQLFieldData, + GraphQLObjectData, + validate_object_type_with_schema, + validate_object_type_without_schema, +) + +from ..types import GraphQLClassType + +from ..base_object_type import GraphQLBaseObject -from ...utils import parse_definition + +from ..utils import parse_definition from ..base import GraphQLMetadata, GraphQLModel from ..description import get_description_node -from ..graphql_object import ( - GraphQLObject, +from ..object_type import ( GraphQLObjectResolver, GraphQLObjectSource, GraphQLObjectFieldArg, @@ -24,17 +34,32 @@ get_field_args_from_subscriber, get_field_args_out_names, get_field_node_from_obj_field, - get_graphql_object_data, object_subscriber, update_field_args_options, ) from ..value import get_value_node -from .subscription_model import GraphQLSubscriptionModel +from .models import GraphQLSubscriptionModel -class GraphQLSubscription(GraphQLObject): - __abstract__: bool = True +class GraphQLSubscription(GraphQLBaseObject): __valid_type__ = ObjectTypeDefinitionNode + __graphql_type__ = GraphQLClassType.SUBSCRIPTION + __abstract__: bool = True + __description__: Optional[str] = None + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) + cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) + else: + cls.__kwargs__ = validate_object_type_without_schema(cls) @classmethod def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": @@ -48,7 +73,6 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": args_defaults: Dict[str, Dict[str, Any]] = {} resolvers: Dict[str, Resolver] = {} subscribers: Dict[str, Subscriber] = {} - out_names: Dict[str, Dict[str, str]] = {} for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -148,14 +172,14 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": resolvers=resolvers, subscribers=subscribers, aliases=getattr(cls, "__aliases__", {}), - out_names=out_names, + out_names={}, ) @classmethod def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLModel": - type_data = get_graphql_object_data(metadata, cls) + type_data = cls.get_graphql_object_data(metadata) type_aliases = getattr(cls, "__aliases__", None) or {} fields_ast: List[FieldDefinitionNode] = [] @@ -215,3 +239,33 @@ def source( graphql_type=graphql_type, description=description, ) + + @classmethod + def _collect_inherited_objects(cls): + return [ + inherited_obj + for inherited_obj in cls.__mro__[1:] + if getattr(inherited_obj, "__graphql_type__", None) + == GraphQLClassType.SUBSCRIPTION + and not getattr(inherited_obj, "__abstract__", True) + ] + + @classmethod + def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: + fields_data = GraphQLFieldData() + inherited_objects = list(reversed(cls._collect_inherited_objects())) + + for inherited_obj in inherited_objects: + fields_data.type_hints.update(inherited_obj.__annotations__) + fields_data.aliases.update(getattr(inherited_obj, "__aliases__", {})) + + cls._process_type_hints_and_aliases(fields_data) + + for inherited_obj in inherited_objects: + cls._process_class_attributes(inherited_obj, fields_data) + cls._process_class_attributes(cls, fields_data) + + return GraphQLObjectData( + fields=cls._build_fields(fields_data=fields_data), + interfaces=[], + ) diff --git a/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py b/ariadne_graphql_modules/subscription_type/models.py similarity index 95% rename from ariadne_graphql_modules/next/graphql_subscription/subscription_model.py rename to ariadne_graphql_modules/subscription_type/models.py index 95bff2d..d32aa1b 100644 --- a/ariadne_graphql_modules/next/graphql_subscription/subscription_model.py +++ b/ariadne_graphql_modules/subscription_type/models.py @@ -5,7 +5,7 @@ from ariadne.types import Resolver, Subscriber from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema -from ariadne_graphql_modules.next.base import GraphQLModel +from ..base import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/types.py b/ariadne_graphql_modules/types.py index 9c2173b..13f8880 100644 --- a/ariadne_graphql_modules/types.py +++ b/ariadne_graphql_modules/types.py @@ -1,5 +1,6 @@ from typing import Dict, Type +from enum import Enum from graphql import ( DefinitionNode, FieldDefinitionNode, @@ -9,3 +10,10 @@ FieldsDict = Dict[str, FieldDefinitionNode] InputFieldsDict = Dict[str, InputValueDefinitionNode] RequirementsDict = Dict[str, Type[DefinitionNode]] + + +class GraphQLClassType(Enum): + BASE = "base" + OBJECT = "object" + INTERFACE = "interface" + SUBSCRIPTION = "subscription" diff --git a/ariadne_graphql_modules/next/typing.py b/ariadne_graphql_modules/typing.py similarity index 100% rename from ariadne_graphql_modules/next/typing.py rename to ariadne_graphql_modules/typing.py diff --git a/ariadne_graphql_modules/union_type/__init__.py b/ariadne_graphql_modules/union_type/__init__.py new file mode 100644 index 0000000..d6ebe06 --- /dev/null +++ b/ariadne_graphql_modules/union_type/__init__.py @@ -0,0 +1,8 @@ +from .graphql_type import GraphQLUnion +from .models import GraphQLUnionModel + + +__all__ = [ + "GraphQLUnion", + "GraphQLUnionModel", +] diff --git a/ariadne_graphql_modules/next/graphql_union/union_type.py b/ariadne_graphql_modules/union_type/graphql_type.py similarity index 95% rename from ariadne_graphql_modules/next/graphql_union/union_type.py rename to ariadne_graphql_modules/union_type/graphql_type.py index d74c796..154c8c8 100644 --- a/ariadne_graphql_modules/next/graphql_union/union_type.py +++ b/ariadne_graphql_modules/union_type/graphql_type.py @@ -3,13 +3,13 @@ from graphql import NameNode, NamedTypeNode, UnionTypeDefinitionNode from ..base import GraphQLMetadata, GraphQLModel, GraphQLType from ..description import get_description_node -from ..graphql_object.object_type import GraphQLObject -from .union_model import GraphQLUnionModel +from ..object_type.graphql_type import GraphQLObject +from .models import GraphQLUnionModel from .validators import ( validate_union_type, validate_union_type_with_schema, ) -from ...utils import parse_definition +from ..utils import parse_definition class GraphQLUnion(GraphQLType): diff --git a/ariadne_graphql_modules/next/graphql_union/union_model.py b/ariadne_graphql_modules/union_type/models.py similarity index 100% rename from ariadne_graphql_modules/next/graphql_union/union_model.py rename to ariadne_graphql_modules/union_type/models.py diff --git a/ariadne_graphql_modules/next/graphql_union/validators.py b/ariadne_graphql_modules/union_type/validators.py similarity index 96% rename from ariadne_graphql_modules/next/graphql_union/validators.py rename to ariadne_graphql_modules/union_type/validators.py index 3d7f280..d22aece 100644 --- a/ariadne_graphql_modules/next/graphql_union/validators.py +++ b/ariadne_graphql_modules/union_type/validators.py @@ -4,10 +4,10 @@ from ..validators import validate_description, validate_name -from ...utils import parse_definition +from ..utils import parse_definition if TYPE_CHECKING: - from .union_type import GraphQLUnion + from .graphql_type import GraphQLUnion def validate_union_type(cls: Type["GraphQLUnion"]) -> None: diff --git a/ariadne_graphql_modules/v1/__init__.py b/ariadne_graphql_modules/v1/__init__.py new file mode 100644 index 0000000..d6406a3 --- /dev/null +++ b/ariadne_graphql_modules/v1/__init__.py @@ -0,0 +1,38 @@ +from ariadne import gql + +from .bases import BaseType, BindableType, DeferredType, DefinitionType +from .collection_type import CollectionType +from .convert_case import convert_case +from .directive_type import DirectiveType +from .enum_type import EnumType +from .executable_schema import make_executable_schema +from .input_type import InputType +from .interface_type import InterfaceType +from .mutation_type import MutationType +from .object_type import ObjectType +from .scalar_type import ScalarType +from .subscription_type import SubscriptionType +from .union_type import UnionType +from ..utils import create_alias_resolver, parse_definition + +__all__ = [ + "BaseType", + "BindableType", + "CollectionType", + "DeferredType", + "DefinitionType", + "DirectiveType", + "EnumType", + "InputType", + "InterfaceType", + "MutationType", + "ObjectType", + "ScalarType", + "SubscriptionType", + "UnionType", + "convert_case", + "create_alias_resolver", + "gql", + "make_executable_schema", + "parse_definition", +] diff --git a/ariadne_graphql_modules/bases.py b/ariadne_graphql_modules/v1/bases.py similarity index 98% rename from ariadne_graphql_modules/bases.py rename to ariadne_graphql_modules/v1/bases.py index 2ff42e2..cfc983c 100644 --- a/ariadne_graphql_modules/bases.py +++ b/ariadne_graphql_modules/v1/bases.py @@ -8,7 +8,7 @@ ) from .dependencies import Dependencies -from .types import RequirementsDict +from ..types import RequirementsDict __all__ = ["BaseType", "BindableType", "DeferredType", "DefinitionType"] diff --git a/ariadne_graphql_modules/collection_type.py b/ariadne_graphql_modules/v1/collection_type.py similarity index 100% rename from ariadne_graphql_modules/collection_type.py rename to ariadne_graphql_modules/v1/collection_type.py diff --git a/ariadne_graphql_modules/convert_case.py b/ariadne_graphql_modules/v1/convert_case.py similarity index 97% rename from ariadne_graphql_modules/convert_case.py rename to ariadne_graphql_modules/v1/convert_case.py index d0c7b18..f44e878 100644 --- a/ariadne_graphql_modules/convert_case.py +++ b/ariadne_graphql_modules/v1/convert_case.py @@ -4,7 +4,7 @@ from ariadne import convert_camel_case_to_snake from graphql import FieldDefinitionNode -from .types import FieldsDict, InputFieldsDict +from ..types import FieldsDict, InputFieldsDict def convert_case( diff --git a/ariadne_graphql_modules/dependencies.py b/ariadne_graphql_modules/v1/dependencies.py similarity index 99% rename from ariadne_graphql_modules/dependencies.py rename to ariadne_graphql_modules/v1/dependencies.py index f3b436d..cd04c06 100644 --- a/ariadne_graphql_modules/dependencies.py +++ b/ariadne_graphql_modules/v1/dependencies.py @@ -15,7 +15,7 @@ UnionTypeExtensionNode, ) -from .utils import unwrap_type_node +from ..utils import unwrap_type_node GRAPHQL_TYPES = ("ID", "Int", "String", "Boolean", "Float") diff --git a/ariadne_graphql_modules/directive_type.py b/ariadne_graphql_modules/v1/directive_type.py similarity index 97% rename from ariadne_graphql_modules/directive_type.py rename to ariadne_graphql_modules/v1/directive_type.py index d6e2706..14d9817 100644 --- a/ariadne_graphql_modules/directive_type.py +++ b/ariadne_graphql_modules/v1/directive_type.py @@ -7,7 +7,7 @@ ) from .bases import DefinitionType -from .utils import parse_definition +from ..utils import parse_definition class DirectiveType(DefinitionType): diff --git a/ariadne_graphql_modules/enum_type.py b/ariadne_graphql_modules/v1/enum_type.py similarity index 98% rename from ariadne_graphql_modules/enum_type.py rename to ariadne_graphql_modules/v1/enum_type.py index c555b33..66236d4 100644 --- a/ariadne_graphql_modules/enum_type.py +++ b/ariadne_graphql_modules/v1/enum_type.py @@ -10,8 +10,8 @@ ) from .bases import BindableType -from .types import RequirementsDict -from .utils import parse_definition +from ..types import RequirementsDict +from ..utils import parse_definition EnumNodeType = Union[EnumTypeDefinitionNode, EnumTypeExtensionNode] diff --git a/ariadne_graphql_modules/v1/executable_schema.py b/ariadne_graphql_modules/v1/executable_schema.py new file mode 100644 index 0000000..1914742 --- /dev/null +++ b/ariadne_graphql_modules/v1/executable_schema.py @@ -0,0 +1,236 @@ +from typing import ( + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + cast, +) + +from ariadne import ( + SchemaBindable, + SchemaDirectiveVisitor, + repair_schema_default_enum_values, + validate_schema_default_enum_values, +) +from graphql import ( + ConstDirectiveNode, + DocumentNode, + FieldDefinitionNode, + GraphQLSchema, + NamedTypeNode, + ObjectTypeDefinitionNode, + TypeDefinitionNode, + assert_valid_schema, + build_ast_schema, + concat_ast, + parse, +) +from graphql.language import ast + +from .bases import BaseType, BindableType, DeferredType, DefinitionType +from .enum_type import EnumType + +ROOT_TYPES = ["Query", "Mutation", "Subscription"] + + +def make_executable_schema( + *args: Union[Type[BaseType], SchemaBindable, str], + merge_roots: bool = True, + extra_directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, +): + all_types = get_all_types(args) + extra_defs = parse_extra_sdl(args) + extra_bindables: List[SchemaBindable] = [ + arg for arg in args if isinstance(arg, SchemaBindable) + ] + + type_defs: List[Type[DefinitionType]] = [] + for type_ in all_types: + if issubclass(type_, DefinitionType): + type_defs.append(type_) + + validate_no_missing_definitions(all_types, type_defs, extra_defs) + + schema = build_schema(type_defs, extra_defs, merge_roots) + + if extra_bindables: + for bindable in extra_bindables: + bindable.bind_to_schema(schema) + + if extra_directives: + SchemaDirectiveVisitor.visit_schema_directives(schema, extra_directives) + + assert_valid_schema(schema) + validate_schema_default_enum_values(schema) + repair_schema_default_enum_values(schema) + + add_directives_to_schema(schema, type_defs) + + return schema + + +def get_all_types( + args: Sequence[Union[Type[BaseType], SchemaBindable, str]] +) -> List[Type[BaseType]]: + all_types: List[Type[BaseType]] = [] + for arg in args: + if isinstance(arg, (str, SchemaBindable)): + continue # Skip args of unsupported types + + for child_type in arg.__get_types__(): + if child_type not in all_types: + all_types.append(child_type) + return all_types + + +def parse_extra_sdl( + args: Sequence[Union[Type[BaseType], SchemaBindable, str]] +) -> List[TypeDefinitionNode]: + sdl_strings: List[str] = [cast(str, arg) for arg in args if isinstance(arg, str)] + if not sdl_strings: + return [] + + extra_sdl = "\n\n".join(sdl_strings) + return cast( + List[TypeDefinitionNode], + list(parse(extra_sdl).definitions), + ) + + +def validate_no_missing_definitions( + all_types: List[Type[BaseType]], + type_defs: List[Type[DefinitionType]], + extra_defs: List[TypeDefinitionNode], +): + deferred_names: List[str] = [] + for type_ in all_types: + if isinstance(type_, DeferredType): + deferred_names.append(type_.graphql_name) + + real_names = [type_.graphql_name for type_ in type_defs] + real_names += [definition.name.value for definition in extra_defs] + + missing_names = set(deferred_names) - set(real_names) + if missing_names: + raise ValueError( + "Following types are defined as deferred and are missing " + f"from schema: {', '.join(missing_names)}" + ) + + +def build_schema( + type_defs: List[Type[DefinitionType]], + extra_defs: List[TypeDefinitionNode], + merge_roots: bool = True, +) -> GraphQLSchema: + schema_definitions: List[ast.DocumentNode] = [] + if merge_roots: + schema_definitions.append(build_root_schema(type_defs, extra_defs)) + for type_ in type_defs: + if type_.graphql_name not in ROOT_TYPES or not merge_roots: + schema_definitions.append(parse(type_.__schema__)) + for extra_type_def in extra_defs: + if extra_type_def.name.value not in ROOT_TYPES or not merge_roots: + schema_definitions.append(DocumentNode(definitions=(extra_type_def,))) + + ast_document = concat_ast(schema_definitions) + schema = build_ast_schema(ast_document) + + for type_ in type_defs: + if issubclass(type_, BindableType): + type_.__bind_to_schema__(schema) + + return schema + + +RootTypeDef = Tuple[str, DocumentNode] + + +def build_root_schema( + type_defs: List[Type[DefinitionType]], + extra_defs: List[TypeDefinitionNode], +) -> DocumentNode: + root_types: Dict[str, List[RootTypeDef]] = { + "Query": [], + "Mutation": [], + "Subscription": [], + } + + for type_def in type_defs: + if type_def.graphql_name in root_types: + root_types[type_def.graphql_name].append( + (type_def.__name__, parse(type_def.__schema__)) + ) + + for extra_type_def in extra_defs: + if extra_type_def.name.value in root_types: + root_types[extra_type_def.name.value].append( + ("extra_sdl", DocumentNode(definitions=(extra_type_def,))) + ) + + schema: List[DocumentNode] = [] + for root_name, root_type_defs in root_types.items(): + if len(root_type_defs) == 1: + schema.append(root_type_defs[0][1]) + elif root_type_defs: + schema.append(merge_root_types(root_name, root_type_defs)) + + return concat_ast(schema) + + +def merge_root_types(root_name: str, type_defs: List[RootTypeDef]) -> DocumentNode: + interfaces: List[NamedTypeNode] = [] + directives: List[ConstDirectiveNode] = [] + fields: Dict[str, Tuple[str, FieldDefinitionNode]] = {} + + for type_source, type_def in type_defs: + type_definition = cast(ObjectTypeDefinitionNode, type_def.definitions[0]) + interfaces.extend(type_definition.interfaces) + directives.extend(type_definition.directives) + + for field_def in type_definition.fields: + field_name = field_def.name.value + if field_name in fields: + other_type_source = fields[field_name][0] + raise ValueError( + f"Multiple {root_name} types are defining same field " + f"'{field_name}': {other_type_source}, {type_source}" + ) + + fields[field_name] = (type_source, field_def) + + merged_definition = ast.ObjectTypeDefinitionNode() + merged_definition.name = ast.NameNode() + merged_definition.name.value = root_name + merged_definition.interfaces = tuple(interfaces) + merged_definition.directives = tuple(directives) + merged_definition.fields = tuple( + fields[field_name][1] for field_name in sorted(fields) + ) + + merged_document = DocumentNode() + merged_document.definitions = (merged_definition,) + + return merged_document + + +def add_directives_to_schema( + schema: GraphQLSchema, type_defs: List[Type[DefinitionType]] +): + directives: Dict[str, Type[SchemaDirectiveVisitor]] = {} + for type_def in type_defs: + visitor = getattr(type_def, "__visitor__", None) + if visitor and issubclass(visitor, SchemaDirectiveVisitor): + directives[type_def.graphql_name] = visitor + + if directives: + SchemaDirectiveVisitor.visit_schema_directives(schema, directives) + + +def repair_default_enum_values(schema, types_list: List[Type[DefinitionType]]) -> None: + for type_ in types_list: + if issubclass(type_, EnumType): + type_.__bind_to_default_values__(schema) diff --git a/ariadne_graphql_modules/input_type.py b/ariadne_graphql_modules/v1/input_type.py similarity index 97% rename from ariadne_graphql_modules/input_type.py rename to ariadne_graphql_modules/v1/input_type.py index 32295db..fa2eedb 100644 --- a/ariadne_graphql_modules/input_type.py +++ b/ariadne_graphql_modules/v1/input_type.py @@ -8,8 +8,8 @@ from .bases import BindableType from .dependencies import Dependencies, get_dependencies_from_input_type -from .types import InputFieldsDict, RequirementsDict -from .utils import parse_definition +from ..types import InputFieldsDict, RequirementsDict +from ..utils import parse_definition Args = Dict[str, str] InputNodeType = Union[InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode] diff --git a/ariadne_graphql_modules/interface_type.py b/ariadne_graphql_modules/v1/interface_type.py similarity index 98% rename from ariadne_graphql_modules/interface_type.py rename to ariadne_graphql_modules/v1/interface_type.py index ac34188..c51f28f 100644 --- a/ariadne_graphql_modules/interface_type.py +++ b/ariadne_graphql_modules/v1/interface_type.py @@ -16,8 +16,8 @@ from .bases import BindableType from .dependencies import Dependencies, get_dependencies_from_object_type from .resolvers_mixin import ResolversMixin -from .types import FieldsDict, RequirementsDict -from .utils import parse_definition +from ..types import FieldsDict, RequirementsDict +from ..utils import parse_definition InterfaceNodeType = Union[InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode] diff --git a/ariadne_graphql_modules/mutation_type.py b/ariadne_graphql_modules/v1/mutation_type.py similarity index 98% rename from ariadne_graphql_modules/mutation_type.py rename to ariadne_graphql_modules/v1/mutation_type.py index d8e020d..f579d3f 100644 --- a/ariadne_graphql_modules/mutation_type.py +++ b/ariadne_graphql_modules/v1/mutation_type.py @@ -10,8 +10,8 @@ from .bases import BindableType from .dependencies import Dependencies, get_dependencies_from_object_type -from .types import RequirementsDict -from .utils import parse_definition +from ..types import RequirementsDict +from ..utils import parse_definition MutationArgs = Dict[str, str] ObjectNodeType = Union[ObjectTypeDefinitionNode, ObjectTypeExtensionNode] diff --git a/ariadne_graphql_modules/object_type.py b/ariadne_graphql_modules/v1/object_type.py similarity index 98% rename from ariadne_graphql_modules/object_type.py rename to ariadne_graphql_modules/v1/object_type.py index bb54b4a..e5183a6 100644 --- a/ariadne_graphql_modules/object_type.py +++ b/ariadne_graphql_modules/v1/object_type.py @@ -10,8 +10,8 @@ from .bases import BindableType from .dependencies import Dependencies, get_dependencies_from_object_type from .resolvers_mixin import ResolversMixin -from .types import FieldsDict, RequirementsDict -from .utils import parse_definition +from ..types import FieldsDict, RequirementsDict +from ..utils import parse_definition ObjectNodeType = Union[ObjectTypeDefinitionNode, ObjectTypeExtensionNode] diff --git a/ariadne_graphql_modules/resolvers_mixin.py b/ariadne_graphql_modules/v1/resolvers_mixin.py similarity index 97% rename from ariadne_graphql_modules/resolvers_mixin.py rename to ariadne_graphql_modules/v1/resolvers_mixin.py index 9d641dc..ec24112 100644 --- a/ariadne_graphql_modules/resolvers_mixin.py +++ b/ariadne_graphql_modules/v1/resolvers_mixin.py @@ -2,8 +2,8 @@ from graphql import GraphQLFieldResolver, NamedTypeNode -from .types import FieldsDict -from .utils import create_alias_resolver +from ..types import FieldsDict +from ..utils import create_alias_resolver Aliases = Dict[str, str] FieldsArgs = Dict[str, Dict[str, str]] diff --git a/ariadne_graphql_modules/scalar_type.py b/ariadne_graphql_modules/v1/scalar_type.py similarity index 97% rename from ariadne_graphql_modules/scalar_type.py rename to ariadne_graphql_modules/v1/scalar_type.py index a86388b..3da456d 100644 --- a/ariadne_graphql_modules/scalar_type.py +++ b/ariadne_graphql_modules/v1/scalar_type.py @@ -12,8 +12,8 @@ ) from .bases import BindableType -from .types import RequirementsDict -from .utils import parse_definition +from ..types import RequirementsDict +from ..utils import parse_definition ScalarNodeType = Union[ScalarTypeDefinitionNode, ScalarTypeExtensionNode] diff --git a/ariadne_graphql_modules/subscription_type.py b/ariadne_graphql_modules/v1/subscription_type.py similarity index 100% rename from ariadne_graphql_modules/subscription_type.py rename to ariadne_graphql_modules/v1/subscription_type.py diff --git a/ariadne_graphql_modules/union_type.py b/ariadne_graphql_modules/v1/union_type.py similarity index 97% rename from ariadne_graphql_modules/union_type.py rename to ariadne_graphql_modules/v1/union_type.py index 932ba4f..94b8575 100644 --- a/ariadne_graphql_modules/union_type.py +++ b/ariadne_graphql_modules/v1/union_type.py @@ -11,8 +11,8 @@ from .bases import BindableType from .dependencies import Dependencies, get_dependencies_from_union_type -from .types import RequirementsDict -from .utils import parse_definition +from ..types import RequirementsDict +from ..utils import parse_definition UnionNodeType = Union[UnionTypeDefinitionNode, UnionTypeExtensionNode] diff --git a/ariadne_graphql_modules/next/validators.py b/ariadne_graphql_modules/validators.py similarity index 100% rename from ariadne_graphql_modules/next/validators.py rename to ariadne_graphql_modules/validators.py diff --git a/ariadne_graphql_modules/next/value.py b/ariadne_graphql_modules/value.py similarity index 100% rename from ariadne_graphql_modules/next/value.py rename to ariadne_graphql_modules/value.py diff --git a/pyproject.toml b/pyproject.toml index bb2236b..efdba0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ include = [ "ariadne_graphql_modules/**/*.py", "ariadne_graphql_modules/py.typed", ] -exclude = ["tests"] +exclude = ["tests", "tests_v1"] [tool.hatch.envs.default] features = ["test"] @@ -74,5 +74,5 @@ exclude = ''' ''' [tool.pytest.ini_options] -testpaths = ["tests_next"] +testpaths = ["tests"] asyncio_mode = "strict" diff --git a/tests/conftest.py b/tests/conftest.py index 0f16709..c66282b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,33 @@ from pathlib import Path +from textwrap import dedent + import pytest +from graphql import TypeDefinitionNode, GraphQLSchema, print_ast, print_schema + +from ariadne_graphql_modules import GraphQLMetadata + + +@pytest.fixture +def assert_schema_equals(): + def schema_equals_assertion(schema: GraphQLSchema, target: str): + schema_str = print_schema(schema) + assert schema_str == dedent(target).strip() + + return schema_equals_assertion + + +@pytest.fixture +def assert_ast_equals(): + def ast_equals_assertion(ast: TypeDefinitionNode, target: str): + ast_str = print_ast(ast) + assert ast_str == dedent(target).strip() + + return ast_equals_assertion + + +@pytest.fixture +def metadata(): + return GraphQLMetadata() @pytest.fixture(scope="session") diff --git a/tests_next/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml b/tests/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml rename to tests/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml diff --git a/tests_next/snapshots/test_arg_with_description_in_source_with_schema.yml b/tests/snapshots/test_arg_with_description_in_source_with_schema.yml similarity index 100% rename from tests_next/snapshots/test_arg_with_description_in_source_with_schema.yml rename to tests/snapshots/test_arg_with_description_in_source_with_schema.yml diff --git a/tests_next/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml b/tests/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml rename to tests/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml diff --git a/tests_next/snapshots/test_arg_with_name_in_source_with_schema.yml b/tests/snapshots/test_arg_with_name_in_source_with_schema.yml similarity index 100% rename from tests_next/snapshots/test_arg_with_name_in_source_with_schema.yml rename to tests/snapshots/test_arg_with_name_in_source_with_schema.yml diff --git a/tests_next/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml b/tests/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml rename to tests/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml diff --git a/tests_next/snapshots/test_arg_with_type_in_source_with_schema.yml b/tests/snapshots/test_arg_with_type_in_source_with_schema.yml similarity index 100% rename from tests_next/snapshots/test_arg_with_type_in_source_with_schema.yml rename to tests/snapshots/test_arg_with_type_in_source_with_schema.yml diff --git a/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml b/tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml similarity index 100% rename from tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml rename to tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml diff --git a/tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml b/tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml similarity index 100% rename from tests_next/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml rename to tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.yml diff --git a/tests_next/snapshots/test_description_not_str_without_schema.obtained.yml b/tests/snapshots/test_description_not_str_without_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_description_not_str_without_schema.obtained.yml rename to tests/snapshots/test_description_not_str_without_schema.obtained.yml diff --git a/tests_next/snapshots/test_description_not_str_without_schema.yml b/tests/snapshots/test_description_not_str_without_schema.yml similarity index 100% rename from tests_next/snapshots/test_description_not_str_without_schema.yml rename to tests/snapshots/test_description_not_str_without_schema.yml diff --git a/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml b/tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml rename to tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml b/tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml rename to tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.yml diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml b/tests/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml similarity index 100% rename from tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml rename to tests/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.yml b/tests/snapshots/test_enum_type_validation_fails_for_invalid_members.yml similarity index 100% rename from tests_next/snapshots/test_enum_type_validation_fails_for_invalid_members.yml rename to tests/snapshots/test_enum_type_validation_fails_for_invalid_members.yml diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml b/tests/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml similarity index 100% rename from tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml rename to tests/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml diff --git a/tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.yml b/tests/snapshots/test_enum_type_validation_fails_for_missing_members.yml similarity index 100% rename from tests_next/snapshots/test_enum_type_validation_fails_for_missing_members.yml rename to tests/snapshots/test_enum_type_validation_fails_for_missing_members.yml diff --git a/tests_next/snapshots/test_field_name_not_str_without_schema.obtained.yml b/tests/snapshots/test_field_name_not_str_without_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_field_name_not_str_without_schema.obtained.yml rename to tests/snapshots/test_field_name_not_str_without_schema.obtained.yml diff --git a/tests_next/snapshots/test_field_name_not_str_without_schema.yml b/tests/snapshots/test_field_name_not_str_without_schema.yml similarity index 100% rename from tests_next/snapshots/test_field_name_not_str_without_schema.yml rename to tests/snapshots/test_field_name_not_str_without_schema.yml diff --git a/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml similarity index 100% rename from tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml rename to tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml diff --git a/tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml b/tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml similarity index 100% rename from tests_next/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml rename to tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.yml diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml b/tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml rename to tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml b/tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml similarity index 100% rename from tests_next/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml rename to tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.yml diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml b/tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml similarity index 100% rename from tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml rename to tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml b/tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml similarity index 100% rename from tests_next/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml rename to tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.yml diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml b/tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml similarity index 100% rename from tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml rename to tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml diff --git a/tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml b/tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml similarity index 100% rename from tests_next/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml rename to tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.yml diff --git a/tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html b/tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html new file mode 100644 index 0000000..c6a19d9 --- /dev/null +++ b/tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + +

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_no_interface_in_schema.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml
t1Unknown type 'BaseInterface'.t1Query root type must be provided.
2...2...
+ + + + +
Legends
+ + + + +
Colors
 Added 
Changed
Deleted
+ + + + +
Links
(f)irst change
(n)ext change
(t)op
+ + + \ No newline at end of file diff --git a/tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml b/tests/snapshots/test_interface_no_interface_in_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml rename to tests/snapshots/test_interface_no_interface_in_schema.obtained.yml diff --git a/tests_next/snapshots/test_interface_no_interface_in_schema.yml b/tests/snapshots/test_interface_no_interface_in_schema.yml similarity index 100% rename from tests_next/snapshots/test_interface_no_interface_in_schema.yml rename to tests/snapshots/test_interface_no_interface_in_schema.yml diff --git a/tests/snapshots/test_interface_with_different_types.obtained.diff.html b/tests/snapshots/test_interface_with_different_types.obtained.diff.html new file mode 100644 index 0000000..ef57772 --- /dev/null +++ b/tests/snapshots/test_interface_with_different_types.obtained.diff.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + +

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_with_different_types.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_with_different_types.obtained.yml
t1'Query root type must be provided.t1Query root type must be provided.
2 2...
3 
4  Interface field UserInterface.score expects type String! but User.score is type
5  Int!.'
+ + + + +
Legends
+ + + + +
Colors
 Added 
Changed
Deleted
+ + + + +
Links
(f)irst change
(n)ext change
(t)op
+ + + \ No newline at end of file diff --git a/tests/snapshots/test_interface_with_different_types.obtained.yml b/tests/snapshots/test_interface_with_different_types.obtained.yml new file mode 100644 index 0000000..322fe60 --- /dev/null +++ b/tests/snapshots/test_interface_with_different_types.obtained.yml @@ -0,0 +1,2 @@ +Query root type must be provided. +... diff --git a/tests_next/snapshots/test_interface_with_different_types.yml b/tests/snapshots/test_interface_with_different_types.yml similarity index 100% rename from tests_next/snapshots/test_interface_with_different_types.yml rename to tests/snapshots/test_interface_with_different_types.yml diff --git a/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml b/tests/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml rename to tests/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml diff --git a/tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.yml b/tests/snapshots/test_invalid_arg_name_in_source_with_schema.yml similarity index 100% rename from tests_next/snapshots/test_invalid_arg_name_in_source_with_schema.yml rename to tests/snapshots/test_invalid_arg_name_in_source_with_schema.yml diff --git a/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml b/tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml similarity index 100% rename from tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml rename to tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml diff --git a/tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml b/tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml similarity index 100% rename from tests_next/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml rename to tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.yml diff --git a/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html new file mode 100644 index 0000000..4929bd2 --- /dev/null +++ b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + +

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_metadata_raises_key_error_for_unset_data.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml
t1'"No data is set for ''<class ''tests_next.test_metadata.QueryType''>''."'t1'"No data is set for ''<class ''tests.test_metadata.QueryType''>''."'
+ + + + +
Legends
+ + + + +
Colors
 Added 
Changed
Deleted
+ + + + +
Links
(f)irst change
(n)ext change
(t)op
+ + + \ No newline at end of file diff --git a/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml new file mode 100644 index 0000000..ef7ccb8 --- /dev/null +++ b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml @@ -0,0 +1 @@ +'"No data is set for ''''."' diff --git a/tests/snapshots/test_metadata_raises_key_error_for_unset_data.yml b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.yml new file mode 100644 index 0000000..ef7ccb8 --- /dev/null +++ b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.yml @@ -0,0 +1 @@ +'"No data is set for ''''."' diff --git a/tests_next/snapshots/test_missing_type_in_schema.obtained.yml b/tests/snapshots/test_missing_type_in_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_missing_type_in_schema.obtained.yml rename to tests/snapshots/test_missing_type_in_schema.obtained.yml diff --git a/tests_next/snapshots/test_missing_type_in_schema.yml b/tests/snapshots/test_missing_type_in_schema.yml similarity index 100% rename from tests_next/snapshots/test_missing_type_in_schema.yml rename to tests/snapshots/test_missing_type_in_schema.yml diff --git a/tests_next/snapshots/test_missing_type_in_types.obtained.yml b/tests/snapshots/test_missing_type_in_types.obtained.yml similarity index 100% rename from tests_next/snapshots/test_missing_type_in_types.obtained.yml rename to tests/snapshots/test_missing_type_in_types.obtained.yml diff --git a/tests_next/snapshots/test_missing_type_in_types.yml b/tests/snapshots/test_missing_type_in_types.yml similarity index 100% rename from tests_next/snapshots/test_missing_type_in_types.yml rename to tests/snapshots/test_missing_type_in_types.yml diff --git a/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml b/tests/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml rename to tests/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml diff --git a/tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.yml b/tests/snapshots/test_multiple_descriptions_for_source_with_schema.yml similarity index 100% rename from tests_next/snapshots/test_multiple_descriptions_for_source_with_schema.yml rename to tests/snapshots/test_multiple_descriptions_for_source_with_schema.yml diff --git a/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html new file mode 100644 index 0000000..1ffb12e --- /dev/null +++ b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + +

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml
t1Types 'SecondRoot' and '<class 'tests_next.test_make_executable_schema.test_multiple_roots_fail_validation_if_merge_roots_is_disabled.<locals>.FirstRoot'>'t1Types 'SecondRoot' and '<class 'tests.test_make_executable_schema.test_multiple_roots_fail_validation_if_merge_roots_is_disabled.<locals>.FirstRoot'>'
2  both define GraphQL type with name 'Query'.2  both define GraphQL type with name 'Query'.
3...3...
+ + + + +
Legends
+ + + + +
Colors
 Added 
Changed
Deleted
+ + + + +
Links
(f)irst change
(n)ext change
(t)op
+ + + \ No newline at end of file diff --git a/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml new file mode 100644 index 0000000..c22028c --- /dev/null +++ b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml @@ -0,0 +1,3 @@ +Types 'SecondRoot' and '.FirstRoot'>' + both define GraphQL type with name 'Query'. +... diff --git a/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml new file mode 100644 index 0000000..c22028c --- /dev/null +++ b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml @@ -0,0 +1,3 @@ +Types 'SecondRoot' and '.FirstRoot'>' + both define GraphQL type with name 'Query'. +... diff --git a/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml b/tests/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml rename to tests/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml diff --git a/tests_next/snapshots/test_multiple_sourced_for_field_with_schema.yml b/tests/snapshots/test_multiple_sourced_for_field_with_schema.yml similarity index 100% rename from tests_next/snapshots/test_multiple_sourced_for_field_with_schema.yml rename to tests/snapshots/test_multiple_sourced_for_field_with_schema.yml diff --git a/tests_next/snapshots/test_multiple_sources_without_schema.obtained.yml b/tests/snapshots/test_multiple_sources_without_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_multiple_sources_without_schema.obtained.yml rename to tests/snapshots/test_multiple_sources_without_schema.obtained.yml diff --git a/tests_next/snapshots/test_multiple_sources_without_schema.yml b/tests/snapshots/test_multiple_sources_without_schema.yml similarity index 100% rename from tests_next/snapshots/test_multiple_sources_without_schema.yml rename to tests/snapshots/test_multiple_sources_without_schema.yml diff --git a/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml b/tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml similarity index 100% rename from tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml rename to tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml diff --git a/tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml b/tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml similarity index 100% rename from tests_next/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml rename to tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.yml diff --git a/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml rename to tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml diff --git a/tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml b/tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml similarity index 100% rename from tests_next/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml rename to tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.yml b/tests/snapshots/test_object_type_validation_fails_for_alias_resolver.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_alias_resolver.yml rename to tests/snapshots/test_object_type_validation_fails_for_alias_resolver.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml b/tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml rename to tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml b/tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml rename to tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml b/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml rename to tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml b/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml rename to tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.yml b/tests/snapshots/test_object_type_validation_fails_for_invalid_alias.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_invalid_alias.yml rename to tests/snapshots/test_object_type_validation_fails_for_invalid_alias.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml b/tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml rename to tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml b/tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml rename to tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml rename to tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml rename to tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml rename to tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml rename to tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml rename to tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml rename to tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml b/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml rename to tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml rename to tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml diff --git a/tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml b/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml similarity index 100% rename from tests_next/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml rename to tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml rename to tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.yml diff --git a/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml rename to tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml diff --git a/tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml b/tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml rename to tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml rename to tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.yml diff --git a/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml rename to tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml b/tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml rename to tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml diff --git a/tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml similarity index 100% rename from tests_next/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml rename to tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.yml diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml rename to tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml similarity index 100% rename from tests_next/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml rename to tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.yml diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml rename to tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml similarity index 100% rename from tests_next/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml rename to tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.yml diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml rename to tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml diff --git a/tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml similarity index 100% rename from tests_next/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml rename to tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.yml diff --git a/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml b/tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml similarity index 100% rename from tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml rename to tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml diff --git a/tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml b/tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml similarity index 100% rename from tests_next/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml rename to tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.yml diff --git a/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml b/tests/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml rename to tests/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml diff --git a/tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.yml b/tests/snapshots/test_source_args_field_arg_not_dict_without_schema.yml similarity index 100% rename from tests_next/snapshots/test_source_args_field_arg_not_dict_without_schema.yml rename to tests/snapshots/test_source_args_field_arg_not_dict_without_schema.yml diff --git a/tests_next/snapshots/test_source_args_not_dict_without_schema.obtained.yml b/tests/snapshots/test_source_args_not_dict_without_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_source_args_not_dict_without_schema.obtained.yml rename to tests/snapshots/test_source_args_not_dict_without_schema.obtained.yml diff --git a/tests_next/snapshots/test_source_args_not_dict_without_schema.yml b/tests/snapshots/test_source_args_not_dict_without_schema.yml similarity index 100% rename from tests_next/snapshots/test_source_args_not_dict_without_schema.yml rename to tests/snapshots/test_source_args_not_dict_without_schema.yml diff --git a/tests_next/snapshots/test_source_for_undefined_field_with_schema.obtained.yml b/tests/snapshots/test_source_for_undefined_field_with_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_source_for_undefined_field_with_schema.obtained.yml rename to tests/snapshots/test_source_for_undefined_field_with_schema.obtained.yml diff --git a/tests_next/snapshots/test_source_for_undefined_field_with_schema.yml b/tests/snapshots/test_source_for_undefined_field_with_schema.yml similarity index 100% rename from tests_next/snapshots/test_source_for_undefined_field_with_schema.yml rename to tests/snapshots/test_source_for_undefined_field_with_schema.yml diff --git a/tests_next/snapshots/test_undefined_name_without_schema.obtained.yml b/tests/snapshots/test_undefined_name_without_schema.obtained.yml similarity index 100% rename from tests_next/snapshots/test_undefined_name_without_schema.obtained.yml rename to tests/snapshots/test_undefined_name_without_schema.obtained.yml diff --git a/tests_next/snapshots/test_undefined_name_without_schema.yml b/tests/snapshots/test_undefined_name_without_schema.yml similarity index 100% rename from tests_next/snapshots/test_undefined_name_without_schema.yml rename to tests/snapshots/test_undefined_name_without_schema.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml rename to tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml b/tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml rename to tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml rename to tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml rename to tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml rename to tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml rename to tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml rename to tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml diff --git a/tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml similarity index 100% rename from tests_next/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml rename to tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.yml diff --git a/tests_next/test_compatibility_layer.py b/tests/test_compatibility_layer.py similarity index 93% rename from tests_next/test_compatibility_layer.py rename to tests/test_compatibility_layer.py index 320bdc4..90dc845 100644 --- a/tests_next/test_compatibility_layer.py +++ b/tests/test_compatibility_layer.py @@ -2,17 +2,17 @@ from datetime import date, datetime from graphql import StringValueNode -from ariadne_graphql_modules.bases import DeferredType -from ariadne_graphql_modules.collection_type import CollectionType -from ariadne_graphql_modules.enum_type import EnumType -from ariadne_graphql_modules.input_type import InputType -from ariadne_graphql_modules.interface_type import InterfaceType -from ariadne_graphql_modules.next.compatibility_layer import wrap_legacy_types -from ariadne_graphql_modules.next.executable_schema import make_executable_schema -from ariadne_graphql_modules.object_type import ObjectType -from ariadne_graphql_modules.scalar_type import ScalarType -from ariadne_graphql_modules.subscription_type import SubscriptionType -from ariadne_graphql_modules.union_type import UnionType +from ariadne_graphql_modules.v1.bases import DeferredType +from ariadne_graphql_modules.v1.collection_type import CollectionType +from ariadne_graphql_modules.v1.enum_type import EnumType +from ariadne_graphql_modules.v1.input_type import InputType +from ariadne_graphql_modules.v1.interface_type import InterfaceType +from ariadne_graphql_modules.compatibility_layer import wrap_legacy_types +from ariadne_graphql_modules.executable_schema import make_executable_schema +from ariadne_graphql_modules.v1.object_type import ObjectType +from ariadne_graphql_modules.v1.scalar_type import ScalarType +from ariadne_graphql_modules.v1.subscription_type import SubscriptionType +from ariadne_graphql_modules.v1.union_type import UnionType def test_object_type( diff --git a/tests_next/test_deferred_type.py b/tests/test_deferred_type.py similarity index 73% rename from tests_next/test_deferred_type.py rename to tests/test_deferred_type.py index 964e1a3..7f72937 100644 --- a/tests_next/test_deferred_type.py +++ b/tests/test_deferred_type.py @@ -2,25 +2,25 @@ import pytest -from ariadne_graphql_modules.next import deferred +from ariadne_graphql_modules import deferred def test_deferred_returns_deferred_type_with_abs_path(): - deferred_type = deferred("tests_next.types") - assert deferred_type.path == "tests_next.types" + deferred_type = deferred("tests.types") + assert deferred_type.path == "tests.types" def test_deferred_returns_deferred_type_with_relative_path(): class MockType: deferred_type = deferred(".types") - assert MockType.deferred_type.path == "tests_next.types" + assert MockType.deferred_type.path == "tests.types" def test_deferred_returns_deferred_type_with_higher_level_relative_path(monkeypatch): frame_mock = Mock(f_globals={"__package__": "lorem.ipsum"}) monkeypatch.setattr( - "ariadne_graphql_modules.next.deferredtype.sys._getframe", + "ariadne_graphql_modules.deferredtype.sys._getframe", Mock(return_value=frame_mock), ) @@ -33,7 +33,7 @@ class MockType: def test_deferred_raises_error_for_invalid_relative_path(monkeypatch, data_regression): frame_mock = Mock(f_globals={"__package__": "lorem"}) monkeypatch.setattr( - "ariadne_graphql_modules.next.deferredtype.sys._getframe", + "ariadne_graphql_modules.deferredtype.sys._getframe", Mock(return_value=frame_mock), ) diff --git a/tests_next/test_description_node.py b/tests/test_description_node.py similarity index 94% rename from tests_next/test_description_node.py rename to tests/test_description_node.py index e9f7101..19a6960 100644 --- a/tests_next/test_description_node.py +++ b/tests/test_description_node.py @@ -1,4 +1,4 @@ -from ariadne_graphql_modules.next import get_description_node +from ariadne_graphql_modules import get_description_node def test_no_description_is_returned_for_none(): diff --git a/tests/test_enum_type.py b/tests/test_enum_type.py index 7ea4585..8e051d7 100644 --- a/tests/test_enum_type.py +++ b/tests/test_enum_type.py @@ -1,304 +1,522 @@ from enum import Enum -import pytest -from ariadne import SchemaDirectiveVisitor -from graphql import GraphQLError, graphql_sync +from graphql import graphql_sync from ariadne_graphql_modules import ( - DirectiveType, - EnumType, - ObjectType, + GraphQLEnum, + GraphQLObject, make_executable_schema, ) -def test_enum_type_raises_attribute_error_when_defined_without_schema(data_regression): - with pytest.raises(AttributeError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - pass +class UserLevelEnum(Enum): + GUEST = 0 + MEMBER = 1 + ADMIN = 2 - data_regression.check(str(err.value)) +def test_enum_field_returning_enum_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = UserLevelEnum -def test_enum_type_raises_error_when_defined_with_invalid_schema_type(data_regression): - with pytest.raises(TypeError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = True + class QueryType(GraphQLObject): + level: UserLevel - data_regression.check(str(err.value)) + @GraphQLObject.resolver("level") + def resolve_level(*_) -> UserLevelEnum: + return UserLevelEnum.MEMBER + schema = make_executable_schema(QueryType) -def test_enum_type_raises_error_when_defined_with_invalid_schema_str(data_regression): - with pytest.raises(GraphQLError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = "enom UserRole" + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } - data_regression.check(str(err.value)) + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) + result = graphql_sync(schema, "{ level }") -def test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = "scalar UserRole" + assert not result.errors + assert result.data == {"level": "MEMBER"} - data_regression.check(str(err.value)) +def test_enum_field_returning_dict_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } -def test_enum_type_raises_error_when_defined_with_multiple_types_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> dict: + return 0 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "GUEST"} + + +def test_enum_field_returning_str_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = [ + "GUEST", + "MEMBER", + "ADMIN", + ] + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> str: + return "ADMIN" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_enum_type_with_custom_name(assert_schema_equals): + class UserLevel(GraphQLEnum): + __graphql_name__ = "UserLevelEnum" + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + level: UserLevel + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + level: UserLevelEnum! + } + + enum UserLevelEnum { + GUEST + MEMBER + ADMIN + } + """, + ) + + +def test_enum_type_with_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __description__ = "Hello world." + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + level: UserLevel + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) + + +def test_enum_type_with_member_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = UserLevelEnum + __members_descriptions__ = {"MEMBER": "Hello world."} + + class QueryType(GraphQLObject): + level: UserLevel - enum Category { - CATEGORY - LINK + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + + enum UserLevel { + GUEST + + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN + } + """, + ) + + +def test_schema_enum_field_returning_enum_value(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN } """ + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> UserLevelEnum: + return UserLevelEnum.MEMBER + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) + + result = graphql_sync(schema, "{ level }") - data_regression.check(str(err.value)) + assert not result.errors + assert result.data == {"level": "MEMBER"} -def test_enum_type_extracts_graphql_name(): - class UserRoleEnum(EnumType): +def test_schema_enum_field_returning_dict_value(assert_schema_equals): + class UserLevel(GraphQLEnum): __schema__ = """ - enum UserRole { - USER - MOD - ADMIN + enum UserLevel { + GUEST + MEMBER + ADMIN } + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + level: UserLevel! + } + + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) - assert UserRoleEnum.graphql_name == "UserRole" + result = graphql_sync(schema, "{ level }") + assert not result.errors + assert result.data == {"level": "ADMIN"} -def test_enum_type_can_be_extended_with_new_values(): - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): + +def test_schema_enum_field_returning_str_value(assert_schema_equals): + class UserLevel(GraphQLEnum): __schema__ = """ - enum UserRole { - USER - MOD - ADMIN + enum UserLevel { + GUEST + MEMBER + ADMIN } + """ + + class QueryType(GraphQLObject): + level: UserLevel + + @GraphQLObject.resolver("level") + def resolve_level(*_) -> str: + return "GUEST" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + level: UserLevel! + } - class ExtendUserRoleEnum(EnumType): - __schema__ = """ - extend enum UserRole { - MVP + enum UserLevel { + GUEST + MEMBER + ADMIN } - """ - __requires__ = [UserRoleEnum] + """, + ) + + result = graphql_sync(schema, "{ level }") + assert not result.errors + assert result.data == {"level": "GUEST"} -def test_enum_type_can_be_extended_with_directive(): - # pylint: disable=unused-variable - class ExampleDirective(DirectiveType): - __schema__ = "directive @example on ENUM" - __visitor__ = SchemaDirectiveVisitor - class UserRoleEnum(EnumType): +def test_schema_enum_with_description_attr(assert_schema_equals): + class UserLevel(GraphQLEnum): __schema__ = """ - enum UserRole { - USER - MOD - ADMIN + enum UserLevel { + GUEST + MEMBER + ADMIN } - """ + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + __description__ = "Hello world." - class ExtendUserRoleEnum(EnumType): - __schema__ = "extend enum UserRole @example" - __requires__ = [UserRoleEnum, ExampleDirective] + class QueryType(GraphQLObject): + level: UserLevel + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 -class BaseQueryType(ObjectType): - __abstract__ = True - __schema__ = """ - type Query { - enumToRepr(enum: UserRole = USER): String! - reprToEnum: UserRole! - } - """ - __aliases__ = { - "enumToRepr": "enum_repr", - } + schema = make_executable_schema(QueryType) - @staticmethod - def resolve_enum_repr(*_, enum) -> str: - return repr(enum) + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) -def make_test_schema(enum_type): - class QueryType(BaseQueryType): - __requires__ = [enum_type] + result = graphql_sync(schema, "{ level }") - return make_executable_schema(QueryType) + assert not result.errors + assert result.data == {"level": "ADMIN"} -def test_enum_type_can_be_defined_with_dict_mapping(): - class UserRoleEnum(EnumType): +def test_schema_enum_with_schema_description(assert_schema_equals): + class UserLevel(GraphQLEnum): __schema__ = """ - enum UserRole { - USER - MOD - ADMIN + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN } - """ - __enum__ = { - "USER": 0, - "MOD": 1, + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, "ADMIN": 2, } - schema = make_test_schema(UserRoleEnum) + class QueryType(GraphQLObject): + level: UserLevel - # Specfied enum value is reversed - result = graphql_sync(schema, "{ enumToRepr(enum: MOD) }") - assert result.data["enumToRepr"] == "1" + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 - # Default enum value is reversed - result = graphql_sync(schema, "{ enumToRepr }") - assert result.data["enumToRepr"] == "0" + schema = make_executable_schema(QueryType) - # Python value is converted to enum - result = graphql_sync(schema, "{ reprToEnum }", root_value={"reprToEnum": 2}) - assert result.data["reprToEnum"] == "ADMIN" + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + \"\"\"Hello world.\"\"\" + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) -def test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } - """ - __enum__ = { - "USER": 0, - "MODERATOR": 1, - "ADMIN": 2, - } + result = graphql_sync(schema, "{ level }") - data_regression.check(str(err.value)) - - -def test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } - """ - __enum__ = { - "USER": 0, - "REVIEW": 1, - "MOD": 2, - "ADMIN": 3, + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_schema_enum_with_member_description(assert_schema_equals): + class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN } + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } + __members_descriptions__ = {"MEMBER": "Hello world."} - data_regression.check(str(err.value)) + class QueryType(GraphQLObject): + level: UserLevel + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 -def test_enum_type_can_be_defined_with_str_enum_mapping(): - class RoleEnum(str, Enum): - USER = "user" - MOD = "moderator" - ADMIN = "administrator" + schema = make_executable_schema(QueryType) - class UserRoleEnum(EnumType): + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + + enum UserLevel { + GUEST + + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN + } + """, + ) + + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} + + +def test_schema_enum_with_member_schema_description(assert_schema_equals): + class UserLevel(GraphQLEnum): __schema__ = """ - enum UserRole { - USER - MOD - ADMIN + enum UserLevel { + GUEST + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN } - """ - __enum__ = RoleEnum + """ + __members__ = { + "GUEST": 0, + "MEMBER": 1, + "ADMIN": 2, + } - schema = make_test_schema(UserRoleEnum) + class QueryType(GraphQLObject): + level: UserLevel - # Specfied enum value is reversed - result = graphql_sync(schema, "{ enumToRepr(enum: MOD) }") - assert result.data["enumToRepr"] == repr(RoleEnum.MOD) + @GraphQLObject.resolver("level") + def resolve_level(*_) -> int: + return 2 - # Default enum value is reversed - result = graphql_sync(schema, "{ enumToRepr }") - assert result.data["enumToRepr"] == repr(RoleEnum.USER) + schema = make_executable_schema(QueryType) - # Python value is converted to enum - result = graphql_sync( - schema, "{ reprToEnum }", root_value={"reprToEnum": "administrator"} + assert_schema_equals( + schema, + """ + type Query { + level: UserLevel! + } + + enum UserLevel { + GUEST + + \"\"\"Hello world.\"\"\" + MEMBER + ADMIN + } + """, ) - assert result.data["reprToEnum"] == "ADMIN" - - -def test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition( - data_regression, -): - class RoleEnum(str, Enum): - USER = "user" - MODERATOR = "moderator" - ADMIN = "administrator" - - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } - """ - __enum__ = RoleEnum - - data_regression.check(str(err.value)) - - -def test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition( - data_regression, -): - class RoleEnum(str, Enum): - USER = "user" - REVIEW = "review" - MOD = "moderator" - ADMIN = "administrator" - - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } - """ - __enum__ = RoleEnum - data_regression.check(str(err.value)) + result = graphql_sync(schema, "{ level }") + + assert not result.errors + assert result.data == {"level": "ADMIN"} diff --git a/tests_next/test_enum_type_validation.py b/tests/test_enum_type_validation.py similarity index 98% rename from tests_next/test_enum_type_validation.py rename to tests/test_enum_type_validation.py index 29e983a..a5106d1 100644 --- a/tests_next/test_enum_type_validation.py +++ b/tests/test_enum_type_validation.py @@ -2,8 +2,8 @@ import pytest -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next import GraphQLEnum +from ariadne import gql +from ariadne_graphql_modules import GraphQLEnum def test_schema_enum_type_validation_fails_for_invalid_type_schema(data_regression): diff --git a/tests_next/test_get_field_args_from_resolver.py b/tests/test_get_field_args_from_resolver.py similarity index 98% rename from tests_next/test_get_field_args_from_resolver.py rename to tests/test_get_field_args_from_resolver.py index c7e0924..b281c28 100644 --- a/tests_next/test_get_field_args_from_resolver.py +++ b/tests/test_get_field_args_from_resolver.py @@ -1,4 +1,4 @@ -from ariadne_graphql_modules.next.graphql_object import get_field_args_from_resolver +from ariadne_graphql_modules.object_type import get_field_args_from_resolver def test_field_has_no_args_after_obj_and_info_args(): diff --git a/tests_next/test_id_type.py b/tests/test_id_type.py similarity index 98% rename from tests_next/test_id_type.py rename to tests/test_id_type.py index 1a4b2e1..9d53923 100644 --- a/tests_next/test_id_type.py +++ b/tests/test_id_type.py @@ -1,6 +1,6 @@ from graphql import graphql_sync -from ariadne_graphql_modules.next import ( +from ariadne_graphql_modules import ( GraphQLID, GraphQLInput, GraphQLObject, diff --git a/tests/test_input_type.py b/tests/test_input_type.py index daad5e0..d5d09bd 100644 --- a/tests/test_input_type.py +++ b/tests/test_input_type.py @@ -1,293 +1,628 @@ +from typing import Optional + import pytest -from ariadne import SchemaDirectiveVisitor -from graphql import GraphQLError, graphql_sync +from graphql import graphql_sync +from ariadne import gql from ariadne_graphql_modules import ( - DeferredType, - DirectiveType, - EnumType, - InputType, - InterfaceType, - ObjectType, - ScalarType, + GraphQLInput, + GraphQLObject, make_executable_schema, ) -def test_input_type_raises_attribute_error_when_defined_without_schema(data_regression): - with pytest.raises(AttributeError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - pass +def test_input_type_instance_with_all_attrs_values(): + class SearchInput(GraphQLInput): + query: str + age: int + + obj = SearchInput(query="search", age=20) + assert obj.query == "search" + assert obj.age == 20 + + +def test_input_type_instance_with_omitted_attrs_being_none(): + class SearchInput(GraphQLInput): + query: str + age: int + + obj = SearchInput(age=20) + assert obj.query is None + assert obj.age == 20 + + +def test_input_type_instance_with_default_attrs_values(): + class SearchInput(GraphQLInput): + query: str = "default" + age: int = 42 + + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 + + +def test_input_type_instance_with_all_fields_values(): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field() + age: int = GraphQLInput.field() - data_regression.check(str(err.value)) + obj = SearchInput(query="search", age=20) + assert obj.query == "search" + assert obj.age == 20 -def test_input_type_raises_error_when_defined_with_invalid_schema_type(data_regression): - with pytest.raises(TypeError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = True +def test_input_type_instance_with_all_fields_default_values(): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(default_value="default") + age: int = GraphQLInput.field(default_value=42) - data_regression.check(str(err.value)) + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 -def test_input_type_raises_error_when_defined_with_invalid_schema_str(data_regression): - with pytest.raises(GraphQLError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = "inpet UserInput" +def test_input_type_instance_with_invalid_attrs_raising_error(data_regression): + class SearchInput(GraphQLInput): + query: str + age: int - data_regression.check(str(err.value)) + with pytest.raises(TypeError) as exc_info: + SearchInput(age=20, invalid="Ok") + data_regression.check(str(exc_info.value)) -def test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = """ - type User { - id: ID! + +def test_schema_input_type_instance_with_all_attrs_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String + age: Int } """ + ) - data_regression.check(str(err.value)) + query: str + age: int + obj = SearchInput(query="search", age=20) + assert obj.query == "search" + assert obj.age == 20 -def test_input_type_raises_error_when_defined_with_multiple_types_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = """ - input User - input Group +def test_schema_input_type_instance_with_omitted_attrs_being_none(): + class SearchInput(GraphQLInput): + __schema__ = gql( """ + input Search { + query: String + age: Int + } + """ + ) - data_regression.check(str(err.value)) + query: str + age: int + obj = SearchInput(age=20) + assert obj.query is None + assert obj.age == 20 -def test_input_type_raises_error_when_defined_without_fields(data_regression): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = "input User" - data_regression.check(str(err.value)) +def test_schema_input_type_instance_with_default_attrs_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String = "default" + age: Int = 42 + } + """ + ) + query: str + age: int -def test_input_type_extracts_graphql_name(): - class UserInput(InputType): - __schema__ = """ - input User { - id: ID! - } - """ + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 - assert UserInput.graphql_name == "User" +def test_schema_input_type_instance_with_all_attrs_default_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String = "default" + age: Int = 42 + } + """ + ) + + query: str + age: int + + obj = SearchInput() + assert obj.query == "default" + assert obj.age == 42 -def test_input_type_raises_error_when_defined_without_field_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = """ - input User { - id: ID! - role: Role! + +def test_schema_input_type_instance_with_default_attrs_python_values(): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String + age: Int } """ + ) - data_regression.check(str(err.value)) + query: str = "default" + age: int = 42 + obj = SearchInput(age=20) + assert obj.query == "default" + assert obj.age == 20 -def test_input_type_verifies_field_dependency(): - # pylint: disable=unused-variable - class RoleEnum(EnumType): - __schema__ = """ - enum Role { - USER - ADMIN - } + +def test_schema_input_type_instance_with_invalid_attrs_raising_error(data_regression): + class SearchInput(GraphQLInput): + __schema__ = gql( + """ + input Search { + query: String + age: Int + } + """ + ) + + query: str + age: int + + with pytest.raises(TypeError) as exc_info: + SearchInput(age=20, invalid="Ok") + + data_regression.check(str(exc_info.value)) + + +def test_input_type_arg(assert_schema_equals): + class SearchInput(GraphQLInput): + query: Optional[str] + age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + age: Int + } + """, + ) - class UserInput(InputType): + result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') + + assert not result.errors + assert result.data == {"search": "['Hello', None]"} + + +def test_schema_input_type_arg(assert_schema_equals): + class SearchInput(GraphQLInput): __schema__ = """ - input User { - id: ID! - role: Role! + input SearchInput { + query: String + age: Int } """ - __requires__ = [RoleEnum] + query: Optional[str] + age: Optional[int] -def test_input_type_verifies_circular_dependency(): - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = """ - input User { - id: ID! - patron: User + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + age: Int } + """, + ) + + result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') + + assert not result.errors + assert result.data == {"search": "['Hello', None]"} + + +def test_input_type_automatic_out_name_arg(assert_schema_equals): + class SearchInput(GraphQLInput): + query: Optional[str] + min_age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.min_age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + minAge: Int + } + """, + ) + + result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + + assert not result.errors + assert result.data == {"search": "[None, 21]"} -def test_input_type_verifies_circular_dependency_using_deferred_type(): - # pylint: disable=unused-variable - class GroupInput(InputType): +def test_schema_input_type_automatic_out_name_arg(assert_schema_equals): + class SearchInput(GraphQLInput): __schema__ = """ - input Group { - id: ID! - patron: User + input SearchInput { + query: String + minAge: Int } """ - __requires__ = [DeferredType("User")] - class UserInput(InputType): + query: Optional[str] + min_age: Optional[int] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.min_age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + minAge: Int + } + """, + ) + + result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + + assert not result.errors + assert result.data == {"search": "[None, 21]"} + + +def test_schema_input_type_explicit_out_name_arg(assert_schema_equals): + class SearchInput(GraphQLInput): __schema__ = """ - input User { - id: ID! - group: Group + input SearchInput { + query: String + minAge: Int } """ - __requires__ = [GroupInput] + __out_names__ = {"minAge": "age"} + query: Optional[str] + age: Optional[int] -def test_input_type_can_be_extended_with_new_fields(): - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = """ - input User { - id: ID! + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String + minAge: Int } + """, + ) + + result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + + assert not result.errors + assert result.data == {"search": "[None, 21]"} + + +def test_input_type_self_reference(assert_schema_equals): + class SearchInput(GraphQLInput): + query: Optional[str] + extra: Optional["SearchInput"] + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + if input.extra: + extra_repr = input.extra.query + else: + extra_repr = None + + return f"{repr([input.query, extra_repr])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + search(input: SearchInput!): String! + } - class ExtendUserInput(InputType): - __schema__ = """ - extend input User { - name: String! + input SearchInput { + query: String + extra: SearchInput } + """, + ) + + result = graphql_sync( + schema, """ - __requires__ = [UserInput] + { + search( + input: { query: "Hello", extra: { query: "Other" } } + ) + } + """, + ) + assert not result.errors + assert result.data == {"search": "['Hello', 'Other']"} -def test_input_type_can_be_extended_with_directive(): - # pylint: disable=unused-variable - class ExampleDirective(DirectiveType): - __schema__ = "directive @example on INPUT_OBJECT" - __visitor__ = SchemaDirectiveVisitor - class UserInput(InputType): +def test_schema_input_type_with_default_value(assert_schema_equals): + class SearchInput(GraphQLInput): __schema__ = """ - input User { - id: ID! + input SearchInput { + query: String = "Search" + age: Int = 42 } """ - class ExtendUserInput(InputType): - __schema__ = """ - extend input User @example + query: str + age: int + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ - __requires__ = [UserInput, ExampleDirective] - - -def test_input_type_raises_error_when_defined_without_extended_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExtendUserInput(InputType): - __schema__ = """ - extend input User { - name: String! - } - """ + type Query { + search(input: SearchInput!): String! + } - data_regression.check(str(err.value)) + input SearchInput { + query: String = "Search" + age: Int = 42 + } + """, + ) + result = graphql_sync(schema, "{ search(input: {}) }") -def test_input_type_raises_error_when_extended_dependency_is_wrong_type( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface User { - id: ID! - } - """ + assert not result.errors + assert result.data == {"search": "['Search', 42]"} - class ExtendUserInput(InputType): - __schema__ = """ - extend input User { - name: String! - } - """ - __requires__ = [ExampleInterface] - data_regression.check(str(err.value)) +def test_input_type_with_field_default_value(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = "default" + age: int = 42 + flag: bool = False + class QueryType(GraphQLObject): + search: str -def test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserInput(InputType): - __schema__ = """ - input User { - id: ID! - } - """ - __args__ = { - "fullName": "full_name", - } + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age, input.flag])}" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String! = "default" + age: Int! = 42 + flag: Boolean! = false + } + """, + ) + + result = graphql_sync(schema, "{ search(input: {}) }") + + assert not result.errors + assert result.data == {"search": "['default', 42, False]"} + + +def test_input_type_with_field_instance_default_value(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(default_value="default") + age: int = GraphQLInput.field(default_value=42) + flag: bool = GraphQLInput.field(default_value=False) + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age, input.flag])}" - data_regression.check(str(err.value)) + schema = make_executable_schema(QueryType) + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + query: String! = "default" + age: Int! = 42 + flag: Boolean! = false + } + """, + ) + + result = graphql_sync(schema, "{ search(input: {}) }") -class UserInput(InputType): - __schema__ = """ - input UserInput { - id: ID! - fullName: String! - } - """ - __args__ = { - "fullName": "full_name", - } + assert not result.errors + assert result.data == {"search": "['default', 42, False]"} -class GenericScalar(ScalarType): - __schema__ = "scalar Generic" +def test_input_type_with_field_type(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(graphql_type=int) + class QueryType(GraphQLObject): + search: str -class QueryType(ObjectType): - __schema__ = """ - type Query { - reprInput(input: UserInput): Generic! - } - """ - __aliases__ = {"reprInput": "repr_input"} - __requires__ = [GenericScalar, UserInput] + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return str(input) - @staticmethod - def resolve_repr_input(*_, input): # pylint: disable=redefined-builtin - return input + schema = make_executable_schema(QueryType) + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } -schema = make_executable_schema(QueryType) + input SearchInput { + query: Int! + } + """, + ) -def test_input_type_maps_args_to_python_dict_keys(): - result = graphql_sync(schema, '{ reprInput(input: {id: "1", fullName: "Alice"}) }') - assert result.data == { - "reprInput": {"id": "1", "full_name": "Alice"}, - } +def test_schema_input_type_with_field_description(assert_schema_equals): + class SearchInput(GraphQLInput): + __schema__ = """ + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + """ + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return str(input) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + """, + ) + + +def test_input_type_with_field_description(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(description="Hello world.") + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return str(input) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + """, + ) diff --git a/tests_next/test_input_type_validation.py b/tests/test_input_type_validation.py similarity index 97% rename from tests_next/test_input_type_validation.py rename to tests/test_input_type_validation.py index f1e6273..94ea418 100644 --- a/tests_next/test_input_type_validation.py +++ b/tests/test_input_type_validation.py @@ -1,7 +1,7 @@ import pytest -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next import GraphQLInput +from ariadne import gql +from ariadne_graphql_modules import GraphQLInput def test_schema_input_type_validation_fails_for_invalid_type_schema(data_regression): diff --git a/tests/test_interface_type.py b/tests/test_interface_type.py index be3637d..6ff2bbb 100644 --- a/tests/test_interface_type.py +++ b/tests/test_interface_type.py @@ -1,462 +1,369 @@ -from dataclasses import dataclass +from typing import List, Union -import pytest -from ariadne import SchemaDirectiveVisitor -from graphql import GraphQLError, graphql_sync +from graphql import graphql_sync from ariadne_graphql_modules import ( - DeferredType, - DirectiveType, - InterfaceType, - ObjectType, + GraphQLID, + GraphQLObject, + GraphQLInterface, + GraphQLUnion, make_executable_schema, ) -def test_interface_type_raises_attribute_error_when_defined_without_schema( - data_regression, -): - with pytest.raises(AttributeError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - pass +class CommentType(GraphQLObject): + id: GraphQLID + content: str - data_regression.check(str(err.value)) +def test_interface_without_schema(assert_schema_equals): + class UserInterface(GraphQLInterface): + summary: str + score: int -def test_interface_type_raises_error_when_defined_with_invalid_schema_type( - data_regression, -): - with pytest.raises(TypeError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = True + class UserType(GraphQLObject, UserInterface): + name: str - data_regression.check(str(err.value)) + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(id=1, username="Bob"), + CommentType(id=2, content="Hello World!"), + ] -def test_interface_type_raises_error_when_defined_with_invalid_schema_str( - data_regression, -): - with pytest.raises(GraphQLError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = "interfaco Example" - - data_regression.check(str(err.value)) + schema = make_executable_schema(QueryType, UserInterface, UserType) + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } -def test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = "type Example" + union Result = User | Comment - data_regression.check(str(err.value)) + type User implements UserInterface { + summary: String! + score: Int! + name: String! + } + type Comment { + id: ID! + content: String! + } -def test_interface_type_raises_error_when_defined_with_multiple_types_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example + interface UserInterface { + summary: String! + score: Int! + } - interface Other - """ + """, + ) + + +def test_interface_inheritance_without_schema(assert_schema_equals): + def hello_resolver(*_, name: str) -> str: + return f"Hello {name}!" + + class UserInterface(GraphQLInterface): + summary: str + score: str = GraphQLInterface.field( + hello_resolver, + name="better_score", + graphql_type=str, + args={"name": GraphQLInterface.argument(name="json")}, + description="desc", + default_value="my_json", + ) + + class UserType(GraphQLObject, UserInterface): + name: str = GraphQLInterface.field( + name="name", + graphql_type=str, + args={"name": GraphQLInterface.argument(name="json")}, + default_value="my_json", + ) + + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(), + CommentType(id=2, content="Hello World!"), + ] + + schema = make_executable_schema(QueryType, UserInterface, UserType) + + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } - data_regression.check(str(err.value)) + union Result = User | Comment + type User implements UserInterface { + summary: String! -def test_interface_type_raises_error_when_defined_without_fields(data_regression): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = "interface Example" + \"\"\"desc\"\"\" + better_score(json: String!): String! + name: String! + } - data_regression.check(str(err.value)) + type Comment { + id: ID! + content: String! + } + interface UserInterface { + summary: String! -def test_interface_type_extracts_graphql_name(): - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - id: ID! + \"\"\"desc\"\"\" + better_score(json: String!): String! } - """ - assert ExampleInterface.graphql_name == "Example" + """, + ) + result = graphql_sync( + schema, '{ search { ... on User{ better_score(json: "test") } } }' + ) -def test_interface_type_raises_error_when_defined_without_return_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - group: Group - groups: [Group!] - } - """ + assert not result.errors + assert result.data == {"search": [{"better_score": "Hello test!"}, {}]} - data_regression.check(str(err.value)) - -def test_interface_type_verifies_field_dependency(): - # pylint: disable=unused-variable - class GroupType(ObjectType): +def test_interface_with_schema(assert_schema_equals): + class UserInterface(GraphQLInterface): __schema__ = """ - type Group { - id: ID! + interface UserInterface { + summary: String! + score: Int! } """ - class ExampleInterface(InterfaceType): + class UserType(GraphQLObject): __schema__ = """ - interface Example { - group: Group - groups: [Group!] + type User implements UserInterface { + id: ID! + name: String! + summary: String! + score: Int! } """ - __requires__ = [GroupType] + __implements__ = [UserInterface] -def test_interface_type_verifies_circural_dependency(): - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - parent: Example - } - """ + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(id=1, username="Bob"), + CommentType(id=2, content="Hello World!"), + ] -def test_interface_type_raises_error_when_defined_without_argument_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - actions(input: UserInput): [String!]! - } - """ + schema = make_executable_schema(QueryType, UserType) - data_regression.check(str(err.value)) + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } + union Result = User | Comment -def test_interface_type_verifies_circular_dependency_using_deferred_type(): - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - id: ID! - users: [User] + type User implements UserInterface { + id: ID! + name: String! + summary: String! + score: Int! } - """ - __requires__ = [DeferredType("User")] - class UserType(ObjectType): - __schema__ = """ - type User { - roles: [Example] + interface UserInterface { + summary: String! + score: Int! } - """ - __requires__ = [ExampleInterface] - -def test_interface_type_can_be_extended_with_new_fields(): - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - id: ID! + type Comment { + id: ID! + content: String! } + + """, + ) + + +def test_interface_inheritance2(assert_schema_equals): + class BaseEntityInterface(GraphQLInterface): + id: GraphQLID + + class UserInterface(BaseEntityInterface): + username: str + + class UserType(GraphQLObject, UserInterface): + name: str + + class SuperUserType(UserType): + is_super_user: bool + + class QueryType(GraphQLObject): + @GraphQLObject.field + def users(*_) -> List[UserInterface]: + return [ + UserType(id="1", username="test_user"), + SuperUserType( + id="2", + username="test_super_user", + is_super_user=True, + ), + ] + + schema = make_executable_schema( + QueryType, BaseEntityInterface, UserInterface, UserType, SuperUserType + ) + + assert_schema_equals( + schema, """ + type Query { + users: [UserInterface!]! + } - class ExtendExampleInterface(InterfaceType): - __schema__ = """ - extend interface Example { - name: String + interface UserInterface { + id: ID! + username: String! } - """ - __requires__ = [ExampleInterface] + interface BaseEntityInterface { + id: ID! + } -def test_interface_type_can_be_extended_with_directive(): - # pylint: disable=unused-variable - class ExampleDirective(DirectiveType): - __schema__ = "directive @example on INTERFACE" - __visitor__ = SchemaDirectiveVisitor + type User implements BaseEntityInterface & UserInterface { + id: ID! + username: String! + name: String! + } - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - id: ID! + type SuperUser implements BaseEntityInterface & UserInterface { + id: ID! + username: String! + name: String! + isSuperUser: Boolean! } - """ + """, + ) - class ExtendExampleInterface(InterfaceType): - __schema__ = """ - extend interface Example @example + +def test_interface_descriptions(assert_schema_equals): + class UserInterface(GraphQLInterface): + summary: str + score: int + + __description__ = "Lorem ipsum." + + class UserType(GraphQLObject, UserInterface): + id: GraphQLID + username: str + + class QueryType(GraphQLObject): + @GraphQLObject.field + def user(*_) -> UserType: + return UserType(id="1", username="test_user") + + schema = make_executable_schema(QueryType, UserType, UserInterface) + + assert_schema_equals( + schema, """ - __requires__ = [ExampleInterface, ExampleDirective] + type Query { + user: User! + } + type User implements UserInterface { + summary: String! + score: Int! + id: ID! + username: String! + } -def test_interface_type_can_be_extended_with_other_interface(): - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - id: ID! + \"\"\"Lorem ipsum.\"\"\" + interface UserInterface { + summary: String! + score: Int! } + """, + ) + + +def test_interface_resolvers_and_field_descriptions(assert_schema_equals): + class UserInterface(GraphQLInterface): + summary: str + score: int + + @GraphQLInterface.resolver("score", description="Lorem ipsum.") + def resolve_score(*_): + return 200 + + class UserType(GraphQLObject, UserInterface): + id: GraphQLID + + class MyType(GraphQLObject, UserInterface): + id: GraphQLID + name: str + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[UserInterface]) + def users(*_) -> List[UserInterface]: + return [MyType(id="2", name="old", summary="ss", score=22)] + + schema = make_executable_schema(QueryType, UserType, MyType, UserInterface) + + assert_schema_equals( + schema, """ + type Query { + users: [UserInterface!]! + } - class OtherInterface(InterfaceType): - __schema__ = """ - interface Other { - depth: Int! + interface UserInterface { + summary: String! + + \"\"\"Lorem ipsum.\"\"\" + score: Int! } - """ - class ExtendExampleInterface(InterfaceType): - __schema__ = """ - extend interface Example implements Other - """ - __requires__ = [ExampleInterface, OtherInterface] - - -def test_interface_type_raises_error_when_defined_without_extended_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExtendExampleInterface(ObjectType): - __schema__ = """ - extend interface Example { - name: String - } - """ - - data_regression.check(str(err.value)) - - -def test_interface_type_raises_error_when_extended_dependency_is_wrong_type( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleType(ObjectType): - __schema__ = """ - type Example { - id: ID! - } - """ - - class ExampleInterface(InterfaceType): - __schema__ = """ - extend interface Example { - name: String - } - """ - __requires__ = [ExampleType] - - data_regression.check(str(err.value)) - - -def test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface User { - name: String - } - """ - __aliases__ = { - "joinedDate": "joined_date", - } - - data_regression.check(str(err.value)) - - -def test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface User { - name: String - } - """ - - @staticmethod - def resolve_group(*_): - return None - - data_regression.check(str(err.value)) - - -@dataclass -class User: - id: int - name: str - summary: str - - -@dataclass -class Comment: - id: int - message: str - summary: str - - -class ResultInterface(InterfaceType): - __schema__ = """ - interface Result { - summary: String! - score: Int! - } - """ - - @staticmethod - def resolve_type(instance, *_): - if isinstance(instance, Comment): - return "Comment" - - if isinstance(instance, User): - return "User" - - return None - - @staticmethod - def resolve_score(*_): - return 42 - - -class UserType(ObjectType): - __schema__ = """ - type User implements Result { - id: ID! - name: String! - summary: String! - score: Int! - } - """ - __requires__ = [ResultInterface] - - -class CommentType(ObjectType): - __schema__ = """ - type Comment implements Result { - id: ID! - message: String! - summary: String! - score: Int! - } - """ - __requires__ = [ResultInterface] - - @staticmethod - def resolve_score(*_): - return 16 - - -class QueryType(ObjectType): - __schema__ = """ - type Query { - results: [Result!]! - } - """ - __requires__ = [ResultInterface] - - @staticmethod - def resolve_results(*_): - return [ - User(id=1, name="Alice", summary="Summary for Alice"), - Comment(id=1, message="Hello world!", summary="Summary for comment"), - ] - - -schema = make_executable_schema(QueryType, UserType, CommentType) - - -def test_interface_type_binds_type_resolver(): - query = """ - query { - results { - ... on User { - __typename - id - name - summary - } - ... on Comment { - __typename - id - message - summary - } + type User implements UserInterface { + summary: String! + + \"\"\"Lorem ipsum.\"\"\" + score: Int! + id: ID! } - } - """ - - result = graphql_sync(schema, query) - assert result.data == { - "results": [ - { - "__typename": "User", - "id": "1", - "name": "Alice", - "summary": "Summary for Alice", - }, - { - "__typename": "Comment", - "id": "1", - "message": "Hello world!", - "summary": "Summary for comment", - }, - ], - } - - -def test_interface_type_binds_field_resolvers_to_implementing_types_fields(): - query = """ - query { - results { - ... on User { - __typename - score - } - ... on Comment { - __typename - score - } + + type My implements UserInterface { + summary: String! + + \"\"\"Lorem ipsum.\"\"\" + score: Int! + id: ID! + name: String! } - } - """ - - result = graphql_sync(schema, query) - assert result.data == { - "results": [ - { - "__typename": "User", - "score": 42, - }, - { - "__typename": "Comment", - "score": 16, - }, - ], - } + """, + ) + result = graphql_sync(schema, "{ users { ... on My { __typename score } } }") + + assert not result.errors + assert result.data == {"users": [{"__typename": "My", "score": 200}]} diff --git a/tests/test_interface_type_validation.py b/tests/test_interface_type_validation.py new file mode 100644 index 0000000..d441080 --- /dev/null +++ b/tests/test_interface_type_validation.py @@ -0,0 +1,23 @@ +import pytest + +from ariadne_graphql_modules import ( + GraphQLID, + GraphQLObject, + GraphQLInterface, + make_executable_schema, +) + + +def test_interface_no_interface_in_schema(data_regression): + with pytest.raises(TypeError) as exc_info: + + class BaseInterface(GraphQLInterface): + id: GraphQLID + + class UserType(GraphQLObject, BaseInterface): + username: str + email: str + + make_executable_schema(UserType) + + data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_make_executable_schema.py b/tests/test_make_executable_schema.py similarity index 98% rename from tests_next/test_make_executable_schema.py rename to tests/test_make_executable_schema.py index b927649..635901b 100644 --- a/tests_next/test_make_executable_schema.py +++ b/tests/test_make_executable_schema.py @@ -10,7 +10,7 @@ ) from ariadne import QueryType, SchemaDirectiveVisitor -from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema +from ariadne_graphql_modules import GraphQLObject, make_executable_schema def test_executable_schema_from_vanilla_schema_definition(assert_schema_equals): diff --git a/tests_next/test_metadata.py b/tests/test_metadata.py similarity index 96% rename from tests_next/test_metadata.py rename to tests/test_metadata.py index d02f5f6..443d022 100644 --- a/tests_next/test_metadata.py +++ b/tests/test_metadata.py @@ -2,7 +2,7 @@ import pytest -from ariadne_graphql_modules.next import GraphQLObject, graphql_enum +from ariadne_graphql_modules import GraphQLObject, graphql_enum class QueryType(GraphQLObject): diff --git a/tests/test_object_type.py b/tests/test_object_type.py index 8adf0e6..1d5f247 100644 --- a/tests/test_object_type.py +++ b/tests/test_object_type.py @@ -1,410 +1,1015 @@ +from typing import Optional + import pytest -from ariadne import SchemaDirectiveVisitor -from graphql import GraphQLError, graphql_sync +from graphql import graphql_sync -from ariadne_graphql_modules import ( - DeferredType, - DirectiveType, - InterfaceType, - ObjectType, - make_executable_schema, -) +from ariadne import gql +from ariadne_graphql_modules import GraphQLObject, make_executable_schema -def test_object_type_raises_attribute_error_when_defined_without_schema( - data_regression, -): - with pytest.raises(AttributeError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - pass +def test_object_type_instance_with_all_attrs_values(): + class CategoryType(GraphQLObject): + name: str + posts: int - data_regression.check(str(err.value)) + obj = CategoryType(name="Welcome", posts=20) + assert obj.name == "Welcome" + assert obj.posts == 20 -def test_object_type_raises_error_when_defined_with_invalid_schema_type( - data_regression, -): - with pytest.raises(TypeError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = True +def test_object_type_instance_with_omitted_attrs_being_none(): + class CategoryType(GraphQLObject): + name: str + posts: int - data_regression.check(str(err.value)) + obj = CategoryType(posts=20) + assert obj.name is None + assert obj.posts == 20 -def test_object_type_raises_error_when_defined_with_invalid_schema_str(data_regression): - with pytest.raises(GraphQLError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = "typo User" +def test_object_type_instance_with_aliased_attrs_values(): + class CategoryType(GraphQLObject): + name: str + posts: int - data_regression.check(str(err.value)) + __aliases__ = {"name": "title"} + title: str -def test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = "scalar DateTime" + obj = CategoryType(title="Welcome", posts=20) + assert obj.title == "Welcome" + assert obj.posts == 20 - data_regression.check(str(err.value)) +def test_object_type_instance_with_omitted_attrs_being_default_values(): + class CategoryType(GraphQLObject): + name: str = "Hello" + posts: int = 42 -def test_object_type_raises_error_when_defined_with_multiple_types_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User + obj = CategoryType(posts=20) + assert obj.name == "Hello" + assert obj.posts == 20 + + +def test_object_type_instance_with_all_attrs_being_default_values(): + class CategoryType(GraphQLObject): + name: str = "Hello" + posts: int = 42 + + obj = CategoryType() + assert obj.name == "Hello" + assert obj.posts == 42 + + +def test_object_type_instance_with_invalid_attrs_raising_error(data_regression): + class CategoryType(GraphQLObject): + name: str + posts: int - type Group + with pytest.raises(TypeError) as exc_info: + CategoryType(name="Welcome", invalid="Ok") + + data_regression.check(str(exc_info.value)) + + +def test_schema_object_type_instance_with_all_attrs_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } """ + ) + + name: str + posts: int - data_regression.check(str(err.value)) + obj = CategoryType(name="Welcome", posts=20) + assert obj.name == "Welcome" + assert obj.posts == 20 -def test_object_type_raises_error_when_defined_without_fields(data_regression): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = "type User" +def test_schema_object_type_instance_with_omitted_attrs_being_none(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) - data_regression.check(str(err.value)) + name: str + posts: int + obj = CategoryType(posts=20) + assert obj.name is None + assert obj.posts == 20 -def test_object_type_extracts_graphql_name(): - class GroupType(ObjectType): - __schema__ = """ - type Group { - id: ID! - } - """ - assert GroupType.graphql_name == "Group" +def test_schema_object_type_instance_with_omitted_attrs_being_default_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + name: str = "Hello" + posts: int = 42 -def test_object_type_accepts_all_builtin_scalar_types(): - # pylint: disable=unused-variable - class FancyObjectType(ObjectType): - __schema__ = """ - type FancyObject { - id: ID! - someInt: Int! - someFloat: Float! - someBoolean: Boolean! - someString: String! - } - """ + obj = CategoryType(posts=20) + assert obj.name == "Hello" + assert obj.posts == 20 -def test_object_type_raises_error_when_defined_without_return_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - group: Group - groups: [Group!] +def test_schema_object_type_instance_with_all_attrs_being_default_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int } """ + ) - data_regression.check(str(err.value)) + name: str = "Hello" + posts: int = 42 + obj = CategoryType() + assert obj.name == "Hello" + assert obj.posts == 42 -def test_object_type_verifies_field_dependency(): - # pylint: disable=unused-variable - class GroupType(ObjectType): - __schema__ = """ - type Group { - id: ID! - } - """ - class UserType(ObjectType): - __schema__ = """ - type User { - group: Group - groups: [Group!] - } - """ - __requires__ = [GroupType] +def test_schema_object_type_instance_with_aliased_attrs_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + __aliases__ = {"name": "title"} + title: str = "Hello" + posts: int = 42 -def test_object_type_verifies_circular_dependency(): - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - follows: User - } - """ + obj = CategoryType(title="Ok") + assert obj.title == "Ok" + assert obj.posts == 42 -def test_object_type_raises_error_when_defined_without_argument_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - actions(input: UserInput): [String!]! +def test_schema_object_type_instance_with_aliased_attrs_default_values(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int } """ + ) + __aliases__ = {"name": "title"} - data_regression.check(str(err.value)) + title: str = "Hello" + posts: int = 42 + obj = CategoryType() + assert obj.title == "Hello" + assert obj.posts == 42 -def test_object_type_verifies_circular_dependency_using_deferred_type(): - # pylint: disable=unused-variable - class GroupType(ObjectType): - __schema__ = """ - type Group { - id: ID! - users: [User] - } + +def test_schema_object_type_instance_with_invalid_attrs_raising_error(data_regression): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str + posts: int + + with pytest.raises(TypeError) as exc_info: + CategoryType(name="Welcome", invalid="Ok") + + data_regression.check(str(exc_info.value)) + + +def test_schema_object_type_instance_with_aliased_attr_value(): + class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + __aliases__ = {"name": "title"} + + title: str + posts: int + + obj = CategoryType(title="Welcome", posts=20) + assert obj.title == "Welcome" + assert obj.posts == 20 + + +def test_object_type_with_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ - __requires__ = [DeferredType("User")] + type Query { + hello: String! + } + """, + ) - class UserType(ObjectType): - __schema__ = """ - type User { - group: Group + result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_alias(assert_schema_equals): + class QueryType(GraphQLObject): + __aliases__ = {"hello": "welcome_message"} + + hello: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! } + """, + ) + + result = graphql_sync( + schema, "{ hello }", root_value={"welcome_message": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_alias_excludes_alias_targets(assert_schema_equals): + class QueryType(GraphQLObject): + __aliases__ = {"hello": "welcome"} + + hello: str + welcome: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ - __requires__ = [GroupType] + type Query { + hello: String! + } + """, + ) + result = graphql_sync(schema, "{ hello }", root_value={"welcome": "Hello World!"}) -def test_object_type_can_be_extended_with_new_fields(): - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_alias_includes_aliased_field_instances(assert_schema_equals): + class QueryType(GraphQLObject): + __aliases__ = {"hello": "welcome"} + + hello: str + welcome: str = GraphQLObject.field() + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! + welcome: String! } + """, + ) + + result = graphql_sync( + schema, "{ hello welcome }", root_value={"welcome": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"hello": "Hello World!", "welcome": "Hello World!"} + + +def test_object_type_with_attr_automatic_alias(assert_schema_equals): + class QueryType(GraphQLObject): + test_message: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + testMessage: String! + } + """, + ) - class ExtendUserType(ObjectType): - __schema__ = """ - extend type User { - name: String + result = graphql_sync( + schema, "{ testMessage }", root_value={"test_message": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"testMessage": "Hello World!"} + + +def test_object_type_with_field_instance_automatic_alias(assert_schema_equals): + class QueryType(GraphQLObject): + message: str = GraphQLObject.field(name="testMessage") + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + testMessage: String! } + """, + ) + + result = graphql_sync( + schema, "{ testMessage }", root_value={"message": "Hello World!"} + ) + + assert not result.errors + assert result.data == {"testMessage": "Hello World!"} + + +def test_object_type_with_field_resolver(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field + def hello(obj, info) -> str: + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ - __requires__ = [UserType] + type Query { + hello: String! + } + """, + ) + result = graphql_sync(schema, "{ hello }") -def test_object_type_can_be_extended_with_directive(): - # pylint: disable=unused-variable - class ExampleDirective(DirectiveType): - __schema__ = "directive @example on OBJECT" - __visitor__ = SchemaDirectiveVisitor + assert not result.errors + assert result.data == {"hello": "Hello World!"} - class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! + +def test_object_type_with_typed_field_instance(assert_schema_equals): + class QueryType(GraphQLObject): + hello = GraphQLObject.field(lambda *_: "Hello World!", graphql_type=str) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_annotated_field_instance(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str = GraphQLObject.field(lambda *_: "Hello World!") + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} - class ExtendUserType(ObjectType): - __schema__ = """ - extend type User @example + +def test_object_type_with_typed_field_and_field_resolver(assert_schema_equals): + class QueryType(GraphQLObject): + name: str + + @GraphQLObject.field + def hello(obj, info) -> str: + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ - __requires__ = [UserType, ExampleDirective] + type Query { + name: String! + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ name hello }", root_value={"name": "Ok"}) + + assert not result.errors + assert result.data == {"name": "Ok", "hello": "Hello World!"} -def test_object_type_can_be_extended_with_interface(): - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Interface { - id: ID! +def test_object_type_with_schema(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! + } + """ + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello: String! } + """, + ) + + result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_object_type_with_nested_types(assert_schema_equals): + class UserType(GraphQLObject): + name: str + + class PostType(GraphQLObject): + message: str + + class QueryType(GraphQLObject): + user: UserType + + @GraphQLObject.field(graphql_type=PostType) + def post(obj, info): + return {"message": "test"} + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + user: User! + post: Post! + } - class UserType(ObjectType): - __schema__ = """ type User { - id: ID! + name: String! + } + + type Post { + message: String! } + """, + ) + + result = graphql_sync( + schema, + "{ user { name } post { message } }", + root_value={"user": {"name": "Bob"}}, + ) + + assert not result.errors + assert result.data == { + "user": { + "name": "Bob", + }, + "post": {"message": "test"}, + } + + +def test_resolver_decorator_sets_resolver_for_type_hint_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_resolver_for_instance_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str = GraphQLObject.field(name="hello") - class ExtendUserType(ObjectType): - __schema__ = """ - extend type User implements Interface + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, """ - __requires__ = [UserType, ExampleInterface] + type Query { + hello: String! + } + """, + ) + result = graphql_sync(schema, "{ hello }") -def test_object_type_raises_error_when_defined_without_extended_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExtendUserType(ObjectType): - __schema__ = """ - extend type User { - name: String + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_resolver_for_field_in_schema(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello: String! } """ + ) - data_regression.check(str(err.value)) + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" + schema = make_executable_schema(QueryType) -def test_object_type_raises_error_when_extended_dependency_is_wrong_type( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Example { - id: ID! - } - """ + assert_schema_equals( + schema, + """ + type Query { + hello: String! + } + """, + ) - class ExampleType(ObjectType): - __schema__ = """ - extend type Example { - name: String - } - """ - __requires__ = [ExampleInterface] + result = graphql_sync(schema, "{ hello }") - data_regression.check(str(err.value)) + assert not result.errors + assert result.data == {"hello": "Hello World!"} -def test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - name: String - } +def test_object_type_with_description(assert_schema_equals): + class QueryType(GraphQLObject): + __description__ = "Lorem ipsum." + + hello: str + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + \"\"\"Lorem ipsum.\"\"\" + type Query { + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_field_decorator_sets_description_for_field(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field(description="Lorem ipsum.") + def hello(obj, info) -> str: + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + \"\"\"Lorem ipsum.\"\"\" + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_field_decorator_sets_description_for_field_arg(assert_schema_equals): + class QueryType(GraphQLObject): + @GraphQLObject.field( + args={"name": GraphQLObject.argument(description="Lorem ipsum.")} + ) + def hello(obj, info, name: str) -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_resolver_decorator_sets_description_for_type_hint_field(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello", description="Lorem ipsum.") + def resolve_hello(*_): + return "Hello World!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + \"\"\"Lorem ipsum.\"\"\" + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_description_for_field_in_schema(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( """ - __aliases__ = { - "joinedDate": "joined_date", + type Query { + hello: String! } + """ + ) - data_regression.check(str(err.value)) + @GraphQLObject.resolver("hello", description="Lorem ipsum.") + def resolve_hello(*_): + return "Hello World!" + schema = make_executable_schema(QueryType) -def test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - name: String + assert_schema_equals( + schema, + """ + type Query { + \"\"\"Lorem ipsum.\"\"\" + hello: String! + } + """, + ) + + result = graphql_sync(schema, "{ hello }") + + assert not result.errors + assert result.data == {"hello": "Hello World!"} + + +def test_resolver_decorator_sets_description_for_field_arg(assert_schema_equals): + class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(description="Lorem ipsum.")} + ) + def resolve_hello(obj, info, name: str) -> str: + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_schema_sets_description_for_field_arg(assert_schema_equals): + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! } """ + ) - @staticmethod - def resolve_group(*_): - return None + @GraphQLObject.resolver("hello") + def resolve_hello(*_, name: str): + return f"Hello {name}!" - data_regression.check(str(err.value)) + schema = make_executable_schema(QueryType) + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Lorem ipsum.\"\"\" + name: String! + ): String! + } + """, + ) -def test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field( - data_regression, + result = graphql_sync(schema, '{ hello(name: "Bob") }') + + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} + + +def test_resolver_decorator_sets_description_for_field_arg_in_schema( + assert_schema_equals, ): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - name: String + class QueryType(GraphQLObject): + __schema__ = gql( + """ + type Query { + hello(name: String!): String! } """ - __fields_args__ = {"group": {}} + ) + + @GraphQLObject.resolver( + "hello", args={"name": GraphQLObject.argument(description="Description")} + ) + def resolve_hello(*_, name: str): + return f"Hello {name}!" + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + hello( + \"\"\"Description\"\"\" + name: String! + ): String! + } + """, + ) + + result = graphql_sync(schema, '{ hello(name: "Bob") }') - data_regression.check(str(err.value)) + assert not result.errors + assert result.data == {"hello": "Hello Bob!"} -def test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg( - data_regression, +def test_object_type_self_reference( + assert_schema_equals, ): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UserType(ObjectType): - __schema__ = """ - type User { - name: String - } - """ - __fields_args__ = {"name": {"arg": "arg2"}} + class CategoryType(GraphQLObject): + name: str + parent: Optional["CategoryType"] - data_regression.check(str(err.value)) + class QueryType(GraphQLObject): + category: CategoryType + schema = make_executable_schema(QueryType) -class QueryType(ObjectType): - __schema__ = """ - type Query { - field: String! - other: String! - firstField: String! - secondField: String! - fieldWithArg(someArg: String): String! - } - """ - __aliases__ = { - "firstField": "first_field", - "secondField": "second_field", - "fieldWithArg": "field_with_arg", + assert_schema_equals( + schema, + """ + type Query { + category: Category! + } + + type Category { + name: String! + parent: Category + } + """, + ) + + result = graphql_sync( + schema, + "{ category { name parent { name } } }", + root_value={ + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + }, + }, + ) + + assert not result.errors + assert result.data == { + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + }, } - __fields_args__ = {"fieldWithArg": {"someArg": "some_arg"}} - @staticmethod - def resolve_other(*_): - return "Word Up!" - @staticmethod - def resolve_second_field(obj, *_): - return "Obj: %s" % obj["secondField"] +def test_object_type_return_instance( + assert_schema_equals, +): + class CategoryType(GraphQLObject): + name: str + color: str + + class QueryType(GraphQLObject): + @GraphQLObject.field() + def category(*_) -> CategoryType: + return CategoryType( + name="Welcome", + color="#FF00FF", + ) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + category: Category! + } - @staticmethod - def resolve_field_with_arg(*_, some_arg): - return some_arg + type Category { + name: String! + color: String! + } + """, + ) + result = graphql_sync(schema, "{ category { name color } }") -schema = make_executable_schema(QueryType) + assert not result.errors + assert result.data == { + "category": { + "name": "Welcome", + "color": "#FF00FF", + }, + } -def test_object_resolves_field_with_default_resolver(): - result = graphql_sync(schema, "{ field }", root_value={"field": "Hello!"}) - assert result.data["field"] == "Hello!" +def test_object_type_nested_type( + assert_schema_equals, +): + class UserType(GraphQLObject): + username: str + class CategoryType(GraphQLObject): + name: str + parent: Optional["CategoryType"] + owner: UserType -def test_object_resolves_field_with_custom_resolver(): - result = graphql_sync(schema, "{ other }") - assert result.data["other"] == "Word Up!" + class QueryType(GraphQLObject): + category: CategoryType + schema = make_executable_schema(QueryType) -def test_object_resolves_field_with_aliased_default_resolver(): - result = graphql_sync( - schema, "{ firstField }", root_value={"first_field": "Howdy?"} - ) - assert result.data["firstField"] == "Howdy?" + assert_schema_equals( + schema, + """ + type Query { + category: Category! + } + type Category { + name: String! + parent: Category + owner: User! + } -def test_object_resolves_field_with_aliased_custom_resolver(): - result = graphql_sync(schema, "{ secondField }", root_value={"secondField": "Hey!"}) - assert result.data["secondField"] == "Obj: Hey!" + type User { + username: String! + } + """, + ) + result = graphql_sync( + schema, + "{ category { name parent { name } owner { username } } }", + root_value={ + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + "owner": { + "username": "John", + }, + }, + }, + ) -def test_object_resolves_field_with_arg_out_name_customized(): - result = graphql_sync(schema, '{ fieldWithArg(someArg: "test") }') - assert result.data["fieldWithArg"] == "test" + assert not result.errors + assert result.data == { + "category": { + "name": "Lorem", + "parent": { + "name": "Ipsum", + }, + "owner": { + "username": "John", + }, + }, + } diff --git a/tests_next/test_object_type_field_args.py b/tests/test_object_type_field_args.py similarity index 99% rename from tests_next/test_object_type_field_args.py rename to tests/test_object_type_field_args.py index 2a358cb..10e815b 100644 --- a/tests_next/test_object_type_field_args.py +++ b/tests/test_object_type_field_args.py @@ -1,6 +1,6 @@ from graphql import GraphQLResolveInfo, graphql_sync -from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema +from ariadne_graphql_modules import GraphQLObject, make_executable_schema def test_object_type_field_resolver_with_scalar_arg(assert_schema_equals): diff --git a/tests_next/test_object_type_validation.py b/tests/test_object_type_validation.py similarity index 99% rename from tests_next/test_object_type_validation.py rename to tests/test_object_type_validation.py index 2fcf813..4396a51 100644 --- a/tests_next/test_object_type_validation.py +++ b/tests/test_object_type_validation.py @@ -1,7 +1,7 @@ import pytest -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next import GraphQLObject +from ariadne import gql +from ariadne_graphql_modules import GraphQLObject def test_schema_object_type_validation_fails_for_invalid_type_schema(data_regression): diff --git a/tests/test_scalar_type.py b/tests/test_scalar_type.py index ec85340..a880d5b 100644 --- a/tests/test_scalar_type.py +++ b/tests/test_scalar_type.py @@ -1,287 +1,111 @@ -from datetime import date, datetime +from datetime import date -import pytest -from ariadne import SchemaDirectiveVisitor -from graphql import GraphQLError, StringValueNode, graphql_sync +from graphql import graphql_sync +from ariadne import gql from ariadne_graphql_modules import ( - DirectiveType, - ObjectType, - ScalarType, + GraphQLObject, + GraphQLScalar, make_executable_schema, ) -def test_scalar_type_raises_attribute_error_when_defined_without_schema( - data_regression, -): - with pytest.raises(AttributeError) as err: - # pylint: disable=unused-variable - class DateScalar(ScalarType): - pass +class DateScalar(GraphQLScalar[date]): + @classmethod + def serialize(cls, value): + if isinstance(value, cls): + return str(value.unwrap()) - data_regression.check(str(err.value)) + return str(value) -def test_scalar_type_raises_error_when_defined_with_invalid_schema_type( - data_regression, -): - with pytest.raises(TypeError) as err: - # pylint: disable=unused-variable - class DateScalar(ScalarType): - __schema__ = True +def test_scalar_field_returning_scalar_instance(assert_schema_equals): + class QueryType(GraphQLObject): + date: DateScalar - data_regression.check(str(err.value)) + @GraphQLObject.resolver("date") + def resolve_date(*_) -> DateScalar: + return DateScalar(date(1989, 10, 30)) + schema = make_executable_schema(QueryType) -def test_scalar_type_raises_error_when_defined_with_invalid_schema_str(data_regression): - with pytest.raises(GraphQLError) as err: - # pylint: disable=unused-variable - class DateScalar(ScalarType): - __schema__ = "scalor Date" - - data_regression.check(str(err.value)) - - -def test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class DateScalar(ScalarType): - __schema__ = "type DateTime" - - data_regression.check(str(err.value)) - - -def test_scalar_type_raises_error_when_defined_with_multiple_types_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class DateScalar(ScalarType): - __schema__ = """ - scalar Date - - scalar DateTime - """ - - data_regression.check(str(err.value)) - - -def test_scalar_type_extracts_graphql_name(): - class DateScalar(ScalarType): - __schema__ = "scalar Date" - - assert DateScalar.graphql_name == "Date" - - -def test_scalar_type_can_be_extended_with_directive(): - # pylint: disable=unused-variable - class ExampleDirective(DirectiveType): - __schema__ = "directive @example on SCALAR" - __visitor__ = SchemaDirectiveVisitor - - class DateScalar(ScalarType): - __schema__ = "scalar Date" - - class ExtendDateScalar(ScalarType): - __schema__ = "extend scalar Date @example" - __requires__ = [DateScalar, ExampleDirective] - - -class DateReadOnlyScalar(ScalarType): - __schema__ = "scalar DateReadOnly" - - @staticmethod - def serialize(date): - return date.strftime("%Y-%m-%d") - - -class DateInputScalar(ScalarType): - __schema__ = "scalar DateInput" - - @staticmethod - def parse_value(formatted_date): - parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") - return parsed_datetime.date() - - @staticmethod - def parse_literal(ast, variable_values=None): # pylint: disable=unused-argument - if not isinstance(ast, StringValueNode): - raise ValueError() - - formatted_date = ast.value - parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") - return parsed_datetime.date() - - -class DefaultParserScalar(ScalarType): - __schema__ = "scalar DefaultParser" - - @staticmethod - def parse_value(value): - return type(value).__name__ - - -TEST_DATE = date(2006, 9, 13) -TEST_DATE_SERIALIZED = TEST_DATE.strftime("%Y-%m-%d") - - -class QueryType(ObjectType): - __schema__ = """ - type Query { - testSerialize: DateReadOnly! - testInput(value: DateInput!): Boolean! - testInputValueType(value: DefaultParser!): String! - } - """ - __requires__ = [ - DateReadOnlyScalar, - DateInputScalar, - DefaultParserScalar, - ] - __aliases__ = { - "testSerialize": "test_serialize", - "testInput": "test_input", - "testInputValueType": "test_input_value_type", - } - - @staticmethod - def resolve_test_serialize(*_): - return TEST_DATE - - @staticmethod - def resolve_test_input(*_, value): - assert value == TEST_DATE - return True - - @staticmethod - def resolve_test_input_value_type(*_, value): - return value - - -schema = make_executable_schema(QueryType) - - -def test_attempt_deserialize_str_literal_without_valid_date_raises_error(): - test_input = "invalid string" - result = graphql_sync(schema, '{ testInput(value: "%s") }' % test_input) - assert result.errors is not None - assert str(result.errors[0]).splitlines()[:1] == [ - "Expected value of type 'DateInput!', found \"invalid string\"; " - "time data 'invalid string' does not match format '%Y-%m-%d'" - ] - - -def test_attempt_deserialize_wrong_type_literal_raises_error(): - test_input = 123 - result = graphql_sync(schema, "{ testInput(value: %s) }" % test_input) - assert result.errors is not None - assert str(result.errors[0]).splitlines()[:1] == [ - "Expected value of type 'DateInput!', found 123; " - ] - - -def test_default_literal_parser_is_used_to_extract_value_str_from_ast_node(): - class ValueParserOnlyScalar(ScalarType): - __schema__ = "scalar DateInput" - - @staticmethod - def parse_value(formatted_date): - parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") - return parsed_datetime.date() + assert_schema_equals( + schema, + """ + scalar Date - class ValueParserOnlyQueryType(ObjectType): - __schema__ = """ type Query { - parse(value: DateInput!): String! + date: Date! } - """ - __requires__ = [ValueParserOnlyScalar] - - @staticmethod - def resolve_parse(*_, value): - return value - - schema = make_executable_schema(ValueParserOnlyQueryType) - result = graphql_sync(schema, """{ parse(value: "%s") }""" % TEST_DATE_SERIALIZED) - assert result.errors is None - assert result.data == {"parse": "2006-09-13"} + """, + ) + result = graphql_sync(schema, "{ date }") -parametrized_query = """ - query parseValueTest($value: DateInput!) { - testInput(value: $value) - } -""" + assert not result.errors + assert result.data == {"date": "1989-10-30"} -def test_variable_with_valid_date_string_is_deserialized_to_python_date(): - variables = {"value": TEST_DATE_SERIALIZED} - result = graphql_sync(schema, parametrized_query, variable_values=variables) - assert result.errors is None - assert result.data == {"testInput": True} +def test_scalar_field_returning_scalar_wrapped_type(assert_schema_equals): + class QueryType(GraphQLObject): + scalar_date: DateScalar + @GraphQLObject.resolver("scalar_date", graphql_type=DateScalar) + def resolve_date(*_) -> date: + return date(1989, 10, 30) -def test_attempt_deserialize_str_variable_without_valid_date_raises_error(): - variables = {"value": "invalid string"} - result = graphql_sync(schema, parametrized_query, variable_values=variables) - assert result.errors is not None - assert str(result.errors[0]).splitlines()[:1] == [ - "Variable '$value' got invalid value 'invalid string'; " - "Expected type 'DateInput'. " - "time data 'invalid string' does not match format '%Y-%m-%d'" - ] + schema = make_executable_schema(QueryType) + assert_schema_equals( + schema, + """ + scalar Date -def test_attempt_deserialize_wrong_type_variable_raises_error(): - variables = {"value": 123} - result = graphql_sync(schema, parametrized_query, variable_values=variables) - assert result.errors is not None - assert str(result.errors[0]).splitlines()[:1] == [ - "Variable '$value' got invalid value 123; Expected type 'DateInput'. " - "strptime() argument 1 must be str, not int" - ] + type Query { + scalarDate: Date! + } + """, + ) + result = graphql_sync(schema, "{ scalarDate }") -def test_literal_string_is_deserialized_by_default_parser(): - result = graphql_sync(schema, '{ testInputValueType(value: "test") }') - assert result.errors is None - assert result.data == {"testInputValueType": "str"} + assert not result.errors + assert result.data == {"scalarDate": "1989-10-30"} -def test_literal_int_is_deserialized_by_default_parser(): - result = graphql_sync(schema, "{ testInputValueType(value: 123) }") - assert result.errors is None - assert result.data == {"testInputValueType": "int"} +class SchemaDateScalar(GraphQLScalar[date]): + __schema__ = gql("scalar Date") + @classmethod + def serialize(cls, value): + if isinstance(value, cls): + return str(value.unwrap()) -def test_literal_float_is_deserialized_by_default_parser(): - result = graphql_sync(schema, "{ testInputValueType(value: 1.5) }") - assert result.errors is None - assert result.data == {"testInputValueType": "float"} + return str(value) -def test_literal_bool_true_is_deserialized_by_default_parser(): - result = graphql_sync(schema, "{ testInputValueType(value: true) }") - assert result.errors is None - assert result.data == {"testInputValueType": "bool"} +def test_schema_scalar_field_returning_scalar_instance(assert_schema_equals): + class QueryType(GraphQLObject): + date: SchemaDateScalar + @GraphQLObject.resolver("date") + def resolve_date(*_) -> SchemaDateScalar: + return SchemaDateScalar(date(1989, 10, 30)) -def test_literal_bool_false_is_deserialized_by_default_parser(): - result = graphql_sync(schema, "{ testInputValueType(value: false) }") - assert result.errors is None - assert result.data == {"testInputValueType": "bool"} + schema = make_executable_schema(QueryType) + assert_schema_equals( + schema, + """ + scalar Date -def test_literal_object_is_deserialized_by_default_parser(): - result = graphql_sync(schema, "{ testInputValueType(value: {}) }") - assert result.errors is None - assert result.data == {"testInputValueType": "dict"} + type Query { + date: Date! + } + """, + ) + result = graphql_sync(schema, "{ date }") -def test_literal_list_is_deserialized_by_default_parser(): - result = graphql_sync(schema, "{ testInputValueType(value: []) }") - assert result.errors is None - assert result.data == {"testInputValueType": "list"} + assert not result.errors + assert result.data == {"date": "1989-10-30"} diff --git a/tests_next/test_scalar_type_validation.py b/tests/test_scalar_type_validation.py similarity index 91% rename from tests_next/test_scalar_type_validation.py rename to tests/test_scalar_type_validation.py index d473cb7..edd193e 100644 --- a/tests_next/test_scalar_type_validation.py +++ b/tests/test_scalar_type_validation.py @@ -1,7 +1,7 @@ import pytest -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next import GraphQLScalar +from ariadne import gql +from ariadne_graphql_modules import GraphQLScalar def test_schema_scalar_type_validation_fails_for_invalid_type_schema(data_regression): diff --git a/tests_next/test_standard_enum.py b/tests/test_standard_enum.py similarity index 99% rename from tests_next/test_standard_enum.py rename to tests/test_standard_enum.py index 9f8eded..ca95562 100644 --- a/tests_next/test_standard_enum.py +++ b/tests/test_standard_enum.py @@ -3,7 +3,7 @@ import pytest from graphql import graphql_sync -from ariadne_graphql_modules.next import ( +from ariadne_graphql_modules import ( GraphQLObject, create_graphql_enum_model, graphql_enum, diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index c0067cf..bb43729 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -1,325 +1,754 @@ -import pytest -from ariadne import SchemaDirectiveVisitor -from graphql import GraphQLError, build_schema +from typing import List +from ariadne import gql +from graphql import subscribe, parse +import pytest from ariadne_graphql_modules import ( - DirectiveType, - InterfaceType, - ObjectType, - SubscriptionType, + GraphQLID, + GraphQLObject, + GraphQLSubscription, + GraphQLUnion, + make_executable_schema, ) -def test_subscription_type_raises_attribute_error_when_defined_without_schema( - data_regression, -): - with pytest.raises(AttributeError) as err: - # pylint: disable=unused-variable - class UsersSubscription(SubscriptionType): - pass +class Message(GraphQLObject): + id: GraphQLID + content: str + author: str - data_regression.check(str(err.value)) +class User(GraphQLObject): + id: GraphQLID + username: str -def test_subscription_type_raises_error_when_defined_with_invalid_schema_type( - data_regression, -): - with pytest.raises(TypeError) as err: - # pylint: disable=unused-variable - class UsersSubscription(SubscriptionType): - __schema__ = True - data_regression.check(str(err.value)) +class Notification(GraphQLUnion): + __types__ = [Message, User] -def test_subscription_type_raises_error_when_defined_with_invalid_schema_str( - data_regression, -): - with pytest.raises(GraphQLError) as err: - # pylint: disable=unused-variable - class UsersSubscription(SubscriptionType): - __schema__ = "typo Subscription" +@pytest.mark.asyncio +async def test_basic_subscription_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + message_added: Message - data_regression.check(str(err.value)) + @GraphQLSubscription.source("message_added") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + @GraphQLSubscription.resolver("message_added", graphql_type=Message) + async def resolve_message_added(message, info): + return message -def test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UsersSubscription(SubscriptionType): - __schema__ = "scalar Subscription" + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" - data_regression.check(str(err.value)) + schema = make_executable_schema(QueryType, SubscriptionType) + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } -def test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UsersSubscription(SubscriptionType): - __schema__ = "type Other" + type Subscription { + messageAdded: Message! + } - data_regression.check(str(err.value)) + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + query = parse("subscription { messageAdded {id content author} }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message", "author": "Anon"} + } + + +@pytest.mark.asyncio +async def test_subscription_with_arguments_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source( + "message_added", + args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, + ) + async def message_added_generator(obj, info, channel: GraphQLID): + while True: + yield { + "id": "some_id", + "content": f"message_{channel}", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "message_added", + graphql_type=Message, + ) + async def resolve_message_added(message, *_, channel: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } -def test_subscription_type_raises_error_when_defined_without_fields(data_regression): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class UsersSubscription(SubscriptionType): - __schema__ = "type Subscription" + type Subscription { + messageAdded( + \"\"\"Lorem ipsum.\"\"\" + channel: ID! + ): Message! + } - data_regression.check(str(err.value)) + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + query = parse('subscription { messageAdded(channel: "123") {id content author} }') + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message_123", "author": "Anon"} + } + + +@pytest.mark.asyncio +async def test_multiple_supscriptions_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + message_added: Message + user_joined: User + + @GraphQLSubscription.source( + "message_added", + args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, + ) + async def message_added_generator(obj, info, channel: GraphQLID): + while True: + yield { + "id": "some_id", + "content": f"message_{channel}", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "message_added", + graphql_type=Message, + ) + async def resolve_message_added(message, *_, channel: GraphQLID): + return message + + @GraphQLSubscription.source( + "user_joined", + ) + async def user_joined_generator(obj, info): + while True: + yield { + "id": "some_id", + "username": "username", + } + + @GraphQLSubscription.resolver( + "user_joined", + graphql_type=Message, + ) + async def resolve_user_joined(user, *_): + return user + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } -def test_subscription_type_extracts_graphql_name(): - class UsersSubscription(SubscriptionType): - __schema__ = """ type Subscription { - thread: ID! + messageAdded( + \"\"\"Lorem ipsum.\"\"\" + channel: ID! + ): Message! + userJoined: User! + } + + type Message { + id: ID! + content: String! + author: String! } + + type User { + id: ID! + username: String! + } + """, + ) + + query = parse("subscription { userJoined {id username} }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == {"userJoined": {"id": "some_id", "username": "username"}} + + +@pytest.mark.asyncio +async def test_subscription_with_complex_data_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + messages_in_channel: List[Message] + + @GraphQLSubscription.source( + "messages_in_channel", + args={"channel_id": GraphQLObject.argument(description="Lorem ipsum.")}, + ) + async def message_added_generator(obj, info, channel_id: GraphQLID): + while True: + yield [ + { + "id": "some_id", + "content": f"message_{channel_id}", + "author": "Anon", + } + ] + + @GraphQLSubscription.resolver( + "messages_in_channel", + graphql_type=Message, + ) + async def resolve_message_added(message, *_, channel_id: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, """ + type Query { + searchSth: String! + } - assert UsersSubscription.graphql_name == "Subscription" + type Subscription { + messagesInChannel( + \"\"\"Lorem ipsum.\"\"\" + channelId: ID! + ): [Message!]! + } + type Message { + id: ID! + content: String! + author: String! + } + """, + ) -def test_subscription_type_raises_error_when_defined_without_return_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ChatSubscription(SubscriptionType): - __schema__ = """ - type Subscription { - chat: Chat - Chats: [Chat!] - } - """ + query = parse( + 'subscription { messagesInChannel(channelId: "123") {id content author} }' + ) + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messagesInChannel": [ + {"id": "some_id", "content": "message_123", "author": "Anon"} + ] + } + + +@pytest.mark.asyncio +async def test_subscription_with_union_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + notification_received: Notification + + @GraphQLSubscription.source( + "notification_received", + ) + async def message_added_generator(obj, info): + while True: + yield Message(id=1, content="content", author="anon") + + @GraphQLSubscription.resolver( + "notification_received", + ) + async def resolve_message_added(message, *_): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } - data_regression.check(str(err.value)) + type Subscription { + notificationReceived: Notification! + } + union Notification = Message | User -def test_subscription_type_verifies_field_dependency(): - # pylint: disable=unused-variable - class ChatType(ObjectType): - __schema__ = """ - type Chat { - id: ID! + type Message { + id: ID! + content: String! + author: String! } - """ - class ChatSubscription(SubscriptionType): - __schema__ = """ - type Subscription { - chat: Chat - Chats: [Chat!] + type User { + id: ID! + username: String! } - """ - __requires__ = [ChatType] + """, + ) + + query = parse("subscription { notificationReceived { ... on Message { id } } }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + # Validate the result + assert not result.errors + assert result.data == {"notificationReceived": {"id": "1"}} -def test_subscription_type_raises_error_when_defined_without_argument_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ChatSubscription(SubscriptionType): - __schema__ = """ + +@pytest.mark.asyncio +async def test_basic_subscription_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ type Subscription { - chat(input: ChannelInput): [String!]! + messageAdded: Message! } """ + ) + + @GraphQLSubscription.source("messageAdded") + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.resolver("messageAdded", graphql_type=Message) + async def resolve_message_added(message, info): + return message - data_regression.check(str(err.value)) + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + schema = make_executable_schema(QueryType, SubscriptionType, Message) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } -def test_subscription_type_can_be_extended_with_new_fields(): - # pylint: disable=unused-variable - class ChatSubscription(SubscriptionType): - __schema__ = """ type Subscription { - chat: ID! + messageAdded: Message! } - """ - class ExtendChatSubscription(SubscriptionType): - __schema__ = """ - extend type Subscription { - thread: ID! + type Message { + id: ID! + content: String! + author: String! } - """ - __requires__ = [ChatSubscription] + """, + ) + query = parse("subscription { messageAdded {id content author} }") + sub = await subscribe(schema, query) -def test_subscription_type_can_be_extended_with_directive(): - # pylint: disable=unused-variable - class ExampleDirective(DirectiveType): - __schema__ = "directive @example on OBJECT" - __visitor__ = SchemaDirectiveVisitor + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - class ChatSubscription(SubscriptionType): - __schema__ = """ - type Subscription { - chat: ID! - } - """ + # Fetch the first result + result = await sub.__anext__() - class ExtendChatSubscription(SubscriptionType): - __schema__ = "extend type Subscription @example" - __requires__ = [ChatSubscription, ExampleDirective] + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message", "author": "Anon"} + } -def test_subscription_type_can_be_extended_with_interface(): - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Interface { - threads: ID! - } +@pytest.mark.asyncio +async def test_subscription_with_arguments_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ + type Subscription { + messageAdded(channel: ID!): Message! + } + """ + ) + + @GraphQLSubscription.source( + "messageAdded", + ) + async def message_added_generator(obj, info, channel: GraphQLID): + while True: + yield { + "id": "some_id", + "content": f"message_{channel}", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "messageAdded", + graphql_type=Message, + ) + async def resolve_message_added(message, *_, channel: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Message) + + assert_schema_equals( + schema, """ + type Query { + searchSth: String! + } - class ChatSubscription(SubscriptionType): - __schema__ = """ type Subscription { - chat: ID! + messageAdded(channel: ID!): Message! } - """ - class ExtendChatSubscription(SubscriptionType): - __schema__ = """ - extend type Subscription implements Interface { - threads: ID! + type Message { + id: ID! + content: String! + author: String! } - """ - __requires__ = [ChatSubscription, ExampleInterface] - - -def test_subscription_type_raises_error_when_defined_without_extended_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExtendChatSubscription(SubscriptionType): - __schema__ = """ - extend type Subscription { - thread: ID! - } - """ + """, + ) - data_regression.check(str(err.value)) + query = parse('subscription { messageAdded(channel: "123") {id content author} }') + sub = await subscribe(schema, query) + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") -def test_subscription_type_raises_error_when_extended_dependency_is_wrong_type( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleInterface(InterfaceType): - __schema__ = """ - interface Subscription { - id: ID! - } - """ + # Fetch the first result + result = await sub.__anext__() - class ExtendChatSubscription(SubscriptionType): - __schema__ = """ - extend type Subscription { - thread: ID! - } - """ - __requires__ = [ExampleInterface] - - data_regression.check(str(err.value)) + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message_123", "author": "Anon"} + } -def test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ChatSubscription(SubscriptionType): - __schema__ = """ +@pytest.mark.asyncio +async def test_multiple_supscriptions_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ type Subscription { - chat: ID! + messageAdded: Message! + userJoined: User! } """ - __aliases__ = { - "userAlerts": "user_alerts", - } + ) + + @GraphQLSubscription.source( + "messageAdded", + ) + async def message_added_generator(obj, info): + while True: + yield { + "id": "some_id", + "content": "message", + "author": "Anon", + } + + @GraphQLSubscription.resolver( + "messageAdded", + ) + async def resolve_message_added(message, *_): + return message + + @GraphQLSubscription.source( + "userJoined", + ) + async def user_joined_generator(obj, info): + while True: + yield { + "id": "some_id", + "username": "username", + } + + @GraphQLSubscription.resolver( + "userJoined", + ) + async def resolve_user_joined(user, *_): + return user + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Message, User) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } - data_regression.check(str(err.value)) + type Subscription { + messageAdded: Message! + userJoined: User! + } + type Message { + id: ID! + content: String! + author: String! + } -def test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ChatSubscription(SubscriptionType): - __schema__ = """ - type Subscription { - chat: ID! - } - """ + type User { + id: ID! + username: String! + } + """, + ) + + query = parse("subscription { userJoined {id username} }") + sub = await subscribe(schema, query) - @staticmethod - def resolve_group(*_): - return None + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - data_regression.check(str(err.value)) + # Fetch the first result + result = await sub.__anext__() + # Validate the result + assert not result.errors + assert result.data == {"userJoined": {"id": "some_id", "username": "username"}} -def test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ChatSubscription(SubscriptionType): - __schema__ = """ + +@pytest.mark.asyncio +async def test_subscription_with_complex_data_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ type Subscription { - chat: ID! + messagesInChannel(channelId: ID!): [Message!]! } """ + ) + + @GraphQLSubscription.source( + "messagesInChannel", + ) + async def message_added_generator(obj, info, channelId: GraphQLID): + while True: + yield [ + { + "id": "some_id", + "content": f"message_{channelId}", + "author": "Anon", + } + ] + + @GraphQLSubscription.resolver( + "messagesInChannel", + ) + async def resolve_message_added(message, *_, channelId: GraphQLID): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Message) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messagesInChannel(channelId: ID!): [Message!]! + } - @staticmethod - def subscribe_group(*_): - return None + type Message { + id: ID! + content: String! + author: String! + } + """, + ) - data_regression.check(str(err.value)) + query = parse( + 'subscription { messagesInChannel(channelId: "123") {id content author} }' + ) + sub = await subscribe(schema, query) + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") -def test_subscription_type_binds_resolver_and_subscriber_to_schema(): - schema = build_schema( - """ - type Query { - hello: String - } + # Fetch the first result + result = await sub.__anext__() + # Validate the result + assert not result.errors + assert result.data == { + "messagesInChannel": [ + {"id": "some_id", "content": "message_123", "author": "Anon"} + ] + } + + +@pytest.mark.asyncio +async def test_subscription_with_union_with_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + """ type Subscription { - chat: ID! + notificationReceived: Notification! } + """ + ) + + @GraphQLSubscription.source( + "notificationReceived", + ) + async def message_added_generator(obj, info): + while True: + yield Message(id=1, content="content", author="anon") + + @GraphQLSubscription.resolver( + "notificationReceived", + ) + async def resolve_message_added(message, *_): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Notification) + + assert_schema_equals( + schema, """ - ) + type Query { + searchSth: String! + } - class ChatSubscription(SubscriptionType): - __schema__ = """ type Subscription { - chat: ID! + notificationReceived: Notification! } - """ - @staticmethod - def resolve_chat(*_): - return None + union Notification = Message | User + + type Message { + id: ID! + content: String! + author: String! + } + + type User { + id: ID! + username: String! + } + """, + ) + + query = parse("subscription { notificationReceived { ... on Message { id } } }") + sub = await subscribe(schema, query) - @staticmethod - def subscribe_chat(*_): - return None + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - ChatSubscription.__bind_to_schema__(schema) + # Fetch the first result + result = await sub.__anext__() - field = schema.type_map.get("Subscription").fields["chat"] - assert field.resolve is ChatSubscription.resolve_chat - assert field.subscribe is ChatSubscription.subscribe_chat + # Validate the result + assert not result.errors + assert result.data == {"notificationReceived": {"id": "1"}} diff --git a/tests_next/test_subscription_type_validation.py b/tests/test_subscription_type_validation.py similarity index 99% rename from tests_next/test_subscription_type_validation.py rename to tests/test_subscription_type_validation.py index 4b59cc7..2f5d1df 100644 --- a/tests_next/test_subscription_type_validation.py +++ b/tests/test_subscription_type_validation.py @@ -1,6 +1,6 @@ from ariadne import gql import pytest -from ariadne_graphql_modules.next import ( +from ariadne_graphql_modules import ( GraphQLID, GraphQLObject, GraphQLSubscription, diff --git a/tests_next/test_typing.py b/tests/test_typing.py similarity index 95% rename from tests_next/test_typing.py rename to tests/test_typing.py index 9e4e3fb..3723470 100644 --- a/tests_next/test_typing.py +++ b/tests/test_typing.py @@ -5,8 +5,8 @@ from graphql import ListTypeNode, NameNode, NamedTypeNode, NonNullTypeNode import pytest -from ariadne_graphql_modules.next import GraphQLObject, deferred, graphql_enum -from ariadne_graphql_modules.next.typing import get_graphql_type, get_type_node +from ariadne_graphql_modules import GraphQLObject, deferred, graphql_enum +from ariadne_graphql_modules.typing import get_graphql_type, get_type_node if TYPE_CHECKING: from .types import ForwardEnum, ForwardScalar @@ -124,7 +124,7 @@ def test_get_non_null_graphql_list_type_node_from_python_builtin_type(metadata): def test_get_graphql_type_node_from_annotated_type(metadata): class MockType(GraphQLObject): - field: Annotated["ForwardScalar", deferred("tests_next.types")] + field: Annotated["ForwardScalar", deferred("tests.types")] assert_non_null_type( get_type_node(metadata, MockType.__annotations__["field"]), "Forward" @@ -142,7 +142,7 @@ class MockType(GraphQLObject): def test_get_graphql_type_node_from_nullable_annotated_type(metadata): class MockType(GraphQLObject): - field: Optional[Annotated["ForwardScalar", deferred("tests_next.types")]] + field: Optional[Annotated["ForwardScalar", deferred("tests.types")]] assert_named_type( get_type_node(metadata, MockType.__annotations__["field"]), "Forward" @@ -151,7 +151,7 @@ class MockType(GraphQLObject): def test_get_graphql_type_node_from_annotated_enum(metadata): class MockType(GraphQLObject): - field: Annotated["ForwardEnum", deferred("tests_next.types")] + field: Annotated["ForwardEnum", deferred("tests.types")] assert_non_null_type( get_type_node(metadata, MockType.__annotations__["field"]), "ForwardEnum" diff --git a/tests/test_union_type.py b/tests/test_union_type.py index 61c8a3a..da926c5 100644 --- a/tests/test_union_type.py +++ b/tests/test_union_type.py @@ -1,251 +1,223 @@ -from dataclasses import dataclass +from typing import List, Union -import pytest -from ariadne import SchemaDirectiveVisitor -from graphql import GraphQLError, graphql_sync +from graphql import graphql_sync from ariadne_graphql_modules import ( - DirectiveType, - ObjectType, - UnionType, + GraphQLID, + GraphQLObject, + GraphQLUnion, make_executable_schema, ) -def test_union_type_raises_attribute_error_when_defined_without_schema(data_regression): - with pytest.raises(AttributeError) as err: - # pylint: disable=unused-variable - class ExampleUnion(UnionType): - pass +class UserType(GraphQLObject): + id: GraphQLID + username: str - data_regression.check(str(err.value)) +class CommentType(GraphQLObject): + id: GraphQLID + content: str -def test_union_type_raises_error_when_defined_with_invalid_schema_type(data_regression): - with pytest.raises(TypeError) as err: - # pylint: disable=unused-variable - class ExampleUnion(UnionType): - __schema__ = True - data_regression.check(str(err.value)) +def test_union_field_returning_object_instance(assert_schema_equals): + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(id=1, username="Bob"), + CommentType(id=2, content="Hello World!"), + ] -def test_union_type_raises_error_when_defined_with_invalid_schema_str(data_regression): - with pytest.raises(GraphQLError) as err: - # pylint: disable=unused-variable - class ExampleUnion(UnionType): - __schema__ = "unien Example = A | B" - - data_regression.check(str(err.value)) - - -def test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleUnion(UnionType): - __schema__ = "scalar DateTime" - - data_regression.check(str(err.value)) - - -def test_union_type_raises_error_when_defined_with_multiple_types_schema( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleUnion(UnionType): - __schema__ = """ - union A = C | D - - union B = C | D - """ - - data_regression.check(str(err.value)) - - -@dataclass -class User: - id: int - name: str + schema = make_executable_schema(QueryType) + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } -@dataclass -class Comment: - id: int - message: str + union Result = User | Comment + type User { + id: ID! + username: String! + } -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - name: String! - } - """ + type Comment { + id: ID! + content: String! + } + """, + ) + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } + } + } + """, + ) -class CommentType(ObjectType): - __schema__ = """ - type Comment { - id: ID! - message: String! + assert not result.errors + assert result.data == { + "search": [ + {"id": "1", "username": "Bob"}, + {"id": "2", "content": "Hello World!"}, + ] } - """ - -class ResultUnion(UnionType): - __schema__ = "union Result = Comment | User" - __requires__ = [CommentType, UserType] - @staticmethod - def resolve_type(instance, *_): - if isinstance(instance, Comment): - return "Comment" +def test_union_field_returning_empty_list(): + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] - if isinstance(instance, User): - return "User" + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [] - return None + schema = make_executable_schema(QueryType) + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } + } + } + """, + ) + assert not result.errors + assert result.data == {"search": []} -class QueryType(ObjectType): - __schema__ = """ - type Query { - results: [Result!]! - } - """ - __requires__ = [ResultUnion] - - @staticmethod - def resolve_results(*_): - return [ - User(id=1, name="Alice"), - Comment(id=1, message="Hello world!"), - ] +def test_union_field_with_invalid_type_access(assert_schema_equals): + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] -schema = make_executable_schema(QueryType, UserType, CommentType) + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(id=1, username="Bob"), + "InvalidType", + ] + schema = make_executable_schema(QueryType) -def test_union_type_extracts_graphql_name(): - class ExampleUnion(UnionType): - __schema__ = "union Example = User | Comment" - __requires__ = [UserType, CommentType] + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } + } + } + """, + ) + assert result.errors + assert "InvalidType" in str(result.errors) - assert ExampleUnion.graphql_name == "Example" +def test_serialization_error_handling(assert_schema_equals): + class InvalidType: + def __init__(self, value): + self.value = value -def test_union_type_raises_error_when_defined_without_member_type_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleUnion(UnionType): - __schema__ = "union Example = User | Comment" - __requires__ = [UserType] + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] - data_regression.check(str(err.value)) + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType, InvalidType]]: + return [InvalidType("This should cause an error")] + schema = make_executable_schema(QueryType) -def test_interface_type_binds_type_resolver(): - query = """ - query { - results { - ... on User { - __typename - id - name - } - ... on Comment { - __typename - id - message + result = graphql_sync( + schema, + """ + { + search { + ... on User { + id + username + } } } - } - """ - - result = graphql_sync(schema, query) - assert result.data == { - "results": [ - { - "__typename": "User", - "id": "1", - "name": "Alice", - }, - { - "__typename": "Comment", - "id": "1", - "message": "Hello world!", - }, - ], - } + """, + ) + assert result.errors -def test_union_type_can_be_extended_with_new_types(): - # pylint: disable=unused-variable - class ExampleUnion(UnionType): - __schema__ = "union Result = User | Comment" - __requires__ = [UserType, CommentType] - - class ThreadType(ObjectType): +def test_union_with_schema_definition(assert_schema_equals): + class SearchResultUnion(GraphQLUnion): __schema__ = """ - type Thread { - id: ID! - title: String! - } + union SearchResult = User | Comment """ + __types__ = [UserType, CommentType] - class ExtendExampleUnion(UnionType): - __schema__ = "union Result = Thread" - __requires__ = [ExampleUnion, ThreadType] - + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[SearchResultUnion]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(id="1", username="Alice"), + CommentType(id="2", content="Test post"), + ] -def test_union_type_can_be_extended_with_directive(): - # pylint: disable=unused-variable - class ExampleDirective(DirectiveType): - __schema__ = "directive @example on UNION" - __visitor__ = SchemaDirectiveVisitor + schema = make_executable_schema(QueryType, SearchResultUnion) - class ExampleUnion(UnionType): - __schema__ = "union Result = User | Comment" - __requires__ = [UserType, CommentType] - - class ExtendExampleUnion(UnionType): - __schema__ = """ - extend union Result @example + result = graphql_sync( + schema, """ - __requires__ = [ExampleUnion, ExampleDirective] - - -def test_union_type_raises_error_when_defined_without_extended_dependency( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExtendExampleUnion(UnionType): - __schema__ = "extend union Result = User" - __requires__ = [UserType] - - data_regression.check(str(err.value)) - - -def test_union_type_raises_error_when_extended_dependency_is_wrong_type( - data_regression, -): - with pytest.raises(ValueError) as err: - # pylint: disable=unused-variable - class ExampleType(ObjectType): - __schema__ = """ - type Example { - id: ID! + { + search { + ... on User { + id + username + } + ... on Comment { + id + content + } } - """ - - class ExtendExampleUnion(UnionType): - __schema__ = "extend union Example = User" - __requires__ = [ExampleType, UserType] - - data_regression.check(str(err.value)) + } + """, + ) + assert not result.errors + assert result.data == { + "search": [ + {"id": "1", "username": "Alice"}, + {"id": "2", "content": "Test post"}, + ] + } diff --git a/tests_next/test_union_type_validation.py b/tests/test_union_type_validation.py similarity index 92% rename from tests_next/test_union_type_validation.py rename to tests/test_union_type_validation.py index f9930d4..fac5b1f 100644 --- a/tests_next/test_union_type_validation.py +++ b/tests/test_union_type_validation.py @@ -1,9 +1,9 @@ -from ariadne_graphql_modules.next import ( +from ariadne_graphql_modules import ( GraphQLID, GraphQLObject, GraphQLUnion, ) -from ariadne_graphql_modules.next.graphql_union.validators import ( +from ariadne_graphql_modules.union_type.validators import ( validate_union_type_with_schema, ) import pytest diff --git a/tests_next/test_validators.py b/tests/test_validators.py similarity index 92% rename from tests_next/test_validators.py rename to tests/test_validators.py index 45f4948..bdb50e2 100644 --- a/tests_next/test_validators.py +++ b/tests/test_validators.py @@ -1,8 +1,7 @@ import pytest from graphql import parse -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next.validators import validate_description, validate_name +from ariadne_graphql_modules.validators import validate_description, validate_name def test_description_validator_passes_type_without_description(): diff --git a/tests_next/test_value_node.py b/tests/test_value_node.py similarity index 96% rename from tests_next/test_value_node.py rename to tests/test_value_node.py index 48c754b..e557438 100644 --- a/tests_next/test_value_node.py +++ b/tests/test_value_node.py @@ -14,7 +14,7 @@ print_ast, ) -from ariadne_graphql_modules.next import get_value_from_node, get_value_node +from ariadne_graphql_modules import get_value_from_node, get_value_node def test_get_false_value(): @@ -121,7 +121,7 @@ class CustomType: get_value_node(CustomType()) error_message = str(exc_info.value) - assert error_message.startswith("Python value '' can't be represented as a GraphQL value node.") diff --git a/tests_next/types.py b/tests/types.py similarity index 73% rename from tests_next/types.py rename to tests/types.py index 02761c7..96233ee 100644 --- a/tests_next/types.py +++ b/tests/types.py @@ -1,6 +1,6 @@ from enum import Enum -from ariadne_graphql_modules.next import GraphQLScalar +from ariadne_graphql_modules import GraphQLScalar class ForwardScalar(GraphQLScalar): diff --git a/tests_next/conftest.py b/tests_next/conftest.py deleted file mode 100644 index 46da232..0000000 --- a/tests_next/conftest.py +++ /dev/null @@ -1,40 +0,0 @@ -from pathlib import Path -from textwrap import dedent - -import pytest -from graphql import TypeDefinitionNode, GraphQLSchema, print_ast, print_schema - -from ariadne_graphql_modules.next import GraphQLMetadata - - -@pytest.fixture -def assert_schema_equals(): - def schema_equals_assertion(schema: GraphQLSchema, target: str): - schema_str = print_schema(schema) - assert schema_str == dedent(target).strip() - - return schema_equals_assertion - - -@pytest.fixture -def assert_ast_equals(): - def ast_equals_assertion(ast: TypeDefinitionNode, target: str): - ast_str = print_ast(ast) - assert ast_str == dedent(target).strip() - - return ast_equals_assertion - - -@pytest.fixture -def metadata(): - return GraphQLMetadata() - - -@pytest.fixture(scope="session") -def datadir() -> Path: - return Path(__file__).parent / "snapshots" - - -@pytest.fixture(scope="session") -def original_datadir() -> Path: - return Path(__file__).parent / "snapshots" diff --git a/tests_next/snapshots/test_interface_with_different_types.obtained.yml b/tests_next/snapshots/test_interface_with_different_types.obtained.yml deleted file mode 100644 index f10dac9..0000000 --- a/tests_next/snapshots/test_interface_with_different_types.obtained.yml +++ /dev/null @@ -1,5 +0,0 @@ -'Query root type must be provided. - - - Interface field UserInterface.score expects type String! but User.score is type - Int!.' diff --git a/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml b/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml deleted file mode 100644 index d7bf90b..0000000 --- a/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'"No data is set for ''''."' diff --git a/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml b/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml deleted file mode 100644 index d7bf90b..0000000 --- a/tests_next/snapshots/test_metadata_raises_key_error_for_unset_data.yml +++ /dev/null @@ -1 +0,0 @@ -'"No data is set for ''''."' diff --git a/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml b/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml deleted file mode 100644 index 4eccfe9..0000000 --- a/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Types 'SecondRoot' and '.FirstRoot'>' - both define GraphQL type with name 'Query'. -... diff --git a/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml b/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml deleted file mode 100644 index 4eccfe9..0000000 --- a/tests_next/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml +++ /dev/null @@ -1,3 +0,0 @@ -Types 'SecondRoot' and '.FirstRoot'>' - both define GraphQL type with name 'Query'. -... diff --git a/tests_next/test_enum_type.py b/tests_next/test_enum_type.py deleted file mode 100644 index e4bf3c0..0000000 --- a/tests_next/test_enum_type.py +++ /dev/null @@ -1,522 +0,0 @@ -from enum import Enum - -from graphql import graphql_sync - -from ariadne_graphql_modules.next import ( - GraphQLEnum, - GraphQLObject, - make_executable_schema, -) - - -class UserLevelEnum(Enum): - GUEST = 0 - MEMBER = 1 - ADMIN = 2 - - -def test_enum_field_returning_enum_value(assert_schema_equals): - class UserLevel(GraphQLEnum): - __members__ = UserLevelEnum - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> UserLevelEnum: - return UserLevelEnum.MEMBER - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "MEMBER"} - - -def test_enum_field_returning_dict_value(assert_schema_equals): - class UserLevel(GraphQLEnum): - __members__ = { - "GUEST": 0, - "MEMBER": 1, - "ADMIN": 2, - } - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> dict: - return 0 - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "GUEST"} - - -def test_enum_field_returning_str_value(assert_schema_equals): - class UserLevel(GraphQLEnum): - __members__ = [ - "GUEST", - "MEMBER", - "ADMIN", - ] - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> str: - return "ADMIN" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "ADMIN"} - - -def test_enum_type_with_custom_name(assert_schema_equals): - class UserLevel(GraphQLEnum): - __graphql_name__ = "UserLevelEnum" - __members__ = UserLevelEnum - - class QueryType(GraphQLObject): - level: UserLevel - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevelEnum! - } - - enum UserLevelEnum { - GUEST - MEMBER - ADMIN - } - """, - ) - - -def test_enum_type_with_description(assert_schema_equals): - class UserLevel(GraphQLEnum): - __description__ = "Hello world." - __members__ = UserLevelEnum - - class QueryType(GraphQLObject): - level: UserLevel - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - \"\"\"Hello world.\"\"\" - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - -def test_enum_type_with_member_description(assert_schema_equals): - class UserLevel(GraphQLEnum): - __members__ = UserLevelEnum - __members_descriptions__ = {"MEMBER": "Hello world."} - - class QueryType(GraphQLObject): - level: UserLevel - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - - \"\"\"Hello world.\"\"\" - MEMBER - ADMIN - } - """, - ) - - -def test_schema_enum_field_returning_enum_value(assert_schema_equals): - class UserLevel(GraphQLEnum): - __schema__ = """ - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """ - __members__ = UserLevelEnum - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> UserLevelEnum: - return UserLevelEnum.MEMBER - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "MEMBER"} - - -def test_schema_enum_field_returning_dict_value(assert_schema_equals): - class UserLevel(GraphQLEnum): - __schema__ = """ - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """ - __members__ = { - "GUEST": 0, - "MEMBER": 1, - "ADMIN": 2, - } - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> int: - return 2 - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "ADMIN"} - - -def test_schema_enum_field_returning_str_value(assert_schema_equals): - class UserLevel(GraphQLEnum): - __schema__ = """ - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """ - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> str: - return "GUEST" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "GUEST"} - - -def test_schema_enum_with_description_attr(assert_schema_equals): - class UserLevel(GraphQLEnum): - __schema__ = """ - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """ - __members__ = { - "GUEST": 0, - "MEMBER": 1, - "ADMIN": 2, - } - __description__ = "Hello world." - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> int: - return 2 - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - \"\"\"Hello world.\"\"\" - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "ADMIN"} - - -def test_schema_enum_with_schema_description(assert_schema_equals): - class UserLevel(GraphQLEnum): - __schema__ = """ - \"\"\"Hello world.\"\"\" - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """ - __members__ = { - "GUEST": 0, - "MEMBER": 1, - "ADMIN": 2, - } - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> int: - return 2 - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - \"\"\"Hello world.\"\"\" - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "ADMIN"} - - -def test_schema_enum_with_member_description(assert_schema_equals): - class UserLevel(GraphQLEnum): - __schema__ = """ - enum UserLevel { - GUEST - MEMBER - ADMIN - } - """ - __members__ = { - "GUEST": 0, - "MEMBER": 1, - "ADMIN": 2, - } - __members_descriptions__ = {"MEMBER": "Hello world."} - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> int: - return 2 - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - - \"\"\"Hello world.\"\"\" - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "ADMIN"} - - -def test_schema_enum_with_member_schema_description(assert_schema_equals): - class UserLevel(GraphQLEnum): - __schema__ = """ - enum UserLevel { - GUEST - \"\"\"Hello world.\"\"\" - MEMBER - ADMIN - } - """ - __members__ = { - "GUEST": 0, - "MEMBER": 1, - "ADMIN": 2, - } - - class QueryType(GraphQLObject): - level: UserLevel - - @GraphQLObject.resolver("level") - def resolve_level(*_) -> int: - return 2 - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - level: UserLevel! - } - - enum UserLevel { - GUEST - - \"\"\"Hello world.\"\"\" - MEMBER - ADMIN - } - """, - ) - - result = graphql_sync(schema, "{ level }") - - assert not result.errors - assert result.data == {"level": "ADMIN"} diff --git a/tests_next/test_input_type.py b/tests_next/test_input_type.py deleted file mode 100644 index 977dfcb..0000000 --- a/tests_next/test_input_type.py +++ /dev/null @@ -1,628 +0,0 @@ -from typing import Optional - -import pytest -from graphql import graphql_sync - -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next import ( - GraphQLInput, - GraphQLObject, - make_executable_schema, -) - - -def test_input_type_instance_with_all_attrs_values(): - class SearchInput(GraphQLInput): - query: str - age: int - - obj = SearchInput(query="search", age=20) - assert obj.query == "search" - assert obj.age == 20 - - -def test_input_type_instance_with_omitted_attrs_being_none(): - class SearchInput(GraphQLInput): - query: str - age: int - - obj = SearchInput(age=20) - assert obj.query is None - assert obj.age == 20 - - -def test_input_type_instance_with_default_attrs_values(): - class SearchInput(GraphQLInput): - query: str = "default" - age: int = 42 - - obj = SearchInput(age=20) - assert obj.query == "default" - assert obj.age == 20 - - -def test_input_type_instance_with_all_fields_values(): - class SearchInput(GraphQLInput): - query: str = GraphQLInput.field() - age: int = GraphQLInput.field() - - obj = SearchInput(query="search", age=20) - assert obj.query == "search" - assert obj.age == 20 - - -def test_input_type_instance_with_all_fields_default_values(): - class SearchInput(GraphQLInput): - query: str = GraphQLInput.field(default_value="default") - age: int = GraphQLInput.field(default_value=42) - - obj = SearchInput(age=20) - assert obj.query == "default" - assert obj.age == 20 - - -def test_input_type_instance_with_invalid_attrs_raising_error(data_regression): - class SearchInput(GraphQLInput): - query: str - age: int - - with pytest.raises(TypeError) as exc_info: - SearchInput(age=20, invalid="Ok") - - data_regression.check(str(exc_info.value)) - - -def test_schema_input_type_instance_with_all_attrs_values(): - class SearchInput(GraphQLInput): - __schema__ = gql( - """ - input Search { - query: String - age: Int - } - """ - ) - - query: str - age: int - - obj = SearchInput(query="search", age=20) - assert obj.query == "search" - assert obj.age == 20 - - -def test_schema_input_type_instance_with_omitted_attrs_being_none(): - class SearchInput(GraphQLInput): - __schema__ = gql( - """ - input Search { - query: String - age: Int - } - """ - ) - - query: str - age: int - - obj = SearchInput(age=20) - assert obj.query is None - assert obj.age == 20 - - -def test_schema_input_type_instance_with_default_attrs_values(): - class SearchInput(GraphQLInput): - __schema__ = gql( - """ - input Search { - query: String = "default" - age: Int = 42 - } - """ - ) - - query: str - age: int - - obj = SearchInput(age=20) - assert obj.query == "default" - assert obj.age == 20 - - -def test_schema_input_type_instance_with_all_attrs_default_values(): - class SearchInput(GraphQLInput): - __schema__ = gql( - """ - input Search { - query: String = "default" - age: Int = 42 - } - """ - ) - - query: str - age: int - - obj = SearchInput() - assert obj.query == "default" - assert obj.age == 42 - - -def test_schema_input_type_instance_with_default_attrs_python_values(): - class SearchInput(GraphQLInput): - __schema__ = gql( - """ - input Search { - query: String - age: Int - } - """ - ) - - query: str = "default" - age: int = 42 - - obj = SearchInput(age=20) - assert obj.query == "default" - assert obj.age == 20 - - -def test_schema_input_type_instance_with_invalid_attrs_raising_error(data_regression): - class SearchInput(GraphQLInput): - __schema__ = gql( - """ - input Search { - query: String - age: Int - } - """ - ) - - query: str - age: int - - with pytest.raises(TypeError) as exc_info: - SearchInput(age=20, invalid="Ok") - - data_regression.check(str(exc_info.value)) - - -def test_input_type_arg(assert_schema_equals): - class SearchInput(GraphQLInput): - query: Optional[str] - age: Optional[int] - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String - age: Int - } - """, - ) - - result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') - - assert not result.errors - assert result.data == {"search": "['Hello', None]"} - - -def test_schema_input_type_arg(assert_schema_equals): - class SearchInput(GraphQLInput): - __schema__ = """ - input SearchInput { - query: String - age: Int - } - """ - - query: Optional[str] - age: Optional[int] - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String - age: Int - } - """, - ) - - result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') - - assert not result.errors - assert result.data == {"search": "['Hello', None]"} - - -def test_input_type_automatic_out_name_arg(assert_schema_equals): - class SearchInput(GraphQLInput): - query: Optional[str] - min_age: Optional[int] - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.min_age])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String - minAge: Int - } - """, - ) - - result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") - - assert not result.errors - assert result.data == {"search": "[None, 21]"} - - -def test_schema_input_type_automatic_out_name_arg(assert_schema_equals): - class SearchInput(GraphQLInput): - __schema__ = """ - input SearchInput { - query: String - minAge: Int - } - """ - - query: Optional[str] - min_age: Optional[int] - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.min_age])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String - minAge: Int - } - """, - ) - - result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") - - assert not result.errors - assert result.data == {"search": "[None, 21]"} - - -def test_schema_input_type_explicit_out_name_arg(assert_schema_equals): - class SearchInput(GraphQLInput): - __schema__ = """ - input SearchInput { - query: String - minAge: Int - } - """ - __out_names__ = {"minAge": "age"} - - query: Optional[str] - age: Optional[int] - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String - minAge: Int - } - """, - ) - - result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") - - assert not result.errors - assert result.data == {"search": "[None, 21]"} - - -def test_input_type_self_reference(assert_schema_equals): - class SearchInput(GraphQLInput): - query: Optional[str] - extra: Optional["SearchInput"] - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - if input.extra: - extra_repr = input.extra.query - else: - extra_repr = None - - return f"{repr([input.query, extra_repr])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String - extra: SearchInput - } - """, - ) - - result = graphql_sync( - schema, - """ - { - search( - input: { query: "Hello", extra: { query: "Other" } } - ) - } - """, - ) - - assert not result.errors - assert result.data == {"search": "['Hello', 'Other']"} - - -def test_schema_input_type_with_default_value(assert_schema_equals): - class SearchInput(GraphQLInput): - __schema__ = """ - input SearchInput { - query: String = "Search" - age: Int = 42 - } - """ - - query: str - age: int - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String = "Search" - age: Int = 42 - } - """, - ) - - result = graphql_sync(schema, "{ search(input: {}) }") - - assert not result.errors - assert result.data == {"search": "['Search', 42]"} - - -def test_input_type_with_field_default_value(assert_schema_equals): - class SearchInput(GraphQLInput): - query: str = "default" - age: int = 42 - flag: bool = False - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age, input.flag])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String! = "default" - age: Int! = 42 - flag: Boolean! = false - } - """, - ) - - result = graphql_sync(schema, "{ search(input: {}) }") - - assert not result.errors - assert result.data == {"search": "['default', 42, False]"} - - -def test_input_type_with_field_instance_default_value(assert_schema_equals): - class SearchInput(GraphQLInput): - query: str = GraphQLInput.field(default_value="default") - age: int = GraphQLInput.field(default_value=42) - flag: bool = GraphQLInput.field(default_value=False) - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age, input.flag])}" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: String! = "default" - age: Int! = 42 - flag: Boolean! = false - } - """, - ) - - result = graphql_sync(schema, "{ search(input: {}) }") - - assert not result.errors - assert result.data == {"search": "['default', 42, False]"} - - -def test_input_type_with_field_type(assert_schema_equals): - class SearchInput(GraphQLInput): - query: str = GraphQLInput.field(graphql_type=int) - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return str(input) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - query: Int! - } - """, - ) - - -def test_schema_input_type_with_field_description(assert_schema_equals): - class SearchInput(GraphQLInput): - __schema__ = """ - input SearchInput { - \"\"\"Hello world.\"\"\" - query: String! - } - """ - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return str(input) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - \"\"\"Hello world.\"\"\" - query: String! - } - """, - ) - - -def test_input_type_with_field_description(assert_schema_equals): - class SearchInput(GraphQLInput): - query: str = GraphQLInput.field(description="Hello world.") - - class QueryType(GraphQLObject): - search: str - - @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return str(input) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search(input: SearchInput!): String! - } - - input SearchInput { - \"\"\"Hello world.\"\"\" - query: String! - } - """, - ) diff --git a/tests_next/test_interface_type.py b/tests_next/test_interface_type.py deleted file mode 100644 index 74064ad..0000000 --- a/tests_next/test_interface_type.py +++ /dev/null @@ -1,366 +0,0 @@ -from typing import List, Union - -from graphql import graphql_sync - -from ariadne_graphql_modules.next import ( - GraphQLID, - GraphQLObject, - GraphQLInterface, - GraphQLUnion, - make_executable_schema, -) - - -class CommentType(GraphQLObject): - id: GraphQLID - content: str - - -def test_interface_without_schema(assert_schema_equals): - class UserInterface(GraphQLInterface): - summary: str - score: int - - class UserType(GraphQLObject): - name: str - summary: str - score: int - - __implements__ = [UserInterface] - - class ResultType(GraphQLUnion): - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[Union[UserType, CommentType]]: - return [ - UserType(id=1, username="Bob"), - CommentType(id=2, content="Hello World!"), - ] - - schema = make_executable_schema(QueryType, UserInterface, UserType) - - assert_schema_equals( - schema, - """ - type Query { - search: [Result!]! - } - - union Result = User | Comment - - type User implements UserInterface { - summary: String! - score: Int! - name: String! - } - - type Comment { - id: ID! - content: String! - } - - interface UserInterface { - summary: String! - score: Int! - } - - """, - ) - - -def test_interface_inheritance_without_schema(assert_schema_equals): - def hello_resolver(*_, name: str) -> str: - return f"Hello {name}!" - - class UserInterface(GraphQLInterface): - summary: str - score: str = GraphQLInterface.field( - hello_resolver, - name="better_score", - graphql_type=str, - args={"name": GraphQLInterface.argument(name="json")}, - description="desc", - default_value="my_json", - ) - - class UserType(GraphQLObject): - name: str = GraphQLInterface.field( - name="name", - graphql_type=str, - args={"name": GraphQLInterface.argument(name="json")}, - default_value="my_json", - ) - - __implements__ = [UserInterface] - - class ResultType(GraphQLUnion): - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[Union[UserType, CommentType]]: - return [ - UserType(), - CommentType(id=2, content="Hello World!"), - ] - - schema = make_executable_schema(QueryType, UserInterface, UserType) - - assert_schema_equals( - schema, - """ - type Query { - search: [Result!]! - } - - union Result = User | Comment - - type User implements UserInterface { - summary: String! - - \"\"\"desc\"\"\" - better_score(json: String!): String! - name: String! - } - - type Comment { - id: ID! - content: String! - } - - interface UserInterface { - summary: String! - - \"\"\"desc\"\"\" - better_score(json: String!): String! - } - - """, - ) - - result = graphql_sync( - schema, '{ search { ... on User{ better_score(json: "test") } } }' - ) - - assert not result.errors - assert result.data == {"search": [{"better_score": "Hello test!"}, {}]} - - -def test_interface_with_schema(assert_schema_equals): - class UserInterface(GraphQLInterface): - __schema__ = """ - interface UserInterface { - summary: String! - score: Int! - } - """ - - class UserType(GraphQLObject): - __schema__ = """ - type User implements UserInterface { - id: ID! - name: String! - summary: String! - score: Int! - } - """ - - __implements__ = [UserInterface] - - class ResultType(GraphQLUnion): - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[Union[UserType, CommentType]]: - return [ - UserType(id=1, username="Bob"), - CommentType(id=2, content="Hello World!"), - ] - - schema = make_executable_schema(QueryType, UserType) - - assert_schema_equals( - schema, - """ - type Query { - search: [Result!]! - } - - union Result = User | Comment - - type User implements UserInterface { - id: ID! - name: String! - summary: String! - score: Int! - } - - interface UserInterface { - summary: String! - score: Int! - } - - type Comment { - id: ID! - content: String! - } - - """, - ) - - -def test_interface_inheritance(assert_schema_equals): - class BaseEntityInterface(GraphQLInterface): - id: GraphQLID - - class UserInterface(GraphQLInterface): - username: str - - __implements__ = [BaseEntityInterface] - - class UserType(GraphQLObject): - - __implements__ = [UserInterface, BaseEntityInterface] - - class QueryType(GraphQLObject): - @GraphQLObject.field - def user(*_) -> UserType: - return UserType(id="1", username="test_user") - - schema = make_executable_schema( - QueryType, BaseEntityInterface, UserInterface, UserType - ) - - assert_schema_equals( - schema, - """ - type Query { - user: User! - } - - type User implements UserInterface & BaseEntityInterface { - id: ID! - username: String! - } - - interface UserInterface implements BaseEntityInterface { - id: ID! - username: String! - } - - interface BaseEntityInterface { - id: ID! - } - """, - ) - - -def test_interface_descriptions(assert_schema_equals): - class UserInterface(GraphQLInterface): - summary: str - score: int - - __description__ = "Lorem ipsum." - - class UserType(GraphQLObject): - id: GraphQLID - username: str - - __implements__ = [UserInterface] - - class QueryType(GraphQLObject): - @GraphQLObject.field - def user(*_) -> UserType: - return UserType(id="1", username="test_user") - - schema = make_executable_schema(QueryType, UserType, UserInterface) - - assert_schema_equals( - schema, - """ - type Query { - user: User! - } - - type User implements UserInterface { - summary: String! - score: Int! - id: ID! - username: String! - } - - \"\"\"Lorem ipsum.\"\"\" - interface UserInterface { - summary: String! - score: Int! - } - """, - ) - - -def test_interface_resolvers_and_field_descriptions(assert_schema_equals): - class UserInterface(GraphQLInterface): - summary: str - score: int - - @GraphQLInterface.resolver("score", description="Lorem ipsum.") - def resolve_score(*_): - return 200 - - class UserType(GraphQLObject): - id: GraphQLID - - __implements__ = [UserInterface] - - class MyType(GraphQLObject): - id: GraphQLID - name: str - - __implements__ = [UserInterface] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[UserInterface]) - def users(*_) -> List[Union[UserType, MyType]]: - return [MyType(id="2", name="old", summary="ss", score=22)] - - schema = make_executable_schema(QueryType, UserType, MyType, UserInterface) - - assert_schema_equals( - schema, - """ - type Query { - users: [UserInterface!]! - } - - interface UserInterface { - summary: String! - - \"\"\"Lorem ipsum.\"\"\" - score: Int! - } - - type User implements UserInterface { - summary: String! - - \"\"\"Lorem ipsum.\"\"\" - score: Int! - id: ID! - } - - type My implements UserInterface { - summary: String! - - \"\"\"Lorem ipsum.\"\"\" - score: Int! - id: ID! - name: String! - } - """, - ) - result = graphql_sync(schema, "{ users { ... on My { __typename score } } }") - - assert not result.errors - assert result.data == {"users": [{"__typename": "My", "score": 200}]} diff --git a/tests_next/test_interface_type_validation.py b/tests_next/test_interface_type_validation.py deleted file mode 100644 index 0c96f8e..0000000 --- a/tests_next/test_interface_type_validation.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest - -from ariadne_graphql_modules.next import ( - GraphQLID, - GraphQLObject, - GraphQLInterface, - make_executable_schema, -) - - -def test_interface_with_different_types(data_regression): - with pytest.raises(TypeError) as exc_info: - - class UserInterface(GraphQLInterface): - summary: str - score: str - - class UserType(GraphQLObject): - name: str - summary: str - score: int - - __implements__ = [UserInterface] - - make_executable_schema(UserType, UserInterface) - - data_regression.check(str(exc_info.value)) - - -def test_interface_no_interface_in_schema(data_regression): - with pytest.raises(TypeError) as exc_info: - - class BaseInterface(GraphQLInterface): - id: GraphQLID - - class UserType(GraphQLObject): - id: GraphQLID - username: str - email: str - - __implements__ = [BaseInterface] - - make_executable_schema(UserType) - - data_regression.check(str(exc_info.value)) diff --git a/tests_next/test_object_type.py b/tests_next/test_object_type.py deleted file mode 100644 index 6655bec..0000000 --- a/tests_next/test_object_type.py +++ /dev/null @@ -1,1015 +0,0 @@ -from typing import Optional - -import pytest -from graphql import graphql_sync - -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next import GraphQLObject, make_executable_schema - - -def test_object_type_instance_with_all_attrs_values(): - class CategoryType(GraphQLObject): - name: str - posts: int - - obj = CategoryType(name="Welcome", posts=20) - assert obj.name == "Welcome" - assert obj.posts == 20 - - -def test_object_type_instance_with_omitted_attrs_being_none(): - class CategoryType(GraphQLObject): - name: str - posts: int - - obj = CategoryType(posts=20) - assert obj.name is None - assert obj.posts == 20 - - -def test_object_type_instance_with_aliased_attrs_values(): - class CategoryType(GraphQLObject): - name: str - posts: int - - __aliases__ = {"name": "title"} - - title: str - - obj = CategoryType(title="Welcome", posts=20) - assert obj.title == "Welcome" - assert obj.posts == 20 - - -def test_object_type_instance_with_omitted_attrs_being_default_values(): - class CategoryType(GraphQLObject): - name: str = "Hello" - posts: int = 42 - - obj = CategoryType(posts=20) - assert obj.name == "Hello" - assert obj.posts == 20 - - -def test_object_type_instance_with_all_attrs_being_default_values(): - class CategoryType(GraphQLObject): - name: str = "Hello" - posts: int = 42 - - obj = CategoryType() - assert obj.name == "Hello" - assert obj.posts == 42 - - -def test_object_type_instance_with_invalid_attrs_raising_error(data_regression): - class CategoryType(GraphQLObject): - name: str - posts: int - - with pytest.raises(TypeError) as exc_info: - CategoryType(name="Welcome", invalid="Ok") - - data_regression.check(str(exc_info.value)) - - -def test_schema_object_type_instance_with_all_attrs_values(): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - - name: str - posts: int - - obj = CategoryType(name="Welcome", posts=20) - assert obj.name == "Welcome" - assert obj.posts == 20 - - -def test_schema_object_type_instance_with_omitted_attrs_being_none(): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - - name: str - posts: int - - obj = CategoryType(posts=20) - assert obj.name is None - assert obj.posts == 20 - - -def test_schema_object_type_instance_with_omitted_attrs_being_default_values(): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - - name: str = "Hello" - posts: int = 42 - - obj = CategoryType(posts=20) - assert obj.name == "Hello" - assert obj.posts == 20 - - -def test_schema_object_type_instance_with_all_attrs_being_default_values(): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - - name: str = "Hello" - posts: int = 42 - - obj = CategoryType() - assert obj.name == "Hello" - assert obj.posts == 42 - - -def test_schema_object_type_instance_with_aliased_attrs_values(): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - __aliases__ = {"name": "title"} - - title: str = "Hello" - posts: int = 42 - - obj = CategoryType(title="Ok") - assert obj.title == "Ok" - assert obj.posts == 42 - - -def test_schema_object_type_instance_with_aliased_attrs_default_values(): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - __aliases__ = {"name": "title"} - - title: str = "Hello" - posts: int = 42 - - obj = CategoryType() - assert obj.title == "Hello" - assert obj.posts == 42 - - -def test_schema_object_type_instance_with_invalid_attrs_raising_error(data_regression): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - - name: str - posts: int - - with pytest.raises(TypeError) as exc_info: - CategoryType(name="Welcome", invalid="Ok") - - data_regression.check(str(exc_info.value)) - - -def test_schema_object_type_instance_with_aliased_attr_value(): - class CategoryType(GraphQLObject): - __schema__ = gql( - """ - type Category { - name: String - posts: Int - } - """ - ) - __aliases__ = {"name": "title"} - - title: str - posts: int - - obj = CategoryType(title="Welcome", posts=20) - assert obj.title == "Welcome" - assert obj.posts == 20 - - -def test_object_type_with_field(assert_schema_equals): - class QueryType(GraphQLObject): - hello: str - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_alias(assert_schema_equals): - class QueryType(GraphQLObject): - __aliases__ = {"hello": "welcome_message"} - - hello: str - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync( - schema, "{ hello }", root_value={"welcome_message": "Hello World!"} - ) - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_alias_excludes_alias_targets(assert_schema_equals): - class QueryType(GraphQLObject): - __aliases__ = {"hello": "welcome"} - - hello: str - welcome: str - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }", root_value={"welcome": "Hello World!"}) - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_alias_includes_aliased_field_instances(assert_schema_equals): - class QueryType(GraphQLObject): - __aliases__ = {"hello": "welcome"} - - hello: str - welcome: str = GraphQLObject.field() - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - welcome: String! - } - """, - ) - - result = graphql_sync( - schema, "{ hello welcome }", root_value={"welcome": "Hello World!"} - ) - - assert not result.errors - assert result.data == {"hello": "Hello World!", "welcome": "Hello World!"} - - -def test_object_type_with_attr_automatic_alias(assert_schema_equals): - class QueryType(GraphQLObject): - test_message: str - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - testMessage: String! - } - """, - ) - - result = graphql_sync( - schema, "{ testMessage }", root_value={"test_message": "Hello World!"} - ) - - assert not result.errors - assert result.data == {"testMessage": "Hello World!"} - - -def test_object_type_with_field_instance_automatic_alias(assert_schema_equals): - class QueryType(GraphQLObject): - message: str = GraphQLObject.field(name="testMessage") - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - testMessage: String! - } - """, - ) - - result = graphql_sync( - schema, "{ testMessage }", root_value={"message": "Hello World!"} - ) - - assert not result.errors - assert result.data == {"testMessage": "Hello World!"} - - -def test_object_type_with_field_resolver(assert_schema_equals): - class QueryType(GraphQLObject): - @GraphQLObject.field - def hello(obj, info) -> str: - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_typed_field_instance(assert_schema_equals): - class QueryType(GraphQLObject): - hello = GraphQLObject.field(lambda *_: "Hello World!", graphql_type=str) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_annotated_field_instance(assert_schema_equals): - class QueryType(GraphQLObject): - hello: str = GraphQLObject.field(lambda *_: "Hello World!") - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_typed_field_and_field_resolver(assert_schema_equals): - class QueryType(GraphQLObject): - name: str - - @GraphQLObject.field - def hello(obj, info) -> str: - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - name: String! - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ name hello }", root_value={"name": "Ok"}) - - assert not result.errors - assert result.data == {"name": "Ok", "hello": "Hello World!"} - - -def test_object_type_with_schema(assert_schema_equals): - class QueryType(GraphQLObject): - __schema__ = gql( - """ - type Query { - hello: String! - } - """ - ) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_nested_types(assert_schema_equals): - class UserType(GraphQLObject): - name: str - - class PostType(GraphQLObject): - message: str - - class QueryType(GraphQLObject): - user: UserType - - @GraphQLObject.field(graphql_type=PostType) - def post(obj, info): - return {"message": "test"} - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - user: User! - post: Post! - } - - type User { - name: String! - } - - type Post { - message: String! - } - """, - ) - - result = graphql_sync( - schema, - "{ user { name } post { message } }", - root_value={"user": {"name": "Bob"}}, - ) - - assert not result.errors - assert result.data == { - "user": { - "name": "Bob", - }, - "post": {"message": "test"}, - } - - -def test_resolver_decorator_sets_resolver_for_type_hint_field(assert_schema_equals): - class QueryType(GraphQLObject): - hello: str - - @GraphQLObject.resolver("hello") - def resolve_hello(*_): - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_resolver_decorator_sets_resolver_for_instance_field(assert_schema_equals): - class QueryType(GraphQLObject): - hello: str = GraphQLObject.field(name="hello") - - @GraphQLObject.resolver("hello") - def resolve_hello(*_): - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_resolver_decorator_sets_resolver_for_field_in_schema(assert_schema_equals): - class QueryType(GraphQLObject): - __schema__ = gql( - """ - type Query { - hello: String! - } - """ - ) - - @GraphQLObject.resolver("hello") - def resolve_hello(*_): - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_object_type_with_description(assert_schema_equals): - class QueryType(GraphQLObject): - __description__ = "Lorem ipsum." - - hello: str - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - \"\"\"Lorem ipsum.\"\"\" - type Query { - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }", root_value={"hello": "Hello World!"}) - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_field_decorator_sets_description_for_field(assert_schema_equals): - class QueryType(GraphQLObject): - @GraphQLObject.field(description="Lorem ipsum.") - def hello(obj, info) -> str: - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - \"\"\"Lorem ipsum.\"\"\" - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_field_decorator_sets_description_for_field_arg(assert_schema_equals): - class QueryType(GraphQLObject): - @GraphQLObject.field( - args={"name": GraphQLObject.argument(description="Lorem ipsum.")} - ) - def hello(obj, info, name: str) -> str: - return f"Hello {name}!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello( - \"\"\"Lorem ipsum.\"\"\" - name: String! - ): String! - } - """, - ) - - result = graphql_sync(schema, '{ hello(name: "Bob") }') - - assert not result.errors - assert result.data == {"hello": "Hello Bob!"} - - -def test_resolver_decorator_sets_description_for_type_hint_field(assert_schema_equals): - class QueryType(GraphQLObject): - hello: str - - @GraphQLObject.resolver("hello", description="Lorem ipsum.") - def resolve_hello(*_): - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - \"\"\"Lorem ipsum.\"\"\" - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_resolver_decorator_sets_description_for_field_in_schema(assert_schema_equals): - class QueryType(GraphQLObject): - __schema__ = gql( - """ - type Query { - hello: String! - } - """ - ) - - @GraphQLObject.resolver("hello", description="Lorem ipsum.") - def resolve_hello(*_): - return "Hello World!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - \"\"\"Lorem ipsum.\"\"\" - hello: String! - } - """, - ) - - result = graphql_sync(schema, "{ hello }") - - assert not result.errors - assert result.data == {"hello": "Hello World!"} - - -def test_resolver_decorator_sets_description_for_field_arg(assert_schema_equals): - class QueryType(GraphQLObject): - hello: str - - @GraphQLObject.resolver( - "hello", args={"name": GraphQLObject.argument(description="Lorem ipsum.")} - ) - def resolve_hello(obj, info, name: str) -> str: - return f"Hello {name}!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello( - \"\"\"Lorem ipsum.\"\"\" - name: String! - ): String! - } - """, - ) - - result = graphql_sync(schema, '{ hello(name: "Bob") }') - - assert not result.errors - assert result.data == {"hello": "Hello Bob!"} - - -def test_schema_sets_description_for_field_arg(assert_schema_equals): - class QueryType(GraphQLObject): - __schema__ = gql( - """ - type Query { - hello( - \"\"\"Lorem ipsum.\"\"\" - name: String! - ): String! - } - """ - ) - - @GraphQLObject.resolver("hello") - def resolve_hello(*_, name: str): - return f"Hello {name}!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello( - \"\"\"Lorem ipsum.\"\"\" - name: String! - ): String! - } - """, - ) - - result = graphql_sync(schema, '{ hello(name: "Bob") }') - - assert not result.errors - assert result.data == {"hello": "Hello Bob!"} - - -def test_resolver_decorator_sets_description_for_field_arg_in_schema( - assert_schema_equals, -): - class QueryType(GraphQLObject): - __schema__ = gql( - """ - type Query { - hello(name: String!): String! - } - """ - ) - - @GraphQLObject.resolver( - "hello", args={"name": GraphQLObject.argument(description="Description")} - ) - def resolve_hello(*_, name: str): - return f"Hello {name}!" - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - hello( - \"\"\"Description\"\"\" - name: String! - ): String! - } - """, - ) - - result = graphql_sync(schema, '{ hello(name: "Bob") }') - - assert not result.errors - assert result.data == {"hello": "Hello Bob!"} - - -def test_object_type_self_reference( - assert_schema_equals, -): - class CategoryType(GraphQLObject): - name: str - parent: Optional["CategoryType"] - - class QueryType(GraphQLObject): - category: CategoryType - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - category: Category! - } - - type Category { - name: String! - parent: Category - } - """, - ) - - result = graphql_sync( - schema, - "{ category { name parent { name } } }", - root_value={ - "category": { - "name": "Lorem", - "parent": { - "name": "Ipsum", - }, - }, - }, - ) - - assert not result.errors - assert result.data == { - "category": { - "name": "Lorem", - "parent": { - "name": "Ipsum", - }, - }, - } - - -def test_object_type_return_instance( - assert_schema_equals, -): - class CategoryType(GraphQLObject): - name: str - color: str - - class QueryType(GraphQLObject): - @GraphQLObject.field() - def category(*_) -> CategoryType: - return CategoryType( - name="Welcome", - color="#FF00FF", - ) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - category: Category! - } - - type Category { - name: String! - color: String! - } - """, - ) - - result = graphql_sync(schema, "{ category { name color } }") - - assert not result.errors - assert result.data == { - "category": { - "name": "Welcome", - "color": "#FF00FF", - }, - } - - -def test_object_type_nested_type( - assert_schema_equals, -): - class UserType(GraphQLObject): - username: str - - class CategoryType(GraphQLObject): - name: str - parent: Optional["CategoryType"] - owner: UserType - - class QueryType(GraphQLObject): - category: CategoryType - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - category: Category! - } - - type Category { - name: String! - parent: Category - owner: User! - } - - type User { - username: String! - } - """, - ) - - result = graphql_sync( - schema, - "{ category { name parent { name } owner { username } } }", - root_value={ - "category": { - "name": "Lorem", - "parent": { - "name": "Ipsum", - }, - "owner": { - "username": "John", - }, - }, - }, - ) - - assert not result.errors - assert result.data == { - "category": { - "name": "Lorem", - "parent": { - "name": "Ipsum", - }, - "owner": { - "username": "John", - }, - }, - } diff --git a/tests_next/test_scalar_type.py b/tests_next/test_scalar_type.py deleted file mode 100644 index 2f578c6..0000000 --- a/tests_next/test_scalar_type.py +++ /dev/null @@ -1,111 +0,0 @@ -from datetime import date - -from graphql import graphql_sync - -from ariadne_graphql_modules import gql -from ariadne_graphql_modules.next import ( - GraphQLObject, - GraphQLScalar, - make_executable_schema, -) - - -class DateScalar(GraphQLScalar[date]): - @classmethod - def serialize(cls, value): - if isinstance(value, cls): - return str(value.unwrap()) - - return str(value) - - -def test_scalar_field_returning_scalar_instance(assert_schema_equals): - class QueryType(GraphQLObject): - date: DateScalar - - @GraphQLObject.resolver("date") - def resolve_date(*_) -> DateScalar: - return DateScalar(date(1989, 10, 30)) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - scalar Date - - type Query { - date: Date! - } - """, - ) - - result = graphql_sync(schema, "{ date }") - - assert not result.errors - assert result.data == {"date": "1989-10-30"} - - -def test_scalar_field_returning_scalar_wrapped_type(assert_schema_equals): - class QueryType(GraphQLObject): - scalar_date: DateScalar - - @GraphQLObject.resolver("scalar_date", graphql_type=DateScalar) - def resolve_date(*_) -> date: - return date(1989, 10, 30) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - scalar Date - - type Query { - scalarDate: Date! - } - """, - ) - - result = graphql_sync(schema, "{ scalarDate }") - - assert not result.errors - assert result.data == {"scalarDate": "1989-10-30"} - - -class SchemaDateScalar(GraphQLScalar[date]): - __schema__ = gql("scalar Date") - - @classmethod - def serialize(cls, value): - if isinstance(value, cls): - return str(value.unwrap()) - - return str(value) - - -def test_schema_scalar_field_returning_scalar_instance(assert_schema_equals): - class QueryType(GraphQLObject): - date: SchemaDateScalar - - @GraphQLObject.resolver("date") - def resolve_date(*_) -> SchemaDateScalar: - return SchemaDateScalar(date(1989, 10, 30)) - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - scalar Date - - type Query { - date: Date! - } - """, - ) - - result = graphql_sync(schema, "{ date }") - - assert not result.errors - assert result.data == {"date": "1989-10-30"} diff --git a/tests_next/test_subscription_type.py b/tests_next/test_subscription_type.py deleted file mode 100644 index b8bd2a5..0000000 --- a/tests_next/test_subscription_type.py +++ /dev/null @@ -1,754 +0,0 @@ -from typing import List - -from ariadne import gql -from graphql import subscribe, parse -import pytest -from ariadne_graphql_modules.next import ( - GraphQLID, - GraphQLObject, - GraphQLSubscription, - GraphQLUnion, - make_executable_schema, -) - - -class Message(GraphQLObject): - id: GraphQLID - content: str - author: str - - -class User(GraphQLObject): - id: GraphQLID - username: str - - -class Notification(GraphQLUnion): - __types__ = [Message, User] - - -@pytest.mark.asyncio -async def test_basic_subscription_without_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - message_added: Message - - @GraphQLSubscription.source("message_added") - async def message_added_generator(obj, info): - while True: - yield {"id": "some_id", "content": "message", "author": "Anon"} - - @GraphQLSubscription.resolver("message_added", graphql_type=Message) - async def resolve_message_added(message, info): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messageAdded: Message! - } - - type Message { - id: ID! - content: String! - author: String! - } - """, - ) - - query = parse("subscription { messageAdded {id content author} }") - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == { - "messageAdded": {"id": "some_id", "content": "message", "author": "Anon"} - } - - -@pytest.mark.asyncio -async def test_subscription_with_arguments_without_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - message_added: Message - - @GraphQLSubscription.source( - "message_added", - args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, - ) - async def message_added_generator(obj, info, channel: GraphQLID): - while True: - yield { - "id": "some_id", - "content": f"message_{channel}", - "author": "Anon", - } - - @GraphQLSubscription.resolver( - "message_added", - graphql_type=Message, - ) - async def resolve_message_added(message, *_, channel: GraphQLID): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messageAdded( - \"\"\"Lorem ipsum.\"\"\" - channel: ID! - ): Message! - } - - type Message { - id: ID! - content: String! - author: String! - } - """, - ) - - query = parse('subscription { messageAdded(channel: "123") {id content author} }') - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == { - "messageAdded": {"id": "some_id", "content": "message_123", "author": "Anon"} - } - - -@pytest.mark.asyncio -async def test_multiple_supscriptions_without_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - message_added: Message - user_joined: User - - @GraphQLSubscription.source( - "message_added", - args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, - ) - async def message_added_generator(obj, info, channel: GraphQLID): - while True: - yield { - "id": "some_id", - "content": f"message_{channel}", - "author": "Anon", - } - - @GraphQLSubscription.resolver( - "message_added", - graphql_type=Message, - ) - async def resolve_message_added(message, *_, channel: GraphQLID): - return message - - @GraphQLSubscription.source( - "user_joined", - ) - async def user_joined_generator(obj, info): - while True: - yield { - "id": "some_id", - "username": "username", - } - - @GraphQLSubscription.resolver( - "user_joined", - graphql_type=Message, - ) - async def resolve_user_joined(user, *_): - return user - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messageAdded( - \"\"\"Lorem ipsum.\"\"\" - channel: ID! - ): Message! - userJoined: User! - } - - type Message { - id: ID! - content: String! - author: String! - } - - type User { - id: ID! - username: String! - } - """, - ) - - query = parse("subscription { userJoined {id username} }") - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == {"userJoined": {"id": "some_id", "username": "username"}} - - -@pytest.mark.asyncio -async def test_subscription_with_complex_data_without_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - messages_in_channel: List[Message] - - @GraphQLSubscription.source( - "messages_in_channel", - args={"channel_id": GraphQLObject.argument(description="Lorem ipsum.")}, - ) - async def message_added_generator(obj, info, channel_id: GraphQLID): - while True: - yield [ - { - "id": "some_id", - "content": f"message_{channel_id}", - "author": "Anon", - } - ] - - @GraphQLSubscription.resolver( - "messages_in_channel", - graphql_type=Message, - ) - async def resolve_message_added(message, *_, channel_id: GraphQLID): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messagesInChannel( - \"\"\"Lorem ipsum.\"\"\" - channelId: ID! - ): [Message!]! - } - - type Message { - id: ID! - content: String! - author: String! - } - """, - ) - - query = parse( - 'subscription { messagesInChannel(channelId: "123") {id content author} }' - ) - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == { - "messagesInChannel": [ - {"id": "some_id", "content": "message_123", "author": "Anon"} - ] - } - - -@pytest.mark.asyncio -async def test_subscription_with_union_without_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - notification_received: Notification - - @GraphQLSubscription.source( - "notification_received", - ) - async def message_added_generator(obj, info): - while True: - yield Message(id=1, content="content", author="anon") - - @GraphQLSubscription.resolver( - "notification_received", - ) - async def resolve_message_added(message, *_): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - notificationReceived: Notification! - } - - union Notification = Message | User - - type Message { - id: ID! - content: String! - author: String! - } - - type User { - id: ID! - username: String! - } - """, - ) - - query = parse("subscription { notificationReceived { ... on Message { id } } }") - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == {"notificationReceived": {"id": "1"}} - - -@pytest.mark.asyncio -async def test_basic_subscription_with_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - __schema__ = gql( - """ - type Subscription { - messageAdded: Message! - } - """ - ) - - @GraphQLSubscription.source("messageAdded") - async def message_added_generator(obj, info): - while True: - yield {"id": "some_id", "content": "message", "author": "Anon"} - - @GraphQLSubscription.resolver("messageAdded", graphql_type=Message) - async def resolve_message_added(message, info): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType, Message) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messageAdded: Message! - } - - type Message { - id: ID! - content: String! - author: String! - } - """, - ) - - query = parse("subscription { messageAdded {id content author} }") - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == { - "messageAdded": {"id": "some_id", "content": "message", "author": "Anon"} - } - - -@pytest.mark.asyncio -async def test_subscription_with_arguments_with_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - __schema__ = gql( - """ - type Subscription { - messageAdded(channel: ID!): Message! - } - """ - ) - - @GraphQLSubscription.source( - "messageAdded", - ) - async def message_added_generator(obj, info, channel: GraphQLID): - while True: - yield { - "id": "some_id", - "content": f"message_{channel}", - "author": "Anon", - } - - @GraphQLSubscription.resolver( - "messageAdded", - graphql_type=Message, - ) - async def resolve_message_added(message, *_, channel: GraphQLID): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType, Message) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messageAdded(channel: ID!): Message! - } - - type Message { - id: ID! - content: String! - author: String! - } - """, - ) - - query = parse('subscription { messageAdded(channel: "123") {id content author} }') - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == { - "messageAdded": {"id": "some_id", "content": "message_123", "author": "Anon"} - } - - -@pytest.mark.asyncio -async def test_multiple_supscriptions_with_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - __schema__ = gql( - """ - type Subscription { - messageAdded: Message! - userJoined: User! - } - """ - ) - - @GraphQLSubscription.source( - "messageAdded", - ) - async def message_added_generator(obj, info): - while True: - yield { - "id": "some_id", - "content": "message", - "author": "Anon", - } - - @GraphQLSubscription.resolver( - "messageAdded", - ) - async def resolve_message_added(message, *_): - return message - - @GraphQLSubscription.source( - "userJoined", - ) - async def user_joined_generator(obj, info): - while True: - yield { - "id": "some_id", - "username": "username", - } - - @GraphQLSubscription.resolver( - "userJoined", - ) - async def resolve_user_joined(user, *_): - return user - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType, Message, User) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messageAdded: Message! - userJoined: User! - } - - type Message { - id: ID! - content: String! - author: String! - } - - type User { - id: ID! - username: String! - } - """, - ) - - query = parse("subscription { userJoined {id username} }") - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == {"userJoined": {"id": "some_id", "username": "username"}} - - -@pytest.mark.asyncio -async def test_subscription_with_complex_data_with_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - __schema__ = gql( - """ - type Subscription { - messagesInChannel(channelId: ID!): [Message!]! - } - """ - ) - - @GraphQLSubscription.source( - "messagesInChannel", - ) - async def message_added_generator(obj, info, channelId: GraphQLID): - while True: - yield [ - { - "id": "some_id", - "content": f"message_{channelId}", - "author": "Anon", - } - ] - - @GraphQLSubscription.resolver( - "messagesInChannel", - ) - async def resolve_message_added(message, *_, channelId: GraphQLID): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType, Message) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - messagesInChannel(channelId: ID!): [Message!]! - } - - type Message { - id: ID! - content: String! - author: String! - } - """, - ) - - query = parse( - 'subscription { messagesInChannel(channelId: "123") {id content author} }' - ) - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == { - "messagesInChannel": [ - {"id": "some_id", "content": "message_123", "author": "Anon"} - ] - } - - -@pytest.mark.asyncio -async def test_subscription_with_union_with_schema(assert_schema_equals): - class SubscriptionType(GraphQLSubscription): - __schema__ = gql( - """ - type Subscription { - notificationReceived: Notification! - } - """ - ) - - @GraphQLSubscription.source( - "notificationReceived", - ) - async def message_added_generator(obj, info): - while True: - yield Message(id=1, content="content", author="anon") - - @GraphQLSubscription.resolver( - "notificationReceived", - ) - async def resolve_message_added(message, *_): - return message - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=str) - def search_sth(*_) -> str: - return "search" - - schema = make_executable_schema(QueryType, SubscriptionType, Notification) - - assert_schema_equals( - schema, - """ - type Query { - searchSth: String! - } - - type Subscription { - notificationReceived: Notification! - } - - union Notification = Message | User - - type Message { - id: ID! - content: String! - author: String! - } - - type User { - id: ID! - username: String! - } - """, - ) - - query = parse("subscription { notificationReceived { ... on Message { id } } }") - sub = await subscribe(schema, query) - - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") - - # Fetch the first result - result = await sub.__anext__() - - # Validate the result - assert not result.errors - assert result.data == {"notificationReceived": {"id": "1"}} diff --git a/tests_next/test_union_type.py b/tests_next/test_union_type.py deleted file mode 100644 index 6d20267..0000000 --- a/tests_next/test_union_type.py +++ /dev/null @@ -1,223 +0,0 @@ -from typing import List, Union - -from graphql import graphql_sync - -from ariadne_graphql_modules.next import ( - GraphQLID, - GraphQLObject, - GraphQLUnion, - make_executable_schema, -) - - -class UserType(GraphQLObject): - id: GraphQLID - username: str - - -class CommentType(GraphQLObject): - id: GraphQLID - content: str - - -def test_union_field_returning_object_instance(assert_schema_equals): - class ResultType(GraphQLUnion): - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[Union[UserType, CommentType]]: - return [ - UserType(id=1, username="Bob"), - CommentType(id=2, content="Hello World!"), - ] - - schema = make_executable_schema(QueryType) - - assert_schema_equals( - schema, - """ - type Query { - search: [Result!]! - } - - union Result = User | Comment - - type User { - id: ID! - username: String! - } - - type Comment { - id: ID! - content: String! - } - """, - ) - - result = graphql_sync( - schema, - """ - { - search { - ... on User { - id - username - } - ... on Comment { - id - content - } - } - } - """, - ) - - assert not result.errors - assert result.data == { - "search": [ - {"id": "1", "username": "Bob"}, - {"id": "2", "content": "Hello World!"}, - ] - } - - -def test_union_field_returning_empty_list(): - class ResultType(GraphQLUnion): - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[Union[UserType, CommentType]]: - return [] - - schema = make_executable_schema(QueryType) - - result = graphql_sync( - schema, - """ - { - search { - ... on User { - id - username - } - ... on Comment { - id - content - } - } - } - """, - ) - assert not result.errors - assert result.data == {"search": []} - - -def test_union_field_with_invalid_type_access(assert_schema_equals): - class ResultType(GraphQLUnion): - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[Union[UserType, CommentType]]: - return [ - UserType(id=1, username="Bob"), - "InvalidType", - ] - - schema = make_executable_schema(QueryType) - - result = graphql_sync( - schema, - """ - { - search { - ... on User { - id - username - } - ... on Comment { - id - content - } - } - } - """, - ) - assert result.errors - assert "InvalidType" in str(result.errors) - - -def test_serialization_error_handling(assert_schema_equals): - class InvalidType: - def __init__(self, value): - self.value = value - - class ResultType(GraphQLUnion): - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) - def search(*_) -> List[Union[UserType, CommentType, InvalidType]]: - return [InvalidType("This should cause an error")] - - schema = make_executable_schema(QueryType) - - result = graphql_sync( - schema, - """ - { - search { - ... on User { - id - username - } - } - } - """, - ) - assert result.errors - - -def test_union_with_schema_definition(assert_schema_equals): - class SearchResultUnion(GraphQLUnion): - __schema__ = """ - union SearchResult = User | Comment - """ - __types__ = [UserType, CommentType] - - class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[SearchResultUnion]) - def search(*_) -> List[Union[UserType, CommentType]]: - return [ - UserType(id="1", username="Alice"), - CommentType(id="2", content="Test post"), - ] - - schema = make_executable_schema(QueryType, SearchResultUnion) - - result = graphql_sync( - schema, - """ - { - search { - ... on User { - id - username - } - ... on Comment { - id - content - } - } - } - """, - ) - assert not result.errors - assert result.data == { - "search": [ - {"id": "1", "username": "Alice"}, - {"id": "2", "content": "Test post"}, - ] - } diff --git a/tests_next/__init__.py b/tests_v1/__init__.py similarity index 100% rename from tests_next/__init__.py rename to tests_v1/__init__.py diff --git a/tests_v1/conftest.py b/tests_v1/conftest.py new file mode 100644 index 0000000..0f16709 --- /dev/null +++ b/tests_v1/conftest.py @@ -0,0 +1,12 @@ +from pathlib import Path +import pytest + + +@pytest.fixture(scope="session") +def datadir() -> Path: + return Path(__file__).parent / "snapshots" + + +@pytest.fixture(scope="session") +def original_datadir() -> Path: + return Path(__file__).parent / "snapshots" diff --git a/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml b/tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml similarity index 100% rename from tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml rename to tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml diff --git a/tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml b/tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml similarity index 100% rename from tests/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml rename to tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.yml diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml b/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml similarity index 100% rename from tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml rename to tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml b/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml similarity index 100% rename from tests/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml rename to tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.yml diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml b/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml similarity index 100% rename from tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml rename to tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml diff --git a/tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml b/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml similarity index 100% rename from tests/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml rename to tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.yml diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml rename to tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml diff --git a/tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml b/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml rename to tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml diff --git a/tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml similarity index 100% rename from tests/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml rename to tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.yml diff --git a/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml b/tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml similarity index 100% rename from tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml rename to tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml diff --git a/tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml b/tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml similarity index 100% rename from tests/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml rename to tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.yml diff --git a/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_defined_without_fields.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_defined_without_fields.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml diff --git a/tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml similarity index 100% rename from tests/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml rename to tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.yml diff --git a/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml diff --git a/tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml similarity index 100% rename from tests/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml rename to tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.yml diff --git a/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml diff --git a/tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml similarity index 100% rename from tests/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml rename to tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.yml diff --git a/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_fields.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_fields.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml diff --git a/tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml similarity index 100% rename from tests/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml rename to tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.yml diff --git a/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml diff --git a/tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml similarity index 100% rename from tests/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml rename to tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.yml diff --git a/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml b/tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml rename to tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml diff --git a/tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml b/tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml similarity index 100% rename from tests/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml rename to tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.yml diff --git a/tests/test_collection_type.py b/tests_v1/test_collection_type.py similarity index 100% rename from tests/test_collection_type.py rename to tests_v1/test_collection_type.py diff --git a/tests/test_convert_case.py b/tests_v1/test_convert_case.py similarity index 100% rename from tests/test_convert_case.py rename to tests_v1/test_convert_case.py diff --git a/tests/test_definition_parser.py b/tests_v1/test_definition_parser.py similarity index 100% rename from tests/test_definition_parser.py rename to tests_v1/test_definition_parser.py diff --git a/tests/test_directive_type.py b/tests_v1/test_directive_type.py similarity index 100% rename from tests/test_directive_type.py rename to tests_v1/test_directive_type.py diff --git a/tests_v1/test_enum_type.py b/tests_v1/test_enum_type.py new file mode 100644 index 0000000..7ea4585 --- /dev/null +++ b/tests_v1/test_enum_type.py @@ -0,0 +1,304 @@ +from enum import Enum + +import pytest +from ariadne import SchemaDirectiveVisitor +from graphql import GraphQLError, graphql_sync + +from ariadne_graphql_modules import ( + DirectiveType, + EnumType, + ObjectType, + make_executable_schema, +) + + +def test_enum_type_raises_attribute_error_when_defined_without_schema(data_regression): + with pytest.raises(AttributeError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + pass + + data_regression.check(str(err.value)) + + +def test_enum_type_raises_error_when_defined_with_invalid_schema_type(data_regression): + with pytest.raises(TypeError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = True + + data_regression.check(str(err.value)) + + +def test_enum_type_raises_error_when_defined_with_invalid_schema_str(data_regression): + with pytest.raises(GraphQLError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = "enom UserRole" + + data_regression.check(str(err.value)) + + +def test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = "scalar UserRole" + + data_regression.check(str(err.value)) + + +def test_enum_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + + enum Category { + CATEGORY + LINK + } + """ + + data_regression.check(str(err.value)) + + +def test_enum_type_extracts_graphql_name(): + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + + assert UserRoleEnum.graphql_name == "UserRole" + + +def test_enum_type_can_be_extended_with_new_values(): + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + + class ExtendUserRoleEnum(EnumType): + __schema__ = """ + extend enum UserRole { + MVP + } + """ + __requires__ = [UserRoleEnum] + + +def test_enum_type_can_be_extended_with_directive(): + # pylint: disable=unused-variable + class ExampleDirective(DirectiveType): + __schema__ = "directive @example on ENUM" + __visitor__ = SchemaDirectiveVisitor + + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + + class ExtendUserRoleEnum(EnumType): + __schema__ = "extend enum UserRole @example" + __requires__ = [UserRoleEnum, ExampleDirective] + + +class BaseQueryType(ObjectType): + __abstract__ = True + __schema__ = """ + type Query { + enumToRepr(enum: UserRole = USER): String! + reprToEnum: UserRole! + } + """ + __aliases__ = { + "enumToRepr": "enum_repr", + } + + @staticmethod + def resolve_enum_repr(*_, enum) -> str: + return repr(enum) + + +def make_test_schema(enum_type): + class QueryType(BaseQueryType): + __requires__ = [enum_type] + + return make_executable_schema(QueryType) + + +def test_enum_type_can_be_defined_with_dict_mapping(): + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + __enum__ = { + "USER": 0, + "MOD": 1, + "ADMIN": 2, + } + + schema = make_test_schema(UserRoleEnum) + + # Specfied enum value is reversed + result = graphql_sync(schema, "{ enumToRepr(enum: MOD) }") + assert result.data["enumToRepr"] == "1" + + # Default enum value is reversed + result = graphql_sync(schema, "{ enumToRepr }") + assert result.data["enumToRepr"] == "0" + + # Python value is converted to enum + result = graphql_sync(schema, "{ reprToEnum }", root_value={"reprToEnum": 2}) + assert result.data["reprToEnum"] == "ADMIN" + + +def test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + __enum__ = { + "USER": 0, + "MODERATOR": 1, + "ADMIN": 2, + } + + data_regression.check(str(err.value)) + + +def test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + __enum__ = { + "USER": 0, + "REVIEW": 1, + "MOD": 2, + "ADMIN": 3, + } + + data_regression.check(str(err.value)) + + +def test_enum_type_can_be_defined_with_str_enum_mapping(): + class RoleEnum(str, Enum): + USER = "user" + MOD = "moderator" + ADMIN = "administrator" + + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + __enum__ = RoleEnum + + schema = make_test_schema(UserRoleEnum) + + # Specfied enum value is reversed + result = graphql_sync(schema, "{ enumToRepr(enum: MOD) }") + assert result.data["enumToRepr"] == repr(RoleEnum.MOD) + + # Default enum value is reversed + result = graphql_sync(schema, "{ enumToRepr }") + assert result.data["enumToRepr"] == repr(RoleEnum.USER) + + # Python value is converted to enum + result = graphql_sync( + schema, "{ reprToEnum }", root_value={"reprToEnum": "administrator"} + ) + assert result.data["reprToEnum"] == "ADMIN" + + +def test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition( + data_regression, +): + class RoleEnum(str, Enum): + USER = "user" + MODERATOR = "moderator" + ADMIN = "administrator" + + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + __enum__ = RoleEnum + + data_regression.check(str(err.value)) + + +def test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition( + data_regression, +): + class RoleEnum(str, Enum): + USER = "user" + REVIEW = "review" + MOD = "moderator" + ADMIN = "administrator" + + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserRoleEnum(EnumType): + __schema__ = """ + enum UserRole { + USER + MOD + ADMIN + } + """ + __enum__ = RoleEnum + + data_regression.check(str(err.value)) diff --git a/tests/test_executable_schema.py b/tests_v1/test_executable_schema.py similarity index 100% rename from tests/test_executable_schema.py rename to tests_v1/test_executable_schema.py diff --git a/tests/test_executable_schema_compat.py b/tests_v1/test_executable_schema_compat.py similarity index 100% rename from tests/test_executable_schema_compat.py rename to tests_v1/test_executable_schema_compat.py diff --git a/tests_v1/test_input_type.py b/tests_v1/test_input_type.py new file mode 100644 index 0000000..daad5e0 --- /dev/null +++ b/tests_v1/test_input_type.py @@ -0,0 +1,293 @@ +import pytest +from ariadne import SchemaDirectiveVisitor +from graphql import GraphQLError, graphql_sync + +from ariadne_graphql_modules import ( + DeferredType, + DirectiveType, + EnumType, + InputType, + InterfaceType, + ObjectType, + ScalarType, + make_executable_schema, +) + + +def test_input_type_raises_attribute_error_when_defined_without_schema(data_regression): + with pytest.raises(AttributeError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + pass + + data_regression.check(str(err.value)) + + +def test_input_type_raises_error_when_defined_with_invalid_schema_type(data_regression): + with pytest.raises(TypeError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = True + + data_regression.check(str(err.value)) + + +def test_input_type_raises_error_when_defined_with_invalid_schema_str(data_regression): + with pytest.raises(GraphQLError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = "inpet UserInput" + + data_regression.check(str(err.value)) + + +def test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = """ + type User { + id: ID! + } + """ + + data_regression.check(str(err.value)) + + +def test_input_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = """ + input User + + input Group + """ + + data_regression.check(str(err.value)) + + +def test_input_type_raises_error_when_defined_without_fields(data_regression): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = "input User" + + data_regression.check(str(err.value)) + + +def test_input_type_extracts_graphql_name(): + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + } + """ + + assert UserInput.graphql_name == "User" + + +def test_input_type_raises_error_when_defined_without_field_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + role: Role! + } + """ + + data_regression.check(str(err.value)) + + +def test_input_type_verifies_field_dependency(): + # pylint: disable=unused-variable + class RoleEnum(EnumType): + __schema__ = """ + enum Role { + USER + ADMIN + } + """ + + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + role: Role! + } + """ + __requires__ = [RoleEnum] + + +def test_input_type_verifies_circular_dependency(): + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + patron: User + } + """ + + +def test_input_type_verifies_circular_dependency_using_deferred_type(): + # pylint: disable=unused-variable + class GroupInput(InputType): + __schema__ = """ + input Group { + id: ID! + patron: User + } + """ + __requires__ = [DeferredType("User")] + + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + group: Group + } + """ + __requires__ = [GroupInput] + + +def test_input_type_can_be_extended_with_new_fields(): + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + } + """ + + class ExtendUserInput(InputType): + __schema__ = """ + extend input User { + name: String! + } + """ + __requires__ = [UserInput] + + +def test_input_type_can_be_extended_with_directive(): + # pylint: disable=unused-variable + class ExampleDirective(DirectiveType): + __schema__ = "directive @example on INPUT_OBJECT" + __visitor__ = SchemaDirectiveVisitor + + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + } + """ + + class ExtendUserInput(InputType): + __schema__ = """ + extend input User @example + """ + __requires__ = [UserInput, ExampleDirective] + + +def test_input_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExtendUserInput(InputType): + __schema__ = """ + extend input User { + name: String! + } + """ + + data_regression.check(str(err.value)) + + +def test_input_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface User { + id: ID! + } + """ + + class ExtendUserInput(InputType): + __schema__ = """ + extend input User { + name: String! + } + """ + __requires__ = [ExampleInterface] + + data_regression.check(str(err.value)) + + +def test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserInput(InputType): + __schema__ = """ + input User { + id: ID! + } + """ + __args__ = { + "fullName": "full_name", + } + + data_regression.check(str(err.value)) + + +class UserInput(InputType): + __schema__ = """ + input UserInput { + id: ID! + fullName: String! + } + """ + __args__ = { + "fullName": "full_name", + } + + +class GenericScalar(ScalarType): + __schema__ = "scalar Generic" + + +class QueryType(ObjectType): + __schema__ = """ + type Query { + reprInput(input: UserInput): Generic! + } + """ + __aliases__ = {"reprInput": "repr_input"} + __requires__ = [GenericScalar, UserInput] + + @staticmethod + def resolve_repr_input(*_, input): # pylint: disable=redefined-builtin + return input + + +schema = make_executable_schema(QueryType) + + +def test_input_type_maps_args_to_python_dict_keys(): + result = graphql_sync(schema, '{ reprInput(input: {id: "1", fullName: "Alice"}) }') + assert result.data == { + "reprInput": {"id": "1", "full_name": "Alice"}, + } diff --git a/tests_v1/test_interface_type.py b/tests_v1/test_interface_type.py new file mode 100644 index 0000000..be3637d --- /dev/null +++ b/tests_v1/test_interface_type.py @@ -0,0 +1,462 @@ +from dataclasses import dataclass + +import pytest +from ariadne import SchemaDirectiveVisitor +from graphql import GraphQLError, graphql_sync + +from ariadne_graphql_modules import ( + DeferredType, + DirectiveType, + InterfaceType, + ObjectType, + make_executable_schema, +) + + +def test_interface_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): + with pytest.raises(AttributeError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + pass + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): + with pytest.raises(TypeError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = True + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_defined_with_invalid_schema_str( + data_regression, +): + with pytest.raises(GraphQLError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = "interfaco Example" + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = "type Example" + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example + + interface Other + """ + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_defined_without_fields(data_regression): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = "interface Example" + + data_regression.check(str(err.value)) + + +def test_interface_type_extracts_graphql_name(): + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + id: ID! + } + """ + + assert ExampleInterface.graphql_name == "Example" + + +def test_interface_type_raises_error_when_defined_without_return_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + group: Group + groups: [Group!] + } + """ + + data_regression.check(str(err.value)) + + +def test_interface_type_verifies_field_dependency(): + # pylint: disable=unused-variable + class GroupType(ObjectType): + __schema__ = """ + type Group { + id: ID! + } + """ + + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + group: Group + groups: [Group!] + } + """ + __requires__ = [GroupType] + + +def test_interface_type_verifies_circural_dependency(): + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + parent: Example + } + """ + + +def test_interface_type_raises_error_when_defined_without_argument_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + actions(input: UserInput): [String!]! + } + """ + + data_regression.check(str(err.value)) + + +def test_interface_type_verifies_circular_dependency_using_deferred_type(): + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + id: ID! + users: [User] + } + """ + __requires__ = [DeferredType("User")] + + class UserType(ObjectType): + __schema__ = """ + type User { + roles: [Example] + } + """ + __requires__ = [ExampleInterface] + + +def test_interface_type_can_be_extended_with_new_fields(): + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + id: ID! + } + """ + + class ExtendExampleInterface(InterfaceType): + __schema__ = """ + extend interface Example { + name: String + } + """ + __requires__ = [ExampleInterface] + + +def test_interface_type_can_be_extended_with_directive(): + # pylint: disable=unused-variable + class ExampleDirective(DirectiveType): + __schema__ = "directive @example on INTERFACE" + __visitor__ = SchemaDirectiveVisitor + + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + id: ID! + } + """ + + class ExtendExampleInterface(InterfaceType): + __schema__ = """ + extend interface Example @example + """ + __requires__ = [ExampleInterface, ExampleDirective] + + +def test_interface_type_can_be_extended_with_other_interface(): + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + id: ID! + } + """ + + class OtherInterface(InterfaceType): + __schema__ = """ + interface Other { + depth: Int! + } + """ + + class ExtendExampleInterface(InterfaceType): + __schema__ = """ + extend interface Example implements Other + """ + __requires__ = [ExampleInterface, OtherInterface] + + +def test_interface_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExtendExampleInterface(ObjectType): + __schema__ = """ + extend interface Example { + name: String + } + """ + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleType(ObjectType): + __schema__ = """ + type Example { + id: ID! + } + """ + + class ExampleInterface(InterfaceType): + __schema__ = """ + extend interface Example { + name: String + } + """ + __requires__ = [ExampleType] + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface User { + name: String + } + """ + __aliases__ = { + "joinedDate": "joined_date", + } + + data_regression.check(str(err.value)) + + +def test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface User { + name: String + } + """ + + @staticmethod + def resolve_group(*_): + return None + + data_regression.check(str(err.value)) + + +@dataclass +class User: + id: int + name: str + summary: str + + +@dataclass +class Comment: + id: int + message: str + summary: str + + +class ResultInterface(InterfaceType): + __schema__ = """ + interface Result { + summary: String! + score: Int! + } + """ + + @staticmethod + def resolve_type(instance, *_): + if isinstance(instance, Comment): + return "Comment" + + if isinstance(instance, User): + return "User" + + return None + + @staticmethod + def resolve_score(*_): + return 42 + + +class UserType(ObjectType): + __schema__ = """ + type User implements Result { + id: ID! + name: String! + summary: String! + score: Int! + } + """ + __requires__ = [ResultInterface] + + +class CommentType(ObjectType): + __schema__ = """ + type Comment implements Result { + id: ID! + message: String! + summary: String! + score: Int! + } + """ + __requires__ = [ResultInterface] + + @staticmethod + def resolve_score(*_): + return 16 + + +class QueryType(ObjectType): + __schema__ = """ + type Query { + results: [Result!]! + } + """ + __requires__ = [ResultInterface] + + @staticmethod + def resolve_results(*_): + return [ + User(id=1, name="Alice", summary="Summary for Alice"), + Comment(id=1, message="Hello world!", summary="Summary for comment"), + ] + + +schema = make_executable_schema(QueryType, UserType, CommentType) + + +def test_interface_type_binds_type_resolver(): + query = """ + query { + results { + ... on User { + __typename + id + name + summary + } + ... on Comment { + __typename + id + message + summary + } + } + } + """ + + result = graphql_sync(schema, query) + assert result.data == { + "results": [ + { + "__typename": "User", + "id": "1", + "name": "Alice", + "summary": "Summary for Alice", + }, + { + "__typename": "Comment", + "id": "1", + "message": "Hello world!", + "summary": "Summary for comment", + }, + ], + } + + +def test_interface_type_binds_field_resolvers_to_implementing_types_fields(): + query = """ + query { + results { + ... on User { + __typename + score + } + ... on Comment { + __typename + score + } + } + } + """ + + result = graphql_sync(schema, query) + assert result.data == { + "results": [ + { + "__typename": "User", + "score": 42, + }, + { + "__typename": "Comment", + "score": 16, + }, + ], + } diff --git a/tests/test_mutation_type.py b/tests_v1/test_mutation_type.py similarity index 100% rename from tests/test_mutation_type.py rename to tests_v1/test_mutation_type.py diff --git a/tests_v1/test_object_type.py b/tests_v1/test_object_type.py new file mode 100644 index 0000000..8adf0e6 --- /dev/null +++ b/tests_v1/test_object_type.py @@ -0,0 +1,410 @@ +import pytest +from ariadne import SchemaDirectiveVisitor +from graphql import GraphQLError, graphql_sync + +from ariadne_graphql_modules import ( + DeferredType, + DirectiveType, + InterfaceType, + ObjectType, + make_executable_schema, +) + + +def test_object_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): + with pytest.raises(AttributeError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + pass + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): + with pytest.raises(TypeError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = True + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_invalid_schema_str(data_regression): + with pytest.raises(GraphQLError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = "typo User" + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = "scalar DateTime" + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User + + type Group + """ + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_without_fields(data_regression): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = "type User" + + data_regression.check(str(err.value)) + + +def test_object_type_extracts_graphql_name(): + class GroupType(ObjectType): + __schema__ = """ + type Group { + id: ID! + } + """ + + assert GroupType.graphql_name == "Group" + + +def test_object_type_accepts_all_builtin_scalar_types(): + # pylint: disable=unused-variable + class FancyObjectType(ObjectType): + __schema__ = """ + type FancyObject { + id: ID! + someInt: Int! + someFloat: Float! + someBoolean: Boolean! + someString: String! + } + """ + + +def test_object_type_raises_error_when_defined_without_return_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + group: Group + groups: [Group!] + } + """ + + data_regression.check(str(err.value)) + + +def test_object_type_verifies_field_dependency(): + # pylint: disable=unused-variable + class GroupType(ObjectType): + __schema__ = """ + type Group { + id: ID! + } + """ + + class UserType(ObjectType): + __schema__ = """ + type User { + group: Group + groups: [Group!] + } + """ + __requires__ = [GroupType] + + +def test_object_type_verifies_circular_dependency(): + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + follows: User + } + """ + + +def test_object_type_raises_error_when_defined_without_argument_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + actions(input: UserInput): [String!]! + } + """ + + data_regression.check(str(err.value)) + + +def test_object_type_verifies_circular_dependency_using_deferred_type(): + # pylint: disable=unused-variable + class GroupType(ObjectType): + __schema__ = """ + type Group { + id: ID! + users: [User] + } + """ + __requires__ = [DeferredType("User")] + + class UserType(ObjectType): + __schema__ = """ + type User { + group: Group + } + """ + __requires__ = [GroupType] + + +def test_object_type_can_be_extended_with_new_fields(): + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + id: ID! + } + """ + + class ExtendUserType(ObjectType): + __schema__ = """ + extend type User { + name: String + } + """ + __requires__ = [UserType] + + +def test_object_type_can_be_extended_with_directive(): + # pylint: disable=unused-variable + class ExampleDirective(DirectiveType): + __schema__ = "directive @example on OBJECT" + __visitor__ = SchemaDirectiveVisitor + + class UserType(ObjectType): + __schema__ = """ + type User { + id: ID! + } + """ + + class ExtendUserType(ObjectType): + __schema__ = """ + extend type User @example + """ + __requires__ = [UserType, ExampleDirective] + + +def test_object_type_can_be_extended_with_interface(): + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Interface { + id: ID! + } + """ + + class UserType(ObjectType): + __schema__ = """ + type User { + id: ID! + } + """ + + class ExtendUserType(ObjectType): + __schema__ = """ + extend type User implements Interface + """ + __requires__ = [UserType, ExampleInterface] + + +def test_object_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExtendUserType(ObjectType): + __schema__ = """ + extend type User { + name: String + } + """ + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Example { + id: ID! + } + """ + + class ExampleType(ObjectType): + __schema__ = """ + extend type Example { + name: String + } + """ + __requires__ = [ExampleInterface] + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + name: String + } + """ + __aliases__ = { + "joinedDate": "joined_date", + } + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + name: String + } + """ + + @staticmethod + def resolve_group(*_): + return None + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + name: String + } + """ + __fields_args__ = {"group": {}} + + data_regression.check(str(err.value)) + + +def test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UserType(ObjectType): + __schema__ = """ + type User { + name: String + } + """ + __fields_args__ = {"name": {"arg": "arg2"}} + + data_regression.check(str(err.value)) + + +class QueryType(ObjectType): + __schema__ = """ + type Query { + field: String! + other: String! + firstField: String! + secondField: String! + fieldWithArg(someArg: String): String! + } + """ + __aliases__ = { + "firstField": "first_field", + "secondField": "second_field", + "fieldWithArg": "field_with_arg", + } + __fields_args__ = {"fieldWithArg": {"someArg": "some_arg"}} + + @staticmethod + def resolve_other(*_): + return "Word Up!" + + @staticmethod + def resolve_second_field(obj, *_): + return "Obj: %s" % obj["secondField"] + + @staticmethod + def resolve_field_with_arg(*_, some_arg): + return some_arg + + +schema = make_executable_schema(QueryType) + + +def test_object_resolves_field_with_default_resolver(): + result = graphql_sync(schema, "{ field }", root_value={"field": "Hello!"}) + assert result.data["field"] == "Hello!" + + +def test_object_resolves_field_with_custom_resolver(): + result = graphql_sync(schema, "{ other }") + assert result.data["other"] == "Word Up!" + + +def test_object_resolves_field_with_aliased_default_resolver(): + result = graphql_sync( + schema, "{ firstField }", root_value={"first_field": "Howdy?"} + ) + assert result.data["firstField"] == "Howdy?" + + +def test_object_resolves_field_with_aliased_custom_resolver(): + result = graphql_sync(schema, "{ secondField }", root_value={"secondField": "Hey!"}) + assert result.data["secondField"] == "Obj: Hey!" + + +def test_object_resolves_field_with_arg_out_name_customized(): + result = graphql_sync(schema, '{ fieldWithArg(someArg: "test") }') + assert result.data["fieldWithArg"] == "test" diff --git a/tests_v1/test_scalar_type.py b/tests_v1/test_scalar_type.py new file mode 100644 index 0000000..ec85340 --- /dev/null +++ b/tests_v1/test_scalar_type.py @@ -0,0 +1,287 @@ +from datetime import date, datetime + +import pytest +from ariadne import SchemaDirectiveVisitor +from graphql import GraphQLError, StringValueNode, graphql_sync + +from ariadne_graphql_modules import ( + DirectiveType, + ObjectType, + ScalarType, + make_executable_schema, +) + + +def test_scalar_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): + with pytest.raises(AttributeError) as err: + # pylint: disable=unused-variable + class DateScalar(ScalarType): + pass + + data_regression.check(str(err.value)) + + +def test_scalar_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): + with pytest.raises(TypeError) as err: + # pylint: disable=unused-variable + class DateScalar(ScalarType): + __schema__ = True + + data_regression.check(str(err.value)) + + +def test_scalar_type_raises_error_when_defined_with_invalid_schema_str(data_regression): + with pytest.raises(GraphQLError) as err: + # pylint: disable=unused-variable + class DateScalar(ScalarType): + __schema__ = "scalor Date" + + data_regression.check(str(err.value)) + + +def test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class DateScalar(ScalarType): + __schema__ = "type DateTime" + + data_regression.check(str(err.value)) + + +def test_scalar_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class DateScalar(ScalarType): + __schema__ = """ + scalar Date + + scalar DateTime + """ + + data_regression.check(str(err.value)) + + +def test_scalar_type_extracts_graphql_name(): + class DateScalar(ScalarType): + __schema__ = "scalar Date" + + assert DateScalar.graphql_name == "Date" + + +def test_scalar_type_can_be_extended_with_directive(): + # pylint: disable=unused-variable + class ExampleDirective(DirectiveType): + __schema__ = "directive @example on SCALAR" + __visitor__ = SchemaDirectiveVisitor + + class DateScalar(ScalarType): + __schema__ = "scalar Date" + + class ExtendDateScalar(ScalarType): + __schema__ = "extend scalar Date @example" + __requires__ = [DateScalar, ExampleDirective] + + +class DateReadOnlyScalar(ScalarType): + __schema__ = "scalar DateReadOnly" + + @staticmethod + def serialize(date): + return date.strftime("%Y-%m-%d") + + +class DateInputScalar(ScalarType): + __schema__ = "scalar DateInput" + + @staticmethod + def parse_value(formatted_date): + parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") + return parsed_datetime.date() + + @staticmethod + def parse_literal(ast, variable_values=None): # pylint: disable=unused-argument + if not isinstance(ast, StringValueNode): + raise ValueError() + + formatted_date = ast.value + parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") + return parsed_datetime.date() + + +class DefaultParserScalar(ScalarType): + __schema__ = "scalar DefaultParser" + + @staticmethod + def parse_value(value): + return type(value).__name__ + + +TEST_DATE = date(2006, 9, 13) +TEST_DATE_SERIALIZED = TEST_DATE.strftime("%Y-%m-%d") + + +class QueryType(ObjectType): + __schema__ = """ + type Query { + testSerialize: DateReadOnly! + testInput(value: DateInput!): Boolean! + testInputValueType(value: DefaultParser!): String! + } + """ + __requires__ = [ + DateReadOnlyScalar, + DateInputScalar, + DefaultParserScalar, + ] + __aliases__ = { + "testSerialize": "test_serialize", + "testInput": "test_input", + "testInputValueType": "test_input_value_type", + } + + @staticmethod + def resolve_test_serialize(*_): + return TEST_DATE + + @staticmethod + def resolve_test_input(*_, value): + assert value == TEST_DATE + return True + + @staticmethod + def resolve_test_input_value_type(*_, value): + return value + + +schema = make_executable_schema(QueryType) + + +def test_attempt_deserialize_str_literal_without_valid_date_raises_error(): + test_input = "invalid string" + result = graphql_sync(schema, '{ testInput(value: "%s") }' % test_input) + assert result.errors is not None + assert str(result.errors[0]).splitlines()[:1] == [ + "Expected value of type 'DateInput!', found \"invalid string\"; " + "time data 'invalid string' does not match format '%Y-%m-%d'" + ] + + +def test_attempt_deserialize_wrong_type_literal_raises_error(): + test_input = 123 + result = graphql_sync(schema, "{ testInput(value: %s) }" % test_input) + assert result.errors is not None + assert str(result.errors[0]).splitlines()[:1] == [ + "Expected value of type 'DateInput!', found 123; " + ] + + +def test_default_literal_parser_is_used_to_extract_value_str_from_ast_node(): + class ValueParserOnlyScalar(ScalarType): + __schema__ = "scalar DateInput" + + @staticmethod + def parse_value(formatted_date): + parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") + return parsed_datetime.date() + + class ValueParserOnlyQueryType(ObjectType): + __schema__ = """ + type Query { + parse(value: DateInput!): String! + } + """ + __requires__ = [ValueParserOnlyScalar] + + @staticmethod + def resolve_parse(*_, value): + return value + + schema = make_executable_schema(ValueParserOnlyQueryType) + result = graphql_sync(schema, """{ parse(value: "%s") }""" % TEST_DATE_SERIALIZED) + assert result.errors is None + assert result.data == {"parse": "2006-09-13"} + + +parametrized_query = """ + query parseValueTest($value: DateInput!) { + testInput(value: $value) + } +""" + + +def test_variable_with_valid_date_string_is_deserialized_to_python_date(): + variables = {"value": TEST_DATE_SERIALIZED} + result = graphql_sync(schema, parametrized_query, variable_values=variables) + assert result.errors is None + assert result.data == {"testInput": True} + + +def test_attempt_deserialize_str_variable_without_valid_date_raises_error(): + variables = {"value": "invalid string"} + result = graphql_sync(schema, parametrized_query, variable_values=variables) + assert result.errors is not None + assert str(result.errors[0]).splitlines()[:1] == [ + "Variable '$value' got invalid value 'invalid string'; " + "Expected type 'DateInput'. " + "time data 'invalid string' does not match format '%Y-%m-%d'" + ] + + +def test_attempt_deserialize_wrong_type_variable_raises_error(): + variables = {"value": 123} + result = graphql_sync(schema, parametrized_query, variable_values=variables) + assert result.errors is not None + assert str(result.errors[0]).splitlines()[:1] == [ + "Variable '$value' got invalid value 123; Expected type 'DateInput'. " + "strptime() argument 1 must be str, not int" + ] + + +def test_literal_string_is_deserialized_by_default_parser(): + result = graphql_sync(schema, '{ testInputValueType(value: "test") }') + assert result.errors is None + assert result.data == {"testInputValueType": "str"} + + +def test_literal_int_is_deserialized_by_default_parser(): + result = graphql_sync(schema, "{ testInputValueType(value: 123) }") + assert result.errors is None + assert result.data == {"testInputValueType": "int"} + + +def test_literal_float_is_deserialized_by_default_parser(): + result = graphql_sync(schema, "{ testInputValueType(value: 1.5) }") + assert result.errors is None + assert result.data == {"testInputValueType": "float"} + + +def test_literal_bool_true_is_deserialized_by_default_parser(): + result = graphql_sync(schema, "{ testInputValueType(value: true) }") + assert result.errors is None + assert result.data == {"testInputValueType": "bool"} + + +def test_literal_bool_false_is_deserialized_by_default_parser(): + result = graphql_sync(schema, "{ testInputValueType(value: false) }") + assert result.errors is None + assert result.data == {"testInputValueType": "bool"} + + +def test_literal_object_is_deserialized_by_default_parser(): + result = graphql_sync(schema, "{ testInputValueType(value: {}) }") + assert result.errors is None + assert result.data == {"testInputValueType": "dict"} + + +def test_literal_list_is_deserialized_by_default_parser(): + result = graphql_sync(schema, "{ testInputValueType(value: []) }") + assert result.errors is None + assert result.data == {"testInputValueType": "list"} diff --git a/tests_v1/test_subscription_type.py b/tests_v1/test_subscription_type.py new file mode 100644 index 0000000..c0067cf --- /dev/null +++ b/tests_v1/test_subscription_type.py @@ -0,0 +1,325 @@ +import pytest +from ariadne import SchemaDirectiveVisitor +from graphql import GraphQLError, build_schema + +from ariadne_graphql_modules import ( + DirectiveType, + InterfaceType, + ObjectType, + SubscriptionType, +) + + +def test_subscription_type_raises_attribute_error_when_defined_without_schema( + data_regression, +): + with pytest.raises(AttributeError) as err: + # pylint: disable=unused-variable + class UsersSubscription(SubscriptionType): + pass + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_with_invalid_schema_type( + data_regression, +): + with pytest.raises(TypeError) as err: + # pylint: disable=unused-variable + class UsersSubscription(SubscriptionType): + __schema__ = True + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_with_invalid_schema_str( + data_regression, +): + with pytest.raises(GraphQLError) as err: + # pylint: disable=unused-variable + class UsersSubscription(SubscriptionType): + __schema__ = "typo Subscription" + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UsersSubscription(SubscriptionType): + __schema__ = "scalar Subscription" + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UsersSubscription(SubscriptionType): + __schema__ = "type Other" + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_without_fields(data_regression): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class UsersSubscription(SubscriptionType): + __schema__ = "type Subscription" + + data_regression.check(str(err.value)) + + +def test_subscription_type_extracts_graphql_name(): + class UsersSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + thread: ID! + } + """ + + assert UsersSubscription.graphql_name == "Subscription" + + +def test_subscription_type_raises_error_when_defined_without_return_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: Chat + Chats: [Chat!] + } + """ + + data_regression.check(str(err.value)) + + +def test_subscription_type_verifies_field_dependency(): + # pylint: disable=unused-variable + class ChatType(ObjectType): + __schema__ = """ + type Chat { + id: ID! + } + """ + + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: Chat + Chats: [Chat!] + } + """ + __requires__ = [ChatType] + + +def test_subscription_type_raises_error_when_defined_without_argument_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat(input: ChannelInput): [String!]! + } + """ + + data_regression.check(str(err.value)) + + +def test_subscription_type_can_be_extended_with_new_fields(): + # pylint: disable=unused-variable + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + + class ExtendChatSubscription(SubscriptionType): + __schema__ = """ + extend type Subscription { + thread: ID! + } + """ + __requires__ = [ChatSubscription] + + +def test_subscription_type_can_be_extended_with_directive(): + # pylint: disable=unused-variable + class ExampleDirective(DirectiveType): + __schema__ = "directive @example on OBJECT" + __visitor__ = SchemaDirectiveVisitor + + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + + class ExtendChatSubscription(SubscriptionType): + __schema__ = "extend type Subscription @example" + __requires__ = [ChatSubscription, ExampleDirective] + + +def test_subscription_type_can_be_extended_with_interface(): + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Interface { + threads: ID! + } + """ + + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + + class ExtendChatSubscription(SubscriptionType): + __schema__ = """ + extend type Subscription implements Interface { + threads: ID! + } + """ + __requires__ = [ChatSubscription, ExampleInterface] + + +def test_subscription_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExtendChatSubscription(SubscriptionType): + __schema__ = """ + extend type Subscription { + thread: ID! + } + """ + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleInterface(InterfaceType): + __schema__ = """ + interface Subscription { + id: ID! + } + """ + + class ExtendChatSubscription(SubscriptionType): + __schema__ = """ + extend type Subscription { + thread: ID! + } + """ + __requires__ = [ExampleInterface] + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + __aliases__ = { + "userAlerts": "user_alerts", + } + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + + @staticmethod + def resolve_group(*_): + return None + + data_regression.check(str(err.value)) + + +def test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + + @staticmethod + def subscribe_group(*_): + return None + + data_regression.check(str(err.value)) + + +def test_subscription_type_binds_resolver_and_subscriber_to_schema(): + schema = build_schema( + """ + type Query { + hello: String + } + + type Subscription { + chat: ID! + } + """ + ) + + class ChatSubscription(SubscriptionType): + __schema__ = """ + type Subscription { + chat: ID! + } + """ + + @staticmethod + def resolve_chat(*_): + return None + + @staticmethod + def subscribe_chat(*_): + return None + + ChatSubscription.__bind_to_schema__(schema) + + field = schema.type_map.get("Subscription").fields["chat"] + assert field.resolve is ChatSubscription.resolve_chat + assert field.subscribe is ChatSubscription.subscribe_chat diff --git a/tests_v1/test_union_type.py b/tests_v1/test_union_type.py new file mode 100644 index 0000000..61c8a3a --- /dev/null +++ b/tests_v1/test_union_type.py @@ -0,0 +1,251 @@ +from dataclasses import dataclass + +import pytest +from ariadne import SchemaDirectiveVisitor +from graphql import GraphQLError, graphql_sync + +from ariadne_graphql_modules import ( + DirectiveType, + ObjectType, + UnionType, + make_executable_schema, +) + + +def test_union_type_raises_attribute_error_when_defined_without_schema(data_regression): + with pytest.raises(AttributeError) as err: + # pylint: disable=unused-variable + class ExampleUnion(UnionType): + pass + + data_regression.check(str(err.value)) + + +def test_union_type_raises_error_when_defined_with_invalid_schema_type(data_regression): + with pytest.raises(TypeError) as err: + # pylint: disable=unused-variable + class ExampleUnion(UnionType): + __schema__ = True + + data_regression.check(str(err.value)) + + +def test_union_type_raises_error_when_defined_with_invalid_schema_str(data_regression): + with pytest.raises(GraphQLError) as err: + # pylint: disable=unused-variable + class ExampleUnion(UnionType): + __schema__ = "unien Example = A | B" + + data_regression.check(str(err.value)) + + +def test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleUnion(UnionType): + __schema__ = "scalar DateTime" + + data_regression.check(str(err.value)) + + +def test_union_type_raises_error_when_defined_with_multiple_types_schema( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleUnion(UnionType): + __schema__ = """ + union A = C | D + + union B = C | D + """ + + data_regression.check(str(err.value)) + + +@dataclass +class User: + id: int + name: str + + +@dataclass +class Comment: + id: int + message: str + + +class UserType(ObjectType): + __schema__ = """ + type User { + id: ID! + name: String! + } + """ + + +class CommentType(ObjectType): + __schema__ = """ + type Comment { + id: ID! + message: String! + } + """ + + +class ResultUnion(UnionType): + __schema__ = "union Result = Comment | User" + __requires__ = [CommentType, UserType] + + @staticmethod + def resolve_type(instance, *_): + if isinstance(instance, Comment): + return "Comment" + + if isinstance(instance, User): + return "User" + + return None + + +class QueryType(ObjectType): + __schema__ = """ + type Query { + results: [Result!]! + } + """ + __requires__ = [ResultUnion] + + @staticmethod + def resolve_results(*_): + return [ + User(id=1, name="Alice"), + Comment(id=1, message="Hello world!"), + ] + + +schema = make_executable_schema(QueryType, UserType, CommentType) + + +def test_union_type_extracts_graphql_name(): + class ExampleUnion(UnionType): + __schema__ = "union Example = User | Comment" + __requires__ = [UserType, CommentType] + + assert ExampleUnion.graphql_name == "Example" + + +def test_union_type_raises_error_when_defined_without_member_type_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleUnion(UnionType): + __schema__ = "union Example = User | Comment" + __requires__ = [UserType] + + data_regression.check(str(err.value)) + + +def test_interface_type_binds_type_resolver(): + query = """ + query { + results { + ... on User { + __typename + id + name + } + ... on Comment { + __typename + id + message + } + } + } + """ + + result = graphql_sync(schema, query) + assert result.data == { + "results": [ + { + "__typename": "User", + "id": "1", + "name": "Alice", + }, + { + "__typename": "Comment", + "id": "1", + "message": "Hello world!", + }, + ], + } + + +def test_union_type_can_be_extended_with_new_types(): + # pylint: disable=unused-variable + class ExampleUnion(UnionType): + __schema__ = "union Result = User | Comment" + __requires__ = [UserType, CommentType] + + class ThreadType(ObjectType): + __schema__ = """ + type Thread { + id: ID! + title: String! + } + """ + + class ExtendExampleUnion(UnionType): + __schema__ = "union Result = Thread" + __requires__ = [ExampleUnion, ThreadType] + + +def test_union_type_can_be_extended_with_directive(): + # pylint: disable=unused-variable + class ExampleDirective(DirectiveType): + __schema__ = "directive @example on UNION" + __visitor__ = SchemaDirectiveVisitor + + class ExampleUnion(UnionType): + __schema__ = "union Result = User | Comment" + __requires__ = [UserType, CommentType] + + class ExtendExampleUnion(UnionType): + __schema__ = """ + extend union Result @example + """ + __requires__ = [ExampleUnion, ExampleDirective] + + +def test_union_type_raises_error_when_defined_without_extended_dependency( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExtendExampleUnion(UnionType): + __schema__ = "extend union Result = User" + __requires__ = [UserType] + + data_regression.check(str(err.value)) + + +def test_union_type_raises_error_when_extended_dependency_is_wrong_type( + data_regression, +): + with pytest.raises(ValueError) as err: + # pylint: disable=unused-variable + class ExampleType(ObjectType): + __schema__ = """ + type Example { + id: ID! + } + """ + + class ExtendExampleUnion(UnionType): + __schema__ = "extend union Example = User" + __requires__ = [ExampleType, UserType] + + data_regression.check(str(err.value)) From bd939b7f01976013e872cb3ab88d48503fd281bd Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 22 Aug 2024 15:15:36 +0200 Subject: [PATCH 47/63] fix v1 tests --- ...ription_in_source_with_schema.obtained.yml | 3 -- ...th_name_in_source_with_schema.obtained.yml | 3 -- ...th_type_in_source_with_schema.obtained.yml | 3 -- ...ror_for_invalid_relative_path.obtained.yml | 1 - ...iption_not_str_without_schema.obtained.yml | 2 - ...or_type_with_two_descriptions.obtained.yml | 3 -- ...ion_fails_for_invalid_members.obtained.yml | 2 - ...ion_fails_for_missing_members.obtained.yml | 3 -- ...d_name_not_str_without_schema.obtained.yml | 2 - ...h_invalid_attrs_raising_error.obtained.yml | 2 - ..._for_out_names_without_schema.obtained.yml | 3 -- ..._for_unsupported_attr_default.obtained.yml | 3 -- ...upported_field_default_option.obtained.yml | 3 -- ..._no_interface_in_schema.obtained.diff.html | 51 ------------------ ...erface_no_interface_in_schema.obtained.yml | 2 - ...ce_with_different_types.obtained.diff.html | 54 ------------------- ...nterface_with_different_types.obtained.yml | 2 - .../test_interface_with_different_types.yml | 5 -- ...rg_name_in_source_with_schema.obtained.yml | 3 -- ...r_if_called_without_any_types.obtained.yml | 1 - ...ey_error_for_unset_data.obtained.diff.html | 50 ----------------- ...ises_key_error_for_unset_data.obtained.yml | 1 - .../test_missing_type_in_schema.obtained.yml | 2 - .../test_missing_type_in_types.obtained.yml | 2 - ...ptions_for_source_with_schema.obtained.yml | 2 - ...merge_roots_is_disabled.obtained.diff.html | 52 ------------------ ...on_if_merge_roots_is_disabled.obtained.yml | 3 -- ...sourced_for_field_with_schema.obtained.yml | 2 - ...ltiple_sources_without_schema.obtained.yml | 2 - ..._name_and_definition_mismatch.obtained.yml | 3 -- ...h_invalid_attrs_raising_error.obtained.yml | 2 - ...tion_fails_for_alias_resolver.obtained.yml | 3 -- ...ils_for_alias_target_resolver.obtained.yml | 2 - ..._field_with_same_graphql_name.obtained.yml | 2 - ..._for_field_with_multiple_args.obtained.yml | 2 - ...ld_with_multiple_descriptions.obtained.yml | 2 - ...ation_fails_for_invalid_alias.obtained.yml | 2 - ...or_missing_field_resolver_arg.obtained.yml | 2 - ...ails_for_missing_resolver_arg.obtained.yml | 3 -- ..._attrs_with_same_graphql_name.obtained.yml | 2 - ..._for_multiple_field_resolvers.obtained.yml | 2 - ...fields_with_same_graphql_name.obtained.yml | 2 - ...s_for_undefined_attr_resolver.obtained.yml | 2 - ..._undefined_field_resolver_arg.obtained.yml | 3 -- ...ls_for_undefined_resolver_arg.obtained.yml | 4 -- ...upported_resolver_arg_default.obtained.yml | 3 -- ...d_resolver_arg_default_option.obtained.yml | 3 -- ...plicated_members_descriptions.obtained.yml | 2 - ...lidation_fails_for_empty_enum.obtained.yml | 2 - ..._invalid_members_descriptions.obtained.yml | 2 - ...fails_for_invalid_type_schema.obtained.yml | 3 -- ..._fails_for_names_not_matching.obtained.yml | 3 -- ...ema_and_members_dict_mismatch.obtained.yml | 2 - ...ema_and_members_enum_mismatch.obtained.yml | 2 - ...s_for_schema_and_members_list.obtained.yml | 3 -- ...on_fails_for_two_descriptions.obtained.yml | 2 - ...h_invalid_attrs_raising_error.obtained.yml | 2 - ..._fails_for_duplicate_out_name.obtained.yml | 3 -- ...on_fails_for_invalid_out_name.obtained.yml | 3 -- ...fails_for_invalid_type_schema.obtained.yml | 3 -- ..._fails_for_names_not_matching.obtained.yml | 3 -- ...ils_for_schema_missing_fields.obtained.yml | 2 - ...on_fails_for_two_descriptions.obtained.yml | 3 -- ...h_invalid_attrs_raising_error.obtained.yml | 2 - ...tion_fails_for_alias_resolver.obtained.yml | 3 -- ...ils_for_alias_target_resolver.obtained.yml | 2 - ...r_arg_with_double_description.obtained.yml | 3 -- ...ails_for_arg_with_name_option.obtained.yml | 3 -- ...ails_for_arg_with_type_option.obtained.yml | 3 -- ...tion_fails_for_field_instance.obtained.yml | 3 -- ...r_field_with_invalid_arg_name.obtained.yml | 3 -- ...ld_with_multiple_descriptions.obtained.yml | 2 - ...ation_fails_for_invalid_alias.obtained.yml | 2 - ...fails_for_invalid_type_schema.obtained.yml | 3 -- ...tion_fails_for_missing_fields.obtained.yml | 2 - ..._for_multiple_field_resolvers.obtained.yml | 2 - ..._fails_for_names_not_matching.obtained.yml | 3 -- ...on_fails_for_two_descriptions.obtained.yml | 3 -- ..._for_undefined_field_resolver.obtained.yml | 2 - ...upported_resolver_arg_default.obtained.yml | 3 -- ...d_resolver_arg_option_default.obtained.yml | 3 -- ...ion_fails_for_different_names.obtained.yml | 3 -- ...fails_for_invalid_type_schema.obtained.yml | 3 -- ...on_fails_for_two_descriptions.obtained.yml | 3 -- ...ils_if_lazy_type_doesnt_exist.obtained.yml | 2 - ...d_arg_not_dict_without_schema.obtained.yml | 3 -- ..._args_not_dict_without_schema.obtained.yml | 2 - ...r_undefined_field_with_schema.obtained.yml | 2 - ...undefined_name_without_schema.obtained.yml | 2 - ..._include_members_are_combined.obtained.yml | 1 - ...tion_is_set_for_excluded_item.obtained.yml | 3 -- ...ption_is_set_for_missing_item.obtained.yml | 3 -- ...ption_is_set_for_omitted_item.obtained.yml | 3 -- ...a_str_contains_multiple_types.obtained.yml | 2 - ...schema_str_has_invalid_syntax.obtained.yml | 2 - ...r_when_schema_type_is_invalid.obtained.yml | 1 - ...r_when_defined_without_schema.obtained.yml | 2 - ..._when_defined_without_visitor.obtained.yml | 2 - ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...fined_with_invalid_schema_str.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...ed_with_multiple_types_schema.obtained.yml | 2 - ...r_when_defined_without_schema.obtained.yml | 2 - ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...fined_with_invalid_schema_str.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...ed_with_multiple_types_schema.obtained.yml | 2 - ...extra_items_not_in_definition.obtained.yml | 2 - ..._misses_items_from_definition.obtained.yml | 2 - ...extra_items_not_in_definition.obtained.yml | 2 - ..._misses_items_from_definition.obtained.yml | 2 - ...erged_types_define_same_field.obtained.yml | 1 - ...r_when_defined_without_schema.obtained.yml | 2 - ...rgs_map_for_nonexisting_field.obtained.yml | 1 - ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...fined_with_invalid_schema_str.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...ed_with_multiple_types_schema.obtained.yml | 2 - ...d_without_extended_dependency.obtained.yml | 3 -- ...without_field_type_dependency.obtained.yml | 2 - ...r_when_defined_without_fields.obtained.yml | 2 - ...nded_dependency_is_wrong_type.obtained.yml | 3 -- ...r_when_defined_without_schema.obtained.yml | 2 - ...h_alias_for_nonexisting_field.obtained.yml | 1 - ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...fined_with_invalid_schema_str.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...ed_with_multiple_types_schema.obtained.yml | 2 - ...esolver_for_nonexisting_field.obtained.yml | 2 - ...hout_argument_type_dependency.obtained.yml | 3 -- ...d_without_extended_dependency.obtained.yml | 2 - ...r_when_defined_without_fields.obtained.yml | 3 -- ...ithout_return_type_dependency.obtained.yml | 3 -- ...nded_dependency_is_wrong_type.obtained.yml | 3 -- ...r_when_defined_without_schema.obtained.yml | 2 - ...fined_for_different_type_name.obtained.yml | 3 -- ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ..._defined_with_multiple_fields.obtained.yml | 3 -- ...ed_with_multiple_types_schema.obtained.yml | 2 - ...defined_with_nonexistant_args.obtained.yml | 2 - ...allable_resolve_mutation_attr.obtained.yml | 3 -- ...r_when_defined_without_fields.obtained.yml | 3 -- ...without_resolve_mutation_attr.obtained.yml | 2 - ...ithout_return_type_dependency.obtained.yml | 3 -- ...r_when_defined_without_schema.obtained.yml | 2 - ...h_alias_for_nonexisting_field.obtained.yml | 1 - ...ield_args_for_nonexisting_arg.obtained.yml | 1 - ...ld_args_for_nonexisting_field.obtained.yml | 2 - ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...ed_with_multiple_types_schema.obtained.yml | 2 - ...esolver_for_nonexisting_field.obtained.yml | 1 - ...hout_argument_type_dependency.obtained.yml | 3 -- ...d_without_extended_dependency.obtained.yml | 3 -- ...r_when_defined_without_fields.obtained.yml | 2 - ...ithout_return_type_dependency.obtained.yml | 2 - ...nded_dependency_is_wrong_type.obtained.yml | 2 - ...r_when_defined_without_schema.obtained.yml | 2 - ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...fined_with_invalid_schema_str.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...ed_with_multiple_types_schema.obtained.yml | 2 - ...r_when_defined_without_schema.obtained.yml | 2 - ...h_alias_for_nonexisting_field.obtained.yml | 1 - ...ith_invalid_graphql_type_name.obtained.yml | 3 -- ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...fined_with_invalid_schema_str.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...esolver_for_nonexisting_field.obtained.yml | 2 - ...ith_sub_for_nonexisting_field.obtained.yml | 2 - ...hout_argument_type_dependency.obtained.yml | 3 -- ...d_without_extended_dependency.obtained.yml | 3 -- ...r_when_defined_without_fields.obtained.yml | 3 -- ...ithout_return_type_dependency.obtained.yml | 3 -- ...nded_dependency_is_wrong_type.obtained.yml | 3 -- ...r_when_defined_without_schema.obtained.yml | 2 - ...h_invalid_graphql_type_schema.obtained.yml | 2 - ...fined_with_invalid_schema_str.obtained.yml | 2 - ...ined_with_invalid_schema_type.obtained.yml | 1 - ...ed_with_multiple_types_schema.obtained.yml | 2 - ...d_without_extended_dependency.obtained.yml | 3 -- ...ithout_member_type_dependency.obtained.yml | 3 -- ...nded_dependency_is_wrong_type.obtained.yml | 3 -- tests_v1/test_collection_type.py | 2 +- tests_v1/test_convert_case.py | 2 +- tests_v1/test_definition_parser.py | 2 +- tests_v1/test_directive_type.py | 2 +- tests_v1/test_enum_type.py | 2 +- tests_v1/test_executable_schema.py | 2 +- tests_v1/test_executable_schema_compat.py | 2 +- tests_v1/test_input_type.py | 2 +- tests_v1/test_interface_type.py | 2 +- tests_v1/test_mutation_type.py | 2 +- tests_v1/test_object_type.py | 2 +- tests_v1/test_scalar_type.py | 2 +- tests_v1/test_subscription_type.py | 2 +- tests_v1/test_union_type.py | 2 +- 198 files changed, 14 insertions(+), 629 deletions(-) delete mode 100644 tests/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml delete mode 100644 tests/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml delete mode 100644 tests/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml delete mode 100644 tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml delete mode 100644 tests/snapshots/test_description_not_str_without_schema.obtained.yml delete mode 100644 tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml delete mode 100644 tests/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml delete mode 100644 tests/snapshots/test_field_name_not_str_without_schema.obtained.yml delete mode 100644 tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml delete mode 100644 tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml delete mode 100644 tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml delete mode 100644 tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml delete mode 100644 tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html delete mode 100644 tests/snapshots/test_interface_no_interface_in_schema.obtained.yml delete mode 100644 tests/snapshots/test_interface_with_different_types.obtained.diff.html delete mode 100644 tests/snapshots/test_interface_with_different_types.obtained.yml delete mode 100644 tests/snapshots/test_interface_with_different_types.yml delete mode 100644 tests/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml delete mode 100644 tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml delete mode 100644 tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html delete mode 100644 tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml delete mode 100644 tests/snapshots/test_missing_type_in_schema.obtained.yml delete mode 100644 tests/snapshots/test_missing_type_in_types.obtained.yml delete mode 100644 tests/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml delete mode 100644 tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html delete mode 100644 tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml delete mode 100644 tests/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml delete mode 100644 tests/snapshots/test_multiple_sources_without_schema.obtained.yml delete mode 100644 tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml delete mode 100644 tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml delete mode 100644 tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml delete mode 100644 tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml delete mode 100644 tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml delete mode 100644 tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml delete mode 100644 tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml delete mode 100644 tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml delete mode 100644 tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml delete mode 100644 tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml delete mode 100644 tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml delete mode 100644 tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml delete mode 100644 tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml delete mode 100644 tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml delete mode 100644 tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml delete mode 100644 tests/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml delete mode 100644 tests/snapshots/test_source_args_not_dict_without_schema.obtained.yml delete mode 100644 tests/snapshots/test_source_for_undefined_field_with_schema.obtained.yml delete mode 100644 tests/snapshots/test_undefined_name_without_schema.obtained.yml delete mode 100644 tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml delete mode 100644 tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml delete mode 100644 tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml delete mode 100644 tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml delete mode 100644 tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml delete mode 100644 tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml delete mode 100644 tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml delete mode 100644 tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml delete mode 100644 tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml delete mode 100644 tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml delete mode 100644 tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml delete mode 100644 tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml delete mode 100644 tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml delete mode 100644 tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml delete mode 100644 tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml delete mode 100644 tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml diff --git a/tests/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml b/tests/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml deleted file mode 100644 index 37ea424..0000000 --- a/tests/snapshots/test_arg_with_description_in_source_with_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' - field. This is not supported for types defining '__schema__'. -... diff --git a/tests/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml b/tests/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml deleted file mode 100644 index 5e93ffd..0000000 --- a/tests/snapshots/test_arg_with_name_in_source_with_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'SubscriptionType' defines 'name' option for 'channel' argument of the 'messageAdded' - field. This is not supported for types defining '__schema__'. -... diff --git a/tests/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml b/tests/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml deleted file mode 100644 index 37ea424..0000000 --- a/tests/snapshots/test_arg_with_type_in_source_with_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'SubscriptionType' defines 'type' option for 'channel' argument of the 'messageAdded' - field. This is not supported for types defining '__schema__'. -... diff --git a/tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml b/tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml deleted file mode 100644 index 82cb498..0000000 --- a/tests/snapshots/test_deferred_raises_error_for_invalid_relative_path.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'''...types'' points outside of the ''lorem'' package.' diff --git a/tests/snapshots/test_description_not_str_without_schema.obtained.yml b/tests/snapshots/test_description_not_str_without_schema.obtained.yml deleted file mode 100644 index b43461c..0000000 --- a/tests/snapshots/test_description_not_str_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -The description for message_added_generator must be a string if provided. -... diff --git a/tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml b/tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml deleted file mode 100644 index ba71cfb..0000000 --- a/tests/snapshots/test_description_validator_raises_error_for_type_with_two_descriptions.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines description in both '__description__' and '__schema__' - attributes. -... diff --git a/tests/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml b/tests/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml deleted file mode 100644 index f905b16..0000000 --- a/tests/snapshots/test_enum_type_validation_fails_for_invalid_members.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''UserLevel'' ''__members__'' attribute is of unsupported type. Expected ''Dict[str, - Any]'', ''Type[Enum]'' or List[str]. (found: '''')' diff --git a/tests/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml b/tests/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml deleted file mode 100644 index 1be2153..0000000 --- a/tests/snapshots/test_enum_type_validation_fails_for_missing_members.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'UserLevel' '__members__' attribute is either missing or empty. Either define - it or provide full SDL for this enum using the '__schema__' attribute. -... diff --git a/tests/snapshots/test_field_name_not_str_without_schema.obtained.yml b/tests/snapshots/test_field_name_not_str_without_schema.obtained.yml deleted file mode 100644 index 17a44f2..0000000 --- a/tests/snapshots/test_field_name_not_str_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -The field name for message_added_generator must be a string. -... diff --git a/tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml deleted file mode 100644 index c51434c..0000000 --- a/tests/snapshots/test_input_type_instance_with_invalid_attrs_raising_error.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'SearchInput.__init__() got an unexpected keyword argument ''invalid''. Valid keyword - arguments: ''query'', ''age''' diff --git a/tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml b/tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml deleted file mode 100644 index 2eeb09c..0000000 --- a/tests/snapshots/test_input_type_validation_fails_for_out_names_without_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines '__out_names__' attribute. This is not supported for types - not defining '__schema__'. -... diff --git a/tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml b/tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml deleted file mode 100644 index 87d2d4e..0000000 --- a/tests/snapshots/test_input_type_validation_fails_for_unsupported_attr_default.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'QueryType' defines default value for the 'attr' field that can't be represented - in GraphQL schema. -... diff --git a/tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml b/tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml deleted file mode 100644 index 87d2d4e..0000000 --- a/tests/snapshots/test_input_type_validation_fails_for_unsupported_field_default_option.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'QueryType' defines default value for the 'attr' field that can't be represented - in GraphQL schema. -... diff --git a/tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html b/tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html deleted file mode 100644 index c6a19d9..0000000 --- a/tests/snapshots/test_interface_no_interface_in_schema.obtained.diff.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_no_interface_in_schema.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_no_interface_in_schema.obtained.yml
t1Unknown type 'BaseInterface'.t1Query root type must be provided.
2...2...
- - - - -
Legends
- - - - -
Colors
 Added 
Changed
Deleted
- - - - -
Links
(f)irst change
(n)ext change
(t)op
- - - \ No newline at end of file diff --git a/tests/snapshots/test_interface_no_interface_in_schema.obtained.yml b/tests/snapshots/test_interface_no_interface_in_schema.obtained.yml deleted file mode 100644 index 63f9405..0000000 --- a/tests/snapshots/test_interface_no_interface_in_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Unknown type 'BaseInterface'. -... diff --git a/tests/snapshots/test_interface_with_different_types.obtained.diff.html b/tests/snapshots/test_interface_with_different_types.obtained.diff.html deleted file mode 100644 index ef57772..0000000 --- a/tests/snapshots/test_interface_with_different_types.obtained.diff.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - -

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_with_different_types.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests_next/snapshots/test_interface_with_different_types.obtained.yml
t1'Query root type must be provided.t1Query root type must be provided.
2 2...
3 
4  Interface field UserInterface.score expects type String! but User.score is type
5  Int!.'
- - - - -
Legends
- - - - -
Colors
 Added 
Changed
Deleted
- - - - -
Links
(f)irst change
(n)ext change
(t)op
- - - \ No newline at end of file diff --git a/tests/snapshots/test_interface_with_different_types.obtained.yml b/tests/snapshots/test_interface_with_different_types.obtained.yml deleted file mode 100644 index 322fe60..0000000 --- a/tests/snapshots/test_interface_with_different_types.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Query root type must be provided. -... diff --git a/tests/snapshots/test_interface_with_different_types.yml b/tests/snapshots/test_interface_with_different_types.yml deleted file mode 100644 index f10dac9..0000000 --- a/tests/snapshots/test_interface_with_different_types.yml +++ /dev/null @@ -1,5 +0,0 @@ -'Query root type must be provided. - - - Interface field UserInterface.score expects type String! but User.score is type - Int!.' diff --git a/tests/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml b/tests/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml deleted file mode 100644 index 78d9da1..0000000 --- a/tests/snapshots/test_invalid_arg_name_in_source_with_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'SubscriptionType' defines options for 'channelID' argument of the 'messageAdded' - field that doesn't exist. -... diff --git a/tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml b/tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml deleted file mode 100644 index e939646..0000000 --- a/tests/snapshots/test_make_executable_schema_raises_error_if_called_without_any_types.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'''make_executable_schema'' was called without any GraphQL types.' diff --git a/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html deleted file mode 100644 index 4929bd2..0000000 --- a/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.diff.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_metadata_raises_key_error_for_unset_data.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml
t1'"No data is set for ''<class ''tests_next.test_metadata.QueryType''>''."'t1'"No data is set for ''<class ''tests.test_metadata.QueryType''>''."'
- - - - -
Legends
- - - - -
Colors
 Added 
Changed
Deleted
- - - - -
Links
(f)irst change
(n)ext change
(t)op
- - - \ No newline at end of file diff --git a/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml b/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml deleted file mode 100644 index ef7ccb8..0000000 --- a/tests/snapshots/test_metadata_raises_key_error_for_unset_data.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'"No data is set for ''''."' diff --git a/tests/snapshots/test_missing_type_in_schema.obtained.yml b/tests/snapshots/test_missing_type_in_schema.obtained.yml deleted file mode 100644 index 8c91f42..0000000 --- a/tests/snapshots/test_missing_type_in_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Types 'Comment', 'Post' are in '__types__' but not in '__schema__'. -... diff --git a/tests/snapshots/test_missing_type_in_types.obtained.yml b/tests/snapshots/test_missing_type_in_types.obtained.yml deleted file mode 100644 index e504065..0000000 --- a/tests/snapshots/test_missing_type_in_types.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Types 'Comment' are in '__schema__' but not in '__types__'. -... diff --git a/tests/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml b/tests/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml deleted file mode 100644 index fb20bdf..0000000 --- a/tests/snapshots/test_multiple_descriptions_for_source_with_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'SubscriptionType' defines multiple descriptions for field 'messageAdded'. -... diff --git a/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html deleted file mode 100644 index 1ffb12e..0000000 --- a/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.diff.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -

/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.yml
/Users/d0cza/Projects/Ariadne/ariadne-graphql-modules/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml
t1Types 'SecondRoot' and '<class 'tests_next.test_make_executable_schema.test_multiple_roots_fail_validation_if_merge_roots_is_disabled.<locals>.FirstRoot'>'t1Types 'SecondRoot' and '<class 'tests.test_make_executable_schema.test_multiple_roots_fail_validation_if_merge_roots_is_disabled.<locals>.FirstRoot'>'
2  both define GraphQL type with name 'Query'.2  both define GraphQL type with name 'Query'.
3...3...
- - - - -
Legends
- - - - -
Colors
 Added 
Changed
Deleted
- - - - -
Links
(f)irst change
(n)ext change
(t)op
- - - \ No newline at end of file diff --git a/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml b/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml deleted file mode 100644 index c22028c..0000000 --- a/tests/snapshots/test_multiple_roots_fail_validation_if_merge_roots_is_disabled.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Types 'SecondRoot' and '.FirstRoot'>' - both define GraphQL type with name 'Query'. -... diff --git a/tests/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml b/tests/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml deleted file mode 100644 index 5fc64fd..0000000 --- a/tests/snapshots/test_multiple_sourced_for_field_with_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'SubscriptionType' defines multiple sources for field 'messageAdded'. -... diff --git a/tests/snapshots/test_multiple_sources_without_schema.obtained.yml b/tests/snapshots/test_multiple_sources_without_schema.obtained.yml deleted file mode 100644 index ba187af..0000000 --- a/tests/snapshots/test_multiple_sources_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'SubscriptionType' defines multiple sources for field 'message_added'. -... diff --git a/tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml b/tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml deleted file mode 100644 index d649dfe..0000000 --- a/tests/snapshots/test_name_validator_raises_error_for_name_and_definition_mismatch.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but - names in those don't match. ('Example' != 'Custom') -... diff --git a/tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml deleted file mode 100644 index b5049ef..0000000 --- a/tests/snapshots/test_object_type_instance_with_invalid_attrs_raising_error.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'CategoryType.__init__() got an unexpected keyword argument ''invalid''. Valid keyword - arguments: ''name'', ''posts''' diff --git a/tests/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml deleted file mode 100644 index c38c902..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_alias_resolver.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines an alias for a field 'hello' that already has a custom - resolver. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml deleted file mode 100644 index ed6cc30..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_alias_target_resolver.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''CustomType'' defines resolver for an undefined field ''welcome''. (Valid - fields: ''hello'')' diff --git a/tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml deleted file mode 100644 index 874b9cc..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_attr_and_field_with_same_graphql_name.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple fields with GraphQL name 'userId'. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml deleted file mode 100644 index 9868e1a..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_args.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple resolvers for field 'lorem'. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml deleted file mode 100644 index 694fd36..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple descriptions for field 'hello'. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml deleted file mode 100644 index 352c185..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_invalid_alias.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''CustomType'' defines an alias for an undefined field ''invalid''. (Valid - fields: ''hello'')' diff --git a/tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml deleted file mode 100644 index 2741e54..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_missing_field_resolver_arg.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''CustomType'' defines ''hello'' field with extra configuration for ''invalid'' - argument thats not defined on the resolver function. (expected one of: ''name'')' diff --git a/tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml deleted file mode 100644 index 80d1f4b..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_missing_resolver_arg.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -'Class ''CustomType'' defines ''resolve_hello'' resolver with extra configuration - for ''invalid'' argument thats not defined on the resolver function. (expected one - of: ''name'')' diff --git a/tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml deleted file mode 100644 index 874b9cc..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_multiple_attrs_with_same_graphql_name.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple fields with GraphQL name 'userId'. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml deleted file mode 100644 index 81b2a07..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple resolvers for field 'hello'. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml deleted file mode 100644 index d792e40..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple fields with GraphQL name 'hello'. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml deleted file mode 100644 index bcb3882..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_undefined_attr_resolver.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''QueryType'' defines resolver for an undefined field ''other''. (Valid fields: - ''hello'')' diff --git a/tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml deleted file mode 100644 index 2de19e8..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_undefined_field_resolver_arg.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines 'hello' field with extra configuration for 'invalid' argument - thats not defined on the resolver function. (function accepts no extra arguments) -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml deleted file mode 100644 index dc19fb7..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_undefined_resolver_arg.obtained.yml +++ /dev/null @@ -1,4 +0,0 @@ -Class 'CustomType' defines 'resolve_hello' resolver with extra configuration for 'invalid' - argument thats not defined on the resolver function. (function accepts no extra - arguments) -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml deleted file mode 100644 index 6fb04ed..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'QueryType' defines default value for 'name' argument of the 'hello' field that - can't be represented in GraphQL schema. -... diff --git a/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml b/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml deleted file mode 100644 index 6fb04ed..0000000 --- a/tests/snapshots/test_object_type_validation_fails_for_unsupported_resolver_arg_default_option.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'QueryType' defines default value for 'name' argument of the 'hello' field that - can't be represented in GraphQL schema. -... diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml deleted file mode 100644 index 96581ba..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_duplicated_members_descriptions.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''UserLevel'' ''__members_descriptions__'' attribute defines descriptions for - enum members that also have description in ''__schema__'' attribute. (members: ''MEMBER'')' diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml deleted file mode 100644 index f472c0f..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_empty_enum.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'UserLevel' defines '__schema__' attribute that doesn't declare any enum members. -... diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml deleted file mode 100644 index 69ed30b..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_members_descriptions.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''UserLevel'' ''__members_descriptions__'' attribute defines descriptions for - undefined enum members. (undefined members: ''INVALID'')' diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml deleted file mode 100644 index ec84efc..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_invalid_type_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'UserLevel' defines '__schema__' attribute with declaration for an invalid GraphQL - type. ('ScalarTypeDefinitionNode' != 'EnumTypeDefinitionNode') -... diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml deleted file mode 100644 index 76417dc..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_names_not_matching.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'UserLevel' defines both '__graphql_name__' and '__schema__' attributes, but - names in those don't match. ('UserRank' != 'Custom') -... diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml deleted file mode 100644 index 7945146..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_dict_mismatch.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''UserLevel'' ''__members__'' is missing values for enum members defined in - ''__schema__''. (missing items: ''MEMBER'')' diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml deleted file mode 100644 index 9221777..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_enum_mismatch.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''UserLevel'' ''__members__'' is missing values for enum members defined in - ''__schema__''. (missing items: ''MODERATOR'')' diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml deleted file mode 100644 index ac227b0..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_schema_and_members_list.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'UserLevel' '__members__' attribute can't be a list when used together with - '__schema__'. -... diff --git a/tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml deleted file mode 100644 index 7e8271a..0000000 --- a/tests/snapshots/test_schema_enum_type_validation_fails_for_two_descriptions.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'UserLevel' defines description in both '__description__' and '__schema__' attributes. -... diff --git a/tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml deleted file mode 100644 index c51434c..0000000 --- a/tests/snapshots/test_schema_input_type_instance_with_invalid_attrs_raising_error.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'SearchInput.__init__() got an unexpected keyword argument ''invalid''. Valid keyword - arguments: ''query'', ''age''' diff --git a/tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml deleted file mode 100644 index 58146cd..0000000 --- a/tests/snapshots/test_schema_input_type_validation_fails_for_duplicate_out_name.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines multiple fields with an outname 'ok' in it's '__out_names__' - attribute. -... diff --git a/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml deleted file mode 100644 index 3b496e3..0000000 --- a/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_out_name.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines an outname for 'invalid' field in it's '__out_names__' - attribute which is not defined in '__schema__'. -... diff --git a/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml deleted file mode 100644 index c7632c5..0000000 --- a/tests/snapshots/test_schema_input_type_validation_fails_for_invalid_type_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines '__schema__' attribute with declaration for an invalid - GraphQL type. ('ScalarTypeDefinitionNode' != 'InputObjectTypeDefinitionNode') -... diff --git a/tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml deleted file mode 100644 index 5d0a017..0000000 --- a/tests/snapshots/test_schema_input_type_validation_fails_for_names_not_matching.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but - names in those don't match. ('Lorem' != 'Custom') -... diff --git a/tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml deleted file mode 100644 index 7a624a4..0000000 --- a/tests/snapshots/test_schema_input_type_validation_fails_for_schema_missing_fields.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''CustomType'' defines ''__schema__'' attribute with declaration for an input - type without any fields. ' diff --git a/tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml deleted file mode 100644 index ba71cfb..0000000 --- a/tests/snapshots/test_schema_input_type_validation_fails_for_two_descriptions.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines description in both '__description__' and '__schema__' - attributes. -... diff --git a/tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml b/tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml deleted file mode 100644 index b5049ef..0000000 --- a/tests/snapshots/test_schema_object_type_instance_with_invalid_attrs_raising_error.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'CategoryType.__init__() got an unexpected keyword argument ''invalid''. Valid keyword - arguments: ''name'', ''posts''' diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml deleted file mode 100644 index c38c902..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_alias_resolver.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines an alias for a field 'hello' that already has a custom - resolver. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml deleted file mode 100644 index e114d51..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_alias_target_resolver.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''CustomType'' defines resolver for an undefined field ''ok''. (Valid fields: - ''hello'')' diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml deleted file mode 100644 index 7fb95f5..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_double_description.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines duplicate descriptions for 'name' argument of the 'hello' - field. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml deleted file mode 100644 index f9ac74d..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_name_option.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines 'name' option for 'name' argument of the 'hello' field. - This is not supported for types defining '__schema__'. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml deleted file mode 100644 index 021b821..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_arg_with_type_option.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines 'type' option for 'name' argument of the 'hello' field. - This is not supported for types defining '__schema__'. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml deleted file mode 100644 index 1fb66ce..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_field_instance.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines 'GraphQLObjectField' instance. This is not supported for - types defining '__schema__'. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml deleted file mode 100644 index 5c96002..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_invalid_arg_name.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines options for 'other' argument of the 'hello' field that - doesn't exist. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml deleted file mode 100644 index 694fd36..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_field_with_multiple_descriptions.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple descriptions for field 'hello'. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml deleted file mode 100644 index 352c185..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_alias.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''CustomType'' defines an alias for an undefined field ''invalid''. (Valid - fields: ''hello'')' diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml deleted file mode 100644 index 998c1f6..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_invalid_type_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines '__schema__' attribute with declaration for an invalid - GraphQL type. ('ScalarTypeDefinitionNode' != 'ObjectTypeDefinitionNode') -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml deleted file mode 100644 index 246403c..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_missing_fields.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''CustomType'' defines ''__schema__'' attribute with declaration for an object - type without any fields. ' diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml deleted file mode 100644 index 81b2a07..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_multiple_field_resolvers.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Class 'CustomType' defines multiple resolvers for field 'hello'. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml deleted file mode 100644 index 5d0a017..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_names_not_matching.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines both '__graphql_name__' and '__schema__' attributes, but - names in those don't match. ('Lorem' != 'Custom') -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml deleted file mode 100644 index ba71cfb..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_two_descriptions.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomType' defines description in both '__description__' and '__schema__' - attributes. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml deleted file mode 100644 index bcb3882..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_undefined_field_resolver.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''QueryType'' defines resolver for an undefined field ''other''. (Valid fields: - ''hello'')' diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml deleted file mode 100644 index 6fb04ed..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_default.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'QueryType' defines default value for 'name' argument of the 'hello' field that - can't be represented in GraphQL schema. -... diff --git a/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml b/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml deleted file mode 100644 index 6fb04ed..0000000 --- a/tests/snapshots/test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'QueryType' defines default value for 'name' argument of the 'hello' field that - can't be represented in GraphQL schema. -... diff --git a/tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml deleted file mode 100644 index 5d15013..0000000 --- a/tests/snapshots/test_schema_scalar_type_validation_fails_for_different_names.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomScalar' defines both '__graphql_name__' and '__schema__' attributes, - but names in those don't match. ('Date' != 'Custom') -... diff --git a/tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml deleted file mode 100644 index 0eec05e..0000000 --- a/tests/snapshots/test_schema_scalar_type_validation_fails_for_invalid_type_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomScalar' defines '__schema__' attribute with declaration for an invalid - GraphQL type. ('ObjectTypeDefinitionNode' != 'ScalarTypeDefinitionNode') -... diff --git a/tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml b/tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml deleted file mode 100644 index e10ef27..0000000 --- a/tests/snapshots/test_schema_scalar_type_validation_fails_for_two_descriptions.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Class 'CustomScalar' defines description in both '__description__' and '__schema__' - attributes. -... diff --git a/tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml b/tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml deleted file mode 100644 index f68971b..0000000 --- a/tests/snapshots/test_schema_validation_fails_if_lazy_type_doesnt_exist.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -Unknown type 'Missing'. -... diff --git a/tests/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml b/tests/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml deleted file mode 100644 index 51d3732..0000000 --- a/tests/snapshots/test_source_args_field_arg_not_dict_without_schema.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Argument channel for message_added_generator must have a GraphQLObjectFieldArg as - its info. -... diff --git a/tests/snapshots/test_source_args_not_dict_without_schema.obtained.yml b/tests/snapshots/test_source_args_not_dict_without_schema.obtained.yml deleted file mode 100644 index d390e7e..0000000 --- a/tests/snapshots/test_source_args_not_dict_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -The args for message_added_generator must be a dictionary if provided. -... diff --git a/tests/snapshots/test_source_for_undefined_field_with_schema.obtained.yml b/tests/snapshots/test_source_for_undefined_field_with_schema.obtained.yml deleted file mode 100644 index d520895..0000000 --- a/tests/snapshots/test_source_for_undefined_field_with_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''SubscriptionType'' defines source for an undefined field ''message_added''. - (Valid fields: ''messageAdded'')' diff --git a/tests/snapshots/test_undefined_name_without_schema.obtained.yml b/tests/snapshots/test_undefined_name_without_schema.obtained.yml deleted file mode 100644 index c82b237..0000000 --- a/tests/snapshots/test_undefined_name_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'Class ''SubscriptionType'' defines source for an undefined field ''messageAdded''. - (Valid fields: ''message_added'')' diff --git a/tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml deleted file mode 100644 index 393636c..0000000 --- a/tests/snapshots/test_value_error_is_raised_if_exclude_and_include_members_are_combined.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'''members_include'' and ''members_exclude'' options are mutually exclusive.' diff --git a/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml deleted file mode 100644 index 87f633f..0000000 --- a/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_excluded_item.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Member description was specified for a member 'ADMINISTRATOR' not present in final - GraphQL enum. -... diff --git a/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml deleted file mode 100644 index 06e660c..0000000 --- a/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_missing_item.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Member description was specified for a member 'MISSING' not present in final GraphQL - enum. -... diff --git a/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml b/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml deleted file mode 100644 index 87f633f..0000000 --- a/tests/snapshots/test_value_error_is_raised_if_member_description_is_set_for_omitted_item.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -Member description was specified for a member 'ADMINISTRATOR' not present in final - GraphQL enum. -... diff --git a/tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml b/tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml deleted file mode 100644 index 1901e66..0000000 --- a/tests_v1/snapshots/test_definition_parser_raises_error_schema_str_contains_multiple_types.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'MyType class was defined with __schema__ containing more than one GraphQL definition - (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml b/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml deleted file mode 100644 index d16bce5..0000000 --- a/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_str_has_invalid_syntax.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo User\n |\ - \ ^" diff --git a/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml b/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml deleted file mode 100644 index e12dbfa..0000000 --- a/tests_v1/snapshots/test_definition_parser_raises_error_when_schema_type_is_invalid.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'MyType class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index 4b6d226..0000000 --- a/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'ExampleDirective' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml deleted file mode 100644 index ccef661..0000000 --- a/tests_v1/snapshots/test_directive_type_raises_attribute_error_when_defined_without_visitor.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -ExampleDirective class was defined without __visitor__ attribute -... diff --git a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index f43a447..0000000 --- a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -ExampleDirective class was defined with __schema__ without GraphQL directive -... diff --git a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index 2b09463..0000000 --- a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'directivo'.\n\nGraphQL request:1:1\n1 | directivo\ - \ @example on FIELD_DEFINITION\n | ^" diff --git a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index b287e70..0000000 --- a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'ExampleDirective class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index 417c07b..0000000 --- a/tests_v1/snapshots/test_directive_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'ExampleDirective class was defined with __schema__ containing more than one GraphQL - definition (found: DirectiveDefinitionNode, DirectiveDefinitionNode)' diff --git a/tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index 3a7b959..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'UserRoleEnum' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index bc4de25..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserRoleEnum class was defined with __schema__ without GraphQL enum -... diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index c01e216..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'enom'.\n\nGraphQL request:1:1\n1 | enom UserRole\n\ - \ | ^" diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index 6ee07b7..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserRoleEnum class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index f513b5e..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserRoleEnum class was defined with __schema__ containing more than one GraphQL definition - (found: EnumTypeDefinitionNode, EnumTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml deleted file mode 100644 index e71b37e..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_has_extra_items_not_in_definition.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserRoleEnum class was defined with __enum__ containing extra items missing in GraphQL - definition: REVIEW' diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml deleted file mode 100644 index afa4a5a..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_dict_mapping_misses_items_from_definition.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserRoleEnum class was defined with __enum__ missing following items required by - GraphQL definition: MOD' diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml deleted file mode 100644 index e71b37e..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_has_extra_items_not_in_definition.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserRoleEnum class was defined with __enum__ containing extra items missing in GraphQL - definition: REVIEW' diff --git a/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml b/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml deleted file mode 100644 index afa4a5a..0000000 --- a/tests_v1/snapshots/test_enum_type_raises_error_when_enum_mapping_misses_items_from_definition.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserRoleEnum class was defined with __enum__ missing following items required by - GraphQL definition: MOD' diff --git a/tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml b/tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml deleted file mode 100644 index 15490e9..0000000 --- a/tests_v1/snapshots/test_executable_schema_raises_value_error_if_merged_types_define_same_field.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'Multiple Query types are defining same field ''city'': CityQueryType, YearQueryType' diff --git a/tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index e789b2a..0000000 --- a/tests_v1/snapshots/test_input_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'UserInput' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml deleted file mode 100644 index e522523..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_args_map_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserInput class was defined with args for fields not in GraphQL input: fullName' diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index 755ecaa..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserInput class was defined with __schema__ without GraphQL input -... diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index b0e2a72..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'inpet'.\n\nGraphQL request:1:1\n1 | inpet UserInput\n\ - \ | ^" diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index 4b5db00..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserInput class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index 77630d7..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserInput class was defined with __schema__ containing more than one GraphQL definition - (found: InputObjectTypeDefinitionNode, InputObjectTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml deleted file mode 100644 index 5140f86..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_extended_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExtendUserInput graphql type was defined without required GraphQL type definition - for 'User' in __requires__ -... diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml deleted file mode 100644 index 97a9adf..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_field_type_dependency.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserInput class was defined without required GraphQL definition for 'Role' in __requires__ -... diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml deleted file mode 100644 index 3959a6b..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_defined_without_fields.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserInput class was defined with __schema__ containing empty GraphQL input definition -... diff --git a/tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml deleted file mode 100644 index 258b184..0000000 --- a/tests_v1/snapshots/test_input_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExtendUserInput requires 'User' to be GraphQL input but other type was provided in - '__requires__' -... diff --git a/tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index 41480da..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'ExampleInterface' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml deleted file mode 100644 index 00161da..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'ExampleInterface class was defined with aliases for fields not in GraphQL type: joinedDate' diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index c03787f..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -ExampleInterface class was defined with __schema__ without GraphQL interface -... diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index 8217a60..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'interfaco'.\n\nGraphQL request:1:1\n1 | interfaco\ - \ Example\n | ^" diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index 6efffd8..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'ExampleInterface class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index c4a1d3d..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'ExampleInterface class was defined with __schema__ containing more than one GraphQL - definition (found: InterfaceTypeDefinitionNode, InterfaceTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml deleted file mode 100644 index 4b67ef2..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'ExampleInterface class was defined with resolvers for fields not in GraphQL type: - resolve_group' diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml deleted file mode 100644 index c70e6fa..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExampleInterface class was defined without required GraphQL definition for 'UserInput' - in __requires__ -... diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml deleted file mode 100644 index 8c4dc90..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_extended_dependency.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -ExtendExampleInterface class was defined with __schema__ without GraphQL type -... diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml deleted file mode 100644 index 5e0e878..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_fields.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExampleInterface class was defined with __schema__ containing empty GraphQL interface - definition -... diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml deleted file mode 100644 index 8503826..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_defined_without_return_type_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExampleInterface class was defined without required GraphQL definition for 'Group' - in __requires__ -... diff --git a/tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml deleted file mode 100644 index 0b64116..0000000 --- a/tests_v1/snapshots/test_interface_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExampleInterface requires 'Example' to be GraphQL interface but other type was provided - in '__requires__' -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index c33b88b..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'UserCreateMutation' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml deleted file mode 100644 index c529e91..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_for_different_type_name.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UserCreateMutation class was defined with __schema__ containing GraphQL definition - for 'type User' while 'type Mutation' was expected -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index 04cd779..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserCreateMutation class was defined with __schema__ without GraphQL type -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index 769faab..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserCreateMutation class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml deleted file mode 100644 index 1b4c4d3..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_fields.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UserCreateMutation class subclasses 'MutationType' class which requires __schema__ - to define exactly one field -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index 686b110..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserCreateMutation class was defined with __schema__ containing more than one GraphQL - definition (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml deleted file mode 100644 index 141c105..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_with_nonexistant_args.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserCreateMutation class was defined with args not on ''userCreate'' GraphQL field: - realName' diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml deleted file mode 100644 index 52d3d13..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_callable_resolve_mutation_attr.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UserCreateMutation class was defined with attribute 'resolve_mutation' but it's not - callable -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml deleted file mode 100644 index b5d09d3..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_fields.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UserCreateMutation class was defined with __schema__ containing empty GraphQL type - definition -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml deleted file mode 100644 index ce96568..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_resolve_mutation_attr.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserCreateMutation class was defined without required 'resolve_mutation' attribute -... diff --git a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml deleted file mode 100644 index bcb3e21..0000000 --- a/tests_v1/snapshots/test_mutation_type_raises_error_when_defined_without_return_type_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UserCreateMutation class was defined without required GraphQL definition for 'UserCreateResult' - in __requires__ -... diff --git a/tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index ed1a122..0000000 --- a/tests_v1/snapshots/test_object_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'UserType' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml deleted file mode 100644 index a8109e2..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserType class was defined with aliases for fields not in GraphQL type: joinedDate' diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml deleted file mode 100644 index 52b6f4e..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_arg.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserType class was defined with args mappings not in not in ''name'' field: arg' diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml deleted file mode 100644 index db29360..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_field_args_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserType class was defined with fields args mappings for fields not in GraphQL type: - group' diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index 8d5f64d..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserType class was defined with __schema__ without GraphQL type -... diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index a32b7b5..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserType class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index 54101dd..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'UserType class was defined with __schema__ containing more than one GraphQL definition - (found: ObjectTypeDefinitionNode, ObjectTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml deleted file mode 100644 index 59ac07b..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UserType class was defined with resolvers for fields not in GraphQL type: resolve_group' diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml deleted file mode 100644 index a26f534..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UserType class was defined without required GraphQL definition for 'UserInput' in - __requires__ -... diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml deleted file mode 100644 index 9b74478..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_extended_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExtendUserType graphql type was defined without required GraphQL type definition for - 'User' in __requires__ -... diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml deleted file mode 100644 index d1ef4ac..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_fields.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserType class was defined with __schema__ containing empty GraphQL type definition -... diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml deleted file mode 100644 index 19d2bbe..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_without_return_type_dependency.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UserType class was defined without required GraphQL definition for 'Group' in __requires__ -... diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml deleted file mode 100644 index 5222a0e..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -ExampleType requires 'Example' to be GraphQL type but other type was provided in '__requires__' -... diff --git a/tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index 343ade4..0000000 --- a/tests_v1/snapshots/test_scalar_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'DateScalar' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index 1090fab..0000000 --- a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -DateScalar class was defined with __schema__ without GraphQL scalar -... diff --git a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index 8cb39c7..0000000 --- a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'scalor'.\n\nGraphQL request:1:1\n1 | scalor Date\n\ - \ | ^" diff --git a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index b55ebcd..0000000 --- a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'DateScalar class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index 1775123..0000000 --- a/tests_v1/snapshots/test_scalar_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'DateScalar class was defined with __schema__ containing more than one GraphQL definition - (found: ScalarTypeDefinitionNode, ScalarTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index 8f88a6f..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'UsersSubscription' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml deleted file mode 100644 index d0e6232..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_alias_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'ChatSubscription class was defined with aliases for fields not in GraphQL type: userAlerts' diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml deleted file mode 100644 index c7df1c7..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_name.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UsersSubscription class was defined with __schema__ containing GraphQL definition - for 'type Other' (expected 'type Subscription') -... diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index 5075396..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -UsersSubscription class was defined with __schema__ without GraphQL type -... diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index 218344a..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo Subscription\n\ - \ | ^" diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index 18f9545..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'UsersSubscription class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml deleted file mode 100644 index 9bdf884..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_resolver_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'ChatSubscription class was defined with resolvers for fields not in GraphQL type: - resolve_group' diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml deleted file mode 100644 index d8dd479..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_with_sub_for_nonexisting_field.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'ChatSubscription class was defined with subscribers for fields not in GraphQL type: - resolve_group' diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml deleted file mode 100644 index 822b19a..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_argument_type_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ChatSubscription class was defined without required GraphQL definition for 'ChannelInput' - in __requires__ -... diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml deleted file mode 100644 index cd1dd29..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_extended_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExtendChatSubscription graphql type was defined without required GraphQL type definition - for 'Subscription' in __requires__ -... diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml deleted file mode 100644 index aba35e1..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_fields.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -UsersSubscription class was defined with __schema__ containing empty GraphQL type - definition -... diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml deleted file mode 100644 index 2ed5d80..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_defined_without_return_type_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ChatSubscription class was defined without required GraphQL definition for 'Chat' - in __requires__ -... diff --git a/tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml deleted file mode 100644 index 20aa4af..0000000 --- a/tests_v1/snapshots/test_subscription_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExtendChatSubscription requires 'Subscription' to be GraphQL type but other type was - provided in '__requires__' -... diff --git a/tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml b/tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml deleted file mode 100644 index dc8ba31..0000000 --- a/tests_v1/snapshots/test_union_type_raises_attribute_error_when_defined_without_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -type object 'ExampleUnion' has no attribute '__schema__' -... diff --git a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml deleted file mode 100644 index 7925073..0000000 --- a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_graphql_type_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -ExampleUnion class was defined with __schema__ without GraphQL union -... diff --git a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index 5bf1156..0000000 --- a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'unien'.\n\nGraphQL request:1:1\n1 | unien Example\ - \ = A | B\n | ^" diff --git a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml deleted file mode 100644 index 6213798..0000000 --- a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_invalid_schema_type.obtained.yml +++ /dev/null @@ -1 +0,0 @@ -'ExampleUnion class was defined with __schema__ of invalid type: bool' diff --git a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml deleted file mode 100644 index 0613370..0000000 --- a/tests_v1/snapshots/test_union_type_raises_error_when_defined_with_multiple_types_schema.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -'ExampleUnion class was defined with __schema__ containing more than one GraphQL definition - (found: UnionTypeDefinitionNode, UnionTypeDefinitionNode)' diff --git a/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml deleted file mode 100644 index db77044..0000000 --- a/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_extended_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExtendExampleUnion class was defined without required GraphQL union definition for - 'Result' in __requires__ -... diff --git a/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml deleted file mode 100644 index b4673df..0000000 --- a/tests_v1/snapshots/test_union_type_raises_error_when_defined_without_member_type_dependency.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExampleUnion class was defined without required GraphQL definition for 'Comment' in - __requires__ -... diff --git a/tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml b/tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml deleted file mode 100644 index 0ce118e..0000000 --- a/tests_v1/snapshots/test_union_type_raises_error_when_extended_dependency_is_wrong_type.obtained.yml +++ /dev/null @@ -1,3 +0,0 @@ -ExtendExampleUnion requires 'Example' to be GraphQL union but other type was provided - in '__requires__' -... diff --git a/tests_v1/test_collection_type.py b/tests_v1/test_collection_type.py index c5f43c5..9c5d784 100644 --- a/tests_v1/test_collection_type.py +++ b/tests_v1/test_collection_type.py @@ -1,6 +1,6 @@ from graphql import graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( CollectionType, DeferredType, ObjectType, diff --git a/tests_v1/test_convert_case.py b/tests_v1/test_convert_case.py index f4ea36f..e7147a8 100644 --- a/tests_v1/test_convert_case.py +++ b/tests_v1/test_convert_case.py @@ -1,4 +1,4 @@ -from ariadne_graphql_modules import InputType, MutationType, ObjectType, convert_case +from ariadne_graphql_modules.v1 import InputType, MutationType, ObjectType, convert_case def test_cases_are_mapped_for_aliases(): diff --git a/tests_v1/test_definition_parser.py b/tests_v1/test_definition_parser.py index ab5ab43..9cc8ebc 100644 --- a/tests_v1/test_definition_parser.py +++ b/tests_v1/test_definition_parser.py @@ -2,7 +2,7 @@ from graphql import GraphQLError from graphql.language.ast import ObjectTypeDefinitionNode -from ariadne_graphql_modules import parse_definition +from ariadne_graphql_modules.v1 import parse_definition def test_definition_parser_returns_definition_type_from_valid_schema_string(): diff --git a/tests_v1/test_directive_type.py b/tests_v1/test_directive_type.py index 15116f1..a3f38cf 100644 --- a/tests_v1/test_directive_type.py +++ b/tests_v1/test_directive_type.py @@ -2,7 +2,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, default_field_resolver, graphql_sync -from ariadne_graphql_modules import DirectiveType, ObjectType, make_executable_schema +from ariadne_graphql_modules.v1 import DirectiveType, ObjectType, make_executable_schema def test_directive_type_raises_attribute_error_when_defined_without_schema( diff --git a/tests_v1/test_enum_type.py b/tests_v1/test_enum_type.py index 7ea4585..084365a 100644 --- a/tests_v1/test_enum_type.py +++ b/tests_v1/test_enum_type.py @@ -4,7 +4,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( DirectiveType, EnumType, ObjectType, diff --git a/tests_v1/test_executable_schema.py b/tests_v1/test_executable_schema.py index 8a59ed3..584b471 100644 --- a/tests_v1/test_executable_schema.py +++ b/tests_v1/test_executable_schema.py @@ -1,7 +1,7 @@ import pytest from graphql import graphql_sync -from ariadne_graphql_modules import ObjectType, make_executable_schema +from ariadne_graphql_modules.v1 import ObjectType, make_executable_schema def test_executable_schema_is_created_from_object_types(): diff --git a/tests_v1/test_executable_schema_compat.py b/tests_v1/test_executable_schema_compat.py index 5833845..a1f5e7b 100644 --- a/tests_v1/test_executable_schema_compat.py +++ b/tests_v1/test_executable_schema_compat.py @@ -1,5 +1,5 @@ from ariadne import ObjectType as OldObjectType, QueryType, gql, graphql_sync -from ariadne_graphql_modules import DeferredType, ObjectType, make_executable_schema +from ariadne_graphql_modules.v1 import DeferredType, ObjectType, make_executable_schema def test_old_schema_definition_is_executable(): diff --git a/tests_v1/test_input_type.py b/tests_v1/test_input_type.py index daad5e0..350e7af 100644 --- a/tests_v1/test_input_type.py +++ b/tests_v1/test_input_type.py @@ -2,7 +2,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( DeferredType, DirectiveType, EnumType, diff --git a/tests_v1/test_interface_type.py b/tests_v1/test_interface_type.py index be3637d..6bac4fa 100644 --- a/tests_v1/test_interface_type.py +++ b/tests_v1/test_interface_type.py @@ -4,7 +4,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( DeferredType, DirectiveType, InterfaceType, diff --git a/tests_v1/test_mutation_type.py b/tests_v1/test_mutation_type.py index 8b03c9e..0f6f4b9 100644 --- a/tests_v1/test_mutation_type.py +++ b/tests_v1/test_mutation_type.py @@ -1,7 +1,7 @@ import pytest from graphql import GraphQLError, graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( MutationType, ObjectType, make_executable_schema, diff --git a/tests_v1/test_object_type.py b/tests_v1/test_object_type.py index 8adf0e6..50f48f4 100644 --- a/tests_v1/test_object_type.py +++ b/tests_v1/test_object_type.py @@ -2,7 +2,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( DeferredType, DirectiveType, InterfaceType, diff --git a/tests_v1/test_scalar_type.py b/tests_v1/test_scalar_type.py index ec85340..d66f66e 100644 --- a/tests_v1/test_scalar_type.py +++ b/tests_v1/test_scalar_type.py @@ -4,7 +4,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, StringValueNode, graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( DirectiveType, ObjectType, ScalarType, diff --git a/tests_v1/test_subscription_type.py b/tests_v1/test_subscription_type.py index c0067cf..90375a8 100644 --- a/tests_v1/test_subscription_type.py +++ b/tests_v1/test_subscription_type.py @@ -2,7 +2,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, build_schema -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( DirectiveType, InterfaceType, ObjectType, diff --git a/tests_v1/test_union_type.py b/tests_v1/test_union_type.py index 61c8a3a..4965981 100644 --- a/tests_v1/test_union_type.py +++ b/tests_v1/test_union_type.py @@ -4,7 +4,7 @@ from ariadne import SchemaDirectiveVisitor from graphql import GraphQLError, graphql_sync -from ariadne_graphql_modules import ( +from ariadne_graphql_modules.v1 import ( DirectiveType, ObjectType, UnionType, From a2031e8f2ed058c4cd52beb80805165b1fdb305e Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 22 Aug 2024 15:31:36 +0200 Subject: [PATCH 48/63] fix args descriptions --- .../base_object_type/graphql_type.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index 6dc9b9d..4f4d821 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -134,16 +134,16 @@ def _create_fields_and_resolvers_with_schema( field_args = get_field_args_from_resolver(cls_attr.resolver) if field_args: - args_descriptions[cls_attr.field], args_defaults[cls_attr.field] = ( - {}, - {}, - ) + args_descriptions[cls_attr.field] = {} + args_defaults[cls_attr.field] = {} final_args = update_field_args_options(field_args, cls_attr.args) for arg_name, arg_options in final_args.items(): - description_node = get_description_node(cls_attr.description) - if description_node: - descriptions[cls_attr.field] = description_node + arg_description = get_description_node(arg_options.description) + if arg_description: + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description if arg_options.default_value is not None: args_defaults[cls_attr.field][arg_name] = get_value_node( From c878df94a30f1e826f1e56133b1cef53be5c4ccb Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Sun, 25 Aug 2024 18:31:22 +0200 Subject: [PATCH 49/63] Added more tests --- .../base_object_type/graphql_field.py | 12 +- .../base_object_type/graphql_type.py | 39 ++-- .../compatibility_layer.py | 4 +- ariadne_graphql_modules/deferredtype.py | 8 + ariadne_graphql_modules/description.py | 7 + ariadne_graphql_modules/executable_schema.py | 11 +- .../interface_type/graphql_type.py | 36 ++-- .../object_type/graphql_type.py | 47 ++--- .../subscription_type/graphql_type.py | 122 ++++++----- ariadne_graphql_modules/types.py | 8 +- tests/conftest.py | 9 + ...face_with_schema_object_with_no_schema.yml | 2 + tests/test_deferred_type.py | 23 ++- tests/test_description_node.py | 5 + tests/test_enum_type.py | 42 +++- tests/test_id_type.py | 5 + tests/test_input_type.py | 78 +++++++ tests/test_interface_type.py | 82 +++++++- tests/test_interface_type_validation.py | 22 ++ tests/test_make_executable_schema.py | 2 +- tests/test_scalar_type.py | 31 +++ tests/test_subscription_type.py | 193 ++++++++++++++++-- tests/test_union_type.py | 11 +- ...fined_with_invalid_schema_str.obtained.yml | 2 - 24 files changed, 635 insertions(+), 166 deletions(-) create mode 100644 tests/snapshots/test_interface_with_schema_object_with_no_schema.yml delete mode 100644 tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml diff --git a/ariadne_graphql_modules/base_object_type/graphql_field.py b/ariadne_graphql_modules/base_object_type/graphql_field.py index a7ced74..98a875a 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_field.py +++ b/ariadne_graphql_modules/base_object_type/graphql_field.py @@ -1,6 +1,7 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from ariadne.types import Resolver, Subscriber +from graphql import FieldDefinitionNode, NamedTypeNode @dataclass(frozen=True) @@ -15,7 +16,16 @@ class GraphQLObjectFieldArg: @dataclass(frozen=True) class GraphQLObjectData: fields: Dict[str, "GraphQLObjectField"] - interfaces: List[str] + interfaces: List[NamedTypeNode] + + +@dataclass +class GraphQLClassData: + type_aliases: Dict[str, str] = field(default_factory=dict) + fields_ast: Dict[str, FieldDefinitionNode] = field(default_factory=dict) + resolvers: Dict[str, "Resolver"] = field(default_factory=dict) + aliases: Dict[str, str] = field(default_factory=dict) + out_names: Dict[str, Dict[str, str]] = field(default_factory=dict) @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index 4f4d821..81c38d1 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -1,5 +1,4 @@ from copy import deepcopy -from dataclasses import dataclass from enum import Enum from typing import ( Any, @@ -23,6 +22,7 @@ from ..types import GraphQLClassType from .graphql_field import ( + GraphQLClassData, GraphQLFieldData, GraphQLObjectData, GraphQLObjectField, @@ -54,7 +54,7 @@ class GraphQLBaseObject(GraphQLType): __kwargs__: Dict[str, Any] __abstract__: bool = True - __schema__: Optional[str] + __schema__: Optional[str] = None __description__: Optional[str] __aliases__: Optional[Dict[str, str]] __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] @@ -188,33 +188,29 @@ def _create_fields_and_resolvers_with_schema( @classmethod def _process_graphql_fields( - cls, metadata: GraphQLMetadata, type_data, type_aliases - ) -> Tuple[ - List[FieldDefinitionNode], - Dict[str, Resolver], - Dict[str, str], - Dict[str, Dict[str, str]], - ]: - fields_ast = [] - resolvers = {} - aliases = {} - out_names = {} - + cls, + metadata: GraphQLMetadata, + type_data, + type_aliases, + object_model_data: GraphQLClassData, + ): for attr_name, field in type_data.fields.items(): - fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) + object_model_data.fields_ast[attr_name] = get_field_node_from_obj_field( + cls, metadata, field + ) if attr_name in type_aliases and field.name: - aliases[field.name] = type_aliases[attr_name] + object_model_data.aliases[field.name] = type_aliases[attr_name] elif field.name and attr_name != field.name and not field.resolver: - aliases[field.name] = attr_name + object_model_data.aliases[field.name] = attr_name if field.resolver and field.name: - resolvers[field.name] = field.resolver + object_model_data.resolvers[field.name] = field.resolver if field.args and field.name: - out_names[field.name] = get_field_args_out_names(field.args) - - return fields_ast, resolvers, aliases, out_names + object_model_data.out_names[field.name] = get_field_args_out_names( + field.args + ) @classmethod def __get_graphql_types__( @@ -232,7 +228,6 @@ def __get_graphql_types_with_schema__( ) -> Iterable[Type["GraphQLType"]]: types: List[Type["GraphQLType"]] = [cls] types.extend(getattr(cls, "__requires__", [])) - types.extend(getattr(cls, "__implements__", [])) return types @classmethod diff --git a/ariadne_graphql_modules/compatibility_layer.py b/ariadne_graphql_modules/compatibility_layer.py index 182e2a0..e82c2bd 100644 --- a/ariadne_graphql_modules/compatibility_layer.py +++ b/ariadne_graphql_modules/compatibility_layer.py @@ -23,7 +23,7 @@ from .v1.subscription_type import SubscriptionType from .v1.union_type import UnionType from .v1.object_type import ObjectType -from .v1.bases import BindableType +from .v1.bases import BaseType, BindableType from .base import GraphQLModel, GraphQLType from . import ( @@ -38,7 +38,7 @@ def wrap_legacy_types( - *bindable_types: Type[BindableType], + *bindable_types: Type[BaseType], ) -> List[Type["LegacyGraphQLType"]]: all_types = get_all_types(bindable_types) diff --git a/ariadne_graphql_modules/deferredtype.py b/ariadne_graphql_modules/deferredtype.py index 2011c04..480f7b7 100644 --- a/ariadne_graphql_modules/deferredtype.py +++ b/ariadne_graphql_modules/deferredtype.py @@ -5,10 +5,18 @@ @dataclass(frozen=True) class DeferredTypeData: + """Data class representing deferred type information with a module path.""" + path: str def deferred(module_path: str) -> DeferredTypeData: + """ + Create a DeferredTypeData object from a given module path. + + If the module path is relative (starts with '.'), + resolve it based on the caller's package context. + """ if not module_path.startswith("."): return DeferredTypeData(module_path) diff --git a/ariadne_graphql_modules/description.py b/ariadne_graphql_modules/description.py index ff7a8ba..cda949f 100644 --- a/ariadne_graphql_modules/description.py +++ b/ariadne_graphql_modules/description.py @@ -5,6 +5,13 @@ def get_description_node(description: Optional[str]) -> Optional[StringValueNode]: + """Convert a description string into a GraphQL StringValueNode. + + If the description is provided, it will be dedented, stripped of surrounding + whitespace, and used to create a StringValueNode. If the description contains + newline characters, the `block` attribute of the StringValueNode + will be set to `True`. + """ if not description: return None diff --git a/ariadne_graphql_modules/executable_schema.py b/ariadne_graphql_modules/executable_schema.py index 79da59f..2cb1938 100644 --- a/ariadne_graphql_modules/executable_schema.py +++ b/ariadne_graphql_modules/executable_schema.py @@ -26,7 +26,7 @@ def make_executable_schema( - *types: SchemaType, + *types: Union[SchemaType, List[SchemaType]], directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, convert_names_case: Union[bool, SchemaNameConverter] = False, merge_roots: bool = True, @@ -96,7 +96,12 @@ def make_executable_schema( return schema -def find_type_defs(types: Sequence[SchemaType]) -> List[str]: +def find_type_defs( + types: Union[ + tuple[SchemaType | List[SchemaType], ...], + List[SchemaType], + ] +) -> List[str]: type_defs: List[str] = [] for type_def in types: @@ -109,7 +114,7 @@ def find_type_defs(types: Sequence[SchemaType]) -> List[str]: def flatten_types( - types: Sequence[SchemaType], + types: tuple[SchemaType | List[SchemaType], ...], metadata: GraphQLMetadata, ) -> List[SchemaType]: flat_schema_types_list: List[SchemaType] = flatten_schema_types( diff --git a/ariadne_graphql_modules/interface_type/graphql_type.py b/ariadne_graphql_modules/interface_type/graphql_type.py index 9a33c14..2237f1c 100644 --- a/ariadne_graphql_modules/interface_type/graphql_type.py +++ b/ariadne_graphql_modules/interface_type/graphql_type.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Tuple, cast +from typing import Any, Dict, Optional, Tuple, cast from ariadne.types import Resolver from graphql import ( @@ -8,6 +8,8 @@ NamedTypeNode, ) +from ariadne_graphql_modules.base_object_type.graphql_field import GraphQLClassData + from ..base_object_type import ( GraphQLFieldData, GraphQLBaseObject, @@ -29,6 +31,7 @@ class GraphQLInterface(GraphQLBaseObject): __graphql_type__ = GraphQLClassType.INTERFACE __abstract__ = True __description__: Optional[str] = None + __graphql_name__: Optional[str] = None def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -78,19 +81,11 @@ def __get_graphql_model_without_schema__( type_data = cls.get_graphql_object_data(metadata) type_aliases = getattr(cls, "__aliases__", None) or {} - fields_ast: List[FieldDefinitionNode] = [] - resolvers: Dict[str, Resolver] = {} - aliases: Dict[str, str] = {} - out_names: Dict[str, Dict[str, str]] = {} - - fields_ast, resolvers, aliases, out_names = cls._process_graphql_fields( - metadata, type_data, type_aliases + object_model_data = GraphQLClassData() + cls._process_graphql_fields( + metadata, type_data, type_aliases, object_model_data ) - interfaces_ast: List[NamedTypeNode] = [] - for interface_name in type_data.interfaces: - interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) - return GraphQLInterfaceModel( name=name, ast_type=InterfaceTypeDefinitionNode, @@ -99,13 +94,13 @@ def __get_graphql_model_without_schema__( description=get_description_node( getattr(cls, "__description__", None), ), - fields=tuple(fields_ast), - interfaces=tuple(interfaces_ast), + fields=tuple(object_model_data.fields_ast.values()), + interfaces=tuple(type_data.interfaces), ), resolve_type=cls.resolve_type, - resolvers=resolvers, - aliases=aliases, - out_names=out_names, + resolvers=object_model_data.resolvers, + aliases=object_model_data.aliases, + out_names=object_model_data.out_names, ) @staticmethod @@ -143,5 +138,10 @@ def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: return GraphQLObjectData( fields=cls._build_fields(fields_data=fields_data), - interfaces=[], + interfaces=[ + NamedTypeNode(name=NameNode(value=interface.__name__)) + for interface in inherited_objects + if getattr(interface, "__graphql_type__", None) + == GraphQLClassType.INTERFACE + ], ) diff --git a/ariadne_graphql_modules/object_type/graphql_type.py b/ariadne_graphql_modules/object_type/graphql_type.py index 89e0227..a053760 100644 --- a/ariadne_graphql_modules/object_type/graphql_type.py +++ b/ariadne_graphql_modules/object_type/graphql_type.py @@ -1,6 +1,5 @@ from typing import ( Dict, - List, Optional, Tuple, cast, @@ -14,14 +13,14 @@ ObjectTypeDefinitionNode, ) +from ariadne_graphql_modules.base_object_type.graphql_field import GraphQLClassData + from ..types import GraphQLClassType from ..base_object_type import ( GraphQLFieldData, GraphQLBaseObject, GraphQLObjectData, - validate_object_type_with_schema, - validate_object_type_without_schema, ) from .models import GraphQLObjectModel @@ -36,20 +35,8 @@ class GraphQLObject(GraphQLBaseObject): __graphql_type__ = GraphQLClassType.OBJECT __abstract__ = True __description__: Optional[str] = None - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.__dict__.get("__abstract__"): - return - - cls.__abstract__ = False - - if cls.__dict__.get("__schema__"): - valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) - cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) - else: - cls.__kwargs__ = validate_object_type_without_schema(cls) + __schema__: Optional[str] = None + __graphql_name__: Optional[str] = None @classmethod def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": @@ -84,19 +71,11 @@ def __get_graphql_model_without_schema__( type_data = cls.get_graphql_object_data(metadata) type_aliases = getattr(cls, "__aliases__", {}) - fields_ast: List[FieldDefinitionNode] = [] - resolvers: Dict[str, Resolver] = {} - aliases: Dict[str, str] = {} - out_names: Dict[str, Dict[str, str]] = {} - - fields_ast, resolvers, aliases, out_names = cls._process_graphql_fields( - metadata, type_data, type_aliases + object_model_data = GraphQLClassData() + cls._process_graphql_fields( + metadata, type_data, type_aliases, object_model_data ) - interfaces_ast: List[NamedTypeNode] = [] - for interface_name in type_data.interfaces: - interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) - return GraphQLObjectModel( name=name, ast_type=ObjectTypeDefinitionNode, @@ -105,12 +84,12 @@ def __get_graphql_model_without_schema__( description=get_description_node( getattr(cls, "__description__", None), ), - fields=tuple(fields_ast), - interfaces=tuple(interfaces_ast), + fields=tuple(object_model_data.fields_ast.values()), + interfaces=tuple(type_data.interfaces), ), - resolvers=resolvers, - aliases=aliases, - out_names=out_names, + resolvers=object_model_data.resolvers, + aliases=object_model_data.aliases, + out_names=object_model_data.out_names, ) @classmethod @@ -140,7 +119,7 @@ def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: return GraphQLObjectData( fields=cls._build_fields(fields_data=fields_data), interfaces=[ - interface.__name__ + NamedTypeNode(name=NameNode(value=interface.__name__)) for interface in inherited_objects if getattr(interface, "__graphql_type__", None) == GraphQLClassType.INTERFACE diff --git a/ariadne_graphql_modules/subscription_type/graphql_type.py b/ariadne_graphql_modules/subscription_type/graphql_type.py index 90fd7e7..79c62de 100644 --- a/ariadne_graphql_modules/subscription_type/graphql_type.py +++ b/ariadne_graphql_modules/subscription_type/graphql_type.py @@ -4,13 +4,19 @@ FieldDefinitionNode, InputValueDefinitionNode, NameNode, - NamedTypeNode, ObjectTypeDefinitionNode, StringValueNode, ) from ariadne.types import Resolver, Subscriber +from ariadne_graphql_modules.base_object_type.graphql_field import ( + GraphQLObjectField, + object_field, + object_resolver, +) +from ariadne_graphql_modules.convert_name import convert_python_name_to_graphql + from ..base_object_type import ( GraphQLFieldData, GraphQLObjectData, @@ -30,7 +36,6 @@ GraphQLObjectResolver, GraphQLObjectSource, GraphQLObjectFieldArg, - get_field_args_from_resolver, get_field_args_from_subscriber, get_field_args_out_names, get_field_node_from_obj_field, @@ -46,6 +51,7 @@ class GraphQLSubscription(GraphQLBaseObject): __graphql_type__ = GraphQLClassType.SUBSCRIPTION __abstract__: bool = True __description__: Optional[str] = None + __graphql_name__ = GraphQLClassType.SUBSCRIPTION.value def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -78,29 +84,6 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectResolver): resolvers[cls_attr.field] = cls_attr.resolver - description_node = get_description_node(cls_attr.description) - if description_node: - descriptions[cls_attr.field] = description_node - - field_args = get_field_args_from_resolver(cls_attr.resolver) - if field_args: - args_descriptions[cls_attr.field] = {} - args_defaults[cls_attr.field] = {} - - final_args = update_field_args_options(field_args, cls_attr.args) - - for arg_name, arg_options in final_args.items(): - arg_description = get_description_node(arg_options.description) - if arg_description: - args_descriptions[cls_attr.field][ - arg_name - ] = arg_description - - arg_default = arg_options.default_value - if arg_default is not None: - args_defaults[cls_attr.field][arg_name] = get_value_node( - arg_default - ) if isinstance(cls_attr, GraphQLObjectSource): subscribers[cls_attr.field] = cls_attr.subscriber description_node = get_description_node(cls_attr.description) @@ -167,7 +150,6 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": ast=ObjectTypeDefinitionNode( name=NameNode(value=definition.name.value), fields=tuple(fields), - interfaces=definition.interfaces, ), resolvers=resolvers, subscribers=subscribers, @@ -204,10 +186,6 @@ def __get_graphql_model_without_schema__( if field.args and field.name: out_names[field.name] = get_field_args_out_names(field.args) - interfaces_ast: List[NamedTypeNode] = [] - for interface_name in type_data.interfaces: - interfaces_ast.append(NamedTypeNode(name=NameNode(value=interface_name))) - return GraphQLSubscriptionModel( name=name, ast_type=ObjectTypeDefinitionNode, @@ -217,7 +195,6 @@ def __get_graphql_model_without_schema__( getattr(cls, "__description__", None), ), fields=tuple(fields_ast), - interfaces=tuple(interfaces_ast), ), resolvers=resolvers, aliases=aliases, @@ -240,32 +217,79 @@ def source( description=description, ) - @classmethod - def _collect_inherited_objects(cls): - return [ - inherited_obj - for inherited_obj in cls.__mro__[1:] - if getattr(inherited_obj, "__graphql_type__", None) - == GraphQLClassType.SUBSCRIPTION - and not getattr(inherited_obj, "__abstract__", True) - ] + @staticmethod + def resolver(field: str, *args, **_): + """Shortcut for object_resolver()""" + return object_resolver( + field=field, + ) + + @staticmethod + def field( + f: Optional[Resolver] = None, + *, + name: Optional[str] = None, + **_, + ) -> Any: + """Shortcut for object_field()""" + return object_field( + f, + name=name, + ) + + @staticmethod + def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): + for attr_name in dir(target_cls): + if attr_name.startswith("__"): + continue + cls_attr = getattr(target_cls, attr_name) + if isinstance(cls_attr, GraphQLObjectField): + if attr_name not in fields_data.fields_order: + fields_data.fields_order.append(attr_name) + + fields_data.fields_names[attr_name] = ( + cls_attr.name or convert_python_name_to_graphql(attr_name) + ) + if cls_attr.resolver: + fields_data.fields_resolvers[attr_name] = cls_attr.resolver + elif isinstance(cls_attr, GraphQLObjectResolver): + fields_data.fields_resolvers[cls_attr.field] = cls_attr.resolver + elif isinstance(cls_attr, GraphQLObjectSource): + if ( + cls_attr.field_type + and cls_attr.field not in fields_data.fields_types + ): + fields_data.fields_types[cls_attr.field] = cls_attr.field_type + if ( + cls_attr.description + and cls_attr.field not in fields_data.fields_descriptions + ): + fields_data.fields_descriptions[cls_attr.field] = ( + cls_attr.description + ) + fields_data.fields_subscribers[cls_attr.field] = cls_attr.subscriber + field_args = get_field_args_from_subscriber(cls_attr.subscriber) + if field_args: + fields_data.fields_args[cls_attr.field] = update_field_args_options( + field_args, cls_attr.args + ) + + elif attr_name not in fields_data.aliases_targets and not callable( + cls_attr + ): + fields_data.fields_defaults[attr_name] = cls_attr @classmethod def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: fields_data = GraphQLFieldData() - inherited_objects = list(reversed(cls._collect_inherited_objects())) - - for inherited_obj in inherited_objects: - fields_data.type_hints.update(inherited_obj.__annotations__) - fields_data.aliases.update(getattr(inherited_obj, "__aliases__", {})) - cls._process_type_hints_and_aliases(fields_data) - - for inherited_obj in inherited_objects: - cls._process_class_attributes(inherited_obj, fields_data) cls._process_class_attributes(cls, fields_data) return GraphQLObjectData( fields=cls._build_fields(fields_data=fields_data), interfaces=[], ) + + @classmethod + def _collect_inherited_objects(cls): + return [] diff --git a/ariadne_graphql_modules/types.py b/ariadne_graphql_modules/types.py index 13f8880..c9021f1 100644 --- a/ariadne_graphql_modules/types.py +++ b/ariadne_graphql_modules/types.py @@ -13,7 +13,7 @@ class GraphQLClassType(Enum): - BASE = "base" - OBJECT = "object" - INTERFACE = "interface" - SUBSCRIPTION = "subscription" + BASE = "Base" + OBJECT = "Object" + INTERFACE = "Interface" + SUBSCRIPTION = "Subscription" diff --git a/tests/conftest.py b/tests/conftest.py index c66282b..cdfd440 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +import glob +import os from pathlib import Path from textwrap import dedent @@ -38,3 +40,10 @@ def datadir() -> Path: @pytest.fixture(scope="session") def original_datadir() -> Path: return Path(__file__).parent / "snapshots" + + +def pytest_sessionfinish(session, exitstatus): + # This will be called after all tests are done + obtained_files = glob.glob("**/*.obtained.yml", recursive=True) + for file in obtained_files: + os.remove(file) diff --git a/tests/snapshots/test_interface_with_schema_object_with_no_schema.yml b/tests/snapshots/test_interface_with_schema_object_with_no_schema.yml new file mode 100644 index 0000000..e583dda --- /dev/null +++ b/tests/snapshots/test_interface_with_schema_object_with_no_schema.yml @@ -0,0 +1,2 @@ +'Class ''UserType'' defines resolver for an undefined field ''score''. (Valid fields: + ''name'')' diff --git a/tests/test_deferred_type.py b/tests/test_deferred_type.py index 7f72937..a878f30 100644 --- a/tests/test_deferred_type.py +++ b/tests/test_deferred_type.py @@ -3,14 +3,23 @@ import pytest from ariadne_graphql_modules import deferred +from ariadne_graphql_modules.deferredtype import ( + DeferredTypeData, + _resolve_module_path_suffix, +) -def test_deferred_returns_deferred_type_with_abs_path(): +def test_deferred_type_data(): + data = DeferredTypeData(path="some.module.path") + assert data.path == "some.module.path" + + +def test_deferred_abs_path(): deferred_type = deferred("tests.types") assert deferred_type.path == "tests.types" -def test_deferred_returns_deferred_type_with_relative_path(): +def test_deferred_relative_path(): class MockType: deferred_type = deferred(".types") @@ -43,3 +52,13 @@ class MockType: deferred_type = deferred("...types") data_regression.check(str(exc_info.value)) + + +def test_resolve_module_path_suffix(): + result = _resolve_module_path_suffix(".types", "current.package") + assert result == "current.package.types" + + +def test_resolve_module_path_suffix_outside_package(): + with pytest.raises(ValueError): + _resolve_module_path_suffix("...module", "current.package") diff --git a/tests/test_description_node.py b/tests/test_description_node.py index 19a6960..2acbe07 100644 --- a/tests/test_description_node.py +++ b/tests/test_description_node.py @@ -1,3 +1,4 @@ +from graphql import StringValueNode from ariadne_graphql_modules import get_description_node @@ -13,23 +14,27 @@ def test_no_description_is_returned_for_empty_str(): def test_description_is_returned_for_str(): description = get_description_node("Example string.") + assert isinstance(description, StringValueNode) assert description.value == "Example string." assert description.block is False def test_description_is_stripped_for_whitespace(): description = get_description_node(" Example string.\n") + assert isinstance(description, StringValueNode) assert description.value == "Example string." assert description.block is False def test_block_description_is_returned_for_multiline_str(): description = get_description_node("Example string.\nNext line.") + assert isinstance(description, StringValueNode) assert description.value == "Example string.\nNext line." assert description.block is True def test_block_description_is_dedented(): description = get_description_node(" Example string.\n Next line.") + assert isinstance(description, StringValueNode) assert description.value == "Example string.\nNext line." assert description.block is True diff --git a/tests/test_enum_type.py b/tests/test_enum_type.py index 8e051d7..903d80a 100644 --- a/tests/test_enum_type.py +++ b/tests/test_enum_type.py @@ -1,6 +1,6 @@ from enum import Enum -from graphql import graphql_sync +from graphql import GraphQLResolveInfo, graphql_sync from ariadne_graphql_modules import ( GraphQLEnum, @@ -61,7 +61,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") - def resolve_level(*_) -> dict: + def resolve_level(*_) -> int: return 0 schema = make_executable_schema(QueryType) @@ -520,3 +520,41 @@ def resolve_level(*_) -> int: assert not result.errors assert result.data == {"level": "ADMIN"} + + +def test_enum_field_as_argument(assert_schema_equals): + class UserLevel(GraphQLEnum): + __members__ = UserLevelEnum + + class QueryType(GraphQLObject): + set_level: UserLevel + + @GraphQLObject.resolver( + "set_level", args={"level": GraphQLObject.argument(graphql_type=UserLevel)} + ) + def resolve_level( + obj, info: GraphQLResolveInfo, *, level: UserLevelEnum + ) -> UserLevelEnum: + return level + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + setLevel(level: UserLevel!): UserLevel! + } + + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """, + ) + + result = graphql_sync(schema, "{ setLevel(level: GUEST) }") + + assert not result.errors + assert result.data == {"setLevel": "GUEST"} diff --git a/tests/test_id_type.py b/tests/test_id_type.py index 9d53923..af472f6 100644 --- a/tests/test_id_type.py +++ b/tests/test_id_type.py @@ -56,6 +56,11 @@ def test_graphql_id_can_be_compared_to_int(): assert GraphQLID(123) != 321 +def test_graphql_id_can_be_compared_to_others(): + assert GraphQLID("123") != 123.0 + assert GraphQLID(123) != 123.0 + + def test_graphql_id_object_field_type_hint(assert_schema_equals): class QueryType(GraphQLObject): id: GraphQLID diff --git a/tests/test_input_type.py b/tests/test_input_type.py index d5d09bd..28f4676 100644 --- a/tests/test_input_type.py +++ b/tests/test_input_type.py @@ -626,3 +626,81 @@ def resolve_search(*_, input: SearchInput) -> str: } """, ) + + +def test_input_type_omit_magic_fields(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(description="Hello world.") + __i_am_magic_field__: str + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return str(input) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: SearchInput!): String! + } + + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + """, + ) + + +def test_input_type_in_input_type_fields(assert_schema_equals): + class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(description="Hello world.") + + class UserInput(GraphQLInput): + username: str + + class MainInput(GraphQLInput): + search = GraphQLInput.field( + description="Hello world.", graphql_type=SearchInput + ) + search_user: UserInput + __i_am_magic_field__: str + + class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: MainInput) -> str: + return str(input) + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + type Query { + search(input: MainInput!): String! + } + + input MainInput { + searchUser: UserInput! + + \"\"\"Hello world.\"\"\" + search: SearchInput! + } + + input SearchInput { + \"\"\"Hello world.\"\"\" + query: String! + } + + input UserInput { + username: String! + } + """, + ) diff --git a/tests/test_interface_type.py b/tests/test_interface_type.py index 6ff2bbb..93f98b8 100644 --- a/tests/test_interface_type.py +++ b/tests/test_interface_type.py @@ -161,7 +161,7 @@ class UserType(GraphQLObject): } """ - __implements__ = [UserInterface] + __requires__ = [UserInterface] class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] @@ -206,7 +206,7 @@ def search(*_) -> List[Union[UserType, CommentType]]: ) -def test_interface_inheritance2(assert_schema_equals): +def test_interface_inherit_interface(assert_schema_equals): class BaseEntityInterface(GraphQLInterface): id: GraphQLID @@ -242,7 +242,7 @@ def users(*_) -> List[UserInterface]: users: [UserInterface!]! } - interface UserInterface { + interface UserInterface implements BaseEntityInterface { id: ID! username: String! } @@ -367,3 +367,79 @@ def users(*_) -> List[UserInterface]: assert not result.errors assert result.data == {"users": [{"__typename": "My", "score": 200}]} + + +def test_interface_with_schema_object_with_schema(assert_schema_equals): + class UserInterface(GraphQLInterface): + __schema__ = """ + interface UserInterface { + summary: String! + score: Int! + } + """ + + @GraphQLInterface.resolver("summary") + def resolve_summary(*_): + return "base_line" + + class UserType(GraphQLObject): + __schema__ = """ + type User implements UserInterface { + summary: String! + score: Int! + name: String! + } + """ + __requires__ = [UserInterface] + + class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(name="Bob"), + CommentType(id=2, content="Hello World!"), + ] + + schema = make_executable_schema(QueryType, UserType) + + assert_schema_equals( + schema, + """ + type Query { + search: [Result!]! + } + + union Result = User | Comment + + type User implements UserInterface { + summary: String! + score: Int! + name: String! + } + + interface UserInterface { + summary: String! + score: Int! + } + + type Comment { + id: ID! + content: String! + } + + """, + ) + result = graphql_sync(schema, "{ search { ... on User{ summary } } }") + + assert not result.errors + assert result.data == { + "search": [ + { + "summary": "base_line", + }, + {}, + ] + } diff --git a/tests/test_interface_type_validation.py b/tests/test_interface_type_validation.py index d441080..7994fd8 100644 --- a/tests/test_interface_type_validation.py +++ b/tests/test_interface_type_validation.py @@ -1,3 +1,4 @@ +from typing import List import pytest from ariadne_graphql_modules import ( @@ -21,3 +22,24 @@ class UserType(GraphQLObject, BaseInterface): make_executable_schema(UserType) data_regression.check(str(exc_info.value)) + + +def test_interface_with_schema_object_with_no_schema(data_regression): + with pytest.raises(ValueError) as exc_info: + + class UserInterface(GraphQLInterface): + __schema__ = """ + interface UserInterface { + summary: String! + score: Int! + } + """ + + @GraphQLInterface.resolver("score") + def resolve_score(*_): + return 2211 + + class UserType(GraphQLObject, UserInterface): + name: str + + data_regression.check(str(exc_info.value)) diff --git a/tests/test_make_executable_schema.py b/tests/test_make_executable_schema.py index 635901b..cacdf0b 100644 --- a/tests/test_make_executable_schema.py +++ b/tests/test_make_executable_schema.py @@ -102,7 +102,7 @@ class ThirdRoot(GraphQLObject): score: int - schema = make_executable_schema(FirstRoot, SecondRoot, ThirdRoot) + schema = make_executable_schema([FirstRoot, SecondRoot, ThirdRoot]) assert_schema_equals( schema, diff --git a/tests/test_scalar_type.py b/tests/test_scalar_type.py index a880d5b..03e34e3 100644 --- a/tests/test_scalar_type.py +++ b/tests/test_scalar_type.py @@ -19,6 +19,10 @@ def serialize(cls, value): return str(value) +class SerializeTestScalar(GraphQLScalar[str]): + pass + + def test_scalar_field_returning_scalar_instance(assert_schema_equals): class QueryType(GraphQLObject): date: DateScalar @@ -84,6 +88,33 @@ def serialize(cls, value): return str(value) +def test_unwrap_scalar_field_returning_scalar_instance(assert_schema_equals): + class QueryType(GraphQLObject): + test: SerializeTestScalar + + @GraphQLObject.resolver("test", graphql_type=str) + def resolve_date(*_) -> SerializeTestScalar: + return SerializeTestScalar(value="Hello!") + + schema = make_executable_schema(QueryType) + + assert_schema_equals( + schema, + """ + scalar SerializeTest + + type Query { + test: SerializeTest! + } + """, + ) + + result = graphql_sync(schema, "{ test }") + + assert not result.errors + assert result.data == {"test": "Hello!"} + + def test_schema_scalar_field_returning_scalar_instance(assert_schema_equals): class QueryType(GraphQLObject): date: SchemaDateScalar diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index bb43729..5f1baf6 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -32,12 +32,12 @@ async def test_basic_subscription_without_schema(assert_schema_equals): class SubscriptionType(GraphQLSubscription): message_added: Message - @GraphQLSubscription.source("message_added") + @GraphQLSubscription.source("message_added", graphql_type=Message) async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - @GraphQLSubscription.resolver("message_added", graphql_type=Message) + @GraphQLSubscription.resolver("message_added") async def resolve_message_added(message, info): return message @@ -84,13 +84,82 @@ def search_sth(*_) -> str: @pytest.mark.asyncio -async def test_subscription_with_arguments_without_schema(assert_schema_equals): +async def test_basic_many_subscription_without_schema(assert_schema_equals): class SubscriptionType(GraphQLSubscription): message_added: Message + @GraphQLSubscription.source("message_added", graphql_type=Message) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.resolver("message_added") + async def resolve_message_added(message, info): + return message + + class SubscriptionSecondType(GraphQLSubscription): + message_added_second: Message + + @GraphQLSubscription.source("message_added_second", graphql_type=Message) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.resolver("message_added_second") + async def resolve_message_added(message, info): + return message + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, SubscriptionSecondType) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + messageAdded: Message! + messageAddedSecond: Message! + } + + type Message { + id: ID! + content: String! + author: String! + } + """, + ) + + query = parse("subscription { messageAdded {id content author} }") + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == { + "messageAdded": {"id": "some_id", "content": "message", "author": "Anon"} + } + + +@pytest.mark.asyncio +async def test_subscription_with_arguments_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + @GraphQLSubscription.source( "message_added", args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, + graphql_type=Message, ) async def message_added_generator(obj, info, channel: GraphQLID): while True: @@ -100,11 +169,8 @@ async def message_added_generator(obj, info, channel: GraphQLID): "author": "Anon", } - @GraphQLSubscription.resolver( - "message_added", - graphql_type=Message, - ) - async def resolve_message_added(message, *_, channel: GraphQLID): + @GraphQLSubscription.field() + def message_added(message, info, channel: GraphQLID): return message class QueryType(GraphQLObject): @@ -161,6 +227,7 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "message_added", args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, + graphql_type=Message, ) async def message_added_generator(obj, info, channel: GraphQLID): while True: @@ -172,13 +239,13 @@ async def message_added_generator(obj, info, channel: GraphQLID): @GraphQLSubscription.resolver( "message_added", - graphql_type=Message, ) async def resolve_message_added(message, *_, channel: GraphQLID): return message @GraphQLSubscription.source( "user_joined", + graphql_type=Message, ) async def user_joined_generator(obj, info): while True: @@ -189,7 +256,6 @@ async def user_joined_generator(obj, info): @GraphQLSubscription.resolver( "user_joined", - graphql_type=Message, ) async def resolve_user_joined(user, *_): return user @@ -251,6 +317,7 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "messages_in_channel", args={"channel_id": GraphQLObject.argument(description="Lorem ipsum.")}, + graphql_type=List[Message], ) async def message_added_generator(obj, info, channel_id: GraphQLID): while True: @@ -264,7 +331,6 @@ async def message_added_generator(obj, info, channel_id: GraphQLID): @GraphQLSubscription.resolver( "messages_in_channel", - graphql_type=Message, ) async def resolve_message_added(message, *_, channel_id: GraphQLID): return message @@ -322,9 +388,10 @@ def search_sth(*_) -> str: async def test_subscription_with_union_without_schema(assert_schema_equals): class SubscriptionType(GraphQLSubscription): notification_received: Notification + __description__ = "test test" @GraphQLSubscription.source( - "notification_received", + "notification_received", description="hello", graphql_type=Message ) async def message_added_generator(obj, info): while True: @@ -333,7 +400,8 @@ async def message_added_generator(obj, info): @GraphQLSubscription.resolver( "notification_received", ) - async def resolve_message_added(message, *_): + @staticmethod + async def resolve_message_added(message: Message, info): return message class QueryType(GraphQLObject): @@ -350,7 +418,9 @@ def search_sth(*_) -> str: searchSth: String! } + \"\"\"test test\"\"\" type Subscription { + \"\"\"hello\"\"\" notificationReceived: Notification! } @@ -394,12 +464,12 @@ class SubscriptionType(GraphQLSubscription): """ ) - @GraphQLSubscription.source("messageAdded") + @GraphQLSubscription.source("messageAdded", graphql_type=Message) async def message_added_generator(obj, info): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} - @GraphQLSubscription.resolver("messageAdded", graphql_type=Message) + @GraphQLSubscription.resolver("messageAdded") async def resolve_message_added(message, info): return message @@ -458,6 +528,7 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "messageAdded", + graphql_type=Message, ) async def message_added_generator(obj, info, channel: GraphQLID): while True: @@ -469,7 +540,6 @@ async def message_added_generator(obj, info, channel: GraphQLID): @GraphQLSubscription.resolver( "messageAdded", - graphql_type=Message, ) async def resolve_message_added(message, *_, channel: GraphQLID): return message @@ -689,22 +759,103 @@ class SubscriptionType(GraphQLSubscription): __schema__ = gql( """ type Subscription { - notificationReceived: Notification! + notificationReceived(channel: String): Notification! + name: String } """ ) + __aliases__ = {"name": "title"} + + @GraphQLSubscription.resolver("notificationReceived") + async def resolve_message_added(message, info, channel: str): + return message @GraphQLSubscription.source( "notificationReceived", + description="my description", + args={ + "channel": GraphQLObject.argument( + description="Lorem ipsum.", default_value="123" + ) + }, + ) + async def message_added_generator(obj, info, channel: str): + while True: + yield Message(id=1, content="content", author="anon") + + class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=str) + def search_sth(*_) -> str: + return "search" + + schema = make_executable_schema(QueryType, SubscriptionType, Notification) + + assert_schema_equals( + schema, + """ + type Query { + searchSth: String! + } + + type Subscription { + \"\"\"my description\"\"\" + notificationReceived( + \"\"\"Lorem ipsum.\"\"\" + channel: String = "123" + ): Notification! + name: String + } + + union Notification = Message | User + + type Message { + id: ID! + content: String! + author: String! + } + + type User { + id: ID! + username: String! + } + """, + ) + + query = parse( + 'subscription { notificationReceived(channel: "hello") ' + "{ ... on Message { id } } }" + ) + sub = await subscribe(schema, query) + + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") + + # Fetch the first result + result = await sub.__anext__() + + # Validate the result + assert not result.errors + assert result.data == {"notificationReceived": {"id": "1"}} + + +@pytest.mark.asyncio +async def test_subscription_descriptions_without_schema(assert_schema_equals): + class SubscriptionType(GraphQLSubscription): + notification_received: Notification + __description__ = "test test" + + @GraphQLSubscription.source( + "notification_received", description="hello", graphql_type=Message ) async def message_added_generator(obj, info): while True: yield Message(id=1, content="content", author="anon") @GraphQLSubscription.resolver( - "notificationReceived", + "notification_received", ) - async def resolve_message_added(message, *_): + @staticmethod + async def resolve_message_added(message: Message, info): return message class QueryType(GraphQLObject): @@ -712,7 +863,7 @@ class QueryType(GraphQLObject): def search_sth(*_) -> str: return "search" - schema = make_executable_schema(QueryType, SubscriptionType, Notification) + schema = make_executable_schema(QueryType, SubscriptionType) assert_schema_equals( schema, @@ -721,7 +872,9 @@ def search_sth(*_) -> str: searchSth: String! } + \"\"\"test test\"\"\" type Subscription { + \"\"\"hello\"\"\" notificationReceived: Notification! } diff --git a/tests/test_union_type.py b/tests/test_union_type.py index da926c5..17fcd29 100644 --- a/tests/test_union_type.py +++ b/tests/test_union_type.py @@ -26,6 +26,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), @@ -88,6 +89,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [] @@ -114,12 +116,13 @@ def search(*_) -> List[Union[UserType, CommentType]]: assert result.data == {"search": []} -def test_union_field_with_invalid_type_access(assert_schema_equals): +def test_union_field_with_invalid_type_access(): class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), @@ -149,7 +152,7 @@ def search(*_) -> List[Union[UserType, CommentType]]: assert "InvalidType" in str(result.errors) -def test_serialization_error_handling(assert_schema_equals): +def test_serialization_error_handling(): class InvalidType: def __init__(self, value): self.value = value @@ -159,6 +162,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType, InvalidType]]: return [InvalidType("This should cause an error")] @@ -180,7 +184,7 @@ def search(*_) -> List[Union[UserType, CommentType, InvalidType]]: assert result.errors -def test_union_with_schema_definition(assert_schema_equals): +def test_union_with_schema_definition(): class SearchResultUnion(GraphQLUnion): __schema__ = """ union SearchResult = User | Comment @@ -189,6 +193,7 @@ class SearchResultUnion(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[SearchResultUnion]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id="1", username="Alice"), diff --git a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml b/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml deleted file mode 100644 index d16bce5..0000000 --- a/tests_v1/snapshots/test_object_type_raises_error_when_defined_with_invalid_schema_str.obtained.yml +++ /dev/null @@ -1,2 +0,0 @@ -"Syntax Error: Unexpected Name 'typo'.\n\nGraphQL request:1:1\n1 | typo User\n |\ - \ ^" From c8b141bb736bb7853dc4086de48e656c6617f0ab Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 30 Aug 2024 11:20:47 +0200 Subject: [PATCH 50/63] add documentation --- MOVING.md | 179 ----- README.md | 318 +-------- REFERENCE.md | 950 --------------------------- ariadne_graphql_modules/typing.py | 6 + docs/api/base_type.md | 63 ++ docs/api/deferred_type.md | 45 ++ docs/api/enum_type.md | 121 ++++ docs/api/id_type.md | 85 +++ docs/api/input_type.md | 240 +++++++ docs/api/interface_type.md | 119 ++++ docs/api/make_executable_schema.md | 110 ++++ docs/api/object_type.md | 229 +++++++ docs/api/scalar_type.md | 152 +++++ docs/api/subscription_type.md | 136 ++++ docs/api/union_type.md | 106 +++ docs/api/wrap_legacy_types.md | 93 +++ CHANGELOG.md => docs/changelog.md | 10 + docs/contributing.md | 8 + docs/index.md | 93 +++ docs/migration_guide.md | 38 ++ mkdocs.yml | 62 ++ pyproject.toml | 4 +- tests/test_make_executable_schema.py | 39 +- 23 files changed, 1785 insertions(+), 1421 deletions(-) delete mode 100644 MOVING.md delete mode 100644 REFERENCE.md create mode 100644 docs/api/base_type.md create mode 100644 docs/api/deferred_type.md create mode 100644 docs/api/enum_type.md create mode 100644 docs/api/id_type.md create mode 100644 docs/api/input_type.md create mode 100644 docs/api/interface_type.md create mode 100644 docs/api/make_executable_schema.md create mode 100644 docs/api/object_type.md create mode 100644 docs/api/scalar_type.md create mode 100644 docs/api/subscription_type.md create mode 100644 docs/api/union_type.md create mode 100644 docs/api/wrap_legacy_types.md rename CHANGELOG.md => docs/changelog.md (53%) create mode 100644 docs/contributing.md create mode 100644 docs/index.md create mode 100644 docs/migration_guide.md create mode 100644 mkdocs.yml diff --git a/MOVING.md b/MOVING.md deleted file mode 100644 index 2480191..0000000 --- a/MOVING.md +++ /dev/null @@ -1,179 +0,0 @@ -Moving guide -============ - -`make_executable_schema` provided by Ariadne GraphQL Modules supports combining old and new approaches for schema definition. This allows developers using either to make a switch. - - -## Updating `make_executable_schema` - -To be able to mix old and new approaches in your implementation, replace `make_executable_schema` imported from `ariadne` with one from `ariadne_graphql_modules`: - -Old code: - -```python -from ariadne import load_schema_from_path, make_executable_schema - -type_defs = load_schema_from_path("schema.graphql") - -schema = make_executable_schema(type_defs, type_a, type_b, type_c) -``` - -New code: - -```python -from ariadne import load_schema_from_path -from ariadne_graphql_modules import make_executable_schema - -type_defs = load_schema_from_path("schema.graphql") - -schema = make_executable_schema( - type_defs, type_a, type_b, type_c, -) -``` - -If you are passing `type_defs` or types as lists (behavior supported by `ariadne.make_executable_schema`), you'll need to unpack them before passing: - -```python -from ariadne import load_schema_from_path -from ariadne_graphql_modules import make_executable_schema - -type_defs = load_schema_from_path("schema.graphql") - -schema = make_executable_schema( - type_defs, type_a, *user_types, -) -``` - - -If you are using directives, `directives` option is named `extra_directives` in new function: - -```python -from ariadne import load_schema_from_path -from ariadne_graphql_modules import make_executable_schema - -type_defs = load_schema_from_path("schema.graphql") - -schema = make_executable_schema( - type_defs, type_a, type_b, type_c, - extra_directives={"date": MyDateDirective}, -) -``` - -Your resulting schema will remain the same. But you can now pass new types defined using modular approach to add or replace existing ones. - - -## Using old types in requirements - -Use `DeferredType` to satisfy requirement on types defined old way: - -```python -from ariadne_graphql_modules import DeferredType, ObjectType, gql - - -class UserType(ObjectType): - __schema__ = gql( - """ - type User { - id: ID! - name: String! - group: UserGroup! - } - """ - ) - __requires__ = [DeferredType("UserGroup")] -``` - - -## Old types depending on new ones - -In case when old type depends on new one, you only need to make new type known to `make_executable_schema`. This can be done by passing it directly to types list or through requires of other type. - -```python -from ariadne import QueryType, gql -from ariadne_graphql_modules import ObjectType, make_executable_schema - -type_defs = gql( - """ - type Query { - user: User! - } - """ -) - -query_type = QueryType() - -@query_type.field("user") -def resolve_user(*_): - return { - "id": 1, - "name": "Alice", - } - - -class UserType(ObjectType): - __schema__ = gql( - """ - type User { - id: ID! - name: String! - } - """ - ) - - -schema = make_executable_schema( - UserType, type_defs, query_type -) -``` - - -## Combining roots - -If `combine_roots` option of `make_executable_schema` is enabled (default), `Query`, `Mutation` and `Subscription` types defined both old and new way will be combined in final schema: - -```python -from ariadne import QueryType, gql -from ariadne_graphql_modules import ObjectType, make_executable_schema - -type_defs = gql( - """ - type Query { - random: Int! - } - """ -) - -query_type = QueryType() - -@query_type.field("random") -def resolve_random(*_): - return 6 - - -class NewQueryType(ObjectType): - __schema__ = gql( - """ - type Query { - year: Int! - } - """ - ) - - @staticmethod - def resolve_year(*_): - return 2022 - - -schema = make_executable_schema( - NewQueryType, type_defs, query_type -) -``` - -Final `Query` type: - -```graphql -type Query { - random: Int! - year: Int! -} -``` \ No newline at end of file diff --git a/README.md b/README.md index bd2c3a3..c88a1a7 100644 --- a/README.md +++ b/README.md @@ -2,277 +2,49 @@ [![Build Status](https://github.com/mirumee/ariadne-graphql-modules/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/mirumee/ariadne-graphql-modules/actions) -- - - - - +## ⚠️ Important Migration Warning: Version 1.0.0 -# Ariadne GraphQL Modules - -Ariadne package for implementing Ariadne GraphQL schemas using modular approach. - -For reasoning behind this work, please see [this GitHub discussion](https://github.com/mirumee/ariadne/issues/306). - -See [API reference](./REFERENCE.md) file for documentation. - - -## Installation - -Ariadne GraphQL Modules can be installed using pip: - -```console -pip install ariadne-graphql-modules -``` - -Ariadne 0.22 or later is required for library to work. - - -## Examples - -### Basic example - -```python -from datetime import date - -from ariadne.asgi import GraphQL -from ariadne_graphql_modules import ObjectType, gql, make_executable_schema - - -class Query(ObjectType): - __schema__ = gql( - """ - type Query { - message: String! - year: Int! - } - """ - ) - - @staticmethod - def resolve_message(*_): - return "Hello world!" - - @staticmethod - def resolve_year(*_): - return date.today().year - - -schema = make_executable_schema(Query) -app = GraphQL(schema=schema, debug=True) -``` - - -### Dependency injection - -If `__schema__` string contains other type, its definition should be provided via `__requires__` attribute: - -```python -from typing import List, Optional - -from ariadne.asgi import GraphQL -from ariadne_graphql_modules import ObjectType, gql, make_executable_schema - -from my_app.users import User, get_user, get_last_users - - -class UserType(ObjectType): - __schema__ = gql( - """ - type User { - id: ID! - name: String! - email: String - } - """ - ) - - @staticmethod - def resolve_email(user: User, info): - if info.context["is_admin"]: - return user.email - - return None - - -class UsersQueries(ObjectType): - __schema__ = gql( - """ - type Query { - user(id: ID!): User - users: [User!]! - } - """ - ) - __requires__ = [UserType] - - @staticmethod - def resolve_user(*_, id: string) -> Optional[User]: - return get_user(id=id) - - @staticmethod - def resolve_users(*_, id: string) -> List[User]: - return get_last_users() - - -# UsersQueries already knows about `UserType` so it can be omitted -# in make_executable_schema arguments -schema = make_executable_schema(UsersQueries) -app = GraphQL(schema=schema, debug=True) -``` - - -#### Deferred dependencies - -Optionally dependencies can be declared as deferred so they can be provided directly to `make_executable_schema`: - -```python -from typing import List, Optional - -from ariadne.asgi import GraphQL -from ariadne_graphql_modules import DeferredType, ObjectType, gql, make_executable_schema - -from my_app.users import User, get_user, get_last_users - - -class UserType(ObjectType): - __schema__ = gql( - """ - type User { - id: ID! - name: String! - email: String - } - """ - ) - - @staticmethod - def resolve_email(user: User, info): - if info.context["is_admin"]: - return user.email - - return None - - -class UsersQueries(ObjectType): - __schema__ = gql( - """ - type Query { - user(id: ID!): User - users: [User!]! - } - """ - ) - __requires__ = [DeferredType("User")] - - @staticmethod - def resolve_user(*_, id: string) -> Optional[User]: - return get_user(id=id) - - @staticmethod - def resolve_users(*_, id: string) -> List[User]: - return get_last_users() - - -schema = make_executable_schema(UserType, UsersQueries) -app = GraphQL(schema=schema, debug=True) -``` +With the release of version 1.0.0, there have been significant changes to the `ariadne_graphql_modules` API. If you are upgrading from a previous version, **you will need to update your imports** to ensure your code continues to function correctly. +### What You Need to Do -### Automatic case convertion between `python_world` and `clientWorld` +To maintain compatibility with existing code, you must explicitly import types from the `v1` module of `ariadne_graphql_modules`. This is necessary for any code that relies on the legacy API from versions prior to 1.0.0. -#### Resolving fields values +### Example -Use `__aliases__ = convert_case` to automatically set aliases for fields that convert case +**Before upgrading:** ```python -from ariadne_graphql_modules import ObjectType, convert_case, gql - - -class UserType(ObjectType): - __schema__ = gql( - """ - type User { - id: ID! - fullName: String! - } - """ - ) - __aliases__ = convert_case +from ariadne_graphql_modules import ObjectType, EnumType ``` - -#### Converting fields arguments - -Use `__fields_args__ = convert_case` on type to automatically convert field arguments to python case in resolver kwargs: +**After upgrading to 1.0.0:** ```python -from ariadne_graphql_modules import MutationType, convert_case, gql - -from my_app import create_user - - -class UserRegisterMutation(MutationType): - __schema__ = gql( - """ - type Mutation { - registerUser(fullName: String!, email: String!): Boolean! - } - """ - ) - __fields_args__ = convert_case - - @staticmethod - async def resolve_mutation(*_, full_name: str, email: str): - user = await create_user( - full_name=full_name, - email=email, - ) - return bool(user) +from ariadne_graphql_modules.v1 import ObjectType, EnumType ``` +### Why This Change? -#### Converting inputs fields - -Use `__args__ = convert_case` on type to automatically convert input fields to python case in resolver kwargs: - -```python -from ariadne_graphql_modules import InputType, MutationType, convert_case, gql - -from my_app import create_user +The introduction of version 1.0.0 brings a more robust and streamlined API, with better support for modular GraphQL schemas. To facilitate this, legacy types and functionality have been moved to the `v1` submodule, allowing new projects to take full advantage of the updated architecture while providing a clear path for migrating existing codebases. +# Ariadne GraphQL Modules -class UserRegisterInput(InputType): - __schema__ = gql( - """ - input UserRegisterInput { - fullName: String! - email: String! - } - """ - ) - __args__ = convert_case +**Ariadne GraphQL Modules** is an extension for the [Ariadne](https://ariadnegraphql.org/) framework, designed to help developers structure and manage GraphQL schemas in a modular way. This library provides an organized approach to building GraphQL APIs by dividing your schema into self-contained, reusable modules, each responsible for its own part of the schema. +## Installation -class UserRegisterMutation(MutationType): - __schema__ = gql( - """ - type Mutation { - registerUser(input: UserRegisterInput!): Boolean! - } - """ - ) - __requires__ = [UserRegisterInput] +Ariadne GraphQL Modules can be installed using pip: - @staticmethod - async def resolve_mutation(*_, input: dict): - user = await create_user( - full_name=input["full_name"], - email=input["email"], - ) - return bool(user) +```bash +pip install ariadne-graphql-modules ``` +Ariadne 0.23 or later is required for the library to work. -### Roots merging +## Basic Usage -`Query`, `Mutation` and `Subscription` types are automatically merged into one by `make_executable_schema`: +Here is a basic example of how to use Ariadne GraphQL Modules to create a simple GraphQL API: ```python from datetime import date @@ -281,25 +53,12 @@ from ariadne.asgi import GraphQL from ariadne_graphql_modules import ObjectType, gql, make_executable_schema -class YearQuery(ObjectType): - __schema__ = gql( - """ - type Query { - year: Int! - } - """ - ) - - @staticmethod - def resolve_year(*_): - return date.today().year - - -class MessageQuery(ObjectType): +class Query(ObjectType): __schema__ = gql( """ type Query { message: String! + year: Int! } """ ) @@ -308,35 +67,14 @@ class MessageQuery(ObjectType): def resolve_message(*_): return "Hello world!" + @staticmethod + def resolve_year(*_): + return date.today().year -schema = make_executable_schema(YearQuery, MessageQuery) -app = GraphQL(schema=schema, debug=True) -``` - -Final schema will contain single `Query` type thats result of merged tupes: -```graphql -type Query { - message: String! - year: Int! -} +schema = make_executable_schema(Query) +app = GraphQL(schema=schema, debug=True) ``` -Fields on final type will be ordered alphabetically. - - -## Moving declarations from Ariadne - -Ariadne GraphQL Modules support combining old and new approaches to schema definition. - -See [moving guide](./MOVING.md) for examples and details. - - -## Contributing - -We are welcoming contributions to Ariadne GraphQL Modules! If you've found a bug or issue, feel free to use [GitHub issues](https://github.com/mirumee/ariadne/issues). If you have any questions or feedback, please let us know via [GitHub discussions](https://github.com/mirumee/ariadne/discussions/). - -Also make sure you follow [@AriadneGraphQL](https://twitter.com/AriadneGraphQL) on Twitter for latest updates, news and random musings! +In this example, a simple `Query` type is defined within a module. The `make_executable_schema` function is then used to combine the module into a complete schema, which can be used to create a GraphQL server. -**Crafted with ❤️ by [Mirumee Software](http://mirumee.com)** -hello@mirumee.com diff --git a/REFERENCE.md b/REFERENCE.md deleted file mode 100644 index e51ffc0..0000000 --- a/REFERENCE.md +++ /dev/null @@ -1,950 +0,0 @@ -# API Reference - -- [ObjectType](#ObjectType) -- [MutationType](#MutationType) -- [SubscriptionType](#SubscriptionType) -- [InputType](#InputType) -- [ScalarType](#ScalarType) -- [EnumType](#EnumType) -- [InterfaceType](#InterfaceType) -- [UnionType](#UnionType) -- [DirectiveType](#DirectiveType) -- [DeferredType](#DeferredType) -- [CollectionType](#CollectionType) -- [BaseType](#BaseType) -- [DefinitionType](#DefinitionType) -- [BindableType](#BindableType) -- [make_executable_schema](#make_executable_schema) -- [convert_case](#convert_case) - - -## `ObjectType` - -New `ObjectType` is base class for Python classes representing GraphQL types (either `type` or `extend type`). - - -### `__schema__` - -`ObjectType` key attribute is `__schema__` string that can define only one GraphQL type: - -```python -class QueryType(ObjectType): - __schema__ = """ - type Query { - year: Int! - } - """ -``` - -`ObjectType` implements validation logic for `__schema__`. It verifies that its valid SDL string defining exactly one GraphQL type. - - -### Resolvers - -Resolvers are class methods or static methods named after schema's fields: - -```python -class QueryType(ObjectType): - __schema__ = """ - type Query { - year: Int! - } - """ - - @staticmethod - def resolve_year(_, info: GraphQLResolveInfo) -> int: - return 2022 -``` - -If resolver function is not present for field, default resolver implemented by `graphql-core` will be used in its place. - -In situations when field's name should be resolved to different value, custom mappings can be defined via `__aliases__` attribute: - -```python -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - dateJoined: String! - } - """ - __aliases__ = { - "dateJoined": "date_joined" - } -``` - -Above code will result in Ariadne generating resolver resolving `dateJoined` field to `date_joined` attribute on resolved object. - -If `date_joined` exists as `resolve_date_joined` callable on `ObjectType`, it will be used as resolver for `dateJoined`: - -```python -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - dateJoined: String - } - """ - __aliases__ = { - "dateJoined": "date_joined" - } - - @staticmethod - def resolve_date_joined(user, info) -> Optional[str]: - if can_see_activity(info.context): - return user.date_joined - - return None -``` - - -### `__requires__` - -When GraphQL type requires on other GraphQL type (or scalar/directive etc. ect.) `ObjectType` will raise an error about missing dependency. This dependency can be provided through `__requires__` attribute: - -```python -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - dateJoined: String! - } - """ - - -class UsersGroupType(ObjectType): - __schema__ = """ - type UsersGroup { - id: ID! - users: [User!]! - } - """ - __requires__ = [UserType] -``` - -`ObjectType` verifies that types specified in `__requires__` actually define required types. If `__schema__` in `UserType` is not defining `User`, error will be raised about missing dependency. - -In case of circular dependencies, special `DeferredType` can be used: - -```python -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - dateJoined: String! - group: UsersGroup - } - """ - __requires__ = [DeferredType("UsersGroup")] - - -class UsersGroupType(ObjectType): - __schema__ = """ - type UsersGroup { - id: ID! - users: [User!]! - } - """ - __requires__ = [UserType] -``` - -`DeferredType` makes `UserType` happy about `UsersGroup` dependency, deferring dependency check to `make_executable_schema`. If "real" `UsersGroup` is not provided at that time, error will be raised about missing types required to create schema. - - -### `__fields_args__` - -Optional attribute that can be used to specify custom mappings between GraphQL args and Python kwargs: - -```python -from ariadne_graphql_modules import DeferredType, ObjectType, gql - -from my_app.models import Article - - -class SearchQuery(ObjectType): - __schema__ = gql( - """ - type Query { - search(query: String!, includeDrafts: Boolean): [Article!]! - } - """ - ) - __fields_args__ = { - "includeDrafts": "with_drafts", - } - __requires__ = [DeferredType("Article")] - - @staticmethod - async def resolve_search(*_, query: str, with_drafts: bool | None): - articles = Article.query.search(query) - if not with_drafts: - articles = articles.filter(is_draft=False) - return await articles.all() -``` - - -## `MutationType` - -Convenience type for defining single mutation: - -```python -from ariadne_graphql_modules import MutationType, gql - -from my_app import create_user - - -class UserRegisterMutation(MutationType): - __schema__ = gql( - """ - type Mutation { - registerUser(username: String!, email: String!): Boolean! - } - """ - ) - - @staticmethod - async def resolve_mutation(*_, username: str, email: str): - user = await create_user( - full_name=username, - email=email, - ) - return bool(user) -``` - -Recommended use for this type is to create custom base class for your GraphQL API: - -```python -from ariadne_graphql_modules import MutationType, gql - - -class BaseMutation(MutationType): - __abstract__ = True - - @classmethod - async def resolve_mutation(cls, _, *args, **kwargs): - try: - return await cls.perform_mutation(cls, *args, **kwargs) - except Exception as e: - return {"errors": e} - - @classmethod - def get_error_result(cls, error): - return {"errors": [e]} -``` - - -### `__args__` - -Optional attribute that can be used to specify custom mapping between GraphQL schema and Python: - -```python -from ariadne_graphql_modules import MutationType, gql - -from my_app import create_user - - -class UserRegisterMutation(MutationType): - __schema__ = gql( - """ - type Mutation { - registerUser( - userName: String!, - email: String!, - admin: Boolean, - ): Boolean! - } - """ - ) - __args__ = {"userName": "username", "admin": "is_admin"} - - @staticmethod - async def resolve_mutation(*_, username: str, email: str, is_admin: bool | None): - user = await create_user( - full_name=username, - email=email, - is_admin=bool(is_admin), - ) - return bool(user) -``` - - -## `SubscriptionType` - -Specialized subclass of `ObjectType` that defines GraphQL subscription: - -```python -class ChatSubscriptions(SubscriptionType): - __schema__ = """ - type Subscription { - chat: Chat - } - """ - __requires__ = [ChatType] - - @staticmethod - async def subscribe_chat(*_): - async for event in subscribe("chats"): - yield event["chat_id"] - - @staticmethod - async def resolve_chat(chat_id, *_): - # Optional - return await get_chat_from_db(chat_id) -``` - - -## `InputType` - -Defines GraphQL input: - -```python -class UserCreateInput(InputType): - __schema__ = """ - input UserInput { - name: String! - email: String! - fullName: String! - } - """ - __args__ = { - "fullName": "full_name", - } -``` - -### `__args__` - -Optional attribue `__args__` is a `Dict[str, str]` used to override key names for `dict` representing input's data. - -Following JSON: - -```json -{ - "name": "Alice", - "email:" "alice@example.com", - "fullName": "Alice Chains" -} -``` - -Will be represented as following dict: - -```python -{ - "name": "Alice", - "email": "alice@example.com", - "full_name": "Alice Chains", -} -``` - - -## `ScalarType` - -Allows you to define custom scalar in your GraphQL schema. - -```python -class DateScalar(ScalarType): - __schema__ = "scalar Datetime" - - @staticmethod - def serialize(value) -> str: - # Called by GraphQL to serialize Python value to - # JSON-serializable format - return value.strftime("%Y-%m-%d") - - @staticmethod - def parse_value(value) -> str: - # Called by GraphQL to parse JSON-serialized value to - # Python type - parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d") - return parsed_datetime.date() -``` - -Note that those methods are only required if Python type is not JSON serializable, or you want to customize its serialization process. - -Additionally you may define third method called `parse_literal` that customizes value's deserialization from GraphQL query's AST, but this is only useful for complex types that represent objects: - -```python -from graphql import StringValueNode - - -class DateScalar(Scalar): - __schema__ = "scalar Datetime" - - @staticmethod - def def parse_literal(ast, variable_values: Optional[Dict[str, Any]] = None): - if not isinstance(ast, StringValueNode): - raise ValueError() - - parsed_datetime = datetime.strptime(ast.value, "%Y-%m-%d") - return parsed_datetime.date() -``` - -If you won't define `parse_literal`, GraphQL will use custom logic that will unpack value from AST and then call `parse_value` on it. - - -## `EnumType` - -Defines enum in GraphQL schema: - -```python -class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } - """ -``` - -`__enum__` attribute allows you to specify Python enum to represent GraphQL enum in your Python logic: - -```python -class UserRole(IntEnum): - USER = 0 - MOD = 1 - ADMIN = 1 - - -class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } - """ - __enum__ = UserRole -``` - -You can also make `__enum__` a dict to skip enum if you want: - -```python -class UserRoleEnum(EnumType): - __schema__ = """ - enum UserRole { - USER - MOD - ADMIN - } - """ - __enum__ = { - "USER": 0, - "MOD": 1, - "ADMIN": 2, - } -``` - - -## `InterfaceType` - -Defines interface in GraphQL schema: - -```python -class SearchResultInterface(InterfaceType): - __schema__ = """ - interface SearchResult { - summary: String! - score: Int! - } - """ - - @staticmethod - def resolve_type(obj, info): - # Returns string with name of GraphQL type representing Python type - # from your business logic - if isinstance(obj, UserModel): - return UserType.graphql_name - - if isinstance(obj, CommentModel): - return CommentType.graphql_name - - return None - - @staticmethod - def resolve_summary(obj, info): - # Optional default resolver for summary field, used by types implementing - # this interface when they don't implement their own -``` - - -## `UnionType` - -Defines GraphQL union: - -```python -class SearchResultUnion(UnionType): - __schema__ = "union SearchResult = User | Post | Thread" - __requires__ = [UserType, PostType, ThreadType] - - @staticmethod - def resolve_type(obj, info): - # Returns string with name of GraphQL type representing Python type - # from your business logic - if isinstance(obj, UserModel): - return UserType.graphql_name - - if isinstance(obj, PostModel): - return PostType.graphql_name - - if isinstance(obj, ThreadModel): - return ThreadType.graphql_name - - return None -``` - - -## `DirectiveType` - -Defines new GraphQL directive in your schema and specifies `SchemaDirectiveVisitor` for it: - - -```python -from ariadne import SchemaDirectiveVisitor -from graphql import default_field_resolver - - -class PrefixStringSchemaVisitor(SchemaDirectiveVisitor): - def visit_field_definition(self, field, object_type): - original_resolver = field.resolve or default_field_resolver - - def resolve_prefixed_value(obj, info, **kwargs): - result = original_resolver(obj, info, **kwargs) - if result: - return f"PREFIX: {result}" - return result - - field.resolve = resolve_prefixed_value - return field - - -class PrefixStringDirective(DirectiveType): - __schema__ = "directive @example on FIELD_DEFINITION" - __visitor__ = PrefixStringSchemaVisitor -``` - - -## `make_executable_schema` - -New `make_executable_schema` takes list of Ariadne's types and constructs executable schema from them, performing last-stage validation for types consistency: - -```python -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - username: String! - } - """ - - -class QueryType(ObjectType): - __schema__ = """ - type Query { - user: User - } - """ - __requires__ = [UserType] - - @staticmethod - def user(*_): - return { - "id": 1, - "username": "Alice", - } - - -schema = make_executable_schema(QueryType) -``` - - -### Automatic merging of roots - -Passing multiple `Query`, `Mutation` or `Subscription` definitions to `make_executable_schema` by default results in schema defining single types containing sum of all fields defined on those types, ordered alphabetically by field name. - -```python -class UserQueriesType(ObjectType): - __schema__ = """ - type Query { - user(id: ID!): User - } - """ - ... - - -class ProductsQueriesType(ObjectType): - __schema__ = """ - type Query { - product(id: ID!): Product - } - """ - ... - -schema = make_executable_schema(UserQueriesType, ProductsQueriesType) -``` - -Above schema will have single `Query` type looking like this: - -```graphql -type Query { - product(id: ID!): Product - user(id: ID!): User -} -``` - -To opt out of this behavior use `merge_roots=False` option: - -```python -schema = make_executable_schema( - UserQueriesType, - ProductsQueriesType, - merge_roots=False, -) -``` - -## `DeferredType` - -`DeferredType` names required GraphQL type as provided at later time: - -- Via `make_executable_schema` call -- Or via other type's `__requires__` - -It's mostly used to define lazy relationships in reusable modules and to break circular relationships. - - -### Type with `DeferredType` and other type passed to `make_executable_schema`: - -```python -class QueryType(ObjectType): - __schema__ = """ - type Query { - id: ID! - users: [User!]! - } - """ - __requires__ = [DeferredType("User")] - - -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - dateJoined: String! - } - """ - - -schema = make_excutable_schema(QueryType, UserType) -``` - - -### Type with `DeferredType` and other type passed as dependency of third type: - -```python -class UsersGroupType(ObjectType): - __schema__ = """ - type UsersGroup { - id: ID! - users: [User!]! - } - """ - __requires__ = [UserType] - - -class UserType(ObjectType): - __schema__ = """ - type User { - id: ID! - dateJoined: String! - } - """ - - -class QueryType(ObjectType): - __schema__ = """ - type Query { - id: ID! - users: [User!]! - groups: [UsersGroup!]! - } - """ - __requires__ = [DeferredType("User"), UsersGroupType] - - -schema = make_excutable_schema(QueryType) -``` - - -## `CollectionType` - -Collection is an utility type that gathers multiple types into single object: - -```python -class UserMutations(CollectionType): - __types__ = [ - BanUserMutation, - UnbanUserMutation, - CreateUserMutation, - UpdateUserMutation, - DeleteUserMutation, - ] - - -schema = make_excutable_schema(UserMutations) -``` - - -## `BaseType` - -Base type that all other types extend. You can use it to create custom types: - -```python -from typing import Dict - -from ariadne_graphql_modules import BaseType -from django.db.models import Model -from graphql import GraphQLFieldResolver - -class MyType(BaseType) - __abstract__ = True - - @classmethod - def __get_types__(cls) -> List[Type["BaseType"]]: - # Called by make_executable_schema to get list of types - # to build GraphQL schema from. - return [] -``` - - -## `DefinitionType` - -Base for types that define `__schema__`: - -```python -class MyType(DefinitionType) - __abstract__: bool = True - __schema__: str - __requires__: List[Union[Type["DefinitionType"], DeferredType]] = [] - - graphql_name: str - graphql_type: Type[DefinitionNode] -``` - -Extends `BaseType`. - - -## `BindableType` - -Base for types that define `__bind_to_schema__` class method: - -```python -class MyType(BindableType) - __abstract__: bool = True - __schema__: str - __requires__: List[Union[Type["DefinitionType"], DeferredType]] = [] - - graphql_name: str - graphql_type: Type[DefinitionNode] - - @classmethod - def __bind_to_schema__(cls, schema: GraphQLSchema): - pass # Bind python logic to GraphQL schema here -``` - -Extends `DefinitionType`. - - -## `make_executable_schema` - -```python -def make_executable_schema( - *args: Union[Type[BaseType], SchemaBindable, str], - merge_roots: bool = True, - extra_sdl: Optional[Union[str, Sequence[str]]] = None, - extra_bindables: Optional[Sequence[SchemaBindable]] = None, - extra_directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, -) -> GraphQLSchema: - ... -``` - -Utility function that takes args with types definitions and creates executable schema. - - -### `merge_roots: bool = True` - -If set to true (default), `make_executable_schema` will automatically merge multiple `Query`, `Mutation` and `Subscription` types instead of raising error. - -Final merged types fields will be ordered alphabetically: - -```python -class YearType(ObjectType): - __schema__ = """ - type Query { - year: Int! - } - """ - - @staticmethod - def resolve_year(*_): - return 2022 - - -class MonthType(ObjectType): - __schema__ = """ - type Query { - month: Int! - } - """ - - @staticmethod - def resolve_month(*_): - return 10 - - -schema = make_executable_schema(YearType, MonthType) - -assert print_schema(schema) == """ -type Query { - month: Int! - year: Int! -} -""" -``` - -When `merge_roots=False` is explicitly set, `make_executable_schema` will raise an GraphQL error that `type Query` is defined more than once. - - -### `extra_directives` - -Dict of Ariadne's directives names and implementation. Optional. - -Used when moving schema definition to Ariadne GraphQL Modules approach from existing schema definition. - -See [moving guide](./MOVING.md) for examples and details. - - -## `convert_case` - -Utility function that can be used to automatically setup case conversion rules for types. - - -#### Resolving fields values - -Use `__aliases__ = convert_case` to automatically set aliases for fields that convert case - -```python -from ariadne_graphql_modules import ObjectType, convert_case, gql - - -class UserType(ObjectType): - __schema__ = gql( - """ - type User { - id: ID! - fullName: String! - } - """ - ) - __aliases__ = convert_case -``` - - -#### Converting fields arguments - -Use `__fields_args__ = convert_case` on type to automatically convert field arguments to python case in resolver kwargs: - -```python -from ariadne_graphql_modules import DeferredType, ObjectType, convert_case, gql - -from my_app.models import Article - - -class SearchQuery(ObjectType): - __schema__ = gql( - """ - type Query { - search(query: String!, includeDrafts: Boolean): [Article!]! - } - """ - ) - __fields_args__ = convert_case - __requires__ = [DeferredType("Article")] - - @staticmethod - async def resolve_search(*_, query: str, include_drafts: bool | None): - articles = Article.query.search(query) - if not include_drafts: - articles = articles.filter(is_draft=False) - return await articles.all() -``` - - -#### Converting mutation arguments - -Use `__args__ = convert_case` on `MutationType` to automatically convert input fields to python case in resolver kwargs: - -```python -from ariadne_graphql_modules import MutationType, convert_case, gql - -from my_app import create_user - - -class UserRegisterMutation(MutationType): - __schema__ = gql( - """ - type Mutation { - registerUser(fullName: String!, email: String!): Boolean! - } - """ - ) - __args__ = convert_case - - @staticmethod - async def resolve_mutation(*_, full_name: str, email: str): - user = await create_user( - full_name=full_name, - email=email, - ) - return bool(user) -``` - - -#### Converting inputs fields - -Use `__args__ = convert_case` on `InputType` to automatically convert input fields to python case in resolver kwargs: - -```python -from ariadne_graphql_modules import InputType, MutationType, convert_case, gql - -from my_app import create_user - - -class UserRegisterInput(InputType): - __schema__ = gql( - """ - input UserRegisterInput { - fullName: String! - email: String! - } - """ - ) - __args__ = convert_case - - -class UserRegisterMutation(MutationType): - __schema__ = gql( - """ - type Mutation { - registerUser(input: UserRegisterInput!): Boolean! - } - """ - ) - __requires__ = [UserRegisterInput] - - @staticmethod - async def resolve_mutation(*_, input: dict): - user = await create_user( - full_name=input["full_name"], - email=input["email"], - ) - return bool(user) -``` \ No newline at end of file diff --git a/ariadne_graphql_modules/typing.py b/ariadne_graphql_modules/typing.py index a482c0d..bf902de 100644 --- a/ariadne_graphql_modules/typing.py +++ b/ariadne_graphql_modules/typing.py @@ -142,6 +142,12 @@ def get_graphql_type(annotation: Any) -> Optional[Union[Type[GraphQLType], Type[ if is_nullable(annotation) or is_list(annotation): return get_graphql_type(unwrap_type(annotation)) + if get_origin(annotation) is Annotated: + forward_ref, type_meta = get_args(annotation) + if not type_meta or not isinstance(type_meta, DeferredTypeData): + return None + return get_deferred_type(annotation, forward_ref, type_meta) + if not isclass(annotation): return None diff --git a/docs/api/base_type.md b/docs/api/base_type.md new file mode 100644 index 0000000..9937659 --- /dev/null +++ b/docs/api/base_type.md @@ -0,0 +1,63 @@ + +# `GraphQLType` Class Documentation + +The `GraphQLType` class is a foundational class in the `ariadne_graphql_modules` library. It provides the absolute minimum structure needed to create a custom GraphQL type. This class is designed to be subclassed, allowing developers to define custom types with specific behaviors and properties in a GraphQL schema. + +## Class Attributes + +- **`__graphql_name__`**: *(Optional[str])* The custom name for the GraphQL type. If not provided, it will be generated based on the class name. +- **`__description__`**: *(Optional[str])* A description of the GraphQL type, included in the schema documentation. +- **`__abstract__`**: *(bool)* Indicates whether the class is abstract. Defaults to `True`. + +## Core Methods + +### `__get_graphql_name__(cls) -> str` + +This method returns the GraphQL name of the type. If the `__graphql_name__` attribute is set, it returns that value. Otherwise, it generates a name based on the class name by removing common suffixes like "GraphQL", "Type", etc. + +- **Returns:** `str` - The GraphQL name of the type. + +### `__get_graphql_model__(cls, metadata: "GraphQLMetadata") -> "GraphQLModel"` + +This method must be implemented by subclasses. It is responsible for generating the GraphQL model for the type. The model defines how the type is represented in a GraphQL schema. + +- **Parameters:** + - `metadata` (GraphQLMetadata): Metadata for the GraphQL model. +- **Returns:** `GraphQLModel` - The model representation of the type. + +### `__get_graphql_types__(cls, _: "GraphQLMetadata") -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]` + +This method returns an iterable containing the GraphQL types associated with this type. By default, it returns the class itself. + +- **Parameters:** + - `_` (GraphQLMetadata): Metadata for the GraphQL model (unused in this method). +- **Returns:** `Iterable[Union[Type[GraphQLType], Type[Enum]]]` - The associated GraphQL types. + +## Example Usage + +### Creating a Custom GraphQL Type + +To create a custom GraphQL type, subclass `GraphQLType` and implement the required methods. + +```python +from ariadne_graphql_modules import GraphQLType, GraphQLMetadata, GraphQLModel + +class MyCustomType(GraphQLType): + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> GraphQLModel: + # Implement the logic to create and return a GraphQLModel + pass +``` + +### Custom Naming + +You can specify a custom GraphQL name by setting the `__graphql_name__` attribute: + +```python +class MyCustomType(GraphQLType): + __graphql_name__ = "MyType" + + @classmethod + def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> GraphQLModel: + pass +``` diff --git a/docs/api/deferred_type.md b/docs/api/deferred_type.md new file mode 100644 index 0000000..bd55a24 --- /dev/null +++ b/docs/api/deferred_type.md @@ -0,0 +1,45 @@ +# deferred + +This module provides a mechanism to defer type information, particularly useful for handling forward references and circular dependencies in Python modules. The key components include the `DeferredTypeData` data class and the `deferred` function. + +## deferred Function + +The `deferred` function creates a `DeferredTypeData` object from a given module path. If the module path is relative (i.e., starts with a '.'), the function resolves it based on the caller's package context. + +### Parameters + +- `module_path` (str): The module path where the deferred type resides. This can be an absolute or a relative path. + +### Example Usage + +```python +from ariadne_graphql_modules import deferred + +# Deferring a type with an absolute module path +deferred_type = deferred('some_module.TypeName') + +# Deferring a type with a relative module path +deferred_type_relative = deferred('.TypeName') +``` + +### Error Handling + +- Raises `RuntimeError` if the `deferred` function is not called within a class attribute definition context. +- Raises `ValueError` if the relative module path points outside of the current package. + +### Advanced Usage Example + +This example demonstrates how to use the `deferred` function with `Annotated` for forward type references within a `GraphQLObject`. + +```python +from typing import TYPE_CHECKING, Annotated +from ariadne_graphql_modules import GraphQLObject, deferred + +if TYPE_CHECKING: + from .types import ForwardScalar + +class MockType(GraphQLObject): + field: Annotated["ForwardScalar", deferred("tests.types")] +``` + +In this example, the `deferred` function is used in conjunction with `Annotated` to specify that the `field` in `MockType` references a type (`ForwardScalar`) that is defined in the `tests.types` module. This approach is particularly useful when dealing with forward references or circular dependencies in type annotations. diff --git a/docs/api/enum_type.md b/docs/api/enum_type.md new file mode 100644 index 0000000..a036a3f --- /dev/null +++ b/docs/api/enum_type.md @@ -0,0 +1,121 @@ +# `GraphQLEnum` + +`GraphQLEnum` is a base class for defining GraphQL enum types in a modular and reusable manner. It allows for the creation of enums using Python's `Enum` class or custom mappings, providing flexibility and ease of integration into your GraphQL schema. + +## Inheritance + +- Inherits from `GraphQLType`. + +## Attributes + +- **`__schema__`**: *(Optional[str])* Holds the GraphQL schema definition string for the object type if provided. +- **`__graphql_name__`**: *(Optional[str])* The custom name for the GraphQL object type. +- **`__description__`**: *(Optional)* A description for the enum, which is included in the schema. +- **`__members__`**: Specifies the members of the enum. This can be a Python `Enum`, a dictionary, or a list of strings. +- **`__members_descriptions__`**: *(Optional)* A dictionary mapping enum members to their descriptions. + + +### `graphql_enum` Function + +The `graphql_enum` function is a decorator that simplifies the process of converting a Python `Enum` class into a GraphQL enum type. This function allows you to specify additional properties such as the GraphQL enum's name, description, and member descriptions, as well as control which members are included or excluded in the GraphQL schema. + +#### Parameters + +- **`cls`**: *(Optional[Type[Enum]])* + The Python `Enum` class that you want to convert to a GraphQL enum. This parameter is optional because the function can be used as a decorator. + +- **`name`**: *(Optional[str])* + The name of the GraphQL enum. If not provided, the name of the `Enum` class will be used. + +- **`description`**: *(Optional[str])* + A description for the GraphQL enum, which will be included in the schema. + +- **`members_descriptions`**: *(Optional[Dict[str, str]])* + A dictionary mapping enum members to their descriptions. This allows for detailed documentation of each enum member in the GraphQL schema. + +- **`members_include`**: *(Optional[Iterable[str]])* + A list of member names to include in the GraphQL enum. If not provided, all members of the `Enum` will be included. + +- **`members_exclude`**: *(Optional[Iterable[str]])* + A list of member names to exclude from the GraphQL enum. This allows you to omit specific members from the GraphQL schema. + +#### Returns + +- **`graphql_enum_decorator`**: *(Callable)* + A decorator function that attaches a `__get_graphql_model__` method to the `Enum` class. This method returns the GraphQL model for the enum, making it ready to be integrated into your GraphQL schema. + +## Usage Examples + +### Basic Enum Definition + +Here’s how to define a basic enum type using `GraphQLEnum` with a Python `Enum`: + +```python +from enum import Enum +from ariadne_graphql_modules import GraphQLEnum + +class UserLevelEnum(Enum): + GUEST = 0 + MEMBER = 1 + ADMIN = 2 + +class UserLevel(GraphQLEnum): + __members__ = UserLevelEnum +``` + +### Enum with Custom Schema + +You can define the enum schema directly using SDL: + +```python +class UserLevel(GraphQLEnum): + __schema__ = """ + enum UserLevel { + GUEST + MEMBER + ADMIN + } + """ + __members__ = UserLevelEnum +``` + +### Enum with Descriptions + +You can add descriptions to both the enum and its members: + +```python +class UserLevel(GraphQLEnum): + __description__ = "User access levels." + __members__ = UserLevelEnum + __members_descriptions__ = { + "MEMBER": "A registered user.", + "ADMIN": "An administrator with full access." + } +``` + +### Example Usage of *graphql_enum* function + +Here’s an example of how to use the `graphql_enum` decorator: + +```python +from enum import Enum +from ariadne_graphql_modules import graphql_enum + +@graphql_enum +class SeverityLevel(Enum): + LOW = 0 + MEDIUM = 1 + HIGH = 2 + +# Access the GraphQL model +graphql_model = SeverityLevel.__get_graphql_model__() + +# The GraphQL model can now be used in your schema +``` + +In this example: + +- The `SeverityLevel` enum is decorated with `graphql_enum`, automatically converting it into a GraphQL enum. +- The `__get_graphql_model__` method is added to `SeverityLevel`, which returns the GraphQL model, including the enum name, members, and corresponding AST. + +This function allows for an easy transition from Python enums to GraphQL enums, providing flexibility in customizing the GraphQL schema with descriptions and member selections. diff --git a/docs/api/id_type.md b/docs/api/id_type.md new file mode 100644 index 0000000..ba7f717 --- /dev/null +++ b/docs/api/id_type.md @@ -0,0 +1,85 @@ + +# `GraphQLID` + +The `GraphQLID` class represents a unique identifier in a GraphQL schema. This class is designed to handle ID values in a consistent manner by allowing them to be treated as either strings or integers. + +## Class Attributes + +- `value`: **str**. This attribute stores the string representation of the ID value. + +## Usage + +Below is an example of how the `GraphQLID` class can be used to create and compare ID values: + +```python +id1 = GraphQLID(123) +id2 = GraphQLID("123") +assert id1 == id2 # True, because both represent the same ID +``` + +## Methods + +### `__init__(self, value: Union[int, str])` +The constructor accepts either an integer or a string and stores it as a string in the `value` attribute. + +- **Parameters:** + - `value` (Union[int, str]): The ID value to be stored. + +### `__eq__(self, value: Any) -> bool` +Compares the `GraphQLID` instance with another value. It returns `True` if the other value is either a string, an integer, or another `GraphQLID` instance that represents the same ID. + +- **Parameters:** + - `value` (Any): The value to compare with. +- **Returns:** `bool` - `True` if the values are equal, `False` otherwise. + +### `__int__(self) -> int` +Converts the `GraphQLID` value to an integer. + +- **Returns:** `int` - The integer representation of the ID. + +### `__str__(self) -> str` +Returns the string representation of the `GraphQLID` value. + +- **Returns:** `str` - The string representation of the ID. + +## Example Use Cases + +### 1. Creating IDs +You can create a `GraphQLID` from either an integer or a string, and it will always store the value as a string internally. + +```python +id1 = GraphQLID(123) +id2 = GraphQLID("456") +``` + +### 2. Comparing IDs +The `GraphQLID` class allows for comparison between different types (e.g., integers, strings, or other `GraphQLID` instances) as long as they represent the same ID. + +```python +id1 = GraphQLID(123) +id2 = GraphQLID("123") +assert id1 == id2 # True +``` + +### 3. Converting to Integer or String +You can easily convert a `GraphQLID` to either an integer or a string. + +```python +id1 = GraphQLID(789) +print(int(id1)) # Outputs: 789 +print(str(id1)) # Outputs: "789" +``` + +### 4. Using `GraphQLID` in Other Object Types +The `GraphQLID` type can be used as an identifier in other GraphQL object types, allowing you to define unique fields across your schema. + +```python +from ariadne_graphql_modules import GraphQLObject, GraphQLID + +class Message(GraphQLObject): + id: GraphQLID + content: str + author: str +``` + +In this example, `Message` is a GraphQL object type where `id` is a `GraphQLID`, ensuring each message has a unique identifier. diff --git a/docs/api/input_type.md b/docs/api/input_type.md new file mode 100644 index 0000000..9898812 --- /dev/null +++ b/docs/api/input_type.md @@ -0,0 +1,240 @@ +# `GraphQLInput` + +`GraphQLInput` is a base class used to define GraphQL input types in a modular way. It allows you to create structured input objects with fields, default values, and custom validation, making it easier to handle complex input scenarios in your GraphQL API. + +## Inheritance + +- Inherits from `GraphQLType`. + +## Class Attributes + +- **`__schema__`**: *(Optional[str])* - The GraphQL schema definition string for the union, if provided. +- **`__graphql_name__`**: *(Optional[str])* The custom name for the GraphQL type. If not provided, it will be generated based on the class name. +- **`__description__`**: *(Optional[str])* A description of the GraphQL type, included in the schema documentation. +- **`__out_names__`**: *(Optional)* A dictionary to customize the names of fields when they are used as output in other parts of the schema. + +## Class Methods + +### `field` + +A static method that defines a field within the input type. It allows you to specify the field's name, type, description, and default value. + +```python +@staticmethod +def field( + *, + name: Optional[str] = None, + graphql_type: Optional[Any] = None, + description: Optional[str] = None, + default_value: Optional[Any] = None, +) -> Any: + ... +``` + +## Examples + +### Basic Input Type + +Here’s how to define a basic input type using `GraphQLInput`: + +```python +from ariadne_graphql_modules import GraphQLInput + +class SearchInput(GraphQLInput): + query: str + age: int +``` + +### Input Type with Default Values + +You can define default values for input fields using class attributes: + +```python +class SearchInput(GraphQLInput): + query: str = "default search" + age: int = 18 +``` + +### Input Type with Custom Field Definitions + +You can use the `field` method to define fields with more control over their attributes, such as setting a default value or adding a description: + +```python +class SearchInput(GraphQLInput): + query: str = GraphQLInput.field(default_value="default search", description="Search query") + age: int = GraphQLInput.field(default_value=18, description="Age filter") +``` + +### Using `GraphQLInput` in a Query + +The `GraphQLInput` can be used as an argument in GraphQL object types. This allows you to pass complex structured data to queries or mutations. + +```python +from ariadne_graphql_modules import GraphQLInput, GraphQLObject, make_executable_schema + + +class SearchInput(GraphQLInput): + query: Optional[str] + age: Optional[int] + +class QueryType(GraphQLObject): + search: str + + @GraphQLObject.resolver("search") + def resolve_search(*_, input: SearchInput) -> str: + return f"{repr([input.query, input.age])}" +``` + +In this example, `SearchInput` is used as an argument in the `search` query, allowing clients to pass in a structured input object. + +## Validation + +Validation is a key feature of `GraphQLInput`. The class provides built-in validation to ensure that the input schema and fields are correctly defined and that they adhere to GraphQL standards. + +### Validation Scenarios + +#### 1. Invalid Schema Type + +If the schema defined in `__schema__` does not correspond to an input type, a `ValueError` is raised. + +```python +from ariadne import gql +from ariadne_graphql_modules import GraphQLInput + +try: + class CustomType(GraphQLInput): + __schema__ = gql("scalar Custom") +except ValueError as e: + print(e) # Outputs error regarding invalid type schema +``` + +#### 2. Name Mismatch + +If the class name and the name defined in the schema do not match, a `ValueError` is raised. + +```python +try: + class CustomType(GraphQLInput): + __graphql_name__ = "Lorem" + __schema__ = gql( + ''' + input Custom { + hello: String! + } + ''' + ) +except ValueError as e: + print(e) # Outputs error regarding name mismatch +``` + +#### 3. Duplicate Descriptions + +If both the class and the schema define a description, a `ValueError` is raised due to the conflict. + +```python +try: + class CustomType(GraphQLInput): + __description__ = "Hello world!" + __schema__ = gql( + """ + Other description + """ + input Custom { + hello: String! + } + ''' + ) +except ValueError as e: + print(e) # Outputs error regarding duplicate descriptions +``` + +#### 4. Missing Fields in Schema + +If the input schema is missing fields, a `ValueError` is raised. + +```python +try: + class CustomType(GraphQLInput): + __schema__ = gql("input Custom") +except ValueError as e: + print(e) # Outputs error regarding missing fields +``` + +#### 5. Invalid `__out_names__` without Schema + +If `__out_names__` is defined without a schema, a `ValueError` is raised since this feature requires an explicit schema. + +```python +try: + class CustomType(GraphQLInput): + hello: str + + __out_names__ = { + "hello": "ok", + } +except ValueError as e: + print(e) # Outputs error regarding unsupported out_names without schema +``` + +#### 6. Invalid or Duplicate Out Names + +If an out name is defined for a non-existent field or if there are duplicate out names, a `ValueError` is raised. + +```python +try: + class CustomType(GraphQLInput): + __schema__ = gql( + ''' + input Query { + hello: String! + } + ''' + ) + + __out_names__ = { + "invalid": "ok", # Invalid field name + } +except ValueError as e: + print(e) # Outputs error regarding invalid out_name + +try: + class CustomType(GraphQLInput): + __schema__ = gql( + ''' + input Query { + hello: String! + name: String! + } + ''' + ) + + __out_names__ = { + "hello": "ok", + "name": "ok", # Duplicate out name + } +except ValueError as e: + print(e) # Outputs error regarding duplicate out_names +``` + +#### 7. Unsupported Default Values + +If a default value cannot be represented in the GraphQL schema, a `TypeError` is raised. + +```python +class InvalidType: + pass + +try: + class QueryType(GraphQLInput): + attr: str = InvalidType() +except TypeError as e: + print(e) # Outputs error regarding unsupported default value + +try: + class QueryType(GraphQLInput): + attr: str = GraphQLInput.field(default_value=InvalidType()) +except TypeError as e: + print(e) # Outputs error regarding unsupported field default option +``` + +These validation mechanisms ensure that your `GraphQLInput` types are correctly configured and adhere to GraphQL standards, helping you catch errors early in development. diff --git a/docs/api/interface_type.md b/docs/api/interface_type.md new file mode 100644 index 0000000..c24c8ce --- /dev/null +++ b/docs/api/interface_type.md @@ -0,0 +1,119 @@ + +# `GraphQLInterface` + +`GraphQLInterface` is a base class used to define GraphQL interfaces in a modular and reusable manner. It provides a structured way to create interfaces with fields, resolvers, and descriptions, making it easier to manage complex GraphQL schemas. + +## Inheritance + +- Inherits from `GraphQLBaseObject`. + +## Class Attributes + +- **`__schema__`**: *(Optional[str])* - The GraphQL schema definition string for the union, if provided. +- **`__graphql_name__`**: *(Optional[str])* The custom name for the GraphQL type. If not provided, it will be generated based on the class name. +- **`__description__`**: *(Optional[str])* A description of the GraphQL type, included in the schema documentation. +- **`__graphql_type__`**: Defines the GraphQL class type, set to `GraphQLClassType.BASE`. +- **`__aliases__`**: *(Optional[Dict[str, str]])* Defines field aliases for the object type. +- **`__requires__`**: *(Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]])* Specifies other types or enums that are required by this object type. + +## Core Methods + +### `resolve_type` + +A static method that resolves the type of an object when using interfaces or unions. This method can be overridden to provide custom type resolution logic. + +### `resolver` + +Defines a resolver for a subscription. The resolver processes the data provided by the source before sending it to the client. + +- **Parameters:** + - `field: str`: The name of the resolver field. + +### `field` + +Defines a field in the subscription type. This is a shortcut for defining subscription fields without a full schema. + +- **Parameters:** + - `name: Optional[str]`: The name of the field that will be created. + +## Inheritance feature + +The `GraphQLInterface` class supports inheritance, allowing interfaces to extend other interfaces. This enables the creation of complex and reusable schemas by composing interfaces from other interfaces. + +- **Interface Inheritance**: When an interface inherits from another interface, it automatically implements the inherited interface, and the `implements` clause will be included in the GraphQL schema. +- **Inheritance Example**: + +```python +class BaseInterface(GraphQLInterface): + summary: str + +class AdvancedInterface(BaseInterface): + details: str +``` + +In this example, `AdvancedInterface` inherits from `BaseInterface`, meaning it will include all fields and logic from `BaseInterface`, and the resulting schema will reflect that `AdvancedInterface` implements `BaseInterface`. + +## Example + +### Basic Interface Definition + +Here’s how to define a basic interface using `GraphQLInterface`: + +```python +from ariadne_graphql_modules import GraphQLInterface + +class UserInterface(GraphQLInterface): + summary: str + score: int +``` + +### Interface with Schema Definition + +You can define the interface schema directly using SDL: + +```python +class UserInterface(GraphQLInterface): + __schema__ = ''' + interface UserInterface { + summary: String! + score: Int! + } + ''' +``` + +### Custom Type Resolution + +You can implement custom logic for resolving types when using the interface: + +```python +class UserInterface(GraphQLInterface): + @staticmethod + def resolve_type(obj, *_): + if isinstance(obj, UserType): + return "UserType" + raise ValueError(f"Cannot resolve type for object {obj}.") +``` + +## Validation + +The `GraphQLInterface` class includes validation logic to ensure that the defined interface schema is correct and consistent with the class attributes. This validation process includes: + +- **Schema Validation**: If the `__schema__` attribute is defined, it is parsed and validated to ensure that it corresponds to a valid GraphQL interface. +- **Name and Description Validation**: Validates that the names and descriptions of the interface and its fields are consistent with GraphQL conventions. +- **Field Validation**: Ensures that all fields defined in the schema have corresponding attributes in the class, and that their types and default values are valid. + +### Validation Example + +If the schema definition or fields are not correctly defined, a `ValueError` will be raised during the validation process: + +```python +from ariadne_graphql_modules import GraphQLInterface + +class InvalidInterface(GraphQLInterface): + __schema__ = ''' + interface InvalidInterface { + summary: String! + invalidField: NonExistentType! + } + ''' +``` diff --git a/docs/api/make_executable_schema.md b/docs/api/make_executable_schema.md new file mode 100644 index 0000000..3d4c47f --- /dev/null +++ b/docs/api/make_executable_schema.md @@ -0,0 +1,110 @@ + +## `make_executable_schema` + +The `make_executable_schema` function constructs an executable GraphQL schema from a list of types, including custom types, SDL strings, and various other GraphQL elements. This function also validates the types for consistency and allows for customization through options like `convert_names_case` and `merge_roots`. + +### Basic Usage + +```python +class UserType(GraphQLObject): + __schema__ = """ + type User { + id: ID! + username: String! + } + """ + + +class QueryType(GraphQLObject): + __schema__ = """ + type Query { + user: User + } + """ + __requires__ = [UserType] + + @staticmethod + def user(*_): + return { + "id": 1, + "username": "Alice", + } + + +schema = make_executable_schema(QueryType) +``` + +### Automatic Merging of Roots + +By default, when multiple `Query`, `Mutation`, or `Subscription` types are passed to `make_executable_schema`, they are merged into a single type containing all the fields from the provided definitions, ordered alphabetically by field name. + +```python +class UserQueriesType(GraphQLObject): + __schema__ = """ + type Query { + user(id: ID!): User + } + """ + + +class ProductsQueriesType(GraphQLObject): + __schema__ = """ + type Query { + product(id: ID!): Product + } + """ + +schema = make_executable_schema(UserQueriesType, ProductsQueriesType) +``` + +This will result in a single `Query` type in the schema: + +```graphql +type Query { + product(id: ID!): Product + user(id: ID!): User +} +``` + +To disable this behavior, you can use the `merge_roots=False` option: + +```python +schema = make_executable_schema( + UserQueriesType, + ProductsQueriesType, + merge_roots=False, +) +``` + +### Name Conversion with `convert_names_case` + +The `convert_names_case` option allows you to apply custom naming conventions to the types, fields, and other elements within the schema. When this option is enabled, it triggers the `convert_schema_names` function. + +```python +def uppercase_name_converter(name: str, schema: GraphQLSchema, path: Tuple[str, ...]) -> str: + return name.upper() + +schema = make_executable_schema(QueryType, convert_names_case=uppercase_name_converter) +``` + +#### How `convert_schema_names` Works + +The `convert_schema_names` function traverses the schema and applies a given `SchemaNameConverter` to rename elements according to custom logic. The converter is a callable that receives the original name, the schema, and the path to the element, and it returns the new name. + +- **`schema`**: The GraphQL schema being modified. +- **`converter`**: A callable that transforms the names based on your custom logic. + +The function ensures that all types, fields, and other schema elements adhere to the naming conventions specified by your converter function. + +#### Example + +If `convert_names_case` is set to `True`, the function will automatically convert names to the convention defined by the `SchemaNameConverter`. For instance, you might convert all names to uppercase: + +```python +def uppercase_converter(name: str, schema: GraphQLSchema, path: Tuple[str, ...]) -> str: + return name.upper() + +schema = make_executable_schema(QueryType, convert_names_case=uppercase_converter) +``` + +This would convert all type and field names in the schema to uppercase. diff --git a/docs/api/object_type.md b/docs/api/object_type.md new file mode 100644 index 0000000..b738b77 --- /dev/null +++ b/docs/api/object_type.md @@ -0,0 +1,229 @@ + +# `GraphQLObject` + +The `GraphQLObject` class is a core component of the `ariadne_graphql_modules` library used to define GraphQL object types. This class allows you to create GraphQL objects that represent structured data with fields, interfaces, and custom resolvers. The class also supports schema-based and schema-less definitions. + +## Inheritance + +- Inherits from `GraphQLBaseObject`. + +## Class Attributes + +- **`__schema__`**: *(Optional[str])* Holds the GraphQL schema definition string for the object type if provided. +- **`__graphql_name__`**: *(Optional[str])* The custom name for the GraphQL object type. +- **`__description__`**: *(Optional[str])* A description of the object type, included in the GraphQL schema. +- **`__graphql_type__`**: Defines the GraphQL type as `GraphQLClassType.OBJECT`. +- **`__aliases__`**: *(Optional[Dict[str, str]])* Defines field aliases for the object type. +- **`__requires__`**: *(Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]])* Specifies other types or enums that are required by this object type. + +## Core Methods + +### `field` + +The `field` method is a static method that provides a shortcut for defining fields in a `GraphQLObject`. It allows for custom configuration of fields, including setting a specific name, type, arguments, and a description. + +- **Parameters:** + - `f` (Optional[Resolver]): The resolver function for the field. If not provided, the field will use the default resolver. + - `name` (Optional[str]): The name of the field in the GraphQL schema. If not provided, the field name defaults to the attribute name in the class. + - `graphql_type` (Optional[Any]): The GraphQL type of the field. This can be a basic GraphQL type, a custom type, or a list type. + - `args` (Optional[Dict[str, GraphQLObjectFieldArg]]): A dictionary of arguments that the field can accept. + - `description` (Optional[str]): A description for the field, which is included in the GraphQL schema. + - `default_value` (Optional[Any]): A default value for the field. + +- **Returns:** A configured field that can be used in a `GraphQLObject`. + +**Example Usage:** + +```python +class QueryType(GraphQLObject): + hello: str = GraphQLObject.field(description="Returns a greeting", default_value="Hello World!") +``` + +### `resolver` + +The `resolver` method is a static method used to define a resolver for a specific field in a `GraphQLObject`. The resolver processes the data before returning it to the client. + +- **Parameters:** + - `field` (str): The name of the field for which the resolver is being defined. + - `graphql_type` (Optional[Any]): The GraphQL type of the field. + - `args` (Optional[Dict[str, GraphQLObjectFieldArg]]): A dictionary of arguments that the field can accept. + - `description` (Optional[str]): A description for the resolver, which is included in the GraphQL schema. + +- **Returns:** A decorator that can be used to wrap a function, making it a resolver for the specified field. + +**Example Usage:** + +```python +class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" +``` + +### `argument` + +The `argument` method is a static method used to define an argument for a field in a `GraphQLObject`. This method is particularly useful for adding descriptions and default values to arguments. + +- **Parameters:** + - `name` (Optional[str]): The name of the argument. + - `description` (Optional[str]): A description for the argument. + - `graphql_type` (Optional[Any]): The GraphQL type of the argument. + - `default_value` (Optional[Any]): A default value for the argument. + +- **Returns:** A `GraphQLObjectFieldArg` instance that represents the argument. + +**Example Usage:** + +```python +class QueryType(GraphQLObject): + @GraphQLObject.field( + args={"message": GraphQLObject.argument(description="Message to echo", graphql_type=str)} + ) + def echo_message(obj, info, message: str) -> str: + return message +``` + +## Inheritance Feature and Interface Implementation + +The `GraphQLObject` class supports inheritance, allowing you to extend existing object types or interfaces. This feature enables you to reuse and extend functionality across multiple object types, enhancing modularity and code reuse. + +### Inheriting from `GraphQLInterface` + +When you inherit from a `GraphQLInterface`, the resulting object type will automatically include an `implements` clause in the GraphQL schema. This means the object type will implement all the fields defined by the interface. + +```python +from ariadne_graphql_modules import GraphQLObject, GraphQLInterface + +class NodeInterface(GraphQLInterface): + id: str + +class UserType(GraphQLObject, NodeInterface): + name: str +``` + +In this example, `UserType` will implement the `Node` interface, and the GraphQL schema will include `type User implements Node`. + +### Inheriting from Another Object Type + +When you inherit from another `GraphQLObject`, the derived class will inherit all attributes, resolvers, and custom fields from the base object type. However, the derived object type will not include an `implements` clause in the schema. + +```python +class BaseCategoryType(GraphQLObject): + name: str + description: str + +class CategoryType(BaseCategoryType): + posts: int +``` + +In this example, `CategoryType` inherits the `name` and `description` fields from `BaseCategoryType`, along with any custom resolvers or field configurations. + +## Example Usage + +### Defining an Object Type + +To define an object type, subclass `GraphQLObject` and specify fields using class attributes. + +```python +from ariadne_graphql_modules import GraphQLObject, GraphQLID + +class CategoryType(GraphQLObject): + name: str + posts: int +``` + +### Using a Schema to Define an Object Type + +You can define an object type using a GraphQL schema by setting the `__schema__` attribute. + +```python +from ariadne import gql +from ariadne_graphql_modules import GraphQLObject + +class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str + posts: int +``` + +### Adding Resolvers to an Object Type + +You can add custom resolvers to fields in an object type by using the `@GraphQLObject.resolver` decorator. + +```python +class QueryType(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + def resolve_hello(*_): + return "Hello World!" +``` + +### Handling Aliases in Object Types + +You can define aliases for fields in an object type using the `__aliases__` attribute. + +```python +class CategoryType(GraphQLObject): + __aliases__ = {"name": "title"} + + title: str + posts: int +``` + +## Validation + +The `GraphQLObject` class includes validation mechanisms to ensure that object types are correctly defined. Validation is performed both for schema-based and schema-less object types. + +### Validation Process + +1. **Type Checking**: Ensures that the class is of the correct GraphQL type (`OBJECT`). +2. **Schema Parsing and Validation**: If a schema is provided, it is parsed and validated against the class definition. +3. **Field and Alias Validation**: Ensures that all fields and aliases are correctly defined and do not conflict. + +### Validation Example + +Here is an example where validation checks that the provided schema matches the class definition: + +```python +from graphql import gql +from ariadne_graphql_modules import GraphQLObject + +class CategoryType(GraphQLObject): + __schema__ = gql( + """ + type Category { + name: String + posts: Int + } + """ + ) + + name: str + posts: int +``` + +If the class definition and the schema do not match, a `ValueError` will be raised. + +## Example: Using a Field with a Custom Resolver + +```python +from ariadne_graphql_modules import GraphQLObject + +class QueryType(GraphQLObject): + @GraphQLObject.field() + def hello(obj, info) -> str: + return "Hello World!" +``` + +This method will resolve the `hello` field to return `"Hello World!"`. \ No newline at end of file diff --git a/docs/api/scalar_type.md b/docs/api/scalar_type.md new file mode 100644 index 0000000..4793007 --- /dev/null +++ b/docs/api/scalar_type.md @@ -0,0 +1,152 @@ +# `GraphQLScalar` + +The `GraphQLScalar` class is a generic base class in the `ariadne_graphql_modules` library used to define custom scalar types in GraphQL. Scalars in GraphQL represent primitive data types like `Int`, `Float`, `String`, `Boolean`, etc. This class provides the framework for creating custom scalars with serialization and parsing logic. + +## Inheritance + +- Inherits from `GraphQLType` and `Generic[T]`. + +## Class Attributes + +- **`__schema__`**: *(Optional[str])* - The GraphQL schema definition string for the union, if provided. +- **`__graphql_name__`**: *(Optional[str])* The custom name for the GraphQL type. If not provided, it will be generated based on the class name. +- **`__description__`**: *(Optional[str])* A description of the GraphQL type, included in the schema documentation. +- **`wrapped_value`** (T): Stores the value wrapped by the scalar. + +## Initialization + +### `__init__(self, value: T)` + +- **Parameters:** + - `value` (T): The value to be wrapped by the scalar instance. Saved on `wrapped_value` class attribute + +## Class Methods + +### `serialize` + +Serializes the scalar's value. If the value is an instance of the scalar, it unwraps the value before serialization. + +- **Parameters:** + - `value: Any`: The value to serialize. + +### `parse_value` + +Parses the given value, typically used when receiving input from a GraphQL query. + +- **Parameters:** + - `value: Any`: The value to parse. + +### `parse_literal` + +Parses a literal GraphQL value. This method is used to handle literals in GraphQL queries. + +- **Parameters:** + - `node: ValueNode`: The AST node representing the value. + - `variables: (Optional[Dict[str, Any]])`: A dictionary of variables in the GraphQL query. + +## Example + +### Defining a Custom Scalar + +To define a custom scalar, subclass `GraphQLScalar` and implement the required serialization and parsing methods. + +```python +from datetime import date +from ariadne_graphql_modules import GraphQLScalar + +class DateScalar(GraphQLScalar[date]): + @classmethod + def serialize(cls, value): + if isinstance(value, cls): + return str(value.unwrap()) + return str(value) +``` + +### Using a Custom Scalar in a Schema + +```python +from ariadne_graphql_modules import GraphQLObject, make_executable_schema + +class QueryType(GraphQLObject): + date: DateScalar + + @GraphQLObject.resolver("date") + def resolve_date(*_) -> DateScalar: + return DateScalar(date(1989, 10, 30)) + +schema = make_executable_schema(QueryType) +``` + +### Handling Scalars with and without Schema + +You can define a scalar with or without an explicit schema definition: + +```python +class SchemaDateScalar(GraphQLScalar[date]): + __schema__ = "scalar Date" + + @classmethod + def serialize(cls, value): + if isinstance(value, cls): + return str(value.unwrap()) + return str(value) +``` + +### Using `parse_value` and `parse_literal` + +The `parse_value` and `parse_literal` methods are crucial for processing input values in GraphQL queries. They ensure that the values provided by the client are correctly interpreted according to the scalar's logic. + +#### Example: `parse_value` + +The `parse_value` method is typically used to convert input values from GraphQL queries into the appropriate type for use in the application. + +```python +class DateScalar(GraphQLScalar[str]): + @classmethod + def parse_value(cls, value): + # Assume value is a string representing a date, e.g., "2023-01-01" + try: + return datetime.strptime(value, "%Y-%m-%d").date() + except ValueError: + raise ValueError(f"Invalid date format: {value}") + +# Example usage in a query +parsed_date = DateScalar.parse_value("2023-01-01") +print(parsed_date) # Outputs: 2023-01-01 as a `date` object +``` + +In this example, `parse_value` converts a string into a `date` object. If the string does not match the expected format, an error is raised. + +#### Example: `parse_literal` + +The `parse_literal` method is used to handle literal values directly from the GraphQL query's Abstract Syntax Tree (AST). This is useful when dealing with inline values in queries. + +```python +from graphql import StringValueNode + +class DateScalar(GraphQLScalar[str]): + @classmethod + def parse_literal(cls, node, variables=None): + if isinstance(node, StringValueNode): + return cls.parse_value(node.value) + raise ValueError("Invalid AST node type") + +# Example usage in a query +parsed_date = DateScalar.parse_literal(StringValueNode(value="2023-01-01")) +print(parsed_date) # Outputs: 2023-01-01 as a `date` object +``` + +In this example, `parse_literal` checks if the AST node is of the correct type (`StringValueNode`) and then applies the `parse_value` logic to convert it. + +### Validation + +The `GraphQLScalar` class includes a validation mechanism to ensure that custom scalar types are correctly defined and conform to GraphQL standards. This validation is especially important when defining a scalar with an explicit schema using the `__schema__` attribute. + +#### Validation Process + +When a custom scalar class defines a schema using the `__schema__` attribute, the following validation steps occur: + +1. **Schema Parsing**: The schema string is parsed to ensure it corresponds to a valid GraphQL scalar type. +2. **Type Checking**: The parsed schema is checked to confirm it is of the correct type (`ScalarTypeDefinitionNode`). +3. **Name Validation**: The scalar's name is validated to ensure it adheres to GraphQL naming conventions. +4. **Description Validation**: The scalar's description is validated to ensure it is consistent and correctly formatted. diff --git a/docs/api/subscription_type.md b/docs/api/subscription_type.md new file mode 100644 index 0000000..e5f83a9 --- /dev/null +++ b/docs/api/subscription_type.md @@ -0,0 +1,136 @@ + +# `GraphQLSubscription` + +The `GraphQLSubscription` class is designed to facilitate the creation and management of GraphQL subscriptions within a schema. Subscriptions in GraphQL allow clients to receive real-time updates when specific events occur. + +## Inheritance + +- Inherits from `GraphQLBaseObject`. + +## Class Attributes + +- **`__schema__`**: *(Optional[str])* - The GraphQL schema definition string for the union, if provided. +- **`__graphql_name__`**: *(Optional[str])* The custom name for the GraphQL type. If not provided, it will be generated based on the class name. +- **`__description__`**: *(Optional[str])* A description of the GraphQL type, included in the schema documentation. +- **`__graphql_type__`**: Defines the GraphQL class type, set to `GraphQLClassType.BASE`. +- **`__aliases__`**: *(Optional[Dict[str, str]])* Defines field aliases for the object type. +- **`__requires__`**: *(Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]])* Specifies other types or enums that are required by this object type. + +## Class Methods and Decorators + +### `source` + +Defines a source for a subscription. The source is an async generator that yields the data to be sent to clients. +- **Parameters:** + - `field: str`: The name of the subscription field. + - `graphql_type: Optional[Any]`: The GraphQL type of the field. + - `args: Optional[Dict[str, GraphQLObjectFieldArg]]`: Optional arguments that the subscription can accept. + - `description: Optional[str]`: An optional description for the subscription field. + +### `resolver` + +Defines a resolver for a subscription. The resolver processes the data provided by the source before sending it to the client. + +- **Parameters:** + - `field: str`: The name of the resolver field. + +### `field` + +Defines a field in the subscription type. This is a shortcut for defining subscription fields without a full schema. + +- **Parameters:** + - `name: Optional[str]`: The name of the field that will be created. + +## Field Definition Limitations + +In `GraphQLSubscription`, detailed field definitions, such as specifying arguments or custom types, must be done using the `source` method. This ensures that the source and resolver methods are properly aligned and that the subscription functions as expected. + +## Inheritance feature + +`GraphQLSubscription` does not support inheritance from other subscription classes. Each subscription class must define its fields and logic independently. This limitation is intentional to prevent issues with overlapping field definitions and resolver conflicts. + +## Example + +### Basic Subscription + +Define a simple subscription that notifies clients whenever a new message is added: + +```python +from ariadne_graphql_modules import GraphQLSubscription, GraphQLObject, make_executable_schema + +class Message(GraphQLObject): + id: GraphQLID + content: str + author: str + +class SubscriptionType(GraphQLSubscription): + message_added: Message + + @GraphQLSubscription.source("message_added", graphql_type=Message) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.resolver("message_added") + async def resolve_message_added(message, info): + return message + +schema = make_executable_schema(SubscriptionType) +``` + +### Subscriptions with Arguments + +Handle subscriptions that accept arguments: + +```python +class SubscriptionType(GraphQLSubscription): + + @GraphQLSubscription.source( + "message_added", + args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, + graphql_type=Message, + ) + async def message_added_generator(obj, info, channel: GraphQLID): + while True: + yield {"id": "some_id", "content": f"message_{channel}", "author": "Anon"} + + @GraphQLSubscription.field() + def message_added(message, info, channel: GraphQLID): + return message +``` + +### Schema-based Subscription + +Define a subscription using a GraphQL schema: + +```python +from ariadne import gql + +class SubscriptionType(GraphQLSubscription): + __schema__ = gql( + ''' + type Subscription { + messageAdded: Message! + } + ''' + ) + + @GraphQLSubscription.source("messageAdded", graphql_type=Message) + async def message_added_generator(obj, info): + while True: + yield {"id": "some_id", "content": "message", "author": "Anon"} + + @GraphQLSubscription.resolver("messageAdded") + async def resolve_message_added(message, info): + return message +``` + +## Validation + +Validation in `GraphQLSubscription` works similarly to that in `GraphQLObject`. The following validation checks are performed: + +- **Field Definitions**: Fields must be properly defined, either through SDL in the `__schema__` attribute or directly within the class using the `source` and `resolver` decorators. +- **Schema Consistency**: The schema is checked for consistency, ensuring that all fields and their types are correctly defined. +- **Field Names**: The field names defined in the subscription must match those in the schema. + +If any of these checks fail, a `ValueError` will be raised during schema construction. diff --git a/docs/api/union_type.md b/docs/api/union_type.md new file mode 100644 index 0000000..352c8a2 --- /dev/null +++ b/docs/api/union_type.md @@ -0,0 +1,106 @@ +# `GraphQLUnion` + +The `GraphQLUnion` class is used to define custom union types in GraphQL. A union type is a GraphQL feature that allows a field to return one of several different types, but only one of them at any given time. This class provides the framework for defining unions, managing their types, and handling type resolution. + +## Inheritance + +- Inherits from `GraphQLType`. + +## Class Attributes + +- **`__types__`**: **Sequence[Type[GraphQLType]]** - A sequence of GraphQL types that belong to the union. +- **`__schema__`**: *(Optional[str])* - The GraphQL schema definition string for the union, if provided. + +## Class Methods + +### `resolve_type(obj: Any, *_) -> str` + +Resolves the type of an object at runtime. It returns the name of the GraphQL type that matches the object's type. + +- **Parameters:** + - `obj` (Any): The object to resolve the type for. + +- **Returns:** `str` - The name of the resolved GraphQL type. + +## Example Usage + +### Defining a Union Type + +To define a union type, subclass `GraphQLUnion` and specify the types that belong to the union using the `__types__` attribute. + +```python +from ariadne_graphql_modules import GraphQLUnion, GraphQLObject + +class UserType(GraphQLObject): + id: GraphQLID + username: str + +class CommentType(GraphQLObject): + id: GraphQLID + content: str + +class ResultType(GraphQLUnion): + __types__ = [UserType, CommentType] +``` + +### Using a Union Type in a Schema + +You can use the defined union type in a GraphQL schema to handle fields that can return multiple types. + +```python +from ariadne_graphql_modules import GraphQLObject, make_executable_schema + +class QueryType(GraphQLObject): + @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod + def search(*_) -> List[Union[UserType, CommentType]]: + return [ + UserType(id=1, username="Bob"), + CommentType(id=2, content="Hello World!"), + ] + +schema = make_executable_schema(QueryType) +``` + +### Handling Unions with and without Schema + +You can define a union with or without an explicit schema definition: + +```python +class SearchResultUnion(GraphQLUnion): + __schema__ = "union SearchResult = User | Comment" + __types__ = [UserType, CommentType] +``` + +### Validation + +The `GraphQLUnion` class includes validation mechanisms to ensure that union types are correctly defined. The validation checks whether the types in `__types__` match those in `__schema__` if a schema is provided. + +#### Validation Process + +1. **Type List Validation**: Ensures that the `__types__` attribute is provided and contains valid types. +2. **Schema Parsing and Validation**: If `__schema__` is provided, it is parsed to ensure it corresponds to a valid GraphQL union type. +3. **Type Matching**: Validates that the types in `__types__` match the types declared in `__schema__`, if a schema is provided. + +#### Validation Example + +```python +from graphql import gql +from ariadne_graphql_modules import GraphQLUnion, GraphQLObject + +class UserType(GraphQLObject): + id: GraphQLID + username: str + +class CommentType(GraphQLObject): + id: GraphQLID + content: str + +class ResultType(GraphQLUnion): + __schema__ = gql(""" + union Result = User | Comment + """) + __types__ = [UserType, CommentType] +``` + +If the `__schema__` defines types that do not match those in `__types__`, a `ValueError` will be raised. diff --git a/docs/api/wrap_legacy_types.md b/docs/api/wrap_legacy_types.md new file mode 100644 index 0000000..9fcee9d --- /dev/null +++ b/docs/api/wrap_legacy_types.md @@ -0,0 +1,93 @@ + +# `wrap_legacy_types` + +The `wrap_legacy_types` function is part of the `ariadne_graphql_modules` library and provides a way to migrate legacy GraphQL types from version 1 of the library to be compatible with the newer version. This function wraps the legacy types in a way that they can be used seamlessly in the new system without rewriting their definitions. + +## Function Signature + +```python +def wrap_legacy_types( + *bindable_types: Type[BaseType], +) -> List[Type["LegacyGraphQLType"]]: +``` + +## Parameters + +- `*bindable_types`: A variable number of legacy GraphQL type classes (from v1 of the library) that should be wrapped to work with the newer version of the library. + + - **Type:** `Type[BaseType]` + - **Description:** Each of these types could be one of the legacy types such as `ObjectType`, `InterfaceType`, `UnionType`, etc. + +## Returns + +- **List[Type["LegacyGraphQLType"]]:** A list of new classes that inherit from `LegacyGraphQLType`, each corresponding to one of the legacy types passed in as arguments. + +## Usage Example + +Here’s an example of how to use the `wrap_legacy_types` function: + +```python +from ariadne_graphql_modules.compatibility_layer import wrap_legacy_types +from ariadne_graphql_modules.v1.object_type import ObjectType +from ariadne_graphql_modules.v1.enum_type import EnumType + +class FancyObjectType(ObjectType): + __schema__ = ''' + type FancyObject { + id: ID! + someInt: Int! + someFloat: Float! + someBoolean: Boolean! + someString: String! + } + ''' + +class UserRoleEnum(EnumType): + __schema__ = ''' + enum UserRole { + USER + MOD + ADMIN + } + ''' + +wrapped_types = wrap_legacy_types(FancyObjectType, UserRoleEnum) +``` + +## Detailed Example + +### Wrapping and Using Legacy Types in Schema + +```python +from ariadne_graphql_modules.compatibility_layer import wrap_legacy_types +from ariadne_graphql_modules.v1.object_type import ObjectType +from ariadne_graphql_modules.v1.scalar_type import ScalarType +from ariadne_graphql_modules.executable_schema import make_executable_schema + +class DateScalar(ScalarType): + __schema__ = "scalar Date" + + @staticmethod + def serialize(value): + return value.isoformat() + + @staticmethod + def parse_value(value): + return datetime.strptime(value, "%Y-%m-%d").date() + +class QueryType(ObjectType): + __schema__ = ''' + type Query { + today: Date! + } + ''' + + @staticmethod + def resolve_today(*_): + return date.today() + +wrapped_types = wrap_legacy_types(QueryType, DateScalar) +schema = make_executable_schema(*wrapped_types) +``` + +In this example, `QueryType` and `DateScalar` are legacy types that have been wrapped and used to create an executable schema compatible with the new version of the library. diff --git a/CHANGELOG.md b/docs/changelog.md similarity index 53% rename from CHANGELOG.md rename to docs/changelog.md index 7327b4e..5559ba9 100644 --- a/CHANGELOG.md +++ b/docs/changelog.md @@ -1,5 +1,15 @@ # CHANGELOG + +## 1.0.0 (UNREALEASED) + +- Major API Redesign: The entire API has been restructured for better modularity and flexibility. +- New Type System: Introduced a new type system, replacing the old v1 types. +- Migration Support: Added wrap_legacy_types to help transition from v1 types to the new system without a complete rewrite. +- Enhanced make_executable_schema: Now supports both legacy and new types with improved validation and root type merging. +- Deprecation Notice: Direct use of v1 types is deprecated. Transition to the new system or use wrap_legacy_types for continued support. + + ## 0.8.0 (2024-02-21) - Added support for Ariadne 0.22. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..dffbc5c --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,8 @@ +## Contributing + +We welcome contributions to Ariadne GraphQL Modules! If you've found a bug or issue, feel free to use [GitHub issues](https://github.com/mirumee/ariadne/issues). If you have any questions or feedback, please let us know via [GitHub discussions](https://github.com/mirumee/ariadne/discussions/). + +Also, make sure to follow [@AriadneGraphQL](https://twitter.com/AriadneGraphQL) on Twitter for the latest updates, news, and random musings! + +**Crafted with ❤️ by [Mirumee Software](http://mirumee.com)** +hello@mirumee.com diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a616065 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,93 @@ + +[![Ariadne](https://ariadnegraphql.org/img/logo-horizontal-sm.png)](https://ariadnegraphql.org) + +[![Build Status](https://github.com/mirumee/ariadne-graphql-modules/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/mirumee/ariadne-graphql-modules/actions) + +## ⚠️ Important Migration Warning: Version 1.0.0 + +With the release of version 1.0.0, there have been significant changes to the `ariadne_graphql_modules` API. If you are upgrading from a previous version, **you will need to update your imports** to ensure your code continues to function correctly. + +### What You Need to Do + +To maintain compatibility with existing code, you must explicitly import types from the `v1` module of `ariadne_graphql_modules`. This is necessary for any code that relies on the legacy API from versions prior to 1.0.0. + +### Example + +**Before upgrading:** + +```python +from ariadne_graphql_modules import ObjectType, EnumType +``` + +**After upgrading to 1.0.0:** + +```python +from ariadne_graphql_modules.v1 import ObjectType, EnumType +``` + +### Why This Change? + +The introduction of version 1.0.0 brings a more robust and streamlined API, with better support for modular GraphQL schemas. To facilitate this, legacy types and functionality have been moved to the `v1` submodule, allowing new projects to take full advantage of the updated architecture while providing a clear path for migrating existing codebases. + +# Ariadne GraphQL Modules + +**Ariadne GraphQL Modules** is an extension for the [Ariadne](https://ariadnegraphql.org/) framework, designed to help developers structure and manage GraphQL schemas in a modular way. This library provides an organized approach to building GraphQL APIs by dividing your schema into self-contained, reusable modules, each responsible for its own part of the schema. + +## How It Works + +Ariadne GraphQL Modules operates by allowing you to define your GraphQL schema in isolated modules, each with its own types, resolvers, and dependencies. These modules can then be combined into a single executable schema using the provided utility functions. + +## Key Functionalities + +- **Modular Schema Design**: Enables the breakdown of GraphQL schemas into smaller, independent modules. Each module can define its own types, queries, mutations, and subscriptions. +- **Flexible Schema Definitions**: Supports both declarative (using schema strings) and programmatic (using Python code) approaches to defining schemas, allowing developers to choose the most appropriate method for their project. +- **Automatic Merging of Roots**: Automatically merges `Query`, `Mutation`, and `Subscription` types from different modules into a single schema, ensuring that your API is consistent and well-organized. +- **Case Conversion**: Includes tools for automatically converting field names and arguments between different naming conventions (e.g., `snake_case` to `camelCase`), making it easier to integrate with various client conventions. +- **Deferred Dependencies**: Allows for the declaration of deferred dependencies that can be resolved at the time of schema creation, giving developers more control over module initialization. + +## Installation + +Ariadne GraphQL Modules can be installed using pip: + +```bash +pip install ariadne-graphql-modules +``` + +Ariadne 0.23 or later is required for the library to work. + +## Basic Usage + +Here is a basic example of how to use Ariadne GraphQL Modules to create a simple GraphQL API: + +```python +from datetime import date + +from ariadne.asgi import GraphQL +from ariadne_graphql_modules import ObjectType, gql, make_executable_schema + + +class Query(ObjectType): + __schema__ = gql( + """ + type Query { + message: String! + year: Int! + } + """ + ) + + @staticmethod + def resolve_message(*_): + return "Hello world!" + + @staticmethod + def resolve_year(*_): + return date.today().year + + +schema = make_executable_schema(Query) +app = GraphQL(schema=schema, debug=True) +``` + +In this example, a simple `Query` type is defined within a module. The `make_executable_schema` function is then used to combine the module into a complete schema, which can be used to create a GraphQL server. + diff --git a/docs/migration_guide.md b/docs/migration_guide.md new file mode 100644 index 0000000..79e74e0 --- /dev/null +++ b/docs/migration_guide.md @@ -0,0 +1,38 @@ +# Migration Guide + +## `ariadne_graphql_modules.v1` to Current Version + +This guide provides a streamlined process for migrating your code from `ariadne_graphql_modules.v1` to the current version. The focus is on maintaining compatibility using the `wrap_legacy_types` function, which allows you to transition smoothly without needing to rewrite your entire codebase. + +## Migration Overview + +### Wrapping Legacy Types + +To maintain compatibility with the current version, all legacy types from `ariadne_graphql_modules.v1` can be wrapped using the `wrap_legacy_types` function. This approach allows you to continue using your existing types with minimal changes. + +### Example: + +```python +from ariadne_graphql_modules.v1 import ObjectType, EnumType +from ariadne_graphql_modules import make_executable_schema, wrap_legacy_types, GraphQLObject + +# Your existing types can remain as is +class QueryType(ObjectType): + ... + +class UserRoleEnum(EnumType): + ... + +# You can mix new types with old types +class NewType(GraphQLObject): + ... + +# Wrap them for the new system +my_legacy_types = wrap_legacy_types(QueryType, UserRoleEnum) +schema = make_executable_schema(*my_legacy_types, NewType) +``` + + +### Encouragement to Migrate + +While `wrap_legacy_types` provides a quick solution, it is recommended to gradually transition to the current version’s types for better support and new features. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f614727 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,62 @@ +site_name: Ariadne Graphql Modules +site_url: https://mirumee.github.io/ariadne-graphql-modules/ +repo_url: https://github.com/mirumee/ariadne-graphql-modules +repo_name: mirumee/ariadne-graphql-modules +copyright: Copyright © 2024 - Mirumee Software + +theme: + name: material + +nav: + - Get Started: + - Welcome to Ariadne Graphql Modules: index.md + - Migration Guide: migration_guide.md + - Contributing: contributing.md + - Changelog: changelog.md + - API Documentation: + - GraphQLObject: api/object_type.md + - GraphQLSubscription: api/subscription_type.md + - GraphQLInput: api/input_type.md + - GraphQLScalar: api/scalar_type.md + - GraphQLEnum: api/enum_type.md + - GraphQLInterface: api/interface_type.md + - GraphQLUnion: api/union_type.md + - Deferred: api/deferred_type.md + - GraphQLType: api/base_type.md + - GraphQLID: api/id_type.md + - make_executable_schema: api/make_executable_schema.md + - wrap_legacy_types: api/wrap_legacy_types.md + +plugins: + - offline: + enabled: !ENV [OFFLINE, false] + - search + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower + - footnotes + - tables + - toc: + permalink: true diff --git a/pyproject.toml b/pyproject.toml index efdba0c..7e5c443 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] -dependencies = ["ariadne==0.23.0"] +dependencies = ["ariadne==0.23.0", "mkdocs-material"] [project.optional-dependencies] test = [ @@ -30,10 +30,12 @@ test = [ "mypy", "pylint", "pytest", + "pytest-cov", "pytest-asyncio", "pytest-regressions", "pytest-datadir", ] +docs = ["mkdocs-material"] [project.urls] "Homepage" = "https://ariadnegraphql.org/" diff --git a/tests/test_make_executable_schema.py b/tests/test_make_executable_schema.py index cacdf0b..9dc96b3 100644 --- a/tests/test_make_executable_schema.py +++ b/tests/test_make_executable_schema.py @@ -1,10 +1,11 @@ -from typing import Union +from typing import Tuple, Union import pytest from graphql import ( GraphQLField, GraphQLInterfaceType, GraphQLObjectType, + GraphQLSchema, default_field_resolver, graphql_sync, ) @@ -290,3 +291,39 @@ def test_make_executable_schema_supports_vanilla_directives(): root_value={"field": "first"}, ) assert result.data == {"field": "FIRST"} + + +def test_make_executable_schema_custom_convert_name_case(assert_schema_equals): + def custom_name_converter( + name: str, schema: GraphQLSchema, path: Tuple[str, ...] + ) -> str: + return f"custom_{name}" + + class QueryType(GraphQLObject): + other_field: str + + type_def = """ + type Query { + firstField: String! + } + """ + + schema = make_executable_schema( + QueryType, type_def, convert_names_case=custom_name_converter + ) + assert_schema_equals( + schema, + """ + type Query { + firstField: String! + otherField: String! + } + """, + ) + result = graphql_sync( + schema, + "{ firstField otherField }", + root_value={"custom_firstField": "first", "other_field": "other"}, + ) + + assert result.data == {"firstField": "first", "otherField": "other"} From de7b336bff6c8f554598b6782027c2481dd5d775 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 30 Aug 2024 11:26:58 +0200 Subject: [PATCH 51/63] fix for 3.9 --- ariadne_graphql_modules/executable_schema.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ariadne_graphql_modules/executable_schema.py b/ariadne_graphql_modules/executable_schema.py index 2cb1938..d7bd6fe 100644 --- a/ariadne_graphql_modules/executable_schema.py +++ b/ariadne_graphql_modules/executable_schema.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, Dict, List, Optional, Sequence, Type, Union +from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union from ariadne import ( SchemaBindable, @@ -98,7 +98,7 @@ def make_executable_schema( def find_type_defs( types: Union[ - tuple[SchemaType | List[SchemaType], ...], + Tuple[Union[SchemaType, List[SchemaType]], ...], List[SchemaType], ] ) -> List[str]: @@ -114,7 +114,7 @@ def find_type_defs( def flatten_types( - types: tuple[SchemaType | List[SchemaType], ...], + types: Tuple[Union[SchemaType, List[SchemaType]], ...], metadata: GraphQLMetadata, ) -> List[SchemaType]: flat_schema_types_list: List[SchemaType] = flatten_schema_types( From 257f0ed31478d3b7e89ae572762be852a2e503fb Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 30 Aug 2024 13:17:56 +0200 Subject: [PATCH 52/63] fix staticmethods --- ariadne_graphql_modules/base_object_type/graphql_field.py | 6 ++++++ ariadne_graphql_modules/base_object_type/graphql_type.py | 8 ++++++-- ariadne_graphql_modules/base_object_type/utils.py | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ariadne_graphql_modules/base_object_type/graphql_field.py b/ariadne_graphql_modules/base_object_type/graphql_field.py index 98a875a..6c20974 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_field.py +++ b/ariadne_graphql_modules/base_object_type/graphql_field.py @@ -100,6 +100,8 @@ def object_field( graphql_type: Optional[Any] = None, default_value: Optional[Any] = None, ) -> GraphQLObjectField: + if isinstance(resolver, staticmethod): + resolver = resolver.__func__ field_type: Any = graphql_type if not graphql_type and resolver: field_type = get_field_type_from_resolver(resolver) @@ -128,6 +130,8 @@ def object_resolver( description: Optional[str] = None, ): def object_resolver_factory(f: Resolver) -> GraphQLObjectResolver: + if isinstance(f, staticmethod): + f = f.__func__ return GraphQLObjectResolver( args=args, description=description, @@ -146,6 +150,8 @@ def object_subscriber( description: Optional[str] = None, ): def object_subscriber_factory(f: Subscriber) -> GraphQLObjectSource: + if isinstance(f, staticmethod): + f = f.__func__ return GraphQLObjectSource( args=args, description=description, diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index 81c38d1..8076ee6 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -329,7 +329,7 @@ def _build_fields(fields_data: GraphQLFieldData) -> Dict[str, "GraphQLObjectFiel fields[field_name] = GraphQLObjectField( name=fields_data.fields_names[field_name], description=fields_data.fields_descriptions.get(field_name), - field_type=fields_data.fields_types[field_name], + field_type=fields_data.fields_types.get(field_name), args=fields_data.fields_args.get(field_name), resolver=fields_data.fields_resolvers.get(field_name), subscriber=fields_data.fields_subscribers.get(field_name), @@ -377,7 +377,11 @@ def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): if cls_attr.description: fields_data.fields_descriptions[attr_name] = cls_attr.description if cls_attr.resolver: - fields_data.fields_resolvers[attr_name] = cls_attr.resolver + fields_data.fields_resolvers[attr_name] = ( + cls_attr.resolver.__func__ + if isinstance(cls_attr.resolver, staticmethod) + else cls_attr.resolver + ) field_args = get_field_args_from_resolver(cls_attr.resolver) if field_args: fields_data.fields_args[attr_name] = update_field_args_options( diff --git a/ariadne_graphql_modules/base_object_type/utils.py b/ariadne_graphql_modules/base_object_type/utils.py index a3d9025..1ad2fcb 100644 --- a/ariadne_graphql_modules/base_object_type/utils.py +++ b/ariadne_graphql_modules/base_object_type/utils.py @@ -32,6 +32,8 @@ def get_field_node_from_obj_field( def get_field_args_from_resolver( resolver: Resolver, ) -> Dict[str, GraphQLObjectFieldArg]: + if isinstance(resolver, staticmethod): + resolver = resolver.__func__ resolver_signature = signature(resolver) type_hints = resolver.__annotations__ type_hints.pop("return", None) From ba76514b7fc33d074c88623f38aa0e0dda992915 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 30 Aug 2024 13:22:09 +0200 Subject: [PATCH 53/63] ignore mypy error --- ariadne_graphql_modules/base_object_type/graphql_type.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index 8076ee6..515adfd 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -377,11 +377,10 @@ def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): if cls_attr.description: fields_data.fields_descriptions[attr_name] = cls_attr.description if cls_attr.resolver: - fields_data.fields_resolvers[attr_name] = ( - cls_attr.resolver.__func__ - if isinstance(cls_attr.resolver, staticmethod) - else cls_attr.resolver - ) + resolver = cls_attr.resolver + if isinstance(resolver, staticmethod): + resolver = cls_attr.resolver.__func__ # type: ignore[attr-defined] + fields_data.fields_resolvers[attr_name] = resolver field_args = get_field_args_from_resolver(cls_attr.resolver) if field_args: fields_data.fields_args[attr_name] = update_field_args_options( From e566ecdbb319228c5639308956a198e4097a4c0c Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 2 Sep 2024 09:59:45 +0200 Subject: [PATCH 54/63] fix pylint and mypy on tests --- .github/workflows/tests.yml | 2 +- .../base_object_type/graphql_type.py | 20 -- .../enum_type/graphql_type.py | 4 +- .../interface_type/graphql_type.py | 16 +- .../object_type/graphql_type.py | 20 +- .../subscription_type/graphql_type.py | 6 +- tests/conftest.py | 2 +- tests/test_compatibility_layer.py | 2 +- tests/test_deferred_type.py | 1 + tests/test_enum_type.py | 17 +- tests/test_enum_type_validation.py | 3 +- tests/test_get_field_args_from_resolver.py | 2 + tests/test_id_type.py | 15 +- tests/test_input_type.py | 120 ++++++----- tests/test_input_type_validation.py | 3 +- tests/test_interface_type.py | 13 +- tests/test_interface_type_validation.py | 8 +- tests/test_make_executable_schema.py | 34 ++- tests/test_metadata.py | 6 +- tests/test_object_type.py | 27 ++- tests/test_object_type_field_args.py | 25 ++- tests/test_object_type_validation.py | 58 ++++-- tests/test_scalar_type.py | 4 + tests/test_scalar_type_validation.py | 1 + tests/test_standard_enum.py | 9 +- tests/test_subscription_type.py | 197 +++++++++++------- tests/test_subscription_type_validation.py | 54 +++-- tests/test_typing.py | 24 +-- tests/test_union_type.py | 2 +- tests/test_union_type_validation.py | 2 +- tests/test_validators.py | 12 +- 31 files changed, 434 insertions(+), 275 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a555ccd..67bb60f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,6 @@ jobs: pytest - name: Linters run: | - pylint ariadne_graphql_modules + pylint ariadne_graphql_modules tests mypy ariadne_graphql_modules --ignore-missing-imports black --check . diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index 515adfd..3e4333f 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -15,7 +15,6 @@ from graphql import ( FieldDefinitionNode, InputValueDefinitionNode, - ObjectTypeDefinitionNode, StringValueNode, ) @@ -44,10 +43,6 @@ from ..convert_name import convert_python_name_to_graphql from ..description import get_description_node from ..typing import get_graphql_type -from .validators import ( - validate_object_type_with_schema, - validate_object_type_without_schema, -) from ..value import get_value_node @@ -55,7 +50,6 @@ class GraphQLBaseObject(GraphQLType): __kwargs__: Dict[str, Any] __abstract__: bool = True __schema__: Optional[str] = None - __description__: Optional[str] __aliases__: Optional[Dict[str, str]] __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] __graphql_type__ = GraphQLClassType.BASE @@ -81,20 +75,6 @@ def __init__(self, **kwargs: Any): for kwarg, default in default_values.items(): setattr(self, kwarg, kwargs.get(kwarg, deepcopy(default))) - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.__dict__.get("__abstract__"): - return - - cls.__abstract__ = False - - if cls.__dict__.get("__schema__"): - valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) - cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) - else: - cls.__kwargs__ = validate_object_type_without_schema(cls) - @classmethod def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": name = cls.__get_graphql_name__() diff --git a/ariadne_graphql_modules/enum_type/graphql_type.py b/ariadne_graphql_modules/enum_type/graphql_type.py index fc33745..cff5d83 100644 --- a/ariadne_graphql_modules/enum_type/graphql_type.py +++ b/ariadne_graphql_modules/enum_type/graphql_type.py @@ -313,7 +313,7 @@ def create_graphql_enum_model( def graphql_enum( - cls: Optional[Type[Enum]] = None, + cls=None, *, name: Optional[str] = None, description: Optional[str] = None, @@ -321,7 +321,7 @@ def graphql_enum( members_include: Optional[Iterable[str]] = None, members_exclude: Optional[Iterable[str]] = None, ): - def graphql_enum_decorator(cls: Type[Enum]): + def graphql_enum_decorator(cls): graphql_model = create_graphql_enum_model( cls, name=name, diff --git a/ariadne_graphql_modules/interface_type/graphql_type.py b/ariadne_graphql_modules/interface_type/graphql_type.py index 2237f1c..8bad8e8 100644 --- a/ariadne_graphql_modules/interface_type/graphql_type.py +++ b/ariadne_graphql_modules/interface_type/graphql_type.py @@ -20,35 +20,33 @@ from ..types import GraphQLClassType from ..utils import parse_definition -from ..base import GraphQLMetadata +from ..base import GraphQLMetadata, GraphQLModel from ..description import get_description_node from ..object_type import GraphQLObject from .models import GraphQLInterfaceModel class GraphQLInterface(GraphQLBaseObject): - __valid_type__ = InterfaceTypeDefinitionNode __graphql_type__ = GraphQLClassType.INTERFACE __abstract__ = True - __description__: Optional[str] = None __graphql_name__: Optional[str] = None + __description__: Optional[str] = None def __init_subclass__(cls) -> None: - super().__init_subclass__() - if cls.__dict__.get("__abstract__"): return cls.__abstract__ = False if cls.__dict__.get("__schema__"): - valid_type = getattr(cls, "__valid_type__", InterfaceTypeDefinitionNode) - cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) + cls.__kwargs__ = validate_object_type_with_schema( + cls, InterfaceTypeDefinitionNode + ) else: cls.__kwargs__ = validate_object_type_without_schema(cls) @classmethod - def __get_graphql_model_with_schema__(cls) -> "GraphQLInterfaceModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": definition = cast( InterfaceTypeDefinitionNode, parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), @@ -77,7 +75,7 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLInterfaceModel": @classmethod def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str - ) -> "GraphQLInterfaceModel": + ) -> "GraphQLModel": type_data = cls.get_graphql_object_data(metadata) type_aliases = getattr(cls, "__aliases__", None) or {} diff --git a/ariadne_graphql_modules/object_type/graphql_type.py b/ariadne_graphql_modules/object_type/graphql_type.py index a053760..848b73b 100644 --- a/ariadne_graphql_modules/object_type/graphql_type.py +++ b/ariadne_graphql_modules/object_type/graphql_type.py @@ -14,6 +14,10 @@ ) from ariadne_graphql_modules.base_object_type.graphql_field import GraphQLClassData +from ariadne_graphql_modules.base_object_type.validators import ( + validate_object_type_with_schema, + validate_object_type_without_schema, +) from ..types import GraphQLClassType @@ -31,13 +35,27 @@ class GraphQLObject(GraphQLBaseObject): - __valid_type__ = ObjectTypeDefinitionNode __graphql_type__ = GraphQLClassType.OBJECT __abstract__ = True __description__: Optional[str] = None __schema__: Optional[str] = None __graphql_name__: Optional[str] = None + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.__dict__.get("__abstract__"): + return + + cls.__abstract__ = False + + if cls.__dict__.get("__schema__"): + cls.__kwargs__ = validate_object_type_with_schema( + cls, ObjectTypeDefinitionNode + ) + else: + cls.__kwargs__ = validate_object_type_without_schema(cls) + @classmethod def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": definition = cast( diff --git a/ariadne_graphql_modules/subscription_type/graphql_type.py b/ariadne_graphql_modules/subscription_type/graphql_type.py index 79c62de..98fde09 100644 --- a/ariadne_graphql_modules/subscription_type/graphql_type.py +++ b/ariadne_graphql_modules/subscription_type/graphql_type.py @@ -47,7 +47,6 @@ class GraphQLSubscription(GraphQLBaseObject): - __valid_type__ = ObjectTypeDefinitionNode __graphql_type__ = GraphQLClassType.SUBSCRIPTION __abstract__: bool = True __description__: Optional[str] = None @@ -62,8 +61,9 @@ def __init_subclass__(cls) -> None: cls.__abstract__ = False if cls.__dict__.get("__schema__"): - valid_type = getattr(cls, "__valid_type__", ObjectTypeDefinitionNode) - cls.__kwargs__ = validate_object_type_with_schema(cls, valid_type) + cls.__kwargs__ = validate_object_type_with_schema( + cls, ObjectTypeDefinitionNode + ) else: cls.__kwargs__ = validate_object_type_without_schema(cls) diff --git a/tests/conftest.py b/tests/conftest.py index cdfd440..b8b17ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ def original_datadir() -> Path: return Path(__file__).parent / "snapshots" -def pytest_sessionfinish(session, exitstatus): +def pytest_sessionfinish(*_): # This will be called after all tests are done obtained_files = glob.glob("**/*.obtained.yml", recursive=True) for file in obtained_files: diff --git a/tests/test_compatibility_layer.py b/tests/test_compatibility_layer.py index 90dc845..ca17d43 100644 --- a/tests/test_compatibility_layer.py +++ b/tests/test_compatibility_layer.py @@ -353,7 +353,7 @@ class ExampleInterface(InterfaceType): """ @staticmethod - def resolve_type(instance, *_): + def resolve_type(*_): return "Threads" class ChatSubscription(SubscriptionType): diff --git a/tests/test_deferred_type.py b/tests/test_deferred_type.py index a878f30..015a024 100644 --- a/tests/test_deferred_type.py +++ b/tests/test_deferred_type.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-variable from unittest.mock import Mock import pytest diff --git a/tests/test_enum_type.py b/tests/test_enum_type.py index 903d80a..3be906f 100644 --- a/tests/test_enum_type.py +++ b/tests/test_enum_type.py @@ -1,6 +1,6 @@ from enum import Enum -from graphql import GraphQLResolveInfo, graphql_sync +from graphql import graphql_sync from ariadne_graphql_modules import ( GraphQLEnum, @@ -23,6 +23,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> UserLevelEnum: return UserLevelEnum.MEMBER @@ -61,6 +62,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> int: return 0 @@ -99,6 +101,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> str: return "ADMIN" @@ -221,6 +224,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> UserLevelEnum: return UserLevelEnum.MEMBER @@ -266,6 +270,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> int: return 2 @@ -306,6 +311,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> str: return "GUEST" @@ -352,6 +358,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> int: return 2 @@ -399,6 +406,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> int: return 2 @@ -446,6 +454,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> int: return 2 @@ -494,6 +503,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> int: return 2 @@ -532,9 +542,8 @@ class QueryType(GraphQLObject): @GraphQLObject.resolver( "set_level", args={"level": GraphQLObject.argument(graphql_type=UserLevel)} ) - def resolve_level( - obj, info: GraphQLResolveInfo, *, level: UserLevelEnum - ) -> UserLevelEnum: + @staticmethod + def resolve_level(*_, level: UserLevelEnum) -> UserLevelEnum: return level schema = make_executable_schema(QueryType) diff --git a/tests/test_enum_type_validation.py b/tests/test_enum_type_validation.py index a5106d1..6c11655 100644 --- a/tests/test_enum_type_validation.py +++ b/tests/test_enum_type_validation.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-variable from enum import Enum import pytest @@ -177,6 +178,6 @@ def test_enum_type_validation_fails_for_invalid_members(data_regression): with pytest.raises(ValueError) as exc_info: class UserLevel(GraphQLEnum): - __members__ = "INVALID" + __members__ = "INVALID" # type: ignore data_regression.check(str(exc_info.value)) diff --git a/tests/test_get_field_args_from_resolver.py b/tests/test_get_field_args_from_resolver.py index b281c28..0cc824a 100644 --- a/tests/test_get_field_args_from_resolver.py +++ b/tests/test_get_field_args_from_resolver.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-argument from ariadne_graphql_modules.object_type import get_field_args_from_resolver @@ -112,6 +113,7 @@ def field_resolver(obj, info, name, age_cutoff: int): def test_field_has_arg_after_obj_and_info_args_on_class_function(): class CustomObject: + @staticmethod def field_resolver(obj, info, name): pass diff --git a/tests/test_id_type.py b/tests/test_id_type.py index af472f6..c313de8 100644 --- a/tests/test_id_type.py +++ b/tests/test_id_type.py @@ -84,8 +84,9 @@ class QueryType(GraphQLObject): def test_graphql_id_object_field_instance(assert_schema_equals): class QueryType(GraphQLObject): - @GraphQLObject.field() - def id(*_) -> GraphQLID: + @GraphQLObject.field(name="id") + @staticmethod + def id_type(*_) -> GraphQLID: return GraphQLID(115) schema = make_executable_schema(QueryType) @@ -107,8 +108,9 @@ def id(*_) -> GraphQLID: def test_graphql_id_object_field_instance_arg(assert_schema_equals): class QueryType(GraphQLObject): - @GraphQLObject.field() - def id(*_, arg: GraphQLID) -> str: + @GraphQLObject.field(name="id") + @staticmethod + def id_type(*_, arg: GraphQLID) -> str: return str(arg) schema = make_executable_schema(QueryType) @@ -133,8 +135,9 @@ class ArgType(GraphQLInput): id: GraphQLID class QueryType(GraphQLObject): - @GraphQLObject.field() - def id(*_, arg: ArgType) -> str: + @GraphQLObject.field(name="id") + @staticmethod + def id_type(*_, arg: ArgType) -> str: return str(arg.id) schema = make_executable_schema(QueryType) diff --git a/tests/test_input_type.py b/tests/test_input_type.py index 28f4676..dc74d4b 100644 --- a/tests/test_input_type.py +++ b/tests/test_input_type.py @@ -196,8 +196,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.age])}" schema = make_executable_schema(QueryType) @@ -205,7 +206,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -215,7 +216,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') + result = graphql_sync(schema, '{ search(queryInput: { query: "Hello" }) }') assert not result.errors assert result.data == {"search": "['Hello', None]"} @@ -237,8 +238,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.age])}" schema = make_executable_schema(QueryType) @@ -246,7 +248,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -256,7 +258,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, '{ search(input: { query: "Hello" }) }') + result = graphql_sync(schema, '{ search(queryInput: { query: "Hello" }) }') assert not result.errors assert result.data == {"search": "['Hello', None]"} @@ -271,8 +273,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.min_age])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.min_age])}" schema = make_executable_schema(QueryType) @@ -280,7 +283,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -290,7 +293,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + result = graphql_sync(schema, "{ search(queryInput: { minAge: 21 }) }") assert not result.errors assert result.data == {"search": "[None, 21]"} @@ -312,8 +315,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.min_age])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.min_age])}" schema = make_executable_schema(QueryType) @@ -321,7 +325,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -331,7 +335,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + result = graphql_sync(schema, "{ search(queryInput: { minAge: 21 }) }") assert not result.errors assert result.data == {"search": "[None, 21]"} @@ -354,8 +358,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.age])}" schema = make_executable_schema(QueryType) @@ -363,7 +368,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -373,7 +378,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, "{ search(input: { minAge: 21 }) }") + result = graphql_sync(schema, "{ search(queryInput: { minAge: 21 }) }") assert not result.errors assert result.data == {"search": "[None, 21]"} @@ -388,13 +393,14 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - if input.extra: - extra_repr = input.extra.query + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + if query_input.extra: + extra_repr = query_input.extra.query else: extra_repr = None - return f"{repr([input.query, extra_repr])}" + return f"{repr([query_input.query, extra_repr])}" schema = make_executable_schema(QueryType) @@ -402,7 +408,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -417,7 +423,7 @@ def resolve_search(*_, input: SearchInput) -> str: """ { search( - input: { query: "Hello", extra: { query: "Other" } } + queryInput: { query: "Hello", extra: { query: "Other" } } ) } """, @@ -443,8 +449,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.age])}" schema = make_executable_schema(QueryType) @@ -452,7 +459,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -462,7 +469,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, "{ search(input: {}) }") + result = graphql_sync(schema, "{ search(queryInput: {}) }") assert not result.errors assert result.data == {"search": "['Search', 42]"} @@ -478,8 +485,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age, input.flag])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.age, query_input.flag])}" schema = make_executable_schema(QueryType) @@ -487,7 +495,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -498,7 +506,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, "{ search(input: {}) }") + result = graphql_sync(schema, "{ search(queryInput: {}) }") assert not result.errors assert result.data == {"search": "['default', 42, False]"} @@ -514,8 +522,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return f"{repr([input.query, input.age, input.flag])}" + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return f"{repr([query_input.query, query_input.age, query_input.flag])}" schema = make_executable_schema(QueryType) @@ -523,7 +532,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -534,7 +543,7 @@ def resolve_search(*_, input: SearchInput) -> str: """, ) - result = graphql_sync(schema, "{ search(input: {}) }") + result = graphql_sync(schema, "{ search(queryInput: {}) }") assert not result.errors assert result.data == {"search": "['default', 42, False]"} @@ -548,8 +557,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return str(input) + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return str(query_input) schema = make_executable_schema(QueryType) @@ -557,7 +567,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -580,8 +590,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return str(input) + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return str(query_input) schema = make_executable_schema(QueryType) @@ -589,7 +600,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -608,8 +619,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return str(input) + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return str(query_input) schema = make_executable_schema(QueryType) @@ -617,7 +629,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -637,8 +649,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: SearchInput) -> str: - return str(input) + @staticmethod + def resolve_search(*_, query_input: SearchInput) -> str: + return str(query_input) schema = make_executable_schema(QueryType) @@ -646,7 +659,7 @@ def resolve_search(*_, input: SearchInput) -> str: schema, """ type Query { - search(input: SearchInput!): String! + search(queryInput: SearchInput!): String! } input SearchInput { @@ -675,8 +688,9 @@ class QueryType(GraphQLObject): search: str @GraphQLObject.resolver("search") - def resolve_search(*_, input: MainInput) -> str: - return str(input) + @staticmethod + def resolve_search(*_, query_input: MainInput) -> str: + return str(query_input) schema = make_executable_schema(QueryType) @@ -684,7 +698,7 @@ def resolve_search(*_, input: MainInput) -> str: schema, """ type Query { - search(input: MainInput!): String! + search(queryInput: MainInput!): String! } input MainInput { diff --git a/tests/test_input_type_validation.py b/tests/test_input_type_validation.py index 94ea418..ceab9a0 100644 --- a/tests/test_input_type_validation.py +++ b/tests/test_input_type_validation.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-variable import pytest from ariadne import gql @@ -116,7 +117,7 @@ def test_input_type_validation_fails_for_unsupported_attr_default(data_regressio with pytest.raises(TypeError) as exc_info: class QueryType(GraphQLInput): - attr: str = InvalidType() + attr: str = InvalidType() # type: ignore data_regression.check(str(exc_info.value)) diff --git a/tests/test_interface_type.py b/tests/test_interface_type.py index 93f98b8..ceab065 100644 --- a/tests/test_interface_type.py +++ b/tests/test_interface_type.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import List, Optional, Union from graphql import graphql_sync @@ -29,6 +29,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), @@ -94,6 +95,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(), @@ -168,6 +170,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), @@ -221,6 +224,7 @@ class SuperUserType(UserType): class QueryType(GraphQLObject): @GraphQLObject.field + @staticmethod def users(*_) -> List[UserInterface]: return [ UserType(id="1", username="test_user"), @@ -272,7 +276,7 @@ class UserInterface(GraphQLInterface): summary: str score: int - __description__ = "Lorem ipsum." + __description__: Optional[str] = "Lorem ipsum." class UserType(GraphQLObject, UserInterface): id: GraphQLID @@ -280,6 +284,7 @@ class UserType(GraphQLObject, UserInterface): class QueryType(GraphQLObject): @GraphQLObject.field + @staticmethod def user(*_) -> UserType: return UserType(id="1", username="test_user") @@ -314,6 +319,7 @@ class UserInterface(GraphQLInterface): score: int @GraphQLInterface.resolver("score", description="Lorem ipsum.") + @staticmethod def resolve_score(*_): return 200 @@ -326,6 +332,7 @@ class MyType(GraphQLObject, UserInterface): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[UserInterface]) + @staticmethod def users(*_) -> List[UserInterface]: return [MyType(id="2", name="old", summary="ss", score=22)] @@ -379,6 +386,7 @@ class UserInterface(GraphQLInterface): """ @GraphQLInterface.resolver("summary") + @staticmethod def resolve_summary(*_): return "base_line" @@ -397,6 +405,7 @@ class ResultType(GraphQLUnion): class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=List[ResultType]) + @staticmethod def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(name="Bob"), diff --git a/tests/test_interface_type_validation.py b/tests/test_interface_type_validation.py index 7994fd8..1e01d9f 100644 --- a/tests/test_interface_type_validation.py +++ b/tests/test_interface_type_validation.py @@ -1,4 +1,5 @@ -from typing import List +# pylint: disable=unused-variable +from typing import Optional import pytest from ariadne_graphql_modules import ( @@ -28,7 +29,9 @@ def test_interface_with_schema_object_with_no_schema(data_regression): with pytest.raises(ValueError) as exc_info: class UserInterface(GraphQLInterface): - __schema__ = """ + __schema__: Optional[ + str + ] = """ interface UserInterface { summary: String! score: Int! @@ -36,6 +39,7 @@ class UserInterface(GraphQLInterface): """ @GraphQLInterface.resolver("score") + @staticmethod def resolve_score(*_): return 2211 diff --git a/tests/test_make_executable_schema.py b/tests/test_make_executable_schema.py index 9dc96b3..3445e13 100644 --- a/tests/test_make_executable_schema.py +++ b/tests/test_make_executable_schema.py @@ -1,11 +1,10 @@ -from typing import Tuple, Union +from typing import Union import pytest from graphql import ( GraphQLField, GraphQLInterfaceType, GraphQLObjectType, - GraphQLSchema, default_field_resolver, graphql_sync, ) @@ -178,8 +177,9 @@ class ThirdRoot(GraphQLObject): def test_schema_validation_fails_if_lazy_type_doesnt_exist(data_regression): class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=list["Missing"]) - def other(obj, info): + @GraphQLObject.field(graphql_type=list["Missing"]) # type: ignore + @staticmethod + def other(*_): return None with pytest.raises(TypeError) as exc_info: @@ -190,8 +190,9 @@ def other(obj, info): def test_schema_validation_passes_if_lazy_type_exists(): class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=list["Exists"]) - def other(obj, info): + @GraphQLObject.field(graphql_type=list["Exists"]) # type: ignore + @staticmethod + def other(*_): return None type_def = """ @@ -212,15 +213,6 @@ def test_make_executable_schema_raises_error_if_called_without_any_types( data_regression.check(str(exc_info.value)) -def test_make_executable_schema_raises_error_if_called_without_any_types( - data_regression, -): - with pytest.raises(ValueError) as exc_info: - make_executable_schema(QueryType) - - data_regression.check(str(exc_info.value)) - - def test_make_executable_schema_doesnt_set_resolvers_if_convert_names_case_is_not_enabled(): class QueryType(GraphQLObject): other_field: str @@ -283,8 +275,12 @@ def test_make_executable_schema_supports_vanilla_directives(): field: String! @upper } """ - - schema = make_executable_schema(type_def, directives={"upper": UpperDirective}) + schema = make_executable_schema( + type_def, + directives={ + "upper": UpperDirective, # type: ignore + }, + ) result = graphql_sync( schema, "{ field }", @@ -294,9 +290,7 @@ def test_make_executable_schema_supports_vanilla_directives(): def test_make_executable_schema_custom_convert_name_case(assert_schema_equals): - def custom_name_converter( - name: str, schema: GraphQLSchema, path: Tuple[str, ...] - ) -> str: + def custom_name_converter(name: str, *_) -> str: return f"custom_{name}" class QueryType(GraphQLObject): diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 443d022..51523ac 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -37,7 +37,7 @@ def test_metadata_returns_model_for_type(assert_ast_equals, metadata): ) -def test_metadata_returns_graphql_name_for_type(assert_ast_equals, metadata): +def test_metadata_returns_graphql_name_for_type(metadata): assert metadata.get_graphql_name(QueryType) == "Query" @@ -67,7 +67,7 @@ def test_metadata_returns_model_for_standard_enum(assert_ast_equals, metadata): ) -def test_metadata_returns_graphql_name_for_standard_enum(assert_ast_equals, metadata): +def test_metadata_returns_graphql_name_for_standard_enum(metadata): assert metadata.get_graphql_name(UserLevel) == "UserLevel" @@ -96,5 +96,5 @@ def test_metadata_returns_model_for_annotated_enum(assert_ast_equals, metadata): ) -def test_metadata_returns_graphql_name_for_annotated_enum(assert_ast_equals, metadata): +def test_metadata_returns_graphql_name_for_annotated_enum(metadata): assert metadata.get_graphql_name(SeverityLevel) == "SeverityEnum" diff --git a/tests/test_object_type.py b/tests/test_object_type.py index 1d5f247..563d1f7 100644 --- a/tests/test_object_type.py +++ b/tests/test_object_type.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines from typing import Optional import pytest @@ -374,7 +375,8 @@ class QueryType(GraphQLObject): def test_object_type_with_field_resolver(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field - def hello(obj, info) -> str: + @staticmethod + def hello(*_) -> str: return "Hello World!" schema = make_executable_schema(QueryType) @@ -441,7 +443,8 @@ class QueryType(GraphQLObject): name: str @GraphQLObject.field - def hello(obj, info) -> str: + @staticmethod + def hello(*_) -> str: return "Hello World!" schema = make_executable_schema(QueryType) @@ -500,7 +503,8 @@ class QueryType(GraphQLObject): user: UserType @GraphQLObject.field(graphql_type=PostType) - def post(obj, info): + @staticmethod + def post(*_): return {"message": "test"} schema = make_executable_schema(QueryType) @@ -543,6 +547,7 @@ class QueryType(GraphQLObject): hello: str @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -568,6 +573,7 @@ class QueryType(GraphQLObject): hello: str = GraphQLObject.field(name="hello") @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -599,6 +605,7 @@ class QueryType(GraphQLObject): ) @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -646,7 +653,8 @@ class QueryType(GraphQLObject): def test_field_decorator_sets_description_for_field(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field(description="Lorem ipsum.") - def hello(obj, info) -> str: + @staticmethod + def hello(*_) -> str: return "Hello World!" schema = make_executable_schema(QueryType) @@ -672,7 +680,8 @@ class QueryType(GraphQLObject): @GraphQLObject.field( args={"name": GraphQLObject.argument(description="Lorem ipsum.")} ) - def hello(obj, info, name: str) -> str: + @staticmethod + def hello(*_, name: str) -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -700,6 +709,7 @@ class QueryType(GraphQLObject): hello: str @GraphQLObject.resolver("hello", description="Lorem ipsum.") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -732,6 +742,7 @@ class QueryType(GraphQLObject): ) @GraphQLObject.resolver("hello", description="Lorem ipsum.") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -760,7 +771,8 @@ class QueryType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"name": GraphQLObject.argument(description="Lorem ipsum.")} ) - def resolve_hello(obj, info, name: str) -> str: + @staticmethod + def resolve_hello(*_, name: str) -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -797,6 +809,7 @@ class QueryType(GraphQLObject): ) @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_, name: str): return f"Hello {name}!" @@ -835,6 +848,7 @@ class QueryType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"name": GraphQLObject.argument(description="Description")} ) + @staticmethod def resolve_hello(*_, name: str): return f"Hello {name}!" @@ -917,6 +931,7 @@ class CategoryType(GraphQLObject): class QueryType(GraphQLObject): @GraphQLObject.field() + @staticmethod def category(*_) -> CategoryType: return CategoryType( name="Welcome", diff --git a/tests/test_object_type_field_args.py b/tests/test_object_type_field_args.py index 10e815b..87fa422 100644 --- a/tests/test_object_type_field_args.py +++ b/tests/test_object_type_field_args.py @@ -1,4 +1,4 @@ -from graphql import GraphQLResolveInfo, graphql_sync +from graphql import graphql_sync from ariadne_graphql_modules import GraphQLObject, make_executable_schema @@ -6,7 +6,8 @@ def test_object_type_field_resolver_with_scalar_arg(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field - def hello(obj, info: GraphQLResolveInfo, *, name: str) -> str: + @staticmethod + def hello(*_, name: str) -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -53,7 +54,8 @@ class QueryType(GraphQLObject): def test_object_type_field_resolver_with_arg_default_value(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field - def hello(obj, info: GraphQLResolveInfo, *, name: str = "Anon") -> str: + @staticmethod + def hello(*_, name: str = "Anon") -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -104,7 +106,8 @@ class QueryType(GraphQLObject): @GraphQLObject.field( args={"name": GraphQLObject.argument(default_value="Anon")}, ) - def hello(obj, info: GraphQLResolveInfo, *, name: str) -> str: + @staticmethod + def hello(*_, name: str) -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -164,7 +167,8 @@ class QueryType(GraphQLObject): """ @GraphQLObject.resolver("hello") - def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str) -> str: + @staticmethod + def resolve_hello(*_, name: str) -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -195,7 +199,8 @@ class QueryType(GraphQLObject): """ @GraphQLObject.resolver("hello") - def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str = "Anon") -> str: + @staticmethod + def resolve_hello(*_, name: str = "Anon") -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -228,7 +233,8 @@ class QueryType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"name": GraphQLObject.argument(default_value="Other")} ) - def resolve_hello(obj, info: GraphQLResolveInfo, *, name: str = "Anon") -> str: + @staticmethod + def resolve_hello(*_, name: str = "Anon") -> str: return f"Hello {name}!" schema = make_executable_schema(QueryType) @@ -289,6 +295,7 @@ class QueryType(GraphQLObject): hello: str @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_, name: str): return f"Hello {name}!" @@ -348,6 +355,7 @@ class QueryType(GraphQLObject): hello: str @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_, first_name: str) -> str: return f"Hello {first_name}!" @@ -371,6 +379,7 @@ def resolve_hello(*_, first_name: str) -> str: def test_object_type_field_instance_arg_with_out_name(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field + @staticmethod def hello(*_, first_name: str) -> str: return f"Hello {first_name}!" @@ -398,6 +407,7 @@ class QueryType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"first_name": GraphQLObject.argument(name="name")} ) + @staticmethod def resolve_hello(*_, first_name: str) -> str: return f"Hello {first_name}!" @@ -421,6 +431,7 @@ def resolve_hello(*_, first_name: str) -> str: def test_object_type_field_instance_arg_with_custom_name(assert_schema_equals): class QueryType(GraphQLObject): @GraphQLObject.field(args={"first_name": GraphQLObject.argument(name="name")}) + @staticmethod def hello(*_, first_name: str) -> str: return f"Hello {first_name}!" diff --git a/tests/test_object_type_validation.py b/tests/test_object_type_validation.py index 4396a51..2ec6999 100644 --- a/tests/test_object_type_validation.py +++ b/tests/test_object_type_validation.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-variable import pytest from ariadne import gql @@ -62,6 +63,7 @@ class QueryType(GraphQLObject): hello: str @GraphQLObject.resolver("other") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -83,6 +85,7 @@ class QueryType(GraphQLObject): ) @GraphQLObject.resolver("other") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -110,6 +113,7 @@ class CustomType(GraphQLObject): user_id: str @GraphQLObject.field(name="userId") + @staticmethod def lorem(*_) -> str: return "Hello World!" @@ -123,10 +127,12 @@ def test_object_type_validation_fails_for_multiple_fields_with_same_graphql_name class CustomType(GraphQLObject): @GraphQLObject.field(name="hello") + @staticmethod def lorem(*_) -> str: return "Hello World!" @GraphQLObject.field(name="hello") + @staticmethod def ipsum(*_) -> str: return "Hello World!" @@ -138,6 +144,7 @@ def test_object_type_validation_fails_for_undefined_field_resolver_arg(data_regr class CustomType(GraphQLObject): @GraphQLObject.field(args={"invalid": GraphQLObject.argument(name="test")}) + @staticmethod def hello(*_) -> str: return "Hello World!" @@ -153,6 +160,7 @@ class CustomType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"invalid": GraphQLObject.argument(name="test")} ) + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -164,8 +172,9 @@ def test_object_type_validation_fails_for_missing_field_resolver_arg(data_regres class CustomType(GraphQLObject): @GraphQLObject.field(args={"invalid": GraphQLObject.argument(name="test")}) + @staticmethod def hello(*_, name: str) -> str: - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -179,8 +188,9 @@ class CustomType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"invalid": GraphQLObject.argument(name="test")} ) + @staticmethod def resolve_hello(*_, name: str): - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -192,10 +202,12 @@ class CustomType(GraphQLObject): hello: str @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_): return "Hello World!" @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello_other(*_): return "Hello World!" @@ -217,10 +229,12 @@ class CustomType(GraphQLObject): ) @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_): return "Hello World!" @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello_other(*_): return "Hello World!" @@ -236,6 +250,7 @@ class CustomType(GraphQLObject): hello: str = GraphQLObject.field(description="Description") @GraphQLObject.resolver("hello", description="Other") + @staticmethod def ipsum(*_) -> str: return "Hello World!" @@ -256,6 +271,7 @@ class CustomType(GraphQLObject): """ @GraphQLObject.resolver("hello", description="Other") + @staticmethod def ipsum(*_) -> str: return "Hello World!" @@ -277,8 +293,9 @@ class CustomType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"other": GraphQLObject.argument(description="Ok")} ) + @staticmethod def ipsum(*_, name: str) -> str: - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -298,8 +315,9 @@ class CustomType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"name": GraphQLObject.argument(name="Other")} ) + @staticmethod def ipsum(*_, name: str) -> str: - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -319,8 +337,9 @@ class CustomType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"name": GraphQLObject.argument(graphql_type=str)} ) + @staticmethod def ipsum(*_, name: str) -> str: - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -343,8 +362,9 @@ class CustomType(GraphQLObject): @GraphQLObject.resolver( "hello", args={"name": GraphQLObject.argument(description="Other")} ) + @staticmethod def ipsum(*_, name: str) -> str: - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -355,11 +375,13 @@ def test_object_type_validation_fails_for_field_with_multiple_args( with pytest.raises(ValueError) as exc_info: class CustomType(GraphQLObject): - @GraphQLObject.field(name="hello", args={""}) + @GraphQLObject.field(name="hello", args={""}) # type: ignore + @staticmethod def lorem(*_, a: int) -> str: - return "Hello World!" + return f"Hello {a}!" @GraphQLObject.resolver("lorem", description="Other") + @staticmethod def ipsum(*_) -> str: return "Hello World!" @@ -409,6 +431,7 @@ class CustomType(GraphQLObject): } @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -427,6 +450,7 @@ class CustomType(GraphQLObject): } @GraphQLObject.resolver("welcome") + @staticmethod def resolve_welcome(*_): return "Hello World!" @@ -450,6 +474,7 @@ class CustomType(GraphQLObject): } @GraphQLObject.resolver("hello") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -475,6 +500,7 @@ class CustomType(GraphQLObject): ok: str @GraphQLObject.resolver("ok") + @staticmethod def resolve_hello(*_): return "Hello World!" @@ -509,8 +535,9 @@ def test_object_type_validation_fails_for_unsupported_resolver_arg_default( class QueryType(GraphQLObject): @GraphQLObject.field - def hello(*_, name: str = InvalidType): - return "Hello World!" + @staticmethod + def hello(*_, name: str = InvalidType): # type: ignore + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -524,8 +551,9 @@ class QueryType(GraphQLObject): @GraphQLObject.field( args={"name": GraphQLObject.argument(default_value=InvalidType)} ) + @staticmethod def hello(*_, name: str): - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -545,8 +573,9 @@ class QueryType(GraphQLObject): ) @GraphQLObject.resolver("hello") - def resolve_hello(*_, name: str = InvalidType): - return "Hello World!" + @staticmethod + def resolve_hello(*_, name: str = InvalidType): # type: ignore + return f"Hello {name}!" data_regression.check(str(exc_info.value)) @@ -569,7 +598,8 @@ class QueryType(GraphQLObject): "hello", args={"name": GraphQLObject.argument(default_value=InvalidType)}, ) + @staticmethod def resolve_hello(*_, name: str): - return "Hello World!" + return f"Hello {name}!" data_regression.check(str(exc_info.value)) diff --git a/tests/test_scalar_type.py b/tests/test_scalar_type.py index 03e34e3..1c386b7 100644 --- a/tests/test_scalar_type.py +++ b/tests/test_scalar_type.py @@ -28,6 +28,7 @@ class QueryType(GraphQLObject): date: DateScalar @GraphQLObject.resolver("date") + @staticmethod def resolve_date(*_) -> DateScalar: return DateScalar(date(1989, 10, 30)) @@ -55,6 +56,7 @@ class QueryType(GraphQLObject): scalar_date: DateScalar @GraphQLObject.resolver("scalar_date", graphql_type=DateScalar) + @staticmethod def resolve_date(*_) -> date: return date(1989, 10, 30) @@ -93,6 +95,7 @@ class QueryType(GraphQLObject): test: SerializeTestScalar @GraphQLObject.resolver("test", graphql_type=str) + @staticmethod def resolve_date(*_) -> SerializeTestScalar: return SerializeTestScalar(value="Hello!") @@ -120,6 +123,7 @@ class QueryType(GraphQLObject): date: SchemaDateScalar @GraphQLObject.resolver("date") + @staticmethod def resolve_date(*_) -> SchemaDateScalar: return SchemaDateScalar(date(1989, 10, 30)) diff --git a/tests/test_scalar_type_validation.py b/tests/test_scalar_type_validation.py index edd193e..c0b1b36 100644 --- a/tests/test_scalar_type_validation.py +++ b/tests/test_scalar_type_validation.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-variable import pytest from ariadne import gql diff --git a/tests/test_standard_enum.py b/tests/test_standard_enum.py index ca95562..82453c9 100644 --- a/tests/test_standard_enum.py +++ b/tests/test_standard_enum.py @@ -62,7 +62,7 @@ def __get_graphql_model__(): assert graphql_model == "CustomModel" -def test_graphql_enum_model_is_created_with_name_from_method(assert_ast_equals): +def test_graphql_enum_model_is_created_with_name_from_method(): class UserLevel(Enum): GUEST = 0 MEMBER = 1 @@ -233,6 +233,7 @@ class QueryType(GraphQLObject): level: UserLevel @GraphQLObject.resolver("level") + @staticmethod def resolve_level(*_) -> UserLevel: return UserLevel.MODERATOR @@ -260,6 +261,7 @@ def resolve_level(*_) -> UserLevel: assert result.data == {"level": "MODERATOR"} +# pylint: disable=no-member def test_graphql_enum_decorator_without_options_sets_model_on_enum(assert_ast_equals): @graphql_enum class SeverityLevel(Enum): @@ -267,7 +269,7 @@ class SeverityLevel(Enum): MEDIUM = 1 HIGH = 2 - graphql_model = SeverityLevel.__get_graphql_model__() + graphql_model = SeverityLevel.__get_graphql_model__() # type: ignore assert graphql_model.name == "SeverityLevel" assert graphql_model.members == { @@ -288,6 +290,7 @@ class SeverityLevel(Enum): ) +# pylint: disable=no-member def test_graphql_enum_decorator_with_options_sets_model_on_enum(assert_ast_equals): @graphql_enum(name="SeverityEnum", members_exclude=["HIGH"]) class SeverityLevel(Enum): @@ -295,7 +298,7 @@ class SeverityLevel(Enum): MEDIUM = 1 HIGH = 2 - graphql_model = SeverityLevel.__get_graphql_model__() + graphql_model = SeverityLevel.__get_graphql_model__() # type: ignore assert graphql_model.name == "SeverityEnum" assert graphql_model.members == { diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index 5f1baf6..07b54d4 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -1,4 +1,4 @@ -from typing import List +from typing import AsyncIterator, List from ariadne import gql from graphql import subscribe, parse @@ -33,16 +33,19 @@ class SubscriptionType(GraphQLSubscription): message_added: Message @GraphQLSubscription.source("message_added", graphql_type=Message) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @GraphQLSubscription.resolver("message_added") - async def resolve_message_added(message, info): + @staticmethod + async def resolve_message_added(message, *_): return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -70,11 +73,10 @@ def search_sth(*_) -> str: query = parse("subscription { messageAdded {id content author} }") sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -89,28 +91,33 @@ class SubscriptionType(GraphQLSubscription): message_added: Message @GraphQLSubscription.source("message_added", graphql_type=Message) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @GraphQLSubscription.resolver("message_added") - async def resolve_message_added(message, info): + @staticmethod + async def resolve_message_added(message, *_): return message class SubscriptionSecondType(GraphQLSubscription): message_added_second: Message @GraphQLSubscription.source("message_added_second", graphql_type=Message) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @GraphQLSubscription.resolver("message_added_second") - async def resolve_message_added(message, info): + @staticmethod + async def resolve_message_added(message, *_): return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -139,11 +146,10 @@ def search_sth(*_) -> str: query = parse("subscription { messageAdded {id content author} }") sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -161,7 +167,8 @@ class SubscriptionType(GraphQLSubscription): args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, graphql_type=Message, ) - async def message_added_generator(obj, info, channel: GraphQLID): + @staticmethod + async def message_added_generator(*_, channel: GraphQLID): while True: yield { "id": "some_id", @@ -170,11 +177,15 @@ async def message_added_generator(obj, info, channel: GraphQLID): } @GraphQLSubscription.field() - def message_added(message, info, channel: GraphQLID): + @staticmethod + def message_added( + message, *_, channel: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -205,11 +216,10 @@ def search_sth(*_) -> str: query = parse('subscription { messageAdded(channel: "123") {id content author} }') sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -229,7 +239,8 @@ class SubscriptionType(GraphQLSubscription): args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, graphql_type=Message, ) - async def message_added_generator(obj, info, channel: GraphQLID): + @staticmethod + async def message_added_generator(*_, channel: GraphQLID): while True: yield { "id": "some_id", @@ -240,14 +251,18 @@ async def message_added_generator(obj, info, channel: GraphQLID): @GraphQLSubscription.resolver( "message_added", ) - async def resolve_message_added(message, *_, channel: GraphQLID): + @staticmethod + async def resolve_message_added( + message, *_, channel: GraphQLID # pylint: disable=unused-argument + ): return message @GraphQLSubscription.source( "user_joined", graphql_type=Message, ) - async def user_joined_generator(obj, info): + @staticmethod + async def user_joined_generator(*_): while True: yield { "id": "some_id", @@ -257,11 +272,13 @@ async def user_joined_generator(obj, info): @GraphQLSubscription.resolver( "user_joined", ) + @staticmethod async def resolve_user_joined(user, *_): return user class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -287,7 +304,7 @@ def search_sth(*_) -> str: content: String! author: String! } - + type User { id: ID! username: String! @@ -298,11 +315,10 @@ def search_sth(*_) -> str: query = parse("subscription { userJoined {id username} }") sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -319,7 +335,8 @@ class SubscriptionType(GraphQLSubscription): args={"channel_id": GraphQLObject.argument(description="Lorem ipsum.")}, graphql_type=List[Message], ) - async def message_added_generator(obj, info, channel_id: GraphQLID): + @staticmethod + async def message_added_generator(*_, channel_id: GraphQLID): while True: yield [ { @@ -332,11 +349,15 @@ async def message_added_generator(obj, info, channel_id: GraphQLID): @GraphQLSubscription.resolver( "messages_in_channel", ) - async def resolve_message_added(message, *_, channel_id: GraphQLID): + @staticmethod + async def resolve_message_added( + message, *_, channel_id: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -369,11 +390,10 @@ def search_sth(*_) -> str: ) sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -393,7 +413,8 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "notification_received", description="hello", graphql_type=Message ) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield Message(id=1, content="content", author="anon") @@ -401,11 +422,12 @@ async def message_added_generator(obj, info): "notification_received", ) @staticmethod - async def resolve_message_added(message: Message, info): + async def resolve_message_added(message: Message, *_): return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -442,11 +464,10 @@ def search_sth(*_) -> str: query = parse("subscription { notificationReceived { ... on Message { id } } }") sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -465,16 +486,19 @@ class SubscriptionType(GraphQLSubscription): ) @GraphQLSubscription.source("messageAdded", graphql_type=Message) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @GraphQLSubscription.resolver("messageAdded") - async def resolve_message_added(message, info): + @staticmethod + async def resolve_message_added(message, *_): return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -502,11 +526,10 @@ def search_sth(*_) -> str: query = parse("subscription { messageAdded {id content author} }") sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -530,7 +553,8 @@ class SubscriptionType(GraphQLSubscription): "messageAdded", graphql_type=Message, ) - async def message_added_generator(obj, info, channel: GraphQLID): + @staticmethod + async def message_added_generator(*_, channel: GraphQLID): while True: yield { "id": "some_id", @@ -541,11 +565,15 @@ async def message_added_generator(obj, info, channel: GraphQLID): @GraphQLSubscription.resolver( "messageAdded", ) - async def resolve_message_added(message, *_, channel: GraphQLID): + @staticmethod + async def resolve_message_added( + message, *_, channel: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -573,11 +601,10 @@ def search_sth(*_) -> str: query = parse('subscription { messageAdded(channel: "123") {id content author} }') sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -601,7 +628,8 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "messageAdded", ) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield { "id": "some_id", @@ -612,13 +640,15 @@ async def message_added_generator(obj, info): @GraphQLSubscription.resolver( "messageAdded", ) + @staticmethod async def resolve_message_added(message, *_): return message @GraphQLSubscription.source( "userJoined", ) - async def user_joined_generator(obj, info): + @staticmethod + async def user_joined_generator(*_): while True: yield { "id": "some_id", @@ -628,11 +658,13 @@ async def user_joined_generator(obj, info): @GraphQLSubscription.resolver( "userJoined", ) + @staticmethod async def resolve_user_joined(user, *_): return user class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -666,11 +698,10 @@ def search_sth(*_) -> str: query = parse("subscription { userJoined {id username} }") sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -691,7 +722,10 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "messagesInChannel", ) - async def message_added_generator(obj, info, channelId: GraphQLID): + @staticmethod + async def message_added_generator( + obj, info, channelId: GraphQLID + ): # pylint: disable=unused-argument while True: yield [ { @@ -704,11 +738,15 @@ async def message_added_generator(obj, info, channelId: GraphQLID): @GraphQLSubscription.resolver( "messagesInChannel", ) - async def resolve_message_added(message, *_, channelId: GraphQLID): + @staticmethod + async def resolve_message_added( + message, *_, channelId: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -738,11 +776,10 @@ def search_sth(*_) -> str: ) sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -767,7 +804,10 @@ class SubscriptionType(GraphQLSubscription): __aliases__ = {"name": "title"} @GraphQLSubscription.resolver("notificationReceived") - async def resolve_message_added(message, info, channel: str): + @staticmethod + async def resolve_message_added( + message, *_, channel: str # pylint: disable=unused-argument + ): return message @GraphQLSubscription.source( @@ -779,12 +819,16 @@ async def resolve_message_added(message, info, channel: str): ) }, ) - async def message_added_generator(obj, info, channel: str): + @staticmethod + async def message_added_generator( + *_, channel: str # pylint: disable=unused-argument + ): while True: yield Message(id=1, content="content", author="anon") class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -827,11 +871,10 @@ def search_sth(*_) -> str: ) sub = await subscribe(schema, query) - # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors @@ -847,7 +890,8 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "notification_received", description="hello", graphql_type=Message ) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield Message(id=1, content="content", author="anon") @@ -855,11 +899,12 @@ async def message_added_generator(obj, info): "notification_received", ) @staticmethod - async def resolve_message_added(message: Message, info): + async def resolve_message_added(message: Message, *_): return message class QueryType(GraphQLObject): @GraphQLObject.field(graphql_type=str) + @staticmethod def search_sth(*_) -> str: return "search" @@ -897,10 +942,10 @@ def search_sth(*_) -> str: sub = await subscribe(schema, query) # Ensure the subscription is an async iterator - assert hasattr(sub, "__aiter__") + assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" - # Fetch the first result - result = await sub.__anext__() + # Fetch the first result using anext + result = await anext(sub) # Validate the result assert not result.errors diff --git a/tests/test_subscription_type_validation.py b/tests/test_subscription_type_validation.py index 2f5d1df..22aaf68 100644 --- a/tests/test_subscription_type_validation.py +++ b/tests/test_subscription_type_validation.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-variable from ariadne import gql import pytest from ariadne_graphql_modules import ( @@ -31,7 +32,8 @@ class SubscriptionType(GraphQLSubscription): message_added: Message @GraphQLSubscription.source("messageAdded") - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -45,8 +47,9 @@ async def test_field_name_not_str_without_schema(data_regression): class SubscriptionType(GraphQLSubscription): message_added: Message - @GraphQLSubscription.source(23) - async def message_added_generator(obj, info): + @GraphQLSubscription.source(23) # type: ignore + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -61,12 +64,14 @@ class SubscriptionType(GraphQLSubscription): message_added: Message @GraphQLSubscription.source("message_added") - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @GraphQLSubscription.source("message_added") - async def message_added_generator_2(obj, info): + @staticmethod + async def message_added_generator_2(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -80,8 +85,9 @@ async def test_description_not_str_without_schema(data_regression): class SubscriptionType(GraphQLSubscription): message_added: Message - @GraphQLSubscription.source("message_added", description=12) - async def message_added_generator(obj, info): + @GraphQLSubscription.source("message_added", description=12) # type: ignore + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -97,9 +103,10 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "message_added", - args={"channel": 123}, + args={"channel": 123}, # type: ignore ) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -115,9 +122,10 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.source( "message_added", - args=123, + args=123, # type: ignore ) - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -138,7 +146,8 @@ class SubscriptionType(GraphQLSubscription): ) @GraphQLSubscription.source("message_added") - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -159,12 +168,14 @@ class SubscriptionType(GraphQLSubscription): ) @GraphQLSubscription.source("messageAdded") - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @GraphQLSubscription.source("messageAdded") - async def message_added_generator_2(obj, info): + @staticmethod + async def message_added_generator_2(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -186,7 +197,8 @@ class SubscriptionType(GraphQLSubscription): ) @GraphQLSubscription.source("messageAdded", description="hello") - async def message_added_generator(obj, info): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -210,7 +222,8 @@ class SubscriptionType(GraphQLSubscription): "messageAdded", args={"channelID": GraphQLObject.argument(description="Lorem ipsum.")}, ) - async def message_added_generator(obj, info, channel): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -238,7 +251,8 @@ class SubscriptionType(GraphQLSubscription): ) }, ) - async def message_added_generator(obj, info, channel): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -266,7 +280,8 @@ class SubscriptionType(GraphQLSubscription): ) }, ) - async def message_added_generator(obj, info, channel): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} @@ -297,7 +312,8 @@ class SubscriptionType(GraphQLSubscription): ) }, ) - async def message_added_generator(obj, info, channel): + @staticmethod + async def message_added_generator(*_): while True: yield {"id": "some_id", "content": "message", "author": "Anon"} diff --git a/tests/test_typing.py b/tests/test_typing.py index 3723470..5133c07 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -36,7 +36,7 @@ def assert_named_type(type_node, name: str): assert type_node.name.value == name -def test_get_graphql_type_from_python_builtin_type_returns_none(metadata): +def test_get_graphql_type_from_python_builtin_type_returns_none(): assert get_graphql_type(Optional[str]) is None assert get_graphql_type(Union[int, None]) is None assert get_graphql_type(Optional[bool]) is None @@ -46,11 +46,11 @@ def test_get_graphql_type_from_python_builtin_type_returns_none(metadata): sys.version_info >= (3, 9) and sys.version_info < (3, 10), reason="Skip test for Python 3.9", ) -def test_get_graphql_type_from_python_builtin_type_returns_none_pipe_union(metadata): +def test_get_graphql_type_from_python_builtin_type_returns_none_pipe_union(): assert get_graphql_type(float | None) is None -def test_get_graphql_type_from_graphql_type_subclass_returns_type(metadata): +def test_get_graphql_type_from_graphql_type_subclass_returns_type(): class UserType(GraphQLObject): ... assert get_graphql_type(UserType) == UserType @@ -59,7 +59,7 @@ class UserType(GraphQLObject): ... assert get_graphql_type(Optional[List[Optional[UserType]]]) == UserType -def test_get_graphql_type_from_enum_returns_type(metadata): +def test_get_graphql_type_from_enum_returns_type(): class UserLevel(Enum): GUEST = 0 MEMBER = 1 @@ -124,37 +124,37 @@ def test_get_non_null_graphql_list_type_node_from_python_builtin_type(metadata): def test_get_graphql_type_node_from_annotated_type(metadata): class MockType(GraphQLObject): - field: Annotated["ForwardScalar", deferred("tests.types")] + custom_field: Annotated["ForwardScalar", deferred("tests.types")] assert_non_null_type( - get_type_node(metadata, MockType.__annotations__["field"]), "Forward" + get_type_node(metadata, MockType.__annotations__["custom_field"]), "Forward" ) def test_get_graphql_type_node_from_annotated_type_with_relative_path(metadata): class MockType(GraphQLObject): - field: Annotated["ForwardScalar", deferred(".types")] + custom_field: Annotated["ForwardScalar", deferred(".types")] assert_non_null_type( - get_type_node(metadata, MockType.__annotations__["field"]), "Forward" + get_type_node(metadata, MockType.__annotations__["custom_field"]), "Forward" ) def test_get_graphql_type_node_from_nullable_annotated_type(metadata): class MockType(GraphQLObject): - field: Optional[Annotated["ForwardScalar", deferred("tests.types")]] + custom_field: Optional[Annotated["ForwardScalar", deferred("tests.types")]] assert_named_type( - get_type_node(metadata, MockType.__annotations__["field"]), "Forward" + get_type_node(metadata, MockType.__annotations__["custom_field"]), "Forward" ) def test_get_graphql_type_node_from_annotated_enum(metadata): class MockType(GraphQLObject): - field: Annotated["ForwardEnum", deferred("tests.types")] + custom_field: Annotated["ForwardEnum", deferred("tests.types")] assert_non_null_type( - get_type_node(metadata, MockType.__annotations__["field"]), "ForwardEnum" + get_type_node(metadata, MockType.__annotations__["custom_field"]), "ForwardEnum" ) diff --git a/tests/test_union_type.py b/tests/test_union_type.py index 17fcd29..97d3108 100644 --- a/tests/test_union_type.py +++ b/tests/test_union_type.py @@ -126,7 +126,7 @@ class QueryType(GraphQLObject): def search(*_) -> List[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), - "InvalidType", + "InvalidType", # type: ignore ] schema = make_executable_schema(QueryType) diff --git a/tests/test_union_type_validation.py b/tests/test_union_type_validation.py index fac5b1f..99235a8 100644 --- a/tests/test_union_type_validation.py +++ b/tests/test_union_type_validation.py @@ -1,3 +1,4 @@ +import pytest from ariadne_graphql_modules import ( GraphQLID, GraphQLObject, @@ -6,7 +7,6 @@ from ariadne_graphql_modules.union_type.validators import ( validate_union_type_with_schema, ) -import pytest class UserType(GraphQLObject): diff --git a/tests/test_validators.py b/tests/test_validators.py index bdb50e2..8690f82 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -8,14 +8,14 @@ def test_description_validator_passes_type_without_description(): class CustomType: pass - validate_description(CustomType, parse("scalar Custom").definitions[0]) + validate_description(CustomType, parse("scalar Custom").definitions[0]) # type: ignore def test_description_validator_passes_type_with_description_attr(): class CustomType: __description__ = "Example scalar" - validate_description(CustomType, parse("scalar Custom").definitions[0]) + validate_description(CustomType, parse("scalar Custom").definitions[0]) # type: ignore def test_description_validator_raises_error_for_type_with_two_descriptions( @@ -27,7 +27,7 @@ class CustomType: __description__ = "Example scalar" validate_description( - CustomType, + CustomType, # type: ignore parse( """ \"\"\"Lorem ipsum\"\"\" @@ -43,14 +43,14 @@ def test_name_validator_passes_type_without_explicit_name(): class CustomType: pass - validate_name(CustomType, parse("type Custom").definitions[0]) + validate_name(CustomType, parse("type Custom").definitions[0]) # type: ignore def test_name_validator_passes_type_with_graphql_name_attr_matching_definition(): class CustomType: __graphql_name__ = "Custom" - validate_name(CustomType, parse("type Custom").definitions[0]) + validate_name(CustomType, parse("type Custom").definitions[0]) # type: ignore def test_name_validator_raises_error_for_name_and_definition_mismatch(data_regression): @@ -60,7 +60,7 @@ class CustomType: __graphql_name__ = "Example" validate_name( - CustomType, + CustomType, # type: ignore parse("type Custom").definitions[0], ) From 8253bcd8c024e78f7fb179cc85d22f9bde2aa8ca Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 2 Sep 2024 10:20:53 +0200 Subject: [PATCH 55/63] fix code for python 3.9 --- .../base_object_type/graphql_field.py | 4 + .../base_object_type/graphql_type.py | 18 ++-- .../subscription_type/graphql_type.py | 25 ++++-- tests/test_subscription_type.py | 87 +++++++++++-------- tests/test_typing.py | 1 + 5 files changed, 86 insertions(+), 49 deletions(-) diff --git a/ariadne_graphql_modules/base_object_type/graphql_field.py b/ariadne_graphql_modules/base_object_type/graphql_field.py index 6c20974..28d8b91 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_field.py +++ b/ariadne_graphql_modules/base_object_type/graphql_field.py @@ -116,10 +116,14 @@ def object_field( def get_field_type_from_resolver(resolver: Resolver) -> Any: + if isinstance(resolver, staticmethod): + resolver = resolver.__func__ return resolver.__annotations__.get("return") def get_field_type_from_subscriber(subscriber: Subscriber) -> Any: + if isinstance(subscriber, staticmethod): + subscriber = subscriber.__func__ return subscriber.__annotations__.get("return") diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index 3e4333f..f003de7 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -359,9 +359,9 @@ def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): if cls_attr.resolver: resolver = cls_attr.resolver if isinstance(resolver, staticmethod): - resolver = cls_attr.resolver.__func__ # type: ignore[attr-defined] + resolver = resolver.__func__ # type: ignore[attr-defined] fields_data.fields_resolvers[attr_name] = resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) + field_args = get_field_args_from_resolver(resolver) if field_args: fields_data.fields_args[attr_name] = update_field_args_options( field_args, cls_attr.args @@ -381,8 +381,11 @@ def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): fields_data.fields_descriptions[cls_attr.field] = ( cls_attr.description ) - fields_data.fields_resolvers[cls_attr.field] = cls_attr.resolver - field_args = get_field_args_from_resolver(cls_attr.resolver) + resolver = cls_attr.resolver + if isinstance(resolver, staticmethod): + resolver = resolver.__func__ # type: ignore[attr-defined] + fields_data.fields_resolvers[cls_attr.field] = resolver + field_args = get_field_args_from_resolver(resolver) if field_args and not fields_data.fields_args.get(cls_attr.field): fields_data.fields_args[cls_attr.field] = update_field_args_options( field_args, cls_attr.args @@ -400,8 +403,11 @@ def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): fields_data.fields_descriptions[cls_attr.field] = ( cls_attr.description ) - fields_data.fields_subscribers[cls_attr.field] = cls_attr.subscriber - field_args = get_field_args_from_subscriber(cls_attr.subscriber) + subscriber = cls_attr.subscriber + if isinstance(subscriber, staticmethod): + subscriber = subscriber.__func__ # type: ignore[attr-defined] + fields_data.fields_subscribers[cls_attr.field] = subscriber + field_args = get_field_args_from_subscriber(subscriber) if field_args: fields_data.fields_args[cls_attr.field] = update_field_args_options( field_args, cls_attr.args diff --git a/ariadne_graphql_modules/subscription_type/graphql_type.py b/ariadne_graphql_modules/subscription_type/graphql_type.py index 98fde09..d7c35a3 100644 --- a/ariadne_graphql_modules/subscription_type/graphql_type.py +++ b/ariadne_graphql_modules/subscription_type/graphql_type.py @@ -83,9 +83,15 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) if isinstance(cls_attr, GraphQLObjectResolver): + resolver = cls_attr.resolver + if isinstance(resolver, staticmethod): + resolver = resolver.__func__ # type: ignore[attr-defined] resolvers[cls_attr.field] = cls_attr.resolver if isinstance(cls_attr, GraphQLObjectSource): - subscribers[cls_attr.field] = cls_attr.subscriber + subscriber = cls_attr.subscriber + if isinstance(subscriber, staticmethod): + subscriber = subscriber.__func__ # type: ignore[attr-defined] + subscribers[cls_attr.field] = subscriber description_node = get_description_node(cls_attr.description) if description_node: descriptions[cls_attr.field] = description_node @@ -251,9 +257,15 @@ def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): cls_attr.name or convert_python_name_to_graphql(attr_name) ) if cls_attr.resolver: - fields_data.fields_resolvers[attr_name] = cls_attr.resolver + resolver = cls_attr.resolver + if isinstance(resolver, staticmethod): + resolver = resolver.__func__ # type: ignore[attr-defined] + fields_data.fields_resolvers[attr_name] = resolver elif isinstance(cls_attr, GraphQLObjectResolver): - fields_data.fields_resolvers[cls_attr.field] = cls_attr.resolver + resolver = cls_attr.resolver + if isinstance(resolver, staticmethod): + resolver = resolver.__func__ # type: ignore[attr-defined] + fields_data.fields_resolvers[cls_attr.field] = resolver elif isinstance(cls_attr, GraphQLObjectSource): if ( cls_attr.field_type @@ -267,8 +279,11 @@ def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): fields_data.fields_descriptions[cls_attr.field] = ( cls_attr.description ) - fields_data.fields_subscribers[cls_attr.field] = cls_attr.subscriber - field_args = get_field_args_from_subscriber(cls_attr.subscriber) + subscriber = cls_attr.subscriber + if isinstance(subscriber, staticmethod): + subscriber = subscriber.__func__ # type: ignore[attr-defined] + fields_data.fields_subscribers[cls_attr.field] = subscriber + field_args = get_field_args_from_subscriber(subscriber) if field_args: fields_data.fields_args[cls_attr.field] = update_field_args_options( field_args, cls_attr.args diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index 07b54d4..cc1d3d7 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -1,4 +1,4 @@ -from typing import AsyncIterator, List +from typing import List from ariadne import gql from graphql import subscribe, parse @@ -73,10 +73,11 @@ def search_sth(*_) -> str: query = parse("subscription { messageAdded {id content author} }") sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -146,10 +147,11 @@ def search_sth(*_) -> str: query = parse("subscription { messageAdded {id content author} }") sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -176,7 +178,7 @@ async def message_added_generator(*_, channel: GraphQLID): "author": "Anon", } - @GraphQLSubscription.field() + @GraphQLSubscription.field @staticmethod def message_added( message, *_, channel: GraphQLID @@ -216,10 +218,11 @@ def search_sth(*_) -> str: query = parse('subscription { messageAdded(channel: "123") {id content author} }') sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -315,10 +318,11 @@ def search_sth(*_) -> str: query = parse("subscription { userJoined {id username} }") sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -390,10 +394,11 @@ def search_sth(*_) -> str: ) sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -464,10 +469,11 @@ def search_sth(*_) -> str: query = parse("subscription { notificationReceived { ... on Message { id } } }") sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -526,10 +532,11 @@ def search_sth(*_) -> str: query = parse("subscription { messageAdded {id content author} }") sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -601,10 +608,11 @@ def search_sth(*_) -> str: query = parse('subscription { messageAdded(channel: "123") {id content author} }') sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -698,10 +706,11 @@ def search_sth(*_) -> str: query = parse("subscription { userJoined {id username} }") sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -776,10 +785,11 @@ def search_sth(*_) -> str: ) sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -871,10 +881,11 @@ def search_sth(*_) -> str: ) sub = await subscribe(schema, query) - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + # Ensure the subscription is an async iterator + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors @@ -942,10 +953,10 @@ def search_sth(*_) -> str: sub = await subscribe(schema, query) # Ensure the subscription is an async iterator - assert isinstance(sub, AsyncIterator), "Subscription should be an async iterator" + assert hasattr(sub, "__aiter__") - # Fetch the first result using anext - result = await anext(sub) + # Fetch the first result + result = await sub.__anext__() # type: ignore # Validate the result assert not result.errors diff --git a/tests/test_typing.py b/tests/test_typing.py index 5133c07..4faa3fc 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,3 +1,4 @@ +# pylint: disable=no-member, unsupported-binary-operation from enum import Enum import sys from typing import TYPE_CHECKING, Annotated, List, Optional, Union From 0ed1284319b5dac659d076ab9f94c00fb1c99eac Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 2 Sep 2024 10:22:41 +0200 Subject: [PATCH 56/63] disable pylint error --- tests/test_subscription_type.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index cc1d3d7..a9ad793 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -1,3 +1,4 @@ +# pylint: disable=unnecessary-dunder-call from typing import List from ariadne import gql From 907b38c963156e552a1459a1d4e3e70fb74bf7ba Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 5 Sep 2024 12:40:13 +0200 Subject: [PATCH 57/63] introduce ruff --- .github/workflows/tests.yml | 30 +- ariadne_graphql_modules/__init__.py | 42 ++- ariadne_graphql_modules/base.py | 27 +- .../base_object_type/__init__.py | 10 +- .../base_object_type/graphql_field.py | 51 +-- .../base_object_type/graphql_type.py | 74 ++-- .../base_object_type/utils.py | 47 +-- .../base_object_type/validators.py | 122 +++---- .../compatibility_layer.py | 58 ++- ariadne_graphql_modules/enum_type/__init__.py | 8 +- .../enum_type/graphql_type.py | 52 +-- ariadne_graphql_modules/enum_type/models.py | 7 +- ariadne_graphql_modules/enum_type/type.py | 343 ------------------ ariadne_graphql_modules/executable_schema.py | 69 ++-- .../input_type/__init__.py | 5 +- .../input_type/graphql_type.py | 43 +-- ariadne_graphql_modules/input_type/models.py | 6 +- .../input_type/validators.py | 39 +- .../interface_type/__init__.py | 5 +- .../interface_type/graphql_type.py | 31 +- .../interface_type/models.py | 10 +- .../object_type/__init__.py | 14 +- .../object_type/graphql_type.py | 32 +- ariadne_graphql_modules/object_type/models.py | 10 +- ariadne_graphql_modules/roots.py | 14 +- .../scalar_type/__init__.py | 5 +- .../scalar_type/graphql_type.py | 20 +- ariadne_graphql_modules/scalar_type/models.py | 5 +- .../scalar_type/validators.py | 11 +- ariadne_graphql_modules/sort.py | 38 +- .../subscription_type/__init__.py | 5 +- .../subscription_type/graphql_type.py | 78 ++-- .../subscription_type/models.py | 12 +- ariadne_graphql_modules/types.py | 9 +- ariadne_graphql_modules/typing.py | 20 +- .../union_type/__init__.py | 5 +- .../union_type/graphql_type.py | 27 +- ariadne_graphql_modules/union_type/models.py | 2 +- .../union_type/validators.py | 19 +- ariadne_graphql_modules/utils.py | 3 +- ariadne_graphql_modules/validators.py | 8 +- ariadne_graphql_modules/value.py | 3 +- pyproject.toml | 57 +-- tests/conftest.py | 2 +- ...st_invalid_resolver_arg_option_default.yml | 3 + tests/test_compatibility_layer.py | 11 +- tests/test_description_node.py | 1 + tests/test_enum_type_validation.py | 2 +- tests/test_input_type.py | 2 +- tests/test_input_type_validation.py | 2 +- tests/test_interface_type.py | 26 +- tests/test_interface_type_validation.py | 7 +- tests/test_make_executable_schema.py | 8 +- tests/test_object_type.py | 2 +- tests/test_object_type_validation.py | 4 +- tests/test_scalar_type.py | 2 +- tests/test_scalar_type_validation.py | 2 +- tests/test_subscription_type.py | 48 ++- tests/test_subscription_type_validation.py | 3 +- tests/test_typing.py | 30 +- tests/test_union_type.py | 22 +- tests/test_union_type_validation.py | 1 + 62 files changed, 669 insertions(+), 985 deletions(-) delete mode 100644 ariadne_graphql_modules/enum_type/type.py create mode 100644 tests/snapshots/test_invalid_resolver_arg_option_default.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67bb60f..c45e236 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,20 +15,28 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@master - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 + with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[test] - - name: Pytest - run: | - pytest - - name: Linters - run: | - pylint ariadne_graphql_modules tests - mypy ariadne_graphql_modules --ignore-missing-imports - black --check . + pip install -e .[test, lint] + + - name: Run Ruff + run: hatch run ruff . + + - name: Run Black + run: hatch run black --check . + + - name: Run MyPy + run: hatch run mypy . + + - name: Run Pylint + run: hatch run pylint ariadne_graphql_modules + + - name: Run Pytest + run: hatch run pytest --cov=ariadne_graphql_modules diff --git a/ariadne_graphql_modules/__init__.py b/ariadne_graphql_modules/__init__.py index c80762d..906aa5a 100644 --- a/ariadne_graphql_modules/__init__.py +++ b/ariadne_graphql_modules/__init__.py @@ -1,27 +1,37 @@ -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .convert_name import ( +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.convert_name import ( convert_graphql_name_to_python, convert_python_name_to_graphql, ) -from .deferredtype import deferred -from .description import get_description_node -from .enum_type import ( +from ariadne_graphql_modules.deferredtype import deferred +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.enum_type import ( GraphQLEnum, GraphQLEnumModel, create_graphql_enum_model, graphql_enum, ) -from .executable_schema import make_executable_schema -from .idtype import GraphQLID -from .input_type import GraphQLInput, GraphQLInputModel -from .object_type import GraphQLObject, GraphQLObjectModel, object_field -from .roots import ROOTS_NAMES, merge_root_nodes -from .scalar_type import GraphQLScalar, GraphQLScalarModel -from .sort import sort_schema_document -from .union_type import GraphQLUnion, GraphQLUnionModel -from .value import get_value_from_node, get_value_node -from .interface_type import GraphQLInterface, GraphQLInterfaceModel -from .subscription_type import GraphQLSubscription, GraphQLSubscriptionModel +from ariadne_graphql_modules.executable_schema import make_executable_schema +from ariadne_graphql_modules.idtype import GraphQLID +from ariadne_graphql_modules.input_type import GraphQLInput, GraphQLInputModel +from ariadne_graphql_modules.interface_type import ( + GraphQLInterface, + GraphQLInterfaceModel, +) +from ariadne_graphql_modules.object_type import ( + GraphQLObject, + GraphQLObjectModel, + object_field, +) +from ariadne_graphql_modules.roots import ROOTS_NAMES, merge_root_nodes +from ariadne_graphql_modules.scalar_type import GraphQLScalar, GraphQLScalarModel +from ariadne_graphql_modules.sort import sort_schema_document +from ariadne_graphql_modules.subscription_type import ( + GraphQLSubscription, + GraphQLSubscriptionModel, +) +from ariadne_graphql_modules.union_type import GraphQLUnion, GraphQLUnionModel +from ariadne_graphql_modules.value import get_value_from_node, get_value_node __all__ = [ "GraphQLEnum", diff --git a/ariadne_graphql_modules/base.py b/ariadne_graphql_modules/base.py index aa57f2a..8c1670b 100644 --- a/ariadne_graphql_modules/base.py +++ b/ariadne_graphql_modules/base.py @@ -1,6 +1,7 @@ +from collections.abc import Iterable from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, Iterable, Optional, Type, Union +from typing import Any, Optional, Union from graphql import GraphQLSchema, TypeDefinitionNode @@ -42,7 +43,7 @@ def __get_graphql_model__(cls, metadata: "GraphQLMetadata") -> "GraphQLModel": @classmethod def __get_graphql_types__( cls, _: "GraphQLMetadata" - ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: + ) -> Iterable[Union[type["GraphQLType"], type[Enum]]]: """Returns iterable with GraphQL types associated with this type""" return [cls] @@ -51,7 +52,7 @@ def __get_graphql_types__( class GraphQLModel: name: str ast: TypeDefinitionNode - ast_type: Type[TypeDefinitionNode] + ast_type: type[TypeDefinitionNode] def bind_to_schema(self, schema: GraphQLSchema): pass @@ -59,34 +60,32 @@ def bind_to_schema(self, schema: GraphQLSchema): @dataclass(frozen=True) class GraphQLMetadata: - data: Dict[Union[Type[GraphQLType], Type[Enum]], Any] = field(default_factory=dict) - names: Dict[Union[Type[GraphQLType], Type[Enum]], str] = field(default_factory=dict) - models: Dict[Union[Type[GraphQLType], Type[Enum]], GraphQLModel] = field( + data: dict[Union[type[GraphQLType], type[Enum]], Any] = field(default_factory=dict) + names: dict[Union[type[GraphQLType], type[Enum]], str] = field(default_factory=dict) + models: dict[Union[type[GraphQLType], type[Enum]], GraphQLModel] = field( default_factory=dict ) - def get_data(self, graphql_type: Union[Type[GraphQLType], Type[Enum]]) -> Any: + def get_data(self, graphql_type: Union[type[GraphQLType], type[Enum]]) -> Any: try: return self.data[graphql_type] except KeyError as e: raise KeyError(f"No data is set for '{graphql_type}'.") from e def set_data( - self, graphql_type: Union[Type[GraphQLType], Type[Enum]], data: Any + self, graphql_type: Union[type[GraphQLType], type[Enum]], data: Any ) -> Any: self.data[graphql_type] = data return data def get_graphql_model( - self, graphql_type: Union[Type[GraphQLType], Type[Enum]] + self, graphql_type: Union[type[GraphQLType], type[Enum]] ) -> GraphQLModel: if graphql_type not in self.models: if hasattr(graphql_type, "__get_graphql_model__"): self.models[graphql_type] = graphql_type.__get_graphql_model__(self) elif issubclass(graphql_type, Enum): - from .enum_type import ( # pylint: disable=R0401,C0415 - create_graphql_enum_model, - ) + from ariadne_graphql_modules.enum_type import create_graphql_enum_model self.models[graphql_type] = create_graphql_enum_model(graphql_type) else: @@ -95,12 +94,12 @@ def get_graphql_model( return self.models[graphql_type] def set_graphql_name( - self, graphql_type: Union[Type[GraphQLType], Type[Enum]], name: str + self, graphql_type: Union[type[GraphQLType], type[Enum]], name: str ): self.names[graphql_type] = name def get_graphql_name( - self, graphql_type: Union[Type[GraphQLType], Type[Enum]] + self, graphql_type: Union[type[GraphQLType], type[Enum]] ) -> str: if graphql_type not in self.names: model = self.get_graphql_model(graphql_type) diff --git a/ariadne_graphql_modules/base_object_type/__init__.py b/ariadne_graphql_modules/base_object_type/__init__.py index 6884aa9..8b58aea 100644 --- a/ariadne_graphql_modules/base_object_type/__init__.py +++ b/ariadne_graphql_modules/base_object_type/__init__.py @@ -1,11 +1,13 @@ -from .graphql_type import GraphQLBaseObject -from .graphql_field import GraphQLFieldData, GraphQLObjectData -from .validators import ( +from ariadne_graphql_modules.base_object_type.graphql_field import ( + GraphQLFieldData, + GraphQLObjectData, +) +from ariadne_graphql_modules.base_object_type.graphql_type import GraphQLBaseObject +from ariadne_graphql_modules.base_object_type.validators import ( validate_object_type_with_schema, validate_object_type_without_schema, ) - __all__ = [ "GraphQLBaseObject", "GraphQLObjectData", diff --git a/ariadne_graphql_modules/base_object_type/graphql_field.py b/ariadne_graphql_modules/base_object_type/graphql_field.py index 28d8b91..350ce8f 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_field.py +++ b/ariadne_graphql_modules/base_object_type/graphql_field.py @@ -1,5 +1,6 @@ from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional +from typing import Any, Optional + from ariadne.types import Resolver, Subscriber from graphql import FieldDefinitionNode, NamedTypeNode @@ -15,17 +16,17 @@ class GraphQLObjectFieldArg: @dataclass(frozen=True) class GraphQLObjectData: - fields: Dict[str, "GraphQLObjectField"] - interfaces: List[NamedTypeNode] + fields: dict[str, "GraphQLObjectField"] + interfaces: list[NamedTypeNode] @dataclass class GraphQLClassData: - type_aliases: Dict[str, str] = field(default_factory=dict) - fields_ast: Dict[str, FieldDefinitionNode] = field(default_factory=dict) - resolvers: Dict[str, "Resolver"] = field(default_factory=dict) - aliases: Dict[str, str] = field(default_factory=dict) - out_names: Dict[str, Dict[str, str]] = field(default_factory=dict) + type_aliases: dict[str, str] = field(default_factory=dict) + fields_ast: dict[str, FieldDefinitionNode] = field(default_factory=dict) + resolvers: dict[str, "Resolver"] = field(default_factory=dict) + aliases: dict[str, str] = field(default_factory=dict) + out_names: dict[str, dict[str, str]] = field(default_factory=dict) @dataclass(frozen=True) @@ -33,7 +34,7 @@ class GraphQLObjectResolver: resolver: Resolver field: str description: Optional[str] = None - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None + args: Optional[dict[str, GraphQLObjectFieldArg]] = None field_type: Optional[Any] = None @@ -42,25 +43,25 @@ class GraphQLObjectSource: subscriber: Subscriber field: str description: Optional[str] = None - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None + args: Optional[dict[str, GraphQLObjectFieldArg]] = None field_type: Optional[Any] = None @dataclass class GraphQLFieldData: - fields_types: Dict[str, str] = field(default_factory=dict) - fields_names: Dict[str, str] = field(default_factory=dict) - fields_descriptions: Dict[str, str] = field(default_factory=dict) - fields_args: Dict[str, Dict[str, GraphQLObjectFieldArg]] = field( + fields_types: dict[str, str] = field(default_factory=dict) + fields_names: dict[str, str] = field(default_factory=dict) + fields_descriptions: dict[str, str] = field(default_factory=dict) + fields_args: dict[str, dict[str, GraphQLObjectFieldArg]] = field( default_factory=dict ) - fields_resolvers: Dict[str, Resolver] = field(default_factory=dict) - fields_subscribers: Dict[str, Subscriber] = field(default_factory=dict) - fields_defaults: Dict[str, Any] = field(default_factory=dict) - fields_order: List[str] = field(default_factory=list) - type_hints: Dict[str, Any] = field(default_factory=dict) - aliases: Dict[str, str] = field(default_factory=dict) - aliases_targets: List[str] = field(default_factory=list) + fields_resolvers: dict[str, Resolver] = field(default_factory=dict) + fields_subscribers: dict[str, Subscriber] = field(default_factory=dict) + fields_defaults: dict[str, Any] = field(default_factory=dict) + fields_order: list[str] = field(default_factory=list) + type_hints: dict[str, Any] = field(default_factory=dict) + aliases: dict[str, str] = field(default_factory=dict) + aliases_targets: list[str] = field(default_factory=list) class GraphQLObjectField: @@ -70,7 +71,7 @@ def __init__( name: Optional[str] = None, description: Optional[str] = None, field_type: Optional[Any] = None, - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + args: Optional[dict[str, GraphQLObjectFieldArg]] = None, resolver: Optional[Resolver] = None, subscriber: Optional[Subscriber] = None, default_value: Optional[Any] = None, @@ -94,7 +95,7 @@ def __call__(self, resolver: Resolver): def object_field( resolver: Optional[Resolver] = None, *, - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + args: Optional[dict[str, GraphQLObjectFieldArg]] = None, name: Optional[str] = None, description: Optional[str] = None, graphql_type: Optional[Any] = None, @@ -130,7 +131,7 @@ def get_field_type_from_subscriber(subscriber: Subscriber) -> Any: def object_resolver( field: str, graphql_type: Optional[Any] = None, - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + args: Optional[dict[str, GraphQLObjectFieldArg]] = None, description: Optional[str] = None, ): def object_resolver_factory(f: Resolver) -> GraphQLObjectResolver: @@ -150,7 +151,7 @@ def object_resolver_factory(f: Resolver) -> GraphQLObjectResolver: def object_subscriber( field: str, graphql_type: Optional[Any] = None, - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + args: Optional[dict[str, GraphQLObjectFieldArg]] = None, description: Optional[str] = None, ): def object_subscriber_factory(f: Subscriber) -> GraphQLObjectSource: diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index f003de7..4a78118 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -1,13 +1,9 @@ +from collections.abc import Iterable from copy import deepcopy from enum import Enum from typing import ( Any, - Dict, - Iterable, - List, Optional, - Tuple, - Type, Union, ) @@ -18,9 +14,8 @@ StringValueNode, ) -from ..types import GraphQLClassType - -from .graphql_field import ( +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.base_object_type.graphql_field import ( GraphQLClassData, GraphQLFieldData, GraphQLObjectData, @@ -31,31 +26,30 @@ object_field, object_resolver, ) -from .utils import ( +from ariadne_graphql_modules.base_object_type.utils import ( get_field_args_from_resolver, get_field_args_from_subscriber, get_field_args_out_names, get_field_node_from_obj_field, update_field_args_options, ) - -from ..base import GraphQLMetadata, GraphQLModel, GraphQLType -from ..convert_name import convert_python_name_to_graphql -from ..description import get_description_node -from ..typing import get_graphql_type -from ..value import get_value_node +from ariadne_graphql_modules.convert_name import convert_python_name_to_graphql +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.types import GraphQLClassType +from ariadne_graphql_modules.typing import get_graphql_type +from ariadne_graphql_modules.value import get_value_node class GraphQLBaseObject(GraphQLType): - __kwargs__: Dict[str, Any] + __kwargs__: dict[str, Any] __abstract__: bool = True __schema__: Optional[str] = None - __aliases__: Optional[Dict[str, str]] - __requires__: Optional[Iterable[Union[Type[GraphQLType], Type[Enum]]]] + __aliases__: Optional[dict[str, str]] + __requires__: Optional[Iterable[Union[type[GraphQLType], type[Enum]]]] __graphql_type__ = GraphQLClassType.BASE def __init__(self, **kwargs: Any): - default_values: Dict[str, Any] = {} + default_values: dict[str, Any] = {} for inherited_obj in self._collect_inherited_objects(): if hasattr(inherited_obj, "__kwargs__"): @@ -97,12 +91,12 @@ def __get_graphql_model_without_schema__( @classmethod def _create_fields_and_resolvers_with_schema( - cls, definition_fields: Tuple["FieldDefinitionNode", ...] - ) -> Tuple[tuple[FieldDefinitionNode, ...], Dict[str, Resolver]]: - descriptions: Dict[str, StringValueNode] = {} - args_descriptions: Dict[str, Dict[str, StringValueNode]] = {} - args_defaults: Dict[str, Dict[str, Any]] = {} - resolvers: Dict[str, Resolver] = {} + cls, definition_fields: tuple["FieldDefinitionNode", ...] + ) -> tuple[tuple[FieldDefinitionNode, ...], dict[str, Resolver]]: + descriptions: dict[str, StringValueNode] = {} + args_descriptions: dict[str, dict[str, StringValueNode]] = {} + args_defaults: dict[str, dict[str, Any]] = {} + resolvers: dict[str, Resolver] = {} for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -121,21 +115,21 @@ def _create_fields_and_resolvers_with_schema( for arg_name, arg_options in final_args.items(): arg_description = get_description_node(arg_options.description) if arg_description: - args_descriptions[cls_attr.field][ - arg_name - ] = arg_description + args_descriptions[cls_attr.field][arg_name] = ( + arg_description + ) if arg_options.default_value is not None: args_defaults[cls_attr.field][arg_name] = get_value_node( arg_options.default_value ) - fields: List[FieldDefinitionNode] = [] + fields: list[FieldDefinitionNode] = [] for field in definition_fields: field_args_descriptions = args_descriptions.get(field.name.value, {}) field_args_defaults = args_defaults.get(field.name.value, {}) - args: List[InputValueDefinitionNode] = [] + args: list[InputValueDefinitionNode] = [] for arg in field.arguments: arg_name = arg.name.value args.append( @@ -195,7 +189,7 @@ def _process_graphql_fields( @classmethod def __get_graphql_types__( cls, metadata: "GraphQLMetadata" - ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: + ) -> Iterable[Union[type["GraphQLType"], type[Enum]]]: """Returns iterable with GraphQL types associated with this type""" if getattr(cls, "__schema__", None): return cls.__get_graphql_types_with_schema__(metadata) @@ -205,16 +199,16 @@ def __get_graphql_types__( @classmethod def __get_graphql_types_with_schema__( cls, _: "GraphQLMetadata" - ) -> Iterable[Type["GraphQLType"]]: - types: List[Type["GraphQLType"]] = [cls] + ) -> Iterable[type["GraphQLType"]]: + types: list[type[GraphQLType]] = [cls] types.extend(getattr(cls, "__requires__", [])) return types @classmethod def __get_graphql_types_without_schema__( cls, metadata: "GraphQLMetadata" - ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: - types: List[Union[Type["GraphQLType"], Type[Enum]]] = [cls] + ) -> Iterable[Union[type["GraphQLType"], type[Enum]]]: + types: list[Union[type[GraphQLType], type[Enum]]] = [cls] type_data = cls.get_graphql_object_data(metadata) for field in type_data.fields.values(): @@ -236,7 +230,7 @@ def field( *, name: Optional[str] = None, graphql_type: Optional[Any] = None, - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + args: Optional[dict[str, GraphQLObjectFieldArg]] = None, description: Optional[str] = None, default_value: Optional[Any] = None, ) -> Any: @@ -254,7 +248,7 @@ def field( def resolver( field: str, graphql_type: Optional[Any] = None, - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + args: Optional[dict[str, GraphQLObjectFieldArg]] = None, description: Optional[str] = None, ): """Shortcut for object_resolver()""" @@ -303,7 +297,7 @@ def create_graphql_object_data_without_schema(cls) -> GraphQLObjectData: raise NotImplementedError() @staticmethod - def _build_fields(fields_data: GraphQLFieldData) -> Dict[str, "GraphQLObjectField"]: + def _build_fields(fields_data: GraphQLFieldData) -> dict[str, "GraphQLObjectField"]: fields = {} for field_name in fields_data.fields_order: fields[field_name] = GraphQLObjectField( @@ -339,7 +333,9 @@ def _process_type_hints_and_aliases(cls, fields_data: GraphQLFieldData): fields_data.fields_types[attr_name] = attr_type @staticmethod - def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): + def _process_class_attributes( # noqa: C901 + target_cls, fields_data: GraphQLFieldData + ): for attr_name in dir(target_cls): if attr_name.startswith("__"): continue diff --git a/ariadne_graphql_modules/base_object_type/utils.py b/ariadne_graphql_modules/base_object_type/utils.py index 1ad2fcb..79be529 100644 --- a/ariadne_graphql_modules/base_object_type/utils.py +++ b/ariadne_graphql_modules/base_object_type/utils.py @@ -1,23 +1,26 @@ from dataclasses import replace from inspect import signature -from typing import TYPE_CHECKING, Dict, Optional, Tuple, Type +from typing import TYPE_CHECKING, Optional from ariadne.types import Resolver, Subscriber from graphql import FieldDefinitionNode, InputValueDefinitionNode, NameNode -from ..base import GraphQLMetadata -from ..convert_name import convert_python_name_to_graphql -from ..description import get_description_node -from ..typing import get_type_node -from ..value import get_value_node -from .graphql_field import GraphQLObjectField, GraphQLObjectFieldArg +from ariadne_graphql_modules.base import GraphQLMetadata +from ariadne_graphql_modules.base_object_type.graphql_field import ( + GraphQLObjectField, + GraphQLObjectFieldArg, +) +from ariadne_graphql_modules.convert_name import convert_python_name_to_graphql +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.typing import get_type_node +from ariadne_graphql_modules.value import get_value_node if TYPE_CHECKING: - from .graphql_type import GraphQLBaseObject + from ariadne_graphql_modules.base_object_type.graphql_type import GraphQLBaseObject def get_field_node_from_obj_field( - parent_type: Type["GraphQLBaseObject"], + parent_type: type["GraphQLBaseObject"], metadata: GraphQLMetadata, field: GraphQLObjectField, ) -> FieldDefinitionNode: @@ -31,14 +34,14 @@ def get_field_node_from_obj_field( def get_field_args_from_resolver( resolver: Resolver, -) -> Dict[str, GraphQLObjectFieldArg]: +) -> dict[str, GraphQLObjectFieldArg]: if isinstance(resolver, staticmethod): resolver = resolver.__func__ resolver_signature = signature(resolver) type_hints = resolver.__annotations__ type_hints.pop("return", None) - field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args: dict[str, GraphQLObjectFieldArg] = {} field_args_start = 0 # Fist pass: (arg, *_, something, something) or (arg, *, something, something): @@ -78,12 +81,12 @@ def get_field_args_from_resolver( def get_field_args_from_subscriber( subscriber: Subscriber, -) -> Dict[str, GraphQLObjectFieldArg]: +) -> dict[str, GraphQLObjectFieldArg]: subscriber_signature = signature(subscriber) type_hints = subscriber.__annotations__ type_hints.pop("return", None) - field_args: Dict[str, GraphQLObjectFieldArg] = {} + field_args: dict[str, GraphQLObjectFieldArg] = {} field_args_start = 0 # Fist pass: (arg, *_, something, something) or (arg, *, something, something): @@ -122,9 +125,9 @@ def get_field_args_from_subscriber( def get_field_args_out_names( - field_args: Dict[str, GraphQLObjectFieldArg], -) -> Dict[str, str]: - out_names: Dict[str, str] = {} + field_args: dict[str, GraphQLObjectFieldArg], +) -> dict[str, str]: + out_names: dict[str, str] = {} for field_arg in field_args.values(): if field_arg.name and field_arg.out_name: out_names[field_arg.name] = field_arg.out_name @@ -150,8 +153,8 @@ def get_field_arg_node_from_obj_field_arg( def get_field_args_nodes_from_obj_field_args( metadata: GraphQLMetadata, - field_args: Optional[Dict[str, GraphQLObjectFieldArg]], -) -> Optional[Tuple[InputValueDefinitionNode, ...]]: + field_args: Optional[dict[str, GraphQLObjectFieldArg]], +) -> Optional[tuple[InputValueDefinitionNode, ...]]: if not field_args: return None @@ -162,13 +165,13 @@ def get_field_args_nodes_from_obj_field_args( def update_field_args_options( - field_args: Dict[str, GraphQLObjectFieldArg], - args_options: Optional[Dict[str, GraphQLObjectFieldArg]], -) -> Dict[str, GraphQLObjectFieldArg]: + field_args: dict[str, GraphQLObjectFieldArg], + args_options: Optional[dict[str, GraphQLObjectFieldArg]], +) -> dict[str, GraphQLObjectFieldArg]: if not args_options: return field_args - updated_args: Dict[str, GraphQLObjectFieldArg] = {} + updated_args: dict[str, GraphQLObjectFieldArg] = {} for arg_name in field_args: arg_options = args_options.get(arg_name) if not arg_options: diff --git a/ariadne_graphql_modules/base_object_type/validators.py b/ariadne_graphql_modules/base_object_type/validators.py index f104655..a5a5de1 100644 --- a/ariadne_graphql_modules/base_object_type/validators.py +++ b/ariadne_graphql_modules/base_object_type/validators.py @@ -1,34 +1,34 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union, cast +from typing import TYPE_CHECKING, Any, Optional, Union, cast from graphql import FieldDefinitionNode, ObjectTypeDefinitionNode, TypeDefinitionNode -from ..convert_name import convert_python_name_to_graphql -from .graphql_field import ( +from ariadne_graphql_modules.base_object_type.graphql_field import ( GraphQLObjectField, GraphQLObjectFieldArg, GraphQLObjectResolver, GraphQLObjectSource, ) -from .utils import ( +from ariadne_graphql_modules.base_object_type.utils import ( get_field_args_from_resolver, get_field_args_from_subscriber, ) -from ..validators import validate_description, validate_name -from ..value import get_value_node -from ..utils import parse_definition +from ariadne_graphql_modules.convert_name import convert_python_name_to_graphql +from ariadne_graphql_modules.utils import parse_definition +from ariadne_graphql_modules.validators import validate_description, validate_name +from ariadne_graphql_modules.value import get_value_node if TYPE_CHECKING: - from .graphql_type import GraphQLBaseObject + from ariadne_graphql_modules.base_object_type.graphql_type import GraphQLBaseObject @dataclass class GraphQLObjectValidationData: - aliases: Dict[str, str] - fields_attrs: List[str] - fields_instances: Dict[str, GraphQLObjectField] - resolvers_instances: Dict[str, GraphQLObjectResolver] - sources_instances: Dict[str, GraphQLObjectSource] + aliases: dict[str, str] + fields_attrs: list[str] + fields_instances: dict[str, GraphQLObjectField] + resolvers_instances: dict[str, GraphQLObjectResolver] + sources_instances: dict[str, GraphQLObjectSource] def get_all_annotations(cls): @@ -38,10 +38,10 @@ def get_all_annotations(cls): return annotations -def validate_object_type_with_schema( - cls: Type["GraphQLBaseObject"], - valid_type: Type[TypeDefinitionNode] = ObjectTypeDefinitionNode, -) -> Dict[str, Any]: +def validate_object_type_with_schema( # noqa: C901 + cls: type["GraphQLBaseObject"], + valid_type: type[TypeDefinitionNode] = ObjectTypeDefinitionNode, +) -> dict[str, Any]: definition = cast( ObjectTypeDefinitionNode, parse_definition(valid_type, cls.__schema__) ) @@ -63,13 +63,13 @@ def validate_object_type_with_schema( "with declaration for an object type without any fields. " ) - field_names: List[str] = [f.name.value for f in definition.fields] - field_definitions: Dict[str, FieldDefinitionNode] = { + field_names: list[str] = [f.name.value for f in definition.fields] + field_definitions: dict[str, FieldDefinitionNode] = { f.name.value: f for f in definition.fields } - fields_resolvers: List[str] = [] - source_fields: List[str] = [] + fields_resolvers: list[str] = [] + source_fields: list[str] = [] valid_fields: str = "" for attr_name in dir(cls): @@ -213,15 +213,15 @@ def validate_object_type_with_schema( cls, cls_attr.field, arg_name, arg_obj.default_value ) - aliases: Dict[str, str] = getattr(cls, "__aliases__", None) or {} + aliases: dict[str, str] = getattr(cls, "__aliases__", None) or {} validate_object_aliases(cls, aliases, field_names, fields_resolvers) return get_object_type_with_schema_kwargs(cls, aliases, field_names) def validate_object_type_without_schema( - cls: Type["GraphQLBaseObject"], -) -> Dict[str, Any]: + cls: type["GraphQLBaseObject"], +) -> dict[str, Any]: data = get_object_type_validation_data(cls) # Alias target is not present in schema as a field if its not an @@ -242,7 +242,7 @@ def validate_object_type_without_schema( validate_object_fields_args(cls) # Gather names of field attrs with defined resolver - fields_resolvers: List[str] = [] + fields_resolvers: list[str] = [] for attr_name, field_instance in data.fields_instances.items(): if field_instance.resolver: fields_resolvers.append(attr_name) @@ -255,11 +255,11 @@ def validate_object_type_without_schema( def validate_object_unique_graphql_names( - cls: Type["GraphQLBaseObject"], - fields_attrs: List[str], - fields_instances: Dict[str, GraphQLObjectField], + cls: type["GraphQLBaseObject"], + fields_attrs: list[str], + fields_instances: dict[str, GraphQLObjectField], ): - graphql_names: List[str] = [] + graphql_names: list[str] = [] for attr_name in fields_attrs: if attr_name in fields_instances and fields_instances[attr_name].name: attr_graphql_name = fields_instances[attr_name].name @@ -281,12 +281,12 @@ def validate_object_unique_graphql_names( def validate_object_resolvers( - cls: Type["GraphQLBaseObject"], - fields_names: List[str], - fields_instances: Dict[str, GraphQLObjectField], - resolvers_instances: Dict[str, GraphQLObjectResolver], + cls: type["GraphQLBaseObject"], + fields_names: list[str], + fields_instances: dict[str, GraphQLObjectField], + resolvers_instances: dict[str, GraphQLObjectResolver], ): - resolvers_fields: List[str] = [] + resolvers_fields: list[str] = [] for field_attr, field in fields_instances.items(): if field.resolver: @@ -326,11 +326,11 @@ def validate_object_resolvers( def validate_object_subscribers( - cls: Type["GraphQLBaseObject"], - fields_names: List[str], - sources_instances: Dict[str, GraphQLObjectSource], + cls: type["GraphQLBaseObject"], + fields_names: list[str], + sources_instances: dict[str, GraphQLObjectSource], ): - source_fields: List[str] = [] + source_fields: list[str] = [] for key, source in sources_instances.items(): if not isinstance(source.field, str): @@ -365,7 +365,7 @@ def validate_object_subscribers( ) -def validate_object_fields_args(cls: Type["GraphQLBaseObject"]): +def validate_object_fields_args(cls: type["GraphQLBaseObject"]): for field_name in dir(cls): field_instance = getattr(cls, field_name) if ( @@ -376,7 +376,7 @@ def validate_object_fields_args(cls: Type["GraphQLBaseObject"]): def validate_object_field_args( - cls: Type["GraphQLBaseObject"], + cls: type["GraphQLBaseObject"], field_name: str, field_instance: Union["GraphQLObjectField", "GraphQLObjectResolver"], ): @@ -393,7 +393,7 @@ def validate_object_field_args( resolver_args_names = list(resolver_args.keys()) if resolver_args_names: - error_help = "expected one of: '%s'" % ("', '".join(resolver_args_names)) + error_help = "expected one of: '{}'".format("', '".join(resolver_args_names)) else: error_help = "function accepts no extra arguments" @@ -420,10 +420,10 @@ def validate_object_field_args( def validate_object_aliases( - cls: Type["GraphQLBaseObject"], - aliases: Dict[str, str], - fields_names: List[str], - fields_resolvers: List[str], + cls: type["GraphQLBaseObject"], + aliases: dict[str, str], + fields_names: list[str], + fields_resolvers: list[str], ): for alias in aliases: if alias not in fields_names: @@ -441,7 +441,7 @@ def validate_object_aliases( def validate_field_arg_default_value( - cls: Type["GraphQLBaseObject"], field_name: str, arg_name: str, default_value: Any + cls: type["GraphQLBaseObject"], field_name: str, arg_name: str, default_value: Any ): if default_value is None: return @@ -457,18 +457,18 @@ def validate_field_arg_default_value( ) from e -def get_object_type_validation_data( - cls: Type["GraphQLBaseObject"], +def get_object_type_validation_data( # noqa: C901 + cls: type["GraphQLBaseObject"], ) -> GraphQLObjectValidationData: - fields_attrs: List[str] = [ + fields_attrs: list[str] = [ attr_name for attr_name in get_all_annotations(cls) if not attr_name.startswith("__") ] - fields_instances: Dict[str, GraphQLObjectField] = {} - resolvers_instances: Dict[str, GraphQLObjectResolver] = {} - sources_instances: Dict[str, GraphQLObjectSource] = {} + fields_instances: dict[str, GraphQLObjectField] = {} + resolvers_instances: dict[str, GraphQLObjectResolver] = {} + sources_instances: dict[str, GraphQLObjectSource] = {} for attr_name in dir(cls): if attr_name.startswith("__"): @@ -505,10 +505,10 @@ def get_object_type_validation_data( def get_object_type_kwargs( - cls: Type["GraphQLBaseObject"], - aliases: Dict[str, str], -) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} + cls: type["GraphQLBaseObject"], + aliases: dict[str, str], +) -> dict[str, Any]: + kwargs: dict[str, Any] = {} for attr_name in get_all_annotations(cls): if attr_name.startswith("__"): @@ -538,11 +538,11 @@ def get_object_type_kwargs( def get_object_type_with_schema_kwargs( - cls: Type["GraphQLBaseObject"], - aliases: Dict[str, str], - field_names: List[str], -) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} + cls: type["GraphQLBaseObject"], + aliases: dict[str, str], + field_names: list[str], +) -> dict[str, Any]: + kwargs: dict[str, Any] = {} for field_name in field_names: final_name = aliases.get(field_name, field_name) diff --git a/ariadne_graphql_modules/compatibility_layer.py b/ariadne_graphql_modules/compatibility_layer.py index e82c2bd..1d3e117 100644 --- a/ariadne_graphql_modules/compatibility_layer.py +++ b/ariadne_graphql_modules/compatibility_layer.py @@ -1,6 +1,6 @@ from enum import Enum from inspect import isclass -from typing import Any, Dict, List, Type, Union, cast +from typing import Any, Union, cast from graphql import ( EnumTypeDefinitionNode, @@ -12,34 +12,32 @@ UnionTypeDefinitionNode, ) -from .v1.executable_schema import get_all_types - -from .v1.directive_type import DirectiveType -from .v1.enum_type import EnumType -from .v1.input_type import InputType -from .v1.interface_type import InterfaceType -from .v1.mutation_type import MutationType -from .v1.scalar_type import ScalarType -from .v1.subscription_type import SubscriptionType -from .v1.union_type import UnionType -from .v1.object_type import ObjectType -from .v1.bases import BaseType, BindableType - -from .base import GraphQLModel, GraphQLType -from . import ( - GraphQLObjectModel, +from ariadne_graphql_modules import ( GraphQLEnumModel, GraphQLInputModel, - GraphQLScalarModel, GraphQLInterfaceModel, + GraphQLObjectModel, + GraphQLScalarModel, GraphQLSubscriptionModel, GraphQLUnionModel, ) +from ariadne_graphql_modules.base import GraphQLModel, GraphQLType +from ariadne_graphql_modules.v1.bases import BaseType, BindableType +from ariadne_graphql_modules.v1.directive_type import DirectiveType +from ariadne_graphql_modules.v1.enum_type import EnumType +from ariadne_graphql_modules.v1.executable_schema import get_all_types +from ariadne_graphql_modules.v1.input_type import InputType +from ariadne_graphql_modules.v1.interface_type import InterfaceType +from ariadne_graphql_modules.v1.mutation_type import MutationType +from ariadne_graphql_modules.v1.object_type import ObjectType +from ariadne_graphql_modules.v1.scalar_type import ScalarType +from ariadne_graphql_modules.v1.subscription_type import SubscriptionType +from ariadne_graphql_modules.v1.union_type import UnionType def wrap_legacy_types( - *bindable_types: Type[BaseType], -) -> List[Type["LegacyGraphQLType"]]: + *bindable_types: type[BaseType], +) -> list[type["LegacyGraphQLType"]]: all_types = get_all_types(bindable_types) return [ @@ -49,7 +47,7 @@ def wrap_legacy_types( class LegacyGraphQLType(GraphQLType): - __base_type__: Type[BindableType] + __base_type__: type[BindableType] __abstract__: bool = False @classmethod @@ -76,7 +74,7 @@ def __get_graphql_model__(cls, *_) -> GraphQLModel: @classmethod def construct_object_model( - cls, base_type: Type[Union[ObjectType, MutationType]] + cls, base_type: type[Union[ObjectType, MutationType]] ) -> GraphQLObjectModel: return GraphQLObjectModel( name=base_type.graphql_name, @@ -88,9 +86,9 @@ def construct_object_model( ) @classmethod - def construct_enum_model(cls, base_type: Type[EnumType]) -> GraphQLEnumModel: + def construct_enum_model(cls, base_type: type[EnumType]) -> GraphQLEnumModel: members = base_type.__enum__ or {} - members_values: Dict[str, Any] = {} + members_values: dict[str, Any] = {} if isinstance(members, dict): members_values = dict(members.items()) @@ -105,11 +103,11 @@ def construct_enum_model(cls, base_type: Type[EnumType]) -> GraphQLEnumModel: ) @classmethod - def construct_directive_model(cls, base_type: Type[DirectiveType]): + def construct_directive_model(cls, base_type: type[DirectiveType]): """TODO: https://github.com/mirumee/ariadne-graphql-modules/issues/29""" @classmethod - def construct_input_model(cls, base_type: Type[InputType]) -> GraphQLInputModel: + def construct_input_model(cls, base_type: type[InputType]) -> GraphQLInputModel: return GraphQLInputModel( name=base_type.graphql_name, ast_type=InputObjectTypeDefinitionNode, @@ -120,7 +118,7 @@ def construct_input_model(cls, base_type: Type[InputType]) -> GraphQLInputModel: @classmethod def construct_interface_model( - cls, base_type: Type[InterfaceType] + cls, base_type: type[InterfaceType] ) -> GraphQLInterfaceModel: return GraphQLInterfaceModel( name=base_type.graphql_name, @@ -133,7 +131,7 @@ def construct_interface_model( ) @classmethod - def construct_scalar_model(cls, base_type: Type[ScalarType]) -> GraphQLScalarModel: + def construct_scalar_model(cls, base_type: type[ScalarType]) -> GraphQLScalarModel: return GraphQLScalarModel( name=base_type.graphql_name, ast_type=ScalarTypeDefinitionNode, @@ -145,7 +143,7 @@ def construct_scalar_model(cls, base_type: Type[ScalarType]) -> GraphQLScalarMod @classmethod def construct_subscription_model( - cls, base_type: Type[SubscriptionType] + cls, base_type: type[SubscriptionType] ) -> GraphQLSubscriptionModel: return GraphQLSubscriptionModel( name=base_type.graphql_name, @@ -158,7 +156,7 @@ def construct_subscription_model( ) @classmethod - def construct_union_model(cls, base_type: Type[UnionType]) -> GraphQLUnionModel: + def construct_union_model(cls, base_type: type[UnionType]) -> GraphQLUnionModel: return GraphQLUnionModel( name=base_type.graphql_name, ast_type=UnionTypeDefinitionNode, diff --git a/ariadne_graphql_modules/enum_type/__init__.py b/ariadne_graphql_modules/enum_type/__init__.py index 2782ef5..4bf34ec 100644 --- a/ariadne_graphql_modules/enum_type/__init__.py +++ b/ariadne_graphql_modules/enum_type/__init__.py @@ -1,5 +1,9 @@ -from .graphql_type import GraphQLEnum, graphql_enum, create_graphql_enum_model -from .models import GraphQLEnumModel +from ariadne_graphql_modules.enum_type.graphql_type import ( + GraphQLEnum, + create_graphql_enum_model, + graphql_enum, +) +from ariadne_graphql_modules.enum_type.models import GraphQLEnumModel __all__ = [ "GraphQLEnum", diff --git a/ariadne_graphql_modules/enum_type/graphql_type.py b/ariadne_graphql_modules/enum_type/graphql_type.py index cff5d83..3f7c764 100644 --- a/ariadne_graphql_modules/enum_type/graphql_type.py +++ b/ariadne_graphql_modules/enum_type/graphql_type.py @@ -1,21 +1,23 @@ +from collections.abc import Iterable from enum import Enum from inspect import isclass -from typing import Any, Dict, Iterable, List, Optional, Set, Type, Union, cast +from typing import Any, Optional, Union, cast from graphql import EnumTypeDefinitionNode, EnumValueDefinitionNode, NameNode -from ..base import GraphQLMetadata, GraphQLModel, GraphQLType -from ..description import get_description_node -from .models import GraphQLEnumModel -from ..validators import validate_description, validate_name -from ..utils import parse_definition + +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.enum_type.models import GraphQLEnumModel +from ariadne_graphql_modules.utils import parse_definition +from ariadne_graphql_modules.validators import validate_description, validate_name class GraphQLEnum(GraphQLType): __abstract__: bool = True __schema__: Optional[str] __description__: Optional[str] - __members__: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] - __members_descriptions__: Optional[Dict[str, str]] + __members__: Optional[Union[type[Enum], dict[str, Any], list[str]]] + __members_descriptions__: Optional[dict[str, str]] def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -43,7 +45,7 @@ def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLEnumModel": ) members = getattr(cls, "__members__", []) - members_values: Dict[str, Any] = {} + members_values: dict[str, Any] = {} if isinstance(members, dict): members_values = dict(members.items()) @@ -131,14 +133,16 @@ def _validate_enum_type_with_schema(cls): raise ValueError( f"Class '{cls.__name__}' defines '__schema__' attribute " f"with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != '{EnumTypeDefinitionNode.__name__}')" + f"('{definition.__class__.__name__}' != " + f"'{EnumTypeDefinitionNode.__name__}')" ) validate_name(cls, definition) validate_description(cls, definition) members_names = { - value.name.value for value in definition.values # pylint: disable=no-member + value.name.value + for value in definition.values # pylint: disable=no-member } if not members_names: raise ValueError( @@ -164,7 +168,8 @@ def _validate_enum_type_with_schema(cls): if duplicate_descriptions: raise ValueError( f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " - f"descriptions for enum members that also have description in '__schema__' " + "descriptions for enum members that also " + "have description in '__schema__' " f"attribute. (members: '{', '.join(duplicate_descriptions)}')" ) @@ -206,7 +211,8 @@ def _validate_enum_type(cls): ] ): raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute is of unsupported type. " + f"Class '{cls.__name__}' '__members__' " + "attribute is of unsupported type. " f"Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. " f"(found: '{type(members_values)}')" ) @@ -217,7 +223,7 @@ def _validate_enum_type(cls): @classmethod def validate_enum_members_descriptions( - cls, members: Set[str], members_descriptions: dict + cls, members: set[str], members_descriptions: dict ): invalid_descriptions = set(members_descriptions) - members if invalid_descriptions: @@ -230,8 +236,8 @@ def validate_enum_members_descriptions( @staticmethod def get_members_set( - members: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] - ) -> Set[str]: + members: Optional[Union[type[Enum], dict[str, Any], list[str]]], + ) -> set[str]: if isinstance(members, dict): return set(members.keys()) @@ -247,12 +253,12 @@ def get_members_set( ) -def create_graphql_enum_model( - enum: Type[Enum], +def create_graphql_enum_model( # noqa: C901 + enum: type[Enum], *, name: Optional[str] = None, description: Optional[str] = None, - members_descriptions: Optional[Dict[str, str]] = None, + members_descriptions: Optional[dict[str, str]] = None, members_include: Optional[Iterable[str]] = None, members_exclude: Optional[Iterable[str]] = None, ) -> "GraphQLEnumModel": @@ -270,8 +276,8 @@ def create_graphql_enum_model( else: name = enum.__name__ - members: Dict[str, Any] = {i.name: i for i in enum} - final_members: Dict[str, Any] = {} + members: dict[str, Any] = {i.name: i for i in enum} + final_members: dict[str, Any] = {} if members_include: for key, value in members.items(): @@ -317,7 +323,7 @@ def graphql_enum( *, name: Optional[str] = None, description: Optional[str] = None, - members_descriptions: Optional[Dict[str, str]] = None, + members_descriptions: Optional[dict[str, str]] = None, members_include: Optional[Iterable[str]] = None, members_exclude: Optional[Iterable[str]] = None, ): @@ -331,7 +337,7 @@ def graphql_enum_decorator(cls): members_exclude=members_exclude, ) - def __get_graphql_model__(*_) -> GraphQLEnumModel: + def __get_graphql_model__(*_) -> GraphQLEnumModel: # noqa: N807 return graphql_model setattr(cls, "__get_graphql_model__", classmethod(__get_graphql_model__)) diff --git a/ariadne_graphql_modules/enum_type/models.py b/ariadne_graphql_modules/enum_type/models.py index b455784..2b30e93 100644 --- a/ariadne_graphql_modules/enum_type/models.py +++ b/ariadne_graphql_modules/enum_type/models.py @@ -1,14 +1,15 @@ from dataclasses import dataclass -from typing import Any, Dict +from typing import Any + from ariadne import EnumType from graphql import GraphQLSchema -from ..base import GraphQLModel +from ariadne_graphql_modules.base import GraphQLModel @dataclass(frozen=True) class GraphQLEnumModel(GraphQLModel): - members: Dict[str, Any] + members: dict[str, Any] def bind_to_schema(self, schema: GraphQLSchema): bindable = EnumType(self.name, values=self.members) diff --git a/ariadne_graphql_modules/enum_type/type.py b/ariadne_graphql_modules/enum_type/type.py deleted file mode 100644 index fc33745..0000000 --- a/ariadne_graphql_modules/enum_type/type.py +++ /dev/null @@ -1,343 +0,0 @@ -from enum import Enum -from inspect import isclass -from typing import Any, Dict, Iterable, List, Optional, Set, Type, Union, cast - -from graphql import EnumTypeDefinitionNode, EnumValueDefinitionNode, NameNode -from ..base import GraphQLMetadata, GraphQLModel, GraphQLType -from ..description import get_description_node -from .models import GraphQLEnumModel -from ..validators import validate_description, validate_name -from ..utils import parse_definition - - -class GraphQLEnum(GraphQLType): - __abstract__: bool = True - __schema__: Optional[str] - __description__: Optional[str] - __members__: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] - __members_descriptions__: Optional[Dict[str, str]] - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.__dict__.get("__abstract__"): - return - - cls.__abstract__ = False - cls._validate() - - @classmethod - def __get_graphql_model__(cls, metadata: GraphQLMetadata) -> "GraphQLModel": - name = cls.__get_graphql_name__() - - if getattr(cls, "__schema__", None): - return cls.__get_graphql_model_with_schema__(name) - - return cls.__get_graphql_model_without_schema__(name) - - @classmethod - def __get_graphql_model_with_schema__(cls, name: str) -> "GraphQLEnumModel": - definition: EnumTypeDefinitionNode = cast( - EnumTypeDefinitionNode, - parse_definition(EnumTypeDefinitionNode, cls.__schema__), - ) - - members = getattr(cls, "__members__", []) - members_values: Dict[str, Any] = {} - - if isinstance(members, dict): - members_values = dict(members.items()) - elif isclass(members) and issubclass(members, Enum): - members_values = {member.name: member for member in members} - else: - members_values = { - value.name.value: value.name.value - for value in definition.values # pylint: disable=no-member - } - - members_descriptions = getattr(cls, "__members_descriptions__", {}) - - return GraphQLEnumModel( - name=name, - members=members_values, - ast_type=EnumTypeDefinitionNode, - ast=EnumTypeDefinitionNode( - name=NameNode(value=name), - directives=definition.directives, - description=definition.description - or (get_description_node(getattr(cls, "__description__", None))), - values=tuple( - EnumValueDefinitionNode( - name=value.name, - directives=value.directives, - description=value.description - or ( - get_description_node( - members_descriptions.get(value.name.value), - ) - ), - ) - for value in definition.values # pylint: disable=no-member - ), - ), - ) - - @classmethod - def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLEnumModel": - members = getattr(cls, "__members__", []) - members_values = {} - if isinstance(members, dict): - members_values = dict(members.items()) - elif isclass(members) and issubclass(members, Enum): - members_values = {i.name: i for i in members} - elif isinstance(members, list): - members_values = {kv: kv for kv in members} - - members_descriptions = getattr(cls, "__members_descriptions__", {}) - - return GraphQLEnumModel( - name=name, - members=members_values, - ast_type=EnumTypeDefinitionNode, - ast=EnumTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node( - getattr(cls, "__description__", None), - ), - values=tuple( - EnumValueDefinitionNode( - name=NameNode(value=value_name), - description=get_description_node( - members_descriptions.get(value_name) - ), - ) - for value_name in members_values - ), - ), - ) - - @classmethod - def _validate(cls): - if getattr(cls, "__schema__", None): - cls._validate_enum_type_with_schema() - else: - cls._validate_enum_type() - - @classmethod - def _validate_enum_type_with_schema(cls): - definition = parse_definition(EnumTypeDefinitionNode, cls.__schema__) - - if not isinstance(definition, EnumTypeDefinitionNode): - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - f"with declaration for an invalid GraphQL type. " - f"('{definition.__class__.__name__}' != '{EnumTypeDefinitionNode.__name__}')" - ) - - validate_name(cls, definition) - validate_description(cls, definition) - - members_names = { - value.name.value for value in definition.values # pylint: disable=no-member - } - if not members_names: - raise ValueError( - f"Class '{cls.__name__}' defines '__schema__' attribute " - "that doesn't declare any enum members." - ) - - members_values = getattr(cls, "__members__", None) - if members_values: - cls.validate_members_values(members_values, members_names) - - members_descriptions = getattr(cls, "__members_descriptions__", {}) - cls.validate_enum_members_descriptions(members_names, members_descriptions) - - duplicate_descriptions = [ - ast_member.name.value - for ast_member in definition.values # pylint: disable=no-member - if ast_member.description - and ast_member.description.value - and members_descriptions.get(ast_member.name.value) - ] - - if duplicate_descriptions: - raise ValueError( - f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " - f"descriptions for enum members that also have description in '__schema__' " - f"attribute. (members: '{', '.join(duplicate_descriptions)}')" - ) - - @classmethod - def validate_members_values(cls, members_values, members_names): - if isinstance(members_values, list): - raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute " - "can't be a list when used together with '__schema__'." - ) - - missing_members = None - if isinstance(members_values, dict): - missing_members = members_names - set(members_values) - elif isclass(members_values) and issubclass(members_values, Enum): - missing_members = members_names - {value.name for value in members_values} - - if missing_members: - raise ValueError( - f"Class '{cls.__name__}' '__members__' is missing values " - f"for enum members defined in '__schema__'. " - f"(missing items: '{', '.join(missing_members)}')" - ) - - @classmethod - def _validate_enum_type(cls): - members_values = getattr(cls, "__members__", None) - if not members_values: - raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute is either missing or " - "empty. Either define it or provide full SDL for this enum using " - "the '__schema__' attribute." - ) - - if not any( - [ - isinstance(members_values, (dict, list)), - isclass(members_values) and issubclass(members_values, Enum), - ] - ): - raise ValueError( - f"Class '{cls.__name__}' '__members__' attribute is of unsupported type. " - f"Expected 'Dict[str, Any]', 'Type[Enum]' or List[str]. " - f"(found: '{type(members_values)}')" - ) - - members_names = cls.get_members_set(members_values) - members_descriptions = getattr(cls, "__members_descriptions__", {}) - cls.validate_enum_members_descriptions(members_names, members_descriptions) - - @classmethod - def validate_enum_members_descriptions( - cls, members: Set[str], members_descriptions: dict - ): - invalid_descriptions = set(members_descriptions) - members - if invalid_descriptions: - invalid_descriptions_str = "', '".join(invalid_descriptions) - raise ValueError( - f"Class '{cls.__name__}' '__members_descriptions__' attribute defines " - f"descriptions for undefined enum members. " - f"(undefined members: '{invalid_descriptions_str}')" - ) - - @staticmethod - def get_members_set( - members: Optional[Union[Type[Enum], Dict[str, Any], List[str]]] - ) -> Set[str]: - if isinstance(members, dict): - return set(members.keys()) - - if isclass(members) and issubclass(members, Enum): - return set(member.name for member in members) - - if isinstance(members, list): - return set(members) - - raise TypeError( - f"Expected members to be of type Dict[str, Any], List[str], or Enum." - f"Got {type(members).__name__} instead." - ) - - -def create_graphql_enum_model( - enum: Type[Enum], - *, - name: Optional[str] = None, - description: Optional[str] = None, - members_descriptions: Optional[Dict[str, str]] = None, - members_include: Optional[Iterable[str]] = None, - members_exclude: Optional[Iterable[str]] = None, -) -> "GraphQLEnumModel": - if members_include and members_exclude: - raise ValueError( - "'members_include' and 'members_exclude' options are mutually exclusive." - ) - - if hasattr(enum, "__get_graphql_model__"): - return cast(GraphQLEnumModel, enum.__get_graphql_model__()) - - if not name: - if hasattr(enum, "__get_graphql_name__"): - name = cast("str", enum.__get_graphql_name__()) - else: - name = enum.__name__ - - members: Dict[str, Any] = {i.name: i for i in enum} - final_members: Dict[str, Any] = {} - - if members_include: - for key, value in members.items(): - if key in members_include: - final_members[key] = value - elif members_exclude: - for key, value in members.items(): - if key not in members_exclude: - final_members[key] = value - else: - final_members = members - - members_descriptions = members_descriptions or {} - for member in members_descriptions: - if member not in final_members: - raise ValueError( - f"Member description was specified for a member '{member}' " - "not present in final GraphQL enum." - ) - - return GraphQLEnumModel( - name=name, - members=final_members, - ast_type=EnumTypeDefinitionNode, - ast=EnumTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node(description), - values=tuple( - EnumValueDefinitionNode( - name=NameNode(value=value_name), - description=get_description_node( - members_descriptions.get(value_name) - ), - ) - for value_name in final_members - ), - ), - ) - - -def graphql_enum( - cls: Optional[Type[Enum]] = None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - members_descriptions: Optional[Dict[str, str]] = None, - members_include: Optional[Iterable[str]] = None, - members_exclude: Optional[Iterable[str]] = None, -): - def graphql_enum_decorator(cls: Type[Enum]): - graphql_model = create_graphql_enum_model( - cls, - name=name, - description=description, - members_descriptions=members_descriptions, - members_include=members_include, - members_exclude=members_exclude, - ) - - def __get_graphql_model__(*_) -> GraphQLEnumModel: - return graphql_model - - setattr(cls, "__get_graphql_model__", classmethod(__get_graphql_model__)) - return cls - - if cls: - return graphql_enum_decorator(cls) - - return graphql_enum_decorator diff --git a/ariadne_graphql_modules/executable_schema.py b/ariadne_graphql_modules/executable_schema.py index d7bd6fe..f2bae53 100644 --- a/ariadne_graphql_modules/executable_schema.py +++ b/ariadne_graphql_modules/executable_schema.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from enum import Enum -from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union +from typing import Any, Optional, Union from ariadne import ( SchemaBindable, @@ -14,38 +15,38 @@ GraphQLSchema, assert_valid_schema, build_ast_schema, - parse, concat_ast, + parse, ) -from .base import GraphQLMetadata, GraphQLModel, GraphQLType -from .roots import ROOTS_NAMES, merge_root_nodes -from .sort import sort_schema_document +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.roots import ROOTS_NAMES, merge_root_nodes +from ariadne_graphql_modules.sort import sort_schema_document -SchemaType = Union[str, Enum, SchemaBindable, Type[GraphQLType], Type[Enum]] +SchemaType = Union[str, Enum, SchemaBindable, type[GraphQLType], type[Enum]] -def make_executable_schema( - *types: Union[SchemaType, List[SchemaType]], - directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None, +def make_executable_schema( # noqa: C901 + *types: Union[SchemaType, list[SchemaType]], + directives: Optional[dict[str, type[SchemaDirectiveVisitor]]] = None, convert_names_case: Union[bool, SchemaNameConverter] = False, merge_roots: bool = True, ) -> GraphQLSchema: metadata = GraphQLMetadata() - type_defs: List[str] = find_type_defs(types) - types_list: List[SchemaType] = flatten_types(types, metadata) + type_defs: list[str] = find_type_defs(types) + types_list: list[SchemaType] = flatten_types(types, metadata) assert_types_unique(types_list, merge_roots) assert_types_not_abstract(types_list) - schema_bindables: List[Union[SchemaBindable, GraphQLModel]] = [] + schema_bindables: list[Union[SchemaBindable, GraphQLModel]] = [] for type_def in types_list: if isinstance(type_def, SchemaBindable): schema_bindables.append(type_def) elif isinstance(type_def, type) and issubclass(type_def, (GraphQLType, Enum)): schema_bindables.append(metadata.get_graphql_model(type_def)) - schema_models: List[GraphQLModel] = [ + schema_models: list[GraphQLModel] = [ type_def for type_def in schema_bindables if isinstance(type_def, GraphQLModel) ] @@ -98,11 +99,11 @@ def make_executable_schema( def find_type_defs( types: Union[ - Tuple[Union[SchemaType, List[SchemaType]], ...], - List[SchemaType], - ] -) -> List[str]: - type_defs: List[str] = [] + tuple[Union[SchemaType, list[SchemaType]], ...], + list[SchemaType], + ], +) -> list[str]: + type_defs: list[str] = [] for type_def in types: if isinstance(type_def, str): @@ -114,14 +115,14 @@ def find_type_defs( def flatten_types( - types: Tuple[Union[SchemaType, List[SchemaType]], ...], + types: tuple[Union[SchemaType, list[SchemaType]], ...], metadata: GraphQLMetadata, -) -> List[SchemaType]: - flat_schema_types_list: List[SchemaType] = flatten_schema_types( +) -> list[SchemaType]: + flat_schema_types_list: list[SchemaType] = flatten_schema_types( types, metadata, dedupe=True ) - types_list: List[SchemaType] = [] + types_list: list[SchemaType] = [] for type_def in flat_schema_types_list: if isinstance(type_def, SchemaBindable): types_list.append(type_def) @@ -146,13 +147,13 @@ def flatten_types( return types_list -def flatten_schema_types( - types: Sequence[Union[SchemaType, List[SchemaType]]], +def flatten_schema_types( # noqa: C901 + types: Sequence[Union[SchemaType, list[SchemaType]]], metadata: GraphQLMetadata, dedupe: bool, -) -> List[SchemaType]: - flat_list: List[SchemaType] = [] - checked_types: List[Type[GraphQLType]] = [] +) -> list[SchemaType]: + flat_list: list[SchemaType] = [] + checked_types: list[type[GraphQLType]] = [] for type_def in types: if isinstance(type_def, str): @@ -171,7 +172,7 @@ def flatten_schema_types( if not dedupe: return flat_list - unique_list: List[SchemaType] = [] + unique_list: list[SchemaType] = [] for type_def in flat_list: if type_def not in unique_list: unique_list.append(type_def) @@ -180,9 +181,9 @@ def flatten_schema_types( def add_graphql_type_to_flat_list( - flat_list: List[SchemaType], - checked_types: List[Type[GraphQLType]], - type_def: Type[GraphQLType], + flat_list: list[SchemaType], + checked_types: list[type[GraphQLType]], + type_def: type[GraphQLType], metadata: GraphQLMetadata, ) -> None: if type_def in checked_types: @@ -212,8 +213,8 @@ def get_graphql_type_name(type_def: SchemaType) -> Optional[str]: return None -def assert_types_unique(type_defs: List[SchemaType], merge_roots: bool): - types_names: Dict[str, Any] = {} +def assert_types_unique(type_defs: list[SchemaType], merge_roots: bool): + types_names: dict[str, Any] = {} for type_def in type_defs: type_name = get_graphql_type_name(type_def) if not type_name: @@ -232,7 +233,7 @@ def assert_types_unique(type_defs: List[SchemaType], merge_roots: bool): types_names[type_name] = type_def -def assert_types_not_abstract(type_defs: List[SchemaType]): +def assert_types_not_abstract(type_defs: list[SchemaType]): for type_def in type_defs: if isinstance(type_def, SchemaBindable): continue diff --git a/ariadne_graphql_modules/input_type/__init__.py b/ariadne_graphql_modules/input_type/__init__.py index 23a8e12..2f8c31d 100644 --- a/ariadne_graphql_modules/input_type/__init__.py +++ b/ariadne_graphql_modules/input_type/__init__.py @@ -1,6 +1,5 @@ -from .graphql_type import GraphQLInput -from .models import GraphQLInputModel - +from ariadne_graphql_modules.input_type.graphql_type import GraphQLInput +from ariadne_graphql_modules.input_type.models import GraphQLInputModel __all__ = [ "GraphQLInput", diff --git a/ariadne_graphql_modules/input_type/graphql_type.py b/ariadne_graphql_modules/input_type/graphql_type.py index 9670ea4..14496aa 100644 --- a/ariadne_graphql_modules/input_type/graphql_type.py +++ b/ariadne_graphql_modules/input_type/graphql_type.py @@ -1,30 +1,31 @@ +from collections.abc import Iterable from copy import deepcopy from enum import Enum -from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast +from typing import Any, Optional, Union, cast from graphql import InputObjectTypeDefinitionNode, InputValueDefinitionNode, NameNode -from .graphql_field import GraphQLInputField -from ..base import GraphQLMetadata, GraphQLModel, GraphQLType -from ..convert_name import ( +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.convert_name import ( convert_graphql_name_to_python, convert_python_name_to_graphql, ) -from ..description import get_description_node -from .models import GraphQLInputModel -from .validators import ( +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.input_type.graphql_field import GraphQLInputField +from ariadne_graphql_modules.input_type.models import GraphQLInputModel +from ariadne_graphql_modules.input_type.validators import ( validate_input_type, validate_input_type_with_schema, ) -from ..typing import get_graphql_type, get_type_node -from ..value import get_value_node -from ..utils import parse_definition +from ariadne_graphql_modules.typing import get_graphql_type, get_type_node +from ariadne_graphql_modules.utils import parse_definition +from ariadne_graphql_modules.value import get_value_node class GraphQLInput(GraphQLType): - __kwargs__: Dict[str, Any] + __kwargs__: dict[str, Any] __schema__: Optional[str] - __out_names__: Optional[Dict[str, str]] = None + __out_names__: Optional[dict[str, str]] = None def __init__(self, **kwargs: Any): for kwarg in kwargs: @@ -53,7 +54,7 @@ def __init_subclass__(cls) -> None: cls.__kwargs__ = validate_input_type(cls) @classmethod - def create_from_data(cls, data: Dict[str, Any]) -> "GraphQLInput": + def create_from_data(cls, data: dict[str, Any]) -> "GraphQLInput": return cls(**data) @classmethod @@ -73,9 +74,9 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLInputModel": parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), ) - out_names: Dict[str, str] = getattr(cls, "__out_names__") or {} + out_names: dict[str, str] = getattr(cls, "__out_names__") or {} - fields: List[InputValueDefinitionNode] = [] + fields: list[InputValueDefinitionNode] = [] for field in definition.fields: fields.append( InputValueDefinitionNode( @@ -107,14 +108,14 @@ def __get_graphql_model_without_schema__( cls, metadata: GraphQLMetadata, name: str ) -> "GraphQLInputModel": type_hints = cls.__annotations__ # pylint: disable=no-member - fields_instances: Dict[str, GraphQLInputField] = { + fields_instances: dict[str, GraphQLInputField] = { attr_name: getattr(cls, attr_name) for attr_name in dir(cls) if isinstance(getattr(cls, attr_name), GraphQLInputField) } - fields_ast: List[InputValueDefinitionNode] = [] - out_names: Dict[str, str] = {} + fields_ast: list[InputValueDefinitionNode] = [] + out_names: dict[str, str] = {} for hint_name, hint_type in type_hints.items(): if hint_name.startswith("__"): @@ -180,9 +181,9 @@ def __get_graphql_model_without_schema__( @classmethod def __get_graphql_types__( cls, _: "GraphQLMetadata" - ) -> Iterable[Union[Type["GraphQLType"], Type[Enum]]]: + ) -> Iterable[Union[type["GraphQLType"], type[Enum]]]: """Returns iterable with GraphQL types associated with this type""" - types: List[Union[Type["GraphQLType"], Type[Enum]]] = [cls] + types: list[Union[type[GraphQLType], type[Enum]]] = [cls] for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -221,7 +222,7 @@ def field( def get_field_node_from_type_hint( - parent_type: Type[GraphQLInput], + parent_type: type[GraphQLInput], metadata: GraphQLMetadata, field_name: str, field_type: Any, diff --git a/ariadne_graphql_modules/input_type/models.py b/ariadne_graphql_modules/input_type/models.py index 6510d11..5e21480 100644 --- a/ariadne_graphql_modules/input_type/models.py +++ b/ariadne_graphql_modules/input_type/models.py @@ -1,16 +1,16 @@ from dataclasses import dataclass -from typing import Any, Dict +from typing import Any from ariadne import InputType as InputTypeBindable from graphql import GraphQLSchema -from ..base import GraphQLModel +from ariadne_graphql_modules.base import GraphQLModel @dataclass(frozen=True) class GraphQLInputModel(GraphQLModel): out_type: Any - out_names: Dict[str, str] + out_names: dict[str, str] def bind_to_schema(self, schema: GraphQLSchema): bindable = InputTypeBindable(self.name, self.out_type, self.out_names) diff --git a/ariadne_graphql_modules/input_type/validators.py b/ariadne_graphql_modules/input_type/validators.py index 68432b8..099c475 100644 --- a/ariadne_graphql_modules/input_type/validators.py +++ b/ariadne_graphql_modules/input_type/validators.py @@ -1,17 +1,18 @@ -from typing import Any, Dict, List, Type, cast, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from graphql import InputObjectTypeDefinitionNode -from ..convert_name import convert_graphql_name_to_python -from ..validators import validate_description, validate_name -from ..value import get_value_from_node, get_value_node -from ..utils import parse_definition -from .graphql_field import GraphQLInputField + +from ariadne_graphql_modules.convert_name import convert_graphql_name_to_python +from ariadne_graphql_modules.input_type.graphql_field import GraphQLInputField +from ariadne_graphql_modules.utils import parse_definition +from ariadne_graphql_modules.validators import validate_description, validate_name +from ariadne_graphql_modules.value import get_value_from_node, get_value_node if TYPE_CHECKING: - from .graphql_type import GraphQLInput + from ariadne_graphql_modules.input_type.graphql_type import GraphQLInput -def validate_input_type_with_schema(cls: Type["GraphQLInput"]) -> Dict[str, Any]: +def validate_input_type_with_schema(cls: type["GraphQLInput"]) -> dict[str, Any]: definition = cast( InputObjectTypeDefinitionNode, parse_definition(InputObjectTypeDefinitionNode, cls.__schema__), @@ -34,10 +35,10 @@ def validate_input_type_with_schema(cls: Type["GraphQLInput"]) -> Dict[str, Any] "with declaration for an input type without any fields. " ) - fields_names: List[str] = [field.name.value for field in definition.fields] - used_out_names: List[str] = [] + fields_names: list[str] = [field.name.value for field in definition.fields] + used_out_names: list[str] = [] - out_names: Dict[str, str] = getattr(cls, "__out_names__", {}) or {} + out_names: dict[str, str] = getattr(cls, "__out_names__", {}) or {} for field_name, out_name in out_names.items(): if field_name not in fields_names: raise ValueError( @@ -58,11 +59,11 @@ def validate_input_type_with_schema(cls: Type["GraphQLInput"]) -> Dict[str, Any] def get_input_type_with_schema_kwargs( - cls: Type["GraphQLInput"], + cls: type["GraphQLInput"], definition: InputObjectTypeDefinitionNode, - out_names: Dict[str, str], -) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} + out_names: dict[str, str], +) -> dict[str, Any]: + kwargs: dict[str, Any] = {} for field in definition.fields: try: python_name = out_names[field.name.value] @@ -82,7 +83,7 @@ def get_input_type_with_schema_kwargs( return kwargs -def validate_input_type(cls: Type["GraphQLInput"]) -> Dict[str, Any]: +def validate_input_type(cls: type["GraphQLInput"]) -> dict[str, Any]: if cls.__out_names__: raise ValueError( f"Class '{cls.__name__}' defines '__out_names__' attribute. " @@ -92,8 +93,8 @@ def validate_input_type(cls: Type["GraphQLInput"]) -> Dict[str, Any]: return get_input_type_kwargs(cls) -def get_input_type_kwargs(cls: Type["GraphQLInput"]) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} +def get_input_type_kwargs(cls: type["GraphQLInput"]) -> dict[str, Any]: + kwargs: dict[str, Any] = {} for attr_name in cls.__annotations__: if attr_name.startswith("__"): @@ -111,7 +112,7 @@ def get_input_type_kwargs(cls: Type["GraphQLInput"]) -> Dict[str, Any]: def validate_field_default_value( - cls: Type["GraphQLInput"], field_name: str, default_value: Any + cls: type["GraphQLInput"], field_name: str, default_value: Any ): if default_value is None: return diff --git a/ariadne_graphql_modules/interface_type/__init__.py b/ariadne_graphql_modules/interface_type/__init__.py index 0ec79d8..f6ddb46 100644 --- a/ariadne_graphql_modules/interface_type/__init__.py +++ b/ariadne_graphql_modules/interface_type/__init__.py @@ -1,6 +1,5 @@ -from .graphql_type import GraphQLInterface -from .models import GraphQLInterfaceModel - +from ariadne_graphql_modules.interface_type.graphql_type import GraphQLInterface +from ariadne_graphql_modules.interface_type.models import GraphQLInterfaceModel __all__ = [ "GraphQLInterface", diff --git a/ariadne_graphql_modules/interface_type/graphql_type.py b/ariadne_graphql_modules/interface_type/graphql_type.py index 8bad8e8..e06b90a 100644 --- a/ariadne_graphql_modules/interface_type/graphql_type.py +++ b/ariadne_graphql_modules/interface_type/graphql_type.py @@ -1,29 +1,27 @@ -from typing import Any, Dict, Optional, Tuple, cast +from typing import Any, Optional, cast from ariadne.types import Resolver from graphql import ( FieldDefinitionNode, InterfaceTypeDefinitionNode, - NameNode, NamedTypeNode, + NameNode, ) -from ariadne_graphql_modules.base_object_type.graphql_field import GraphQLClassData - -from ..base_object_type import ( - GraphQLFieldData, +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel +from ariadne_graphql_modules.base_object_type import ( GraphQLBaseObject, + GraphQLFieldData, GraphQLObjectData, validate_object_type_with_schema, validate_object_type_without_schema, ) -from ..types import GraphQLClassType - -from ..utils import parse_definition -from ..base import GraphQLMetadata, GraphQLModel -from ..description import get_description_node -from ..object_type import GraphQLObject -from .models import GraphQLInterfaceModel +from ariadne_graphql_modules.base_object_type.graphql_field import GraphQLClassData +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.interface_type.models import GraphQLInterfaceModel +from ariadne_graphql_modules.object_type import GraphQLObject +from ariadne_graphql_modules.types import GraphQLClassType +from ariadne_graphql_modules.utils import parse_definition class GraphQLInterface(GraphQLBaseObject): @@ -52,8 +50,8 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": parse_definition(InterfaceTypeDefinitionNode, cls.__schema__), ) - resolvers: Dict[str, Resolver] = {} - fields: Tuple[FieldDefinitionNode, ...] = tuple() + resolvers: dict[str, Resolver] = {} + fields: tuple[FieldDefinitionNode, ...] = tuple() fields, resolvers = cls._create_fields_and_resolvers_with_schema( definition.fields ) @@ -107,7 +105,8 @@ def resolve_type(obj: Any, *_) -> str: return obj.__get_graphql_name__() raise ValueError( - f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." + f"Cannot resolve GraphQL type {obj} " + "for object of type '{type(obj).__name__}'." ) @classmethod diff --git a/ariadne_graphql_modules/interface_type/models.py b/ariadne_graphql_modules/interface_type/models.py index 3dfbf51..153f36b 100644 --- a/ariadne_graphql_modules/interface_type/models.py +++ b/ariadne_graphql_modules/interface_type/models.py @@ -1,19 +1,19 @@ from dataclasses import dataclass -from typing import Dict, cast +from typing import cast from ariadne import InterfaceType from ariadne.types import Resolver from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLTypeResolver -from ..base import GraphQLModel +from ariadne_graphql_modules.base import GraphQLModel @dataclass(frozen=True) class GraphQLInterfaceModel(GraphQLModel): - resolvers: Dict[str, Resolver] + resolvers: dict[str, Resolver] resolve_type: GraphQLTypeResolver - out_names: Dict[str, Dict[str, str]] - aliases: Dict[str, str] + out_names: dict[str, dict[str, str]] + aliases: dict[str, str] def bind_to_schema(self, schema: GraphQLSchema): bindable = InterfaceType(self.name, self.resolve_type) diff --git a/ariadne_graphql_modules/object_type/__init__.py b/ariadne_graphql_modules/object_type/__init__.py index 03db715..0752509 100644 --- a/ariadne_graphql_modules/object_type/__init__.py +++ b/ariadne_graphql_modules/object_type/__init__.py @@ -1,19 +1,19 @@ -from ..base_object_type.graphql_field import ( - object_field, +from ariadne_graphql_modules.base_object_type.graphql_field import ( + GraphQLObjectFieldArg, GraphQLObjectResolver, GraphQLObjectSource, + object_field, object_subscriber, - GraphQLObjectFieldArg, ) -from .graphql_type import GraphQLObject -from .models import GraphQLObjectModel -from ..base_object_type.utils import ( +from ariadne_graphql_modules.base_object_type.utils import ( get_field_args_from_resolver, + get_field_args_from_subscriber, get_field_args_out_names, get_field_node_from_obj_field, update_field_args_options, - get_field_args_from_subscriber, ) +from ariadne_graphql_modules.object_type.graphql_type import GraphQLObject +from ariadne_graphql_modules.object_type.models import GraphQLObjectModel __all__ = [ "GraphQLObject", diff --git a/ariadne_graphql_modules/object_type/graphql_type.py b/ariadne_graphql_modules/object_type/graphql_type.py index 848b73b..c7918a0 100644 --- a/ariadne_graphql_modules/object_type/graphql_type.py +++ b/ariadne_graphql_modules/object_type/graphql_type.py @@ -1,37 +1,31 @@ from typing import ( - Dict, Optional, - Tuple, cast, ) from ariadne.types import Resolver from graphql import ( FieldDefinitionNode, - NameNode, NamedTypeNode, + NameNode, ObjectTypeDefinitionNode, ) +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel +from ariadne_graphql_modules.base_object_type import ( + GraphQLBaseObject, + GraphQLFieldData, + GraphQLObjectData, +) from ariadne_graphql_modules.base_object_type.graphql_field import GraphQLClassData from ariadne_graphql_modules.base_object_type.validators import ( validate_object_type_with_schema, validate_object_type_without_schema, ) - -from ..types import GraphQLClassType - -from ..base_object_type import ( - GraphQLFieldData, - GraphQLBaseObject, - GraphQLObjectData, -) -from .models import GraphQLObjectModel - - -from ..utils import parse_definition -from ..base import GraphQLMetadata, GraphQLModel -from ..description import get_description_node +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.object_type.models import GraphQLObjectModel +from ariadne_graphql_modules.types import GraphQLClassType +from ariadne_graphql_modules.utils import parse_definition class GraphQLObject(GraphQLBaseObject): @@ -63,8 +57,8 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": parse_definition(ObjectTypeDefinitionNode, cls.__schema__), ) - resolvers: Dict[str, Resolver] = {} - fields: Tuple[FieldDefinitionNode, ...] = tuple() + resolvers: dict[str, Resolver] = {} + fields: tuple[FieldDefinitionNode, ...] = tuple() fields, resolvers = cls._create_fields_and_resolvers_with_schema( definition.fields ) diff --git a/ariadne_graphql_modules/object_type/models.py b/ariadne_graphql_modules/object_type/models.py index 1241188..eb4832e 100644 --- a/ariadne_graphql_modules/object_type/models.py +++ b/ariadne_graphql_modules/object_type/models.py @@ -1,18 +1,18 @@ from dataclasses import dataclass -from typing import Dict, cast +from typing import cast from ariadne import ObjectType as ObjectTypeBindable from ariadne.types import Resolver from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema -from ..base import GraphQLModel +from ariadne_graphql_modules.base import GraphQLModel @dataclass(frozen=True) class GraphQLObjectModel(GraphQLModel): - resolvers: Dict[str, Resolver] - aliases: Dict[str, str] - out_names: Dict[str, Dict[str, str]] + resolvers: dict[str, Resolver] + aliases: dict[str, str] + out_names: dict[str, dict[str, str]] def bind_to_schema(self, schema: GraphQLSchema): bindable = ObjectTypeBindable(self.name) diff --git a/ariadne_graphql_modules/roots.py b/ariadne_graphql_modules/roots.py index 6e52563..c4c035f 100644 --- a/ariadne_graphql_modules/roots.py +++ b/ariadne_graphql_modules/roots.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, cast +from typing import Optional, cast from graphql import ( ConstDirectiveNode, @@ -11,13 +11,13 @@ TypeDefinitionNode, ) -DefinitionsList = List[DefinitionNode] +DefinitionsList = list[DefinitionNode] ROOTS_NAMES = ("Query", "Mutation", "Subscription") def merge_root_nodes(document_node: DocumentNode) -> DocumentNode: - roots_definitions: Dict[str, List[TypeDefinitionNode]] = { + roots_definitions: dict[str, list[TypeDefinitionNode]] = { root: [] for root in ROOTS_NAMES } final_definitions: DefinitionsList = [] @@ -40,13 +40,13 @@ def merge_root_nodes(document_node: DocumentNode) -> DocumentNode: return DocumentNode(definitions=tuple(final_definitions)) -def merge_nodes(nodes: List[TypeDefinitionNode]) -> ObjectTypeDefinitionNode: +def merge_nodes(nodes: list[TypeDefinitionNode]) -> ObjectTypeDefinitionNode: root_name = nodes[0].name.value description: Optional[StringValueNode] = None - interfaces: List[NamedTypeNode] = [] - directives: List[ConstDirectiveNode] = [] - fields: Dict[str, FieldDefinitionNode] = {} + interfaces: list[NamedTypeNode] = [] + directives: list[ConstDirectiveNode] = [] + fields: dict[str, FieldDefinitionNode] = {} for node in nodes: node = cast(ObjectTypeDefinitionNode, node) diff --git a/ariadne_graphql_modules/scalar_type/__init__.py b/ariadne_graphql_modules/scalar_type/__init__.py index 82909c7..29a7747 100644 --- a/ariadne_graphql_modules/scalar_type/__init__.py +++ b/ariadne_graphql_modules/scalar_type/__init__.py @@ -1,6 +1,5 @@ -from .graphql_type import GraphQLScalar -from .models import GraphQLScalarModel - +from ariadne_graphql_modules.scalar_type.graphql_type import GraphQLScalar +from ariadne_graphql_modules.scalar_type.models import GraphQLScalarModel __all__ = [ "GraphQLScalar", diff --git a/ariadne_graphql_modules/scalar_type/graphql_type.py b/ariadne_graphql_modules/scalar_type/graphql_type.py index 60f27f3..449c6da 100644 --- a/ariadne_graphql_modules/scalar_type/graphql_type.py +++ b/ariadne_graphql_modules/scalar_type/graphql_type.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Generic, Optional, TypeVar, cast +from typing import Any, Generic, Optional, TypeVar, cast from graphql import ( NameNode, @@ -7,15 +7,13 @@ value_from_ast_untyped, ) -from ..description import get_description_node -from .models import GraphQLScalarModel - -from ..utils import parse_definition - -from .validators import validate_scalar_type_with_schema - -from ..base import GraphQLMetadata, GraphQLModel, GraphQLType - +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.scalar_type.models import GraphQLScalarModel +from ariadne_graphql_modules.scalar_type.validators import ( + validate_scalar_type_with_schema, +) +from ariadne_graphql_modules.utils import parse_definition T = TypeVar("T") @@ -94,7 +92,7 @@ def parse_value(cls, value: Any) -> Any: @classmethod def parse_literal( - cls, node: ValueNode, variables: Optional[Dict[str, Any]] = None + cls, node: ValueNode, variables: Optional[dict[str, Any]] = None ) -> Any: return cls.parse_value(value_from_ast_untyped(node, variables)) diff --git a/ariadne_graphql_modules/scalar_type/models.py b/ariadne_graphql_modules/scalar_type/models.py index f6ad88c..a544ef9 100644 --- a/ariadne_graphql_modules/scalar_type/models.py +++ b/ariadne_graphql_modules/scalar_type/models.py @@ -1,14 +1,15 @@ from dataclasses import dataclass from typing import Optional +from ariadne import ScalarType as ScalarTypeBindable from graphql import ( GraphQLScalarLiteralParser, GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLSchema, ) -from ariadne import ScalarType as ScalarTypeBindable -from ..base import GraphQLModel + +from ariadne_graphql_modules.base import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/scalar_type/validators.py b/ariadne_graphql_modules/scalar_type/validators.py index ddf6512..03a5edd 100644 --- a/ariadne_graphql_modules/scalar_type/validators.py +++ b/ariadne_graphql_modules/scalar_type/validators.py @@ -1,16 +1,15 @@ -from typing import TYPE_CHECKING, Type +from typing import TYPE_CHECKING from graphql import ScalarTypeDefinitionNode -from ..validators import validate_description, validate_name - -from ..utils import parse_definition +from ariadne_graphql_modules.utils import parse_definition +from ariadne_graphql_modules.validators import validate_description, validate_name if TYPE_CHECKING: - from .graphql_type import GraphQLScalar + from ariadne_graphql_modules.scalar_type.graphql_type import GraphQLScalar -def validate_scalar_type_with_schema(cls: Type["GraphQLScalar"]): +def validate_scalar_type_with_schema(cls: type["GraphQLScalar"]): definition = parse_definition(cls.__name__, cls.__schema__) if not isinstance(definition, ScalarTypeDefinitionNode): diff --git a/ariadne_graphql_modules/sort.py b/ariadne_graphql_modules/sort.py index 939d152..883e970 100644 --- a/ariadne_graphql_modules/sort.py +++ b/ariadne_graphql_modules/sort.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Union, cast +from typing import Any, Union, cast from graphql import ( DefinitionNode, @@ -15,12 +15,12 @@ TypeNode, ) -from .roots import ROOTS_NAMES +from ariadne_graphql_modules.roots import ROOTS_NAMES def sort_schema_document(document: DocumentNode) -> DocumentNode: - unsorted_nodes: Dict[str, TypeDefinitionNode] = {} - sorted_nodes: List[Union[TypeDefinitionNode, DefinitionNode]] = [] + unsorted_nodes: dict[str, TypeDefinitionNode] = {} + sorted_nodes: list[Union[TypeDefinitionNode, DefinitionNode]] = [] for node in document.definitions: cast_node = cast(TypeDefinitionNode, node) @@ -41,9 +41,9 @@ def sort_schema_document(document: DocumentNode) -> DocumentNode: def get_sorted_directives( - unsorted_nodes: Dict[str, Any] -) -> List[DirectiveDefinitionNode]: - directives: List[DirectiveDefinitionNode] = [] + unsorted_nodes: dict[str, Any], +) -> list[DirectiveDefinitionNode]: + directives: list[DirectiveDefinitionNode] = [] for name, model in tuple(unsorted_nodes.items()): if isinstance(model, DirectiveDefinitionNode): directives.append(unsorted_nodes.pop(name)) @@ -51,9 +51,9 @@ def get_sorted_directives( def get_sorted_scalars( - unsorted_nodes: Dict[str, Any] -) -> List[ScalarTypeDefinitionNode]: - scalars: List[ScalarTypeDefinitionNode] = [] + unsorted_nodes: dict[str, Any], +) -> list[ScalarTypeDefinitionNode]: + scalars: list[ScalarTypeDefinitionNode] = [] for name, model in tuple(unsorted_nodes.items()): if isinstance(model, ScalarTypeDefinitionNode): scalars.append(unsorted_nodes.pop(name)) @@ -63,9 +63,9 @@ def get_sorted_scalars( def get_sorted_type( root: str, - unsorted_nodes: Dict[str, TypeDefinitionNode], -) -> List[TypeDefinitionNode]: - sorted_nodes: List[TypeDefinitionNode] = [] + unsorted_nodes: dict[str, TypeDefinitionNode], +) -> list[TypeDefinitionNode]: + sorted_nodes: list[TypeDefinitionNode] = [] if root not in unsorted_nodes: return sorted_nodes @@ -82,9 +82,9 @@ def get_sorted_type( def get_sorted_object_dependencies( root_node: Union[ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode], - unsorted_nodes: Dict[str, TypeDefinitionNode], -) -> List[TypeDefinitionNode]: - sorted_nodes: List[TypeDefinitionNode] = [] + unsorted_nodes: dict[str, TypeDefinitionNode], +) -> list[TypeDefinitionNode]: + sorted_nodes: list[TypeDefinitionNode] = [] if root_node.interfaces: for interface in root_node.interfaces: @@ -111,9 +111,9 @@ def get_sorted_object_dependencies( def get_sorted_input_dependencies( root_node: InputObjectTypeDefinitionNode, - unsorted_nodes: Dict[str, TypeDefinitionNode], -) -> List[TypeDefinitionNode]: - sorted_nodes: List[TypeDefinitionNode] = [] + unsorted_nodes: dict[str, TypeDefinitionNode], +) -> list[TypeDefinitionNode]: + sorted_nodes: list[TypeDefinitionNode] = [] for field in root_node.fields: field_type = unwrap_type_name(field.type) diff --git a/ariadne_graphql_modules/subscription_type/__init__.py b/ariadne_graphql_modules/subscription_type/__init__.py index c271e40..8aaa4a9 100644 --- a/ariadne_graphql_modules/subscription_type/__init__.py +++ b/ariadne_graphql_modules/subscription_type/__init__.py @@ -1,6 +1,5 @@ -from .graphql_type import GraphQLSubscription -from .models import GraphQLSubscriptionModel - +from ariadne_graphql_modules.subscription_type.graphql_type import GraphQLSubscription +from ariadne_graphql_modules.subscription_type.models import GraphQLSubscriptionModel __all__ = [ "GraphQLSubscription", diff --git a/ariadne_graphql_modules/subscription_type/graphql_type.py b/ariadne_graphql_modules/subscription_type/graphql_type.py index d7c35a3..94127cc 100644 --- a/ariadne_graphql_modules/subscription_type/graphql_type.py +++ b/ariadne_graphql_modules/subscription_type/graphql_type.py @@ -1,5 +1,6 @@ -from typing import Any, Dict, List, Optional, cast +from typing import Any, Optional, cast +from ariadne.types import Resolver, Subscriber from graphql import ( FieldDefinitionNode, InputValueDefinitionNode, @@ -8,42 +9,35 @@ StringValueNode, ) -from ariadne.types import Resolver, Subscriber - +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel +from ariadne_graphql_modules.base_object_type import ( + GraphQLBaseObject, + GraphQLFieldData, + GraphQLObjectData, + validate_object_type_with_schema, + validate_object_type_without_schema, +) from ariadne_graphql_modules.base_object_type.graphql_field import ( GraphQLObjectField, object_field, object_resolver, ) from ariadne_graphql_modules.convert_name import convert_python_name_to_graphql - -from ..base_object_type import ( - GraphQLFieldData, - GraphQLObjectData, - validate_object_type_with_schema, - validate_object_type_without_schema, -) - -from ..types import GraphQLClassType - -from ..base_object_type import GraphQLBaseObject - - -from ..utils import parse_definition -from ..base import GraphQLMetadata, GraphQLModel -from ..description import get_description_node -from ..object_type import ( +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.object_type import ( + GraphQLObjectFieldArg, GraphQLObjectResolver, GraphQLObjectSource, - GraphQLObjectFieldArg, get_field_args_from_subscriber, get_field_args_out_names, get_field_node_from_obj_field, object_subscriber, update_field_args_options, ) -from ..value import get_value_node -from .models import GraphQLSubscriptionModel +from ariadne_graphql_modules.subscription_type.models import GraphQLSubscriptionModel +from ariadne_graphql_modules.types import GraphQLClassType +from ariadne_graphql_modules.utils import parse_definition +from ariadne_graphql_modules.value import get_value_node class GraphQLSubscription(GraphQLBaseObject): @@ -68,17 +62,17 @@ def __init_subclass__(cls) -> None: cls.__kwargs__ = validate_object_type_without_schema(cls) @classmethod - def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": + def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": # noqa: C901 definition = cast( ObjectTypeDefinitionNode, parse_definition(ObjectTypeDefinitionNode, cls.__schema__), ) - descriptions: Dict[str, StringValueNode] = {} - args_descriptions: Dict[str, Dict[str, StringValueNode]] = {} - args_defaults: Dict[str, Dict[str, Any]] = {} - resolvers: Dict[str, Resolver] = {} - subscribers: Dict[str, Subscriber] = {} + descriptions: dict[str, StringValueNode] = {} + args_descriptions: dict[str, dict[str, StringValueNode]] = {} + args_defaults: dict[str, dict[str, Any]] = {} + resolvers: dict[str, Resolver] = {} + subscribers: dict[str, Subscriber] = {} for attr_name in dir(cls): cls_attr = getattr(cls, attr_name) @@ -106,9 +100,9 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": for arg_name, arg_options in final_args.items(): arg_description = get_description_node(arg_options.description) if arg_description: - args_descriptions[cls_attr.field][ - arg_name - ] = arg_description + args_descriptions[cls_attr.field][arg_name] = ( + arg_description + ) arg_default = arg_options.default_value if arg_default is not None: @@ -116,12 +110,12 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": arg_default ) - fields: List[FieldDefinitionNode] = [] + fields: list[FieldDefinitionNode] = [] for field in definition.fields: field_args_descriptions = args_descriptions.get(field.name.value, {}) field_args_defaults = args_defaults.get(field.name.value, {}) - args: List[InputValueDefinitionNode] = [] + args: list[InputValueDefinitionNode] = [] for arg in field.arguments: arg_name = arg.name.value args.append( @@ -170,11 +164,11 @@ def __get_graphql_model_without_schema__( type_data = cls.get_graphql_object_data(metadata) type_aliases = getattr(cls, "__aliases__", None) or {} - fields_ast: List[FieldDefinitionNode] = [] - resolvers: Dict[str, Resolver] = {} - subscribers: Dict[str, Subscriber] = {} - aliases: Dict[str, str] = {} - out_names: Dict[str, Dict[str, str]] = {} + fields_ast: list[FieldDefinitionNode] = [] + resolvers: dict[str, Resolver] = {} + subscribers: dict[str, Subscriber] = {} + aliases: dict[str, str] = {} + out_names: dict[str, dict[str, str]] = {} for attr_name, field in type_data.fields.items(): fields_ast.append(get_field_node_from_obj_field(cls, metadata, field)) @@ -212,7 +206,7 @@ def __get_graphql_model_without_schema__( def source( field: str, graphql_type: Optional[Any] = None, - args: Optional[Dict[str, GraphQLObjectFieldArg]] = None, + args: Optional[dict[str, GraphQLObjectFieldArg]] = None, description: Optional[str] = None, ): """Shortcut for object_resolver()""" @@ -244,7 +238,9 @@ def field( ) @staticmethod - def _process_class_attributes(target_cls, fields_data: GraphQLFieldData): + def _process_class_attributes( # noqa: C901 + target_cls, fields_data: GraphQLFieldData + ): for attr_name in dir(target_cls): if attr_name.startswith("__"): continue diff --git a/ariadne_graphql_modules/subscription_type/models.py b/ariadne_graphql_modules/subscription_type/models.py index d32aa1b..a3f6809 100644 --- a/ariadne_graphql_modules/subscription_type/models.py +++ b/ariadne_graphql_modules/subscription_type/models.py @@ -1,19 +1,19 @@ from dataclasses import dataclass -from typing import Dict, cast +from typing import cast from ariadne import SubscriptionType from ariadne.types import Resolver, Subscriber from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema -from ..base import GraphQLModel +from ariadne_graphql_modules.base import GraphQLModel @dataclass(frozen=True) class GraphQLSubscriptionModel(GraphQLModel): - resolvers: Dict[str, Resolver] - out_names: Dict[str, Dict[str, str]] - aliases: Dict[str, str] - subscribers: Dict[str, Subscriber] + resolvers: dict[str, Resolver] + out_names: dict[str, dict[str, str]] + aliases: dict[str, str] + subscribers: dict[str, Subscriber] def bind_to_schema(self, schema: GraphQLSchema): bindable = SubscriptionType() diff --git a/ariadne_graphql_modules/types.py b/ariadne_graphql_modules/types.py index c9021f1..d6bcd8c 100644 --- a/ariadne_graphql_modules/types.py +++ b/ariadne_graphql_modules/types.py @@ -1,15 +1,14 @@ -from typing import Dict, Type - from enum import Enum + from graphql import ( DefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, ) -FieldsDict = Dict[str, FieldDefinitionNode] -InputFieldsDict = Dict[str, InputValueDefinitionNode] -RequirementsDict = Dict[str, Type[DefinitionNode]] +FieldsDict = dict[str, FieldDefinitionNode] +InputFieldsDict = dict[str, InputValueDefinitionNode] +RequirementsDict = dict[str, type[DefinitionNode]] class GraphQLClassType(Enum): diff --git a/ariadne_graphql_modules/typing.py b/ariadne_graphql_modules/typing.py index bf902de..bb2b529 100644 --- a/ariadne_graphql_modules/typing.py +++ b/ariadne_graphql_modules/typing.py @@ -1,13 +1,12 @@ +import sys from enum import Enum from importlib import import_module from inspect import isclass -import sys from typing import ( Annotated, Any, ForwardRef, Optional, - Type, Union, get_args, get_origin, @@ -15,14 +14,15 @@ from graphql import ( ListTypeNode, - NameNode, NamedTypeNode, + NameNode, NonNullTypeNode, TypeNode, ) -from .base import GraphQLMetadata, GraphQLType -from .deferredtype import DeferredTypeData -from .idtype import GraphQLID + +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLType +from ariadne_graphql_modules.deferredtype import DeferredTypeData +from ariadne_graphql_modules.idtype import GraphQLID if sys.version_info >= (3, 10): from types import UnionType @@ -30,10 +30,10 @@ UnionType = Union -def get_type_node( +def get_type_node( # noqa: C901 metadata: GraphQLMetadata, type_hint: Any, - parent_type: Optional[Type[GraphQLType]] = None, + parent_type: Optional[type[GraphQLType]] = None, ) -> TypeNode: if is_nullable(type_hint): nullable = True @@ -126,7 +126,7 @@ def unwrap_type(type_hint: Any) -> Any: def get_deferred_type( type_hint: Any, forward_ref: ForwardRef, deferred_type: DeferredTypeData -) -> Union[Type[GraphQLType], Type[Enum]]: +) -> Union[type[GraphQLType], type[Enum]]: type_name = forward_ref.__forward_arg__ module = import_module(deferred_type.path) graphql_type = getattr(module, type_name) @@ -137,7 +137,7 @@ def get_deferred_type( return graphql_type -def get_graphql_type(annotation: Any) -> Optional[Union[Type[GraphQLType], Type[Enum]]]: +def get_graphql_type(annotation: Any) -> Optional[Union[type[GraphQLType], type[Enum]]]: """Utility that extracts GraphQL type from type annotation""" if is_nullable(annotation) or is_list(annotation): return get_graphql_type(unwrap_type(annotation)) diff --git a/ariadne_graphql_modules/union_type/__init__.py b/ariadne_graphql_modules/union_type/__init__.py index d6ebe06..7d6b6ca 100644 --- a/ariadne_graphql_modules/union_type/__init__.py +++ b/ariadne_graphql_modules/union_type/__init__.py @@ -1,6 +1,5 @@ -from .graphql_type import GraphQLUnion -from .models import GraphQLUnionModel - +from ariadne_graphql_modules.union_type.graphql_type import GraphQLUnion +from ariadne_graphql_modules.union_type.models import GraphQLUnionModel __all__ = [ "GraphQLUnion", diff --git a/ariadne_graphql_modules/union_type/graphql_type.py b/ariadne_graphql_modules/union_type/graphql_type.py index 154c8c8..d6003aa 100644 --- a/ariadne_graphql_modules/union_type/graphql_type.py +++ b/ariadne_graphql_modules/union_type/graphql_type.py @@ -1,19 +1,21 @@ -from typing import Any, Iterable, Optional, Sequence, Type, cast - -from graphql import NameNode, NamedTypeNode, UnionTypeDefinitionNode -from ..base import GraphQLMetadata, GraphQLModel, GraphQLType -from ..description import get_description_node -from ..object_type.graphql_type import GraphQLObject -from .models import GraphQLUnionModel -from .validators import ( +from collections.abc import Iterable, Sequence +from typing import Any, Optional, cast + +from graphql import NamedTypeNode, NameNode, UnionTypeDefinitionNode + +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.object_type.graphql_type import GraphQLObject +from ariadne_graphql_modules.union_type.models import GraphQLUnionModel +from ariadne_graphql_modules.union_type.validators import ( validate_union_type, validate_union_type_with_schema, ) -from ..utils import parse_definition +from ariadne_graphql_modules.utils import parse_definition class GraphQLUnion(GraphQLType): - __types__: Sequence[Type[GraphQLType]] + __types__: Sequence[type[GraphQLType]] __schema__: Optional[str] def __init_subclass__(cls) -> None: @@ -74,7 +76,7 @@ def __get_graphql_model_without_schema__(cls, name: str) -> "GraphQLModel": @classmethod def __get_graphql_types__( cls, _: "GraphQLMetadata" - ) -> Iterable[Type["GraphQLType"]]: + ) -> Iterable[type["GraphQLType"]]: """Returns iterable with GraphQL types associated with this type""" return [cls, *cls.__types__] @@ -84,5 +86,6 @@ def resolve_type(obj: Any, *_) -> str: return obj.__get_graphql_name__() raise ValueError( - f"Cannot resolve GraphQL type {obj} for object of type '{type(obj).__name__}'." + f"Cannot resolve GraphQL type {obj} " + "for object of type '{type(obj).__name__}'." ) diff --git a/ariadne_graphql_modules/union_type/models.py b/ariadne_graphql_modules/union_type/models.py index a5e42dd..32c5d25 100644 --- a/ariadne_graphql_modules/union_type/models.py +++ b/ariadne_graphql_modules/union_type/models.py @@ -3,7 +3,7 @@ from ariadne import UnionType from graphql import GraphQLSchema, GraphQLTypeResolver -from ..base import GraphQLModel +from ariadne_graphql_modules.base import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/union_type/validators.py b/ariadne_graphql_modules/union_type/validators.py index d22aece..7d8d244 100644 --- a/ariadne_graphql_modules/union_type/validators.py +++ b/ariadne_graphql_modules/union_type/validators.py @@ -1,16 +1,15 @@ -from typing import Type, cast, TYPE_CHECKING +from typing import TYPE_CHECKING, cast from graphql import UnionTypeDefinitionNode - -from ..validators import validate_description, validate_name -from ..utils import parse_definition +from ariadne_graphql_modules.utils import parse_definition +from ariadne_graphql_modules.validators import validate_description, validate_name if TYPE_CHECKING: - from .graphql_type import GraphQLUnion + from ariadne_graphql_modules.union_type.graphql_type import GraphQLUnion -def validate_union_type(cls: Type["GraphQLUnion"]) -> None: +def validate_union_type(cls: type["GraphQLUnion"]) -> None: types = getattr(cls, "__types__", None) if not types: raise ValueError( @@ -19,7 +18,7 @@ def validate_union_type(cls: Type["GraphQLUnion"]) -> None: ) -def validate_union_type_with_schema(cls: Type["GraphQLUnion"]) -> None: +def validate_union_type_with_schema(cls: type["GraphQLUnion"]) -> None: definition = cast( UnionTypeDefinitionNode, parse_definition(UnionTypeDefinitionNode, cls.__schema__), @@ -46,12 +45,14 @@ def validate_union_type_with_schema(cls: Type["GraphQLUnion"]) -> None: missing_in_schema = sorted(class_type_names - schema_type_names) missing_in_schema_str = "', '".join(missing_in_schema) raise ValueError( - f"Types '{missing_in_schema_str}' are in '__types__' but not in '__schema__'." + f"Types '{missing_in_schema_str}' are in '__types__' " + "but not in '__schema__'." ) if not schema_type_names.issubset(class_type_names): missing_in_types = sorted(schema_type_names - class_type_names) missing_in_types_str = "', '".join(missing_in_types) raise ValueError( - f"Types '{missing_in_types_str}' are in '__schema__' but not in '__types__'." + f"Types '{missing_in_types_str}' are in '__schema__' " + "but not in '__types__'." ) diff --git a/ariadne_graphql_modules/utils.py b/ariadne_graphql_modules/utils.py index b321687..14b9465 100644 --- a/ariadne_graphql_modules/utils.py +++ b/ariadne_graphql_modules/utils.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from graphql import ( DefinitionNode, diff --git a/ariadne_graphql_modules/validators.py b/ariadne_graphql_modules/validators.py index fee52f2..a266768 100644 --- a/ariadne_graphql_modules/validators.py +++ b/ariadne_graphql_modules/validators.py @@ -1,9 +1,9 @@ -from typing import Any, Type +from typing import Any -from .base import GraphQLType +from ariadne_graphql_modules.base import GraphQLType -def validate_name(cls: Type[GraphQLType], definition: Any): +def validate_name(cls: type[GraphQLType], definition: Any): graphql_name = getattr(cls, "__graphql_name__", None) if graphql_name and definition.name.value != graphql_name: @@ -16,7 +16,7 @@ def validate_name(cls: Type[GraphQLType], definition: Any): setattr(cls, "__graphql_name__", definition.name.value) -def validate_description(cls: Type[GraphQLType], definition: Any): +def validate_description(cls: type[GraphQLType], definition: Any): if getattr(cls, "__description__", None) and definition.description: raise ValueError( f"Class '{cls.__name__}' defines description in both " diff --git a/ariadne_graphql_modules/value.py b/ariadne_graphql_modules/value.py index 5e11d90..2dfbd66 100644 --- a/ariadne_graphql_modules/value.py +++ b/ariadne_graphql_modules/value.py @@ -1,6 +1,7 @@ +from collections.abc import Iterable, Mapping from decimal import Decimal from enum import Enum -from typing import Any, Iterable, Mapping +from typing import Any from graphql import ( BooleanValueNode, diff --git a/pyproject.toml b/pyproject.toml index 7e5c443..8446fa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,9 @@ name = "ariadne-graphql-modules" version = "1.0.0" description = "Ariadne toolkit for defining GraphQL schemas in modular fashion." authors = [{ name = "Mirumee Software", email = "hello@mirumee.com" }] -readme = "README.md" +readme = { file = "README.md", content-type = "text/markdown" } license = { file = "LICENSE" } +requires-python = ">=3.9,<3.13" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -26,15 +27,13 @@ dependencies = ["ariadne==0.23.0", "mkdocs-material"] [project.optional-dependencies] test = [ - "black", - "mypy", - "pylint", "pytest", "pytest-cov", "pytest-asyncio", "pytest-regressions", "pytest-datadir", ] +lint = ["black", "mypy", "pylint", "ruff"] docs = ["mkdocs-material"] [project.urls] @@ -43,38 +42,46 @@ docs = ["mkdocs-material"] "Bug Tracker" = "https://github.com/mirumee/ariadne-graphql-modules/issues" "Community" = "https://github.com/mirumee/ariadne/discussions" "Twitter" = "https://twitter.com/AriadneGraphQL" +"Documentation" = "https://mirumee.github.io/ariadne-graphql-modules/" [tool.hatch.build] include = [ "ariadne_graphql_modules/**/*.py", "ariadne_graphql_modules/py.typed", ] -exclude = ["tests", "tests_v1"] +exclude = [ + "tests", + "tests_v1", + "*.pyc", + "*.pyo", + "__pycache__", + ".git", + ".idea", +] [tool.hatch.envs.default] -features = ["test"] +features = ["test", "lint", "docs"] python = "3.12" -[tool.black] +[tool.ruff] line-length = 88 -target-version = ["py39", "py310", "py311", "py312"] -include = '\.pyi?$' -exclude = ''' -/( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - | snapshots -)/ -''' +target-version = "py39" +exclude = ["tests_v1", "ariadne_graphql_modules/v1"] + +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 80 + +[tool.ruff.lint] +select = ["E", "F", "G", "I", "N", "Q", "UP", "C90", "T20", "TID"] + +[tool.ruff.lint.mccabe] +max-complexity = 30 + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false [tool.pytest.ini_options] -testpaths = ["tests"] +testpaths = ["tests", "tests_v1"] asyncio_mode = "strict" diff --git a/tests/conftest.py b/tests/conftest.py index b8b17ec..3752a68 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ from textwrap import dedent import pytest -from graphql import TypeDefinitionNode, GraphQLSchema, print_ast, print_schema +from graphql import GraphQLSchema, TypeDefinitionNode, print_ast, print_schema from ariadne_graphql_modules import GraphQLMetadata diff --git a/tests/snapshots/test_invalid_resolver_arg_option_default.yml b/tests/snapshots/test_invalid_resolver_arg_option_default.yml new file mode 100644 index 0000000..6fb04ed --- /dev/null +++ b/tests/snapshots/test_invalid_resolver_arg_option_default.yml @@ -0,0 +1,3 @@ +Class 'QueryType' defines default value for 'name' argument of the 'hello' field that + can't be represented in GraphQL schema. +... diff --git a/tests/test_compatibility_layer.py b/tests/test_compatibility_layer.py index ca17d43..c358d58 100644 --- a/tests/test_compatibility_layer.py +++ b/tests/test_compatibility_layer.py @@ -2,18 +2,21 @@ from datetime import date, datetime from graphql import StringValueNode + +from ariadne_graphql_modules.compatibility_layer import wrap_legacy_types +from ariadne_graphql_modules.executable_schema import make_executable_schema from ariadne_graphql_modules.v1.bases import DeferredType from ariadne_graphql_modules.v1.collection_type import CollectionType from ariadne_graphql_modules.v1.enum_type import EnumType from ariadne_graphql_modules.v1.input_type import InputType from ariadne_graphql_modules.v1.interface_type import InterfaceType -from ariadne_graphql_modules.compatibility_layer import wrap_legacy_types -from ariadne_graphql_modules.executable_schema import make_executable_schema from ariadne_graphql_modules.v1.object_type import ObjectType from ariadne_graphql_modules.v1.scalar_type import ScalarType from ariadne_graphql_modules.v1.subscription_type import SubscriptionType from ariadne_graphql_modules.v1.union_type import UnionType +TEST_DATE = date(2006, 9, 13) + def test_object_type( assert_schema_equals, @@ -53,7 +56,7 @@ def resolve_other(*_): @staticmethod def resolve_second_field(obj, *_): - return "Obj: %s" % obj["secondField"] + return "Obj: {}".format(obj["secondField"]) @staticmethod def resolve_field_with_arg(*_, some_arg): @@ -258,8 +261,6 @@ class QueryType(ObjectType): def test_scalar_type(assert_schema_equals): - TEST_DATE = date(2006, 9, 13) - class DateReadOnlyScalar(ScalarType): __schema__ = "scalar DateReadOnly" diff --git a/tests/test_description_node.py b/tests/test_description_node.py index 2acbe07..88d42d2 100644 --- a/tests/test_description_node.py +++ b/tests/test_description_node.py @@ -1,4 +1,5 @@ from graphql import StringValueNode + from ariadne_graphql_modules import get_description_node diff --git a/tests/test_enum_type_validation.py b/tests/test_enum_type_validation.py index 6c11655..2ef8b4a 100644 --- a/tests/test_enum_type_validation.py +++ b/tests/test_enum_type_validation.py @@ -2,8 +2,8 @@ from enum import Enum import pytest - from ariadne import gql + from ariadne_graphql_modules import GraphQLEnum diff --git a/tests/test_input_type.py b/tests/test_input_type.py index dc74d4b..b2a0e0a 100644 --- a/tests/test_input_type.py +++ b/tests/test_input_type.py @@ -1,9 +1,9 @@ from typing import Optional import pytest +from ariadne import gql from graphql import graphql_sync -from ariadne import gql from ariadne_graphql_modules import ( GraphQLInput, GraphQLObject, diff --git a/tests/test_input_type_validation.py b/tests/test_input_type_validation.py index ceab9a0..8b3705f 100644 --- a/tests/test_input_type_validation.py +++ b/tests/test_input_type_validation.py @@ -1,7 +1,7 @@ # pylint: disable=unused-variable import pytest - from ariadne import gql + from ariadne_graphql_modules import GraphQLInput diff --git a/tests/test_interface_type.py b/tests/test_interface_type.py index ceab065..d0b2bf0 100644 --- a/tests/test_interface_type.py +++ b/tests/test_interface_type.py @@ -1,11 +1,11 @@ -from typing import List, Optional, Union +from typing import Optional, Union from graphql import graphql_sync from ariadne_graphql_modules import ( GraphQLID, - GraphQLObject, GraphQLInterface, + GraphQLObject, GraphQLUnion, make_executable_schema, ) @@ -28,9 +28,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), CommentType(id=2, content="Hello World!"), @@ -94,9 +94,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [ UserType(), CommentType(id=2, content="Hello World!"), @@ -169,9 +169,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), CommentType(id=2, content="Hello World!"), @@ -225,7 +225,7 @@ class SuperUserType(UserType): class QueryType(GraphQLObject): @GraphQLObject.field @staticmethod - def users(*_) -> List[UserInterface]: + def users(*_) -> list[UserInterface]: return [ UserType(id="1", username="test_user"), SuperUserType( @@ -331,9 +331,9 @@ class MyType(GraphQLObject, UserInterface): name: str class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[UserInterface]) + @GraphQLObject.field(graphql_type=list[UserInterface]) @staticmethod - def users(*_) -> List[UserInterface]: + def users(*_) -> list[UserInterface]: return [MyType(id="2", name="old", summary="ss", score=22)] schema = make_executable_schema(QueryType, UserType, MyType, UserInterface) @@ -404,9 +404,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [ UserType(name="Bob"), CommentType(id=2, content="Hello World!"), diff --git a/tests/test_interface_type_validation.py b/tests/test_interface_type_validation.py index 1e01d9f..7481bbc 100644 --- a/tests/test_interface_type_validation.py +++ b/tests/test_interface_type_validation.py @@ -1,11 +1,12 @@ # pylint: disable=unused-variable from typing import Optional + import pytest from ariadne_graphql_modules import ( GraphQLID, - GraphQLObject, GraphQLInterface, + GraphQLObject, make_executable_schema, ) @@ -29,9 +30,7 @@ def test_interface_with_schema_object_with_no_schema(data_regression): with pytest.raises(ValueError) as exc_info: class UserInterface(GraphQLInterface): - __schema__: Optional[ - str - ] = """ + __schema__: Optional[str] = """ interface UserInterface { summary: String! score: Int! diff --git a/tests/test_make_executable_schema.py b/tests/test_make_executable_schema.py index 3445e13..02a1fca 100644 --- a/tests/test_make_executable_schema.py +++ b/tests/test_make_executable_schema.py @@ -1,6 +1,7 @@ from typing import Union import pytest +from ariadne import QueryType, SchemaDirectiveVisitor from graphql import ( GraphQLField, GraphQLInterfaceType, @@ -9,7 +10,6 @@ graphql_sync, ) -from ariadne import QueryType, SchemaDirectiveVisitor from ariadne_graphql_modules import GraphQLObject, make_executable_schema @@ -177,7 +177,7 @@ class ThirdRoot(GraphQLObject): def test_schema_validation_fails_if_lazy_type_doesnt_exist(data_regression): class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=list["Missing"]) # type: ignore + @GraphQLObject.field(graphql_type=list["Missing"]) # type: ignore # noqa: F821 @staticmethod def other(*_): return None @@ -190,7 +190,7 @@ def other(*_): def test_schema_validation_passes_if_lazy_type_exists(): class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=list["Exists"]) # type: ignore + @GraphQLObject.field(graphql_type=list["Exists"]) # type: ignore # noqa: F821 @staticmethod def other(*_): return None @@ -213,7 +213,7 @@ def test_make_executable_schema_raises_error_if_called_without_any_types( data_regression.check(str(exc_info.value)) -def test_make_executable_schema_doesnt_set_resolvers_if_convert_names_case_is_not_enabled(): +def test_resolvers_not_set_without_name_case_conversion(): class QueryType(GraphQLObject): other_field: str diff --git a/tests/test_object_type.py b/tests/test_object_type.py index 563d1f7..8046bcf 100644 --- a/tests/test_object_type.py +++ b/tests/test_object_type.py @@ -2,9 +2,9 @@ from typing import Optional import pytest +from ariadne import gql from graphql import graphql_sync -from ariadne import gql from ariadne_graphql_modules import GraphQLObject, make_executable_schema diff --git a/tests/test_object_type_validation.py b/tests/test_object_type_validation.py index 2ec6999..d739802 100644 --- a/tests/test_object_type_validation.py +++ b/tests/test_object_type_validation.py @@ -1,7 +1,7 @@ # pylint: disable=unused-variable import pytest - from ariadne import gql + from ariadne_graphql_modules import GraphQLObject @@ -580,7 +580,7 @@ def resolve_hello(*_, name: str = InvalidType): # type: ignore data_regression.check(str(exc_info.value)) -def test_schema_object_type_validation_fails_for_unsupported_resolver_arg_option_default( +def test_invalid_resolver_arg_option_default( data_regression, ): with pytest.raises(TypeError) as exc_info: diff --git a/tests/test_scalar_type.py b/tests/test_scalar_type.py index 1c386b7..706dd1e 100644 --- a/tests/test_scalar_type.py +++ b/tests/test_scalar_type.py @@ -1,8 +1,8 @@ from datetime import date +from ariadne import gql from graphql import graphql_sync -from ariadne import gql from ariadne_graphql_modules import ( GraphQLObject, GraphQLScalar, diff --git a/tests/test_scalar_type_validation.py b/tests/test_scalar_type_validation.py index c0b1b36..8d25f95 100644 --- a/tests/test_scalar_type_validation.py +++ b/tests/test_scalar_type_validation.py @@ -1,7 +1,7 @@ # pylint: disable=unused-variable import pytest - from ariadne import gql + from ariadne_graphql_modules import GraphQLScalar diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index a9ad793..6c8ed63 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -1,9 +1,9 @@ # pylint: disable=unnecessary-dunder-call -from typing import List -from ariadne import gql -from graphql import subscribe, parse import pytest +from ariadne import gql +from graphql import parse, subscribe + from ariadne_graphql_modules import ( GraphQLID, GraphQLObject, @@ -164,7 +164,6 @@ def search_sth(*_) -> str: @pytest.mark.asyncio async def test_subscription_with_arguments_without_schema(assert_schema_equals): class SubscriptionType(GraphQLSubscription): - @GraphQLSubscription.source( "message_added", args={"channel": GraphQLObject.argument(description="Lorem ipsum.")}, @@ -181,9 +180,7 @@ async def message_added_generator(*_, channel: GraphQLID): @GraphQLSubscription.field @staticmethod - def message_added( - message, *_, channel: GraphQLID - ): # pylint: disable=unused-argument + def message_added(message, *_, channel: GraphQLID): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @@ -257,7 +254,9 @@ async def message_added_generator(*_, channel: GraphQLID): ) @staticmethod async def resolve_message_added( - message, *_, channel: GraphQLID # pylint: disable=unused-argument + message, + *_, + channel: GraphQLID, # pylint: disable=unused-argument ): return message @@ -333,12 +332,12 @@ def search_sth(*_) -> str: @pytest.mark.asyncio async def test_subscription_with_complex_data_without_schema(assert_schema_equals): class SubscriptionType(GraphQLSubscription): - messages_in_channel: List[Message] + messages_in_channel: list[Message] @GraphQLSubscription.source( "messages_in_channel", args={"channel_id": GraphQLObject.argument(description="Lorem ipsum.")}, - graphql_type=List[Message], + graphql_type=list[Message], ) @staticmethod async def message_added_generator(*_, channel_id: GraphQLID): @@ -355,9 +354,7 @@ async def message_added_generator(*_, channel_id: GraphQLID): "messages_in_channel", ) @staticmethod - async def resolve_message_added( - message, *_, channel_id: GraphQLID - ): # pylint: disable=unused-argument + async def resolve_message_added(message, *_, channel_id: GraphQLID): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @@ -574,9 +571,7 @@ async def message_added_generator(*_, channel: GraphQLID): "messageAdded", ) @staticmethod - async def resolve_message_added( - message, *_, channel: GraphQLID - ): # pylint: disable=unused-argument + async def resolve_message_added(message, *_, channel: GraphQLID): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @@ -733,14 +728,12 @@ class SubscriptionType(GraphQLSubscription): "messagesInChannel", ) @staticmethod - async def message_added_generator( - obj, info, channelId: GraphQLID - ): # pylint: disable=unused-argument + async def message_added_generator(*_, channel_id: GraphQLID): while True: yield [ { "id": "some_id", - "content": f"message_{channelId}", + "content": f"message_{channel_id}", "author": "Anon", } ] @@ -749,9 +742,7 @@ async def message_added_generator( "messagesInChannel", ) @staticmethod - async def resolve_message_added( - message, *_, channelId: GraphQLID - ): # pylint: disable=unused-argument + async def resolve_message_added(message, *_, channel_id: GraphQLID): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @@ -760,7 +751,9 @@ class QueryType(GraphQLObject): def search_sth(*_) -> str: return "search" - schema = make_executable_schema(QueryType, SubscriptionType, Message) + schema = make_executable_schema( + QueryType, SubscriptionType, Message, convert_names_case=True + ) assert_schema_equals( schema, @@ -817,7 +810,9 @@ class SubscriptionType(GraphQLSubscription): @GraphQLSubscription.resolver("notificationReceived") @staticmethod async def resolve_message_added( - message, *_, channel: str # pylint: disable=unused-argument + message, + *_, + channel: str, # pylint: disable=unused-argument ): return message @@ -832,7 +827,8 @@ async def resolve_message_added( ) @staticmethod async def message_added_generator( - *_, channel: str # pylint: disable=unused-argument + *_, + channel: str, # pylint: disable=unused-argument ): while True: yield Message(id=1, content="content", author="anon") diff --git a/tests/test_subscription_type_validation.py b/tests/test_subscription_type_validation.py index 22aaf68..4d8ebbb 100644 --- a/tests/test_subscription_type_validation.py +++ b/tests/test_subscription_type_validation.py @@ -1,6 +1,7 @@ # pylint: disable=unused-variable -from ariadne import gql import pytest +from ariadne import gql + from ariadne_graphql_modules import ( GraphQLID, GraphQLObject, diff --git a/tests/test_typing.py b/tests/test_typing.py index 4faa3fc..e7365ea 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,10 +1,10 @@ # pylint: disable=no-member, unsupported-binary-operation -from enum import Enum import sys -from typing import TYPE_CHECKING, Annotated, List, Optional, Union +from enum import Enum +from typing import TYPE_CHECKING, Annotated, Optional, Union -from graphql import ListTypeNode, NameNode, NamedTypeNode, NonNullTypeNode import pytest +from graphql import ListTypeNode, NamedTypeNode, NameNode, NonNullTypeNode from ariadne_graphql_modules import GraphQLObject, deferred, graphql_enum from ariadne_graphql_modules.typing import get_graphql_type, get_type_node @@ -56,8 +56,8 @@ class UserType(GraphQLObject): ... assert get_graphql_type(UserType) == UserType assert get_graphql_type(Optional[UserType]) == UserType - assert get_graphql_type(List[UserType]) == UserType - assert get_graphql_type(Optional[List[Optional[UserType]]]) == UserType + assert get_graphql_type(list[UserType]) == UserType + assert get_graphql_type(Optional[list[Optional[UserType]]]) == UserType def test_get_graphql_type_from_enum_returns_type(): @@ -69,8 +69,8 @@ class UserLevel(Enum): assert get_graphql_type(UserLevel) == UserLevel assert get_graphql_type(Optional[UserLevel]) == UserLevel - assert get_graphql_type(List[UserLevel]) == UserLevel - assert get_graphql_type(Optional[List[Optional[UserLevel]]]) == UserLevel + assert get_graphql_type(list[UserLevel]) == UserLevel + assert get_graphql_type(Optional[list[Optional[UserLevel]]]) == UserLevel def test_get_graphql_type_node_from_python_builtin_type(metadata): @@ -102,10 +102,10 @@ class UserType(GraphQLObject): ... def test_get_graphql_list_type_node_from_python_builtin_type(metadata): - assert_list_type(get_type_node(metadata, Optional[List[str]]), "String") - assert_list_type(get_type_node(metadata, Union[List[int], None]), "Int") + assert_list_type(get_type_node(metadata, Optional[list[str]]), "String") + assert_list_type(get_type_node(metadata, Union[list[int], None]), "Int") - assert_list_type(get_type_node(metadata, Optional[List[bool]]), "Boolean") + assert_list_type(get_type_node(metadata, Optional[list[bool]]), "Boolean") @pytest.mark.skipif( @@ -113,14 +113,14 @@ def test_get_graphql_list_type_node_from_python_builtin_type(metadata): reason="Skip test for Python 3.9", ) def test_get_graphql_list_type_node_from_python_builtin_type_pipe_union(metadata): - assert_list_type(get_type_node(metadata, List[float] | None), "Float") + assert_list_type(get_type_node(metadata, list[float] | None), "Float") def test_get_non_null_graphql_list_type_node_from_python_builtin_type(metadata): - assert_non_null_list_type(get_type_node(metadata, List[str]), "String") - assert_non_null_list_type(get_type_node(metadata, List[int]), "Int") - assert_non_null_list_type(get_type_node(metadata, List[float]), "Float") - assert_non_null_list_type(get_type_node(metadata, List[bool]), "Boolean") + assert_non_null_list_type(get_type_node(metadata, list[str]), "String") + assert_non_null_list_type(get_type_node(metadata, list[int]), "Int") + assert_non_null_list_type(get_type_node(metadata, list[float]), "Float") + assert_non_null_list_type(get_type_node(metadata, list[bool]), "Boolean") def test_get_graphql_type_node_from_annotated_type(metadata): diff --git a/tests/test_union_type.py b/tests/test_union_type.py index 97d3108..5104e9b 100644 --- a/tests/test_union_type.py +++ b/tests/test_union_type.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import Union from graphql import graphql_sync @@ -25,9 +25,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), CommentType(id=2, content="Hello World!"), @@ -88,9 +88,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [] schema = make_executable_schema(QueryType) @@ -121,9 +121,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [ UserType(id=1, username="Bob"), "InvalidType", # type: ignore @@ -161,9 +161,9 @@ class ResultType(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[ResultType]) + @GraphQLObject.field(graphql_type=list[ResultType]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType, InvalidType]]: + def search(*_) -> list[Union[UserType, CommentType, InvalidType]]: return [InvalidType("This should cause an error")] schema = make_executable_schema(QueryType) @@ -192,9 +192,9 @@ class SearchResultUnion(GraphQLUnion): __types__ = [UserType, CommentType] class QueryType(GraphQLObject): - @GraphQLObject.field(graphql_type=List[SearchResultUnion]) + @GraphQLObject.field(graphql_type=list[SearchResultUnion]) @staticmethod - def search(*_) -> List[Union[UserType, CommentType]]: + def search(*_) -> list[Union[UserType, CommentType]]: return [ UserType(id="1", username="Alice"), CommentType(id="2", content="Test post"), diff --git a/tests/test_union_type_validation.py b/tests/test_union_type_validation.py index 99235a8..d54c4ff 100644 --- a/tests/test_union_type_validation.py +++ b/tests/test_union_type_validation.py @@ -1,4 +1,5 @@ import pytest + from ariadne_graphql_modules import ( GraphQLID, GraphQLObject, From 33bdb2ad6ef90fa975e306cd814d462b8eba9ac4 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 5 Sep 2024 12:40:29 +0200 Subject: [PATCH 58/63] add github page deployment --- .github/workflows/deploy_docs.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/deploy_docs.yml diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 0000000..d7623f4 --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,27 @@ +name: Deploy documentation + +on: + push: + branches: + - main + +jobs: + docs-publish: + name: publish documentation + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + + - name: Install Hatch + uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc + + - name: Build documentation + run: hatch run mkdocs gh-deploy --force From 4c922de4ba5409ca98bab39fe5d2c4d439855511 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 5 Sep 2024 12:43:31 +0200 Subject: [PATCH 59/63] fix test workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c45e236..b3fbcef 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[test, lint] + pip install -e .[test,lint] - name: Run Ruff run: hatch run ruff . From f41eb63c711f6d58a3ac0789385fc60dd37c62b9 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 5 Sep 2024 12:44:41 +0200 Subject: [PATCH 60/63] fix ruff in workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b3fbcef..ac1b1b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: pip install -e .[test,lint] - name: Run Ruff - run: hatch run ruff . + run: hatch run ruff check . - name: Run Black run: hatch run black --check . From 151a7338bff7a0aee394a64be8b86663a5009b24 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 5 Sep 2024 12:46:46 +0200 Subject: [PATCH 61/63] remove hatch from tests workflow --- .github/workflows/tests.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac1b1b1..485dfbf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,16 +27,16 @@ jobs: pip install -e .[test,lint] - name: Run Ruff - run: hatch run ruff check . + run: ruff check . - name: Run Black - run: hatch run black --check . + run: black --check . - name: Run MyPy - run: hatch run mypy . + run: mypy ariadne_graphql_modules --ignore-missing-imports - name: Run Pylint - run: hatch run pylint ariadne_graphql_modules + run: pylint ariadne_graphql_modules tests - name: Run Pytest - run: hatch run pytest --cov=ariadne_graphql_modules + run: pytest From 92d44a9dc62afd01bb4b512b7fb1bdb9c85cbf7b Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 5 Sep 2024 13:24:00 +0200 Subject: [PATCH 62/63] fix circular import --- ariadne_graphql_modules/__init__.py | 3 +- ariadne_graphql_modules/base.py | 19 +--- ariadne_graphql_modules/base_graphql_model.py | 13 +++ .../base_object_type/graphql_type.py | 9 +- .../compatibility_layer.py | 3 +- ariadne_graphql_modules/enum_type/__init__.py | 6 +- .../enum_type/enum_model_utils.py | 104 ++++++++++++++++++ .../enum_type/graphql_type.py | 100 +---------------- ariadne_graphql_modules/enum_type/models.py | 2 +- ariadne_graphql_modules/executable_schema.py | 3 +- .../input_type/graphql_type.py | 3 +- ariadne_graphql_modules/input_type/models.py | 2 +- .../interface_type/graphql_type.py | 3 +- .../interface_type/models.py | 2 +- .../object_type/graphql_type.py | 3 +- ariadne_graphql_modules/object_type/models.py | 2 +- .../scalar_type/graphql_type.py | 3 +- ariadne_graphql_modules/scalar_type/models.py | 2 +- .../subscription_type/graphql_type.py | 9 +- .../subscription_type/models.py | 2 +- .../union_type/graphql_type.py | 3 +- ariadne_graphql_modules/union_type/models.py | 2 +- tests/test_interface_type_validation.py | 4 +- tests/test_subscription_type.py | 16 ++- 24 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 ariadne_graphql_modules/base_graphql_model.py create mode 100644 ariadne_graphql_modules/enum_type/enum_model_utils.py diff --git a/ariadne_graphql_modules/__init__.py b/ariadne_graphql_modules/__init__.py index 906aa5a..04cc176 100644 --- a/ariadne_graphql_modules/__init__.py +++ b/ariadne_graphql_modules/__init__.py @@ -1,4 +1,5 @@ -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLType +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.convert_name import ( convert_graphql_name_to_python, convert_python_name_to_graphql, diff --git a/ariadne_graphql_modules/base.py b/ariadne_graphql_modules/base.py index 8c1670b..9ef0145 100644 --- a/ariadne_graphql_modules/base.py +++ b/ariadne_graphql_modules/base.py @@ -3,7 +3,7 @@ from enum import Enum from typing import Any, Optional, Union -from graphql import GraphQLSchema, TypeDefinitionNode +from ariadne_graphql_modules.base_graphql_model import GraphQLModel class GraphQLType: @@ -35,7 +35,7 @@ def __get_graphql_name__(cls) -> str: return name @classmethod - def __get_graphql_model__(cls, metadata: "GraphQLMetadata") -> "GraphQLModel": + def __get_graphql_model__(cls, metadata: "GraphQLMetadata") -> GraphQLModel: raise NotImplementedError( "Subclasses of 'GraphQLType' must define '__get_graphql_model__'" ) @@ -48,16 +48,6 @@ def __get_graphql_types__( return [cls] -@dataclass(frozen=True) -class GraphQLModel: - name: str - ast: TypeDefinitionNode - ast_type: type[TypeDefinitionNode] - - def bind_to_schema(self, schema: GraphQLSchema): - pass - - @dataclass(frozen=True) class GraphQLMetadata: data: dict[Union[type[GraphQLType], type[Enum]], Any] = field(default_factory=dict) @@ -85,7 +75,10 @@ def get_graphql_model( if hasattr(graphql_type, "__get_graphql_model__"): self.models[graphql_type] = graphql_type.__get_graphql_model__(self) elif issubclass(graphql_type, Enum): - from ariadne_graphql_modules.enum_type import create_graphql_enum_model + # pylint: disable=import-outside-toplevel + from ariadne_graphql_modules.enum_type.enum_model_utils import ( + create_graphql_enum_model, + ) self.models[graphql_type] = create_graphql_enum_model(graphql_type) else: diff --git a/ariadne_graphql_modules/base_graphql_model.py b/ariadne_graphql_modules/base_graphql_model.py new file mode 100644 index 0000000..30ffa80 --- /dev/null +++ b/ariadne_graphql_modules/base_graphql_model.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + +from graphql import GraphQLSchema, TypeDefinitionNode + + +@dataclass(frozen=True) +class GraphQLModel: + name: str + ast: TypeDefinitionNode + ast_type: type[TypeDefinitionNode] + + def bind_to_schema(self, schema: GraphQLSchema): + pass diff --git a/ariadne_graphql_modules/base_object_type/graphql_type.py b/ariadne_graphql_modules/base_object_type/graphql_type.py index 4a78118..4b31f0d 100644 --- a/ariadne_graphql_modules/base_object_type/graphql_type.py +++ b/ariadne_graphql_modules/base_object_type/graphql_type.py @@ -14,7 +14,8 @@ StringValueNode, ) -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLType +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.base_object_type.graphql_field import ( GraphQLClassData, GraphQLFieldData, @@ -115,9 +116,9 @@ def _create_fields_and_resolvers_with_schema( for arg_name, arg_options in final_args.items(): arg_description = get_description_node(arg_options.description) if arg_description: - args_descriptions[cls_attr.field][arg_name] = ( - arg_description - ) + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description if arg_options.default_value is not None: args_defaults[cls_attr.field][arg_name] = get_value_node( diff --git a/ariadne_graphql_modules/compatibility_layer.py b/ariadne_graphql_modules/compatibility_layer.py index 1d3e117..cf2cffa 100644 --- a/ariadne_graphql_modules/compatibility_layer.py +++ b/ariadne_graphql_modules/compatibility_layer.py @@ -21,7 +21,8 @@ GraphQLSubscriptionModel, GraphQLUnionModel, ) -from ariadne_graphql_modules.base import GraphQLModel, GraphQLType +from ariadne_graphql_modules.base import GraphQLType +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.v1.bases import BaseType, BindableType from ariadne_graphql_modules.v1.directive_type import DirectiveType from ariadne_graphql_modules.v1.enum_type import EnumType diff --git a/ariadne_graphql_modules/enum_type/__init__.py b/ariadne_graphql_modules/enum_type/__init__.py index 4bf34ec..f0c3d80 100644 --- a/ariadne_graphql_modules/enum_type/__init__.py +++ b/ariadne_graphql_modules/enum_type/__init__.py @@ -1,8 +1,10 @@ -from ariadne_graphql_modules.enum_type.graphql_type import ( - GraphQLEnum, +from ariadne_graphql_modules.enum_type.enum_model_utils import ( create_graphql_enum_model, graphql_enum, ) +from ariadne_graphql_modules.enum_type.graphql_type import ( + GraphQLEnum, +) from ariadne_graphql_modules.enum_type.models import GraphQLEnumModel __all__ = [ diff --git a/ariadne_graphql_modules/enum_type/enum_model_utils.py b/ariadne_graphql_modules/enum_type/enum_model_utils.py new file mode 100644 index 0000000..23b69b2 --- /dev/null +++ b/ariadne_graphql_modules/enum_type/enum_model_utils.py @@ -0,0 +1,104 @@ +from collections.abc import Iterable +from enum import Enum +from typing import Any, Optional, cast + +from graphql import EnumTypeDefinitionNode, EnumValueDefinitionNode, NameNode + +from ariadne_graphql_modules.description import get_description_node +from ariadne_graphql_modules.enum_type.models import GraphQLEnumModel + + +def create_graphql_enum_model( # noqa: C901 + enum: type[Enum], + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +) -> "GraphQLEnumModel": + if members_include and members_exclude: + raise ValueError( + "'members_include' and 'members_exclude' options are mutually exclusive." + ) + + if hasattr(enum, "__get_graphql_model__"): + return cast(GraphQLEnumModel, enum.__get_graphql_model__()) + + if not name: + if hasattr(enum, "__get_graphql_name__"): + name = cast("str", enum.__get_graphql_name__()) + else: + name = enum.__name__ + + members: dict[str, Any] = {i.name: i for i in enum} + final_members: dict[str, Any] = {} + + if members_include: + for key, value in members.items(): + if key in members_include: + final_members[key] = value + elif members_exclude: + for key, value in members.items(): + if key not in members_exclude: + final_members[key] = value + else: + final_members = members + + members_descriptions = members_descriptions or {} + for member in members_descriptions: + if member not in final_members: + raise ValueError( + f"Member description was specified for a member '{member}' " + "not present in final GraphQL enum." + ) + + return GraphQLEnumModel( + name=name, + members=final_members, + ast_type=EnumTypeDefinitionNode, + ast=EnumTypeDefinitionNode( + name=NameNode(value=name), + description=get_description_node(description), + values=tuple( + EnumValueDefinitionNode( + name=NameNode(value=value_name), + description=get_description_node( + members_descriptions.get(value_name) + ), + ) + for value_name in final_members + ), + ), + ) + + +def graphql_enum( + cls=None, + *, + name: Optional[str] = None, + description: Optional[str] = None, + members_descriptions: Optional[dict[str, str]] = None, + members_include: Optional[Iterable[str]] = None, + members_exclude: Optional[Iterable[str]] = None, +): + def graphql_enum_decorator(cls): + graphql_model = create_graphql_enum_model( + cls, + name=name, + description=description, + members_descriptions=members_descriptions, + members_include=members_include, + members_exclude=members_exclude, + ) + + def __get_graphql_model__(*_) -> GraphQLEnumModel: # noqa: N807 + return graphql_model + + setattr(cls, "__get_graphql_model__", classmethod(__get_graphql_model__)) + return cls + + if cls: + return graphql_enum_decorator(cls) + + return graphql_enum_decorator diff --git a/ariadne_graphql_modules/enum_type/graphql_type.py b/ariadne_graphql_modules/enum_type/graphql_type.py index 3f7c764..702432f 100644 --- a/ariadne_graphql_modules/enum_type/graphql_type.py +++ b/ariadne_graphql_modules/enum_type/graphql_type.py @@ -1,4 +1,3 @@ -from collections.abc import Iterable from enum import Enum from inspect import isclass from typing import Any, Optional, Union, cast @@ -141,8 +140,7 @@ def _validate_enum_type_with_schema(cls): validate_description(cls, definition) members_names = { - value.name.value - for value in definition.values # pylint: disable=no-member + value.name.value for value in definition.values # pylint: disable=no-member } if not members_names: raise ValueError( @@ -251,99 +249,3 @@ def get_members_set( f"Expected members to be of type Dict[str, Any], List[str], or Enum." f"Got {type(members).__name__} instead." ) - - -def create_graphql_enum_model( # noqa: C901 - enum: type[Enum], - *, - name: Optional[str] = None, - description: Optional[str] = None, - members_descriptions: Optional[dict[str, str]] = None, - members_include: Optional[Iterable[str]] = None, - members_exclude: Optional[Iterable[str]] = None, -) -> "GraphQLEnumModel": - if members_include and members_exclude: - raise ValueError( - "'members_include' and 'members_exclude' options are mutually exclusive." - ) - - if hasattr(enum, "__get_graphql_model__"): - return cast(GraphQLEnumModel, enum.__get_graphql_model__()) - - if not name: - if hasattr(enum, "__get_graphql_name__"): - name = cast("str", enum.__get_graphql_name__()) - else: - name = enum.__name__ - - members: dict[str, Any] = {i.name: i for i in enum} - final_members: dict[str, Any] = {} - - if members_include: - for key, value in members.items(): - if key in members_include: - final_members[key] = value - elif members_exclude: - for key, value in members.items(): - if key not in members_exclude: - final_members[key] = value - else: - final_members = members - - members_descriptions = members_descriptions or {} - for member in members_descriptions: - if member not in final_members: - raise ValueError( - f"Member description was specified for a member '{member}' " - "not present in final GraphQL enum." - ) - - return GraphQLEnumModel( - name=name, - members=final_members, - ast_type=EnumTypeDefinitionNode, - ast=EnumTypeDefinitionNode( - name=NameNode(value=name), - description=get_description_node(description), - values=tuple( - EnumValueDefinitionNode( - name=NameNode(value=value_name), - description=get_description_node( - members_descriptions.get(value_name) - ), - ) - for value_name in final_members - ), - ), - ) - - -def graphql_enum( - cls=None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - members_descriptions: Optional[dict[str, str]] = None, - members_include: Optional[Iterable[str]] = None, - members_exclude: Optional[Iterable[str]] = None, -): - def graphql_enum_decorator(cls): - graphql_model = create_graphql_enum_model( - cls, - name=name, - description=description, - members_descriptions=members_descriptions, - members_include=members_include, - members_exclude=members_exclude, - ) - - def __get_graphql_model__(*_) -> GraphQLEnumModel: # noqa: N807 - return graphql_model - - setattr(cls, "__get_graphql_model__", classmethod(__get_graphql_model__)) - return cls - - if cls: - return graphql_enum_decorator(cls) - - return graphql_enum_decorator diff --git a/ariadne_graphql_modules/enum_type/models.py b/ariadne_graphql_modules/enum_type/models.py index 2b30e93..6ccbd08 100644 --- a/ariadne_graphql_modules/enum_type/models.py +++ b/ariadne_graphql_modules/enum_type/models.py @@ -4,7 +4,7 @@ from ariadne import EnumType from graphql import GraphQLSchema -from ariadne_graphql_modules.base import GraphQLModel +from ariadne_graphql_modules.base_graphql_model import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/executable_schema.py b/ariadne_graphql_modules/executable_schema.py index f2bae53..fa40b99 100644 --- a/ariadne_graphql_modules/executable_schema.py +++ b/ariadne_graphql_modules/executable_schema.py @@ -19,7 +19,8 @@ parse, ) -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLType +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.roots import ROOTS_NAMES, merge_root_nodes from ariadne_graphql_modules.sort import sort_schema_document diff --git a/ariadne_graphql_modules/input_type/graphql_type.py b/ariadne_graphql_modules/input_type/graphql_type.py index 14496aa..4057898 100644 --- a/ariadne_graphql_modules/input_type/graphql_type.py +++ b/ariadne_graphql_modules/input_type/graphql_type.py @@ -5,7 +5,8 @@ from graphql import InputObjectTypeDefinitionNode, InputValueDefinitionNode, NameNode -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLType +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.convert_name import ( convert_graphql_name_to_python, convert_python_name_to_graphql, diff --git a/ariadne_graphql_modules/input_type/models.py b/ariadne_graphql_modules/input_type/models.py index 5e21480..bcffa56 100644 --- a/ariadne_graphql_modules/input_type/models.py +++ b/ariadne_graphql_modules/input_type/models.py @@ -4,7 +4,7 @@ from ariadne import InputType as InputTypeBindable from graphql import GraphQLSchema -from ariadne_graphql_modules.base import GraphQLModel +from ariadne_graphql_modules.base_graphql_model import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/interface_type/graphql_type.py b/ariadne_graphql_modules/interface_type/graphql_type.py index e06b90a..a1da0e5 100644 --- a/ariadne_graphql_modules/interface_type/graphql_type.py +++ b/ariadne_graphql_modules/interface_type/graphql_type.py @@ -8,7 +8,8 @@ NameNode, ) -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel +from ariadne_graphql_modules.base import GraphQLMetadata +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.base_object_type import ( GraphQLBaseObject, GraphQLFieldData, diff --git a/ariadne_graphql_modules/interface_type/models.py b/ariadne_graphql_modules/interface_type/models.py index 153f36b..41f0270 100644 --- a/ariadne_graphql_modules/interface_type/models.py +++ b/ariadne_graphql_modules/interface_type/models.py @@ -5,7 +5,7 @@ from ariadne.types import Resolver from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLTypeResolver -from ariadne_graphql_modules.base import GraphQLModel +from ariadne_graphql_modules.base_graphql_model import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/object_type/graphql_type.py b/ariadne_graphql_modules/object_type/graphql_type.py index c7918a0..4bffd68 100644 --- a/ariadne_graphql_modules/object_type/graphql_type.py +++ b/ariadne_graphql_modules/object_type/graphql_type.py @@ -11,7 +11,8 @@ ObjectTypeDefinitionNode, ) -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel +from ariadne_graphql_modules.base import GraphQLMetadata +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.base_object_type import ( GraphQLBaseObject, GraphQLFieldData, diff --git a/ariadne_graphql_modules/object_type/models.py b/ariadne_graphql_modules/object_type/models.py index eb4832e..b58052e 100644 --- a/ariadne_graphql_modules/object_type/models.py +++ b/ariadne_graphql_modules/object_type/models.py @@ -5,7 +5,7 @@ from ariadne.types import Resolver from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema -from ariadne_graphql_modules.base import GraphQLModel +from ariadne_graphql_modules.base_graphql_model import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/scalar_type/graphql_type.py b/ariadne_graphql_modules/scalar_type/graphql_type.py index 449c6da..dc8d417 100644 --- a/ariadne_graphql_modules/scalar_type/graphql_type.py +++ b/ariadne_graphql_modules/scalar_type/graphql_type.py @@ -7,7 +7,8 @@ value_from_ast_untyped, ) -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLType +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.description import get_description_node from ariadne_graphql_modules.scalar_type.models import GraphQLScalarModel from ariadne_graphql_modules.scalar_type.validators import ( diff --git a/ariadne_graphql_modules/scalar_type/models.py b/ariadne_graphql_modules/scalar_type/models.py index a544ef9..8cf07d8 100644 --- a/ariadne_graphql_modules/scalar_type/models.py +++ b/ariadne_graphql_modules/scalar_type/models.py @@ -9,7 +9,7 @@ GraphQLSchema, ) -from ariadne_graphql_modules.base import GraphQLModel +from ariadne_graphql_modules.base_graphql_model import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/subscription_type/graphql_type.py b/ariadne_graphql_modules/subscription_type/graphql_type.py index 94127cc..94aa075 100644 --- a/ariadne_graphql_modules/subscription_type/graphql_type.py +++ b/ariadne_graphql_modules/subscription_type/graphql_type.py @@ -9,7 +9,8 @@ StringValueNode, ) -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel +from ariadne_graphql_modules.base import GraphQLMetadata +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.base_object_type import ( GraphQLBaseObject, GraphQLFieldData, @@ -100,9 +101,9 @@ def __get_graphql_model_with_schema__(cls) -> "GraphQLModel": # noqa: C901 for arg_name, arg_options in final_args.items(): arg_description = get_description_node(arg_options.description) if arg_description: - args_descriptions[cls_attr.field][arg_name] = ( - arg_description - ) + args_descriptions[cls_attr.field][ + arg_name + ] = arg_description arg_default = arg_options.default_value if arg_default is not None: diff --git a/ariadne_graphql_modules/subscription_type/models.py b/ariadne_graphql_modules/subscription_type/models.py index a3f6809..8d491b9 100644 --- a/ariadne_graphql_modules/subscription_type/models.py +++ b/ariadne_graphql_modules/subscription_type/models.py @@ -5,7 +5,7 @@ from ariadne.types import Resolver, Subscriber from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema -from ariadne_graphql_modules.base import GraphQLModel +from ariadne_graphql_modules.base_graphql_model import GraphQLModel @dataclass(frozen=True) diff --git a/ariadne_graphql_modules/union_type/graphql_type.py b/ariadne_graphql_modules/union_type/graphql_type.py index d6003aa..51fa7fc 100644 --- a/ariadne_graphql_modules/union_type/graphql_type.py +++ b/ariadne_graphql_modules/union_type/graphql_type.py @@ -3,7 +3,8 @@ from graphql import NamedTypeNode, NameNode, UnionTypeDefinitionNode -from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLModel, GraphQLType +from ariadne_graphql_modules.base import GraphQLMetadata, GraphQLType +from ariadne_graphql_modules.base_graphql_model import GraphQLModel from ariadne_graphql_modules.description import get_description_node from ariadne_graphql_modules.object_type.graphql_type import GraphQLObject from ariadne_graphql_modules.union_type.models import GraphQLUnionModel diff --git a/ariadne_graphql_modules/union_type/models.py b/ariadne_graphql_modules/union_type/models.py index 32c5d25..51860d0 100644 --- a/ariadne_graphql_modules/union_type/models.py +++ b/ariadne_graphql_modules/union_type/models.py @@ -3,7 +3,7 @@ from ariadne import UnionType from graphql import GraphQLSchema, GraphQLTypeResolver -from ariadne_graphql_modules.base import GraphQLModel +from ariadne_graphql_modules.base_graphql_model import GraphQLModel @dataclass(frozen=True) diff --git a/tests/test_interface_type_validation.py b/tests/test_interface_type_validation.py index 7481bbc..2ba5684 100644 --- a/tests/test_interface_type_validation.py +++ b/tests/test_interface_type_validation.py @@ -30,7 +30,9 @@ def test_interface_with_schema_object_with_no_schema(data_regression): with pytest.raises(ValueError) as exc_info: class UserInterface(GraphQLInterface): - __schema__: Optional[str] = """ + __schema__: Optional[ + str + ] = """ interface UserInterface { summary: String! score: Int! diff --git a/tests/test_subscription_type.py b/tests/test_subscription_type.py index 6c8ed63..0e82871 100644 --- a/tests/test_subscription_type.py +++ b/tests/test_subscription_type.py @@ -180,7 +180,9 @@ async def message_added_generator(*_, channel: GraphQLID): @GraphQLSubscription.field @staticmethod - def message_added(message, *_, channel: GraphQLID): # pylint: disable=unused-argument + def message_added( + message, *_, channel: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @@ -354,7 +356,9 @@ async def message_added_generator(*_, channel_id: GraphQLID): "messages_in_channel", ) @staticmethod - async def resolve_message_added(message, *_, channel_id: GraphQLID): # pylint: disable=unused-argument + async def resolve_message_added( + message, *_, channel_id: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @@ -571,7 +575,9 @@ async def message_added_generator(*_, channel: GraphQLID): "messageAdded", ) @staticmethod - async def resolve_message_added(message, *_, channel: GraphQLID): # pylint: disable=unused-argument + async def resolve_message_added( + message, *_, channel: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): @@ -742,7 +748,9 @@ async def message_added_generator(*_, channel_id: GraphQLID): "messagesInChannel", ) @staticmethod - async def resolve_message_added(message, *_, channel_id: GraphQLID): # pylint: disable=unused-argument + async def resolve_message_added( + message, *_, channel_id: GraphQLID + ): # pylint: disable=unused-argument return message class QueryType(GraphQLObject): From 8374a61a8d62c8935d4bca4fb933fa6eb988d2ec Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Mon, 9 Sep 2024 11:58:21 +0200 Subject: [PATCH 63/63] change version to dev --- docs/changelog.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 5559ba9..1fbb546 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,7 +1,7 @@ # CHANGELOG -## 1.0.0 (UNREALEASED) +## 1.0.0 (DEV RELEASE) - Major API Redesign: The entire API has been restructured for better modularity and flexibility. - New Type System: Introduced a new type system, replacing the old v1 types. diff --git a/pyproject.toml b/pyproject.toml index 8446fa7..053c3b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ariadne-graphql-modules" -version = "1.0.0" +version = "1.0.0.dev1" description = "Ariadne toolkit for defining GraphQL schemas in modular fashion." authors = [{ name = "Mirumee Software", email = "hello@mirumee.com" }] readme = { file = "README.md", content-type = "text/markdown" }