From 24506f31ed1dbd104d0f5481965d0e0814f127f1 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Wed, 29 Dec 2021 00:12:12 +0100 Subject: [PATCH] Use typed dictionaries for introspection results (#99) --- docs/modules/utilities.rst | 3 + src/graphql/__init__.py | 2 + src/graphql/utilities/__init__.py | 3 +- src/graphql/utilities/build_client_schema.py | 136 +++++++---- .../utilities/get_introspection_query.py | 167 +++++++++++++- .../utilities/introspection_from_schema.py | 11 +- tests/utilities/test_build_client_schema.py | 216 ++++++++++-------- .../test_introspection_from_schema.py | 5 +- 8 files changed, 392 insertions(+), 151 deletions(-) diff --git a/docs/modules/utilities.rst b/docs/modules/utilities.rst index d53836aa..aa624d94 100644 --- a/docs/modules/utilities.rst +++ b/docs/modules/utilities.rst @@ -11,6 +11,9 @@ The GraphQL query recommended for a full schema introspection: .. autofunction:: get_introspection_query +.. autoclass:: IntrospectionQuery + :no-inherited-members: + Get the target Operation from a Document: .. autofunction:: get_operation_ast diff --git a/src/graphql/__init__.py b/src/graphql/__init__.py index b6d7c3f1..b1314dfb 100644 --- a/src/graphql/__init__.py +++ b/src/graphql/__init__.py @@ -164,6 +164,7 @@ # Produce the GraphQL query recommended for a full schema introspection. # Accepts optional IntrospectionOptions. get_introspection_query, + IntrospectionQuery, # Get the target Operation from a Document. get_operation_ast, # Get the Type for the target Operation AST. @@ -700,6 +701,7 @@ "GraphQLSyntaxError", "located_error", "get_introspection_query", + "IntrospectionQuery", "get_operation_ast", "get_operation_root_type", "introspection_from_schema", diff --git a/src/graphql/utilities/__init__.py b/src/graphql/utilities/__init__.py index 6611c641..efb84852 100644 --- a/src/graphql/utilities/__init__.py +++ b/src/graphql/utilities/__init__.py @@ -5,7 +5,7 @@ """ # Produce the GraphQL query recommended for a full schema introspection. -from .get_introspection_query import get_introspection_query +from .get_introspection_query import get_introspection_query, IntrospectionQuery # Get the target Operation from a Document. from .get_operation_ast import get_operation_ast @@ -86,6 +86,7 @@ "BreakingChangeType", "DangerousChange", "DangerousChangeType", + "IntrospectionQuery", "TypeInfo", "TypeInfoVisitor", "assert_valid_name", diff --git a/src/graphql/utilities/build_client_schema.py b/src/graphql/utilities/build_client_schema.py index ff6e8fa9..b9ad32ed 100644 --- a/src/graphql/utilities/build_client_schema.py +++ b/src/graphql/utilities/build_client_schema.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import cast, Callable, Collection, Dict, List +from typing import cast, Callable, Collection, Dict, List, Union from ..language import DirectiveLocation, parse_value from ..pyutils import inspect, Undefined @@ -31,13 +31,27 @@ is_output_type, specified_scalar_types, ) +from .get_introspection_query import ( + IntrospectionDirective, + IntrospectionEnumType, + IntrospectionField, + IntrospectionInterfaceType, + IntrospectionInputObjectType, + IntrospectionInputValue, + IntrospectionObjectType, + IntrospectionQuery, + IntrospectionScalarType, + IntrospectionType, + IntrospectionTypeRef, + IntrospectionUnionType, +) from .value_from_ast import value_from_ast __all__ = ["build_client_schema"] def build_client_schema( - introspection: Dict, assume_valid: bool = False + introspection: IntrospectionQuery, assume_valid: bool = False ) -> GraphQLSchema: """Build a GraphQLSchema for use by client tools. @@ -64,22 +78,25 @@ def build_client_schema( # Given a type reference in introspection, return the GraphQLType instance, # preferring cached instances before building new instances. - def get_type(type_ref: Dict) -> GraphQLType: + def get_type(type_ref: IntrospectionTypeRef) -> GraphQLType: kind = type_ref.get("kind") if kind == TypeKind.LIST.name: item_ref = type_ref.get("ofType") if not item_ref: raise TypeError("Decorated type deeper than introspection query.") + item_ref = cast(IntrospectionTypeRef, item_ref) return GraphQLList(get_type(item_ref)) - elif kind == TypeKind.NON_NULL.name: + if kind == TypeKind.NON_NULL.name: nullable_ref = type_ref.get("ofType") if not nullable_ref: raise TypeError("Decorated type deeper than introspection query.") + nullable_ref = cast(IntrospectionTypeRef, nullable_ref) nullable_type = get_type(nullable_ref) return GraphQLNonNull(assert_nullable_type(nullable_type)) + type_ref = cast(IntrospectionType, type_ref) return get_named_type(type_ref) - def get_named_type(type_ref: Dict) -> GraphQLNamedType: + def get_named_type(type_ref: IntrospectionType) -> GraphQLNamedType: type_name = type_ref.get("name") if not type_name: raise TypeError(f"Unknown type reference: {inspect(type_ref)}.") @@ -93,25 +110,29 @@ def get_named_type(type_ref: Dict) -> GraphQLNamedType: ) return type_ - def get_object_type(type_ref: Dict) -> GraphQLObjectType: + def get_object_type(type_ref: IntrospectionObjectType) -> GraphQLObjectType: return assert_object_type(get_type(type_ref)) - def get_interface_type(type_ref: Dict) -> GraphQLInterfaceType: + def get_interface_type( + type_ref: IntrospectionInterfaceType, + ) -> GraphQLInterfaceType: return assert_interface_type(get_type(type_ref)) # Given a type's introspection result, construct the correct GraphQLType instance. - def build_type(type_: Dict) -> GraphQLNamedType: + def build_type(type_: IntrospectionType) -> GraphQLNamedType: if type_ and "name" in type_ and "kind" in type_: - builder = type_builders.get(cast(str, type_["kind"])) + builder = type_builders.get(type_["kind"]) if builder: # pragma: no cover else - return cast(GraphQLNamedType, builder(type_)) + return builder(type_) raise TypeError( "Invalid or incomplete introspection result." " Ensure that a full introspection query is used in order" f" to build a client schema: {inspect(type_)}." ) - def build_scalar_def(scalar_introspection: Dict) -> GraphQLScalarType: + def build_scalar_def( + scalar_introspection: IntrospectionScalarType, + ) -> GraphQLScalarType: return GraphQLScalarType( name=scalar_introspection["name"], description=scalar_introspection.get("description"), @@ -119,10 +140,12 @@ def build_scalar_def(scalar_introspection: Dict) -> GraphQLScalarType: ) def build_implementations_list( - implementing_introspection: Dict, + implementing_introspection: Union[ + IntrospectionObjectType, IntrospectionInterfaceType + ], ) -> List[GraphQLInterfaceType]: - interfaces = implementing_introspection.get("interfaces") - if interfaces is None: + maybe_interfaces = implementing_introspection.get("interfaces") + if maybe_interfaces is None: # Temporary workaround until GraphQL ecosystem will fully support # 'interfaces' on interface types if implementing_introspection["kind"] == TypeKind.INTERFACE.name: @@ -131,9 +154,12 @@ def build_implementations_list( "Introspection result missing interfaces:" f" {inspect(implementing_introspection)}." ) + interfaces = cast(Collection[IntrospectionInterfaceType], maybe_interfaces) return [get_interface_type(interface) for interface in interfaces] - def build_object_def(object_introspection: Dict) -> GraphQLObjectType: + def build_object_def( + object_introspection: IntrospectionObjectType, + ) -> GraphQLObjectType: return GraphQLObjectType( name=object_introspection["name"], description=object_introspection.get("description"), @@ -141,7 +167,9 @@ def build_object_def(object_introspection: Dict) -> GraphQLObjectType: fields=lambda: build_field_def_map(object_introspection), ) - def build_interface_def(interface_introspection: Dict) -> GraphQLInterfaceType: + def build_interface_def( + interface_introspection: IntrospectionInterfaceType, + ) -> GraphQLInterfaceType: return GraphQLInterfaceType( name=interface_introspection["name"], description=interface_introspection.get("description"), @@ -149,22 +177,23 @@ def build_interface_def(interface_introspection: Dict) -> GraphQLInterfaceType: fields=lambda: build_field_def_map(interface_introspection), ) - def build_union_def(union_introspection: Dict) -> GraphQLUnionType: - possible_types = union_introspection.get("possibleTypes") - if possible_types is None: + def build_union_def( + union_introspection: IntrospectionUnionType, + ) -> GraphQLUnionType: + maybe_possible_types = union_introspection.get("possibleTypes") + if maybe_possible_types is None: raise TypeError( "Introspection result missing possibleTypes:" f" {inspect(union_introspection)}." ) + possible_types = cast(Collection[IntrospectionObjectType], maybe_possible_types) return GraphQLUnionType( name=union_introspection["name"], description=union_introspection.get("description"), - types=lambda: [ - get_object_type(type_) for type_ in cast(List[Dict], possible_types) - ], + types=lambda: [get_object_type(type_) for type_ in possible_types], ) - def build_enum_def(enum_introspection: Dict) -> GraphQLEnumType: + def build_enum_def(enum_introspection: IntrospectionEnumType) -> GraphQLEnumType: if enum_introspection.get("enumValues") is None: raise TypeError( "Introspection result missing enumValues:" @@ -184,7 +213,7 @@ def build_enum_def(enum_introspection: Dict) -> GraphQLEnumType: ) def build_input_object_def( - input_object_introspection: Dict, + input_object_introspection: IntrospectionInputObjectType, ) -> GraphQLInputObjectType: if input_object_introspection.get("inputFields") is None: raise TypeError( @@ -199,16 +228,18 @@ def build_input_object_def( ), ) - type_builders: Dict[str, Callable[[Dict], GraphQLType]] = { - TypeKind.SCALAR.name: build_scalar_def, - TypeKind.OBJECT.name: build_object_def, - TypeKind.INTERFACE.name: build_interface_def, - TypeKind.UNION.name: build_union_def, - TypeKind.ENUM.name: build_enum_def, - TypeKind.INPUT_OBJECT.name: build_input_object_def, + type_builders: Dict[str, Callable[[IntrospectionType], GraphQLNamedType]] = { + TypeKind.SCALAR.name: build_scalar_def, # type: ignore + TypeKind.OBJECT.name: build_object_def, # type: ignore + TypeKind.INTERFACE.name: build_interface_def, # type: ignore + TypeKind.UNION.name: build_union_def, # type: ignore + TypeKind.ENUM.name: build_enum_def, # type: ignore + TypeKind.INPUT_OBJECT.name: build_input_object_def, # type: ignore } - def build_field_def_map(type_introspection: Dict) -> Dict[str, GraphQLField]: + def build_field_def_map( + type_introspection: Union[IntrospectionObjectType, IntrospectionInterfaceType], + ) -> Dict[str, GraphQLField]: if type_introspection.get("fields") is None: raise TypeError( f"Introspection result missing fields: {type_introspection}." @@ -218,8 +249,9 @@ def build_field_def_map(type_introspection: Dict) -> Dict[str, GraphQLField]: for field_introspection in type_introspection["fields"] } - def build_field(field_introspection: Dict) -> GraphQLField: - type_ = get_type(field_introspection["type"]) + def build_field(field_introspection: IntrospectionField) -> GraphQLField: + type_introspection = cast(IntrospectionType, field_introspection["type"]) + type_ = get_type(type_introspection) if not is_output_type(type_): raise TypeError( "Introspection must provide output type for fields," @@ -242,15 +274,18 @@ def build_field(field_introspection: Dict) -> GraphQLField: ) def build_argument_def_map( - input_value_introspections: Dict, + argument_value_introspections: Collection[IntrospectionInputValue], ) -> Dict[str, GraphQLArgument]: return { argument_introspection["name"]: build_argument(argument_introspection) - for argument_introspection in input_value_introspections + for argument_introspection in argument_value_introspections } - def build_argument(argument_introspection: Dict) -> GraphQLArgument: - type_ = get_type(argument_introspection["type"]) + def build_argument( + argument_introspection: IntrospectionInputValue, + ) -> GraphQLArgument: + type_introspection = cast(IntrospectionType, argument_introspection["type"]) + type_ = get_type(type_introspection) if not is_input_type(type_): raise TypeError( "Introspection must provide input type for arguments," @@ -258,11 +293,11 @@ def build_argument(argument_introspection: Dict) -> GraphQLArgument: ) type_ = cast(GraphQLInputType, type_) - default_value = argument_introspection.get("defaultValue") + default_value_introspection = argument_introspection.get("defaultValue") default_value = ( Undefined - if default_value is None - else value_from_ast(parse_value(default_value), type_) + if default_value_introspection is None + else value_from_ast(parse_value(default_value_introspection), type_) ) return GraphQLArgument( type_, @@ -272,7 +307,7 @@ def build_argument(argument_introspection: Dict) -> GraphQLArgument: ) def build_input_value_def_map( - input_value_introspections: Dict, + input_value_introspections: Collection[IntrospectionInputValue], ) -> Dict[str, GraphQLInputField]: return { input_value_introspection["name"]: build_input_value( @@ -281,8 +316,11 @@ def build_input_value_def_map( for input_value_introspection in input_value_introspections } - def build_input_value(input_value_introspection: Dict) -> GraphQLInputField: - type_ = get_type(input_value_introspection["type"]) + def build_input_value( + input_value_introspection: IntrospectionInputValue, + ) -> GraphQLInputField: + type_introspection = cast(IntrospectionType, input_value_introspection["type"]) + type_ = get_type(type_introspection) if not is_input_type(type_): raise TypeError( "Introspection must provide input type for input fields," @@ -290,11 +328,11 @@ def build_input_value(input_value_introspection: Dict) -> GraphQLInputField: ) type_ = cast(GraphQLInputType, type_) - default_value = input_value_introspection.get("defaultValue") + default_value_introspection = input_value_introspection.get("defaultValue") default_value = ( Undefined - if default_value is None - else value_from_ast(parse_value(default_value), type_) + if default_value_introspection is None + else value_from_ast(parse_value(default_value_introspection), type_) ) return GraphQLInputField( type_, @@ -303,7 +341,9 @@ def build_input_value(input_value_introspection: Dict) -> GraphQLInputField: deprecation_reason=input_value_introspection.get("deprecationReason"), ) - def build_directive(directive_introspection: Dict) -> GraphQLDirective: + def build_directive( + directive_introspection: IntrospectionDirective, + ) -> GraphQLDirective: if directive_introspection.get("args") is None: raise TypeError( "Introspection result missing directive args:" diff --git a/src/graphql/utilities/get_introspection_query.py b/src/graphql/utilities/get_introspection_query.py index 700043e2..aed2348a 100644 --- a/src/graphql/utilities/get_introspection_query.py +++ b/src/graphql/utilities/get_introspection_query.py @@ -1,7 +1,31 @@ from textwrap import dedent -from typing import Optional +from typing import Any, Dict, List, Optional, Union -__all__ = ["get_introspection_query"] +from ..language import DirectiveLocation + +try: + from typing import TypedDict, Literal +except ImportError: # Python < 3.8 + from typing_extensions import TypedDict, Literal # type: ignore + +__all__ = [ + "get_introspection_query", + "IntrospectionDirective", + "IntrospectionEnumType", + "IntrospectionField", + "IntrospectionInputObjectType", + "IntrospectionInputValue", + "IntrospectionInterfaceType", + "IntrospectionListType", + "IntrospectionNonNullType", + "IntrospectionObjectType", + "IntrospectionQuery", + "IntrospectionScalarType", + "IntrospectionSchema", + "IntrospectionType", + "IntrospectionTypeRef", + "IntrospectionUnionType", +] def get_introspection_query( @@ -125,3 +149,142 @@ def input_deprecation(string: str) -> Optional[str]: }} """ ) + + +# Unfortunately, the following type definitions are a bit simplistic +# because of current restrictions in the typing system (mypy): +# - no recursion, see https://github.com/python/mypy/issues/731 +# - no generic typed dicts, see https://github.com/python/mypy/issues/3863 + +# simplified IntrospectionNamedType to avoids cycles +SimpleIntrospectionType = Dict[str, Any] + + +class MaybeWithDescription(TypedDict, total=False): + description: Optional[str] + + +class WithName(MaybeWithDescription): + name: str + + +class MaybeWithSpecifiedByUrl(TypedDict, total=False): + specifiedByURL: Optional[str] + + +class WithDeprecated(TypedDict): + isDeprecated: bool + deprecationReason: Optional[str] + + +class MaybeWithDeprecated(TypedDict, total=False): + isDeprecated: bool + deprecationReason: Optional[str] + + +class IntrospectionInputValue(WithName, MaybeWithDeprecated): + type: SimpleIntrospectionType # should be IntrospectionInputType + defaultValue: Optional[str] + + +class IntrospectionField(WithName, WithDeprecated): + args: List[IntrospectionInputValue] + type: SimpleIntrospectionType # should be IntrospectionOutputType + + +class IntrospectionEnumValue(WithName, WithDeprecated): + pass + + +class MaybeWithIsRepeatable(TypedDict, total=False): + isRepeatable: bool + + +class IntrospectionDirective(WithName, MaybeWithIsRepeatable): + locations: List[DirectiveLocation] + args: List[IntrospectionInputValue] + + +class IntrospectionScalarType(WithName, MaybeWithSpecifiedByUrl): + kind: Literal["scalar"] + + +class IntrospectionInterfaceType(WithName): + kind: Literal["interface"] + fields: List[IntrospectionField] + interfaces: List[SimpleIntrospectionType] # should be InterfaceType + possibleTypes: List[SimpleIntrospectionType] # should be NamedType + + +class IntrospectionObjectType(WithName): + kind: Literal["object"] + fields: List[IntrospectionField] + interfaces: List[SimpleIntrospectionType] # should be InterfaceType + + +class IntrospectionUnionType(WithName): + kind: Literal["union"] + possibleTypes: List[SimpleIntrospectionType] # should be NamedType + + +class IntrospectionEnumType(WithName): + kind: Literal["enum"] + enumValues: List[IntrospectionEnumValue] + + +class IntrospectionInputObjectType(WithName): + kind: Literal["input_object"] + inputFields: List[IntrospectionInputValue] + + +IntrospectionType = Union[ + IntrospectionScalarType, + IntrospectionObjectType, + IntrospectionInterfaceType, + IntrospectionUnionType, + IntrospectionEnumType, + IntrospectionInputObjectType, +] + + +IntrospectionOutputType = Union[ + IntrospectionScalarType, + IntrospectionObjectType, + IntrospectionInterfaceType, + IntrospectionUnionType, + IntrospectionEnumType, +] + + +IntrospectionInputType = Union[ + IntrospectionScalarType, IntrospectionEnumType, IntrospectionInputObjectType +] + + +class IntrospectionListType(TypedDict): + kind: Literal["list"] + ofType: SimpleIntrospectionType # should be IntrospectionType + + +class IntrospectionNonNullType(TypedDict): + kind: Literal["non_null"] + ofType: SimpleIntrospectionType # should be IntrospectionType + + +IntrospectionTypeRef = Union[ + IntrospectionType, IntrospectionListType, IntrospectionNonNullType +] + + +class IntrospectionSchema(MaybeWithDescription): + queryType: IntrospectionObjectType + mutationType: Optional[IntrospectionObjectType] + subscriptionType: Optional[IntrospectionObjectType] + types: List[IntrospectionType] + directives: List[IntrospectionDirective] + + +class IntrospectionQuery(TypedDict): + """The root typed dictionary for schema introspections.""" + + __schema: IntrospectionSchema diff --git a/src/graphql/utilities/introspection_from_schema.py b/src/graphql/utilities/introspection_from_schema.py index f9c316ed..e0634860 100644 --- a/src/graphql/utilities/introspection_from_schema.py +++ b/src/graphql/utilities/introspection_from_schema.py @@ -1,16 +1,13 @@ -from typing import Any, Dict +from typing import cast from ..error import GraphQLError from ..language import parse from ..type import GraphQLSchema -from .get_introspection_query import get_introspection_query +from .get_introspection_query import get_introspection_query, IntrospectionQuery __all__ = ["introspection_from_schema"] -IntrospectionSchema = Dict[str, Any] - - def introspection_from_schema( schema: GraphQLSchema, descriptions: bool = True, @@ -18,7 +15,7 @@ def introspection_from_schema( directive_is_repeatable: bool = True, schema_description: bool = True, input_value_deprecation: bool = True, -) -> IntrospectionSchema: +) -> IntrospectionQuery: """Build an IntrospectionQuery from a GraphQLSchema IntrospectionQuery is useful for utilities that care about type and field @@ -46,4 +43,4 @@ def introspection_from_schema( raise result.errors[0] if not result.data: # pragma: no cover raise GraphQLError("Introspection did not return a result") - return result.data + return cast(IntrospectionQuery, result.data) diff --git a/tests/utilities/test_build_client_schema.py b/tests/utilities/test_build_client_schema.py index c350776e..b566ba1b 100644 --- a/tests/utilities/test_build_client_schema.py +++ b/tests/utilities/test_build_client_schema.py @@ -1,3 +1,5 @@ +from typing import cast + from pytest import raises from graphql import graphql_sync @@ -21,11 +23,19 @@ introspection_from_schema, print_schema, ) +from graphql.utilities.get_introspection_query import ( + IntrospectionEnumType, + IntrospectionInputObjectType, + IntrospectionInterfaceType, + IntrospectionObjectType, + IntrospectionType, + IntrospectionUnionType, +) from ..utils import dedent -def cycle_introspection(sdl_string): +def cycle_introspection(sdl_string: str): """Test that the client side introspection gives the same result. This function does a full cycle of going from a string with the contents of the SDL, @@ -75,7 +85,7 @@ def builds_a_schema_without_the_query_type(): schema = build_schema(sdl) introspection = introspection_from_schema(schema) - del introspection["__schema"]["queryType"] + del introspection["__schema"]["queryType"] # type: ignore client_schema = build_client_schema(introspection) assert client_schema.query_type is None @@ -483,7 +493,7 @@ def builds_a_schema_without_directives(): schema = build_schema(sdl) introspection = introspection_from_schema(schema) - del introspection["__schema"]["directives"] + del introspection["__schema"]["directives"] # type: ignore client_schema = build_client_schema(introspection) @@ -653,7 +663,7 @@ def throws_when_introspection_is_missing_schema_property(): with raises(TypeError) as exc_info: # noinspection PyTypeChecker - build_client_schema({}) + build_client_schema({}) # type: ignore assert str(exc_info.value) == ( "Invalid or incomplete introspection result. Ensure that you" @@ -705,9 +715,9 @@ def throws_when_missing_definition_for_one_of_the_standard_scalars(): def throws_when_type_reference_is_missing_name(): introspection = introspection_from_schema(dummy_schema) - - assert introspection["__schema"]["queryType"]["name"] == "Query" - del introspection["__schema"]["queryType"]["name"] + query_type = cast(IntrospectionType, introspection["__schema"]["queryType"]) + assert query_type["name"] == "Query" + del query_type["name"] # type: ignore with raises(TypeError) as exc_info: build_client_schema(introspection) @@ -716,7 +726,6 @@ def throws_when_type_reference_is_missing_name(): def throws_when_missing_kind(): introspection = introspection_from_schema(dummy_schema) - query_type_introspection = next( type_ for type_ in introspection["__schema"]["types"] @@ -735,14 +744,17 @@ def throws_when_missing_kind(): def throws_when_missing_interfaces(): introspection = introspection_from_schema(dummy_schema) - - query_type_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "Query" + query_type_introspection = cast( + IntrospectionObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "Query" + ), ) + assert query_type_introspection["interfaces"] == [] - del query_type_introspection["interfaces"] + del query_type_introspection["interfaces"] # type: ignore with raises( TypeError, @@ -754,28 +766,34 @@ def throws_when_missing_interfaces(): def legacy_support_for_interfaces_with_null_as_interfaces_field(): introspection = introspection_from_schema(dummy_schema) - some_interface_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "SomeInterface" + some_interface_introspection = cast( + IntrospectionInterfaceType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "SomeInterface" + ), ) assert some_interface_introspection["interfaces"] == [] - some_interface_introspection["interfaces"] = None + some_interface_introspection["interfaces"] = None # type: ignore client_schema = build_client_schema(introspection) assert print_schema(client_schema) == print_schema(dummy_schema) def throws_when_missing_fields(): introspection = introspection_from_schema(dummy_schema) - query_type_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "Query" + query_type_introspection = cast( + IntrospectionObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "Query" + ), ) assert query_type_introspection["fields"] - del query_type_introspection["fields"] + del query_type_introspection["fields"] # type: ignore with raises( TypeError, @@ -787,14 +805,18 @@ def throws_when_missing_fields(): def throws_when_missing_field_args(): introspection = introspection_from_schema(dummy_schema) - - query_type_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "Query" + query_type_introspection = cast( + IntrospectionObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "Query" + ), ) - assert query_type_introspection["fields"][0]["args"] - del query_type_introspection["fields"][0]["args"] + + field = query_type_introspection["fields"][0] + assert field["args"] + del field["args"] # type: ignore with raises( TypeError, @@ -805,19 +827,18 @@ def throws_when_missing_field_args(): def throws_when_output_type_is_used_as_an_arg_type(): introspection = introspection_from_schema(dummy_schema) - - query_type_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "Query" - ) - assert ( - query_type_introspection["fields"][0]["args"][0]["type"]["name"] - == "String" + query_type_introspection = cast( + IntrospectionObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "Query" + ), ) - query_type_introspection["fields"][0]["args"][0]["type"][ - "name" - ] = "SomeUnion" + + arg = query_type_introspection["fields"][0]["args"][0] + assert arg["type"]["name"] == "String" + arg["type"]["name"] = "SomeUnion" with raises(TypeError) as exc_info: build_client_schema(introspection) @@ -830,19 +851,18 @@ def throws_when_output_type_is_used_as_an_arg_type(): def throws_when_output_type_is_used_as_an_input_value_type(): introspection = introspection_from_schema(dummy_schema) - - input_object_type_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "SomeInputObject" - ) - assert ( - input_object_type_introspection["inputFields"][0]["type"]["name"] - == "String" + input_object_type_introspection = cast( + IntrospectionInputObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "SomeInputObject" + ), ) - input_object_type_introspection["inputFields"][0]["type"][ - "name" - ] = "SomeUnion" + + input_field = input_object_type_introspection["inputFields"][0] + assert input_field["type"]["name"] == "String" + input_field["type"]["name"] = "SomeUnion" with raises(TypeError) as exc_info: build_client_schema(introspection) @@ -855,14 +875,18 @@ def throws_when_output_type_is_used_as_an_input_value_type(): def throws_when_input_type_is_used_as_a_field_type(): introspection = introspection_from_schema(dummy_schema) - - query_type_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "Query" + query_type_introspection = cast( + IntrospectionObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "Query" + ), ) - assert query_type_introspection["fields"][0]["type"]["name"] == "String" - query_type_introspection["fields"][0]["type"]["name"] = "SomeInputObject" + + field = query_type_introspection["fields"][0] + assert field["type"]["name"] == "String" + field["type"]["name"] = "SomeInputObject" with raises(TypeError) as exc_info: build_client_schema(introspection) @@ -875,14 +899,17 @@ def throws_when_input_type_is_used_as_a_field_type(): def throws_when_missing_possible_types(): introspection = introspection_from_schema(dummy_schema) - - some_union_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "SomeUnion" + some_union_introspection = cast( + IntrospectionUnionType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "SomeUnion" + ), ) + assert some_union_introspection["possibleTypes"] - del some_union_introspection["possibleTypes"] + del some_union_introspection["possibleTypes"] # type: ignore with raises( TypeError, @@ -893,14 +920,17 @@ def throws_when_missing_possible_types(): def throws_when_missing_enum_values(): introspection = introspection_from_schema(dummy_schema) - - some_enum_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "SomeEnum" + some_enum_introspection = cast( + IntrospectionEnumType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "SomeEnum" + ), ) + assert some_enum_introspection["enumValues"] - del some_enum_introspection["enumValues"] + del some_enum_introspection["enumValues"] # type: ignore with raises( TypeError, @@ -911,14 +941,17 @@ def throws_when_missing_enum_values(): def throws_when_missing_input_fields(): introspection = introspection_from_schema(dummy_schema) - - some_input_object_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "SomeInputObject" + some_input_object_introspection = cast( + IntrospectionInputObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "SomeInputObject" + ), ) + assert some_input_object_introspection["inputFields"] - del some_input_object_introspection["inputFields"] + del some_input_object_introspection["inputFields"] # type: ignore with raises( TypeError, @@ -929,11 +962,11 @@ def throws_when_missing_input_fields(): def throws_when_missing_directive_locations(): introspection = introspection_from_schema(dummy_schema) - some_directive_introspection = introspection["__schema"]["directives"][0] + assert some_directive_introspection["name"] == "SomeDirective" assert some_directive_introspection["locations"] == ["QUERY"] - del some_directive_introspection["locations"] + del some_directive_introspection["locations"] # type: ignore with raises( TypeError, @@ -944,11 +977,11 @@ def throws_when_missing_directive_locations(): def throws_when_missing_directive_args(): introspection = introspection_from_schema(dummy_schema) - some_directive_introspection = introspection["__schema"]["directives"][0] + assert some_directive_introspection["name"] == "SomeDirective" assert some_directive_introspection["args"] == [] - del some_directive_introspection["args"] + del some_directive_introspection["args"] # type: ignore with raises( TypeError, @@ -1021,12 +1054,15 @@ def recursive_interfaces(): """ schema = build_schema(sdl, assume_valid=True) introspection = introspection_from_schema(schema) - - foo_introspection = next( - type_ - for type_ in introspection["__schema"]["types"] - if type_["name"] == "Foo" + foo_introspection = cast( + IntrospectionObjectType, + next( + type_ + for type_ in introspection["__schema"]["types"] + if type_["name"] == "Foo" + ), ) + assert foo_introspection["interfaces"] == [] # we need to patch here since invalid interfaces cannot be built with Python foo_introspection["interfaces"] = [ @@ -1050,12 +1086,12 @@ def recursive_union(): """ schema = build_schema(sdl, assume_valid=True) introspection = introspection_from_schema(schema) - foo_introspection = next( type_ for type_ in introspection["__schema"]["types"] if type_["name"] == "Foo" ) + assert foo_introspection["kind"] == "UNION" assert foo_introspection["possibleTypes"] == [] # we need to patch here since invalid unions cannot be built with Python diff --git a/tests/utilities/test_introspection_from_schema.py b/tests/utilities/test_introspection_from_schema.py index 300d2bdd..d06788c0 100644 --- a/tests/utilities/test_introspection_from_schema.py +++ b/tests/utilities/test_introspection_from_schema.py @@ -1,16 +1,15 @@ -from typing import Dict - from graphql.type import GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLString from graphql.utilities import ( build_client_schema, print_schema, introspection_from_schema, + IntrospectionQuery, ) from ..utils import dedent -def introspection_to_sdl(introspection: Dict) -> str: +def introspection_to_sdl(introspection: IntrospectionQuery) -> str: return print_schema(build_client_schema(introspection))