This commit is contained in:
Christian Mantha
2026-03-02 19:10:52 -05:00
commit 2ca0b9ef7c
28907 changed files with 5233713 additions and 0 deletions

View File

@@ -0,0 +1,789 @@
"""GraphQL-core
The primary :mod:`graphql` package includes everything you need to define a GraphQL
schema and fulfill GraphQL requests.
GraphQL-core provides a reference implementation for the GraphQL specification
but is also a useful utility for operating on GraphQL files and building sophisticated
tools.
This top-level package exports a general purpose function for fulfilling all steps
of the GraphQL specification in a single operation, but also includes utilities
for every part of the GraphQL specification:
- Parsing the GraphQL language.
- Building a GraphQL type schema.
- Validating a GraphQL request against a type schema.
- Executing a GraphQL request against a type schema.
This also includes utility functions for operating on GraphQL types and GraphQL
documents to facilitate building tools.
You may also import from each sub-package directly. For example, the following two
import statements are equivalent::
from graphql import parse
from graphql.language import parse
The sub-packages of GraphQL-core 3 are:
- :mod:`graphql.language`: Parse and operate on the GraphQL language.
- :mod:`graphql.type`: Define GraphQL types and schema.
- :mod:`graphql.validation`: The Validation phase of fulfilling a GraphQL result.
- :mod:`graphql.execution`: The Execution phase of fulfilling a GraphQL request.
- :mod:`graphql.error`: Creating and formatting GraphQL errors.
- :mod:`graphql.utilities`:
Common useful computations upon the GraphQL language and type objects.
"""
# The GraphQL-core 3 and GraphQL.js version info.
from .version import version, version_info, version_js, version_info_js
# Utilities for compatibility with the Python language.
from .pyutils import Undefined, UndefinedType
# Create, format, and print GraphQL errors.
from .error import (
GraphQLError,
GraphQLErrorExtensions,
GraphQLFormattedError,
GraphQLSyntaxError,
located_error,
)
# Parse and operate on GraphQL language source files.
from .language import (
Source,
get_location,
# Print source location
print_location,
print_source_location,
# Lex
Lexer,
TokenKind,
# Parse
parse,
parse_value,
parse_const_value,
parse_type,
# Print
print_ast,
# Visit
visit,
ParallelVisitor,
Visitor,
VisitorAction,
VisitorKeyMap,
BREAK,
SKIP,
REMOVE,
IDLE,
DirectiveLocation,
# Predicates
is_definition_node,
is_executable_definition_node,
is_selection_node,
is_value_node,
is_const_value_node,
is_type_node,
is_type_system_definition_node,
is_type_definition_node,
is_type_system_extension_node,
is_type_extension_node,
# Types
SourceLocation,
Location,
Token,
# AST nodes
Node,
# Each kind of AST node
NameNode,
DocumentNode,
DefinitionNode,
ExecutableDefinitionNode,
OperationDefinitionNode,
OperationType,
VariableDefinitionNode,
VariableNode,
SelectionSetNode,
SelectionNode,
FieldNode,
ArgumentNode,
ConstArgumentNode,
FragmentSpreadNode,
InlineFragmentNode,
FragmentDefinitionNode,
ValueNode,
ConstValueNode,
IntValueNode,
FloatValueNode,
StringValueNode,
BooleanValueNode,
NullValueNode,
EnumValueNode,
ListValueNode,
ConstListValueNode,
ObjectValueNode,
ConstObjectValueNode,
ObjectFieldNode,
ConstObjectFieldNode,
DirectiveNode,
ConstDirectiveNode,
TypeNode,
NamedTypeNode,
ListTypeNode,
NonNullTypeNode,
TypeSystemDefinitionNode,
SchemaDefinitionNode,
OperationTypeDefinitionNode,
TypeDefinitionNode,
ScalarTypeDefinitionNode,
ObjectTypeDefinitionNode,
FieldDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
UnionTypeDefinitionNode,
EnumTypeDefinitionNode,
EnumValueDefinitionNode,
InputObjectTypeDefinitionNode,
DirectiveDefinitionNode,
TypeSystemExtensionNode,
SchemaExtensionNode,
TypeExtensionNode,
ScalarTypeExtensionNode,
ObjectTypeExtensionNode,
InterfaceTypeExtensionNode,
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
)
# Utilities for operating on GraphQL type schema and parsed sources.
from .utilities import (
# Produce the GraphQL query recommended for a full schema introspection.
# Accepts optional IntrospectionOptions.
get_introspection_query,
IntrospectionQuery,
# Get the target Operation from a Document.
get_operation_ast,
# Get the Type for the target Operation AST.
get_operation_root_type,
# Convert a GraphQLSchema to an IntrospectionQuery.
introspection_from_schema,
# Build a GraphQLSchema from an introspection result.
build_client_schema,
# Build a GraphQLSchema from a parsed GraphQL Schema language AST.
build_ast_schema,
# Build a GraphQLSchema from a GraphQL schema language document.
build_schema,
# Extend an existing GraphQLSchema from a parsed GraphQL Schema language AST.
extend_schema,
# Sort a GraphQLSchema.
lexicographic_sort_schema,
# Print a GraphQLSchema to GraphQL Schema language.
print_schema,
# Print a GraphQLType to GraphQL Schema language.
print_type,
# Prints the built-in introspection schema in the Schema Language format.
print_introspection_schema,
# Create a GraphQLType from a GraphQL language AST.
type_from_ast,
# Convert a language AST to a dictionary.
ast_to_dict,
# Create a Python value from a GraphQL language AST with a Type.
value_from_ast,
# Create a Python value from a GraphQL language AST without a Type.
value_from_ast_untyped,
# Create a GraphQL language AST from a Python value.
ast_from_value,
# A helper to use within recursive-descent visitors which need to be aware of the
# GraphQL type system.
TypeInfo,
TypeInfoVisitor,
# Coerce a Python value to a GraphQL type, or produce errors.
coerce_input_value,
# Concatenates multiple ASTs together.
concat_ast,
# Separate an AST into an AST per Operation.
separate_operations,
# Strip characters that are not significant to the validity or execution
# of a GraphQL document.
strip_ignored_characters,
# Comparators for types
is_equal_type,
is_type_sub_type_of,
do_types_overlap,
# Assert a string is a valid GraphQL name.
assert_valid_name,
# Determine if a string is a valid GraphQL name.
is_valid_name_error,
# Compare two GraphQLSchemas and detect breaking changes.
BreakingChange,
BreakingChangeType,
DangerousChange,
DangerousChangeType,
find_breaking_changes,
find_dangerous_changes,
)
# Create and operate on GraphQL type definitions and schema.
from .type import (
# Definitions
GraphQLSchema,
GraphQLDirective,
GraphQLScalarType,
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLUnionType,
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
# Standard GraphQL Scalars
specified_scalar_types,
GraphQLInt,
GraphQLFloat,
GraphQLString,
GraphQLBoolean,
GraphQLID,
# Int boundaries constants
GRAPHQL_MAX_INT,
GRAPHQL_MIN_INT,
# Built-in Directives defined by the Spec
specified_directives,
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLDeprecatedDirective,
GraphQLSpecifiedByDirective,
GraphQLOneOfDirective,
# "Enum" of Type Kinds
TypeKind,
# Constant Deprecation Reason
DEFAULT_DEPRECATION_REASON,
# GraphQL Types for introspection.
introspection_types,
# Meta-field definitions.
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
# Predicates
is_schema,
is_directive,
is_type,
is_scalar_type,
is_object_type,
is_interface_type,
is_union_type,
is_enum_type,
is_input_object_type,
is_list_type,
is_non_null_type,
is_input_type,
is_output_type,
is_leaf_type,
is_composite_type,
is_abstract_type,
is_wrapping_type,
is_nullable_type,
is_named_type,
is_required_argument,
is_required_input_field,
is_specified_scalar_type,
is_introspection_type,
is_specified_directive,
# Assertions
assert_schema,
assert_directive,
assert_type,
assert_scalar_type,
assert_object_type,
assert_interface_type,
assert_union_type,
assert_enum_type,
assert_input_object_type,
assert_list_type,
assert_non_null_type,
assert_input_type,
assert_output_type,
assert_leaf_type,
assert_composite_type,
assert_abstract_type,
assert_wrapping_type,
assert_nullable_type,
assert_named_type,
# Un-modifiers
get_nullable_type,
get_named_type,
# Thunk handling
resolve_thunk,
# Validate GraphQL schema.
validate_schema,
assert_valid_schema,
# Uphold the spec rules about naming
assert_name,
assert_enum_value_name,
# Types
GraphQLType,
GraphQLInputType,
GraphQLOutputType,
GraphQLLeafType,
GraphQLCompositeType,
GraphQLAbstractType,
GraphQLWrappingType,
GraphQLNullableType,
GraphQLNamedType,
GraphQLNamedInputType,
GraphQLNamedOutputType,
Thunk,
ThunkCollection,
ThunkMapping,
GraphQLArgument,
GraphQLArgumentMap,
GraphQLEnumValue,
GraphQLEnumValueMap,
GraphQLEnumValuesDefinition,
GraphQLField,
GraphQLFieldMap,
GraphQLFieldResolver,
GraphQLInputField,
GraphQLInputFieldMap,
GraphQLScalarSerializer,
GraphQLScalarValueParser,
GraphQLScalarLiteralParser,
GraphQLIsTypeOfFn,
GraphQLResolveInfo,
ResponsePath,
GraphQLTypeResolver,
# Keyword args
GraphQLArgumentKwargs,
GraphQLDirectiveKwargs,
GraphQLEnumTypeKwargs,
GraphQLEnumValueKwargs,
GraphQLFieldKwargs,
GraphQLInputFieldKwargs,
GraphQLInputObjectTypeKwargs,
GraphQLInterfaceTypeKwargs,
GraphQLNamedTypeKwargs,
GraphQLObjectTypeKwargs,
GraphQLScalarTypeKwargs,
GraphQLSchemaKwargs,
GraphQLUnionTypeKwargs,
)
# Validate GraphQL queries.
from .validation import (
validate,
ValidationContext,
ValidationRule,
ASTValidationRule,
SDLValidationRule,
# All validation rules in the GraphQL Specification.
specified_rules,
recommended_rules,
# Individual validation rules.
ExecutableDefinitionsRule,
FieldsOnCorrectTypeRule,
FragmentsOnCompositeTypesRule,
KnownArgumentNamesRule,
KnownDirectivesRule,
KnownFragmentNamesRule,
KnownTypeNamesRule,
LoneAnonymousOperationRule,
NoFragmentCyclesRule,
NoUndefinedVariablesRule,
NoUnusedFragmentsRule,
NoUnusedVariablesRule,
OverlappingFieldsCanBeMergedRule,
PossibleFragmentSpreadsRule,
ProvidedRequiredArgumentsRule,
ScalarLeafsRule,
SingleFieldSubscriptionsRule,
UniqueArgumentNamesRule,
UniqueDirectivesPerLocationRule,
UniqueFragmentNamesRule,
UniqueInputFieldNamesRule,
UniqueOperationNamesRule,
UniqueVariableNamesRule,
ValuesOfCorrectTypeRule,
VariablesAreInputTypesRule,
VariablesInAllowedPositionRule,
# SDL-specific validation rules
LoneSchemaDefinitionRule,
UniqueOperationTypesRule,
UniqueTypeNamesRule,
UniqueEnumValueNamesRule,
UniqueFieldDefinitionNamesRule,
UniqueArgumentDefinitionNamesRule,
UniqueDirectiveNamesRule,
PossibleTypeExtensionsRule,
# Custom validation rules
NoDeprecatedCustomRule,
NoSchemaIntrospectionCustomRule,
# Recommended validation rules
MaxIntrospectionDepthRule,
)
# Execute GraphQL documents.
from .execution import (
execute,
execute_sync,
default_field_resolver,
default_type_resolver,
get_argument_values,
get_directive_values,
get_variable_values,
# Types
ExecutionContext,
ExecutionResult,
FormattedExecutionResult,
# Subscription
subscribe,
create_source_event_stream,
MapAsyncIterator,
# Middleware
Middleware,
MiddlewareManager,
)
# The primary entry point into fulfilling a GraphQL request.
from .graphql import graphql, graphql_sync
INVALID = Undefined # deprecated alias
# The GraphQL-core version info.
__version__ = version
__version_info__ = version_info
# The GraphQL.js version info.
__version_js__ = version_js
__version_info_js__ = version_info_js
__all__ = [
"version",
"version_info",
"version_js",
"version_info_js",
"graphql",
"graphql_sync",
"GraphQLSchema",
"GraphQLDirective",
"GraphQLScalarType",
"GraphQLObjectType",
"GraphQLInterfaceType",
"GraphQLUnionType",
"GraphQLEnumType",
"GraphQLInputObjectType",
"GraphQLList",
"GraphQLNonNull",
"specified_scalar_types",
"GraphQLInt",
"GraphQLFloat",
"GraphQLString",
"GraphQLBoolean",
"GraphQLID",
"GRAPHQL_MAX_INT",
"GRAPHQL_MIN_INT",
"specified_directives",
"GraphQLIncludeDirective",
"GraphQLSkipDirective",
"GraphQLDeprecatedDirective",
"GraphQLSpecifiedByDirective",
"GraphQLOneOfDirective",
"TypeKind",
"DEFAULT_DEPRECATION_REASON",
"introspection_types",
"SchemaMetaFieldDef",
"TypeMetaFieldDef",
"TypeNameMetaFieldDef",
"is_schema",
"is_directive",
"is_type",
"is_scalar_type",
"is_object_type",
"is_interface_type",
"is_union_type",
"is_enum_type",
"is_input_object_type",
"is_list_type",
"is_non_null_type",
"is_input_type",
"is_output_type",
"is_leaf_type",
"is_composite_type",
"is_abstract_type",
"is_wrapping_type",
"is_nullable_type",
"is_named_type",
"is_required_argument",
"is_required_input_field",
"is_specified_scalar_type",
"is_introspection_type",
"is_specified_directive",
"assert_schema",
"assert_directive",
"assert_type",
"assert_scalar_type",
"assert_object_type",
"assert_interface_type",
"assert_union_type",
"assert_enum_type",
"assert_input_object_type",
"assert_list_type",
"assert_non_null_type",
"assert_input_type",
"assert_output_type",
"assert_leaf_type",
"assert_composite_type",
"assert_abstract_type",
"assert_wrapping_type",
"assert_nullable_type",
"assert_named_type",
"get_nullable_type",
"get_named_type",
"resolve_thunk",
"validate_schema",
"assert_valid_schema",
"assert_name",
"assert_enum_value_name",
"GraphQLType",
"GraphQLInputType",
"GraphQLOutputType",
"GraphQLLeafType",
"GraphQLCompositeType",
"GraphQLAbstractType",
"GraphQLWrappingType",
"GraphQLNullableType",
"GraphQLNamedType",
"GraphQLNamedInputType",
"GraphQLNamedOutputType",
"Thunk",
"ThunkCollection",
"ThunkMapping",
"GraphQLArgument",
"GraphQLArgumentMap",
"GraphQLEnumValue",
"GraphQLEnumValueMap",
"GraphQLEnumValuesDefinition",
"GraphQLField",
"GraphQLFieldMap",
"GraphQLFieldResolver",
"GraphQLInputField",
"GraphQLInputFieldMap",
"GraphQLScalarSerializer",
"GraphQLScalarValueParser",
"GraphQLScalarLiteralParser",
"GraphQLIsTypeOfFn",
"GraphQLResolveInfo",
"ResponsePath",
"GraphQLTypeResolver",
"GraphQLArgumentKwargs",
"GraphQLDirectiveKwargs",
"GraphQLEnumTypeKwargs",
"GraphQLEnumValueKwargs",
"GraphQLFieldKwargs",
"GraphQLInputFieldKwargs",
"GraphQLInputObjectTypeKwargs",
"GraphQLInterfaceTypeKwargs",
"GraphQLNamedTypeKwargs",
"GraphQLObjectTypeKwargs",
"GraphQLScalarTypeKwargs",
"GraphQLSchemaKwargs",
"GraphQLUnionTypeKwargs",
"Source",
"get_location",
"print_location",
"print_source_location",
"Lexer",
"TokenKind",
"parse",
"parse_value",
"parse_const_value",
"parse_type",
"print_ast",
"visit",
"ParallelVisitor",
"TypeInfoVisitor",
"Visitor",
"VisitorAction",
"VisitorKeyMap",
"BREAK",
"SKIP",
"REMOVE",
"IDLE",
"DirectiveLocation",
"is_definition_node",
"is_executable_definition_node",
"is_selection_node",
"is_value_node",
"is_const_value_node",
"is_type_node",
"is_type_system_definition_node",
"is_type_definition_node",
"is_type_system_extension_node",
"is_type_extension_node",
"SourceLocation",
"Location",
"Token",
"Node",
"NameNode",
"DocumentNode",
"DefinitionNode",
"ExecutableDefinitionNode",
"OperationDefinitionNode",
"OperationType",
"VariableDefinitionNode",
"VariableNode",
"SelectionSetNode",
"SelectionNode",
"FieldNode",
"ArgumentNode",
"ConstArgumentNode",
"FragmentSpreadNode",
"InlineFragmentNode",
"FragmentDefinitionNode",
"ValueNode",
"ConstValueNode",
"IntValueNode",
"FloatValueNode",
"StringValueNode",
"BooleanValueNode",
"NullValueNode",
"EnumValueNode",
"ListValueNode",
"ConstListValueNode",
"ObjectValueNode",
"ConstObjectValueNode",
"ObjectFieldNode",
"ConstObjectFieldNode",
"DirectiveNode",
"ConstDirectiveNode",
"TypeNode",
"NamedTypeNode",
"ListTypeNode",
"NonNullTypeNode",
"TypeSystemDefinitionNode",
"SchemaDefinitionNode",
"OperationTypeDefinitionNode",
"TypeDefinitionNode",
"ScalarTypeDefinitionNode",
"ObjectTypeDefinitionNode",
"FieldDefinitionNode",
"InputValueDefinitionNode",
"InterfaceTypeDefinitionNode",
"UnionTypeDefinitionNode",
"EnumTypeDefinitionNode",
"EnumValueDefinitionNode",
"InputObjectTypeDefinitionNode",
"DirectiveDefinitionNode",
"TypeSystemExtensionNode",
"SchemaExtensionNode",
"TypeExtensionNode",
"ScalarTypeExtensionNode",
"ObjectTypeExtensionNode",
"InterfaceTypeExtensionNode",
"UnionTypeExtensionNode",
"EnumTypeExtensionNode",
"InputObjectTypeExtensionNode",
"execute",
"execute_sync",
"default_field_resolver",
"default_type_resolver",
"get_argument_values",
"get_directive_values",
"get_variable_values",
"ExecutionContext",
"ExecutionResult",
"FormattedExecutionResult",
"Middleware",
"MiddlewareManager",
"subscribe",
"create_source_event_stream",
"MapAsyncIterator",
"validate",
"ValidationContext",
"ValidationRule",
"ASTValidationRule",
"SDLValidationRule",
"specified_rules",
"recommended_rules",
"ExecutableDefinitionsRule",
"FieldsOnCorrectTypeRule",
"FragmentsOnCompositeTypesRule",
"KnownArgumentNamesRule",
"KnownDirectivesRule",
"KnownFragmentNamesRule",
"KnownTypeNamesRule",
"LoneAnonymousOperationRule",
"NoFragmentCyclesRule",
"NoUndefinedVariablesRule",
"NoUnusedFragmentsRule",
"NoUnusedVariablesRule",
"OverlappingFieldsCanBeMergedRule",
"PossibleFragmentSpreadsRule",
"ProvidedRequiredArgumentsRule",
"ScalarLeafsRule",
"SingleFieldSubscriptionsRule",
"UniqueArgumentNamesRule",
"UniqueDirectivesPerLocationRule",
"UniqueFragmentNamesRule",
"UniqueInputFieldNamesRule",
"UniqueOperationNamesRule",
"UniqueVariableNamesRule",
"ValuesOfCorrectTypeRule",
"VariablesAreInputTypesRule",
"VariablesInAllowedPositionRule",
"LoneSchemaDefinitionRule",
"UniqueOperationTypesRule",
"UniqueTypeNamesRule",
"UniqueEnumValueNamesRule",
"UniqueFieldDefinitionNamesRule",
"UniqueArgumentDefinitionNamesRule",
"UniqueDirectiveNamesRule",
"PossibleTypeExtensionsRule",
"NoDeprecatedCustomRule",
"NoSchemaIntrospectionCustomRule",
"MaxIntrospectionDepthRule",
"GraphQLError",
"GraphQLErrorExtensions",
"GraphQLFormattedError",
"GraphQLSyntaxError",
"located_error",
"get_introspection_query",
"IntrospectionQuery",
"get_operation_ast",
"get_operation_root_type",
"introspection_from_schema",
"build_client_schema",
"build_ast_schema",
"build_schema",
"extend_schema",
"lexicographic_sort_schema",
"print_schema",
"print_type",
"print_introspection_schema",
"type_from_ast",
"value_from_ast",
"value_from_ast_untyped",
"ast_from_value",
"ast_to_dict",
"TypeInfo",
"coerce_input_value",
"concat_ast",
"separate_operations",
"strip_ignored_characters",
"is_equal_type",
"is_type_sub_type_of",
"do_types_overlap",
"assert_valid_name",
"is_valid_name_error",
"find_breaking_changes",
"find_dangerous_changes",
"BreakingChange",
"BreakingChangeType",
"DangerousChange",
"DangerousChangeType",
"Undefined",
"UndefinedType",
]

View File

@@ -0,0 +1,19 @@
"""GraphQL Errors
The :mod:`graphql.error` package is responsible for creating and formatting GraphQL
errors.
"""
from .graphql_error import GraphQLError, GraphQLErrorExtensions, GraphQLFormattedError
from .syntax_error import GraphQLSyntaxError
from .located_error import located_error
__all__ = [
"GraphQLError",
"GraphQLErrorExtensions",
"GraphQLFormattedError",
"GraphQLSyntaxError",
"located_error",
]

View File

@@ -0,0 +1,264 @@
from sys import exc_info
from typing import Any, Collection, Dict, List, Optional, Union, TYPE_CHECKING
try:
from typing import TypedDict
except ImportError: # Python < 3.8
from typing_extensions import TypedDict
if TYPE_CHECKING:
from ..language.ast import Node # noqa: F401
from ..language.location import (
SourceLocation,
FormattedSourceLocation,
) # noqa: F401
from ..language.source import Source # noqa: F401
__all__ = ["GraphQLError", "GraphQLErrorExtensions", "GraphQLFormattedError"]
# Custom extensions
GraphQLErrorExtensions = Dict[str, Any]
# Use a unique identifier name for your extension, for example the name of
# your library or project. Do not use a shortened identifier as this increases
# the risk of conflicts. We recommend you add at most one extension key,
# a dictionary which can contain all the values you need.
class GraphQLFormattedError(TypedDict, total=False):
"""Formatted GraphQL error"""
# A short, human-readable summary of the problem that **SHOULD NOT** change
# from occurrence to occurrence of the problem, except for purposes of localization.
message: str
# If an error can be associated to a particular point in the requested
# GraphQL document, it should contain a list of locations.
locations: List["FormattedSourceLocation"]
# If an error can be associated to a particular field in the GraphQL result,
# it _must_ contain an entry with the key `path` that details the path of
# the response field which experienced the error. This allows clients to
# identify whether a null result is intentional or caused by a runtime error.
path: List[Union[str, int]]
# Reserved for implementors to extend the protocol however they see fit,
# and hence there are no additional restrictions on its contents.
extensions: GraphQLErrorExtensions
class GraphQLError(Exception):
"""GraphQL Error
A GraphQLError describes an Error found during the parse, validate, or execute
phases of performing a GraphQL operation. In addition to a message, it also includes
information about the locations in a GraphQL document and/or execution result that
correspond to the Error.
"""
message: str
"""A message describing the Error for debugging purposes"""
locations: Optional[List["SourceLocation"]]
"""Source locations
A list of (line, column) locations within the source GraphQL document which
correspond to this error.
Errors during validation often contain multiple locations, for example to point out
two things with the same name. Errors during execution include a single location,
the field which produced the error.
"""
path: Optional[List[Union[str, int]]]
"""
A list of field names and array indexes describing the JSON-path into the execution
response which corresponds to this error.
Only included for errors during execution.
"""
nodes: Optional[List["Node"]]
"""A list of GraphQL AST Nodes corresponding to this error"""
source: Optional["Source"]
"""The source GraphQL document for the first location of this error
Note that if this Error represents more than one node, the source may not represent
nodes after the first node.
"""
positions: Optional[Collection[int]]
"""Error positions
A list of character offsets within the source GraphQL document which correspond
to this error.
"""
original_error: Optional[Exception]
"""The original error thrown from a field resolver during execution"""
extensions: Optional[GraphQLErrorExtensions]
"""Extension fields to add to the formatted error"""
__slots__ = (
"message",
"nodes",
"source",
"positions",
"locations",
"path",
"original_error",
"extensions",
)
__hash__ = Exception.__hash__
def __init__(
self,
message: str,
nodes: Union[Collection["Node"], "Node", None] = None,
source: Optional["Source"] = None,
positions: Optional[Collection[int]] = None,
path: Optional[Collection[Union[str, int]]] = None,
original_error: Optional[Exception] = None,
extensions: Optional[GraphQLErrorExtensions] = None,
) -> None:
super().__init__(message)
self.message = message
if path and not isinstance(path, list):
path = list(path)
self.path = path or None # type: ignore
self.original_error = original_error
# Compute list of blame nodes.
if nodes and not isinstance(nodes, list):
nodes = [nodes] # type: ignore
self.nodes = nodes or None # type: ignore
node_locations = (
[node.loc for node in nodes if node.loc] if nodes else [] # type: ignore
)
# Compute locations in the source for the given nodes/positions.
self.source = source
if not source and node_locations:
loc = node_locations[0]
if loc.source: # pragma: no cover else
self.source = loc.source
if not positions and node_locations:
positions = [loc.start for loc in node_locations]
self.positions = positions or None
if positions and source:
locations: Optional[List["SourceLocation"]] = [
source.get_location(pos) for pos in positions
]
else:
locations = [loc.source.get_location(loc.start) for loc in node_locations]
self.locations = locations or None
if original_error:
self.__traceback__ = original_error.__traceback__
if original_error.__cause__:
self.__cause__ = original_error.__cause__
elif original_error.__context__:
self.__context__ = original_error.__context__
if extensions is None:
original_extensions = getattr(original_error, "extensions", None)
if isinstance(original_extensions, dict):
extensions = original_extensions
self.extensions = extensions or {}
if not self.__traceback__:
self.__traceback__ = exc_info()[2]
def __str__(self) -> str:
# Lazy import to avoid a cyclic dependency between error and language
from ..language.print_location import print_location, print_source_location
output = [self.message]
if self.nodes:
for node in self.nodes:
if node.loc:
output.append(print_location(node.loc))
elif self.source and self.locations:
source = self.source
for location in self.locations:
output.append(print_source_location(source, location))
return "\n\n".join(output)
def __repr__(self) -> str:
args = [repr(self.message)]
if self.locations:
args.append(f"locations={self.locations!r}")
if self.path:
args.append(f"path={self.path!r}")
if self.extensions:
args.append(f"extensions={self.extensions!r}")
return f"{self.__class__.__name__}({', '.join(args)})"
def __eq__(self, other: Any) -> bool:
return (
isinstance(other, GraphQLError)
and self.__class__ == other.__class__
and all(
getattr(self, slot) == getattr(other, slot)
for slot in self.__slots__
if slot != "original_error"
)
) or (
isinstance(other, dict)
and "message" in other
and all(
slot in self.__slots__ and getattr(self, slot) == other.get(slot)
for slot in other
if slot != "original_error"
)
)
def __ne__(self, other: Any) -> bool:
return not self == other
@property
def formatted(self) -> GraphQLFormattedError:
"""Get error formatted according to the specification.
Given a GraphQLError, format it according to the rules described by the
"Response Format, Errors" section of the GraphQL Specification.
"""
formatted: GraphQLFormattedError = {
"message": self.message or "An unknown error occurred.",
}
if self.locations is not None:
formatted["locations"] = [location.formatted for location in self.locations]
if self.path is not None:
formatted["path"] = self.path
if self.extensions:
formatted["extensions"] = self.extensions
return formatted
def print_error(error: GraphQLError) -> str:
"""Print a GraphQLError to a string.
Represents useful location information about the error's position in the source.
.. deprecated:: 3.2
Please use ``str(error)`` instead. Will be removed in v3.3.
"""
if not isinstance(error, GraphQLError):
raise TypeError("Expected a GraphQLError.")
return str(error)
def format_error(error: GraphQLError) -> GraphQLFormattedError:
"""Format a GraphQL error.
Given a GraphQLError, format it according to the rules described by the "Response
Format, Errors" section of the GraphQL Specification.
.. deprecated:: 3.2
Please use ``error.formatted`` instead. Will be removed in v3.3.
"""
if not isinstance(error, GraphQLError):
raise TypeError("Expected a GraphQLError.")
return error.formatted

View File

@@ -0,0 +1,50 @@
from typing import TYPE_CHECKING, Collection, Optional, Union
from ..pyutils import inspect
from .graphql_error import GraphQLError
if TYPE_CHECKING:
from ..language.ast import Node # noqa: F401
__all__ = ["located_error"]
def located_error(
original_error: Exception,
nodes: Optional[Union["None", Collection["Node"]]] = None,
path: Optional[Collection[Union[str, int]]] = None,
) -> GraphQLError:
"""Located GraphQL Error
Given an arbitrary Exception, presumably thrown while attempting to execute a
GraphQL operation, produce a new GraphQLError aware of the location in the document
responsible for the original Exception.
"""
# Sometimes a non-error is thrown, wrap it as a TypeError to ensure consistency.
if not isinstance(original_error, Exception):
original_error = TypeError(f"Unexpected error value: {inspect(original_error)}")
# Note: this uses a brand-check to support GraphQL errors originating from
# other contexts.
if isinstance(original_error, GraphQLError) and original_error.path is not None:
return original_error
try:
# noinspection PyUnresolvedReferences
message = str(original_error.message) # type: ignore
except AttributeError:
message = str(original_error)
try:
# noinspection PyUnresolvedReferences
source = original_error.source # type: ignore
except AttributeError:
source = None
try:
# noinspection PyUnresolvedReferences
positions = original_error.positions # type: ignore
except AttributeError:
positions = None
try:
# noinspection PyUnresolvedReferences
nodes = original_error.nodes or nodes # type: ignore
except AttributeError:
pass
return GraphQLError(message, nodes, source, positions, path, original_error)

View File

@@ -0,0 +1,18 @@
from typing import TYPE_CHECKING
from .graphql_error import GraphQLError
if TYPE_CHECKING:
from ..language.source import Source # noqa: F401
__all__ = ["GraphQLSyntaxError"]
class GraphQLSyntaxError(GraphQLError):
"""A GraphQLError representing a syntax error."""
def __init__(self, source: "Source", position: int, description: str) -> None:
super().__init__(
f"Syntax Error: {description}", source=source, positions=[position]
)
self.description = description

View File

@@ -0,0 +1,38 @@
"""GraphQL Execution
The :mod:`graphql.execution` package is responsible for the execution phase of
fulfilling a GraphQL request.
"""
from .execute import (
execute,
execute_sync,
default_field_resolver,
default_type_resolver,
ExecutionContext,
ExecutionResult,
FormattedExecutionResult,
Middleware,
)
from .map_async_iterator import MapAsyncIterator
from .subscribe import subscribe, create_source_event_stream
from .middleware import MiddlewareManager
from .values import get_argument_values, get_directive_values, get_variable_values
__all__ = [
"create_source_event_stream",
"execute",
"execute_sync",
"default_field_resolver",
"default_type_resolver",
"subscribe",
"ExecutionContext",
"ExecutionResult",
"FormattedExecutionResult",
"MapAsyncIterator",
"Middleware",
"MiddlewareManager",
"get_argument_values",
"get_directive_values",
"get_variable_values",
]

View File

@@ -0,0 +1,174 @@
from typing import Any, Dict, List, Set, Union, cast
from ..language import (
FieldNode,
FragmentDefinitionNode,
FragmentSpreadNode,
InlineFragmentNode,
SelectionSetNode,
)
from ..type import (
GraphQLAbstractType,
GraphQLIncludeDirective,
GraphQLObjectType,
GraphQLSchema,
GraphQLSkipDirective,
is_abstract_type,
)
from ..utilities.type_from_ast import type_from_ast
from .values import get_directive_values
__all__ = ["collect_fields", "collect_sub_fields"]
def collect_fields(
schema: GraphQLSchema,
fragments: Dict[str, FragmentDefinitionNode],
variable_values: Dict[str, Any],
runtime_type: GraphQLObjectType,
selection_set: SelectionSetNode,
) -> Dict[str, List[FieldNode]]:
"""Collect fields.
Given a selection_set, collects all the fields and returns them.
collect_fields requires the "runtime type" of an object. For a field that
returns an Interface or Union type, the "runtime type" will be the actual
object type returned by that field.
For internal use only.
"""
fields: Dict[str, List[FieldNode]] = {}
collect_fields_impl(
schema, fragments, variable_values, runtime_type, selection_set, fields, set()
)
return fields
def collect_sub_fields(
schema: GraphQLSchema,
fragments: Dict[str, FragmentDefinitionNode],
variable_values: Dict[str, Any],
return_type: GraphQLObjectType,
field_nodes: List[FieldNode],
) -> Dict[str, List[FieldNode]]:
"""Collect sub fields.
Given a list of field nodes, collects all the subfields of the passed in fields,
and returns them at the end.
collect_sub_fields requires the "return type" of an object. For a field that
returns an Interface or Union type, the "return type" will be the actual
object type returned by that field.
For internal use only.
"""
sub_field_nodes: Dict[str, List[FieldNode]] = {}
visited_fragment_names: Set[str] = set()
for node in field_nodes:
if node.selection_set:
collect_fields_impl(
schema,
fragments,
variable_values,
return_type,
node.selection_set,
sub_field_nodes,
visited_fragment_names,
)
return sub_field_nodes
def collect_fields_impl(
schema: GraphQLSchema,
fragments: Dict[str, FragmentDefinitionNode],
variable_values: Dict[str, Any],
runtime_type: GraphQLObjectType,
selection_set: SelectionSetNode,
fields: Dict[str, List[FieldNode]],
visited_fragment_names: Set[str],
) -> None:
"""Collect fields (internal implementation)."""
for selection in selection_set.selections:
if isinstance(selection, FieldNode):
if not should_include_node(variable_values, selection):
continue
name = get_field_entry_key(selection)
fields.setdefault(name, []).append(selection)
elif isinstance(selection, InlineFragmentNode):
if not should_include_node(
variable_values, selection
) or not does_fragment_condition_match(schema, selection, runtime_type):
continue
collect_fields_impl(
schema,
fragments,
variable_values,
runtime_type,
selection.selection_set,
fields,
visited_fragment_names,
)
elif isinstance(selection, FragmentSpreadNode): # pragma: no cover else
frag_name = selection.name.value
if frag_name in visited_fragment_names or not should_include_node(
variable_values, selection
):
continue
visited_fragment_names.add(frag_name)
fragment = fragments.get(frag_name)
if not fragment or not does_fragment_condition_match(
schema, fragment, runtime_type
):
continue
collect_fields_impl(
schema,
fragments,
variable_values,
runtime_type,
fragment.selection_set,
fields,
visited_fragment_names,
)
def should_include_node(
variable_values: Dict[str, Any],
node: Union[FragmentSpreadNode, FieldNode, InlineFragmentNode],
) -> bool:
"""Check if node should be included
Determines if a field should be included based on the @include and @skip
directives, where @skip has higher precedence than @include.
"""
skip = get_directive_values(GraphQLSkipDirective, node, variable_values)
if skip and skip["if"]:
return False
include = get_directive_values(GraphQLIncludeDirective, node, variable_values)
if include and not include["if"]:
return False
return True
def does_fragment_condition_match(
schema: GraphQLSchema,
fragment: Union[FragmentDefinitionNode, InlineFragmentNode],
type_: GraphQLObjectType,
) -> bool:
"""Determine if a fragment is applicable to the given type."""
type_condition_node = fragment.type_condition
if not type_condition_node:
return True
conditional_type = type_from_ast(schema, type_condition_node)
if conditional_type is type_:
return True
if is_abstract_type(conditional_type):
return schema.is_sub_type(cast(GraphQLAbstractType, conditional_type), type_)
return False
def get_field_entry_key(node: FieldNode) -> str:
"""Implements the logic to compute the key of a given field's entry"""
return node.alias.value if node.alias else node.name.value

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
from asyncio import CancelledError, Event, Task, ensure_future, wait
from concurrent.futures import FIRST_COMPLETED
from inspect import isasyncgen, isawaitable
from types import TracebackType
from typing import Any, AsyncIterable, Callable, Optional, Set, Type, Union
__all__ = ["MapAsyncIterator"]
# noinspection PyAttributeOutsideInit
class MapAsyncIterator:
"""Map an AsyncIterable over a callback function.
Given an AsyncIterable and a callback function, return an AsyncIterator which
produces values mapped via calling the callback function.
When the resulting AsyncIterator is closed, the underlying AsyncIterable will also
be closed.
"""
def __init__(self, iterable: AsyncIterable, callback: Callable) -> None:
self.iterator = iterable.__aiter__()
self.callback = callback
self._close_event = Event()
def __aiter__(self) -> "MapAsyncIterator":
"""Get the iterator object."""
return self
async def __anext__(self) -> Any:
"""Get the next value of the iterator."""
if self.is_closed:
if not isasyncgen(self.iterator):
raise StopAsyncIteration
value = await self.iterator.__anext__()
else:
aclose = ensure_future(self._close_event.wait())
anext = ensure_future(self.iterator.__anext__())
try:
pending: Set[Task] = (
await wait([aclose, anext], return_when=FIRST_COMPLETED)
)[1]
except CancelledError:
# cancel underlying tasks and close
aclose.cancel()
anext.cancel()
await self.aclose()
raise # re-raise the cancellation
for task in pending:
task.cancel()
if aclose.done():
raise StopAsyncIteration
error = anext.exception()
if error:
raise error
value = anext.result()
result = self.callback(value)
return await result if isawaitable(result) else result
async def athrow(
self,
type_: Union[BaseException, Type[BaseException]],
value: Optional[BaseException] = None,
traceback: Optional[TracebackType] = None,
) -> None:
"""Throw an exception into the asynchronous iterator."""
if self.is_closed:
return
if isinstance(type_, BaseException):
value = type_
type_ = type(value)
traceback = value.__traceback__
athrow = getattr(self.iterator, "athrow", None)
if athrow:
await athrow(type_ if value is None else value)
else:
await self.aclose()
if value is None:
if traceback is None:
raise type_ # pragma: no cover
value = type_ if isinstance(value, BaseException) else type_()
if traceback is not None:
value = value.with_traceback(traceback)
raise value
async def aclose(self) -> None:
"""Close the iterator."""
if not self.is_closed:
aclose = getattr(self.iterator, "aclose", None)
if aclose:
try:
await aclose()
except RuntimeError:
pass
self.is_closed = True
@property
def is_closed(self) -> bool:
"""Check whether the iterator is closed."""
return self._close_event.is_set()
@is_closed.setter
def is_closed(self, value: bool) -> None:
"""Mark the iterator as closed."""
if value:
self._close_event.set()
else:
self._close_event.clear()

View File

@@ -0,0 +1,63 @@
from functools import partial, reduce
from inspect import isfunction
from typing import Callable, Iterator, Dict, List, Tuple, Any, Optional
__all__ = ["MiddlewareManager"]
GraphQLFieldResolver = Callable[..., Any]
class MiddlewareManager:
"""Manager for the middleware chain.
This class helps to wrap resolver functions with the provided middleware functions
and/or objects. The functions take the next middleware function as first argument.
If middleware is provided as an object, it must provide a method ``resolve`` that is
used as the middleware function.
Note that since resolvers return "AwaitableOrValue"s, all middleware functions
must be aware of this and check whether values are awaitable before awaiting them.
"""
# allow custom attributes (not used internally)
__slots__ = "__dict__", "middlewares", "_middleware_resolvers", "_cached_resolvers"
_cached_resolvers: Dict[GraphQLFieldResolver, GraphQLFieldResolver]
_middleware_resolvers: Optional[List[Callable]]
def __init__(self, *middlewares: Any):
self.middlewares = middlewares
self._middleware_resolvers = (
list(get_middleware_resolvers(middlewares)) if middlewares else None
)
self._cached_resolvers = {}
def get_field_resolver(
self, field_resolver: GraphQLFieldResolver
) -> GraphQLFieldResolver:
"""Wrap the provided resolver with the middleware.
Returns a function that chains the middleware functions with the provided
resolver function.
"""
if self._middleware_resolvers is None:
return field_resolver
if field_resolver not in self._cached_resolvers:
self._cached_resolvers[field_resolver] = reduce(
lambda chained_fns, next_fn: partial(next_fn, chained_fns),
self._middleware_resolvers,
field_resolver,
)
return self._cached_resolvers[field_resolver]
def get_middleware_resolvers(middlewares: Tuple[Any, ...]) -> Iterator[Callable]:
"""Get a list of resolver functions from a list of classes or functions."""
for middleware in middlewares:
if isfunction(middleware):
yield middleware
else: # middleware provided as object with 'resolve' method
resolver_func = getattr(middleware, "resolve", None)
if resolver_func is not None:
yield resolver_func

View File

@@ -0,0 +1,212 @@
from inspect import isawaitable
from typing import (
Any,
AsyncIterable,
AsyncIterator,
Dict,
Optional,
Union,
)
from ..error import GraphQLError, located_error
from ..execution.collect_fields import collect_fields
from ..execution.execute import (
assert_valid_execution_arguments,
execute,
get_field_def,
ExecutionContext,
ExecutionResult,
)
from ..execution.values import get_argument_values
from ..language import DocumentNode
from ..pyutils import Path, inspect
from ..type import GraphQLFieldResolver, GraphQLSchema
from .map_async_iterator import MapAsyncIterator
__all__ = ["subscribe", "create_source_event_stream"]
async def subscribe(
schema: GraphQLSchema,
document: DocumentNode,
root_value: Any = None,
context_value: Any = None,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
field_resolver: Optional[GraphQLFieldResolver] = None,
subscribe_field_resolver: Optional[GraphQLFieldResolver] = None,
) -> Union[AsyncIterator[ExecutionResult], ExecutionResult]:
"""Create a GraphQL subscription.
Implements the "Subscribe" algorithm described in the GraphQL spec.
Returns a coroutine object which yields either an AsyncIterator (if successful) or
an ExecutionResult (client error). The coroutine will raise an exception if a server
error occurs.
If the client-provided arguments to this function do not result in a compliant
subscription, a GraphQL Response (ExecutionResult) with descriptive errors and no
data will be returned.
If the source stream could not be created due to faulty subscription resolver logic
or underlying systems, the coroutine object will yield a single ExecutionResult
containing ``errors`` and no ``data``.
If the operation succeeded, the coroutine will yield an AsyncIterator, which yields
a stream of ExecutionResults representing the response stream.
"""
result_or_stream = await create_source_event_stream(
schema,
document,
root_value,
context_value,
variable_values,
operation_name,
subscribe_field_resolver,
)
if isinstance(result_or_stream, ExecutionResult):
return result_or_stream
async def map_source_to_response(payload: Any) -> ExecutionResult:
"""Map source to response.
For each payload yielded from a subscription, map it over the normal GraphQL
:func:`~graphql.execute` function, with ``payload`` as the ``root_value``.
This implements the "MapSourceToResponseEvent" algorithm described in the
GraphQL specification. The :func:`~graphql.execute` function provides the
"ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
"ExecuteQuery" algorithm, for which :func:`~graphql.execute` is also used.
"""
result = execute(
schema,
document,
payload,
context_value,
variable_values,
operation_name,
field_resolver,
)
return await result if isawaitable(result) else result
# Map every source value to a ExecutionResult value as described above.
return MapAsyncIterator(result_or_stream, map_source_to_response)
async def create_source_event_stream(
schema: GraphQLSchema,
document: DocumentNode,
root_value: Any = None,
context_value: Any = None,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
subscribe_field_resolver: Optional[GraphQLFieldResolver] = None,
) -> Union[AsyncIterable[Any], ExecutionResult]:
"""Create source event stream
Implements the "CreateSourceEventStream" algorithm described in the GraphQL
specification, resolving the subscription source event stream.
Returns a coroutine that yields an AsyncIterable.
If the client-provided arguments to this function do not result in a compliant
subscription, a GraphQL Response (ExecutionResult) with descriptive errors and no
data will be returned.
If the source stream could not be created due to faulty subscription resolver logic
or underlying systems, the coroutine object will yield a single ExecutionResult
containing ``errors`` and no ``data``.
A source event stream represents a sequence of events, each of which triggers a
GraphQL execution for that event.
This may be useful when hosting the stateful subscription service in a different
process or machine than the stateless GraphQL execution engine, or otherwise
separating these two steps. For more on this, see the "Supporting Subscriptions
at Scale" information in the GraphQL spec.
"""
# If arguments are missing or incorrectly typed, this is an internal developer
# mistake which should throw an early error.
assert_valid_execution_arguments(schema, document, variable_values)
# If a valid context cannot be created due to incorrect arguments,
# a "Response" with only errors is returned.
context = ExecutionContext.build(
schema,
document,
root_value,
context_value,
variable_values,
operation_name,
subscribe_field_resolver=subscribe_field_resolver,
)
# Return early errors if execution context failed.
if isinstance(context, list):
return ExecutionResult(data=None, errors=context)
try:
event_stream = await execute_subscription(context)
# Assert field returned an event stream, otherwise yield an error.
if not isinstance(event_stream, AsyncIterable):
raise TypeError(
"Subscription field must return AsyncIterable."
f" Received: {inspect(event_stream)}."
)
return event_stream
except GraphQLError as error:
# Report it as an ExecutionResult, containing only errors and no data.
return ExecutionResult(data=None, errors=[error])
async def execute_subscription(context: ExecutionContext) -> AsyncIterable[Any]:
schema = context.schema
root_type = schema.subscription_type
if root_type is None:
raise GraphQLError(
"Schema is not configured to execute subscription operation.",
context.operation,
)
root_fields = collect_fields(
schema,
context.fragments,
context.variable_values,
root_type,
context.operation.selection_set,
)
response_name, field_nodes = next(iter(root_fields.items()))
field_def = get_field_def(schema, root_type, field_nodes[0])
if not field_def:
field_name = field_nodes[0].name.value
raise GraphQLError(
f"The subscription field '{field_name}' is not defined.", field_nodes
)
path = Path(None, response_name, root_type.name)
info = context.build_resolve_info(field_def, field_nodes, root_type, path)
# Implements the "ResolveFieldEventStream" algorithm from GraphQL specification.
# It differs from "ResolveFieldValue" due to providing a different `resolveFn`.
try:
# Build a dictionary of arguments from the field.arguments AST, using the
# variables scope to fulfill any variable references.
args = get_argument_values(field_def, field_nodes[0], context.variable_values)
# Call the `subscribe()` resolver or the default resolver to produce an
# AsyncIterable yielding raw payloads.
resolve_fn = field_def.subscribe or context.subscribe_field_resolver
event_stream = resolve_fn(context.root_value, info, **args)
if context.is_awaitable(event_stream):
event_stream = await event_stream
if isinstance(event_stream, Exception):
raise event_stream
return event_stream
except Exception as error:
raise located_error(error, field_nodes, path.as_list())

View File

@@ -0,0 +1,256 @@
from typing import Any, Callable, Collection, Dict, List, Optional, Union, cast
from ..error import GraphQLError
from ..language import (
DirectiveNode,
EnumValueDefinitionNode,
ExecutableDefinitionNode,
FieldDefinitionNode,
FieldNode,
InputValueDefinitionNode,
NullValueNode,
SchemaDefinitionNode,
SelectionNode,
TypeDefinitionNode,
TypeExtensionNode,
VariableDefinitionNode,
VariableNode,
print_ast,
)
from ..pyutils import Undefined, inspect, print_path_list
from ..type import (
GraphQLDirective,
GraphQLField,
GraphQLInputType,
GraphQLSchema,
is_input_object_type,
is_input_type,
is_non_null_type,
)
from ..utilities.coerce_input_value import coerce_input_value
from ..utilities.type_from_ast import type_from_ast
from ..utilities.value_from_ast import value_from_ast
__all__ = ["get_argument_values", "get_directive_values", "get_variable_values"]
CoercedVariableValues = Union[List[GraphQLError], Dict[str, Any]]
def get_variable_values(
schema: GraphQLSchema,
var_def_nodes: Collection[VariableDefinitionNode],
inputs: Dict[str, Any],
max_errors: Optional[int] = None,
) -> CoercedVariableValues:
"""Get coerced variable values based on provided definitions.
Prepares a dict of variable values of the correct type based on the provided
variable definitions and arbitrary input. If the input cannot be parsed to match
the variable definitions, a GraphQLError will be raised.
"""
errors: List[GraphQLError] = []
def on_error(error: GraphQLError) -> None:
if max_errors is not None and len(errors) >= max_errors:
raise GraphQLError(
"Too many errors processing variables,"
" error limit reached. Execution aborted."
)
errors.append(error)
try:
coerced = coerce_variable_values(schema, var_def_nodes, inputs, on_error)
if not errors:
return coerced
except GraphQLError as e:
errors.append(e)
return errors
def coerce_variable_values(
schema: GraphQLSchema,
var_def_nodes: Collection[VariableDefinitionNode],
inputs: Dict[str, Any],
on_error: Callable[[GraphQLError], None],
) -> Dict[str, Any]:
coerced_values: Dict[str, Any] = {}
for var_def_node in var_def_nodes:
var_name = var_def_node.variable.name.value
var_type = type_from_ast(schema, var_def_node.type)
if not is_input_type(var_type):
# Must use input types for variables. This should be caught during
# validation, however is checked again here for safety.
var_type_str = print_ast(var_def_node.type)
on_error(
GraphQLError(
f"Variable '${var_name}' expected value of type '{var_type_str}'"
" which cannot be used as an input type.",
var_def_node.type,
)
)
continue
var_type = cast(GraphQLInputType, var_type)
if var_name not in inputs:
if var_def_node.default_value:
coerced_values[var_name] = value_from_ast(
var_def_node.default_value, var_type
)
elif is_non_null_type(var_type): # pragma: no cover else
var_type_str = inspect(var_type)
on_error(
GraphQLError(
f"Variable '${var_name}' of required type '{var_type_str}'"
" was not provided.",
var_def_node,
)
)
continue
value = inputs[var_name]
if value is None and is_non_null_type(var_type):
var_type_str = inspect(var_type)
on_error(
GraphQLError(
f"Variable '${var_name}' of non-null type '{var_type_str}'"
" must not be null.",
var_def_node,
)
)
continue
def on_input_value_error(
path: List[Union[str, int]],
invalid_value: Any,
error: GraphQLError,
var_name: str = var_name,
var_def_node: VariableDefinitionNode = var_def_node,
) -> None:
invalid_str = inspect(invalid_value)
prefix = f"Variable '${var_name}' got invalid value {invalid_str}"
if path:
prefix += f" at '{var_name}{print_path_list(path)}'"
on_error(
GraphQLError(
prefix + "; " + error.message,
var_def_node,
original_error=error,
)
)
coerced_values[var_name] = coerce_input_value(
value, var_type, on_input_value_error
)
return coerced_values
def get_argument_values(
type_def: Union[GraphQLField, GraphQLDirective],
node: Union[FieldNode, DirectiveNode],
variable_values: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Get coerced argument values based on provided definitions and nodes.
Prepares a dict of argument values given a list of argument definitions and list
of argument AST nodes.
"""
coerced_values: Dict[str, Any] = {}
arg_node_map = {arg.name.value: arg for arg in node.arguments or []}
for name, arg_def in type_def.args.items():
arg_type = arg_def.type
argument_node = arg_node_map.get(name)
if argument_node is None:
value = arg_def.default_value
if value is not Undefined:
if is_input_object_type(arg_def.type):
# coerce input value so that out_names are used
value = coerce_input_value(value, arg_def.type)
coerced_values[arg_def.out_name or name] = value
elif is_non_null_type(arg_type): # pragma: no cover else
raise GraphQLError(
f"Argument '{name}' of required type '{arg_type}'"
" was not provided.",
node,
)
continue # pragma: no cover
value_node = argument_node.value
is_null = isinstance(argument_node.value, NullValueNode)
if isinstance(value_node, VariableNode):
variable_name = value_node.name.value
if variable_values is None or variable_name not in variable_values:
value = arg_def.default_value
if value is not Undefined:
if is_input_object_type(arg_def.type):
# coerce input value so that out_names are used
value = coerce_input_value(value, arg_def.type)
coerced_values[arg_def.out_name or name] = value
elif is_non_null_type(arg_type): # pragma: no cover else
raise GraphQLError(
f"Argument '{name}' of required type '{arg_type}'"
f" was provided the variable '${variable_name}'"
" which was not provided a runtime value.",
value_node,
)
continue # pragma: no cover
variable_value = variable_values[variable_name]
is_null = variable_value is None or variable_value is Undefined
if is_null and is_non_null_type(arg_type):
raise GraphQLError(
f"Argument '{name}' of non-null type '{arg_type}' must not be null.",
value_node,
)
coerced_value = value_from_ast(value_node, arg_type, variable_values)
if coerced_value is Undefined:
# Note: `values_of_correct_type` validation should catch this before
# execution. This is a runtime check to ensure execution does not
# continue with an invalid argument value.
raise GraphQLError(
f"Argument '{name}' has invalid value {print_ast(value_node)}.",
value_node,
)
coerced_values[arg_def.out_name or name] = coerced_value
return coerced_values
NodeWithDirective = Union[
EnumValueDefinitionNode,
ExecutableDefinitionNode,
FieldDefinitionNode,
InputValueDefinitionNode,
SelectionNode,
SchemaDefinitionNode,
TypeDefinitionNode,
TypeExtensionNode,
]
def get_directive_values(
directive_def: GraphQLDirective,
node: NodeWithDirective,
variable_values: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
"""Get coerced argument values based on provided nodes.
Prepares a dict of argument values given a directive definition and an AST node
which may contain directives. Optionally also accepts a dict of variable values.
If the directive does not exist on the node, returns None.
"""
directives = node.directives
if directives:
directive_name = directive_def.name
for directive in directives:
if directive.name.value == directive_name:
return get_argument_values(directive_def, directive, variable_values)
return None

View File

@@ -0,0 +1,198 @@
from asyncio import ensure_future
from inspect import isawaitable
from typing import Any, Callable, Dict, Optional, Type, Union
from .error import GraphQLError
from .execution import ExecutionContext, ExecutionResult, Middleware, execute
from .language import Source, parse
from .pyutils import AwaitableOrValue
from .type import (
GraphQLFieldResolver,
GraphQLSchema,
GraphQLTypeResolver,
validate_schema,
)
__all__ = ["graphql", "graphql_sync"]
async def graphql(
schema: GraphQLSchema,
source: Union[str, Source],
root_value: Any = None,
context_value: Any = None,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
field_resolver: Optional[GraphQLFieldResolver] = None,
type_resolver: Optional[GraphQLTypeResolver] = None,
middleware: Optional[Middleware] = None,
execution_context_class: Optional[Type[ExecutionContext]] = None,
is_awaitable: Optional[Callable[[Any], bool]] = None,
) -> ExecutionResult:
"""Execute a GraphQL operation asynchronously.
This is the primary entry point function for fulfilling GraphQL operations by
parsing, validating, and executing a GraphQL document along side a GraphQL schema.
More sophisticated GraphQL servers, such as those which persist queries, may wish
to separate the validation and execution phases to a static time tooling step,
and a server runtime step.
Accepts the following arguments:
:arg schema:
The GraphQL type system to use when validating and executing a query.
:arg source:
A GraphQL language formatted string representing the requested operation.
:arg root_value:
The value provided as the first argument to resolver functions on the top level
type (e.g. the query object type).
:arg context_value:
The context value is provided as an attribute of the second argument
(the resolve info) to resolver functions. It is used to pass shared information
useful at any point during query execution, for example the currently logged in
user and connections to databases or other services.
:arg variable_values:
A mapping of variable name to runtime value to use for all variables defined
in the request string.
:arg operation_name:
The name of the operation to use if request string contains multiple possible
operations. Can be omitted if request string contains only one operation.
:arg field_resolver:
A resolver function to use when one is not provided by the schema.
If not provided, the default field resolver is used (which looks for a value
or method on the source value with the field's name).
:arg type_resolver:
A type resolver function to use when none is provided by the schema.
If not provided, the default type resolver is used (which looks for a
``__typename`` field or alternatively calls the
:meth:`~graphql.type.GraphQLObjectType.is_type_of` method).
:arg middleware:
The middleware to wrap the resolvers with
:arg execution_context_class:
The execution context class to use to build the context
:arg is_awaitable:
The predicate to be used for checking whether values are awaitable
"""
# Always return asynchronously for a consistent API.
result = graphql_impl(
schema,
source,
root_value,
context_value,
variable_values,
operation_name,
field_resolver,
type_resolver,
middleware,
execution_context_class,
is_awaitable,
)
if isawaitable(result):
return await result
return result
def assume_not_awaitable(_value: Any) -> bool:
"""Replacement for isawaitable if everything is assumed to be synchronous."""
return False
def graphql_sync(
schema: GraphQLSchema,
source: Union[str, Source],
root_value: Any = None,
context_value: Any = None,
variable_values: Optional[Dict[str, Any]] = None,
operation_name: Optional[str] = None,
field_resolver: Optional[GraphQLFieldResolver] = None,
type_resolver: Optional[GraphQLTypeResolver] = None,
middleware: Optional[Middleware] = None,
execution_context_class: Optional[Type[ExecutionContext]] = None,
check_sync: bool = False,
) -> ExecutionResult:
"""Execute a GraphQL operation synchronously.
The graphql_sync function also fulfills GraphQL operations by parsing, validating,
and executing a GraphQL document along side a GraphQL schema. However, it guarantees
to complete synchronously (or throw an error) assuming that all field resolvers
are also synchronous.
Set check_sync to True to still run checks that no awaitable values are returned.
"""
is_awaitable = (
check_sync
if callable(check_sync)
else (None if check_sync else assume_not_awaitable)
)
result = graphql_impl(
schema,
source,
root_value,
context_value,
variable_values,
operation_name,
field_resolver,
type_resolver,
middleware,
execution_context_class,
is_awaitable,
)
# Assert that the execution was synchronous.
if isawaitable(result):
ensure_future(result).cancel()
raise RuntimeError("GraphQL execution failed to complete synchronously.")
return result
def graphql_impl(
schema: GraphQLSchema,
source: Union[str, Source],
root_value: Any,
context_value: Any,
variable_values: Optional[Dict[str, Any]],
operation_name: Optional[str],
field_resolver: Optional[GraphQLFieldResolver],
type_resolver: Optional[GraphQLTypeResolver],
middleware: Optional[Middleware],
execution_context_class: Optional[Type[ExecutionContext]],
is_awaitable: Optional[Callable[[Any], bool]],
) -> AwaitableOrValue[ExecutionResult]:
"""Execute a query, return asynchronously only if necessary."""
# Validate Schema
schema_validation_errors = validate_schema(schema)
if schema_validation_errors:
return ExecutionResult(data=None, errors=schema_validation_errors)
# Parse
try:
document = parse(source)
except GraphQLError as error:
return ExecutionResult(data=None, errors=[error])
# Validate
from .validation import validate
validation_errors = validate(schema, document)
if validation_errors:
return ExecutionResult(data=None, errors=validation_errors)
# Execute
return execute(
schema,
document,
root_value,
context_value,
variable_values,
operation_name,
field_resolver,
type_resolver,
None,
middleware,
execution_context_class,
is_awaitable,
)

View File

@@ -0,0 +1,208 @@
"""GraphQL Language
The :mod:`graphql.language` package is responsible for parsing and operating on the
GraphQL language.
"""
from .source import Source
from .location import get_location, SourceLocation, FormattedSourceLocation
from .print_location import print_location, print_source_location
from .token_kind import TokenKind
from .lexer import Lexer
from .parser import parse, parse_type, parse_value, parse_const_value
from .printer import print_ast
from .visitor import (
visit,
Visitor,
ParallelVisitor,
VisitorAction,
VisitorKeyMap,
BREAK,
SKIP,
REMOVE,
IDLE,
)
from .ast import (
Location,
Token,
Node,
# Each kind of AST node
NameNode,
DocumentNode,
DefinitionNode,
ExecutableDefinitionNode,
OperationDefinitionNode,
OperationType,
VariableDefinitionNode,
VariableNode,
SelectionSetNode,
SelectionNode,
FieldNode,
ArgumentNode,
ConstArgumentNode,
FragmentSpreadNode,
InlineFragmentNode,
FragmentDefinitionNode,
ValueNode,
ConstValueNode,
IntValueNode,
FloatValueNode,
StringValueNode,
BooleanValueNode,
NullValueNode,
EnumValueNode,
ListValueNode,
ConstListValueNode,
ObjectValueNode,
ConstObjectValueNode,
ObjectFieldNode,
ConstObjectFieldNode,
DirectiveNode,
ConstDirectiveNode,
TypeNode,
NamedTypeNode,
ListTypeNode,
NonNullTypeNode,
TypeSystemDefinitionNode,
SchemaDefinitionNode,
OperationTypeDefinitionNode,
TypeDefinitionNode,
ScalarTypeDefinitionNode,
ObjectTypeDefinitionNode,
FieldDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
UnionTypeDefinitionNode,
EnumTypeDefinitionNode,
EnumValueDefinitionNode,
InputObjectTypeDefinitionNode,
DirectiveDefinitionNode,
TypeSystemExtensionNode,
SchemaExtensionNode,
TypeExtensionNode,
ScalarTypeExtensionNode,
ObjectTypeExtensionNode,
InterfaceTypeExtensionNode,
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
)
from .predicates import (
is_definition_node,
is_executable_definition_node,
is_selection_node,
is_value_node,
is_const_value_node,
is_type_node,
is_type_system_definition_node,
is_type_definition_node,
is_type_system_extension_node,
is_type_extension_node,
)
from .directive_locations import DirectiveLocation
__all__ = [
"get_location",
"SourceLocation",
"FormattedSourceLocation",
"print_location",
"print_source_location",
"TokenKind",
"Lexer",
"parse",
"parse_value",
"parse_const_value",
"parse_type",
"print_ast",
"Source",
"visit",
"Visitor",
"ParallelVisitor",
"VisitorAction",
"VisitorKeyMap",
"BREAK",
"SKIP",
"REMOVE",
"IDLE",
"Location",
"Token",
"DirectiveLocation",
"Node",
"NameNode",
"DocumentNode",
"DefinitionNode",
"ExecutableDefinitionNode",
"OperationDefinitionNode",
"OperationType",
"VariableDefinitionNode",
"VariableNode",
"SelectionSetNode",
"SelectionNode",
"FieldNode",
"ArgumentNode",
"ConstArgumentNode",
"FragmentSpreadNode",
"InlineFragmentNode",
"FragmentDefinitionNode",
"ValueNode",
"ConstValueNode",
"IntValueNode",
"FloatValueNode",
"StringValueNode",
"BooleanValueNode",
"NullValueNode",
"EnumValueNode",
"ListValueNode",
"ConstListValueNode",
"ObjectValueNode",
"ConstObjectValueNode",
"ObjectFieldNode",
"ConstObjectFieldNode",
"DirectiveNode",
"ConstDirectiveNode",
"TypeNode",
"NamedTypeNode",
"ListTypeNode",
"NonNullTypeNode",
"TypeSystemDefinitionNode",
"SchemaDefinitionNode",
"OperationTypeDefinitionNode",
"TypeDefinitionNode",
"ScalarTypeDefinitionNode",
"ObjectTypeDefinitionNode",
"FieldDefinitionNode",
"InputValueDefinitionNode",
"InterfaceTypeDefinitionNode",
"UnionTypeDefinitionNode",
"EnumTypeDefinitionNode",
"EnumValueDefinitionNode",
"InputObjectTypeDefinitionNode",
"DirectiveDefinitionNode",
"TypeSystemExtensionNode",
"SchemaExtensionNode",
"TypeExtensionNode",
"ScalarTypeExtensionNode",
"ObjectTypeExtensionNode",
"InterfaceTypeExtensionNode",
"UnionTypeExtensionNode",
"EnumTypeExtensionNode",
"InputObjectTypeExtensionNode",
"is_definition_node",
"is_executable_definition_node",
"is_selection_node",
"is_value_node",
"is_const_value_node",
"is_type_node",
"is_type_system_definition_node",
"is_type_definition_node",
"is_type_system_extension_node",
"is_type_extension_node",
]

View File

@@ -0,0 +1,807 @@
from copy import copy, deepcopy
from enum import Enum
from typing import Any, Dict, List, Tuple, Optional, Union
from .source import Source
from .token_kind import TokenKind
from ..pyutils import camel_to_snake
__all__ = [
"Location",
"Token",
"Node",
"NameNode",
"DocumentNode",
"DefinitionNode",
"ExecutableDefinitionNode",
"OperationDefinitionNode",
"VariableDefinitionNode",
"SelectionSetNode",
"SelectionNode",
"FieldNode",
"ArgumentNode",
"ConstArgumentNode",
"FragmentSpreadNode",
"InlineFragmentNode",
"FragmentDefinitionNode",
"ValueNode",
"ConstValueNode",
"VariableNode",
"IntValueNode",
"FloatValueNode",
"StringValueNode",
"BooleanValueNode",
"NullValueNode",
"EnumValueNode",
"ListValueNode",
"ConstListValueNode",
"ObjectValueNode",
"ConstObjectValueNode",
"ObjectFieldNode",
"ConstObjectFieldNode",
"DirectiveNode",
"ConstDirectiveNode",
"TypeNode",
"NamedTypeNode",
"ListTypeNode",
"NonNullTypeNode",
"TypeSystemDefinitionNode",
"SchemaDefinitionNode",
"OperationType",
"OperationTypeDefinitionNode",
"TypeDefinitionNode",
"ScalarTypeDefinitionNode",
"ObjectTypeDefinitionNode",
"FieldDefinitionNode",
"InputValueDefinitionNode",
"InterfaceTypeDefinitionNode",
"UnionTypeDefinitionNode",
"EnumTypeDefinitionNode",
"EnumValueDefinitionNode",
"InputObjectTypeDefinitionNode",
"DirectiveDefinitionNode",
"SchemaExtensionNode",
"TypeExtensionNode",
"TypeSystemExtensionNode",
"ScalarTypeExtensionNode",
"ObjectTypeExtensionNode",
"InterfaceTypeExtensionNode",
"UnionTypeExtensionNode",
"EnumTypeExtensionNode",
"InputObjectTypeExtensionNode",
"QUERY_DOCUMENT_KEYS",
]
class Token:
"""AST Token
Represents a range of characters represented by a lexical token within a Source.
"""
__slots__ = "kind", "start", "end", "line", "column", "prev", "next", "value"
kind: TokenKind # the kind of token
start: int # the character offset at which this Node begins
end: int # the character offset at which this Node ends
line: int # the 1-indexed line number on which this Token appears
column: int # the 1-indexed column number at which this Token begins
# for non-punctuation tokens, represents the interpreted value of the token:
value: Optional[str]
# Tokens exist as nodes in a double-linked-list amongst all tokens including
# ignored tokens. <SOF> is always the first node and <EOF> the last.
prev: Optional["Token"]
next: Optional["Token"]
def __init__(
self,
kind: TokenKind,
start: int,
end: int,
line: int,
column: int,
value: Optional[str] = None,
) -> None:
self.kind = kind
self.start, self.end = start, end
self.line, self.column = line, column
self.value = value
self.prev = self.next = None
def __str__(self) -> str:
return self.desc
def __repr__(self) -> str:
"""Print a simplified form when appearing in repr() or inspect()."""
return f"<Token {self.desc} {self.line}:{self.column}>"
def __inspect__(self) -> str:
return repr(self)
def __eq__(self, other: Any) -> bool:
if isinstance(other, Token):
return (
self.kind == other.kind
and self.start == other.start
and self.end == other.end
and self.line == other.line
and self.column == other.column
and self.value == other.value
)
elif isinstance(other, str):
return other == self.desc
return False
def __hash__(self) -> int:
return hash(
(self.kind, self.start, self.end, self.line, self.column, self.value)
)
def __copy__(self) -> "Token":
"""Create a shallow copy of the token"""
token = self.__class__(
self.kind,
self.start,
self.end,
self.line,
self.column,
self.value,
)
token.prev = self.prev
return token
def __deepcopy__(self, memo: Dict) -> "Token":
"""Allow only shallow copies to avoid recursion."""
return copy(self)
def __getstate__(self) -> Dict[str, Any]:
"""Remove the links when pickling.
Keeping the links would make pickling a schema too expensive.
"""
return {
key: getattr(self, key)
for key in self.__slots__
if key not in {"prev", "next"}
}
def __setstate__(self, state: Dict[str, Any]) -> None:
"""Reset the links when un-pickling."""
for key, value in state.items():
setattr(self, key, value)
self.prev = self.next = None
@property
def desc(self) -> str:
"""A helper property to describe a token as a string for debugging"""
kind, value = self.kind.value, self.value
return f"{kind} {value!r}" if value else kind
class Location:
"""AST Location
Contains a range of UTF-8 character offsets and token references that identify the
region of the source from which the AST derived.
"""
__slots__ = (
"start",
"end",
"start_token",
"end_token",
"source",
)
start: int # character offset at which this Node begins
end: int # character offset at which this Node ends
start_token: Token # Token at which this Node begins
end_token: Token # Token at which this Node ends.
source: Source # Source document the AST represents
def __init__(self, start_token: Token, end_token: Token, source: Source) -> None:
self.start = start_token.start
self.end = end_token.end
self.start_token = start_token
self.end_token = end_token
self.source = source
def __str__(self) -> str:
return f"{self.start}:{self.end}"
def __repr__(self) -> str:
"""Print a simplified form when appearing in repr() or inspect()."""
return f"<Location {self.start}:{self.end}>"
def __inspect__(self) -> str:
return repr(self)
def __eq__(self, other: Any) -> bool:
if isinstance(other, Location):
return self.start == other.start and self.end == other.end
elif isinstance(other, (list, tuple)) and len(other) == 2:
return self.start == other[0] and self.end == other[1]
return False
def __ne__(self, other: Any) -> bool:
return not self == other
def __hash__(self) -> int:
return hash((self.start, self.end))
class OperationType(Enum):
QUERY = "query"
MUTATION = "mutation"
SUBSCRIPTION = "subscription"
# Default map from node kinds to their node attributes (internal)
QUERY_DOCUMENT_KEYS: Dict[str, Tuple[str, ...]] = {
"name": (),
"document": ("definitions",),
"operation_definition": (
"name",
"variable_definitions",
"directives",
"selection_set",
),
"variable_definition": ("variable", "type", "default_value", "directives"),
"variable": ("name",),
"selection_set": ("selections",),
"field": ("alias", "name", "arguments", "directives", "selection_set"),
"argument": ("name", "value"),
"fragment_spread": ("name", "directives"),
"inline_fragment": ("type_condition", "directives", "selection_set"),
"fragment_definition": (
# Note: fragment variable definitions are deprecated and will be removed in v3.3
"name",
"variable_definitions",
"type_condition",
"directives",
"selection_set",
),
"list_value": ("values",),
"object_value": ("fields",),
"object_field": ("name", "value"),
"directive": ("name", "arguments"),
"named_type": ("name",),
"list_type": ("type",),
"non_null_type": ("type",),
"schema_definition": ("description", "directives", "operation_types"),
"operation_type_definition": ("type",),
"scalar_type_definition": ("description", "name", "directives"),
"object_type_definition": (
"description",
"name",
"interfaces",
"directives",
"fields",
),
"field_definition": ("description", "name", "arguments", "type", "directives"),
"input_value_definition": (
"description",
"name",
"type",
"default_value",
"directives",
),
"interface_type_definition": (
"description",
"name",
"interfaces",
"directives",
"fields",
),
"union_type_definition": ("description", "name", "directives", "types"),
"enum_type_definition": ("description", "name", "directives", "values"),
"enum_value_definition": ("description", "name", "directives"),
"input_object_type_definition": ("description", "name", "directives", "fields"),
"directive_definition": ("description", "name", "arguments", "locations"),
"schema_extension": ("directives", "operation_types"),
"scalar_type_extension": ("name", "directives"),
"object_type_extension": ("name", "interfaces", "directives", "fields"),
"interface_type_extension": ("name", "interfaces", "directives", "fields"),
"union_type_extension": ("name", "directives", "types"),
"enum_type_extension": ("name", "directives", "values"),
"input_object_type_extension": ("name", "directives", "fields"),
}
# Base AST Node
class Node:
"""AST nodes"""
# allow custom attributes and weak references (not used internally)
__slots__ = "__dict__", "__weakref__", "loc", "_hash"
loc: Optional[Location]
kind: str = "ast" # the kind of the node as a snake_case string
keys: Tuple[str, ...] = ("loc",) # the names of the attributes of this node
def __init__(self, **kwargs: Any) -> None:
"""Initialize the node with the given keyword arguments."""
for key in self.keys:
value = kwargs.get(key)
if isinstance(value, list):
value = tuple(value)
setattr(self, key, value)
def __repr__(self) -> str:
"""Get a simple representation of the node."""
name, loc = self.__class__.__name__, getattr(self, "loc", None)
return f"{name} at {loc}" if loc else name
def __eq__(self, other: Any) -> bool:
"""Test whether two nodes are equal (recursively)."""
return (
isinstance(other, Node)
and self.__class__ == other.__class__
and all(getattr(self, key) == getattr(other, key) for key in self.keys)
)
def __hash__(self) -> int:
"""Get a cached hash value for the node."""
# Caching the hash values improves the performance of AST validators
hashed = getattr(self, "_hash", None)
if hashed is None:
self._hash = id(self) # avoid recursion
hashed = hash(tuple(getattr(self, key) for key in self.keys))
self._hash = hashed
return hashed
def __setattr__(self, key: str, value: Any) -> None:
# reset cashed hash value if attributes are changed
if hasattr(self, "_hash") and key in self.keys:
del self._hash
super().__setattr__(key, value)
def __copy__(self) -> "Node":
"""Create a shallow copy of the node."""
return self.__class__(**{key: getattr(self, key) for key in self.keys})
def __deepcopy__(self, memo: Dict) -> "Node":
"""Create a deep copy of the node"""
# noinspection PyArgumentList
return self.__class__(
**{key: deepcopy(getattr(self, key), memo) for key in self.keys}
)
def __init_subclass__(cls) -> None:
super().__init_subclass__()
name = cls.__name__
try:
name = name.removeprefix("Const").removesuffix("Node")
except AttributeError: # pragma: no cover (Python < 3.9)
if name.startswith("Const"):
name = name[5:]
if name.endswith("Node"):
name = name[:-4]
cls.kind = camel_to_snake(name)
keys: List[str] = []
for base in cls.__bases__:
# noinspection PyUnresolvedReferences
keys.extend(base.keys) # type: ignore
keys.extend(cls.__slots__)
cls.keys = tuple(keys)
def to_dict(self, locations: bool = False) -> Dict:
from ..utilities import ast_to_dict
return ast_to_dict(self, locations)
# Name
class NameNode(Node):
__slots__ = ("value",)
value: str
# Document
class DocumentNode(Node):
__slots__ = ("definitions",)
definitions: Tuple["DefinitionNode", ...]
class DefinitionNode(Node):
__slots__ = ()
class ExecutableDefinitionNode(DefinitionNode):
__slots__ = "name", "directives", "variable_definitions", "selection_set"
name: Optional[NameNode]
directives: Tuple["DirectiveNode", ...]
variable_definitions: Tuple["VariableDefinitionNode", ...]
selection_set: "SelectionSetNode"
class OperationDefinitionNode(ExecutableDefinitionNode):
__slots__ = ("operation",)
operation: OperationType
class VariableDefinitionNode(Node):
__slots__ = "variable", "type", "default_value", "directives"
variable: "VariableNode"
type: "TypeNode"
default_value: Optional["ConstValueNode"]
directives: Tuple["ConstDirectiveNode", ...]
class SelectionSetNode(Node):
__slots__ = ("selections",)
selections: Tuple["SelectionNode", ...]
class SelectionNode(Node):
__slots__ = ("directives",)
directives: Tuple["DirectiveNode", ...]
class FieldNode(SelectionNode):
__slots__ = "alias", "name", "arguments", "selection_set"
alias: Optional[NameNode]
name: NameNode
arguments: Tuple["ArgumentNode", ...]
selection_set: Optional[SelectionSetNode]
class ArgumentNode(Node):
__slots__ = "name", "value"
name: NameNode
value: "ValueNode"
class ConstArgumentNode(ArgumentNode):
value: "ConstValueNode"
# Fragments
class FragmentSpreadNode(SelectionNode):
__slots__ = ("name",)
name: NameNode
class InlineFragmentNode(SelectionNode):
__slots__ = "type_condition", "selection_set"
type_condition: "NamedTypeNode"
selection_set: SelectionSetNode
class FragmentDefinitionNode(ExecutableDefinitionNode):
__slots__ = ("type_condition",)
name: NameNode
type_condition: "NamedTypeNode"
# Values
class ValueNode(Node):
__slots__ = ()
class VariableNode(ValueNode):
__slots__ = ("name",)
name: NameNode
class IntValueNode(ValueNode):
__slots__ = ("value",)
value: str
class FloatValueNode(ValueNode):
__slots__ = ("value",)
value: str
class StringValueNode(ValueNode):
__slots__ = "value", "block"
value: str
block: Optional[bool]
class BooleanValueNode(ValueNode):
__slots__ = ("value",)
value: bool
class NullValueNode(ValueNode):
__slots__ = ()
class EnumValueNode(ValueNode):
__slots__ = ("value",)
value: str
class ListValueNode(ValueNode):
__slots__ = ("values",)
values: Tuple[ValueNode, ...]
class ConstListValueNode(ListValueNode):
values: Tuple["ConstValueNode", ...]
class ObjectValueNode(ValueNode):
__slots__ = ("fields",)
fields: Tuple["ObjectFieldNode", ...]
class ConstObjectValueNode(ObjectValueNode):
fields: Tuple["ConstObjectFieldNode", ...]
class ObjectFieldNode(Node):
__slots__ = "name", "value"
name: NameNode
value: ValueNode
class ConstObjectFieldNode(ObjectFieldNode):
value: "ConstValueNode"
ConstValueNode = Union[
IntValueNode,
FloatValueNode,
StringValueNode,
BooleanValueNode,
NullValueNode,
EnumValueNode,
ConstListValueNode,
ConstObjectValueNode,
]
# Directives
class DirectiveNode(Node):
__slots__ = "name", "arguments"
name: NameNode
arguments: Tuple[ArgumentNode, ...]
class ConstDirectiveNode(DirectiveNode):
arguments: Tuple[ConstArgumentNode, ...]
# Type Reference
class TypeNode(Node):
__slots__ = ()
class NamedTypeNode(TypeNode):
__slots__ = ("name",)
name: NameNode
class ListTypeNode(TypeNode):
__slots__ = ("type",)
type: TypeNode
class NonNullTypeNode(TypeNode):
__slots__ = ("type",)
type: Union[NamedTypeNode, ListTypeNode]
# Type System Definition
class TypeSystemDefinitionNode(DefinitionNode):
__slots__ = ()
class SchemaDefinitionNode(TypeSystemDefinitionNode):
__slots__ = "description", "directives", "operation_types"
description: Optional[StringValueNode]
directives: Tuple[ConstDirectiveNode, ...]
operation_types: Tuple["OperationTypeDefinitionNode", ...]
class OperationTypeDefinitionNode(Node):
__slots__ = "operation", "type"
operation: OperationType
type: NamedTypeNode
# Type Definition
class TypeDefinitionNode(TypeSystemDefinitionNode):
__slots__ = "description", "name", "directives"
description: Optional[StringValueNode]
name: NameNode
directives: Tuple[DirectiveNode, ...]
class ScalarTypeDefinitionNode(TypeDefinitionNode):
__slots__ = ()
directives: Tuple[ConstDirectiveNode, ...]
class ObjectTypeDefinitionNode(TypeDefinitionNode):
__slots__ = "interfaces", "fields"
interfaces: Tuple[NamedTypeNode, ...]
directives: Tuple[ConstDirectiveNode, ...]
fields: Tuple["FieldDefinitionNode", ...]
class FieldDefinitionNode(DefinitionNode):
__slots__ = "description", "name", "directives", "arguments", "type"
description: Optional[StringValueNode]
name: NameNode
directives: Tuple[ConstDirectiveNode, ...]
arguments: Tuple["InputValueDefinitionNode", ...]
type: TypeNode
class InputValueDefinitionNode(DefinitionNode):
__slots__ = "description", "name", "directives", "type", "default_value"
description: Optional[StringValueNode]
name: NameNode
directives: Tuple[ConstDirectiveNode, ...]
type: TypeNode
default_value: Optional[ConstValueNode]
class InterfaceTypeDefinitionNode(TypeDefinitionNode):
__slots__ = "fields", "interfaces"
fields: Tuple["FieldDefinitionNode", ...]
directives: Tuple[ConstDirectiveNode, ...]
interfaces: Tuple[NamedTypeNode, ...]
class UnionTypeDefinitionNode(TypeDefinitionNode):
__slots__ = ("types",)
directives: Tuple[ConstDirectiveNode, ...]
types: Tuple[NamedTypeNode, ...]
class EnumTypeDefinitionNode(TypeDefinitionNode):
__slots__ = ("values",)
directives: Tuple[ConstDirectiveNode, ...]
values: Tuple["EnumValueDefinitionNode", ...]
class EnumValueDefinitionNode(DefinitionNode):
__slots__ = "description", "name", "directives"
description: Optional[StringValueNode]
name: NameNode
directives: Tuple[ConstDirectiveNode, ...]
class InputObjectTypeDefinitionNode(TypeDefinitionNode):
__slots__ = ("fields",)
directives: Tuple[ConstDirectiveNode, ...]
fields: Tuple[InputValueDefinitionNode, ...]
# Directive Definitions
class DirectiveDefinitionNode(TypeSystemDefinitionNode):
__slots__ = "description", "name", "arguments", "repeatable", "locations"
description: Optional[StringValueNode]
name: NameNode
arguments: Tuple[InputValueDefinitionNode, ...]
repeatable: bool
locations: Tuple[NameNode, ...]
# Type System Extensions
class SchemaExtensionNode(Node):
__slots__ = "directives", "operation_types"
directives: Tuple[ConstDirectiveNode, ...]
operation_types: Tuple[OperationTypeDefinitionNode, ...]
# Type Extensions
class TypeExtensionNode(TypeSystemDefinitionNode):
__slots__ = "name", "directives"
name: NameNode
directives: Tuple[ConstDirectiveNode, ...]
TypeSystemExtensionNode = Union[SchemaExtensionNode, TypeExtensionNode]
class ScalarTypeExtensionNode(TypeExtensionNode):
__slots__ = ()
class ObjectTypeExtensionNode(TypeExtensionNode):
__slots__ = "interfaces", "fields"
interfaces: Tuple[NamedTypeNode, ...]
fields: Tuple[FieldDefinitionNode, ...]
class InterfaceTypeExtensionNode(TypeExtensionNode):
__slots__ = "interfaces", "fields"
interfaces: Tuple[NamedTypeNode, ...]
fields: Tuple[FieldDefinitionNode, ...]
class UnionTypeExtensionNode(TypeExtensionNode):
__slots__ = ("types",)
types: Tuple[NamedTypeNode, ...]
class EnumTypeExtensionNode(TypeExtensionNode):
__slots__ = ("values",)
values: Tuple[EnumValueDefinitionNode, ...]
class InputObjectTypeExtensionNode(TypeExtensionNode):
__slots__ = ("fields",)
fields: Tuple[InputValueDefinitionNode, ...]

View File

@@ -0,0 +1,155 @@
from typing import Collection, List
from sys import maxsize
__all__ = [
"dedent_block_string_lines",
"is_printable_as_block_string",
"print_block_string",
]
def dedent_block_string_lines(lines: Collection[str]) -> List[str]:
"""Produce the value of a block string from its parsed raw value.
This function works similar to CoffeeScript's block string,
Python's docstring trim or Ruby's strip_heredoc.
It implements the GraphQL spec's BlockStringValue() static algorithm.
Note that this is very similar to Python's inspect.cleandoc() function.
The difference is that the latter also expands tabs to spaces and
removes whitespace at the beginning of the first line. Python also has
textwrap.dedent() which uses a completely different algorithm.
For internal use only.
"""
common_indent = maxsize
first_non_empty_line = None
last_non_empty_line = -1
for i, line in enumerate(lines):
indent = leading_white_space(line)
if indent == len(line):
continue # skip empty lines
if first_non_empty_line is None:
first_non_empty_line = i
last_non_empty_line = i
if i and indent < common_indent:
common_indent = indent
if first_non_empty_line is None:
first_non_empty_line = 0
return [ # Remove common indentation from all lines but first.
line[common_indent:] if i else line for i, line in enumerate(lines)
][ # Remove leading and trailing blank lines.
first_non_empty_line : last_non_empty_line + 1
]
def leading_white_space(s: str) -> int:
i = 0
for c in s:
if c not in " \t":
return i
i += 1
return i
def is_printable_as_block_string(value: str) -> bool:
"""Check whether the given string is printable as a block string.
For internal use only.
"""
if not isinstance(value, str):
value = str(value) # resolve lazy string proxy object
if not value:
return True # emtpy string is printable
is_empty_line = True
has_indent = False
has_common_indent = True
seen_non_empty_line = False
for c in value:
if c == "\n":
if is_empty_line and not seen_non_empty_line:
return False # has leading new line
seen_non_empty_line = True
is_empty_line = True
has_indent = False
elif c in " \t":
has_indent = has_indent or is_empty_line
elif c <= "\x0f":
return False
else:
has_common_indent = has_common_indent and has_indent
is_empty_line = False
if is_empty_line:
return False # has trailing empty lines
if has_common_indent and seen_non_empty_line:
return False # has internal indent
return True
def print_block_string(value: str, minimize: bool = False) -> str:
"""Print a block string in the indented block form.
Prints a block string in the indented block form by adding a leading and
trailing blank line. However, if a block string starts with whitespace and
is a single-line, adding a leading blank line would strip that whitespace.
For internal use only.
"""
if not isinstance(value, str):
value = str(value) # resolve lazy string proxy object
escaped_value = value.replace('"""', '\\"""')
# Expand a block string's raw value into independent lines.
lines = escaped_value.splitlines() or [""]
num_lines = len(lines)
is_single_line = num_lines == 1
# If common indentation is found,
# we can fix some of those cases by adding a leading new line.
force_leading_new_line = num_lines > 1 and all(
not line or line[0] in " \t" for line in lines[1:]
)
# Trailing triple quotes just looks confusing but doesn't force trailing new line.
has_trailing_triple_quotes = escaped_value.endswith('\\"""')
# Trailing quote (single or double) or slash forces trailing new line
has_trailing_quote = value.endswith('"') and not has_trailing_triple_quotes
has_trailing_slash = value.endswith("\\")
force_trailing_new_line = has_trailing_quote or has_trailing_slash
print_as_multiple_lines = not minimize and (
# add leading and trailing new lines only if it improves readability
not is_single_line
or len(value) > 70
or force_trailing_new_line
or force_leading_new_line
or has_trailing_triple_quotes
)
# Format a multi-line block quote to account for leading space.
skip_leading_new_line = is_single_line and value and value[0] in " \t"
before = (
"\n"
if print_as_multiple_lines
and not skip_leading_new_line
or force_leading_new_line
else ""
)
after = "\n" if print_as_multiple_lines or force_trailing_new_line else ""
return f'"""{before}{escaped_value}{after}"""'

View File

@@ -0,0 +1,68 @@
__all__ = ["is_digit", "is_letter", "is_name_start", "is_name_continue"]
try:
"string".isascii()
except AttributeError: # Python < 3.7
def is_digit(char: str) -> bool:
"""Check whether char is a digit
For internal use by the lexer only.
"""
return "0" <= char <= "9"
def is_letter(char: str) -> bool:
"""Check whether char is a plain ASCII letter
For internal use by the lexer only.
"""
return "a" <= char <= "z" or "A" <= char <= "Z"
def is_name_start(char: str) -> bool:
"""Check whether char is allowed at the beginning of a GraphQL name
For internal use by the lexer only.
"""
return "a" <= char <= "z" or "A" <= char <= "Z" or char == "_"
def is_name_continue(char: str) -> bool:
"""Check whether char is allowed in the continuation of a GraphQL name
For internal use by the lexer only.
"""
return (
"a" <= char <= "z"
or "A" <= char <= "Z"
or "0" <= char <= "9"
or char == "_"
)
else:
def is_digit(char: str) -> bool:
"""Check whether char is a digit
For internal use by the lexer only.
"""
return char.isascii() and char.isdigit()
def is_letter(char: str) -> bool:
"""Check whether char is a plain ASCII letter
For internal use by the lexer only.
"""
return char.isascii() and char.isalpha()
def is_name_start(char: str) -> bool:
"""Check whether char is allowed at the beginning of a GraphQL name
For internal use by the lexer only.
"""
return char.isascii() and (char.isalpha() or char == "_")
def is_name_continue(char: str) -> bool:
"""Check whether char is allowed in the continuation of a GraphQL name
For internal use by the lexer only.
"""
return char.isascii() and (char.isalnum() or char == "_")

View File

@@ -0,0 +1,30 @@
from enum import Enum
__all__ = ["DirectiveLocation"]
class DirectiveLocation(Enum):
"""The enum type representing the directive location values."""
# Request Definitions
QUERY = "query"
MUTATION = "mutation"
SUBSCRIPTION = "subscription"
FIELD = "field"
FRAGMENT_DEFINITION = "fragment definition"
FRAGMENT_SPREAD = "fragment spread"
VARIABLE_DEFINITION = "variable definition"
INLINE_FRAGMENT = "inline fragment"
# Type System Definitions
SCHEMA = "schema"
SCALAR = "scalar"
OBJECT = "object"
FIELD_DEFINITION = "field definition"
ARGUMENT_DEFINITION = "argument definition"
INTERFACE = "interface"
UNION = "union"
ENUM = "enum"
ENUM_VALUE = "enum value"
INPUT_OBJECT = "input object"
INPUT_FIELD_DEFINITION = "input field definition"

View File

@@ -0,0 +1,574 @@
from typing import List, NamedTuple, Optional
from ..error import GraphQLSyntaxError
from .ast import Token
from .block_string import dedent_block_string_lines
from .character_classes import is_digit, is_name_start, is_name_continue
from .source import Source
from .token_kind import TokenKind
__all__ = ["Lexer", "is_punctuator_token_kind"]
class EscapeSequence(NamedTuple):
"""The string value and lexed size of an escape sequence."""
value: str
size: int
class Lexer:
"""GraphQL Lexer
A Lexer is a stateful stream generator in that every time it is advanced, it returns
the next token in the Source. Assuming the source lexes, the final Token emitted by
the lexer will be of kind EOF, after which the lexer will repeatedly return the same
EOF token whenever called.
"""
def __init__(self, source: Source):
"""Given a Source object, initialize a Lexer for that source."""
self.source = source
self.token = self.last_token = Token(TokenKind.SOF, 0, 0, 0, 0)
self.line, self.line_start = 1, 0
def advance(self) -> Token:
"""Advance the token stream to the next non-ignored token."""
self.last_token = self.token
token = self.token = self.lookahead()
return token
def lookahead(self) -> Token:
"""Look ahead and return the next non-ignored token, but do not change state."""
token = self.token
if token.kind != TokenKind.EOF:
while True:
if token.next:
token = token.next
else:
# Read the next token and form a link in the token linked-list.
next_token = self.read_next_token(token.end)
token.next = next_token
next_token.prev = token
token = next_token
if token.kind != TokenKind.COMMENT:
break
return token
def print_code_point_at(self, location: int) -> str:
"""Print the code point at the given location.
Prints the code point (or end of file reference) at a given location in a
source for use in error messages.
Printable ASCII is printed quoted, while other points are printed in Unicode
code point form (ie. U+1234).
"""
body = self.source.body
if location >= len(body):
return TokenKind.EOF.value
char = body[location]
# Printable ASCII
if "\x20" <= char <= "\x7e":
return "'\"'" if char == '"' else f"'{char}'"
# Unicode code point
point = ord(
body[location : location + 2]
.encode("utf-16", "surrogatepass")
.decode("utf-16")
if is_supplementary_code_point(body, location)
else char
)
return f"U+{point:04X}"
def create_token(
self, kind: TokenKind, start: int, end: int, value: Optional[str] = None
) -> Token:
"""Create a token with line and column location information."""
line = self.line
col = 1 + start - self.line_start
return Token(kind, start, end, line, col, value)
def read_next_token(self, start: int) -> Token:
"""Get the next token from the source starting at the given position.
This skips over whitespace until it finds the next lexable token, then lexes
punctuators immediately or calls the appropriate helper function for more
complicated tokens.
"""
body = self.source.body
body_length = len(body)
position = start
while position < body_length:
char = body[position] # SourceCharacter
if char in " \t,\ufeff":
position += 1
continue
elif char == "\n":
position += 1
self.line += 1
self.line_start = position
continue
elif char == "\r":
if body[position + 1 : position + 2] == "\n":
position += 2
else:
position += 1
self.line += 1
self.line_start = position
continue
if char == "#":
return self.read_comment(position)
if char == '"':
if body[position + 1 : position + 3] == '""':
return self.read_block_string(position)
return self.read_string(position)
kind = _KIND_FOR_PUNCT.get(char)
if kind:
return self.create_token(kind, position, position + 1)
if is_digit(char) or char == "-":
return self.read_number(position, char)
if is_name_start(char):
return self.read_name(position)
if char == ".":
if body[position + 1 : position + 3] == "..":
return self.create_token(TokenKind.SPREAD, position, position + 3)
message = (
"Unexpected single quote character ('),"
' did you mean to use a double quote (")?'
if char == "'"
else (
f"Unexpected character: {self.print_code_point_at(position)}."
if is_unicode_scalar_value(char)
or is_supplementary_code_point(body, position)
else f"Invalid character: {self.print_code_point_at(position)}."
)
)
raise GraphQLSyntaxError(self.source, position, message)
return self.create_token(TokenKind.EOF, body_length, body_length)
def read_comment(self, start: int) -> Token:
"""Read a comment token from the source file."""
body = self.source.body
body_length = len(body)
position = start + 1
while position < body_length:
char = body[position]
if char in "\r\n":
break
if is_unicode_scalar_value(char):
position += 1
elif is_supplementary_code_point(body, position):
position += 2
else:
break # pragma: no cover
return self.create_token(
TokenKind.COMMENT,
start,
position,
body[start + 1 : position],
)
def read_number(self, start: int, first_char: str) -> Token:
"""Reads a number token from the source file.
This can be either a FloatValue or an IntValue,
depending on whether a FractionalPart or ExponentPart is encountered.
"""
body = self.source.body
position = start
char = first_char
is_float = False
if char == "-":
position += 1
char = body[position : position + 1]
if char == "0":
position += 1
char = body[position : position + 1]
if is_digit(char):
raise GraphQLSyntaxError(
self.source,
position,
"Invalid number, unexpected digit after 0:"
f" {self.print_code_point_at(position)}.",
)
else:
position = self.read_digits(position, char)
char = body[position : position + 1]
if char == ".":
is_float = True
position += 1
char = body[position : position + 1]
position = self.read_digits(position, char)
char = body[position : position + 1]
if char and char in "Ee":
is_float = True
position += 1
char = body[position : position + 1]
if char and char in "+-":
position += 1
char = body[position : position + 1]
position = self.read_digits(position, char)
char = body[position : position + 1]
# Numbers cannot be followed by . or NameStart
if char and (char == "." or is_name_start(char)):
raise GraphQLSyntaxError(
self.source,
position,
"Invalid number, expected digit but got:"
f" {self.print_code_point_at(position)}.",
)
return self.create_token(
TokenKind.FLOAT if is_float else TokenKind.INT,
start,
position,
body[start:position],
)
def read_digits(self, start: int, first_char: str) -> int:
"""Return the new position in the source after reading one or more digits."""
if not is_digit(first_char):
raise GraphQLSyntaxError(
self.source,
start,
"Invalid number, expected digit but got:"
f" {self.print_code_point_at(start)}.",
)
body = self.source.body
body_length = len(body)
position = start + 1
while position < body_length and is_digit(body[position]):
position += 1
return position
def read_string(self, start: int) -> Token:
"""Read a single-quote string token from the source file."""
body = self.source.body
body_length = len(body)
position = start + 1
chunk_start = position
value: List[str] = []
append = value.append
while position < body_length:
char = body[position]
if char == '"':
append(body[chunk_start:position])
return self.create_token(
TokenKind.STRING,
start,
position + 1,
"".join(value),
)
if char == "\\":
append(body[chunk_start:position])
escape = (
(
self.read_escaped_unicode_variable_width(position)
if body[position + 2 : position + 3] == "{"
else self.read_escaped_unicode_fixed_width(position)
)
if body[position + 1 : position + 2] == "u"
else self.read_escaped_character(position)
)
append(escape.value)
position += escape.size
chunk_start = position
continue
if char in "\r\n":
break
if is_unicode_scalar_value(char):
position += 1
elif is_supplementary_code_point(body, position):
position += 2
else:
raise GraphQLSyntaxError(
self.source,
position,
"Invalid character within String:"
f" {self.print_code_point_at(position)}.",
)
raise GraphQLSyntaxError(self.source, position, "Unterminated string.")
def read_escaped_unicode_variable_width(self, position: int) -> EscapeSequence:
body = self.source.body
point = 0
size = 3
max_size = min(12, len(body) - position)
# Cannot be larger than 12 chars (\u{00000000}).
while size < max_size:
char = body[position + size]
size += 1
if char == "}":
# Must be at least 5 chars (\u{0}) and encode a Unicode scalar value.
if size < 5 or not (
0 <= point <= 0xD7FF or 0xE000 <= point <= 0x10FFFF
):
break
return EscapeSequence(chr(point), size)
# Append this hex digit to the code point.
point = (point << 4) | read_hex_digit(char)
if point < 0:
break
raise GraphQLSyntaxError(
self.source,
position,
f"Invalid Unicode escape sequence: '{body[position: position + size]}'.",
)
def read_escaped_unicode_fixed_width(self, position: int) -> EscapeSequence:
body = self.source.body
code = read_16_bit_hex_code(body, position + 2)
if 0 <= code <= 0xD7FF or 0xE000 <= code <= 0x10FFFF:
return EscapeSequence(chr(code), 6)
# GraphQL allows JSON-style surrogate pair escape sequences, but only when
# a valid pair is formed.
if 0xD800 <= code <= 0xDBFF:
if body[position + 6 : position + 8] == "\\u":
trailing_code = read_16_bit_hex_code(body, position + 8)
if 0xDC00 <= trailing_code <= 0xDFFF:
return EscapeSequence(
(chr(code) + chr(trailing_code))
.encode("utf-16", "surrogatepass")
.decode("utf-16"),
12,
)
raise GraphQLSyntaxError(
self.source,
position,
f"Invalid Unicode escape sequence: '{body[position: position + 6]}'.",
)
def read_escaped_character(self, position: int) -> EscapeSequence:
body = self.source.body
value = _ESCAPED_CHARS.get(body[position + 1])
if value:
return EscapeSequence(value, 2)
raise GraphQLSyntaxError(
self.source,
position,
f"Invalid character escape sequence: '{body[position: position + 2]}'.",
)
def read_block_string(self, start: int) -> Token:
"""Read a block string token from the source file."""
body = self.source.body
body_length = len(body)
line_start = self.line_start
position = start + 3
chunk_start = position
current_line = ""
block_lines = []
while position < body_length:
char = body[position]
if char == '"' and body[position + 1 : position + 3] == '""':
current_line += body[chunk_start:position]
block_lines.append(current_line)
token = self.create_token(
TokenKind.BLOCK_STRING,
start,
position + 3,
# return a string of the lines joined with new lines
"\n".join(dedent_block_string_lines(block_lines)),
)
self.line += len(block_lines) - 1
self.line_start = line_start
return token
if char == "\\" and body[position + 1 : position + 4] == '"""':
current_line += body[chunk_start:position]
chunk_start = position + 1 # skip only slash
position += 4
continue
if char in "\r\n":
current_line += body[chunk_start:position]
block_lines.append(current_line)
if char == "\r" and body[position + 1 : position + 2] == "\n":
position += 2
else:
position += 1
current_line = ""
chunk_start = line_start = position
continue
if is_unicode_scalar_value(char):
position += 1
elif is_supplementary_code_point(body, position):
position += 2
else:
raise GraphQLSyntaxError(
self.source,
position,
"Invalid character within String:"
f" {self.print_code_point_at(position)}.",
)
raise GraphQLSyntaxError(self.source, position, "Unterminated string.")
def read_name(self, start: int) -> Token:
"""Read an alphanumeric + underscore name from the source."""
body = self.source.body
body_length = len(body)
position = start + 1
while position < body_length:
char = body[position]
if not is_name_continue(char):
break
position += 1
return self.create_token(TokenKind.NAME, start, position, body[start:position])
_punctuator_token_kinds = frozenset(
[
TokenKind.BANG,
TokenKind.DOLLAR,
TokenKind.AMP,
TokenKind.PAREN_L,
TokenKind.PAREN_R,
TokenKind.SPREAD,
TokenKind.COLON,
TokenKind.EQUALS,
TokenKind.AT,
TokenKind.BRACKET_L,
TokenKind.BRACKET_R,
TokenKind.BRACE_L,
TokenKind.PIPE,
TokenKind.BRACE_R,
]
)
def is_punctuator_token_kind(kind: TokenKind) -> bool:
"""Check whether the given token kind corresponds to a punctuator.
For internal use only.
"""
return kind in _punctuator_token_kinds
_KIND_FOR_PUNCT = {
"!": TokenKind.BANG,
"$": TokenKind.DOLLAR,
"&": TokenKind.AMP,
"(": TokenKind.PAREN_L,
")": TokenKind.PAREN_R,
":": TokenKind.COLON,
"=": TokenKind.EQUALS,
"@": TokenKind.AT,
"[": TokenKind.BRACKET_L,
"]": TokenKind.BRACKET_R,
"{": TokenKind.BRACE_L,
"}": TokenKind.BRACE_R,
"|": TokenKind.PIPE,
}
_ESCAPED_CHARS = {
'"': '"',
"/": "/",
"\\": "\\",
"b": "\b",
"f": "\f",
"n": "\n",
"r": "\r",
"t": "\t",
}
def read_16_bit_hex_code(body: str, position: int) -> int:
"""Read a 16bit hexadecimal string and return its positive integer value (0-65535).
Reads four hexadecimal characters and returns the positive integer that 16bit
hexadecimal string represents. For example, "000f" will return 15, and "dead"
will return 57005.
Returns a negative number if any char was not a valid hexadecimal digit.
"""
# read_hex_digit() returns -1 on error. ORing a negative value with any other
# value always produces a negative value.
return (
read_hex_digit(body[position]) << 12
| read_hex_digit(body[position + 1]) << 8
| read_hex_digit(body[position + 2]) << 4
| read_hex_digit(body[position + 3])
)
def read_hex_digit(char: str) -> int:
"""Read a hexadecimal character and returns its positive integer value (0-15).
'0' becomes 0, '9' becomes 9
'A' becomes 10, 'F' becomes 15
'a' becomes 10, 'f' becomes 15
Returns -1 if the provided character code was not a valid hexadecimal digit.
"""
if "0" <= char <= "9":
return ord(char) - 48
elif "A" <= char <= "F":
return ord(char) - 55
elif "a" <= char <= "f":
return ord(char) - 87
return -1
def is_unicode_scalar_value(char: str) -> bool:
"""Check whether this is a Unicode scalar value.
A Unicode scalar value is any Unicode code point except surrogate code
points. In other words, the inclusive ranges of values 0x0000 to 0xD7FF and
0xE000 to 0x10FFFF.
"""
return "\x00" <= char <= "\ud7ff" or "\ue000" <= char <= "\U0010ffff"
def is_supplementary_code_point(body: str, location: int) -> bool:
"""
Check whether the current location is a supplementary code point.
The GraphQL specification defines source text as a sequence of unicode scalar
values (which Unicode defines to exclude surrogate code points).
"""
try:
return (
"\ud800" <= body[location] <= "\udbff"
and "\udc00" <= body[location + 1] <= "\udfff"
)
except IndexError:
return False

View File

@@ -0,0 +1,46 @@
from typing import Any, NamedTuple, TYPE_CHECKING
try:
from typing import TypedDict
except ImportError: # Python < 3.8
from typing_extensions import TypedDict
if TYPE_CHECKING:
from .source import Source # noqa: F401
__all__ = ["get_location", "SourceLocation", "FormattedSourceLocation"]
class FormattedSourceLocation(TypedDict):
"""Formatted source location"""
line: int
column: int
class SourceLocation(NamedTuple):
"""Represents a location in a Source."""
line: int
column: int
@property
def formatted(self) -> FormattedSourceLocation:
return dict(line=self.line, column=self.column)
def __eq__(self, other: Any) -> bool:
if isinstance(other, dict):
return self.formatted == other
return tuple(self) == other
def __ne__(self, other: Any) -> bool:
return not self == other
def get_location(source: "Source", position: int) -> SourceLocation:
"""Get the line and column for a character position in the source.
Takes a Source and a UTF-8 character offset, and returns the corresponding line and
column as a SourceLocation.
"""
return source.get_location(position)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
from .ast import (
Node,
DefinitionNode,
ExecutableDefinitionNode,
ListValueNode,
ObjectValueNode,
SchemaExtensionNode,
SelectionNode,
TypeDefinitionNode,
TypeExtensionNode,
TypeNode,
TypeSystemDefinitionNode,
ValueNode,
VariableNode,
)
__all__ = [
"is_definition_node",
"is_executable_definition_node",
"is_selection_node",
"is_value_node",
"is_const_value_node",
"is_type_node",
"is_type_system_definition_node",
"is_type_definition_node",
"is_type_system_extension_node",
"is_type_extension_node",
]
def is_definition_node(node: Node) -> bool:
"""Check whether the given node represents a definition."""
return isinstance(node, DefinitionNode)
def is_executable_definition_node(node: Node) -> bool:
"""Check whether the given node represents an executable definition."""
return isinstance(node, ExecutableDefinitionNode)
def is_selection_node(node: Node) -> bool:
"""Check whether the given node represents a selection."""
return isinstance(node, SelectionNode)
def is_value_node(node: Node) -> bool:
"""Check whether the given node represents a value."""
return isinstance(node, ValueNode)
def is_const_value_node(node: Node) -> bool:
"""Check whether the given node represents a constant value."""
return is_value_node(node) and (
any(is_const_value_node(value) for value in node.values)
if isinstance(node, ListValueNode)
else (
any(is_const_value_node(field.value) for field in node.fields)
if isinstance(node, ObjectValueNode)
else not isinstance(node, VariableNode)
)
)
def is_type_node(node: Node) -> bool:
"""Check whether the given node represents a type."""
return isinstance(node, TypeNode)
def is_type_system_definition_node(node: Node) -> bool:
"""Check whether the given node represents a type system definition."""
return isinstance(node, TypeSystemDefinitionNode)
def is_type_definition_node(node: Node) -> bool:
"""Check whether the given node represents a type definition."""
return isinstance(node, TypeDefinitionNode)
def is_type_system_extension_node(node: Node) -> bool:
"""Check whether the given node represents a type system extension."""
return isinstance(node, (SchemaExtensionNode, TypeExtensionNode))
def is_type_extension_node(node: Node) -> bool:
"""Check whether the given node represents a type extension."""
return isinstance(node, TypeExtensionNode)

View File

@@ -0,0 +1,79 @@
import re
from typing import Optional, Tuple, cast
from .ast import Location
from .location import SourceLocation, get_location
from .source import Source
__all__ = ["print_location", "print_source_location"]
def print_location(location: Location) -> str:
"""Render a helpful description of the location in the GraphQL Source document."""
return print_source_location(
location.source, get_location(location.source, location.start)
)
_re_newline = re.compile(r"\r\n|[\n\r]")
def print_source_location(source: Source, source_location: SourceLocation) -> str:
"""Render a helpful description of the location in the GraphQL Source document."""
first_line_column_offset = source.location_offset.column - 1
body = "".rjust(first_line_column_offset) + source.body
line_index = source_location.line - 1
line_offset = source.location_offset.line - 1
line_num = source_location.line + line_offset
column_offset = first_line_column_offset if source_location.line == 1 else 0
column_num = source_location.column + column_offset
location_str = f"{source.name}:{line_num}:{column_num}\n"
lines = _re_newline.split(body) # works a bit different from splitlines()
location_line = lines[line_index]
# Special case for minified documents
if len(location_line) > 120:
sub_line_index, sub_line_column_num = divmod(column_num, 80)
sub_lines = [
location_line[i : i + 80] for i in range(0, len(location_line), 80)
]
return location_str + print_prefixed_lines(
(f"{line_num} |", sub_lines[0]),
*[("|", sub_line) for sub_line in sub_lines[1 : sub_line_index + 1]],
("|", "^".rjust(sub_line_column_num)),
(
"|",
(
sub_lines[sub_line_index + 1]
if sub_line_index < len(sub_lines) - 1
else None
),
),
)
return location_str + print_prefixed_lines(
(f"{line_num - 1} |", lines[line_index - 1] if line_index > 0 else None),
(f"{line_num} |", location_line),
("|", "^".rjust(column_num)),
(
f"{line_num + 1} |",
lines[line_index + 1] if line_index < len(lines) - 1 else None,
),
)
def print_prefixed_lines(*lines: Tuple[str, Optional[str]]) -> str:
"""Print lines specified like this: ("prefix", "string")"""
existing_lines = [
cast(Tuple[str, str], line) for line in lines if line[1] is not None
]
pad_len = max(len(line[0]) for line in existing_lines)
return "\n".join(
prefix.rjust(pad_len) + (" " + line if line else "")
for prefix, line in existing_lines
)

View File

@@ -0,0 +1,83 @@
__all__ = ["print_string"]
def print_string(s: str) -> str:
"""Print a string as a GraphQL StringValue literal.
Replaces control characters and excluded characters (" U+0022 and \\ U+005C)
with escape sequences.
"""
if not isinstance(s, str):
s = str(s)
return f'"{s.translate(escape_sequences)}"'
escape_sequences = {
0x00: "\\u0000",
0x01: "\\u0001",
0x02: "\\u0002",
0x03: "\\u0003",
0x04: "\\u0004",
0x05: "\\u0005",
0x06: "\\u0006",
0x07: "\\u0007",
0x08: "\\b",
0x09: "\\t",
0x0A: "\\n",
0x0B: "\\u000B",
0x0C: "\\f",
0x0D: "\\r",
0x0E: "\\u000E",
0x0F: "\\u000F",
0x10: "\\u0010",
0x11: "\\u0011",
0x12: "\\u0012",
0x13: "\\u0013",
0x14: "\\u0014",
0x15: "\\u0015",
0x16: "\\u0016",
0x17: "\\u0017",
0x18: "\\u0018",
0x19: "\\u0019",
0x1A: "\\u001A",
0x1B: "\\u001B",
0x1C: "\\u001C",
0x1D: "\\u001D",
0x1E: "\\u001E",
0x1F: "\\u001F",
0x22: '\\"',
0x5C: "\\\\",
0x7F: "\\u007F",
0x80: "\\u0080",
0x81: "\\u0081",
0x82: "\\u0082",
0x83: "\\u0083",
0x84: "\\u0084",
0x85: "\\u0085",
0x86: "\\u0086",
0x87: "\\u0087",
0x88: "\\u0088",
0x89: "\\u0089",
0x8A: "\\u008A",
0x8B: "\\u008B",
0x8C: "\\u008C",
0x8D: "\\u008D",
0x8E: "\\u008E",
0x8F: "\\u008F",
0x90: "\\u0090",
0x91: "\\u0091",
0x92: "\\u0092",
0x93: "\\u0093",
0x94: "\\u0094",
0x95: "\\u0095",
0x96: "\\u0096",
0x97: "\\u0097",
0x98: "\\u0098",
0x99: "\\u0099",
0x9A: "\\u009A",
0x9B: "\\u009B",
0x9C: "\\u009C",
0x9D: "\\u009D",
0x9E: "\\u009E",
0x9F: "\\u009F",
}

View File

@@ -0,0 +1,428 @@
from typing import Any, Collection, Optional
from ..language.ast import Node, OperationType
from .block_string import print_block_string
from .print_string import print_string
from .visitor import visit, Visitor
__all__ = ["print_ast"]
MAX_LINE_LENGTH = 80
Strings = Collection[str]
class PrintedNode:
"""A union type for all nodes that have been processed by the printer."""
alias: str
arguments: Strings
block: bool
default_value: str
definitions: Strings
description: str
directives: str
fields: Strings
interfaces: Strings
locations: Strings
name: str
operation: OperationType
operation_types: Strings
repeatable: bool
selection_set: str
selections: Strings
type: str
type_condition: str
types: Strings
value: str
values: Strings
variable: str
variable_definitions: Strings
def print_ast(ast: Node) -> str:
"""Convert an AST into a string.
The conversion is done using a set of reasonable formatting rules.
"""
return visit(ast, PrintAstVisitor())
class PrintAstVisitor(Visitor):
@staticmethod
def leave_name(node: PrintedNode, *_args: Any) -> str:
return node.value
@staticmethod
def leave_variable(node: PrintedNode, *_args: Any) -> str:
return f"${node.name}"
# Document
@staticmethod
def leave_document(node: PrintedNode, *_args: Any) -> str:
return join(node.definitions, "\n\n")
@staticmethod
def leave_operation_definition(node: PrintedNode, *_args: Any) -> str:
var_defs = wrap("(", join(node.variable_definitions, ", "), ")")
prefix = join(
(
node.operation.value,
join((node.name, var_defs)),
join(node.directives, " "),
),
" ",
)
# Anonymous queries with no directives or variable definitions can use the
# query short form.
return ("" if prefix == "query" else prefix + " ") + node.selection_set
@staticmethod
def leave_variable_definition(node: PrintedNode, *_args: Any) -> str:
return (
f"{node.variable}: {node.type}"
f"{wrap(' = ', node.default_value)}"
f"{wrap(' ', join(node.directives, ' '))}"
)
@staticmethod
def leave_selection_set(node: PrintedNode, *_args: Any) -> str:
return block(node.selections)
@staticmethod
def leave_field(node: PrintedNode, *_args: Any) -> str:
prefix = wrap("", node.alias, ": ") + node.name
args_line = prefix + wrap("(", join(node.arguments, ", "), ")")
if len(args_line) > MAX_LINE_LENGTH:
args_line = prefix + wrap("(\n", indent(join(node.arguments, "\n")), "\n)")
return join((args_line, join(node.directives, " "), node.selection_set), " ")
@staticmethod
def leave_argument(node: PrintedNode, *_args: Any) -> str:
return f"{node.name}: {node.value}"
# Fragments
@staticmethod
def leave_fragment_spread(node: PrintedNode, *_args: Any) -> str:
return f"...{node.name}{wrap(' ', join(node.directives, ' '))}"
@staticmethod
def leave_inline_fragment(node: PrintedNode, *_args: Any) -> str:
return join(
(
"...",
wrap("on ", node.type_condition),
join(node.directives, " "),
node.selection_set,
),
" ",
)
@staticmethod
def leave_fragment_definition(node: PrintedNode, *_args: Any) -> str:
# Note: fragment variable definitions are deprecated and will be removed in v3.3
return (
f"fragment {node.name}"
f"{wrap('(', join(node.variable_definitions, ', '), ')')}"
f" on {node.type_condition}"
f" {wrap('', join(node.directives, ' '), ' ')}"
f"{node.selection_set}"
)
# Value
@staticmethod
def leave_int_value(node: PrintedNode, *_args: Any) -> str:
return node.value
@staticmethod
def leave_float_value(node: PrintedNode, *_args: Any) -> str:
return node.value
@staticmethod
def leave_string_value(node: PrintedNode, *_args: Any) -> str:
if node.block:
return print_block_string(node.value)
return print_string(node.value)
@staticmethod
def leave_boolean_value(node: PrintedNode, *_args: Any) -> str:
return "true" if node.value else "false"
@staticmethod
def leave_null_value(_node: PrintedNode, *_args: Any) -> str:
return "null"
@staticmethod
def leave_enum_value(node: PrintedNode, *_args: Any) -> str:
return node.value
@staticmethod
def leave_list_value(node: PrintedNode, *_args: Any) -> str:
return f"[{join(node.values, ', ')}]"
@staticmethod
def leave_object_value(node: PrintedNode, *_args: Any) -> str:
return f"{{{join(node.fields, ', ')}}}"
@staticmethod
def leave_object_field(node: PrintedNode, *_args: Any) -> str:
return f"{node.name}: {node.value}"
# Directive
@staticmethod
def leave_directive(node: PrintedNode, *_args: Any) -> str:
return f"@{node.name}{wrap('(', join(node.arguments, ', '), ')')}"
# Type
@staticmethod
def leave_named_type(node: PrintedNode, *_args: Any) -> str:
return node.name
@staticmethod
def leave_list_type(node: PrintedNode, *_args: Any) -> str:
return f"[{node.type}]"
@staticmethod
def leave_non_null_type(node: PrintedNode, *_args: Any) -> str:
return f"{node.type}!"
# Type System Definitions
@staticmethod
def leave_schema_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
(
"schema",
join(node.directives, " "),
block(node.operation_types),
),
" ",
)
@staticmethod
def leave_operation_type_definition(node: PrintedNode, *_args: Any) -> str:
return f"{node.operation.value}: {node.type}"
@staticmethod
def leave_scalar_type_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
(
"scalar",
node.name,
join(node.directives, " "),
),
" ",
)
@staticmethod
def leave_object_type_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
(
"type",
node.name,
wrap("implements ", join(node.interfaces, " & ")),
join(node.directives, " "),
block(node.fields),
),
" ",
)
@staticmethod
def leave_field_definition(node: PrintedNode, *_args: Any) -> str:
args = node.arguments
args = (
wrap("(\n", indent(join(args, "\n")), "\n)")
if has_multiline_items(args)
else wrap("(", join(args, ", "), ")")
)
directives = wrap(" ", join(node.directives, " "))
return (
wrap("", node.description, "\n")
+ f"{node.name}{args}: {node.type}{directives}"
)
@staticmethod
def leave_input_value_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
(
f"{node.name}: {node.type}",
wrap("= ", node.default_value),
join(node.directives, " "),
),
" ",
)
@staticmethod
def leave_interface_type_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
(
"interface",
node.name,
wrap("implements ", join(node.interfaces, " & ")),
join(node.directives, " "),
block(node.fields),
),
" ",
)
@staticmethod
def leave_union_type_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
(
"union",
node.name,
join(node.directives, " "),
wrap("= ", join(node.types, " | ")),
),
" ",
)
@staticmethod
def leave_enum_type_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
("enum", node.name, join(node.directives, " "), block(node.values)), " "
)
@staticmethod
def leave_enum_value_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
(node.name, join(node.directives, " ")), " "
)
@staticmethod
def leave_input_object_type_definition(node: PrintedNode, *_args: Any) -> str:
return wrap("", node.description, "\n") + join(
("input", node.name, join(node.directives, " "), block(node.fields)), " "
)
@staticmethod
def leave_directive_definition(node: PrintedNode, *_args: Any) -> str:
args = node.arguments
args = (
wrap("(\n", indent(join(args, "\n")), "\n)")
if has_multiline_items(args)
else wrap("(", join(args, ", "), ")")
)
repeatable = " repeatable" if node.repeatable else ""
locations = join(node.locations, " | ")
return (
wrap("", node.description, "\n")
+ f"directive @{node.name}{args}{repeatable} on {locations}"
)
@staticmethod
def leave_schema_extension(node: PrintedNode, *_args: Any) -> str:
return join(
("extend schema", join(node.directives, " "), block(node.operation_types)),
" ",
)
@staticmethod
def leave_scalar_type_extension(node: PrintedNode, *_args: Any) -> str:
return join(("extend scalar", node.name, join(node.directives, " ")), " ")
@staticmethod
def leave_object_type_extension(node: PrintedNode, *_args: Any) -> str:
return join(
(
"extend type",
node.name,
wrap("implements ", join(node.interfaces, " & ")),
join(node.directives, " "),
block(node.fields),
),
" ",
)
@staticmethod
def leave_interface_type_extension(node: PrintedNode, *_args: Any) -> str:
return join(
(
"extend interface",
node.name,
wrap("implements ", join(node.interfaces, " & ")),
join(node.directives, " "),
block(node.fields),
),
" ",
)
@staticmethod
def leave_union_type_extension(node: PrintedNode, *_args: Any) -> str:
return join(
(
"extend union",
node.name,
join(node.directives, " "),
wrap("= ", join(node.types, " | ")),
),
" ",
)
@staticmethod
def leave_enum_type_extension(node: PrintedNode, *_args: Any) -> str:
return join(
("extend enum", node.name, join(node.directives, " "), block(node.values)),
" ",
)
@staticmethod
def leave_input_object_type_extension(node: PrintedNode, *_args: Any) -> str:
return join(
("extend input", node.name, join(node.directives, " "), block(node.fields)),
" ",
)
def join(strings: Optional[Strings], separator: str = "") -> str:
"""Join strings in a given collection.
Return an empty string if it is None or empty, otherwise join all items together
separated by separator if provided.
"""
return separator.join(s for s in strings if s) if strings else ""
def block(strings: Optional[Strings]) -> str:
"""Return strings inside a block.
Given a collection of strings, return a string with each item on its own line,
wrapped in an indented "{ }" block.
"""
return wrap("{\n", indent(join(strings, "\n")), "\n}")
def wrap(start: str, string: Optional[str], end: str = "") -> str:
"""Wrap string inside other strings at start and end.
If the string is not None or empty, then wrap with start and end, otherwise return
an empty string.
"""
return f"{start}{string}{end}" if string else ""
def indent(string: str) -> str:
"""Indent string with two spaces.
If the string is not None or empty, add two spaces at the beginning of every line
inside the string.
"""
return wrap(" ", string.replace("\n", "\n "))
def is_multiline(string: str) -> bool:
"""Check whether a string consists of multiple lines."""
return "\n" in string
def has_multiline_items(strings: Optional[Strings]) -> bool:
"""Check whether one of the items in the list has multiple lines."""
return any(is_multiline(item) for item in strings) if strings else False

View File

@@ -0,0 +1,70 @@
from typing import Any
from .location import SourceLocation
__all__ = ["Source", "is_source"]
class Source:
"""A representation of source input to GraphQL."""
# allow custom attributes and weak references (not used internally)
__slots__ = "__weakref__", "__dict__", "body", "name", "location_offset"
def __init__(
self,
body: str,
name: str = "GraphQL request",
location_offset: SourceLocation = SourceLocation(1, 1),
) -> None:
"""Initialize source input.
The ``name`` and ``location_offset`` parameters are optional, but they are
useful for clients who store GraphQL documents in source files. For example,
if the GraphQL input starts at line 40 in a file named ``Foo.graphql``, it might
be useful for ``name`` to be ``"Foo.graphql"`` and location to be ``(40, 0)``.
The ``line`` and ``column`` attributes in ``location_offset`` are 1-indexed.
"""
self.body = body
self.name = name
if not isinstance(location_offset, SourceLocation):
location_offset = SourceLocation._make(location_offset)
if location_offset.line <= 0:
raise ValueError(
"line in location_offset is 1-indexed and must be positive."
)
if location_offset.column <= 0:
raise ValueError(
"column in location_offset is 1-indexed and must be positive."
)
self.location_offset = location_offset
def get_location(self, position: int) -> SourceLocation:
lines = self.body[:position].splitlines()
if lines:
line = len(lines)
column = len(lines[-1]) + 1
else:
line = 1
column = 1
return SourceLocation(line, column)
def __repr__(self) -> str:
return f"<{self.__class__.__name__} name={self.name!r}>"
def __eq__(self, other: Any) -> bool:
return (isinstance(other, Source) and other.body == self.body) or (
isinstance(other, str) and other == self.body
)
def __ne__(self, other: Any) -> bool:
return not self == other
def is_source(source: Any) -> bool:
"""Test if the given value is a Source object.
For internal use only.
"""
return isinstance(source, Source)

View File

@@ -0,0 +1,30 @@
from enum import Enum
__all__ = ["TokenKind"]
class TokenKind(Enum):
"""The different kinds of tokens that the lexer emits"""
SOF = "<SOF>"
EOF = "<EOF>"
BANG = "!"
DOLLAR = "$"
AMP = "&"
PAREN_L = "("
PAREN_R = ")"
SPREAD = "..."
COLON = ":"
EQUALS = "="
AT = "@"
BRACKET_L = "["
BRACKET_R = "]"
BRACE_L = "{"
PIPE = "|"
BRACE_R = "}"
NAME = "Name"
INT = "Int"
FLOAT = "Float"
STRING = "String"
BLOCK_STRING = "BlockString"
COMMENT = "Comment"

View File

@@ -0,0 +1,375 @@
from copy import copy
from enum import Enum
from typing import (
Any,
Callable,
Collection,
Dict,
List,
NamedTuple,
Optional,
Tuple,
Union,
)
from ..pyutils import inspect, snake_to_camel
from . import ast
from .ast import QUERY_DOCUMENT_KEYS, Node
__all__ = [
"Visitor",
"ParallelVisitor",
"VisitorAction",
"visit",
"BREAK",
"SKIP",
"REMOVE",
"IDLE",
]
class VisitorActionEnum(Enum):
"""Special return values for the visitor methods.
You can also use the values of this enum directly.
"""
BREAK = True
SKIP = False
REMOVE = Ellipsis
VisitorAction = Optional[VisitorActionEnum]
# Note that in GraphQL.js these are defined differently:
# BREAK = {}, SKIP = false, REMOVE = null, IDLE = undefined
BREAK = VisitorActionEnum.BREAK
SKIP = VisitorActionEnum.SKIP
REMOVE = VisitorActionEnum.REMOVE
IDLE = None
VisitorKeyMap = Dict[str, Tuple[str, ...]]
class EnterLeaveVisitor(NamedTuple):
"""Visitor with functions for entering and leaving."""
enter: Optional[Callable[..., Optional[VisitorAction]]]
leave: Optional[Callable[..., Optional[VisitorAction]]]
class Visitor:
"""Visitor that walks through an AST.
Visitors can define two generic methods "enter" and "leave". The former will be
called when a node is entered in the traversal, the latter is called after visiting
the node and its child nodes. These methods have the following signature::
def enter(self, node, key, parent, path, ancestors):
# The return value has the following meaning:
# IDLE (None): no action
# SKIP: skip visiting this node
# BREAK: stop visiting altogether
# REMOVE: delete this node
# any other value: replace this node with the returned value
return
def leave(self, node, key, parent, path, ancestors):
# The return value has the following meaning:
# IDLE (None) or SKIP: no action
# BREAK: stop visiting altogether
# REMOVE: delete this node
# any other value: replace this node with the returned value
return
The parameters have the following meaning:
:arg node: The current node being visiting.
:arg key: The index or key to this node from the parent node or Array.
:arg parent: the parent immediately above this node, which may be an Array.
:arg path: The key path to get to this node from the root node.
:arg ancestors: All nodes and Arrays visited before reaching parent
of this node. These correspond to array indices in ``path``.
Note: ancestors includes arrays which contain the parent of visited node.
You can also define node kind specific methods by suffixing them with an underscore
followed by the kind of the node to be visited. For instance, to visit ``field``
nodes, you would defined the methods ``enter_field()`` and/or ``leave_field()``,
with the same signature as above. If no kind specific method has been defined
for a given node, the generic method is called.
"""
# Provide special return values as attributes
BREAK, SKIP, REMOVE, IDLE = BREAK, SKIP, REMOVE, IDLE
enter_leave_map: Dict[str, EnterLeaveVisitor]
def __init_subclass__(cls) -> None:
"""Verify that all defined handlers are valid."""
super().__init_subclass__()
for attr, val in cls.__dict__.items():
if attr.startswith("_"):
continue
attr_kind = attr.split("_", 1)
if len(attr_kind) < 2:
kind: Optional[str] = None
else:
attr, kind = attr_kind
if attr in ("enter", "leave") and kind:
name = snake_to_camel(kind) + "Node"
node_cls = getattr(ast, name, None)
if (
not node_cls
or not isinstance(node_cls, type)
or not issubclass(node_cls, Node)
):
raise TypeError(f"Invalid AST node kind: {kind}.")
def __init__(self) -> None:
self.enter_leave_map = {}
def get_enter_leave_for_kind(self, kind: str) -> EnterLeaveVisitor:
"""Given a node kind, return the EnterLeaveVisitor for that kind."""
try:
return self.enter_leave_map[kind]
except KeyError:
enter_fn = getattr(self, f"enter_{kind}", None)
if not enter_fn:
enter_fn = getattr(self, "enter", None)
leave_fn = getattr(self, f"leave_{kind}", None)
if not leave_fn:
leave_fn = getattr(self, "leave", None)
enter_leave = EnterLeaveVisitor(enter_fn, leave_fn)
self.enter_leave_map[kind] = enter_leave
return enter_leave
def get_visit_fn(
self, kind: str, is_leaving: bool = False
) -> Optional[Callable[..., Optional[VisitorAction]]]:
"""Get the visit function for the given node kind and direction.
.. deprecated:: 3.2
Please use ``get_enter_leave_for_kind`` instead. Will be removed in v3.3.
"""
enter_leave = self.get_enter_leave_for_kind(kind)
return enter_leave.leave if is_leaving else enter_leave.enter
class Stack(NamedTuple):
"""A stack for the visit function."""
in_array: bool
idx: int
keys: Tuple[Node, ...]
edits: List[Tuple[Union[int, str], Node]]
prev: Any # 'Stack' (python/mypy/issues/731)
def visit(
root: Node, visitor: Visitor, visitor_keys: Optional[VisitorKeyMap] = None
) -> Any:
"""Visit each node in an AST.
:func:`~.visit` will walk through an AST using a depth-first traversal, calling the
visitor's enter methods at each node in the traversal, and calling the leave methods
after visiting that node and all of its child nodes.
By returning different values from the enter and leave methods, the behavior of the
visitor can be altered, including skipping over a sub-tree of the AST (by returning
False), editing the AST by returning a value or None to remove the value, or to stop
the whole traversal by returning :data:`~.BREAK`.
When using :func:`~.visit` to edit an AST, the original AST will not be modified,
and a new version of the AST with the changes applied will be returned from the
visit function.
To customize the node attributes to be used for traversal, you can provide a
dictionary visitor_keys mapping node kinds to node attributes.
"""
if not isinstance(root, Node):
raise TypeError(f"Not an AST Node: {inspect(root)}.")
if not isinstance(visitor, Visitor):
raise TypeError(f"Not an AST Visitor: {inspect(visitor)}.")
if visitor_keys is None:
visitor_keys = QUERY_DOCUMENT_KEYS
stack: Any = None
in_array = False
keys: Tuple[Node, ...] = (root,)
idx = -1
edits: List[Any] = []
node: Any = root
key: Any = None
parent: Any = None
path: List[Any] = []
path_append = path.append
path_pop = path.pop
ancestors: List[Any] = []
ancestors_append = ancestors.append
ancestors_pop = ancestors.pop
while True:
idx += 1
is_leaving = idx == len(keys)
is_edited = is_leaving and edits
if is_leaving:
key = path[-1] if ancestors else None
node = parent
parent = ancestors_pop() if ancestors else None
if is_edited:
if in_array:
node = list(node)
edit_offset = 0
for edit_key, edit_value in edits:
array_key = edit_key - edit_offset
if edit_value is REMOVE or edit_value is Ellipsis:
node.pop(array_key)
edit_offset += 1
else:
node[array_key] = edit_value
node = tuple(node)
else:
node = copy(node)
for edit_key, edit_value in edits:
setattr(node, edit_key, edit_value)
idx = stack.idx
keys = stack.keys
edits = stack.edits
in_array = stack.in_array
stack = stack.prev
elif parent:
if in_array:
key = idx
node = parent[key]
else:
key = keys[idx]
node = getattr(parent, key, None)
if node is None:
continue
path_append(key)
if isinstance(node, tuple):
result = None
else:
if not isinstance(node, Node):
raise TypeError(f"Invalid AST Node: {inspect(node)}.")
enter_leave = visitor.get_enter_leave_for_kind(node.kind)
visit_fn = enter_leave.leave if is_leaving else enter_leave.enter
if visit_fn:
result = visit_fn(node, key, parent, path, ancestors)
if result is BREAK or result is True:
break
if result is SKIP or result is False:
if not is_leaving:
path_pop()
continue
elif result is not None:
edits.append((key, result))
if not is_leaving:
if isinstance(result, Node):
node = result
else:
path_pop()
continue
else:
result = None
if result is None and is_edited:
edits.append((key, node))
if is_leaving:
if path:
path_pop()
else:
stack = Stack(in_array, idx, keys, edits, stack)
in_array = isinstance(node, tuple)
keys = node if in_array else visitor_keys.get(node.kind, ()) # type: ignore
idx = -1
edits = []
if parent:
ancestors_append(parent)
parent = node
if not stack:
break
if edits:
return edits[-1][1]
return root
class ParallelVisitor(Visitor):
"""A Visitor which delegates to many visitors to run in parallel.
Each visitor will be visited for each node before moving on.
If a prior visitor edits a node, no following visitors will see that node.
"""
def __init__(self, visitors: Collection[Visitor]):
"""Create a new visitor from the given list of parallel visitors."""
super().__init__()
self.visitors = visitors
self.skipping: List[Any] = [None] * len(visitors)
def get_enter_leave_for_kind(self, kind: str) -> EnterLeaveVisitor:
"""Given a node kind, return the EnterLeaveVisitor for that kind."""
try:
return self.enter_leave_map[kind]
except KeyError:
has_visitor = False
enter_list: List[Optional[Callable[..., Optional[VisitorAction]]]] = []
leave_list: List[Optional[Callable[..., Optional[VisitorAction]]]] = []
for visitor in self.visitors:
enter, leave = visitor.get_enter_leave_for_kind(kind)
if not has_visitor and (enter or leave):
has_visitor = True
enter_list.append(enter)
leave_list.append(leave)
if has_visitor:
def enter(node: Node, *args: Any) -> Optional[VisitorAction]:
skipping = self.skipping
for i, fn in enumerate(enter_list):
if not skipping[i]:
if fn:
result = fn(node, *args)
if result is SKIP or result is False:
skipping[i] = node
elif result is BREAK or result is True:
skipping[i] = BREAK
elif result is not None:
return result
return None
def leave(node: Node, *args: Any) -> Optional[VisitorAction]:
skipping = self.skipping
for i, fn in enumerate(leave_list):
if not skipping[i]:
if fn:
result = fn(node, *args)
if result is BREAK or result is True:
skipping[i] = BREAK
elif (
result is not None
and result is not SKIP
and result is not False
):
return result
elif skipping[i] is node:
skipping[i] = None
return None
else:
enter = leave = None
enter_leave = EnterLeaveVisitor(enter, leave)
self.enter_leave_map[kind] = enter_leave
return enter_leave

View File

@@ -0,0 +1 @@
# Marker file for PEP 561. The graphql package uses inline types.

View File

@@ -0,0 +1,65 @@
"""Python Utils
This package contains dependency-free Python utility functions used throughout the
codebase.
Each utility should belong in its own file and be the default export.
These functions are not part of the module interface and are subject to change.
"""
from .convert_case import camel_to_snake, snake_to_camel
from .cached_property import cached_property
from .description import (
Description,
is_description,
register_description,
unregister_description,
)
from .did_you_mean import did_you_mean
from .group_by import group_by
from .identity_func import identity_func
from .inspect import inspect
from .is_awaitable import is_awaitable
from .is_iterable import is_collection, is_iterable
from .natural_compare import natural_comparison_key
from .awaitable_or_value import AwaitableOrValue
from .suggestion_list import suggestion_list
from .frozen_error import FrozenError
from .frozen_list import FrozenList
from .frozen_dict import FrozenDict
from .merge_kwargs import merge_kwargs
from .path import Path
from .print_path_list import print_path_list
from .simple_pub_sub import SimplePubSub, SimplePubSubIterator
from .undefined import Undefined, UndefinedType
__all__ = [
"camel_to_snake",
"snake_to_camel",
"cached_property",
"did_you_mean",
"Description",
"group_by",
"is_description",
"register_description",
"unregister_description",
"identity_func",
"inspect",
"is_awaitable",
"is_collection",
"is_iterable",
"merge_kwargs",
"natural_comparison_key",
"AwaitableOrValue",
"suggestion_list",
"FrozenError",
"FrozenList",
"FrozenDict",
"Path",
"print_path_list",
"SimplePubSub",
"SimplePubSubIterator",
"Undefined",
"UndefinedType",
]

View File

@@ -0,0 +1,8 @@
from typing import Awaitable, TypeVar, Union
__all__ = ["AwaitableOrValue"]
T = TypeVar("T")
AwaitableOrValue = Union[Awaitable[T], T]

View File

@@ -0,0 +1,35 @@
from typing import Any, Callable, TYPE_CHECKING
if TYPE_CHECKING:
standard_cached_property = None
else:
try:
from functools import cached_property as standard_cached_property
except ImportError: # Python < 3.8
standard_cached_property = None
if standard_cached_property:
cached_property = standard_cached_property
else:
# Code taken from https://github.com/bottlepy/bottle
class CachedProperty:
"""A cached property.
A property that is only computed once per instance and then replaces itself with
an ordinary attribute. Deleting the attribute resets the property.
"""
def __init__(self, func: Callable) -> None:
self.__doc__ = getattr(func, "__doc__")
self.func = func
def __get__(self, obj: object, cls: type) -> Any:
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
cached_property = CachedProperty
__all__ = ["cached_property"]

View File

@@ -0,0 +1,25 @@
# uses code from https://github.com/daveoncode/python-string-utils
import re
__all__ = ["camel_to_snake", "snake_to_camel"]
_re_camel_to_snake = re.compile(r"([a-z]|[A-Z0-9]+)(?=[A-Z])")
_re_snake_to_camel = re.compile(r"(_)([a-z\d])")
def camel_to_snake(s: str) -> str:
"""Convert from CamelCase to snake_case"""
return _re_camel_to_snake.sub(r"\1_", s).lower()
def snake_to_camel(s: str, upper: bool = True) -> str:
"""Convert from snake_case to CamelCase
If upper is set, then convert to upper CamelCase, otherwise the first character
keeps its case.
"""
s = _re_snake_to_camel.sub(lambda m: m.group(2).upper(), s)
if upper:
s = s[:1].upper() + s[1:]
return s

View File

@@ -0,0 +1,59 @@
from typing import Any, Tuple, Union
__all__ = [
"Description",
"is_description",
"register_description",
"unregister_description",
]
class Description:
"""Type checker for human readable descriptions.
By default, only ordinary strings are accepted as descriptions,
but you can register() other classes that will also be allowed,
e.g. to support lazy string objects that are evaluated only at runtime.
If you register(object), any object will be allowed as description.
"""
bases: Union[type, Tuple[type, ...]] = str
@classmethod
def isinstance(cls, obj: Any) -> bool:
return isinstance(obj, cls.bases)
@classmethod
def register(cls, base: type) -> None:
"""Register a class that shall be accepted as a description."""
if not isinstance(base, type):
raise TypeError("Only types can be registered.")
if base is object:
cls.bases = object
elif cls.bases is object:
cls.bases = base
elif not isinstance(cls.bases, tuple):
if base is not cls.bases:
cls.bases = (cls.bases, base)
elif base not in cls.bases:
cls.bases += (base,)
@classmethod
def unregister(cls, base: type) -> None:
"""Unregister a class that shall no more be accepted as a description."""
if not isinstance(base, type):
raise TypeError("Only types can be unregistered.")
if isinstance(cls.bases, tuple):
if base in cls.bases: # pragma: no branch
cls.bases = tuple(b for b in cls.bases if b is not base)
if not cls.bases:
cls.bases = object
elif len(cls.bases) == 1:
cls.bases = cls.bases[0]
elif cls.bases is base:
cls.bases = object
is_description = Description.isinstance
register_description = Description.register
unregister_description = Description.unregister

View File

@@ -0,0 +1,28 @@
from typing import Optional, Sequence
__all__ = ["did_you_mean"]
MAX_LENGTH = 5
def did_you_mean(suggestions: Sequence[str], sub_message: Optional[str] = None) -> str:
"""Given [ A, B, C ] return ' Did you mean A, B, or C?'"""
if not suggestions or not MAX_LENGTH:
return ""
parts = [" Did you mean "]
if sub_message:
parts.extend([sub_message, " "])
suggestions = suggestions[:MAX_LENGTH]
n = len(suggestions)
if n == 1:
parts.append(f"'{suggestions[0]}'?")
elif n == 2:
parts.append(f"'{suggestions[0]}' or '{suggestions[1]}'?")
else:
parts.extend(
[
", ".join(f"'{s}'" for s in suggestions[:-1]),
f", or '{suggestions[-1]}'?",
]
)
return "".join(parts)

View File

@@ -0,0 +1,52 @@
from copy import deepcopy
from typing import Dict, TypeVar
from .frozen_error import FrozenError
__all__ = ["FrozenDict"]
KT = TypeVar("KT")
VT = TypeVar("VT")
class FrozenDict(Dict[KT, VT]):
"""Dictionary that can only be read, but not changed.
.. deprecated:: 3.2
Use dicts and the Mapping type instead. Will be removed in v3.3.
"""
def __delitem__(self, key):
raise FrozenError
def __setitem__(self, key, value):
raise FrozenError
def __iadd__(self, value):
raise FrozenError
def __hash__(self) -> int: # type: ignore
return hash(tuple(self.items()))
def __copy__(self) -> "FrozenDict":
return FrozenDict(self)
copy = __copy__
def __deepcopy__(self, memo: Dict) -> "FrozenDict":
return FrozenDict({k: deepcopy(v, memo) for k, v in self.items()})
def clear(self):
raise FrozenError
def pop(self, key, default=None):
raise FrozenError
def popitem(self):
raise FrozenError
def setdefault(self, key, default=None):
raise FrozenError
def update(self, other=None):
raise FrozenError

View File

@@ -0,0 +1,5 @@
__all__ = ["FrozenError"]
class FrozenError(TypeError):
"""Error when trying to change a frozen (read only) collection."""

View File

@@ -0,0 +1,70 @@
from copy import deepcopy
from typing import Dict, List, TypeVar
from .frozen_error import FrozenError
__all__ = ["FrozenList"]
T = TypeVar("T")
class FrozenList(List[T]):
"""List that can only be read, but not changed.
.. deprecated:: 3.2
Use tuples or lists and the Collection type instead. Will be removed in v3.3.
"""
def __delitem__(self, key):
raise FrozenError
def __setitem__(self, key, value):
raise FrozenError
def __add__(self, value):
if isinstance(value, tuple):
value = list(value)
return list.__add__(self, value)
def __iadd__(self, value):
raise FrozenError
def __mul__(self, value):
return list.__mul__(self, value)
def __imul__(self, value):
raise FrozenError
def __hash__(self) -> int: # type: ignore
return hash(tuple(self))
def __copy__(self) -> "FrozenList":
return FrozenList(self)
def __deepcopy__(self, memo: Dict) -> "FrozenList":
return FrozenList(deepcopy(value, memo) for value in self)
def append(self, x):
raise FrozenError
def extend(self, iterable):
raise FrozenError
def insert(self, i, x):
raise FrozenError
def remove(self, x):
raise FrozenError
def pop(self, i=None):
raise FrozenError
def clear(self):
raise FrozenError
def sort(self, *, key=None, reverse=False):
raise FrozenError
def reverse(self):
raise FrozenError

View File

@@ -0,0 +1,16 @@
from collections import defaultdict
from typing import Callable, Collection, Dict, List, TypeVar
__all__ = ["group_by"]
K = TypeVar("K")
T = TypeVar("T")
def group_by(items: Collection[T], key_fn: Callable[[T], K]) -> Dict[K, List[T]]:
"""Group an unsorted collection of items by a key derived via a function."""
result: Dict[K, List[T]] = defaultdict(list)
for item in items:
key = key_fn(item)
result[key].append(item)
return result

View File

@@ -0,0 +1,13 @@
from typing import cast, Any, TypeVar
from .undefined import Undefined
__all__ = ["identity_func"]
T = TypeVar("T")
def identity_func(x: T = cast(Any, Undefined), *_args: Any) -> T:
"""Return the first received argument."""
return x

View File

@@ -0,0 +1,182 @@
from inspect import (
isclass,
ismethod,
isfunction,
isgeneratorfunction,
isgenerator,
iscoroutinefunction,
iscoroutine,
isasyncgenfunction,
isasyncgen,
)
from typing import Any, List
from .undefined import Undefined
__all__ = ["inspect"]
max_recursive_depth = 2
max_str_size = 240
max_list_size = 10
def inspect(value: Any) -> str:
"""Inspect value and a return string representation for error messages.
Used to print values in error messages. We do not use repr() in order to not
leak too much of the inner Python representation of unknown objects, and we
do not use json.dumps() because not all objects can be serialized as JSON and
we want to output strings with single quotes like Python repr() does it.
We also restrict the size of the representation by truncating strings and
collections and allowing only a maximum recursion depth.
"""
return inspect_recursive(value, [])
def inspect_recursive(value: Any, seen_values: List) -> str:
if value is None or value is Undefined or isinstance(value, (bool, float, complex)):
return repr(value)
if isinstance(value, (int, str, bytes, bytearray)):
return trunc_str(repr(value))
if len(seen_values) < max_recursive_depth and value not in seen_values:
# check if we have a custom inspect method
inspect_method = getattr(value, "__inspect__", None)
if inspect_method is not None and callable(inspect_method):
s = inspect_method()
if isinstance(s, str):
return trunc_str(s)
seen_values = [*seen_values, value]
return inspect_recursive(s, seen_values)
# recursively inspect collections
if isinstance(value, (list, tuple, dict, set, frozenset)):
if not value:
return repr(value)
seen_values = [*seen_values, value]
if isinstance(value, list):
items = value
elif isinstance(value, dict):
items = list(value.items())
else:
items = list(value)
items = trunc_list(items)
if isinstance(value, dict):
s = ", ".join(
(
"..."
if v is ELLIPSIS
else inspect_recursive(v[0], seen_values)
+ ": "
+ inspect_recursive(v[1], seen_values)
)
for v in items
)
else:
s = ", ".join(
"..." if v is ELLIPSIS else inspect_recursive(v, seen_values)
for v in items
)
if isinstance(value, tuple):
if len(items) == 1:
return f"({s},)"
return f"({s})"
if isinstance(value, (dict, set)):
return "{" + s + "}"
if isinstance(value, frozenset):
return f"frozenset({{{s}}})"
return f"[{s}]"
else:
# handle collections that are nested too deep
if isinstance(value, (list, tuple, dict, set, frozenset)):
if not value:
return repr(value)
if isinstance(value, list):
return "[...]"
if isinstance(value, tuple):
return "(...)"
if isinstance(value, dict):
return "{...}"
if isinstance(value, set):
return "set(...)"
return "frozenset(...)"
if isinstance(value, Exception):
type_ = "exception"
value = type(value)
elif isclass(value):
type_ = "exception class" if issubclass(value, Exception) else "class"
elif ismethod(value):
type_ = "method"
elif iscoroutinefunction(value):
type_ = "coroutine function"
elif isasyncgenfunction(value):
type_ = "async generator function"
elif isgeneratorfunction(value):
type_ = "generator function"
elif isfunction(value):
type_ = "function"
elif iscoroutine(value):
type_ = "coroutine"
elif isasyncgen(value):
type_ = "async generator"
elif isgenerator(value):
type_ = "generator"
else:
# stringify (only) the well-known GraphQL types
from ..type import (
GraphQLDirective,
GraphQLNamedType,
GraphQLScalarType,
GraphQLWrappingType,
)
if isinstance(
value,
(
GraphQLDirective,
GraphQLNamedType,
GraphQLScalarType,
GraphQLWrappingType,
),
):
return str(value)
try:
name = type(value).__name__
if not name or "<" in name or ">" in name:
raise AttributeError
except AttributeError:
return "<object>"
else:
return f"<{name} instance>"
try:
name = value.__name__
if not name or "<" in name or ">" in name:
raise AttributeError
except AttributeError:
return f"<{type_}>"
else:
return f"<{type_} {name}>"
def trunc_str(s: str) -> str:
"""Truncate strings to maximum length."""
if len(s) > max_str_size:
i = max(0, (max_str_size - 3) // 2)
j = max(0, max_str_size - 3 - i)
s = s[:i] + "..." + s[-j:]
return s
def trunc_list(s: List) -> List:
"""Truncate lists to maximum length."""
if len(s) > max_list_size:
i = max_list_size // 2
j = i - 1
s = s[:i] + [ELLIPSIS] + s[-j:]
return s
class InspectEllipsisType:
"""Singleton class for indicating ellipses in iterables."""
ELLIPSIS = InspectEllipsisType()

View File

@@ -0,0 +1,24 @@
import inspect
from typing import Any
from types import CoroutineType, GeneratorType
__all__ = ["is_awaitable"]
CO_ITERABLE_COROUTINE = inspect.CO_ITERABLE_COROUTINE
def is_awaitable(value: Any) -> bool:
"""Return true if object can be passed to an ``await`` expression.
Instead of testing if the object is an instance of abc.Awaitable, it checks
the existence of an `__await__` attribute. This is much faster.
"""
return (
# check for coroutine objects
isinstance(value, CoroutineType)
# check for old-style generator based coroutine objects
or isinstance(value, GeneratorType)
and bool(value.gi_code.co_flags & CO_ITERABLE_COROUTINE)
# check for other awaitables (e.g. futures)
or hasattr(value, "__await__")
)

View File

@@ -0,0 +1,24 @@
from collections.abc import Collection, Iterable, Mapping, ValuesView
from typing import Any
__all__ = ["is_collection", "is_iterable"]
collection_types: Any = Collection
if not isinstance({}.values(), Collection): # Python < 3.7.2
collection_types = (Collection, ValuesView)
iterable_types: Any = Iterable
not_iterable_types: Any = (bytes, bytearray, memoryview, str, Mapping)
def is_collection(value: Any) -> bool:
"""Check if value is a collection, but not a string or a mapping."""
return isinstance(value, collection_types) and not isinstance(
value, not_iterable_types
)
def is_iterable(value: Any) -> bool:
"""Check if value is an iterable, but not a string or a mapping."""
return isinstance(value, iterable_types) and not isinstance(
value, not_iterable_types
)

View File

@@ -0,0 +1,8 @@
from typing import cast, Any, Dict, TypeVar
T = TypeVar("T")
def merge_kwargs(base_dict: T, **kwargs: Any) -> T:
"""Return arbitrary typed dictionary with some keyword args merged in."""
return cast(T, {**cast(Dict, base_dict), **kwargs})

View File

@@ -0,0 +1,19 @@
import re
from typing import Tuple
from itertools import cycle
__all__ = ["natural_comparison_key"]
_re_digits = re.compile(r"(\d+)")
def natural_comparison_key(key: str) -> Tuple:
"""Comparison key function for sorting strings by natural sort order.
See: https://en.wikipedia.org/wiki/Natural_sort_order
"""
return tuple(
(int(part), part) if is_digit else part
for part, is_digit in zip(_re_digits.split(key), cycle((False, True)))
)

View File

@@ -0,0 +1,28 @@
from typing import Any, List, NamedTuple, Optional, Union
__all__ = ["Path"]
class Path(NamedTuple):
"""A generic path of string or integer indices"""
prev: Any # Optional['Path'] (python/mypy/issues/731)
"""path with the previous indices"""
key: Union[str, int]
"""current index in the path (string or integer)"""
typename: Optional[str]
"""name of the parent type to avoid path ambiguity"""
def add_key(self, key: Union[str, int], typename: Optional[str] = None) -> "Path":
"""Return a new Path containing the given key."""
return Path(self, key, typename)
def as_list(self) -> List[Union[str, int]]:
"""Return a list of the path keys."""
flattened: List[Union[str, int]] = []
append = flattened.append
curr: Path = self
while curr:
append(curr.key)
curr = curr.prev
return flattened[::-1]

View File

@@ -0,0 +1,6 @@
from typing import Collection, Union
def print_path_list(path: Collection[Union[str, int]]) -> str:
"""Build a string describing the path."""
return "".join(f"[{key}]" if isinstance(key, int) else f".{key}" for key in path)

View File

@@ -0,0 +1,81 @@
from asyncio import Future, Queue, ensure_future, sleep
from inspect import isawaitable
from typing import Any, AsyncIterator, Callable, Optional, Set
try:
from asyncio import get_running_loop
except ImportError:
from asyncio import get_event_loop as get_running_loop # Python < 3.7
__all__ = ["SimplePubSub", "SimplePubSubIterator"]
class SimplePubSub:
"""A very simple publish-subscript system.
Creates an AsyncIterator from an EventEmitter.
Useful for mocking a PubSub system for tests.
"""
subscribers: Set[Callable]
def __init__(self) -> None:
self.subscribers = set()
def emit(self, event: Any) -> bool:
"""Emit an event."""
for subscriber in self.subscribers:
result = subscriber(event)
if isawaitable(result):
ensure_future(result)
return bool(self.subscribers)
def get_subscriber(
self, transform: Optional[Callable] = None
) -> "SimplePubSubIterator":
return SimplePubSubIterator(self, transform)
class SimplePubSubIterator(AsyncIterator):
def __init__(self, pubsub: SimplePubSub, transform: Optional[Callable]) -> None:
self.pubsub = pubsub
self.transform = transform
self.pull_queue: Queue[Future] = Queue()
self.push_queue: Queue[Any] = Queue()
self.listening = True
pubsub.subscribers.add(self.push_value)
def __aiter__(self) -> "SimplePubSubIterator":
return self
async def __anext__(self) -> Any:
if not self.listening:
raise StopAsyncIteration
await sleep(0)
if not self.push_queue.empty():
return await self.push_queue.get()
future = get_running_loop().create_future()
await self.pull_queue.put(future)
return future
async def aclose(self) -> None:
if self.listening:
await self.empty_queue()
async def empty_queue(self) -> None:
self.listening = False
self.pubsub.subscribers.remove(self.push_value)
while not self.pull_queue.empty():
future = await self.pull_queue.get()
future.cancel()
while not self.push_queue.empty():
await self.push_queue.get()
async def push_value(self, event: Any) -> None:
value = event if self.transform is None else self.transform(event)
if self.pull_queue.empty():
await self.push_queue.put(value)
else:
(await self.pull_queue.get()).set_result(value)

View File

@@ -0,0 +1,109 @@
from typing import Collection, Optional, List
from .natural_compare import natural_comparison_key
__all__ = ["suggestion_list"]
def suggestion_list(input_: str, options: Collection[str]) -> List[str]:
"""Get list with suggestions for a given input.
Given an invalid input string and list of valid options, returns a filtered list
of valid options sorted based on their similarity with the input.
"""
options_by_distance = {}
lexical_distance = LexicalDistance(input_)
threshold = int(len(input_) * 0.4) + 1
for option in options:
distance = lexical_distance.measure(option, threshold)
if distance is not None:
options_by_distance[option] = distance
# noinspection PyShadowingNames
return sorted(
options_by_distance,
key=lambda option: (
options_by_distance.get(option, 0),
natural_comparison_key(option),
),
)
class LexicalDistance:
"""Computes the lexical distance between strings A and B.
The "distance" between two strings is given by counting the minimum number of edits
needed to transform string A into string B. An edit can be an insertion, deletion,
or substitution of a single character, or a swap of two adjacent characters.
This distance can be useful for detecting typos in input or sorting.
"""
_input: str
_input_lower_case: str
_input_list: List[int]
_rows: List[List[int]]
def __init__(self, input_: str):
self._input = input_
self._input_lower_case = input_.lower()
row_size = len(input_) + 1
self._input_list = list(map(ord, self._input_lower_case))
self._rows = [[0] * row_size, [0] * row_size, [0] * row_size]
def measure(self, option: str, threshold: int) -> Optional[int]:
if self._input == option:
return 0
option_lower_case = option.lower()
# Any case change counts as a single edit
if self._input_lower_case == option_lower_case:
return 1
a, b = list(map(ord, option_lower_case)), self._input_list
a_len, b_len = len(a), len(b)
if a_len < b_len:
a, b = b, a
a_len, b_len = b_len, a_len
if a_len - b_len > threshold:
return None
rows = self._rows
for j in range(b_len + 1):
rows[0][j] = j
for i in range(1, a_len + 1):
up_row = rows[(i - 1) % 3]
current_row = rows[i % 3]
smallest_cell = current_row[0] = i
for j in range(1, b_len + 1):
cost = 0 if a[i - 1] == b[j - 1] else 1
current_cell = min(
up_row[j] + 1, # delete
current_row[j - 1] + 1, # insert
up_row[j - 1] + cost, # substitute
)
if i > 1 and j > 1 and a[i - 1] == b[j - 2] and a[i - 2] == b[j - 1]:
# transposition
double_diagonal_cell = rows[(i - 2) % 3][j - 2]
current_cell = min(current_cell, double_diagonal_cell + 1)
if current_cell < smallest_cell:
smallest_cell = current_cell
current_row[j] = current_cell
# Early exit, since distance can't go smaller than smallest element
# of the previous row.
if smallest_cell > threshold:
return None
distance = rows[a_len % 3][b_len]
return distance if distance <= threshold else None

View File

@@ -0,0 +1,47 @@
import warnings
from typing import Any, Optional
__all__ = ["Undefined", "UndefinedType"]
class UndefinedType(ValueError):
"""Auxiliary class for creating the Undefined singleton."""
_instance: Optional["UndefinedType"] = None
def __new__(cls) -> "UndefinedType":
if cls._instance is None:
cls._instance = super().__new__(cls)
else:
warnings.warn("Redefinition of 'Undefined'", RuntimeWarning, stacklevel=2)
return cls._instance
def __reduce__(self) -> str:
return "Undefined"
def __repr__(self) -> str:
return "Undefined"
__str__ = __repr__
def __hash__(self) -> int:
return hash(UndefinedType)
def __bool__(self) -> bool:
return False
def __eq__(self, other: Any) -> bool:
return other is Undefined
def __ne__(self, other: Any) -> bool:
return not self == other
# Used to indicate undefined or invalid values (like "undefined" in JavaScript):
Undefined = UndefinedType()
Undefined.__doc__ = """Symbol for undefined values
This singleton object is used to describe undefined or invalid values.
It can be used in places where you would use ``undefined`` in GraphQL.js.
"""

Some files were not shown because too many files have changed in this diff Show More