2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,124 @@
"""GraphQL Utilities
The :mod:`graphql.utilities` package contains common useful computations to use with
the GraphQL language and type objects.
"""
# Produce the GraphQL query recommended for a full schema introspection.
from .get_introspection_query import get_introspection_query, IntrospectionQuery
# Get the target Operation from a Document.
from .get_operation_ast import get_operation_ast
# Get the Type for the target Operation AST.
from .get_operation_root_type import get_operation_root_type
# Convert a GraphQLSchema to an IntrospectionQuery.
from .introspection_from_schema import introspection_from_schema
# Build a GraphQLSchema from an introspection result.
from .build_client_schema import build_client_schema
# Build a GraphQLSchema from GraphQL Schema language.
from .build_ast_schema import build_ast_schema, build_schema
# Extend an existing GraphQLSchema from a parsed GraphQL Schema language AST.
from .extend_schema import extend_schema
# Sort a GraphQLSchema.
from .lexicographic_sort_schema import lexicographic_sort_schema
# Print a GraphQLSchema to GraphQL Schema language.
from .print_schema import (
print_introspection_schema,
print_schema,
print_type,
print_value, # deprecated
)
# Create a GraphQLType from a GraphQL language AST.
from .type_from_ast import type_from_ast
# Convert a language AST to a dictionary.
from .ast_to_dict import ast_to_dict
# Create a Python value from a GraphQL language AST with a type.
from .value_from_ast import value_from_ast
# Create a Python value from a GraphQL language AST without a type.
from .value_from_ast_untyped import value_from_ast_untyped
# Create a GraphQL language AST from a Python value.
from .ast_from_value import ast_from_value
# A helper to use within recursive-descent visitors which need to be aware of
# the GraphQL type system
from .type_info import TypeInfo, TypeInfoVisitor
# Coerce a Python value to a GraphQL type, or produce errors.
from .coerce_input_value import coerce_input_value
# Concatenate multiple ASTs together.
from .concat_ast import concat_ast
# Separate an AST into an AST per Operation.
from .separate_operations import separate_operations
# Strip characters that are not significant to the validity or execution
# of a GraphQL document.
from .strip_ignored_characters import strip_ignored_characters
# Comparators for types
from .type_comparators import is_equal_type, is_type_sub_type_of, do_types_overlap
# Assert that a string is a valid GraphQL name.
from .assert_valid_name import assert_valid_name, is_valid_name_error
# Compare two GraphQLSchemas and detect breaking changes.
from .find_breaking_changes import (
BreakingChange,
BreakingChangeType,
DangerousChange,
DangerousChangeType,
find_breaking_changes,
find_dangerous_changes,
)
__all__ = [
"BreakingChange",
"BreakingChangeType",
"DangerousChange",
"DangerousChangeType",
"IntrospectionQuery",
"TypeInfo",
"TypeInfoVisitor",
"assert_valid_name",
"ast_from_value",
"ast_to_dict",
"build_ast_schema",
"build_client_schema",
"build_schema",
"coerce_input_value",
"concat_ast",
"do_types_overlap",
"extend_schema",
"find_breaking_changes",
"find_dangerous_changes",
"get_introspection_query",
"get_operation_ast",
"get_operation_root_type",
"is_equal_type",
"is_type_sub_type_of",
"is_valid_name_error",
"introspection_from_schema",
"lexicographic_sort_schema",
"print_introspection_schema",
"print_schema",
"print_type",
"print_value",
"separate_operations",
"strip_ignored_characters",
"type_from_ast",
"value_from_ast",
"value_from_ast_untyped",
]
@@ -0,0 +1,38 @@
from typing import Optional
from ..type.assert_name import assert_name
from ..error import GraphQLError
__all__ = ["assert_valid_name", "is_valid_name_error"]
def assert_valid_name(name: str) -> str:
"""Uphold the spec rules about naming.
.. deprecated:: 3.2
Please use ``assert_name`` instead. Will be removed in v3.3.
"""
error = is_valid_name_error(name)
if error:
raise error
return name
def is_valid_name_error(name: str) -> Optional[GraphQLError]:
"""Return an Error if a name is invalid.
.. deprecated:: 3.2
Please use ``assert_name`` instead. Will be removed in v3.3.
"""
if not isinstance(name, str):
raise TypeError("Expected name to be a string.")
if name.startswith("__"):
return GraphQLError(
f"Name {name!r} must not begin with '__',"
" which is reserved by GraphQL introspection."
)
try:
assert_name(name)
except GraphQLError as error:
return error
return None
@@ -0,0 +1,139 @@
import re
from math import isfinite
from typing import Any, Mapping, Optional, cast
from ..language import (
BooleanValueNode,
EnumValueNode,
FloatValueNode,
IntValueNode,
ListValueNode,
NameNode,
NullValueNode,
ObjectFieldNode,
ObjectValueNode,
StringValueNode,
ValueNode,
)
from ..pyutils import inspect, is_iterable, Undefined
from ..type import (
GraphQLID,
GraphQLInputType,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
is_enum_type,
is_input_object_type,
is_leaf_type,
is_list_type,
is_non_null_type,
)
__all__ = ["ast_from_value"]
_re_integer_string = re.compile("^-?(?:0|[1-9][0-9]*)$")
def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]:
"""Produce a GraphQL Value AST given a Python object.
This function will match Python/JSON values to GraphQL AST schema format by using
the suggested GraphQLInputType. For example::
ast_from_value('value', GraphQLString)
A GraphQL type must be provided, which will be used to interpret different Python
values.
================ =======================
JSON Value GraphQL Value
================ =======================
Object Input Object
Array List
Boolean Boolean
String String / Enum Value
Number Int / Float
Mixed Enum Value
null NullValue
================ =======================
"""
if is_non_null_type(type_):
type_ = cast(GraphQLNonNull, type_)
ast_value = ast_from_value(value, type_.of_type)
if isinstance(ast_value, NullValueNode):
return None
return ast_value
# only explicit None, not Undefined or NaN
if value is None:
return NullValueNode()
# undefined
if value is Undefined:
return None
# Convert Python list to GraphQL list. If the GraphQLType is a list, but the value
# is not a list, convert the value using the list's item type.
if is_list_type(type_):
type_ = cast(GraphQLList, type_)
item_type = type_.of_type
if is_iterable(value):
maybe_value_nodes = (ast_from_value(item, item_type) for item in value)
value_nodes = tuple(node for node in maybe_value_nodes if node)
return ListValueNode(values=value_nodes)
return ast_from_value(value, item_type)
# Populate the fields of the input object by creating ASTs from each value in the
# Python dict according to the fields in the input type.
if is_input_object_type(type_):
if value is None or not isinstance(value, Mapping):
return None
type_ = cast(GraphQLInputObjectType, type_)
field_items = (
(field_name, ast_from_value(value[field_name], field.type))
for field_name, field in type_.fields.items()
if field_name in value
)
field_nodes = tuple(
ObjectFieldNode(name=NameNode(value=field_name), value=field_value)
for field_name, field_value in field_items
if field_value
)
return ObjectValueNode(fields=field_nodes)
if is_leaf_type(type_):
# Since value is an internally represented value, it must be serialized to an
# externally represented value before converting into an AST.
serialized = type_.serialize(value) # type: ignore
if serialized is None or serialized is Undefined:
return None
# Others serialize based on their corresponding Python scalar types.
if isinstance(serialized, bool):
return BooleanValueNode(value=serialized)
# Python ints and floats correspond nicely to Int and Float values.
if isinstance(serialized, int):
return IntValueNode(value=str(serialized))
if isinstance(serialized, float) and isfinite(serialized):
value = str(serialized)
if value.endswith(".0"):
value = value[:-2]
return FloatValueNode(value=value)
if isinstance(serialized, str):
# Enum types use Enum literals.
if is_enum_type(type_):
return EnumValueNode(value=serialized)
# ID types can use Int literals.
if type_ is GraphQLID and _re_integer_string.match(serialized):
return IntValueNode(value=serialized)
return StringValueNode(value=serialized)
raise TypeError(f"Cannot convert value to AST: {inspect(serialized)}.")
# Not reachable. All possible input types have been considered.
raise TypeError(f"Unexpected input type: {inspect(type_)}.")
@@ -0,0 +1,59 @@
from typing import Any, Collection, Dict, List, Optional, overload
from ..language import Node, OperationType
from ..pyutils import is_iterable
__all__ = ["ast_to_dict"]
@overload
def ast_to_dict(
node: Node, locations: bool = False, cache: Optional[Dict[Node, Any]] = None
) -> Dict: ...
@overload
def ast_to_dict(
node: Collection[Node],
locations: bool = False,
cache: Optional[Dict[Node, Any]] = None,
) -> List[Node]: ...
@overload
def ast_to_dict(
node: OperationType,
locations: bool = False,
cache: Optional[Dict[Node, Any]] = None,
) -> str: ...
def ast_to_dict(
node: Any, locations: bool = False, cache: Optional[Dict[Node, Any]] = None
) -> Any:
"""Convert a language AST to a nested Python dictionary.
Set `locations` to True in order to get the locations as well.
"""
if isinstance(node, Node):
if cache is None:
cache = {}
elif node in cache:
return cache[node]
cache[node] = res = {}
res.update(
{
key: ast_to_dict(getattr(node, key), locations, cache)
for key in ("kind",) + node.keys[1:]
}
)
if locations:
loc = node.loc
if loc:
res["loc"] = dict(start=loc.start, end=loc.end)
return res
if is_iterable(node):
return [ast_to_dict(sub_node, locations, cache) for sub_node in node]
if isinstance(node, OperationType):
return node.value
return node
@@ -0,0 +1,103 @@
from typing import cast, Union
from ..language import DocumentNode, Source, parse
from ..type import (
GraphQLObjectType,
GraphQLSchema,
GraphQLSchemaKwargs,
specified_directives,
)
from .extend_schema import extend_schema_impl
__all__ = [
"build_ast_schema",
"build_schema",
]
def build_ast_schema(
document_ast: DocumentNode,
assume_valid: bool = False,
assume_valid_sdl: bool = False,
) -> GraphQLSchema:
"""Build a GraphQL Schema from a given AST.
This takes the ast of a schema document produced by the parse function in
src/language/parser.py.
If no schema definition is provided, then it will look for types named Query,
Mutation and Subscription.
Given that AST it constructs a GraphQLSchema. The resulting schema has no
resolve methods, so execution will use default resolvers.
When building a schema from a GraphQL service's introspection result, it might
be safe to assume the schema is valid. Set ``assume_valid`` to ``True`` to assume
the produced schema is valid. Set ``assume_valid_sdl`` to ``True`` to assume it is
already a valid SDL document.
"""
if not isinstance(document_ast, DocumentNode):
raise TypeError("Must provide valid Document AST.")
if not (assume_valid or assume_valid_sdl):
from ..validation.validate import assert_valid_sdl
assert_valid_sdl(document_ast)
empty_schema_kwargs = GraphQLSchemaKwargs(
query=None,
mutation=None,
subscription=None,
description=None,
types=(),
directives=(),
extensions={},
ast_node=None,
extension_ast_nodes=(),
assume_valid=False,
)
schema_kwargs = extend_schema_impl(empty_schema_kwargs, document_ast, assume_valid)
if not schema_kwargs["ast_node"]:
for type_ in schema_kwargs["types"] or ():
# Note: While this could make early assertions to get the correctly
# typed values below, that would throw immediately while type system
# validation with validate_schema() will produce more actionable results.
type_name = type_.name
if type_name == "Query":
schema_kwargs["query"] = cast(GraphQLObjectType, type_)
elif type_name == "Mutation":
schema_kwargs["mutation"] = cast(GraphQLObjectType, type_)
elif type_name == "Subscription":
schema_kwargs["subscription"] = cast(GraphQLObjectType, type_)
# If specified directives were not explicitly declared, add them.
directives = schema_kwargs["directives"]
directive_names = set(directive.name for directive in directives)
missing_directives = []
for directive in specified_directives:
if directive.name not in directive_names:
missing_directives.append(directive)
if missing_directives:
schema_kwargs["directives"] = directives + tuple(missing_directives)
return GraphQLSchema(**schema_kwargs)
def build_schema(
source: Union[str, Source],
assume_valid: bool = False,
assume_valid_sdl: bool = False,
no_location: bool = False,
allow_legacy_fragment_variables: bool = False,
) -> GraphQLSchema:
"""Build a GraphQLSchema directly from a source document."""
return build_ast_schema(
parse(
source,
no_location=no_location,
allow_legacy_fragment_variables=allow_legacy_fragment_variables,
),
assume_valid=assume_valid,
assume_valid_sdl=assume_valid_sdl,
)
@@ -0,0 +1,418 @@
from itertools import chain
from typing import cast, Callable, Collection, Dict, List, Union
from ..language import DirectiveLocation, parse_value
from ..pyutils import inspect, Undefined
from ..type import (
GraphQLArgument,
GraphQLDirective,
GraphQLEnumType,
GraphQLEnumValue,
GraphQLField,
GraphQLInputField,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInterfaceType,
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLOutputType,
GraphQLScalarType,
GraphQLSchema,
GraphQLType,
GraphQLUnionType,
TypeKind,
assert_interface_type,
assert_nullable_type,
assert_object_type,
introspection_types,
is_input_type,
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: IntrospectionQuery, assume_valid: bool = False
) -> GraphQLSchema:
"""Build a GraphQLSchema for use by client tools.
Given the result of a client running the introspection query, creates and returns
a GraphQLSchema instance which can be then used with all GraphQL-core 3 tools,
but cannot be used to execute a query, as introspection does not represent the
"resolver", "parse" or "serialize" functions or any other server-internal
mechanisms.
This function expects a complete introspection result. Don't forget to check the
"errors" field of a server response before calling this function.
"""
if not isinstance(introspection, dict) or not isinstance(
introspection.get("__schema"), dict
):
raise TypeError(
"Invalid or incomplete introspection result. Ensure that you"
" are passing the 'data' attribute of an introspection response"
f" and no 'errors' were returned alongside: {inspect(introspection)}."
)
# Get the schema from the introspection result.
schema_introspection = introspection["__schema"]
# Given a type reference in introspection, return the GraphQLType instance,
# preferring cached instances before building new instances.
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))
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: IntrospectionType) -> GraphQLNamedType:
type_name = type_ref.get("name")
if not type_name:
raise TypeError(f"Unknown type reference: {inspect(type_ref)}.")
type_ = type_map.get(type_name)
if not type_:
raise TypeError(
f"Invalid or incomplete schema, unknown type: {type_name}."
" Ensure that a full introspection query is used in order"
" to build a client schema."
)
return type_
def get_object_type(type_ref: IntrospectionObjectType) -> GraphQLObjectType:
return assert_object_type(get_type(type_ref))
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_: IntrospectionType) -> GraphQLNamedType:
if type_ and "name" in type_ and "kind" in type_:
builder = type_builders.get(type_["kind"])
if builder: # pragma: no cover else
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: 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: Union[
IntrospectionObjectType, IntrospectionInterfaceType
],
) -> List[GraphQLInterfaceType]:
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:
return []
raise TypeError(
"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: 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: 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: 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 possible_types],
)
def build_enum_def(enum_introspection: IntrospectionEnumType) -> GraphQLEnumType:
if enum_introspection.get("enumValues") is None:
raise TypeError(
"Introspection result missing enumValues:"
f" {inspect(enum_introspection)}."
)
return GraphQLEnumType(
name=enum_introspection["name"],
description=enum_introspection.get("description"),
values={
value_introspect["name"]: GraphQLEnumValue(
value=value_introspect["name"],
description=value_introspect.get("description"),
deprecation_reason=value_introspect.get("deprecationReason"),
)
for value_introspect in enum_introspection["enumValues"]
},
)
def build_input_object_def(
input_object_introspection: IntrospectionInputObjectType,
) -> GraphQLInputObjectType:
if input_object_introspection.get("inputFields") is None:
raise TypeError(
"Introspection result missing inputFields:"
f" {inspect(input_object_introspection)}."
)
return GraphQLInputObjectType(
name=input_object_introspection["name"],
description=input_object_introspection.get("description"),
fields=lambda: build_input_value_def_map(
input_object_introspection["inputFields"]
),
)
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: Union[IntrospectionObjectType, IntrospectionInterfaceType],
) -> Dict[str, GraphQLField]:
if type_introspection.get("fields") is None:
raise TypeError(
f"Introspection result missing fields: {type_introspection}."
)
return {
field_introspection["name"]: build_field(field_introspection)
for field_introspection in type_introspection["fields"]
}
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,"
f" but received: {inspect(type_)}."
)
type_ = cast(GraphQLOutputType, type_)
args_introspection = field_introspection.get("args")
if args_introspection is None:
raise TypeError(
"Introspection result missing field args:"
f" {inspect(field_introspection)}."
)
return GraphQLField(
type_,
args=build_argument_def_map(args_introspection),
description=field_introspection.get("description"),
deprecation_reason=field_introspection.get("deprecationReason"),
)
def build_argument_def_map(
argument_value_introspections: Collection[IntrospectionInputValue],
) -> Dict[str, GraphQLArgument]:
return {
argument_introspection["name"]: build_argument(argument_introspection)
for argument_introspection in argument_value_introspections
}
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_introspection = argument_introspection.get("defaultValue")
default_value = (
Undefined
if default_value_introspection is None
else value_from_ast(parse_value(default_value_introspection), type_)
)
return GraphQLArgument(
type_,
default_value=default_value,
description=argument_introspection.get("description"),
deprecation_reason=argument_introspection.get("deprecationReason"),
)
def build_input_value_def_map(
input_value_introspections: Collection[IntrospectionInputValue],
) -> Dict[str, GraphQLInputField]:
return {
input_value_introspection["name"]: build_input_value(
input_value_introspection
)
for input_value_introspection in input_value_introspections
}
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_introspection = input_value_introspection.get("defaultValue")
default_value = (
Undefined
if default_value_introspection is None
else value_from_ast(parse_value(default_value_introspection), type_)
)
return GraphQLInputField(
type_,
default_value=default_value,
description=input_value_introspection.get("description"),
deprecation_reason=input_value_introspection.get("deprecationReason"),
)
def build_directive(
directive_introspection: IntrospectionDirective,
) -> GraphQLDirective:
if directive_introspection.get("args") is None:
raise TypeError(
"Introspection result missing directive args:"
f" {inspect(directive_introspection)}."
)
if directive_introspection.get("locations") is None:
raise TypeError(
"Introspection result missing directive locations:"
f" {inspect(directive_introspection)}."
)
return GraphQLDirective(
name=directive_introspection["name"],
description=directive_introspection.get("description"),
is_repeatable=directive_introspection.get("isRepeatable", False),
locations=list(
cast(
Collection[DirectiveLocation],
directive_introspection.get("locations"),
)
),
args=build_argument_def_map(directive_introspection["args"]),
)
# Iterate through all types, getting the type definition for each.
type_map: Dict[str, GraphQLNamedType] = {
type_introspection["name"]: build_type(type_introspection)
for type_introspection in schema_introspection["types"]
}
# Include standard types only if they are used.
for std_type_name, std_type in chain(
specified_scalar_types.items(), introspection_types.items()
):
if std_type_name in type_map:
type_map[std_type_name] = std_type
# Get the root Query, Mutation, and Subscription types.
query_type_ref = schema_introspection.get("queryType")
query_type = None if query_type_ref is None else get_object_type(query_type_ref)
mutation_type_ref = schema_introspection.get("mutationType")
mutation_type = (
None if mutation_type_ref is None else get_object_type(mutation_type_ref)
)
subscription_type_ref = schema_introspection.get("subscriptionType")
subscription_type = (
None
if subscription_type_ref is None
else get_object_type(subscription_type_ref)
)
# Get the directives supported by Introspection, assuming empty-set if directives
# were not queried for.
directive_introspections = schema_introspection.get("directives")
directives = (
[
build_directive(directive_introspection)
for directive_introspection in directive_introspections
]
if directive_introspections
else []
)
# Then produce and return a Schema with these types.
return GraphQLSchema(
query=query_type,
mutation=mutation_type,
subscription=subscription_type,
types=list(type_map.values()),
directives=directives,
description=schema_introspection.get("description"),
assume_valid=assume_valid,
)
@@ -0,0 +1,159 @@
from typing import Any, Callable, Dict, List, Optional, Union, cast
from ..error import GraphQLError
from ..pyutils import (
Path,
did_you_mean,
inspect,
is_iterable,
print_path_list,
suggestion_list,
Undefined,
)
from ..type import (
GraphQLInputObjectType,
GraphQLInputType,
GraphQLList,
GraphQLScalarType,
is_leaf_type,
is_input_object_type,
is_list_type,
is_non_null_type,
GraphQLNonNull,
)
__all__ = ["coerce_input_value"]
OnErrorCB = Callable[[List[Union[str, int]], Any, GraphQLError], None]
def default_on_error(
path: List[Union[str, int]], invalid_value: Any, error: GraphQLError
) -> None:
error_prefix = "Invalid value " + inspect(invalid_value)
if path:
error_prefix += f" at 'value{print_path_list(path)}'"
error.message = error_prefix + ": " + error.message
raise error
def coerce_input_value(
input_value: Any,
type_: GraphQLInputType,
on_error: OnErrorCB = default_on_error,
path: Optional[Path] = None,
) -> Any:
"""Coerce a Python value given a GraphQL Input Type."""
if is_non_null_type(type_):
if input_value is not None and input_value is not Undefined:
type_ = cast(GraphQLNonNull, type_)
return coerce_input_value(input_value, type_.of_type, on_error, path)
on_error(
path.as_list() if path else [],
input_value,
GraphQLError(
f"Expected non-nullable type '{inspect(type_)}' not to be None."
),
)
return Undefined
if input_value is None or input_value is Undefined:
# Explicitly return the value null.
return None
if is_list_type(type_):
type_ = cast(GraphQLList, type_)
item_type = type_.of_type
if is_iterable(input_value):
coerced_list: List[Any] = []
append_item = coerced_list.append
for index, item_value in enumerate(input_value):
append_item(
coerce_input_value(
item_value, item_type, on_error, Path(path, index, None)
)
)
return coerced_list
# Lists accept a non-list value as a list of one.
return [coerce_input_value(input_value, item_type, on_error, path)]
if is_input_object_type(type_):
type_ = cast(GraphQLInputObjectType, type_)
if not isinstance(input_value, dict):
on_error(
path.as_list() if path else [],
input_value,
GraphQLError(f"Expected type '{type_.name}' to be a mapping."),
)
return Undefined
coerced_dict: Dict[str, Any] = {}
fields = type_.fields
for field_name, field in fields.items():
field_value = input_value.get(field_name, Undefined)
if field_value is Undefined:
if field.default_value is not Undefined:
# Use out name as name if it exists (extension of GraphQL.js).
coerced_dict[field.out_name or field_name] = field.default_value
elif is_non_null_type(field.type): # pragma: no cover else
type_str = inspect(field.type)
on_error(
path.as_list() if path else [],
input_value,
GraphQLError(
f"Field '{field_name}' of required type '{type_str}'"
" was not provided."
),
)
continue
coerced_dict[field.out_name or field_name] = coerce_input_value(
field_value, field.type, on_error, Path(path, field_name, type_.name)
)
# Ensure every provided field is defined.
for field_name in input_value:
if field_name not in fields:
suggestions = suggestion_list(field_name, fields)
on_error(
path.as_list() if path else [],
input_value,
GraphQLError(
f"Field '{field_name}' is not defined by type '{type_.name}'."
+ did_you_mean(suggestions)
),
)
return type_.out_type(coerced_dict)
if is_leaf_type(type_):
# Scalars determine if a value is valid via `parse_value()`, which can throw to
# indicate failure. If it throws, maintain a reference to the original error.
type_ = cast(GraphQLScalarType, type_)
try:
parse_result = type_.parse_value(input_value)
except GraphQLError as error:
on_error(path.as_list() if path else [], input_value, error)
return Undefined
except Exception as error:
on_error(
path.as_list() if path else [],
input_value,
GraphQLError(
f"Expected type '{type_.name}'. {error}", original_error=error
),
)
return Undefined
if parse_result is Undefined:
on_error(
path.as_list() if path else [],
input_value,
GraphQLError(f"Expected type '{type_.name}'."),
)
return parse_result
# Not reachable. All possible input types have been considered.
raise TypeError(f"Unexpected input type: {inspect(type_)}.")
@@ -0,0 +1,18 @@
from itertools import chain
from typing import Collection
from ..language.ast import DocumentNode
__all__ = ["concat_ast"]
def concat_ast(asts: Collection[DocumentNode]) -> DocumentNode:
"""Concat ASTs.
Provided a collection of ASTs, presumably each from different files, concatenate
the ASTs together into batched AST, useful for validating many GraphQL source files
which together represent one conceptual application.
"""
return DocumentNode(
definitions=list(chain.from_iterable(document.definitions for document in asts))
)
@@ -0,0 +1,700 @@
from collections import defaultdict
from typing import (
Any,
Callable,
Collection,
DefaultDict,
Dict,
List,
Mapping,
Optional,
Union,
cast,
)
from ..language import (
DirectiveDefinitionNode,
DirectiveLocation,
DocumentNode,
EnumTypeDefinitionNode,
EnumTypeExtensionNode,
EnumValueDefinitionNode,
FieldDefinitionNode,
InputObjectTypeDefinitionNode,
InputObjectTypeExtensionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
InterfaceTypeExtensionNode,
ListTypeNode,
NamedTypeNode,
NonNullTypeNode,
ObjectTypeDefinitionNode,
ObjectTypeExtensionNode,
OperationType,
ScalarTypeDefinitionNode,
ScalarTypeExtensionNode,
SchemaExtensionNode,
SchemaDefinitionNode,
TypeDefinitionNode,
TypeExtensionNode,
TypeNode,
UnionTypeDefinitionNode,
UnionTypeExtensionNode,
)
from ..pyutils import inspect, merge_kwargs
from ..type import (
GraphQLArgument,
GraphQLArgumentMap,
GraphQLDeprecatedDirective,
GraphQLDirective,
GraphQLEnumType,
GraphQLEnumValue,
GraphQLEnumValueMap,
GraphQLField,
GraphQLFieldMap,
GraphQLInputField,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInputFieldMap,
GraphQLInterfaceType,
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLNullableType,
GraphQLObjectType,
GraphQLOutputType,
GraphQLScalarType,
GraphQLSchema,
GraphQLSchemaKwargs,
GraphQLSpecifiedByDirective,
GraphQLType,
GraphQLUnionType,
assert_schema,
is_enum_type,
is_input_object_type,
is_interface_type,
is_list_type,
is_non_null_type,
is_object_type,
is_scalar_type,
is_union_type,
is_introspection_type,
is_specified_scalar_type,
introspection_types,
specified_scalar_types,
)
from .value_from_ast import value_from_ast
__all__ = [
"extend_schema",
"extend_schema_impl",
]
def extend_schema(
schema: GraphQLSchema,
document_ast: DocumentNode,
assume_valid: bool = False,
assume_valid_sdl: bool = False,
) -> GraphQLSchema:
"""Extend the schema with extensions from a given document.
Produces a new schema given an existing schema and a document which may contain
GraphQL type extensions and definitions. The original schema will remain unaltered.
Because a schema represents a graph of references, a schema cannot be extended
without effectively making an entire copy. We do not know until it's too late if
subgraphs remain unchanged.
This algorithm copies the provided schema, applying extensions while producing the
copy. The original schema remains unaltered.
When extending a schema with a known valid extension, it might be safe to assume the
schema is valid. Set ``assume_valid`` to ``True`` to assume the produced schema is
valid. Set ``assume_valid_sdl`` to ``True`` to assume it is already a valid SDL
document.
"""
assert_schema(schema)
if not isinstance(document_ast, DocumentNode):
raise TypeError("Must provide valid Document AST.")
if not (assume_valid or assume_valid_sdl):
from ..validation.validate import assert_valid_sdl_extension
assert_valid_sdl_extension(document_ast, schema)
schema_kwargs = schema.to_kwargs()
extended_kwargs = extend_schema_impl(schema_kwargs, document_ast, assume_valid)
return (
schema if schema_kwargs is extended_kwargs else GraphQLSchema(**extended_kwargs)
)
def extend_schema_impl(
schema_kwargs: GraphQLSchemaKwargs,
document_ast: DocumentNode,
assume_valid: bool = False,
) -> GraphQLSchemaKwargs:
"""Extend the given schema arguments with extensions from a given document.
For internal use only.
"""
# Note: schema_kwargs should become a TypedDict once we require Python 3.8
# Collect the type definitions and extensions found in the document.
type_defs: List[TypeDefinitionNode] = []
type_extensions_map: DefaultDict[str, Any] = defaultdict(list)
# New directives and types are separate because a directives and types can have the
# same name. For example, a type named "skip".
directive_defs: List[DirectiveDefinitionNode] = []
schema_def: Optional[SchemaDefinitionNode] = None
# Schema extensions are collected which may add additional operation types.
schema_extensions: List[SchemaExtensionNode] = []
for def_ in document_ast.definitions:
if isinstance(def_, SchemaDefinitionNode):
schema_def = def_
elif isinstance(def_, SchemaExtensionNode):
schema_extensions.append(def_)
elif isinstance(def_, TypeDefinitionNode):
type_defs.append(def_)
elif isinstance(def_, TypeExtensionNode):
extended_type_name = def_.name.value
type_extensions_map[extended_type_name].append(def_)
elif isinstance(def_, DirectiveDefinitionNode):
directive_defs.append(def_)
# If this document contains no new types, extensions, or directives then return the
# same unmodified GraphQLSchema instance.
if (
not type_extensions_map
and not type_defs
and not directive_defs
and not schema_extensions
and not schema_def
):
return schema_kwargs
# Below are functions used for producing this schema that have closed over this
# scope and have access to the schema, cache, and newly defined types.
# noinspection PyTypeChecker,PyUnresolvedReferences
def replace_type(type_: GraphQLType) -> GraphQLType:
if is_list_type(type_):
return GraphQLList(replace_type(type_.of_type)) # type: ignore
if is_non_null_type(type_):
return GraphQLNonNull(replace_type(type_.of_type)) # type: ignore
return replace_named_type(type_) # type: ignore
def replace_named_type(type_: GraphQLNamedType) -> GraphQLNamedType:
# Note: While this could make early assertions to get the correctly
# typed values below, that would throw immediately while type system
# validation with validate_schema() will produce more actionable results.
return type_map[type_.name]
# noinspection PyShadowingNames
def replace_directive(directive: GraphQLDirective) -> GraphQLDirective:
kwargs = directive.to_kwargs()
return GraphQLDirective(
**merge_kwargs(
kwargs,
args={name: extend_arg(arg) for name, arg in kwargs["args"].items()},
)
)
def extend_named_type(type_: GraphQLNamedType) -> GraphQLNamedType:
if is_introspection_type(type_) or is_specified_scalar_type(type_):
# Builtin types are not extended.
return type_
if is_scalar_type(type_):
type_ = cast(GraphQLScalarType, type_)
return extend_scalar_type(type_)
if is_object_type(type_):
type_ = cast(GraphQLObjectType, type_)
return extend_object_type(type_)
if is_interface_type(type_):
type_ = cast(GraphQLInterfaceType, type_)
return extend_interface_type(type_)
if is_union_type(type_):
type_ = cast(GraphQLUnionType, type_)
return extend_union_type(type_)
if is_enum_type(type_):
type_ = cast(GraphQLEnumType, type_)
return extend_enum_type(type_)
if is_input_object_type(type_):
type_ = cast(GraphQLInputObjectType, type_)
return extend_input_object_type(type_)
# Not reachable. All possible types have been considered.
raise TypeError(f"Unexpected type: {inspect(type_)}.") # pragma: no cover
# noinspection PyShadowingNames
def extend_input_object_type(
type_: GraphQLInputObjectType,
) -> GraphQLInputObjectType:
kwargs = type_.to_kwargs()
extensions = tuple(type_extensions_map[kwargs["name"]])
return GraphQLInputObjectType(
**merge_kwargs(
kwargs,
fields=lambda: {
**{
name: GraphQLInputField(
**merge_kwargs(
field.to_kwargs(),
type_=replace_type(field.type),
)
)
for name, field in kwargs["fields"].items()
},
**build_input_field_map(extensions),
},
extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions,
)
)
def extend_enum_type(type_: GraphQLEnumType) -> GraphQLEnumType:
kwargs = type_.to_kwargs()
extensions = tuple(type_extensions_map[kwargs["name"]])
return GraphQLEnumType(
**merge_kwargs(
kwargs,
values={**kwargs["values"], **build_enum_value_map(extensions)},
extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions,
)
)
def extend_scalar_type(type_: GraphQLScalarType) -> GraphQLScalarType:
kwargs = type_.to_kwargs()
extensions = tuple(type_extensions_map[kwargs["name"]])
specified_by_url = kwargs["specified_by_url"]
for extension_node in extensions:
specified_by_url = get_specified_by_url(extension_node) or specified_by_url
return GraphQLScalarType(
**merge_kwargs(
kwargs,
specified_by_url=specified_by_url,
extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions,
)
)
# noinspection PyShadowingNames
def extend_object_type(type_: GraphQLObjectType) -> GraphQLObjectType:
kwargs = type_.to_kwargs()
extensions = tuple(type_extensions_map[kwargs["name"]])
return GraphQLObjectType(
**merge_kwargs(
kwargs,
interfaces=lambda: [
cast(GraphQLInterfaceType, replace_named_type(interface))
for interface in kwargs["interfaces"]
]
+ build_interfaces(extensions),
fields=lambda: {
**{
name: extend_field(field)
for name, field in kwargs["fields"].items()
},
**build_field_map(extensions),
},
extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions,
)
)
# noinspection PyShadowingNames
def extend_interface_type(type_: GraphQLInterfaceType) -> GraphQLInterfaceType:
kwargs = type_.to_kwargs()
extensions = tuple(type_extensions_map[kwargs["name"]])
return GraphQLInterfaceType(
**merge_kwargs(
kwargs,
interfaces=lambda: [
cast(GraphQLInterfaceType, replace_named_type(interface))
for interface in kwargs["interfaces"]
]
+ build_interfaces(extensions),
fields=lambda: {
**{
name: extend_field(field)
for name, field in kwargs["fields"].items()
},
**build_field_map(extensions),
},
extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions,
)
)
def extend_union_type(type_: GraphQLUnionType) -> GraphQLUnionType:
kwargs = type_.to_kwargs()
extensions = tuple(type_extensions_map[kwargs["name"]])
return GraphQLUnionType(
**merge_kwargs(
kwargs,
types=lambda: [
cast(GraphQLObjectType, replace_named_type(member_type))
for member_type in kwargs["types"]
]
+ build_union_types(extensions),
extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions,
)
)
# noinspection PyShadowingNames
def extend_field(field: GraphQLField) -> GraphQLField:
return GraphQLField(
**merge_kwargs(
field.to_kwargs(),
type_=replace_type(field.type),
args={name: extend_arg(arg) for name, arg in field.args.items()},
)
)
def extend_arg(arg: GraphQLArgument) -> GraphQLArgument:
return GraphQLArgument(
**merge_kwargs(
arg.to_kwargs(),
type_=replace_type(arg.type),
)
)
# noinspection PyShadowingNames
def get_operation_types(
nodes: Collection[Union[SchemaDefinitionNode, SchemaExtensionNode]]
) -> Dict[OperationType, GraphQLNamedType]:
# Note: While this could make early assertions to get the correctly
# typed values below, that would throw immediately while type system
# validation with validate_schema() will produce more actionable results.
return {
operation_type.operation: get_named_type(operation_type.type)
for node in nodes
for operation_type in node.operation_types or []
}
# noinspection PyShadowingNames
def get_named_type(node: NamedTypeNode) -> GraphQLNamedType:
name = node.name.value
type_ = std_type_map.get(name) or type_map.get(name)
if not type_:
raise TypeError(f"Unknown type: '{name}'.")
return type_
def get_wrapped_type(node: TypeNode) -> GraphQLType:
if isinstance(node, ListTypeNode):
return GraphQLList(get_wrapped_type(node.type))
if isinstance(node, NonNullTypeNode):
return GraphQLNonNull(
cast(GraphQLNullableType, get_wrapped_type(node.type))
)
return get_named_type(cast(NamedTypeNode, node))
def build_directive(node: DirectiveDefinitionNode) -> GraphQLDirective:
locations = [DirectiveLocation[node.value] for node in node.locations]
return GraphQLDirective(
name=node.name.value,
description=node.description.value if node.description else None,
locations=locations,
is_repeatable=node.repeatable,
args=build_argument_map(node.arguments),
ast_node=node,
)
def build_field_map(
nodes: Collection[
Union[
InterfaceTypeDefinitionNode,
InterfaceTypeExtensionNode,
ObjectTypeDefinitionNode,
ObjectTypeExtensionNode,
]
],
) -> GraphQLFieldMap:
field_map: GraphQLFieldMap = {}
for node in nodes:
for field in node.fields or []:
# Note: While this could make assertions to get the correctly typed
# value, that would throw immediately while type system validation
# with validate_schema() will produce more actionable results.
field_map[field.name.value] = GraphQLField(
type_=cast(GraphQLOutputType, get_wrapped_type(field.type)),
description=field.description.value if field.description else None,
args=build_argument_map(field.arguments),
deprecation_reason=get_deprecation_reason(field),
ast_node=field,
)
return field_map
def build_argument_map(
args: Optional[Collection[InputValueDefinitionNode]],
) -> GraphQLArgumentMap:
arg_map: GraphQLArgumentMap = {}
for arg in args or []:
# Note: While this could make assertions to get the correctly typed
# value, that would throw immediately while type system validation
# with validate_schema() will produce more actionable results.
type_ = cast(GraphQLInputType, get_wrapped_type(arg.type))
arg_map[arg.name.value] = GraphQLArgument(
type_=type_,
description=arg.description.value if arg.description else None,
default_value=value_from_ast(arg.default_value, type_),
deprecation_reason=get_deprecation_reason(arg),
ast_node=arg,
)
return arg_map
def build_input_field_map(
nodes: Collection[
Union[InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode]
],
) -> GraphQLInputFieldMap:
input_field_map: GraphQLInputFieldMap = {}
for node in nodes:
for field in node.fields or []:
# Note: While this could make assertions to get the correctly typed
# value, that would throw immediately while type system validation
# with validate_schema() will produce more actionable results.
type_ = cast(GraphQLInputType, get_wrapped_type(field.type))
input_field_map[field.name.value] = GraphQLInputField(
type_=type_,
description=field.description.value if field.description else None,
default_value=value_from_ast(field.default_value, type_),
deprecation_reason=get_deprecation_reason(field),
ast_node=field,
)
return input_field_map
def build_enum_value_map(
nodes: Collection[Union[EnumTypeDefinitionNode, EnumTypeExtensionNode]]
) -> GraphQLEnumValueMap:
enum_value_map: GraphQLEnumValueMap = {}
for node in nodes:
for value in node.values or []:
# Note: While this could make assertions to get the correctly typed
# value, that would throw immediately while type system validation
# with validate_schema() will produce more actionable results.
value_name = value.name.value
enum_value_map[value_name] = GraphQLEnumValue(
value=value_name,
description=value.description.value if value.description else None,
deprecation_reason=get_deprecation_reason(value),
ast_node=value,
)
return enum_value_map
def build_interfaces(
nodes: Collection[
Union[
InterfaceTypeDefinitionNode,
InterfaceTypeExtensionNode,
ObjectTypeDefinitionNode,
ObjectTypeExtensionNode,
]
],
) -> List[GraphQLInterfaceType]:
interfaces: List[GraphQLInterfaceType] = []
for node in nodes:
for type_ in node.interfaces or []:
# Note: While this could make assertions to get the correctly typed
# value, that would throw immediately while type system validation
# with validate_schema() will produce more actionable results.
interfaces.append(cast(GraphQLInterfaceType, get_named_type(type_)))
return interfaces
def build_union_types(
nodes: Collection[Union[UnionTypeDefinitionNode, UnionTypeExtensionNode]],
) -> List[GraphQLObjectType]:
types: List[GraphQLObjectType] = []
for node in nodes:
for type_ in node.types or []:
# Note: While this could make assertions to get the correctly typed
# value, that would throw immediately while type system validation
# with validate_schema() will produce more actionable results.
types.append(cast(GraphQLObjectType, get_named_type(type_)))
return types
def build_object_type(ast_node: ObjectTypeDefinitionNode) -> GraphQLObjectType:
extension_nodes = type_extensions_map[ast_node.name.value]
all_nodes: List[Union[ObjectTypeDefinitionNode, ObjectTypeExtensionNode]] = [
ast_node,
*extension_nodes,
]
return GraphQLObjectType(
name=ast_node.name.value,
description=ast_node.description.value if ast_node.description else None,
interfaces=lambda: build_interfaces(all_nodes),
fields=lambda: build_field_map(all_nodes),
ast_node=ast_node,
extension_ast_nodes=extension_nodes,
)
def build_interface_type(
ast_node: InterfaceTypeDefinitionNode,
) -> GraphQLInterfaceType:
extension_nodes = type_extensions_map[ast_node.name.value]
all_nodes: List[
Union[InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode]
] = [ast_node, *extension_nodes]
return GraphQLInterfaceType(
name=ast_node.name.value,
description=ast_node.description.value if ast_node.description else None,
interfaces=lambda: build_interfaces(all_nodes),
fields=lambda: build_field_map(all_nodes),
ast_node=ast_node,
extension_ast_nodes=extension_nodes,
)
def build_enum_type(ast_node: EnumTypeDefinitionNode) -> GraphQLEnumType:
extension_nodes = type_extensions_map[ast_node.name.value]
all_nodes: List[Union[EnumTypeDefinitionNode, EnumTypeExtensionNode]] = [
ast_node,
*extension_nodes,
]
return GraphQLEnumType(
name=ast_node.name.value,
description=ast_node.description.value if ast_node.description else None,
values=build_enum_value_map(all_nodes),
ast_node=ast_node,
extension_ast_nodes=extension_nodes,
)
def build_union_type(ast_node: UnionTypeDefinitionNode) -> GraphQLUnionType:
extension_nodes = type_extensions_map[ast_node.name.value]
all_nodes: List[Union[UnionTypeDefinitionNode, UnionTypeExtensionNode]] = [
ast_node,
*extension_nodes,
]
return GraphQLUnionType(
name=ast_node.name.value,
description=ast_node.description.value if ast_node.description else None,
types=lambda: build_union_types(all_nodes),
ast_node=ast_node,
extension_ast_nodes=extension_nodes,
)
def build_scalar_type(ast_node: ScalarTypeDefinitionNode) -> GraphQLScalarType:
extension_nodes = type_extensions_map[ast_node.name.value]
return GraphQLScalarType(
name=ast_node.name.value,
description=ast_node.description.value if ast_node.description else None,
specified_by_url=get_specified_by_url(ast_node),
ast_node=ast_node,
extension_ast_nodes=extension_nodes,
)
def build_input_object_type(
ast_node: InputObjectTypeDefinitionNode,
) -> GraphQLInputObjectType:
extension_nodes = type_extensions_map[ast_node.name.value]
all_nodes: List[
Union[InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode]
] = [ast_node, *extension_nodes]
return GraphQLInputObjectType(
name=ast_node.name.value,
description=ast_node.description.value if ast_node.description else None,
fields=lambda: build_input_field_map(all_nodes),
ast_node=ast_node,
extension_ast_nodes=extension_nodes,
)
build_type_for_kind = cast(
Dict[str, Callable[[TypeDefinitionNode], GraphQLNamedType]],
{
"object_type_definition": build_object_type,
"interface_type_definition": build_interface_type,
"enum_type_definition": build_enum_type,
"union_type_definition": build_union_type,
"scalar_type_definition": build_scalar_type,
"input_object_type_definition": build_input_object_type,
},
)
def build_type(ast_node: TypeDefinitionNode) -> GraphQLNamedType:
try:
# object_type_definition_node is built with _build_object_type etc.
build_function = build_type_for_kind[ast_node.kind]
except KeyError: # pragma: no cover
# Not reachable. All possible type definition nodes have been considered.
raise TypeError( # pragma: no cover
f"Unexpected type definition node: {inspect(ast_node)}."
)
else:
return build_function(ast_node)
type_map: Dict[str, GraphQLNamedType] = {}
for existing_type in schema_kwargs["types"] or ():
type_map[existing_type.name] = extend_named_type(existing_type)
for type_node in type_defs:
name = type_node.name.value
type_map[name] = std_type_map.get(name) or build_type(type_node)
# Get the extended root operation types.
operation_types: Dict[OperationType, GraphQLNamedType] = {}
for operation_type in OperationType:
original_type = schema_kwargs[operation_type.value]
if original_type:
operation_types[operation_type] = replace_named_type(original_type)
# Then, incorporate schema definition and all schema extensions.
if schema_def:
operation_types.update(get_operation_types([schema_def]))
if schema_extensions:
operation_types.update(get_operation_types(schema_extensions))
# Then produce and return the kwargs for a Schema with these types.
get_operation = operation_types.get
return GraphQLSchemaKwargs(
query=get_operation(OperationType.QUERY), # type: ignore
mutation=get_operation(OperationType.MUTATION), # type: ignore
subscription=get_operation(OperationType.SUBSCRIPTION), # type: ignore
types=tuple(type_map.values()),
directives=tuple(
replace_directive(directive) for directive in schema_kwargs["directives"]
)
+ tuple(build_directive(directive) for directive in directive_defs),
description=(
schema_def.description.value
if schema_def and schema_def.description
else None
),
extensions={},
ast_node=schema_def or schema_kwargs["ast_node"],
extension_ast_nodes=schema_kwargs["extension_ast_nodes"]
+ tuple(schema_extensions),
assume_valid=assume_valid,
)
std_type_map: Mapping[str, Union[GraphQLNamedType, GraphQLObjectType]] = {
**specified_scalar_types,
**introspection_types,
}
def get_deprecation_reason(
node: Union[EnumValueDefinitionNode, FieldDefinitionNode, InputValueDefinitionNode]
) -> Optional[str]:
"""Given a field or enum value node, get deprecation reason as string."""
from ..execution import get_directive_values
deprecated = get_directive_values(GraphQLDeprecatedDirective, node)
return deprecated["reason"] if deprecated else None
def get_specified_by_url(
node: Union[ScalarTypeDefinitionNode, ScalarTypeExtensionNode]
) -> Optional[str]:
"""Given a scalar node, return the string value for the specifiedByURL."""
from ..execution import get_directive_values
specified_by_url = get_directive_values(GraphQLSpecifiedByDirective, node)
return specified_by_url["url"] if specified_by_url else None
@@ -0,0 +1,620 @@
from enum import Enum
from typing import Any, Collection, Dict, List, NamedTuple, Union, cast
from ..language import print_ast
from ..pyutils import inspect, Undefined
from ..type import (
GraphQLEnumType,
GraphQLField,
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLInputType,
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLSchema,
GraphQLType,
GraphQLUnionType,
is_enum_type,
is_input_object_type,
is_interface_type,
is_list_type,
is_named_type,
is_non_null_type,
is_object_type,
is_required_argument,
is_required_input_field,
is_scalar_type,
is_specified_scalar_type,
is_union_type,
)
from ..utilities.sort_value_node import sort_value_node
from .ast_from_value import ast_from_value
__all__ = [
"BreakingChange",
"BreakingChangeType",
"DangerousChange",
"DangerousChangeType",
"find_breaking_changes",
"find_dangerous_changes",
]
class BreakingChangeType(Enum):
TYPE_REMOVED = 10
TYPE_CHANGED_KIND = 11
TYPE_REMOVED_FROM_UNION = 20
VALUE_REMOVED_FROM_ENUM = 21
REQUIRED_INPUT_FIELD_ADDED = 22
IMPLEMENTED_INTERFACE_REMOVED = 23
FIELD_REMOVED = 30
FIELD_CHANGED_KIND = 31
REQUIRED_ARG_ADDED = 40
ARG_REMOVED = 41
ARG_CHANGED_KIND = 42
DIRECTIVE_REMOVED = 50
DIRECTIVE_ARG_REMOVED = 51
REQUIRED_DIRECTIVE_ARG_ADDED = 52
DIRECTIVE_REPEATABLE_REMOVED = 53
DIRECTIVE_LOCATION_REMOVED = 54
class DangerousChangeType(Enum):
VALUE_ADDED_TO_ENUM = 60
TYPE_ADDED_TO_UNION = 61
OPTIONAL_INPUT_FIELD_ADDED = 62
OPTIONAL_ARG_ADDED = 63
IMPLEMENTED_INTERFACE_ADDED = 64
ARG_DEFAULT_VALUE_CHANGE = 65
class BreakingChange(NamedTuple):
type: BreakingChangeType
description: str
class DangerousChange(NamedTuple):
type: DangerousChangeType
description: str
Change = Union[BreakingChange, DangerousChange]
def find_breaking_changes(
old_schema: GraphQLSchema, new_schema: GraphQLSchema
) -> List[BreakingChange]:
"""Find breaking changes.
Given two schemas, returns a list containing descriptions of all the types of
breaking changes covered by the other functions down below.
"""
return [
change
for change in find_schema_changes(old_schema, new_schema)
if isinstance(change.type, BreakingChangeType)
]
def find_dangerous_changes(
old_schema: GraphQLSchema, new_schema: GraphQLSchema
) -> List[DangerousChange]:
"""Find dangerous changes.
Given two schemas, returns a list containing descriptions of all the types of
potentially dangerous changes covered by the other functions down below.
"""
return [
change
for change in find_schema_changes(old_schema, new_schema)
if isinstance(change.type, DangerousChangeType)
]
def find_schema_changes(
old_schema: GraphQLSchema, new_schema: GraphQLSchema
) -> List[Change]:
return find_type_changes(old_schema, new_schema) + find_directive_changes(
old_schema, new_schema
)
def find_directive_changes(
old_schema: GraphQLSchema, new_schema: GraphQLSchema
) -> List[Change]:
schema_changes: List[Change] = []
directives_diff = list_diff(old_schema.directives, new_schema.directives)
for directive in directives_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.DIRECTIVE_REMOVED, f"{directive.name} was removed."
)
)
for old_directive, new_directive in directives_diff.persisted:
args_diff = dict_diff(old_directive.args, new_directive.args)
for arg_name, new_arg in args_diff.added.items():
if is_required_argument(new_arg):
schema_changes.append(
BreakingChange(
BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED,
f"A required arg {arg_name} on directive"
f" {old_directive.name} was added.",
)
)
for arg_name in args_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.DIRECTIVE_ARG_REMOVED,
f"{arg_name} was removed from {new_directive.name}.",
)
)
if old_directive.is_repeatable and not new_directive.is_repeatable:
schema_changes.append(
BreakingChange(
BreakingChangeType.DIRECTIVE_REPEATABLE_REMOVED,
f"Repeatable flag was removed from {old_directive.name}.",
)
)
for location in old_directive.locations:
if location not in new_directive.locations:
schema_changes.append(
BreakingChange(
BreakingChangeType.DIRECTIVE_LOCATION_REMOVED,
f"{location.name} was removed from {new_directive.name}.",
)
)
return schema_changes
def find_type_changes(
old_schema: GraphQLSchema, new_schema: GraphQLSchema
) -> List[Change]:
schema_changes: List[Change] = []
types_diff = dict_diff(old_schema.type_map, new_schema.type_map)
for type_name, old_type in types_diff.removed.items():
schema_changes.append(
BreakingChange(
BreakingChangeType.TYPE_REMOVED,
(
f"Standard scalar {type_name} was removed"
" because it is not referenced anymore."
if is_specified_scalar_type(old_type)
else f"{type_name} was removed."
),
)
)
for type_name, (old_type, new_type) in types_diff.persisted.items():
if is_enum_type(old_type) and is_enum_type(new_type):
schema_changes.extend(find_enum_type_changes(old_type, new_type))
elif is_union_type(old_type) and is_union_type(new_type):
schema_changes.extend(find_union_type_changes(old_type, new_type))
elif is_input_object_type(old_type) and is_input_object_type(new_type):
schema_changes.extend(find_input_object_type_changes(old_type, new_type))
elif is_object_type(old_type) and is_object_type(new_type):
schema_changes.extend(find_field_changes(old_type, new_type))
schema_changes.extend(
find_implemented_interfaces_changes(old_type, new_type)
)
elif is_interface_type(old_type) and is_interface_type(new_type):
schema_changes.extend(find_field_changes(old_type, new_type))
schema_changes.extend(
find_implemented_interfaces_changes(old_type, new_type)
)
elif old_type.__class__ is not new_type.__class__:
schema_changes.append(
BreakingChange(
BreakingChangeType.TYPE_CHANGED_KIND,
f"{type_name} changed from {type_kind_name(old_type)}"
f" to {type_kind_name(new_type)}.",
)
)
return schema_changes
def find_input_object_type_changes(
old_type: Union[GraphQLObjectType, GraphQLInterfaceType],
new_type: Union[GraphQLObjectType, GraphQLInterfaceType],
) -> List[Change]:
schema_changes: List[Change] = []
fields_diff = dict_diff(old_type.fields, new_type.fields)
for field_name, new_field in fields_diff.added.items():
if is_required_input_field(new_field):
schema_changes.append(
BreakingChange(
BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED,
f"A required field {field_name} on"
f" input type {old_type.name} was added.",
)
)
else:
schema_changes.append(
DangerousChange(
DangerousChangeType.OPTIONAL_INPUT_FIELD_ADDED,
f"An optional field {field_name} on"
f" input type {old_type.name} was added.",
)
)
for field_name in fields_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.FIELD_REMOVED,
f"{old_type.name}.{field_name} was removed.",
)
)
for field_name, (old_field, new_field) in fields_diff.persisted.items():
is_safe = is_change_safe_for_input_object_field_or_field_arg(
old_field.type, new_field.type
)
if not is_safe:
schema_changes.append(
BreakingChange(
BreakingChangeType.FIELD_CHANGED_KIND,
f"{old_type.name}.{field_name} changed type"
f" from {old_field.type} to {new_field.type}.",
)
)
return schema_changes
def find_union_type_changes(
old_type: GraphQLUnionType, new_type: GraphQLUnionType
) -> List[Change]:
schema_changes: List[Change] = []
possible_types_diff = list_diff(old_type.types, new_type.types)
for possible_type in possible_types_diff.added:
schema_changes.append(
DangerousChange(
DangerousChangeType.TYPE_ADDED_TO_UNION,
f"{possible_type.name} was added" f" to union type {old_type.name}.",
)
)
for possible_type in possible_types_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.TYPE_REMOVED_FROM_UNION,
f"{possible_type.name} was removed from union type {old_type.name}.",
)
)
return schema_changes
def find_enum_type_changes(
old_type: GraphQLEnumType, new_type: GraphQLEnumType
) -> List[Change]:
schema_changes: List[Change] = []
values_diff = dict_diff(old_type.values, new_type.values)
for value_name in values_diff.added:
schema_changes.append(
DangerousChange(
DangerousChangeType.VALUE_ADDED_TO_ENUM,
f"{value_name} was added to enum type {old_type.name}.",
)
)
for value_name in values_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.VALUE_REMOVED_FROM_ENUM,
f"{value_name} was removed from enum type {old_type.name}.",
)
)
return schema_changes
def find_implemented_interfaces_changes(
old_type: Union[GraphQLObjectType, GraphQLInterfaceType],
new_type: Union[GraphQLObjectType, GraphQLInterfaceType],
) -> List[Change]:
schema_changes: List[Change] = []
interfaces_diff = list_diff(old_type.interfaces, new_type.interfaces)
for interface in interfaces_diff.added:
schema_changes.append(
DangerousChange(
DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED,
f"{interface.name} added to interfaces implemented by {old_type.name}.",
)
)
for interface in interfaces_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED,
f"{old_type.name} no longer implements interface {interface.name}.",
)
)
return schema_changes
def find_field_changes(
old_type: Union[GraphQLObjectType, GraphQLInterfaceType],
new_type: Union[GraphQLObjectType, GraphQLInterfaceType],
) -> List[Change]:
schema_changes: List[Change] = []
fields_diff = dict_diff(old_type.fields, new_type.fields)
for field_name in fields_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.FIELD_REMOVED,
f"{old_type.name}.{field_name} was removed.",
)
)
for field_name, (old_field, new_field) in fields_diff.persisted.items():
schema_changes.extend(
find_arg_changes(old_type, field_name, old_field, new_field)
)
is_safe = is_change_safe_for_object_or_interface_field(
old_field.type, new_field.type
)
if not is_safe:
schema_changes.append(
BreakingChange(
BreakingChangeType.FIELD_CHANGED_KIND,
f"{old_type.name}.{field_name} changed type"
f" from {old_field.type} to {new_field.type}.",
)
)
return schema_changes
def find_arg_changes(
old_type: Union[GraphQLObjectType, GraphQLInterfaceType],
field_name: str,
old_field: GraphQLField,
new_field: GraphQLField,
) -> List[Change]:
schema_changes: List[Change] = []
args_diff = dict_diff(old_field.args, new_field.args)
for arg_name in args_diff.removed:
schema_changes.append(
BreakingChange(
BreakingChangeType.ARG_REMOVED,
f"{old_type.name}.{field_name} arg" f" {arg_name} was removed.",
)
)
for arg_name, (old_arg, new_arg) in args_diff.persisted.items():
is_safe = is_change_safe_for_input_object_field_or_field_arg(
old_arg.type, new_arg.type
)
if not is_safe:
schema_changes.append(
BreakingChange(
BreakingChangeType.ARG_CHANGED_KIND,
f"{old_type.name}.{field_name} arg"
f" {arg_name} has changed type from"
f" {old_arg.type} to {new_arg.type}.",
)
)
elif old_arg.default_value is not Undefined:
if new_arg.default_value is Undefined:
schema_changes.append(
DangerousChange(
DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
f"{old_type.name}.{field_name} arg"
f" {arg_name} defaultValue was removed.",
)
)
else:
# Since we are looking only for client's observable changes we should
# compare default values in the same representation as they are
# represented inside introspection.
old_value_str = stringify_value(old_arg.default_value, old_arg.type)
new_value_str = stringify_value(new_arg.default_value, new_arg.type)
if old_value_str != new_value_str:
schema_changes.append(
DangerousChange(
DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
f"{old_type.name}.{field_name} arg"
f" {arg_name} has changed defaultValue"
f" from {old_value_str} to {new_value_str}.",
)
)
for arg_name, new_arg in args_diff.added.items():
if is_required_argument(new_arg):
schema_changes.append(
BreakingChange(
BreakingChangeType.REQUIRED_ARG_ADDED,
f"A required arg {arg_name} on"
f" {old_type.name}.{field_name} was added.",
)
)
else:
schema_changes.append(
DangerousChange(
DangerousChangeType.OPTIONAL_ARG_ADDED,
f"An optional arg {arg_name} on"
f" {old_type.name}.{field_name} was added.",
)
)
return schema_changes
def is_change_safe_for_object_or_interface_field(
old_type: GraphQLType, new_type: GraphQLType
) -> bool:
if is_list_type(old_type):
return (
# if they're both lists, make sure underlying types are compatible
is_list_type(new_type)
and is_change_safe_for_object_or_interface_field(
cast(GraphQLList, old_type).of_type, cast(GraphQLList, new_type).of_type
)
) or (
# moving from nullable to non-null of same underlying type is safe
is_non_null_type(new_type)
and is_change_safe_for_object_or_interface_field(
old_type, cast(GraphQLNonNull, new_type).of_type
)
)
if is_non_null_type(old_type):
# if they're both non-null, make sure underlying types are compatible
return is_non_null_type(
new_type
) and is_change_safe_for_object_or_interface_field(
cast(GraphQLNonNull, old_type).of_type,
cast(GraphQLNonNull, new_type).of_type,
)
return (
# if they're both named types, see if their names are equivalent
is_named_type(new_type)
and cast(GraphQLNamedType, old_type).name
== cast(GraphQLNamedType, new_type).name
) or (
# moving from nullable to non-null of same underlying type is safe
is_non_null_type(new_type)
and is_change_safe_for_object_or_interface_field(
old_type, cast(GraphQLNonNull, new_type).of_type
)
)
def is_change_safe_for_input_object_field_or_field_arg(
old_type: GraphQLType, new_type: GraphQLType
) -> bool:
if is_list_type(old_type):
return is_list_type(
# if they're both lists, make sure underlying types are compatible
new_type
) and is_change_safe_for_input_object_field_or_field_arg(
cast(GraphQLList, old_type).of_type, cast(GraphQLList, new_type).of_type
)
if is_non_null_type(old_type):
return (
# if they're both non-null, make sure the underlying types are compatible
is_non_null_type(new_type)
and is_change_safe_for_input_object_field_or_field_arg(
cast(GraphQLNonNull, old_type).of_type,
cast(GraphQLNonNull, new_type).of_type,
)
) or (
# moving from non-null to nullable of same underlying type is safe
not is_non_null_type(new_type)
and is_change_safe_for_input_object_field_or_field_arg(
cast(GraphQLNonNull, old_type).of_type, new_type
)
)
return (
# if they're both named types, see if their names are equivalent
is_named_type(new_type)
and cast(GraphQLNamedType, old_type).name
== cast(GraphQLNamedType, new_type).name
)
def type_kind_name(type_: GraphQLNamedType) -> str:
if is_scalar_type(type_):
return "a Scalar type"
if is_object_type(type_):
return "an Object type"
if is_interface_type(type_):
return "an Interface type"
if is_union_type(type_):
return "a Union type"
if is_enum_type(type_):
return "an Enum type"
if is_input_object_type(type_):
return "an Input type"
# Not reachable. All possible output types have been considered.
raise TypeError(f"Unexpected type {inspect(type)}")
def stringify_value(value: Any, type_: GraphQLInputType) -> str:
ast = ast_from_value(value, type_)
if ast is None: # pragma: no cover
raise TypeError(f"Invalid value: {inspect(value)}")
return print_ast(sort_value_node(ast))
class ListDiff(NamedTuple):
"""Tuple with added, removed and persisted list items."""
added: List
removed: List
persisted: List
def list_diff(old_list: Collection, new_list: Collection) -> ListDiff:
"""Get differences between two lists of named items."""
added = []
persisted = []
removed = []
old_set = {item.name for item in old_list}
new_map = {item.name: item for item in new_list}
for old_item in old_list:
new_item = new_map.get(old_item.name)
if new_item:
persisted.append([old_item, new_item])
else:
removed.append(old_item)
for new_item in new_list:
if new_item.name not in old_set:
added.append(new_item)
return ListDiff(added, removed, persisted)
class DictDiff(NamedTuple):
"""Tuple with added, removed and persisted dict entries."""
added: Dict
removed: Dict
persisted: Dict
def dict_diff(old_dict: Dict, new_dict: Dict) -> DictDiff:
"""Get differences between two dicts."""
added = {}
removed = {}
persisted = {}
for old_name, old_item in old_dict.items():
new_item = new_dict.get(old_name)
if new_item:
persisted[old_name] = [old_item, new_item]
else:
removed[old_name] = old_item
for new_name, new_item in new_dict.items():
if new_name not in old_dict:
added[new_name] = new_item
return DictDiff(added, removed, persisted)
@@ -0,0 +1,298 @@
from textwrap import dedent
from typing import Any, Dict, List, Optional, Union
from ..language import DirectiveLocation
try:
from typing import Literal, TypedDict
except ImportError: # Python < 3.8
from typing_extensions import Literal, TypedDict # 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(
descriptions: bool = True,
specified_by_url: bool = False,
directive_is_repeatable: bool = False,
schema_description: bool = False,
input_value_deprecation: bool = False,
) -> str:
"""Get a query for introspection.
Optionally, you can exclude descriptions, include specification URLs,
include repeatability of directives, and specify whether to include
the schema description as well.
"""
maybe_description = "description" if descriptions else ""
maybe_specified_by_url = "specifiedByURL" if specified_by_url else ""
maybe_directive_is_repeatable = "isRepeatable" if directive_is_repeatable else ""
maybe_schema_description = maybe_description if schema_description else ""
def input_deprecation(string: str) -> Optional[str]:
return string if input_value_deprecation else ""
return dedent(
f"""
query IntrospectionQuery {{
__schema {{
{maybe_schema_description}
queryType {{ name }}
mutationType {{ name }}
subscriptionType {{ name }}
types {{
...FullType
}}
directives {{
name
{maybe_description}
{maybe_directive_is_repeatable}
locations
args{input_deprecation("(includeDeprecated: true)")} {{
...InputValue
}}
}}
}}
}}
fragment FullType on __Type {{
kind
name
{maybe_description}
{maybe_specified_by_url}
fields(includeDeprecated: true) {{
name
{maybe_description}
args{input_deprecation("(includeDeprecated: true)")} {{
...InputValue
}}
type {{
...TypeRef
}}
isDeprecated
deprecationReason
}}
inputFields{input_deprecation("(includeDeprecated: true)")} {{
...InputValue
}}
interfaces {{
...TypeRef
}}
enumValues(includeDeprecated: true) {{
name
{maybe_description}
isDeprecated
deprecationReason
}}
possibleTypes {{
...TypeRef
}}
}}
fragment InputValue on __InputValue {{
name
{maybe_description}
type {{ ...TypeRef }}
defaultValue
{input_deprecation("isDeprecated")}
{input_deprecation("deprecationReason")}
}}
fragment TypeRef on __Type {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
ofType {{
kind
name
}}
}}
}}
}}
}}
}}
}}
}}
}}
}}
"""
)
# 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
@@ -0,0 +1,29 @@
from typing import Optional
from ..language import DocumentNode, OperationDefinitionNode
__all__ = ["get_operation_ast"]
def get_operation_ast(
document_ast: DocumentNode, operation_name: Optional[str] = None
) -> Optional[OperationDefinitionNode]:
"""Get operation AST node.
Returns an operation AST given a document AST and optionally an operation
name. If a name is not provided, an operation is only returned if only one
is provided in the document.
"""
operation = None
for definition in document_ast.definitions:
if isinstance(definition, OperationDefinitionNode):
if operation_name is None:
# If no operation name was provided, only return an Operation if there
# is one defined in the document.
# Upon encountering the second, return None.
if operation:
return None
operation = definition
elif definition.name and definition.name.value == operation_name:
return definition
return operation
@@ -0,0 +1,46 @@
from typing import Union
from ..error import GraphQLError
from ..language import (
OperationType,
OperationDefinitionNode,
OperationTypeDefinitionNode,
)
from ..type import GraphQLObjectType, GraphQLSchema
__all__ = ["get_operation_root_type"]
def get_operation_root_type(
schema: GraphQLSchema,
operation: Union[OperationDefinitionNode, OperationTypeDefinitionNode],
) -> GraphQLObjectType:
"""Extract the root type of the operation from the schema.
.. deprecated:: 3.2
Please use `GraphQLSchema.getRootType` instead. Will be removed in v3.3.
"""
operation_type = operation.operation
if operation_type == OperationType.QUERY:
query_type = schema.query_type
if not query_type:
raise GraphQLError(
"Schema does not define the required query root type.", operation
)
return query_type
if operation_type == OperationType.MUTATION:
mutation_type = schema.mutation_type
if not mutation_type:
raise GraphQLError("Schema is not configured for mutations.", operation)
return mutation_type
if operation_type == OperationType.SUBSCRIPTION:
subscription_type = schema.subscription_type
if not subscription_type:
raise GraphQLError("Schema is not configured for subscriptions.", operation)
return subscription_type
raise GraphQLError(
"Can only have query, mutation and subscription operations.", operation
)
@@ -0,0 +1,46 @@
from typing import cast
from ..error import GraphQLError
from ..language import parse
from ..type import GraphQLSchema
from .get_introspection_query import get_introspection_query, IntrospectionQuery
__all__ = ["introspection_from_schema"]
def introspection_from_schema(
schema: GraphQLSchema,
descriptions: bool = True,
specified_by_url: bool = True,
directive_is_repeatable: bool = True,
schema_description: bool = True,
input_value_deprecation: bool = True,
) -> IntrospectionQuery:
"""Build an IntrospectionQuery from a GraphQLSchema
IntrospectionQuery is useful for utilities that care about type and field
relationships, but do not need to traverse through those relationships.
This is the inverse of build_client_schema. The primary use case is outside of the
server context, for instance when doing schema comparisons.
"""
document = parse(
get_introspection_query(
descriptions,
specified_by_url,
directive_is_repeatable,
schema_description,
input_value_deprecation,
)
)
from ..execution.execute import execute_sync, ExecutionResult
result = execute_sync(schema, document)
if not isinstance(result, ExecutionResult): # pragma: no cover
raise RuntimeError("Introspection cannot be executed")
if result.errors: # pragma: no cover
raise result.errors[0]
if not result.data: # pragma: no cover
raise GraphQLError("Introspection did not return a result")
return cast(IntrospectionQuery, result.data)
@@ -0,0 +1,189 @@
from typing import Collection, Dict, Optional, Tuple, Union, cast
from ..language import DirectiveLocation
from ..pyutils import inspect, merge_kwargs, natural_comparison_key
from ..type import (
GraphQLArgument,
GraphQLDirective,
GraphQLEnumType,
GraphQLEnumValue,
GraphQLField,
GraphQLInputField,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInterfaceType,
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLUnionType,
is_enum_type,
is_input_object_type,
is_interface_type,
is_introspection_type,
is_list_type,
is_non_null_type,
is_object_type,
is_scalar_type,
is_union_type,
)
__all__ = ["lexicographic_sort_schema"]
def lexicographic_sort_schema(schema: GraphQLSchema) -> GraphQLSchema:
"""Sort GraphQLSchema.
This function returns a sorted copy of the given GraphQLSchema.
"""
def replace_type(
type_: Union[GraphQLList, GraphQLNonNull, GraphQLNamedType]
) -> Union[GraphQLList, GraphQLNonNull, GraphQLNamedType]:
if is_list_type(type_):
return GraphQLList(replace_type(cast(GraphQLList, type_).of_type))
if is_non_null_type(type_):
return GraphQLNonNull(replace_type(cast(GraphQLNonNull, type_).of_type))
return replace_named_type(cast(GraphQLNamedType, type_))
def replace_named_type(type_: GraphQLNamedType) -> GraphQLNamedType:
return type_map[type_.name]
def replace_maybe_type(
maybe_type: Optional[GraphQLNamedType],
) -> Optional[GraphQLNamedType]:
return maybe_type and replace_named_type(maybe_type)
def sort_directive(directive: GraphQLDirective) -> GraphQLDirective:
return GraphQLDirective(
**merge_kwargs(
directive.to_kwargs(),
locations=sorted(directive.locations, key=sort_by_name_key),
args=sort_args(directive.args),
)
)
def sort_args(args_map: Dict[str, GraphQLArgument]) -> Dict[str, GraphQLArgument]:
args = {}
for name, arg in sorted(args_map.items()):
args[name] = GraphQLArgument(
**merge_kwargs(
arg.to_kwargs(),
type_=replace_type(cast(GraphQLNamedType, arg.type)),
)
)
return args
def sort_fields(fields_map: Dict[str, GraphQLField]) -> Dict[str, GraphQLField]:
fields = {}
for name, field in sorted(fields_map.items()):
fields[name] = GraphQLField(
**merge_kwargs(
field.to_kwargs(),
type_=replace_type(cast(GraphQLNamedType, field.type)),
args=sort_args(field.args),
)
)
return fields
def sort_input_fields(
fields_map: Dict[str, GraphQLInputField]
) -> Dict[str, GraphQLInputField]:
return {
name: GraphQLInputField(
cast(
GraphQLInputType, replace_type(cast(GraphQLNamedType, field.type))
),
description=field.description,
default_value=field.default_value,
ast_node=field.ast_node,
)
for name, field in sorted(fields_map.items())
}
def sort_types(array: Collection[GraphQLNamedType]) -> Tuple[GraphQLNamedType, ...]:
return tuple(
replace_named_type(type_) for type_ in sorted(array, key=sort_by_name_key)
)
def sort_named_type(type_: GraphQLNamedType) -> GraphQLNamedType:
if is_scalar_type(type_) or is_introspection_type(type_):
return type_
if is_object_type(type_):
type_ = cast(GraphQLObjectType, type_)
return GraphQLObjectType(
**merge_kwargs(
type_.to_kwargs(),
interfaces=lambda: sort_types(type_.interfaces),
fields=lambda: sort_fields(type_.fields),
)
)
if is_interface_type(type_):
type_ = cast(GraphQLInterfaceType, type_)
return GraphQLInterfaceType(
**merge_kwargs(
type_.to_kwargs(),
interfaces=lambda: sort_types(type_.interfaces),
fields=lambda: sort_fields(type_.fields),
)
)
if is_union_type(type_):
type_ = cast(GraphQLUnionType, type_)
return GraphQLUnionType(
**merge_kwargs(type_.to_kwargs(), types=lambda: sort_types(type_.types))
)
if is_enum_type(type_):
type_ = cast(GraphQLEnumType, type_)
return GraphQLEnumType(
**merge_kwargs(
type_.to_kwargs(),
values={
name: GraphQLEnumValue(
val.value,
description=val.description,
deprecation_reason=val.deprecation_reason,
ast_node=val.ast_node,
)
for name, val in sorted(type_.values.items())
},
)
)
if is_input_object_type(type_):
type_ = cast(GraphQLInputObjectType, type_)
return GraphQLInputObjectType(
**merge_kwargs(
type_.to_kwargs(),
fields=lambda: sort_input_fields(type_.fields),
)
)
# Not reachable. All possible types have been considered.
raise TypeError(f"Unexpected type: {inspect(type_)}.")
type_map: Dict[str, GraphQLNamedType] = {
type_.name: sort_named_type(type_)
for type_ in sorted(schema.type_map.values(), key=sort_by_name_key)
}
return GraphQLSchema(
types=type_map.values(),
directives=[
sort_directive(directive)
for directive in sorted(schema.directives, key=sort_by_name_key)
],
query=cast(Optional[GraphQLObjectType], replace_maybe_type(schema.query_type)),
mutation=cast(
Optional[GraphQLObjectType], replace_maybe_type(schema.mutation_type)
),
subscription=cast(
Optional[GraphQLObjectType], replace_maybe_type(schema.subscription_type)
),
ast_node=schema.ast_node,
)
def sort_by_name_key(
type_: Union[GraphQLNamedType, GraphQLDirective, DirectiveLocation]
) -> Tuple:
return natural_comparison_key(type_.name)
@@ -0,0 +1,298 @@
from typing import Any, Callable, Dict, List, Optional, Union, cast
from ..language import print_ast, StringValueNode
from ..language.block_string import is_printable_as_block_string
from ..pyutils import inspect
from ..type import (
DEFAULT_DEPRECATION_REASON,
GraphQLArgument,
GraphQLDirective,
GraphQLEnumType,
GraphQLEnumValue,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInterfaceType,
GraphQLNamedType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLSchema,
GraphQLUnionType,
is_enum_type,
is_input_object_type,
is_interface_type,
is_introspection_type,
is_object_type,
is_scalar_type,
is_specified_directive,
is_specified_scalar_type,
is_union_type,
)
from .ast_from_value import ast_from_value
__all__ = ["print_schema", "print_introspection_schema", "print_type", "print_value"]
def print_schema(schema: GraphQLSchema) -> str:
return print_filtered_schema(
schema, lambda n: not is_specified_directive(n), is_defined_type
)
def print_introspection_schema(schema: GraphQLSchema) -> str:
return print_filtered_schema(schema, is_specified_directive, is_introspection_type)
def is_defined_type(type_: GraphQLNamedType) -> bool:
return not is_specified_scalar_type(type_) and not is_introspection_type(type_)
def print_filtered_schema(
schema: GraphQLSchema,
directive_filter: Callable[[GraphQLDirective], bool],
type_filter: Callable[[GraphQLNamedType], bool],
) -> str:
directives = filter(directive_filter, schema.directives)
types = filter(type_filter, schema.type_map.values())
return "\n\n".join(
(
*filter(None, (print_schema_definition(schema),)),
*map(print_directive, directives),
*map(print_type, types),
)
)
def print_schema_definition(schema: GraphQLSchema) -> Optional[str]:
if schema.description is None and is_schema_of_common_names(schema):
return None
operation_types = []
query_type = schema.query_type
if query_type:
operation_types.append(f" query: {query_type.name}")
mutation_type = schema.mutation_type
if mutation_type:
operation_types.append(f" mutation: {mutation_type.name}")
subscription_type = schema.subscription_type
if subscription_type:
operation_types.append(f" subscription: {subscription_type.name}")
return print_description(schema) + "schema {\n" + "\n".join(operation_types) + "\n}"
def is_schema_of_common_names(schema: GraphQLSchema) -> bool:
"""Check whether this schema uses the common naming convention.
GraphQL schema define root types for each type of operation. These types are the
same as any other type and can be named in any manner, however there is a common
naming convention:
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
When using this naming convention, the schema description can be omitted.
"""
query_type = schema.query_type
if query_type and query_type.name != "Query":
return False
mutation_type = schema.mutation_type
if mutation_type and mutation_type.name != "Mutation":
return False
subscription_type = schema.subscription_type
return not subscription_type or subscription_type.name == "Subscription"
def print_type(type_: GraphQLNamedType) -> str:
if is_scalar_type(type_):
type_ = cast(GraphQLScalarType, type_)
return print_scalar(type_)
if is_object_type(type_):
type_ = cast(GraphQLObjectType, type_)
return print_object(type_)
if is_interface_type(type_):
type_ = cast(GraphQLInterfaceType, type_)
return print_interface(type_)
if is_union_type(type_):
type_ = cast(GraphQLUnionType, type_)
return print_union(type_)
if is_enum_type(type_):
type_ = cast(GraphQLEnumType, type_)
return print_enum(type_)
if is_input_object_type(type_):
type_ = cast(GraphQLInputObjectType, type_)
return print_input_object(type_)
# Not reachable. All possible types have been considered.
raise TypeError(f"Unexpected type: {inspect(type_)}.")
def print_scalar(type_: GraphQLScalarType) -> str:
return (
print_description(type_)
+ f"scalar {type_.name}"
+ print_specified_by_url(type_)
)
def print_implemented_interfaces(
type_: Union[GraphQLObjectType, GraphQLInterfaceType]
) -> str:
interfaces = type_.interfaces
return " implements " + " & ".join(i.name for i in interfaces) if interfaces else ""
def print_object(type_: GraphQLObjectType) -> str:
return (
print_description(type_)
+ f"type {type_.name}"
+ print_implemented_interfaces(type_)
+ print_fields(type_)
)
def print_interface(type_: GraphQLInterfaceType) -> str:
return (
print_description(type_)
+ f"interface {type_.name}"
+ print_implemented_interfaces(type_)
+ print_fields(type_)
)
def print_union(type_: GraphQLUnionType) -> str:
types = type_.types
possible_types = " = " + " | ".join(t.name for t in types) if types else ""
return print_description(type_) + f"union {type_.name}" + possible_types
def print_enum(type_: GraphQLEnumType) -> str:
values = [
print_description(value, " ", not i)
+ f" {name}"
+ print_deprecated(value.deprecation_reason)
for i, (name, value) in enumerate(type_.values.items())
]
return print_description(type_) + f"enum {type_.name}" + print_block(values)
def print_input_object(type_: GraphQLInputObjectType) -> str:
fields = [
print_description(field, " ", not i) + " " + print_input_value(name, field)
for i, (name, field) in enumerate(type_.fields.items())
]
return print_description(type_) + f"input {type_.name}" + print_block(fields)
def print_fields(type_: Union[GraphQLObjectType, GraphQLInterfaceType]) -> str:
fields = [
print_description(field, " ", not i)
+ f" {name}"
+ print_args(field.args, " ")
+ f": {field.type}"
+ print_deprecated(field.deprecation_reason)
for i, (name, field) in enumerate(type_.fields.items())
]
return print_block(fields)
def print_block(items: List[str]) -> str:
return " {\n" + "\n".join(items) + "\n}" if items else ""
def print_args(args: Dict[str, GraphQLArgument], indentation: str = "") -> str:
if not args:
return ""
# If every arg does not have a description, print them on one line.
if not any(arg.description for arg in args.values()):
return (
"("
+ ", ".join(print_input_value(name, arg) for name, arg in args.items())
+ ")"
)
return (
"(\n"
+ "\n".join(
print_description(arg, f" {indentation}", not i)
+ f" {indentation}"
+ print_input_value(name, arg)
for i, (name, arg) in enumerate(args.items())
)
+ f"\n{indentation})"
)
def print_input_value(name: str, arg: GraphQLArgument) -> str:
default_ast = ast_from_value(arg.default_value, arg.type)
arg_decl = f"{name}: {arg.type}"
if default_ast:
arg_decl += f" = {print_ast(default_ast)}"
return arg_decl + print_deprecated(arg.deprecation_reason)
def print_directive(directive: GraphQLDirective) -> str:
return (
print_description(directive)
+ f"directive @{directive.name}"
+ print_args(directive.args)
+ (" repeatable" if directive.is_repeatable else "")
+ " on "
+ " | ".join(location.name for location in directive.locations)
)
def print_deprecated(reason: Optional[str]) -> str:
if reason is None:
return ""
if reason != DEFAULT_DEPRECATION_REASON:
ast_value = print_ast(StringValueNode(value=reason))
return f" @deprecated(reason: {ast_value})"
return " @deprecated"
def print_specified_by_url(scalar: GraphQLScalarType) -> str:
if scalar.specified_by_url is None:
return ""
ast_value = print_ast(StringValueNode(value=scalar.specified_by_url))
return f" @specifiedBy(url: {ast_value})"
def print_description(
def_: Union[
GraphQLArgument,
GraphQLDirective,
GraphQLEnumValue,
GraphQLNamedType,
GraphQLSchema,
],
indentation: str = "",
first_in_block: bool = True,
) -> str:
description = def_.description
if description is None:
return ""
block_string = print_ast(
StringValueNode(
value=description, block=is_printable_as_block_string(description)
)
)
prefix = "\n" + indentation if indentation and not first_in_block else indentation
return prefix + block_string.replace("\n", "\n" + indentation) + "\n"
def print_value(value: Any, type_: GraphQLInputType) -> str:
"""@deprecated: Convenience function for printing a Python value"""
return print_ast(ast_from_value(value, type_)) # type: ignore
@@ -0,0 +1,101 @@
from typing import Any, Dict, List, Set
from ..language import (
DocumentNode,
FragmentDefinitionNode,
FragmentSpreadNode,
OperationDefinitionNode,
SelectionSetNode,
Visitor,
visit,
)
__all__ = ["separate_operations"]
DepGraph = Dict[str, List[str]]
def separate_operations(document_ast: DocumentNode) -> Dict[str, DocumentNode]:
"""Separate operations in a given AST document.
This function accepts a single AST document which may contain many operations and
fragments and returns a collection of AST documents each of which contains a single
operation as well the fragment definitions it refers to.
"""
operations: List[OperationDefinitionNode] = []
dep_graph: DepGraph = {}
# Populate metadata and build a dependency graph.
for definition_node in document_ast.definitions:
if isinstance(definition_node, OperationDefinitionNode):
operations.append(definition_node)
elif isinstance(
definition_node, FragmentDefinitionNode
): # pragma: no cover else
dep_graph[definition_node.name.value] = collect_dependencies(
definition_node.selection_set
)
# For each operation, produce a new synthesized AST which includes only what is
# necessary for completing that operation.
separated_document_asts: Dict[str, DocumentNode] = {}
for operation in operations:
dependencies: Set[str] = set()
for fragment_name in collect_dependencies(operation.selection_set):
collect_transitive_dependencies(dependencies, dep_graph, fragment_name)
# Provides the empty string for anonymous operations.
operation_name = operation.name.value if operation.name else ""
# The list of definition nodes to be included for this operation, sorted
# to retain the same order as the original document.
separated_document_asts[operation_name] = DocumentNode(
definitions=[
node
for node in document_ast.definitions
if node is operation
or (
isinstance(node, FragmentDefinitionNode)
and node.name.value in dependencies
)
]
)
return separated_document_asts
def collect_transitive_dependencies(
collected: Set[str], dep_graph: DepGraph, from_name: str
) -> None:
"""Collect transitive dependencies.
From a dependency graph, collects a list of transitive dependencies by recursing
through a dependency graph.
"""
if from_name not in collected:
collected.add(from_name)
immediate_deps = dep_graph.get(from_name)
if immediate_deps is not None:
for to_name in immediate_deps:
collect_transitive_dependencies(collected, dep_graph, to_name)
class DependencyCollector(Visitor):
dependencies: List[str]
def __init__(self) -> None:
super().__init__()
self.dependencies = []
self.add_dependency = self.dependencies.append
def enter_fragment_spread(self, node: FragmentSpreadNode, *_args: Any) -> None:
self.add_dependency(node.name.value)
def collect_dependencies(selection_set: SelectionSetNode) -> List[str]:
collector = DependencyCollector()
visit(selection_set, collector)
return collector.dependencies
@@ -0,0 +1,38 @@
from copy import copy
from typing import Tuple
from ..language import ListValueNode, ObjectFieldNode, ObjectValueNode, ValueNode
from ..pyutils import natural_comparison_key
__all__ = ["sort_value_node"]
def sort_value_node(value_node: ValueNode) -> ValueNode:
"""Sort ValueNode.
This function returns a sorted copy of the given ValueNode
For internal use only.
"""
if isinstance(value_node, ObjectValueNode):
value_node = copy(value_node)
value_node.fields = sort_fields(value_node.fields)
elif isinstance(value_node, ListValueNode):
value_node = copy(value_node)
value_node.values = tuple(sort_value_node(value) for value in value_node.values)
return value_node
def sort_field(field: ObjectFieldNode) -> ObjectFieldNode:
field = copy(field)
field.value = sort_value_node(field.value)
return field
def sort_fields(fields: Tuple[ObjectFieldNode, ...]) -> Tuple[ObjectFieldNode, ...]:
return tuple(
sorted(
(sort_field(field) for field in fields),
key=lambda field: natural_comparison_key(field.name.value),
)
)
@@ -0,0 +1,96 @@
from typing import Union, cast
from ..language import Lexer, TokenKind
from ..language.source import Source, is_source
from ..language.block_string import print_block_string
from ..language.lexer import is_punctuator_token_kind
__all__ = ["strip_ignored_characters"]
def strip_ignored_characters(source: Union[str, Source]) -> str:
"""Strip characters that are ignored anyway.
Strips characters that are not significant to the validity or execution
of a GraphQL document:
- UnicodeBOM
- WhiteSpace
- LineTerminator
- Comment
- Comma
- BlockString indentation
Note: It is required to have a delimiter character between neighboring
non-punctuator tokes and this function always uses single space as delimiter.
It is guaranteed that both input and output documents if parsed would result
in the exact same AST except for nodes location.
Warning: It is guaranteed that this function will always produce stable results.
However, it's not guaranteed that it will stay the same between different
releases due to bugfixes or changes in the GraphQL specification.
""" '''
Query example::
query SomeQuery($foo: String!, $bar: String) {
someField(foo: $foo, bar: $bar) {
a
b {
c
d
}
}
}
Becomes::
query SomeQuery($foo:String!$bar:String){someField(foo:$foo bar:$bar){a b{c d}}}
SDL example::
"""
Type description
"""
type Foo {
"""
Field description
"""
bar: String
}
Becomes::
"""Type description""" type Foo{"""Field description""" bar:String}
'''
source = cast(Source, source) if is_source(source) else Source(cast(str, source))
body = source.body
lexer = Lexer(source)
stripped_body = ""
was_last_added_token_non_punctuator = False
while lexer.advance().kind != TokenKind.EOF:
current_token = lexer.token
token_kind = current_token.kind
# Every two non-punctuator tokens should have space between them.
# Also prevent case of non-punctuator token following by spread resulting
# in invalid token (e.g.`1...` is invalid Float token).
is_non_punctuator = not is_punctuator_token_kind(current_token.kind)
if was_last_added_token_non_punctuator and (
is_non_punctuator or current_token.kind == TokenKind.SPREAD
):
stripped_body += " "
token_body = body[current_token.start : current_token.end]
if token_kind == TokenKind.BLOCK_STRING:
stripped_body += print_block_string(
current_token.value or "", minimize=True
)
else:
stripped_body += token_body
was_last_added_token_non_punctuator = is_non_punctuator
return stripped_body
@@ -0,0 +1,131 @@
from typing import cast
from ..type import (
GraphQLAbstractType,
GraphQLCompositeType,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLType,
is_abstract_type,
is_interface_type,
is_list_type,
is_non_null_type,
is_object_type,
)
__all__ = ["is_equal_type", "is_type_sub_type_of", "do_types_overlap"]
def is_equal_type(type_a: GraphQLType, type_b: GraphQLType) -> bool:
"""Check whether two types are equal.
Provided two types, return true if the types are equal (invariant)."""
# Equivalent types are equal.
if type_a is type_b:
return True
# If either type is non-null, the other must also be non-null.
if is_non_null_type(type_a) and is_non_null_type(type_b):
# noinspection PyUnresolvedReferences
return is_equal_type(type_a.of_type, type_b.of_type) # type:ignore
# If either type is a list, the other must also be a list.
if is_list_type(type_a) and is_list_type(type_b):
# noinspection PyUnresolvedReferences
return is_equal_type(type_a.of_type, type_b.of_type) # type:ignore
# Otherwise the types are not equal.
return False
def is_type_sub_type_of(
schema: GraphQLSchema, maybe_subtype: GraphQLType, super_type: GraphQLType
) -> bool:
"""Check whether a type is subtype of another type in a given schema.
Provided a type and a super type, return true if the first type is either equal or
a subset of the second super type (covariant).
"""
# Equivalent type is a valid subtype
if maybe_subtype is super_type:
return True
# If super_type is non-null, maybe_subtype must also be non-null.
if is_non_null_type(super_type):
if is_non_null_type(maybe_subtype):
return is_type_sub_type_of(
schema,
cast(GraphQLNonNull, maybe_subtype).of_type,
cast(GraphQLNonNull, super_type).of_type,
)
return False
elif is_non_null_type(maybe_subtype):
# If super_type is nullable, maybe_subtype may be non-null or nullable.
return is_type_sub_type_of(
schema, cast(GraphQLNonNull, maybe_subtype).of_type, super_type
)
# If super_type type is a list, maybeSubType type must also be a list.
if is_list_type(super_type):
if is_list_type(maybe_subtype):
return is_type_sub_type_of(
schema,
cast(GraphQLList, maybe_subtype).of_type,
cast(GraphQLList, super_type).of_type,
)
return False
elif is_list_type(maybe_subtype):
# If super_type is not a list, maybe_subtype must also be not a list.
return False
# If super_type type is abstract, check if it is super type of maybe_subtype.
# Otherwise, the child type is not a valid subtype of the parent type.
return (
is_abstract_type(super_type)
and (is_interface_type(maybe_subtype) or is_object_type(maybe_subtype))
and schema.is_sub_type(
cast(GraphQLAbstractType, super_type),
cast(GraphQLObjectType, maybe_subtype),
)
)
def do_types_overlap(
schema: GraphQLSchema, type_a: GraphQLCompositeType, type_b: GraphQLCompositeType
) -> bool:
"""Check whether two types overlap in a given schema.
Provided two composite types, determine if they "overlap". Two composite types
overlap when the Sets of possible concrete types for each intersect.
This is often used to determine if a fragment of a given type could possibly be
visited in a context of another type.
This function is commutative.
"""
# Equivalent types overlap
if type_a is type_b:
return True
if is_abstract_type(type_a):
type_a = cast(GraphQLAbstractType, type_a)
if is_abstract_type(type_b):
# If both types are abstract, then determine if there is any intersection
# between possible concrete types of each.
type_b = cast(GraphQLAbstractType, type_b)
return any(
schema.is_sub_type(type_b, type_)
for type_ in schema.get_possible_types(type_a)
)
# Determine if latter type is a possible concrete type of the former.
return schema.is_sub_type(type_a, type_b)
if is_abstract_type(type_b):
# Determine if former type is a possible concrete type of the latter.
type_b = cast(GraphQLAbstractType, type_b)
return schema.is_sub_type(type_b, type_a)
# Otherwise the types do not overlap.
return False
@@ -0,0 +1,65 @@
from typing import Optional, cast, overload
from ..language import ListTypeNode, NamedTypeNode, NonNullTypeNode, TypeNode
from ..pyutils import inspect
from ..type import (
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLNullableType,
GraphQLSchema,
GraphQLType,
)
__all__ = ["type_from_ast"]
@overload
def type_from_ast(
schema: GraphQLSchema, type_node: NamedTypeNode
) -> Optional[GraphQLNamedType]: ...
@overload
def type_from_ast(
schema: GraphQLSchema, type_node: ListTypeNode
) -> Optional[GraphQLList]: ...
@overload
def type_from_ast(
schema: GraphQLSchema, type_node: NonNullTypeNode
) -> Optional[GraphQLNonNull]: ...
@overload
def type_from_ast(
schema: GraphQLSchema, type_node: TypeNode
) -> Optional[GraphQLType]: ...
def type_from_ast(
schema: GraphQLSchema,
type_node: TypeNode,
) -> Optional[GraphQLType]:
"""Get the GraphQL type definition from an AST node.
Given a Schema and an AST node describing a type, return a GraphQLType definition
which applies to that type. For example, if provided the parsed AST node for
``[User]``, a GraphQLList instance will be returned, containing the type called
"User" found in the schema. If a type called "User" is not found in the schema,
then None will be returned.
"""
inner_type: Optional[GraphQLType]
if isinstance(type_node, ListTypeNode):
inner_type = type_from_ast(schema, type_node.type)
return GraphQLList(inner_type) if inner_type else None
if isinstance(type_node, NonNullTypeNode):
inner_type = type_from_ast(schema, type_node.type)
inner_type = cast(GraphQLNullableType, inner_type)
return GraphQLNonNull(inner_type) if inner_type else None
if isinstance(type_node, NamedTypeNode):
return schema.get_type(type_node.name.value)
# Not reachable. All possible type nodes have been considered.
raise TypeError(f"Unexpected type node: {inspect(type_node)}.")
@@ -0,0 +1,321 @@
from typing import Any, Callable, List, Optional, Union, cast
from ..language import (
ArgumentNode,
DirectiveNode,
EnumValueNode,
FieldNode,
InlineFragmentNode,
ListValueNode,
Node,
ObjectFieldNode,
OperationDefinitionNode,
SelectionSetNode,
VariableDefinitionNode,
Visitor,
)
from ..pyutils import Undefined
from ..type import (
GraphQLArgument,
GraphQLCompositeType,
GraphQLDirective,
GraphQLEnumType,
GraphQLEnumValue,
GraphQLField,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInterfaceType,
GraphQLList,
GraphQLObjectType,
GraphQLOutputType,
GraphQLSchema,
GraphQLType,
is_composite_type,
is_input_type,
is_output_type,
get_named_type,
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
is_object_type,
is_interface_type,
get_nullable_type,
is_list_type,
is_input_object_type,
is_enum_type,
)
from .type_from_ast import type_from_ast
__all__ = ["TypeInfo", "TypeInfoVisitor"]
GetFieldDefFn = Callable[
[GraphQLSchema, GraphQLType, FieldNode], Optional[GraphQLField]
]
class TypeInfo:
"""Utility class for keeping track of type definitions.
TypeInfo is a utility class which, given a GraphQL schema, can keep track of the
current field and type definitions at any point in a GraphQL document AST during
a recursive descent by calling :meth:`enter(node) <.TypeInfo.enter>` and
:meth:`leave(node) <.TypeInfo.leave>`.
"""
def __init__(
self,
schema: GraphQLSchema,
initial_type: Optional[GraphQLType] = None,
get_field_def_fn: Optional[GetFieldDefFn] = None,
) -> None:
"""Initialize the TypeInfo for the given GraphQL schema.
Initial type may be provided in rare cases to facilitate traversals beginning
somewhere other than documents.
The optional last parameter is deprecated and will be removed in v3.3.
"""
self._schema = schema
self._type_stack: List[Optional[GraphQLOutputType]] = []
self._parent_type_stack: List[Optional[GraphQLCompositeType]] = []
self._input_type_stack: List[Optional[GraphQLInputType]] = []
self._field_def_stack: List[Optional[GraphQLField]] = []
self._default_value_stack: List[Any] = []
self._directive: Optional[GraphQLDirective] = None
self._argument: Optional[GraphQLArgument] = None
self._enum_value: Optional[GraphQLEnumValue] = None
self._get_field_def: GetFieldDefFn = get_field_def_fn or get_field_def
if initial_type:
if is_input_type(initial_type):
self._input_type_stack.append(cast(GraphQLInputType, initial_type))
if is_composite_type(initial_type):
self._parent_type_stack.append(cast(GraphQLCompositeType, initial_type))
if is_output_type(initial_type):
self._type_stack.append(cast(GraphQLOutputType, initial_type))
def get_type(self) -> Optional[GraphQLOutputType]:
if self._type_stack:
return self._type_stack[-1]
return None
def get_parent_type(self) -> Optional[GraphQLCompositeType]:
if self._parent_type_stack:
return self._parent_type_stack[-1]
return None
def get_input_type(self) -> Optional[GraphQLInputType]:
if self._input_type_stack:
return self._input_type_stack[-1]
return None
def get_parent_input_type(self) -> Optional[GraphQLInputType]:
if len(self._input_type_stack) > 1:
return self._input_type_stack[-2]
return None
def get_field_def(self) -> Optional[GraphQLField]:
if self._field_def_stack:
return self._field_def_stack[-1]
return None
def get_default_value(self) -> Any:
if self._default_value_stack:
return self._default_value_stack[-1]
return None
def get_directive(self) -> Optional[GraphQLDirective]:
return self._directive
def get_argument(self) -> Optional[GraphQLArgument]:
return self._argument
def get_enum_value(self) -> Optional[GraphQLEnumValue]:
return self._enum_value
def enter(self, node: Node) -> None:
method = getattr(self, "enter_" + node.kind, None)
if method:
method(node)
def leave(self, node: Node) -> None:
method = getattr(self, "leave_" + node.kind, None)
if method:
method()
# noinspection PyUnusedLocal
def enter_selection_set(self, node: SelectionSetNode) -> None:
named_type = get_named_type(self.get_type())
self._parent_type_stack.append(
cast(GraphQLCompositeType, named_type)
if is_composite_type(named_type)
else None
)
def enter_field(self, node: FieldNode) -> None:
parent_type = self.get_parent_type()
if parent_type:
field_def = self._get_field_def(self._schema, parent_type, node)
field_type = field_def.type if field_def else None
else:
field_def = field_type = None
self._field_def_stack.append(field_def)
self._type_stack.append(field_type if is_output_type(field_type) else None)
def enter_directive(self, node: DirectiveNode) -> None:
self._directive = self._schema.get_directive(node.name.value)
def enter_operation_definition(self, node: OperationDefinitionNode) -> None:
root_type = self._schema.get_root_type(node.operation)
self._type_stack.append(root_type if is_object_type(root_type) else None)
def enter_inline_fragment(self, node: InlineFragmentNode) -> None:
type_condition_ast = node.type_condition
output_type = (
type_from_ast(self._schema, type_condition_ast)
if type_condition_ast
else get_named_type(self.get_type())
)
self._type_stack.append(
cast(GraphQLOutputType, output_type)
if is_output_type(output_type)
else None
)
enter_fragment_definition = enter_inline_fragment
def enter_variable_definition(self, node: VariableDefinitionNode) -> None:
input_type = type_from_ast(self._schema, node.type)
self._input_type_stack.append(
cast(GraphQLInputType, input_type) if is_input_type(input_type) else None
)
def enter_argument(self, node: ArgumentNode) -> None:
field_or_directive = self.get_directive() or self.get_field_def()
if field_or_directive:
arg_def = field_or_directive.args.get(node.name.value)
arg_type = arg_def.type if arg_def else None
else:
arg_def = arg_type = None
self._argument = arg_def
self._default_value_stack.append(
arg_def.default_value if arg_def else Undefined
)
self._input_type_stack.append(arg_type if is_input_type(arg_type) else None)
# noinspection PyUnusedLocal
def enter_list_value(self, node: ListValueNode) -> None:
list_type = get_nullable_type(self.get_input_type()) # type: ignore
item_type = (
cast(GraphQLList, list_type).of_type
if is_list_type(list_type)
else list_type
)
# List positions never have a default value.
self._default_value_stack.append(Undefined)
self._input_type_stack.append(item_type if is_input_type(item_type) else None)
def enter_object_field(self, node: ObjectFieldNode) -> None:
object_type = get_named_type(self.get_input_type())
if is_input_object_type(object_type):
input_field = cast(GraphQLInputObjectType, object_type).fields.get(
node.name.value
)
input_field_type = input_field.type if input_field else None
else:
input_field = input_field_type = None
self._default_value_stack.append(
input_field.default_value if input_field else Undefined
)
self._input_type_stack.append(
input_field_type if is_input_type(input_field_type) else None
)
def enter_enum_value(self, node: EnumValueNode) -> None:
enum_type = get_named_type(self.get_input_type())
if is_enum_type(enum_type):
enum_value = cast(GraphQLEnumType, enum_type).values.get(node.value)
else:
enum_value = None
self._enum_value = enum_value
def leave_selection_set(self) -> None:
del self._parent_type_stack[-1:]
def leave_field(self) -> None:
del self._field_def_stack[-1:]
del self._type_stack[-1:]
def leave_directive(self) -> None:
self._directive = None
def leave_operation_definition(self) -> None:
del self._type_stack[-1:]
leave_inline_fragment = leave_operation_definition
leave_fragment_definition = leave_operation_definition
def leave_variable_definition(self) -> None:
del self._input_type_stack[-1:]
def leave_argument(self) -> None:
self._argument = None
del self._default_value_stack[-1:]
del self._input_type_stack[-1:]
def leave_list_value(self) -> None:
del self._default_value_stack[-1:]
del self._input_type_stack[-1:]
leave_object_field = leave_list_value
def leave_enum_value(self) -> None:
self._enum_value = None
def get_field_def(
schema: GraphQLSchema, parent_type: GraphQLType, field_node: FieldNode
) -> Optional[GraphQLField]:
"""Get field definition.
Not exactly the same as the executor's definition of
:func:`graphql.execution.get_field_def`, in this statically evaluated environment
we do not always have an Object type, and need to handle Interface and Union types.
"""
name = field_node.name.value
if name == "__schema" and schema.query_type is parent_type:
return SchemaMetaFieldDef
if name == "__type" and schema.query_type is parent_type:
return TypeMetaFieldDef
if name == "__typename" and is_composite_type(parent_type):
return TypeNameMetaFieldDef
if is_object_type(parent_type) or is_interface_type(parent_type):
parent_type = cast(Union[GraphQLObjectType, GraphQLInterfaceType], parent_type)
return parent_type.fields.get(name)
return None
class TypeInfoVisitor(Visitor):
"""A visitor which maintains a provided TypeInfo."""
def __init__(self, type_info: "TypeInfo", visitor: Visitor):
super().__init__()
self.type_info = type_info
self.visitor = visitor
def enter(self, node: Node, *args: Any) -> Any:
self.type_info.enter(node)
fn = self.visitor.get_enter_leave_for_kind(node.kind).enter
if fn:
result = fn(node, *args)
if result is not None:
self.type_info.leave(node)
if isinstance(result, Node):
self.type_info.enter(result)
return result
def leave(self, node: Node, *args: Any) -> Any:
fn = self.visitor.get_enter_leave_for_kind(node.kind).leave
result = fn(node, *args) if fn else None
self.type_info.leave(node)
return result
@@ -0,0 +1,149 @@
from typing import Any, Dict, List, Optional, cast
from ..language import (
ListValueNode,
NullValueNode,
ObjectValueNode,
ValueNode,
VariableNode,
)
from ..pyutils import inspect, Undefined
from ..type import (
GraphQLInputObjectType,
GraphQLInputType,
GraphQLList,
GraphQLNonNull,
GraphQLScalarType,
is_input_object_type,
is_leaf_type,
is_list_type,
is_non_null_type,
)
__all__ = ["value_from_ast"]
def value_from_ast(
value_node: Optional[ValueNode],
type_: GraphQLInputType,
variables: Optional[Dict[str, Any]] = None,
) -> Any:
"""Produce a Python value given a GraphQL Value AST.
A GraphQL type must be provided, which will be used to interpret different GraphQL
Value literals.
Returns ``Undefined`` when the value could not be validly coerced according
to the provided type.
=================== ============== ================
GraphQL Value JSON Value Python Value
=================== ============== ================
Input Object Object dict
List Array list
Boolean Boolean bool
String String str
Int / Float Number int / float
Enum Value Mixed Any
NullValue null None
=================== ============== ================
"""
if not value_node:
# When there is no node, then there is also no value.
# Importantly, this is different from returning the value null.
return Undefined
if isinstance(value_node, VariableNode):
variable_name = value_node.name.value
if not variables:
return Undefined
variable_value = variables.get(variable_name, Undefined)
if variable_value is None and is_non_null_type(type_):
return Undefined
# Note: This does no further checking that this variable is correct.
# This assumes that this query has been validated and the variable usage here
# is of the correct type.
return variable_value
if is_non_null_type(type_):
if isinstance(value_node, NullValueNode):
return Undefined
type_ = cast(GraphQLNonNull, type_)
return value_from_ast(value_node, type_.of_type, variables)
if isinstance(value_node, NullValueNode):
return None # This is explicitly returning the value None.
if is_list_type(type_):
type_ = cast(GraphQLList, type_)
item_type = type_.of_type
if isinstance(value_node, ListValueNode):
coerced_values: List[Any] = []
append_value = coerced_values.append
for item_node in value_node.values:
if is_missing_variable(item_node, variables):
# If an array contains a missing variable, it is either coerced to
# None or if the item type is non-null, it is considered invalid.
if is_non_null_type(item_type):
return Undefined
append_value(None)
else:
item_value = value_from_ast(item_node, item_type, variables)
if item_value is Undefined:
return Undefined
append_value(item_value)
return coerced_values
coerced_value = value_from_ast(value_node, item_type, variables)
if coerced_value is Undefined:
return Undefined
return [coerced_value]
if is_input_object_type(type_):
if not isinstance(value_node, ObjectValueNode):
return Undefined
type_ = cast(GraphQLInputObjectType, type_)
coerced_obj: Dict[str, Any] = {}
fields = type_.fields
field_nodes = {field.name.value: field for field in value_node.fields}
for field_name, field in fields.items():
field_node = field_nodes.get(field_name)
if not field_node or is_missing_variable(field_node.value, variables):
if field.default_value is not Undefined:
# Use out name as name if it exists (extension of GraphQL.js).
coerced_obj[field.out_name or field_name] = field.default_value
elif is_non_null_type(field.type): # pragma: no cover else
return Undefined
continue
field_value = value_from_ast(field_node.value, field.type, variables)
if field_value is Undefined:
return Undefined
coerced_obj[field.out_name or field_name] = field_value
return type_.out_type(coerced_obj)
if is_leaf_type(type_):
# Scalars fulfill parsing a literal value via `parse_literal()`. Invalid values
# represent a failure to parse correctly, in which case Undefined is returned.
type_ = cast(GraphQLScalarType, type_)
# noinspection PyBroadException
try:
if variables:
result = type_.parse_literal(value_node, variables)
else:
result = type_.parse_literal(value_node)
except Exception:
return Undefined
return result
# Not reachable. All possible input types have been considered.
raise TypeError(f"Unexpected input type: {inspect(type_)}.")
def is_missing_variable(
value_node: ValueNode, variables: Optional[Dict[str, Any]] = None
) -> bool:
"""Check if ``value_node`` is a variable not defined in the ``variables`` dict."""
return isinstance(value_node, VariableNode) and (
not variables or variables.get(value_node.name.value, Undefined) is Undefined
)
@@ -0,0 +1,110 @@
from math import nan
from typing import Any, Callable, Dict, Optional, Union
from ..language import (
ValueNode,
BooleanValueNode,
EnumValueNode,
FloatValueNode,
IntValueNode,
ListValueNode,
NullValueNode,
ObjectValueNode,
StringValueNode,
VariableNode,
)
from ..pyutils import inspect, Undefined
__all__ = ["value_from_ast_untyped"]
def value_from_ast_untyped(
value_node: ValueNode, variables: Optional[Dict[str, Any]] = None
) -> Any:
"""Produce a Python value given a GraphQL Value AST.
Unlike :func:`~graphql.utilities.value_from_ast`, no type is provided.
The resulting Python value will reflect the provided GraphQL value AST.
=================== ============== ================
GraphQL Value JSON Value Python Value
=================== ============== ================
Input Object Object dict
List Array list
Boolean Boolean bool
String / Enum String str
Int / Float Number int / float
Null null None
=================== ============== ================
"""
func = _value_from_kind_functions.get(value_node.kind)
if func:
return func(value_node, variables)
# Not reachable. All possible value nodes have been considered.
raise TypeError( # pragma: no cover
f"Unexpected value node: {inspect(value_node)}."
)
def value_from_null(_value_node: NullValueNode, _variables: Any) -> Any:
return None
def value_from_int(value_node: IntValueNode, _variables: Any) -> Any:
try:
return int(value_node.value)
except ValueError:
return nan
def value_from_float(value_node: FloatValueNode, _variables: Any) -> Any:
try:
return float(value_node.value)
except ValueError:
return nan
def value_from_string(
value_node: Union[BooleanValueNode, EnumValueNode, StringValueNode], _variables: Any
) -> Any:
return value_node.value
def value_from_list(
value_node: ListValueNode, variables: Optional[Dict[str, Any]]
) -> Any:
return [value_from_ast_untyped(node, variables) for node in value_node.values]
def value_from_object(
value_node: ObjectValueNode, variables: Optional[Dict[str, Any]]
) -> Any:
return {
field.name.value: value_from_ast_untyped(field.value, variables)
for field in value_node.fields
}
def value_from_variable(
value_node: VariableNode, variables: Optional[Dict[str, Any]]
) -> Any:
variable_name = value_node.name.value
if not variables:
return Undefined
return variables.get(variable_name, Undefined)
_value_from_kind_functions: Dict[str, Callable] = {
"null_value": value_from_null,
"int_value": value_from_int,
"float_value": value_from_float,
"string_value": value_from_string,
"enum_value": value_from_string,
"boolean_value": value_from_string,
"list_value": value_from_list,
"object_value": value_from_object,
"variable": value_from_variable,
}