Source code for py_gql.schema.types

# -*- 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
        )


[docs]class InputField(InputValue): """ Member of an :class:`py_gql.schema.InputObjectType`. 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: Field name type_: Field type (must be input type) default_value: Default value description: Field description node: Source node used when building type from the SDL Attributes: name (str): Field name description (Optional[str]): Field 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 field 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 "InputField(%s: %s)" % (self.name, self.type) def __repr__(self) -> str: return "InputField(%s: %s at %d)" % (self.name, self.type, id(self))
[docs]class InputObjectType(NamedType): """ Input Object Type Definition An input object defines a structured collection of fields which can be supplied as a field or directive argument. Args: name: Type name fields: Fields 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.InputObjectTypeDefinition,\ py_gql.lang.ast.InputObjectTypeExtension,\ ]\ ]): Source nodes used when building type from the SDL fields (Sequence[InputField]): Object fields. field_map (Dict[str, InputField]): Object fields as a map. """ def __init__( self, name: str, fields: LazySeq[InputField], description: Optional[str] = None, nodes: Optional[ List[ Union[ _ast.InputObjectTypeDefinition, _ast.InputObjectTypeExtension, ] ] ] = None, ): self.name = name self.description = description self._source_fields = fields self._fields = None # type: Optional[Sequence[InputField]] self.nodes = ( [] if nodes is None else nodes ) # noqa: B950, type: List[Union[_ast.InputObjectTypeDefinition, _ast.InputObjectTypeExtension]] @property def fields(self) -> Sequence[InputField]: if self._fields is None: self._fields = lazy(self._source_fields) or [] return self._fields @fields.setter def fields(self, fields: List[InputField]) -> None: self._fields = self._source_fields = fields @property def field_map(self) -> Dict[str, InputField]: return {f.name: f for f in self.fields}
_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)