Skip to content

Commit

Permalink
Use typed dictionaries for introspection results (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cito committed Dec 28, 2021
1 parent e3d6871 commit 70bfee0
Show file tree
Hide file tree
Showing 8 changed files with 391 additions and 151 deletions.
3 changes: 3 additions & 0 deletions docs/modules/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/graphql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -700,6 +701,7 @@
"GraphQLSyntaxError",
"located_error",
"get_introspection_query",
"IntrospectionQuery",
"get_operation_ast",
"get_operation_root_type",
"introspection_from_schema",
Expand Down
3 changes: 2 additions & 1 deletion src/graphql/utilities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,6 +86,7 @@
"BreakingChangeType",
"DangerousChange",
"DangerousChangeType",
"IntrospectionQuery",
"TypeInfo",
"TypeInfoVisitor",
"assert_valid_name",
Expand Down
136 changes: 88 additions & 48 deletions src/graphql/utilities/build_client_schema.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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)}.")
Expand All @@ -93,36 +110,42 @@ 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"),
specified_by_url=scalar_introspection.get("specifiedByURL"),
)

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:
Expand All @@ -131,40 +154,46 @@ 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"),
interfaces=lambda: build_implementations_list(object_introspection),
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"),
interfaces=lambda: build_implementations_list(interface_introspection),
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:"
Expand All @@ -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(
Expand All @@ -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}."
Expand All @@ -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,"
Expand All @@ -242,27 +274,30 @@ 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,"
f" but received: {inspect(type_)}."
)
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_,
Expand All @@ -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(
Expand All @@ -281,20 +316,23 @@ 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,"
f" but received: {inspect(type_)}."
)
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_,
Expand All @@ -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:"
Expand Down
Loading

0 comments on commit 70bfee0

Please sign in to comment.