# -*- coding: utf-8 -*-
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from .._utils import Lazy, lazy
from ..exc import ScalarParsingError, ScalarSerializationError, UnknownEnumValue
from ..lang import ast as _ast
from ..lang.parser import (
DIRECTIVE_LOCATIONS,
RUNTIME_DIRECTIVE_LOCATIONS,
SCHEMA_DIRECTIVE_LOCATONS,
)
from ._types import GraphQLType, ListType, NamedType, NonNullType # noqa: F401
if TYPE_CHECKING:
from ..execution.wrappers import ResolveInfo # noqa: F401
# TODO: Encode as much of the rules for validate_schema in the type system such
# as making sure only output types can be used in `Field`. This most likely
# requires making some types generic, such as `NonNullType`.
# TODO: Multiple objects expose both a X and X_map method which could be
# standardised under either of these.
T = TypeVar("T")
LazySeq = Lazy[Sequence[T]]
# NOTE: Resolver supports keyword arguments for field arguments but we can't
# express this without mypy_extensions.
Resolver = Callable[[Any, Any, "ResolveInfo"], Any]
TypeResolver = Callable[[Any, Any, "ResolveInfo"], "Union[ObjectType, str]"]
_UNSET = object()
class GraphQLAbstractType(NamedType):
"""
Abstract types.
These types may describe the parent context of a selection set and indicate
that the value will be of a concrete ObjectType.
"""
resolve_type = NotImplemented # type: Optional[TypeResolver]
class GraphQLLeafType(NamedType):
"""
Lead types.
These types may describe types which may be leaf values.
"""
pass
class GraphQLCompositeType(NamedType):
"""
Composite types.
These types may describe the parent context of a selection set.
"""
_source_fields = NotImplemented # type: LazySeq[Field]
_fields = NotImplemented # type: Optional[Sequence[Field]]
@property
def fields(self) -> Sequence["Field"]:
if self._fields is None:
self._fields = lazy(self._source_fields) or []
return self._fields
@fields.setter
def fields(self, fields: List["Field"]) -> None:
self._fields = self._source_fields = fields
@property
def field_map(self) -> Dict[str, "Field"]:
return {f.name: f for f in self.fields}
class InputValue:
def __init__(
self,
name: str,
type_: Lazy[GraphQLType],
default_value: Any = _UNSET,
description: Optional[str] = None,
node: Optional[_ast.InputValueDefinition] = None,
python_name: Optional[str] = None,
):
self.name = name
self.description = description
self.has_default_value = default_value is not _UNSET
self.node = node
self._default_value = default_value
self._ltype = type_
self._type = None # type: Optional[GraphQLType]
self.python_name = python_name or name
@property
def default_value(self) -> Any:
if self._default_value is _UNSET:
raise AttributeError("No default value")
return self._default_value
@default_value.setter
def default_value(self, value: Any) -> None:
self._default_value = value
self.has_default_value = True
@default_value.deleter
def default_value(self) -> None:
self._default_value = _UNSET
self.has_default_value = False
@property
def type(self) -> GraphQLType:
if self._type is None:
self._type = lazy(self._ltype)
return self._type
@type.setter
def type(self, type_: GraphQLType) -> None:
self._type = self._ltype = type_
@property
def required(self) -> bool:
return (
isinstance(self.type, NonNullType) and self._default_value is _UNSET
)
_EV = TypeVar("_EV", bound="EnumValue")
[docs]class EnumValue:
"""
Enum value definition.
Args:
name: Name of the value
value: Python value.
Defaults to ``name`` if omitted.
Warning:
Must be hashable to support reverse lookups when coercing python
values into enum values
deprecation_reason:
If set, the value will be marked as deprecated and the introspection
layer will expose this to consumers.
description: Enum value description
node: Source node used when building type from the SDL
Attributes:
name (str): Enum value name
value (str): Enum value value
description (Optional[str]): Enum value description
deprecation_reason (Optional[str]):
If set, the value will be marked as deprecated and the introspection
layer will expose this to consumers.
deprecated: ``True`` if :attr:`deprecation_reason` is set.
node (Optional[py_gql.lang.ast.EnumValueDefinition]):
Source node used when building type from the SDL
"""
__slots__ = (
"deprecated",
"deprecation_reason",
"description",
"name",
"node",
"value",
)
[docs] @classmethod
def from_def(
cls: Type[_EV], definition: Union[_EV, str, Tuple[str, Any]]
) -> _EV:
"""
Create an enum value from various source objects.
This supports existing `EnumValue` instances, strings,
``(name, value)`` tuples and dictionaries matching the signature of
`EnumValue.__init__`.
"""
if isinstance(definition, cls):
return definition
elif isinstance(definition, str):
return cls(definition, definition)
elif isinstance(definition, tuple):
name, value = definition
return cls(name, value)
else:
raise TypeError("Invalid enum value definition %s" % definition)
def __init__(
self,
name: str,
value: Any = _UNSET,
deprecation_reason: Optional[str] = None,
description: Optional[str] = None,
node: Optional[_ast.EnumValueDefinition] = None,
):
if name in ("true", "false", "null"):
raise ValueError('Invalid name "%s" for enum value' % name)
self.name = name
self.value = value if value is not _UNSET else name
self.description = description
self.deprecated = deprecation_reason is not None
self.deprecation_reason = deprecation_reason
self.node = node
def __str__(self) -> str:
return self.name
[docs]class EnumType(GraphQLLeafType, NamedType):
"""
Enum Type Definition
Some leaf values of requests and input values are Enums. GraphQL serializes
Enum values as strings, however internally Enums can be represented by any
kind of Python type.
Args:
name: Enum name
values: List of enum value definition supported by `EnumValue.from_def`
description: Enum description
nodes: Source nodes used when building type from the SDL
Attributes:
name (str): Enum name
values (Dict[str, py_gql.schema.EnumValue]): Values by name
reverse_values: (Dict[H, EnumValue]): Values by value
description (Optional[str]): Enum description
nodes (List[Union[\
py_gql.lang.ast.EnumTypeDefinition, \
py_gql.lang.ast.EnumTypeExtension,\
]]):
Source nodes used when building type from the SDL
"""
[docs] @classmethod
def from_python_enum(cls, enum, description=None, nodes=None):
"""
Create a GraphQL Enum type from a Python enum
"""
return cls(
enum.__name__,
[(v.name, v) for v in enum],
description=description,
nodes=nodes,
)
def __init__(
self,
name: str,
values: Iterable[Union[EnumValue, str, Tuple[str, Any]]],
description: Optional[str] = None,
nodes: Optional[
List[Union[_ast.EnumTypeDefinition, _ast.EnumTypeExtension]]
] = None,
):
self.name = name
self.description = description
self.nodes = (
[] if nodes is None else nodes
) # type: List[Union[_ast.EnumTypeDefinition, _ast.EnumTypeExtension]]
self._set_values(values)
def _set_values(
self, values: Iterable[Union[EnumValue, str, Tuple[str, Any]]]
) -> None:
self.values = [] # type: List[EnumValue]
self._values = {} # type: Dict[str, EnumValue]
self._reverse_values = {} # type: Dict[Any, EnumValue]
for v in values:
v = EnumValue.from_def(v)
if v.name in self._values:
raise ValueError("Duplicate enum value %s" % v.name)
self.values.append(v)
self._reverse_values[v.value] = self._values[v.name] = v
[docs] def get_value(self, name: str) -> Any:
"""
Extract the value for a given name.
Args:
name: Name of the value to extract
Returns:
:class:`py_gql.schema.EnumValue`: The corresponding EnumValue.
Raises:
UnknownEnumValue: when the name is unknown.
"""
try:
return self._values[name].value
except KeyError:
raise UnknownEnumValue(
"Invalid name %s for enum %s" % (name, self.name)
)
[docs] def get_name(self, value: Any) -> str:
"""
Extract the name for a given value.
Args:
value: Value of the value to extract, must be hashable
Returns:
:class:`py_gql.schema.EnumValue`: The corresponding EnumValue
Raises:
UnknownEnumValue: when the value is unknown
"""
try:
return self._reverse_values[value].name
except KeyError:
raise UnknownEnumValue(
"Invalid value %r for enum %s" % (value, self.name)
)
_ScalarValueNode = Union[
_ast.IntValue, _ast.FloatValue, _ast.StringValue, _ast.BooleanValue
]
_ScalarValue = Union[str, int, float, bool, None]
[docs]class ScalarType(GraphQLLeafType, NamedType):
"""
Scalar Type Definition
The leaf values of any request and input values to arguments are
Scalars (or Enums) and are defined with a name and a series of functions
used to parse input from ast or variables and to ensure validity.
To define custom scalars, you can either instantiate this class with the
corresponding arguments (that is how `Int`, `Float`, etc. are implemented)
or subclass this class and implement :meth:`serialize`, :meth:`parse` and
:meth:`parse_literal` (this is how `RegexType` is implemented).
Args:
name: Type name
serialize: Type serializer.
This function will receive a Python value and must output JSON
serializable scalars.
Raise :class:`~py_gql.exc.ScalarSerializationError`,
:py:class:`ValueError` or :py:class:`TypeError` to signify that the
value cannot be serialized.
parse: Type de-serializer.
This function will receive JSON scalars and can outputs any Python
value.
Raise :class:`~py_gql.exc.ScalarParsingError`, :py:class:`ValueError`
or :py:class:`TypeError` to signify that the value cannot be parsed.
parse_literal: Type de-serializer for value nodes.
This function receives a :class:`py_gql.lang.ast.Value`
and can outputs any Python value.
Raise :class:`~py_gql.exc.ScalarParsingError`, :py:class:`ValueError`
or :py:class:`TypeError` to signify that the value cannot be parsed.
If not provided, the execution layer will extract the node's value
as a string and pass it to `parse`.
description: Type description
nodes: Source nodes used when building type from the SDL
Attributes:
name (str): Type name
description (Optional[str]): Type description
nodes (List[Union[\
py_gql.lang.ast.ScalarTypeDefinition, \
py_gql.lang.ast.ScalarTypeExtension\
]]):
Source nodes used when building type from the SDL
"""
def __init__(
self,
name: str,
serialize: Callable[[Any], _ScalarValue],
parse: Callable[[_ScalarValue], Any],
parse_literal: Optional[
Callable[[_ScalarValueNode, Mapping[str, Any]], Any]
] = None,
description: Optional[str] = None,
nodes: Optional[
List[Union[_ast.ScalarTypeDefinition, _ast.ScalarTypeExtension]]
] = None,
):
self.name = name
self.description = description
self._serialize = serialize
self._parse = parse
self._parse_literal = parse_literal
self.nodes = (
[] if nodes is None else nodes
) # type: List[Union[_ast.ScalarTypeDefinition, _ast.ScalarTypeExtension]]
[docs] def serialize(self, value: Any) -> _ScalarValue:
"""
Transform a Python value in a JSON serializable one.
Args:
value: Python level value
Returns:
JSON scalar
Raises:
ScalarSerializationError: when the type's serializer fail with
ValueError or TypeError (other exceptions bubble up).
"""
try:
return self._serialize(value)
except (ValueError, TypeError) as err:
raise ScalarSerializationError(str(err)) from err
[docs] def parse(self, value: _ScalarValue) -> Any:
"""
Transform a GraphQL value in a valid Python value
Args:
value: JSON scalar
Returns:
Python level value
Raises:
ScalarParsingError: when the type's parser fail with
ValueError or TypeError (other exceptions bubble up).
"""
try:
return self._parse(value)
except (ValueError, TypeError) as err:
raise ScalarParsingError(str(err)) from err
[docs] def parse_literal(
self,
node: _ScalarValueNode,
variables: Optional[Mapping[str, Any]] = None,
) -> Any:
"""
Transform an AST node in a valid Python value
Args:
node: Parse node
variables: Raw, JSON decoded variables parsed from the request
Returns:
Python level value
Raises:
ScalarParsingError: when the type's parser fail with
ValueError or TypeError (other exceptions bubble up).
"""
try:
try:
if self._parse_literal is not None:
return self._parse_literal(node, variables or {})
return self.parse(node.value)
except AttributeError:
return self.parse(node.value)
except (ValueError, TypeError) as err:
raise ScalarParsingError(str(err), [node]) from err
[docs]class Argument(InputValue):
"""
Argument definition for use in field or directives.
Warning:
As ``None`` is a valid default value, in order to define an argument
without any default value, the ``default_value`` argument **must** be
omitted.
Args:
name: Argument name
type_: Argument type (must be input type)
default_value: Default value
description: Argument description
node: Source node used when building type from the SDL
Attributes:
name (str): Argument name
description (Optional[str]): Argument description
has_default_value (bool): ``True`` if default value is set
default_value (Any):
Default value if it was set. Accessing this attribute raises an
:py:class:`AttributeError` if default value wasn't set.
type (GraphQLType): Value type.
required (bool):
Whether this argument is required (non nullable and does not have
any default value)
node (Optional[py_gql.lang.ast.InputValueDefinition]):
Source node used when building type from the SDL
"""
def __str__(self) -> str:
return "Argument(%s: %s)" % (self.name, self.type)
def __repr__(self) -> str:
return "Argument(%s: %s at %d)" % (self.name, self.type, id(self))
[docs]class Field:
"""
Member of a composite type.
Args:
name: Field name
type_: Field type (must be output type)
args: Field arguments
description: Field description
deprecation_reason:
If set, the field will be marked as deprecated and the introspection
layer will expose this to consumers.
resolver: Resolver function.
node: Source node used when building type from the SDL
Attributes:
name (str): Field name
description (Optional[str]): Field description
deprecation_reason (Optional[str]):
If set, the field will be marked as deprecated and the introspection
layer will expose this to consumers.
deprecated: ``True`` if :attr:`deprecation_reason` is set.
type (GraphQLType): Field type.
arguments (List[py_gql.schema.Argument]): Field arguments.
argument_map (Dict[str, py_gql.schema.Argument]):
Field arguments in indexed form.
resolver (callable): Field resolver.
subscription_resolver (callable): Field resolver for subscriptions.
node (Optional[py_gql.lang.ast.FieldDefinition]):
Source node used when building type from the SDL
"""
def __init__(
self,
name: str,
type_: Lazy[GraphQLType],
args: Optional[LazySeq[Argument]] = None,
description: Optional[str] = None,
deprecation_reason: Optional[str] = None,
resolver: Optional[Callable[..., Any]] = None,
subscription_resolver: Optional[Callable[..., Any]] = None,
node: Optional[_ast.FieldDefinition] = None,
python_name: Optional[str] = None,
):
self.name = name
self.description = description
self.deprecated = bool(deprecation_reason)
self.deprecation_reason = deprecation_reason
self.resolver = resolver
self.subscription_resolver = subscription_resolver
self._source_args = args
self._args = None # type: Optional[Sequence[Argument]]
self.node = node
self._ltype = type_
self._type = None # type: Optional[GraphQLType]
self.python_name = python_name or name
@property
def type(self) -> GraphQLType:
if self._type is None:
self._type = lazy(self._ltype)
return self._type
@type.setter
def type(self, type_: GraphQLType) -> None:
self._type = self._ltype = type_
@property
def arguments(self) -> Sequence[Argument]:
if self._args is None:
self._args = lazy(self._source_args) or []
return self._args
@arguments.setter
def arguments(self, args: List[Argument]) -> None:
self._args = self._source_args = args
@property
def argument_map(self) -> Dict[str, Argument]:
return {arg.name: arg for arg in self.arguments}
def __str__(self) -> str:
return "Field(%s: %s)" % (self.name, self.type)
def __repr__(self) -> str:
return "Field(%s: %s at %d)" % (self.name, self.type, id(self))
[docs]class InterfaceType(GraphQLCompositeType, GraphQLAbstractType, NamedType):
"""
Interface Type Definition.
When a field can return one of a heterogeneous set of types, a Interface
type is used to describe what types are possible, what fields are in
common across all types, as well as a function to determine which type
is actually used when the field is resolved.
Args:
name: Type name
fields: Fields
resolve_type: Type resolver
description: Type description
nodes: Source nodes used when building type from the SDL
Attributes:
name (str): Type name
description (Optional[str]): Type description
fields (Sequence[py_gql.schema.Field]): Object fields.
field_map (Dict[str, py_gql.schema.Field]):
Object fields as a map.
resolve_type (Optional[callable]): Type resolver
nodes (List[Union[\
py_gql.lang.ast.InterfaceTypeDefinition,\
py_gql.lang.ast.InterfaceTypeExtension,\
]]):
Source nodes used when building type from the SDL
"""
def __init__(
self,
name: str,
fields: LazySeq[Field],
resolve_type: Optional[TypeResolver] = None,
description: Optional[str] = None,
nodes: Optional[
List[
Union[_ast.InterfaceTypeDefinition, _ast.InterfaceTypeExtension]
]
] = None,
):
self.name = name
self.description = description
self._source_fields = fields
self._fields = None # type: Optional[Sequence[Field]]
self.nodes = (
[] if nodes is None else nodes
) # noqa: B950, type: List[Union[_ast.InterfaceTypeDefinition, _ast.InterfaceTypeExtension]]
self.resolve_type = resolve_type
[docs]class ObjectType(GraphQLCompositeType, NamedType):
"""
Object Type Definition
Almost all of the GraphQL types you define will be object types. Object
types have a name, but most importantly describe their fields.
Args:
name: Type name
fields: Fields
interfaces: Implemented interfaces
default_resolver:
description: Type description
nodes: Source nodes used when building type from the SDL
Attributes:
name (str): Type name
description (Optional[str]): Type description
interfaces (Sequence[InterfaceType]):
Implemented interfaces.
fields (Sequence[py_gql.schema.Field]): Object fields.
field_map (Dict[str, py_gql.schema.Field]):
Object fields as a map.
default_resolver (Optional[Callable[..., Any]]):
nodes (List[Union[\
py_gql.lang.ast.ObjectTypeDefinition,\
py_gql.lang.ast.ObjectTypeExtension,\
]]):
Source nodes used when building type from the SDL
"""
def __init__(
self,
name: str,
fields: LazySeq[Field],
interfaces: Optional[LazySeq[InterfaceType]] = None,
default_resolver: Optional[Resolver] = None,
description: Optional[str] = None,
nodes: Optional[
List[Union[_ast.ObjectTypeDefinition, _ast.ObjectTypeExtension]]
] = None,
):
self.name = name
self.description = description
self._source_fields = fields
self._source_fields = fields
self.default_resolver = default_resolver
self._fields = None # type: Optional[Sequence[Field]]
self._source_interfaces = interfaces
self._interfaces = None # type: Optional[Sequence[InterfaceType]]
self.nodes = (
[] if nodes is None else nodes
) # type: List[Union[_ast.ObjectTypeDefinition, _ast.ObjectTypeExtension]]
@property
def interfaces(self) -> Sequence[InterfaceType]:
if self._interfaces is None:
self._interfaces = lazy(self._source_interfaces) or []
return self._interfaces
@interfaces.setter
def interfaces(self, interfaces: List[InterfaceType]) -> None:
self._interfaces = self._source_interfaces = interfaces
[docs]class UnionType(GraphQLCompositeType, GraphQLAbstractType, NamedType):
"""
Union Type Definition
When a field can return one of a heterogeneous set of types, a Union type
is used to describe what types are possible as well as providing a function
to determine which type is actually used when the field is resolved.
Args:
name: Type name
types: Member types
resolve_type: Type resolver
description: Type description
nodes: Source nodes used when building type from the SDL
Attributes:
name (str): Type name
description (Optional[str]): Type description
resolve_type (Optional[callable]): Type resolver
types (Sequence[ObjectType]): Member types.
nodes (List[Union[\
py_gql.lang.ast.UnionTypeDefinition,\
py_gql.lang.ast.UnionTypeExtension,\
]]):
Source nodes used when building type from the SDL
"""
def __init__(
self,
name: str,
types: LazySeq[ObjectType],
resolve_type: Optional[TypeResolver] = None,
description: Optional[str] = None,
nodes: Optional[
List[Union[_ast.UnionTypeDefinition, _ast.UnionTypeExtension]]
] = None,
):
self.name = name
self.description = description
self._source_types = types
self._types = None # type: Optional[Sequence[ObjectType]]
self.nodes = (
[] if nodes is None else nodes
) # type: List[Union[_ast.UnionTypeDefinition, _ast.UnionTypeExtension]]
self.resolve_type = resolve_type
@property
def types(self) -> Sequence[ObjectType]:
if self._types is None:
self._types = lazy(self._source_types) or []
return self._types
@types.setter
def types(self, types: List[ObjectType]) -> None:
self._types = self._source_types = types
[docs]class Directive:
"""
Directive definition
`Directives
<https://graphql.github.io/graphql-spec/June2018/#sec-Type-System.Directives>`_
are used by the GraphQL runtime as a way of modifying execution behavior.
Type system creators will usually not create these directly.
Args:
name: Directive name
locations: Possible locations for that directive
args: Argument definitions
description: Directive description
node: Source node used when building type from the SDL.
Attributes:
name (str): Directive name
description (Optional[str]): Directive description
locations (List[str]): Possible locations for that directive
arguments (List[py_gql.schema.Argument]): Directive arguments.
argument_map (Dict[str, py_gql.schema.Argument]):
Directive arguments in indexed form.
node (Optional[py_gql.lang.ast.DirectiveDefinition]):
Source node used when building type from the SDL
"""
ALL_LOCATIONS = DIRECTIVE_LOCATIONS
RUNTIME_LOCATIONS = RUNTIME_DIRECTIVE_LOCATIONS
SCHEMA_LOCATONS = SCHEMA_DIRECTIVE_LOCATONS
def __init__(
self,
name: str,
locations: Sequence[str],
args: Optional[List[Argument]] = None,
description: Optional[str] = None,
node: Optional[_ast.DirectiveDefinition] = None,
):
if not locations:
raise ValueError("Expected at least one location")
if any(l not in DIRECTIVE_LOCATIONS for l in locations):
raise ValueError(
"Locations must be one of %s but received %r"
% (DIRECTIVE_LOCATIONS, locations)
)
self.name = name
self.description = description
self.locations = locations
self.arguments = args if args is not None else []
self.argument_map = {arg.name: arg for arg in self.arguments}
self.node = node
def is_input_type(type_: GraphQLType) -> bool:
"""
Check if a type is an input type.
These types may be used as input types for arguments and directives.
"""
return isinstance(
unwrap_type(type_), (ScalarType, EnumType, InputObjectType)
)
def is_output_type(type_: GraphQLType) -> bool:
"""
Check if a type is an output type.
These types may be used as output types as the result of fields.
"""
return isinstance(
unwrap_type(type_),
(ScalarType, EnumType, ObjectType, InterfaceType, UnionType),
)
def unwrap_type(type_: GraphQLType) -> NamedType:
"""
Recursively extract inner type from a potentially wrapping type like
`ListType` or `NonNullType`.
"""
cur = type_
while isinstance(cur, (ListType, NonNullType)):
cur = cur.type
return cast(NamedType, cur)