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,98 @@
from .pyutils.version import get_version
from .relay import (
BaseGlobalIDType,
ClientIDMutation,
Connection,
ConnectionField,
DefaultGlobalIDType,
GlobalID,
Node,
PageInfo,
SimpleGlobalIDType,
UUIDGlobalIDType,
is_node,
)
from .types import (
ID,
UUID,
Argument,
Base64,
BigInt,
Boolean,
Context,
Date,
DateTime,
Decimal,
Dynamic,
Enum,
Field,
Float,
InputField,
InputObjectType,
Int,
Interface,
JSONString,
List,
Mutation,
NonNull,
ObjectType,
ResolveInfo,
Scalar,
Schema,
String,
Time,
Union,
)
from .utils.module_loading import lazy_import
from .utils.resolve_only_args import resolve_only_args
VERSION = (3, 4, 3, "final", 0)
__version__ = get_version(VERSION)
__all__ = [
"__version__",
"Argument",
"Base64",
"BigInt",
"BaseGlobalIDType",
"Boolean",
"ClientIDMutation",
"Connection",
"ConnectionField",
"Context",
"Date",
"DateTime",
"Decimal",
"DefaultGlobalIDType",
"Dynamic",
"Enum",
"Field",
"Float",
"GlobalID",
"ID",
"InputField",
"InputObjectType",
"Int",
"Interface",
"JSONString",
"List",
"Mutation",
"Node",
"NonNull",
"ObjectType",
"PageInfo",
"ResolveInfo",
"Scalar",
"Schema",
"SimpleGlobalIDType",
"String",
"Time",
"Union",
"UUID",
"UUIDGlobalIDType",
"is_node",
"lazy_import",
"resolve_only_args",
]

View File

@@ -0,0 +1,76 @@
import datetime
import os
import subprocess
def get_version(version=None):
"Returns a PEP 440-compliant version number from VERSION."
version = get_complete_version(version)
# Now build the two parts of the version number:
# main = X.Y[.Z]
# sub = .devN - for pre-alpha releases
# | {a|b|rc}N - for alpha, beta, and rc releases
main = get_main_version(version)
sub = ""
if version[3] == "alpha" and version[4] == 0:
git_changeset = get_git_changeset()
sub = ".dev%s" % git_changeset if git_changeset else ".dev"
elif version[3] != "final":
mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
sub = mapping[version[3]] + str(version[4])
return str(main + sub)
def get_main_version(version=None):
"Returns main version (X.Y[.Z]) from VERSION."
version = get_complete_version(version)
parts = 2 if version[2] == 0 else 3
return ".".join(str(x) for x in version[:parts])
def get_complete_version(version=None):
"""Returns a tuple of the graphene version. If version argument is non-empty,
then checks for correctness of the tuple provided.
"""
if version is None:
from graphene import VERSION as version
else:
assert len(version) == 5
assert version[3] in ("alpha", "beta", "rc", "final")
return version
def get_docs_version(version=None):
version = get_complete_version(version)
if version[3] != "final":
return "dev"
else:
return "%d.%d" % version[:2]
def get_git_changeset():
"""Returns a numeric identifier of the latest git changeset.
The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format.
This value isn't guaranteed to be unique, but collisions are very unlikely,
so it's sufficient for generating the development version numbers.
"""
repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
try:
git_log = subprocess.Popen(
"git log --pretty=format:%ct --quiet -1 HEAD",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
cwd=repo_dir,
universal_newlines=True,
)
timestamp = git_log.communicate()[0]
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
except Exception:
return None
return timestamp.strftime("%Y%m%d%H%M%S")

View File

@@ -0,0 +1,23 @@
from .node import Node, is_node, GlobalID
from .mutation import ClientIDMutation
from .connection import Connection, ConnectionField, PageInfo
from .id_type import (
BaseGlobalIDType,
DefaultGlobalIDType,
SimpleGlobalIDType,
UUIDGlobalIDType,
)
__all__ = [
"BaseGlobalIDType",
"ClientIDMutation",
"Connection",
"ConnectionField",
"DefaultGlobalIDType",
"GlobalID",
"Node",
"PageInfo",
"SimpleGlobalIDType",
"UUIDGlobalIDType",
"is_node",
]

View File

@@ -0,0 +1,200 @@
import re
from collections.abc import Iterable
from functools import partial
from typing import Type
from graphql_relay import connection_from_array
from ..types import Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union
from ..types.field import Field
from ..types.objecttype import ObjectType, ObjectTypeOptions
from ..utils.thenables import maybe_thenable
from .node import is_node, AbstractNode
def get_edge_class(
connection_class: Type["Connection"],
_node: Type[AbstractNode],
base_name: str,
strict_types: bool = False,
):
edge_class = getattr(connection_class, "Edge", None)
class EdgeBase:
node = Field(
NonNull(_node) if strict_types else _node,
description="The item at the end of the edge",
)
cursor = String(required=True, description="A cursor for use in pagination")
class EdgeMeta:
description = f"A Relay edge containing a `{base_name}` and its cursor."
edge_name = f"{base_name}Edge"
edge_bases = [edge_class, EdgeBase] if edge_class else [EdgeBase]
if not isinstance(edge_class, ObjectType):
edge_bases = [*edge_bases, ObjectType]
return type(edge_name, tuple(edge_bases), {"Meta": EdgeMeta})
class PageInfo(ObjectType):
class Meta:
description = (
"The Relay compliant `PageInfo` type, containing data necessary to"
" paginate this connection."
)
has_next_page = Boolean(
required=True,
name="hasNextPage",
description="When paginating forwards, are there more items?",
)
has_previous_page = Boolean(
required=True,
name="hasPreviousPage",
description="When paginating backwards, are there more items?",
)
start_cursor = String(
name="startCursor",
description="When paginating backwards, the cursor to continue.",
)
end_cursor = String(
name="endCursor",
description="When paginating forwards, the cursor to continue.",
)
# noinspection PyPep8Naming
def page_info_adapter(startCursor, endCursor, hasPreviousPage, hasNextPage):
"""Adapter for creating PageInfo instances"""
return PageInfo(
start_cursor=startCursor,
end_cursor=endCursor,
has_previous_page=hasPreviousPage,
has_next_page=hasNextPage,
)
class ConnectionOptions(ObjectTypeOptions):
node = None
class Connection(ObjectType):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(
cls, node=None, name=None, strict_types=False, _meta=None, **options
):
if not _meta:
_meta = ConnectionOptions(cls)
assert node, f"You have to provide a node in {cls.__name__}.Meta"
assert isinstance(node, NonNull) or issubclass(
node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)
), f'Received incompatible node "{node}" for Connection {cls.__name__}.'
base_name = re.sub("Connection$", "", name or cls.__name__) or node._meta.name
if not name:
name = f"{base_name}Connection"
options["name"] = name
_meta.node = node
if not _meta.fields:
_meta.fields = {}
if "page_info" not in _meta.fields:
_meta.fields["page_info"] = Field(
PageInfo,
name="pageInfo",
required=True,
description="Pagination data for this connection.",
)
if "edges" not in _meta.fields:
edge_class = get_edge_class(cls, node, base_name, strict_types) # type: ignore
cls.Edge = edge_class
_meta.fields["edges"] = Field(
NonNull(List(NonNull(edge_class) if strict_types else edge_class)),
description="Contains the nodes in this connection.",
)
return super(Connection, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
# noinspection PyPep8Naming
def connection_adapter(cls, edges, pageInfo):
"""Adapter for creating Connection instances"""
return cls(edges=edges, page_info=pageInfo)
class IterableConnectionField(Field):
def __init__(self, type_, *args, **kwargs):
kwargs.setdefault("before", String())
kwargs.setdefault("after", String())
kwargs.setdefault("first", Int())
kwargs.setdefault("last", Int())
super(IterableConnectionField, self).__init__(type_, *args, **kwargs)
@property
def type(self):
type_ = super(IterableConnectionField, self).type
connection_type = type_
if isinstance(type_, NonNull):
connection_type = type_.of_type
if is_node(connection_type):
raise Exception(
"ConnectionFields now need a explicit ConnectionType for Nodes.\n"
"Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#node-connections"
)
assert issubclass(
connection_type, Connection
), f'{self.__class__.__name__} type has to be a subclass of Connection. Received "{connection_type}".'
return type_
@classmethod
def resolve_connection(cls, connection_type, args, resolved):
if isinstance(resolved, connection_type):
return resolved
assert isinstance(resolved, Iterable), (
f"Resolved value from the connection field has to be an iterable or instance of {connection_type}. "
f'Received "{resolved}"'
)
connection = connection_from_array(
resolved,
args,
connection_type=partial(connection_adapter, connection_type),
edge_type=connection_type.Edge,
page_info_type=page_info_adapter,
)
connection.iterable = resolved
return connection
@classmethod
def connection_resolver(cls, resolver, connection_type, root, info, **args):
resolved = resolver(root, info, **args)
if isinstance(connection_type, NonNull):
connection_type = connection_type.of_type
on_resolve = partial(cls.resolve_connection, connection_type, args)
return maybe_thenable(resolved, on_resolve)
def wrap_resolve(self, parent_resolver):
resolver = super(IterableConnectionField, self).wrap_resolve(parent_resolver)
return partial(self.connection_resolver, resolver, self.type)
ConnectionField = IterableConnectionField

View File

@@ -0,0 +1,87 @@
from graphql_relay import from_global_id, to_global_id
from ..types import ID, UUID
from ..types.base import BaseType
from typing import Type
class BaseGlobalIDType:
"""
Base class that define the required attributes/method for a type.
"""
graphene_type: Type[BaseType] = ID
@classmethod
def resolve_global_id(cls, info, global_id):
# return _type, _id
raise NotImplementedError
@classmethod
def to_global_id(cls, _type, _id):
# return _id
raise NotImplementedError
class DefaultGlobalIDType(BaseGlobalIDType):
"""
Default global ID type: base64 encoded version of "<node type name>: <node id>".
"""
graphene_type = ID
@classmethod
def resolve_global_id(cls, info, global_id):
try:
_type, _id = from_global_id(global_id)
if not _type:
raise ValueError("Invalid Global ID")
return _type, _id
except Exception as e:
raise Exception(
f'Unable to parse global ID "{global_id}". '
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
f"Exception message: {e}"
)
@classmethod
def to_global_id(cls, _type, _id):
return to_global_id(_type, _id)
class SimpleGlobalIDType(BaseGlobalIDType):
"""
Simple global ID type: simply the id of the object.
To be used carefully as the user is responsible for ensuring that the IDs are indeed global
(otherwise it could cause request caching issues).
"""
graphene_type = ID
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id
class UUIDGlobalIDType(BaseGlobalIDType):
"""
UUID global ID type.
By definition UUID are global so they are used as they are.
"""
graphene_type = UUID
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id

View File

@@ -0,0 +1,66 @@
import re
from ..types import Field, InputObjectType, String
from ..types.mutation import Mutation
from ..utils.thenables import maybe_thenable
class ClientIDMutation(Mutation):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(
cls, output=None, input_fields=None, arguments=None, name=None, **options
):
input_class = getattr(cls, "Input", None)
base_name = re.sub("Payload$", "", name or cls.__name__)
assert not output, "Can't specify any output"
assert not arguments, "Can't specify any arguments"
bases = (InputObjectType,)
if input_class:
bases += (input_class,)
if not input_fields:
input_fields = {}
cls.Input = type(
f"{base_name}Input",
bases,
dict(input_fields, client_mutation_id=String(name="clientMutationId")),
)
arguments = dict(
input=cls.Input(required=True)
# 'client_mutation_id': String(name='clientMutationId')
)
mutate_and_get_payload = getattr(cls, "mutate_and_get_payload", None)
if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__:
assert mutate_and_get_payload, (
f"{name or cls.__name__}.mutate_and_get_payload method is required"
" in a ClientIDMutation."
)
if not name:
name = f"{base_name}Payload"
super(ClientIDMutation, cls).__init_subclass_with_meta__(
output=None, arguments=arguments, name=name, **options
)
cls._meta.fields["client_mutation_id"] = Field(String, name="clientMutationId")
@classmethod
def mutate(cls, root, info, input):
def on_resolve(payload):
try:
payload.client_mutation_id = input.get("client_mutation_id")
except Exception:
raise Exception(
f"Cannot set client_mutation_id in the payload object {repr(payload)}"
)
return payload
result = cls.mutate_and_get_payload(root, info, **input)
return maybe_thenable(result, on_resolve)

View File

@@ -0,0 +1,135 @@
from functools import partial
from inspect import isclass
from ..types import Field, Interface, ObjectType
from ..types.interface import InterfaceOptions
from ..types.utils import get_type
from .id_type import BaseGlobalIDType, DefaultGlobalIDType
def is_node(objecttype):
"""
Check if the given objecttype has Node as an interface
"""
if not isclass(objecttype):
return False
if not issubclass(objecttype, ObjectType):
return False
return any(issubclass(i, Node) for i in objecttype._meta.interfaces)
class GlobalID(Field):
def __init__(
self,
node=None,
parent_type=None,
required=True,
global_id_type=DefaultGlobalIDType,
*args,
**kwargs,
):
super(GlobalID, self).__init__(
global_id_type.graphene_type, required=required, *args, **kwargs
)
self.node = node or Node
self.parent_type_name = parent_type._meta.name if parent_type else None
@staticmethod
def id_resolver(parent_resolver, node, root, info, parent_type_name=None, **args):
type_id = parent_resolver(root, info, **args)
parent_type_name = parent_type_name or info.parent_type.name
return node.to_global_id(parent_type_name, type_id) # root._meta.name
def wrap_resolve(self, parent_resolver):
return partial(
self.id_resolver,
parent_resolver,
self.node,
parent_type_name=self.parent_type_name,
)
class NodeField(Field):
def __init__(self, node, type_=False, **kwargs):
assert issubclass(node, Node), "NodeField can only operate in Nodes"
self.node_type = node
self.field_type = type_
global_id_type = node._meta.global_id_type
super(NodeField, self).__init__(
# If we don't specify a type, the field type will be the node interface
type_ or node,
id=global_id_type.graphene_type(
required=True, description="The ID of the object"
),
**kwargs,
)
def wrap_resolve(self, parent_resolver):
return partial(self.node_type.node_resolver, get_type(self.field_type))
class AbstractNode(Interface):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, global_id_type=DefaultGlobalIDType, **options):
assert issubclass(
global_id_type, BaseGlobalIDType
), "Custom ID type need to be implemented as a subclass of BaseGlobalIDType."
_meta = InterfaceOptions(cls)
_meta.global_id_type = global_id_type
_meta.fields = {
"id": GlobalID(
cls, global_id_type=global_id_type, description="The ID of the object"
)
}
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod
def resolve_global_id(cls, info, global_id):
return cls._meta.global_id_type.resolve_global_id(info, global_id)
class Node(AbstractNode):
"""An object with an ID"""
@classmethod
def Field(cls, *args, **kwargs): # noqa: N802
return NodeField(cls, *args, **kwargs)
@classmethod
def node_resolver(cls, only_type, root, info, id):
return cls.get_node_from_global_id(info, id, only_type=only_type)
@classmethod
def get_node_from_global_id(cls, info, global_id, only_type=None):
_type, _id = cls.resolve_global_id(info, global_id)
graphene_type = info.schema.get_type(_type)
if graphene_type is None:
raise Exception(f'Relay Node "{_type}" not found in schema')
graphene_type = graphene_type.graphene_type
if only_type:
assert (
graphene_type == only_type
), f"Must receive a {only_type._meta.name} id."
# We make sure the ObjectType implements the "Node" interface
if cls not in graphene_type._meta.interfaces:
raise Exception(
f'ObjectType "{_type}" does not implement the "{cls}" interface.'
)
get_node = getattr(graphene_type, "get_node", None)
if get_node:
return get_node(info, _id)
@classmethod
def to_global_id(cls, type_, id):
return cls._meta.global_id_type.to_global_id(type_, id)

View File

@@ -0,0 +1,318 @@
import re
from pytest import raises
from ...types import Argument, Field, Int, List, NonNull, ObjectType, Schema, String
from ..connection import (
Connection,
ConnectionField,
PageInfo,
ConnectionOptions,
get_edge_class,
)
from ..node import Node
class MyObject(ObjectType):
class Meta:
interfaces = [Node]
field = String()
def test_connection():
class MyObjectConnection(Connection):
extra = String()
class Meta:
node = MyObject
class Edge:
other = String()
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["page_info", "edges", "extra"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
assert isinstance(edge_field.type.of_type, List)
assert edge_field.type.of_type.of_type == MyObjectConnection.Edge
assert isinstance(pageinfo_field, Field)
assert isinstance(pageinfo_field.type, NonNull)
assert pageinfo_field.type.of_type == PageInfo
def test_connection_inherit_abstracttype():
class BaseConnection:
extra = String()
class MyObjectConnection(BaseConnection, Connection):
class Meta:
node = MyObject
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["page_info", "edges", "extra"]
def test_connection_extra_abstract_fields():
class ConnectionWithNodes(Connection):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
_meta.fields = {
"nodes": Field(
NonNull(List(node)),
description="Contains all the nodes in this connection.",
),
}
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
node=node, name=name, _meta=_meta, **options
)
class MyObjectConnection(ConnectionWithNodes):
class Meta:
node = MyObject
class Edge:
other = String()
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["nodes", "page_info", "edges"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
nodes_field = fields["nodes"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
assert isinstance(edge_field.type.of_type, List)
assert edge_field.type.of_type.of_type == MyObjectConnection.Edge
assert isinstance(pageinfo_field, Field)
assert isinstance(pageinfo_field.type, NonNull)
assert pageinfo_field.type.of_type == PageInfo
assert isinstance(nodes_field, Field)
assert isinstance(nodes_field.type, NonNull)
assert isinstance(nodes_field.type.of_type, List)
assert nodes_field.type.of_type.of_type == MyObject
def test_connection_override_fields():
class ConnectionWithNodes(Connection):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
base_name = (
re.sub("Connection$", "", name or cls.__name__) or node._meta.name
)
edge_class = get_edge_class(cls, node, base_name)
_meta.fields = {
"page_info": Field(
NonNull(
PageInfo,
name="pageInfo",
required=True,
description="Pagination data for this connection.",
)
),
"edges": Field(
NonNull(List(NonNull(edge_class))),
description="Contains the nodes in this connection.",
),
}
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
node=node, name=name, _meta=_meta, **options
)
class MyObjectConnection(ConnectionWithNodes):
class Meta:
node = MyObject
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["page_info", "edges"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
assert isinstance(edge_field.type.of_type, List)
assert isinstance(edge_field.type.of_type.of_type, NonNull)
assert edge_field.type.of_type.of_type.of_type.__name__ == "MyObjectEdge"
# This page info is NonNull
assert isinstance(pageinfo_field, Field)
assert isinstance(edge_field.type, NonNull)
assert pageinfo_field.type.of_type == PageInfo
def test_connection_name():
custom_name = "MyObjectCustomNameConnection"
class BaseConnection:
extra = String()
class MyObjectConnection(BaseConnection, Connection):
class Meta:
node = MyObject
name = custom_name
assert MyObjectConnection._meta.name == custom_name
def test_edge():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
class Edge:
other = String()
Edge = MyObjectConnection.Edge
assert Edge._meta.name == "MyObjectEdge"
edge_fields = Edge._meta.fields
assert list(edge_fields) == ["node", "cursor", "other"]
assert isinstance(edge_fields["node"], Field)
assert edge_fields["node"].type == MyObject
assert isinstance(edge_fields["other"], Field)
assert edge_fields["other"].type == String
def test_edge_with_bases():
class BaseEdge:
extra = String()
class MyObjectConnection(Connection):
class Meta:
node = MyObject
class Edge(BaseEdge):
other = String()
Edge = MyObjectConnection.Edge
assert Edge._meta.name == "MyObjectEdge"
edge_fields = Edge._meta.fields
assert list(edge_fields) == ["node", "cursor", "extra", "other"]
assert isinstance(edge_fields["node"], Field)
assert edge_fields["node"].type == MyObject
assert isinstance(edge_fields["other"], Field)
assert edge_fields["other"].type == String
def test_edge_with_nonnull_node():
class MyObjectConnection(Connection):
class Meta:
node = NonNull(MyObject)
edge_fields = MyObjectConnection.Edge._meta.fields
assert isinstance(edge_fields["node"], Field)
assert isinstance(edge_fields["node"].type, NonNull)
assert edge_fields["node"].type.of_type == MyObject
def test_pageinfo():
assert PageInfo._meta.name == "PageInfo"
fields = PageInfo._meta.fields
assert list(fields) == [
"has_next_page",
"has_previous_page",
"start_cursor",
"end_cursor",
]
def test_connectionfield():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
field = ConnectionField(MyObjectConnection)
assert field.args == {
"before": Argument(String),
"after": Argument(String),
"first": Argument(Int),
"last": Argument(Int),
}
def test_connectionfield_node_deprecated():
field = ConnectionField(MyObject)
with raises(Exception) as exc_info:
field.type
assert "ConnectionFields now need a explicit ConnectionType for Nodes." in str(
exc_info.value
)
def test_connectionfield_custom_args():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
field = ConnectionField(
MyObjectConnection, before=String(required=True), extra=String()
)
assert field.args == {
"before": Argument(NonNull(String)),
"after": Argument(String),
"first": Argument(Int),
"last": Argument(Int),
"extra": Argument(String),
}
def test_connectionfield_required():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
class Query(ObjectType):
test_connection = ConnectionField(MyObjectConnection, required=True)
def resolve_test_connection(root, info, **args):
return []
schema = Schema(query=Query)
executed = schema.execute("{ testConnection { edges { cursor } } }")
assert not executed.errors
assert executed.data == {"testConnection": {"edges": []}}
def test_connectionfield_strict_types():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
strict_types = True
connection_field = ConnectionField(MyObjectConnection)
edges_field_type = connection_field.type._meta.fields["edges"].type
assert isinstance(edges_field_type, NonNull)
edges_list_element_type = edges_field_type.of_type.of_type
assert isinstance(edges_list_element_type, NonNull)
node_field = edges_list_element_type.of_type._meta.fields["node"]
assert isinstance(node_field.type, NonNull)

View File

@@ -0,0 +1,121 @@
from pytest import mark
from graphql_relay.utils import base64
from graphene.types import ObjectType, Schema, String
from graphene.relay.connection import Connection, ConnectionField, PageInfo
from graphene.relay.node import Node
letter_chars = ["A", "B", "C", "D", "E"]
class Letter(ObjectType):
class Meta:
interfaces = (Node,)
letter = String()
class LetterConnection(Connection):
class Meta:
node = Letter
class Query(ObjectType):
letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(LetterConnection)
async_letters = ConnectionField(LetterConnection)
node = Node.Field()
def resolve_letters(self, info, **args):
return list(letters.values())
async def resolve_async_letters(self, info, **args):
return list(letters.values())
def resolve_connection_letters(self, info, **args):
return LetterConnection(
page_info=PageInfo(has_next_page=True, has_previous_page=False),
edges=[
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
],
)
schema = Schema(Query)
letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter_chars)}
def edges(selected_letters):
return [
{
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
"cursor": base64("arrayconnection:%s" % letter.id),
}
for letter in [letters[i] for i in selected_letters]
]
def cursor_for(ltr):
letter = letters[ltr]
return base64("arrayconnection:%s" % letter.id)
def execute(args=""):
if args:
args = "(" + args + ")"
return schema.execute(
"""
{
letters%s {
edges {
node {
id
letter
}
cursor
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
"""
% args
)
@mark.asyncio
async def test_connection_async():
result = await schema.execute_async(
"""
{
asyncLetters(first:1) {
edges {
node {
id
letter
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
"""
)
assert not result.errors
assert result.data == {
"asyncLetters": {
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}

View File

@@ -0,0 +1,286 @@
from pytest import mark
from graphql_relay.utils import base64
from ...types import ObjectType, Schema, String
from ..connection import Connection, ConnectionField, PageInfo
from ..node import Node
letter_chars = ["A", "B", "C", "D", "E"]
class Letter(ObjectType):
class Meta:
interfaces = (Node,)
letter = String()
class LetterConnection(Connection):
class Meta:
node = Letter
class Query(ObjectType):
letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(LetterConnection)
async_letters = ConnectionField(LetterConnection)
node = Node.Field()
def resolve_letters(self, info, **args):
return list(letters.values())
async def resolve_async_letters(self, info, **args):
return list(letters.values())
def resolve_connection_letters(self, info, **args):
return LetterConnection(
page_info=PageInfo(has_next_page=True, has_previous_page=False),
edges=[
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
],
)
schema = Schema(Query)
letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter_chars)}
def edges(selected_letters):
return [
{
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
"cursor": base64("arrayconnection:%s" % letter.id),
}
for letter in [letters[i] for i in selected_letters]
]
def cursor_for(ltr):
letter = letters[ltr]
return base64("arrayconnection:%s" % letter.id)
async def execute(args=""):
if args:
args = "(" + args + ")"
return await schema.execute_async(
"""
{
letters%s {
edges {
node {
id
letter
}
cursor
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
"""
% args
)
async def check(args, letters, has_previous_page=False, has_next_page=False):
result = await execute(args)
expected_edges = edges(letters)
expected_page_info = {
"hasPreviousPage": has_previous_page,
"hasNextPage": has_next_page,
"endCursor": expected_edges[-1]["cursor"] if expected_edges else None,
"startCursor": expected_edges[0]["cursor"] if expected_edges else None,
}
assert not result.errors
assert result.data == {
"letters": {"edges": expected_edges, "pageInfo": expected_page_info}
}
@mark.asyncio
async def test_returns_all_elements_without_filters():
await check("", "ABCDE")
@mark.asyncio
async def test_respects_a_smaller_first():
await check("first: 2", "AB", has_next_page=True)
@mark.asyncio
async def test_respects_an_overly_large_first():
await check("first: 10", "ABCDE")
@mark.asyncio
async def test_respects_a_smaller_last():
await check("last: 2", "DE", has_previous_page=True)
@mark.asyncio
async def test_respects_an_overly_large_last():
await check("last: 10", "ABCDE")
@mark.asyncio
async def test_respects_first_and_after():
await check(f'first: 2, after: "{cursor_for("B")}"', "CD", has_next_page=True)
@mark.asyncio
async def test_respects_first_and_after_with_long_first():
await check(f'first: 10, after: "{cursor_for("B")}"', "CDE")
@mark.asyncio
async def test_respects_last_and_before():
await check(f'last: 2, before: "{cursor_for("D")}"', "BC", has_previous_page=True)
@mark.asyncio
async def test_respects_last_and_before_with_long_last():
await check(f'last: 10, before: "{cursor_for("D")}"', "ABC")
@mark.asyncio
async def test_respects_first_and_after_and_before_too_few():
await check(
f'first: 2, after: "{cursor_for("A")}", before: "{cursor_for("E")}"',
"BC",
has_next_page=True,
)
@mark.asyncio
async def test_respects_first_and_after_and_before_too_many():
await check(
f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
@mark.asyncio
async def test_respects_first_and_after_and_before_exactly_right():
await check(
f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
@mark.asyncio
async def test_respects_last_and_after_and_before_too_few():
await check(
f'last: 2, after: "{cursor_for("A")}", before: "{cursor_for("E")}"',
"CD",
has_previous_page=True,
)
@mark.asyncio
async def test_respects_last_and_after_and_before_too_many():
await check(
f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
@mark.asyncio
async def test_respects_last_and_after_and_before_exactly_right():
await check(
f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
@mark.asyncio
async def test_returns_no_elements_if_first_is_0():
await check("first: 0", "", has_next_page=True)
@mark.asyncio
async def test_returns_all_elements_if_cursors_are_invalid():
await check('before: "invalid" after: "invalid"', "ABCDE")
@mark.asyncio
async def test_returns_all_elements_if_cursors_are_on_the_outside():
await check(
f'before: "{base64("arrayconnection:%s" % 6)}" after: "{base64("arrayconnection:%s" % -1)}"',
"ABCDE",
)
@mark.asyncio
async def test_returns_no_elements_if_cursors_cross():
await check(
f'before: "{base64("arrayconnection:%s" % 2)}" after: "{base64("arrayconnection:%s" % 4)}"',
"",
)
@mark.asyncio
async def test_connection_type_nodes():
result = await schema.execute_async(
"""
{
connectionLetters {
edges {
node {
id
letter
}
cursor
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
"""
)
assert not result.errors
assert result.data == {
"connectionLetters": {
"edges": [
{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}, "cursor": "a-cursor"}
],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}
@mark.asyncio
async def test_connection_async():
result = await schema.execute_async(
"""
{
asyncLetters(first:1) {
edges {
node {
id
letter
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
"""
)
assert not result.errors
assert result.data == {
"asyncLetters": {
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}

View File

@@ -0,0 +1,325 @@
import re
from uuid import uuid4
from graphql import graphql_sync
from ..id_type import BaseGlobalIDType, SimpleGlobalIDType, UUIDGlobalIDType
from ..node import Node
from ...types import Int, ObjectType, Schema, String
class TestUUIDGlobalID:
def setup_method(self):
self.user_list = [
{"id": uuid4(), "name": "First"},
{"id": uuid4(), "name": "Second"},
{"id": uuid4(), "name": "Third"},
{"id": uuid4(), "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomNode(Node):
class Meta:
global_id_type = UUIDGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: UUID!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: UUID!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query userById($id: UUID!) {
user(id: $id) {
id
name
}
}"""
# UUID need to be converted to string for serialization
result = graphql_sync(
self.graphql_schema,
query,
variable_values={"id": str(self.user_list[0]["id"])},
)
assert not result.errors
assert result.data["user"]["id"] == str(self.user_list[0]["id"])
assert result.data["user"]["name"] == self.user_list[0]["name"]
class TestSimpleGlobalID:
def setup_method(self):
self.user_list = [
{"id": "my global primary key in clear 1", "name": "First"},
{"id": "my global primary key in clear 2", "name": "Second"},
{"id": "my global primary key in clear 3", "name": "Third"},
{"id": "my global primary key in clear 4", "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomNode(Node):
class Meta:
global_id_type = SimpleGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: ID!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: ID!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query {
user(id: "my global primary key in clear 3") {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["id"] == self.user_list[2]["id"]
assert result.data["user"]["name"] == self.user_list[2]["name"]
class TestCustomGlobalID:
def setup_method(self):
self.user_list = [
{"id": 1, "name": "First"},
{"id": 2, "name": "Second"},
{"id": 3, "name": "Third"},
{"id": 4, "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomGlobalIDType(BaseGlobalIDType):
"""
Global id that is simply and integer in clear.
"""
graphene_type = Int
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: Int!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: Int!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["id"] == self.user_list[1]["id"]
assert result.data["user"]["name"] == self.user_list[1]["name"]
class TestIncompleteCustomGlobalID:
def setup_method(self):
self.user_list = [
{"id": 1, "name": "First"},
{"id": 2, "name": "Second"},
{"id": 3, "name": "Third"},
{"id": 4, "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
def test_must_define_to_global_id(self):
"""
Test that if the `to_global_id` method is not defined, we can query the object, but we can't request its ID.
"""
class CustomGlobalIDType(BaseGlobalIDType):
graphene_type = Int
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
query = """query {
user(id: 2) {
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["name"] == self.user_list[1]["name"]
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert result.errors is not None
assert len(result.errors) == 1
assert result.errors[0].path == ["user", "id"]
def test_must_define_resolve_global_id(self):
"""
Test that if the `resolve_global_id` method is not defined, we can't query the object by ID.
"""
class CustomGlobalIDType(BaseGlobalIDType):
graphene_type = Int
@classmethod
def to_global_id(cls, _type, _id):
return _id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert result.errors is not None
assert len(result.errors) == 1
assert result.errors[0].path == ["user"]

View File

@@ -0,0 +1,58 @@
from graphql_relay import to_global_id
from ...types import ID, NonNull, ObjectType, String
from ...types.definitions import GrapheneObjectType
from ..node import GlobalID, Node
class CustomNode(Node):
class Meta:
name = "Node"
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
class Info:
def __init__(self, parent_type):
self.parent_type = GrapheneObjectType(
graphene_type=parent_type,
name=parent_type._meta.name,
description=parent_type._meta.description,
fields=None,
is_type_of=parent_type.is_type_of,
interfaces=None,
)
def test_global_id_defaults_to_required_and_node():
gid = GlobalID()
assert isinstance(gid.type, NonNull)
assert gid.type.of_type == ID
assert gid.node == Node
def test_global_id_allows_overriding_of_node_and_required():
gid = GlobalID(node=CustomNode, required=False)
assert gid.type == ID
assert gid.node == CustomNode
def test_global_id_defaults_to_info_parent_type():
my_id = "1"
gid = GlobalID()
id_resolver = gid.wrap_resolve(lambda *_: my_id)
my_global_id = id_resolver(None, Info(User))
assert my_global_id == to_global_id(User._meta.name, my_id)
def test_global_id_allows_setting_customer_parent_type():
my_id = "1"
gid = GlobalID(parent_type=User)
id_resolver = gid.wrap_resolve(lambda *_: my_id)
my_global_id = id_resolver(None, None)
assert my_global_id == to_global_id(User._meta.name, my_id)

View File

@@ -0,0 +1,206 @@
from pytest import mark, raises
from ...types import (
ID,
Argument,
Field,
InputField,
InputObjectType,
NonNull,
ObjectType,
Schema,
)
from ...types.scalars import String
from ..mutation import ClientIDMutation
class SharedFields:
shared = String()
class MyNode(ObjectType):
# class Meta:
# interfaces = (Node, )
id = ID()
name = String()
class SaySomething(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomething(phrase=str(what))
class FixedSaySomething:
__slots__ = ("phrase",)
def __init__(self, phrase):
self.phrase = phrase
class SaySomethingFixed(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return FixedSaySomething(phrase=str(what))
class SaySomethingAsync(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomething(phrase=str(what))
# MyEdge = MyNode.Connection.Edge
class MyEdge(ObjectType):
node = Field(MyNode)
cursor = String()
class OtherMutation(ClientIDMutation):
class Input(SharedFields):
additional_field = String()
name = String()
my_node_edge = Field(MyEdge)
@staticmethod
def mutate_and_get_payload(
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)
class RootQuery(ObjectType):
something = String()
class Mutation(ObjectType):
say = SaySomething.Field()
say_fixed = SaySomethingFixed.Field()
say_async = SaySomethingAsync.Field()
other = OtherMutation.Field()
schema = Schema(query=RootQuery, mutation=Mutation)
def test_no_mutate_and_get_payload():
with raises(AssertionError) as excinfo:
class MyMutation(ClientIDMutation):
pass
assert (
"MyMutation.mutate_and_get_payload method is required in a ClientIDMutation."
== str(excinfo.value)
)
def test_mutation():
fields = SaySomething._meta.fields
assert list(fields) == ["phrase", "client_mutation_id"]
assert SaySomething._meta.name == "SaySomethingPayload"
assert isinstance(fields["phrase"], Field)
field = SaySomething.Field()
assert field.type == SaySomething
assert list(field.args) == ["input"]
assert isinstance(field.args["input"], Argument)
assert isinstance(field.args["input"].type, NonNull)
assert field.args["input"].type.of_type == SaySomething.Input
assert isinstance(fields["client_mutation_id"], Field)
assert fields["client_mutation_id"].name == "clientMutationId"
assert fields["client_mutation_id"].type == String
def test_mutation_input():
Input = SaySomething.Input
assert issubclass(Input, InputObjectType)
fields = Input._meta.fields
assert list(fields) == ["what", "client_mutation_id"]
assert isinstance(fields["what"], InputField)
assert fields["what"].type == String
assert isinstance(fields["client_mutation_id"], InputField)
assert fields["client_mutation_id"].type == String
def test_subclassed_mutation():
fields = OtherMutation._meta.fields
assert list(fields) == ["name", "my_node_edge", "client_mutation_id"]
assert isinstance(fields["name"], Field)
field = OtherMutation.Field()
assert field.type == OtherMutation
assert list(field.args) == ["input"]
assert isinstance(field.args["input"], Argument)
assert isinstance(field.args["input"].type, NonNull)
assert field.args["input"].type.of_type == OtherMutation.Input
def test_subclassed_mutation_input():
Input = OtherMutation.Input
assert issubclass(Input, InputObjectType)
fields = Input._meta.fields
assert list(fields) == ["shared", "additional_field", "client_mutation_id"]
assert isinstance(fields["shared"], InputField)
assert fields["shared"].type == String
assert isinstance(fields["additional_field"], InputField)
assert fields["additional_field"].type == String
assert isinstance(fields["client_mutation_id"], InputField)
assert fields["client_mutation_id"].type == String
def test_node_query():
executed = schema.execute(
'mutation a { say(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert not executed.errors
assert executed.data == {"say": {"phrase": "hello"}}
def test_node_query_fixed():
executed = schema.execute(
'mutation a { sayFixed(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert "Cannot set client_mutation_id in the payload object" in str(
executed.errors[0]
)
@mark.asyncio
async def test_node_query_async():
executed = await schema.execute_async(
'mutation a { sayAsync(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert not executed.errors
assert executed.data == {"sayAsync": {"phrase": "hello"}}
def test_edge_query():
executed = schema.execute(
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }'
)
assert not executed.errors
assert dict(executed.data) == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
}
}

View File

@@ -0,0 +1,90 @@
from pytest import mark
from graphene.types import ID, Field, ObjectType, Schema
from graphene.types.scalars import String
from graphene.relay.mutation import ClientIDMutation
from graphene.test import Client
class SharedFields(object):
shared = String()
class MyNode(ObjectType):
# class Meta:
# interfaces = (Node, )
id = ID()
name = String()
class SaySomethingAsync(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomethingAsync(phrase=str(what))
# MyEdge = MyNode.Connection.Edge
class MyEdge(ObjectType):
node = Field(MyNode)
cursor = String()
class OtherMutation(ClientIDMutation):
class Input(SharedFields):
additional_field = String()
name = String()
my_node_edge = Field(MyEdge)
@staticmethod
def mutate_and_get_payload(
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)
class RootQuery(ObjectType):
something = String()
class Mutation(ObjectType):
say_promise = SaySomethingAsync.Field()
other = OtherMutation.Field()
schema = Schema(query=RootQuery, mutation=Mutation)
client = Client(schema)
@mark.asyncio
async def test_node_query_promise():
executed = await client.execute_async(
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert isinstance(executed, dict)
assert "errors" not in executed
assert executed["data"] == {"sayPromise": {"phrase": "hello"}}
@mark.asyncio
async def test_edge_query():
executed = await client.execute_async(
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }'
)
assert isinstance(executed, dict)
assert "errors" not in executed
assert executed["data"] == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
}
}

View File

@@ -0,0 +1,219 @@
import re
from textwrap import dedent
from graphql_relay import to_global_id
from ...types import ObjectType, Schema, String
from ..node import Node, is_node
class SharedNodeFields:
shared = String()
something_else = String()
def resolve_something_else(*_):
return "----"
class MyNode(ObjectType):
class Meta:
interfaces = (Node,)
name = String()
@staticmethod
def get_node(info, id):
return MyNode(name=str(id))
class MyOtherNode(SharedNodeFields, ObjectType):
extra_field = String()
class Meta:
interfaces = (Node,)
def resolve_extra_field(self, *_):
return "extra field info."
@staticmethod
def get_node(info, id):
return MyOtherNode(shared=str(id))
class RootQuery(ObjectType):
first = String()
node = Node.Field()
only_node = Node.Field(MyNode)
only_node_lazy = Node.Field(lambda: MyNode)
schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode])
def test_node_good():
assert "id" in MyNode._meta.fields
assert is_node(MyNode)
assert not is_node(object)
assert not is_node("node")
def test_node_query():
executed = schema.execute(
'{ node(id:"%s") { ... on MyNode { name } } }' % Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {"node": {"name": "1"}}
def test_subclassed_node_query():
executed = schema.execute(
'{ node(id:"%s") { ... on MyOtherNode { shared, extraField, somethingElse } } }'
% to_global_id("MyOtherNode", 1)
)
assert not executed.errors
assert executed.data == {
"node": {
"shared": "1",
"extraField": "extra field info.",
"somethingElse": "----",
}
}
def test_node_requesting_non_node():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("RootQuery", 1)
)
assert executed.errors
assert re.match(
r"ObjectType .* does not implement the .* interface.",
executed.errors[0].message,
)
assert executed.data == {"node": None}
def test_node_requesting_unknown_type():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("UnknownType", 1)
)
assert executed.errors
assert re.match(r"Relay Node .* not found in schema", executed.errors[0].message)
assert executed.data == {"node": None}
def test_node_query_incorrect_id():
executed = schema.execute(
'{ node(id:"%s") { ... on MyNode { name } } }' % "something:2"
)
assert executed.errors
assert re.match(r"Unable to parse global ID .*", executed.errors[0].message)
assert executed.data == {"node": None}
def test_node_field():
node_field = Node.Field()
assert node_field.type == Node
assert node_field.node_type == Node
def test_node_field_custom():
node_field = Node.Field(MyNode)
assert node_field.type == MyNode
assert node_field.node_type == Node
def test_node_field_args():
field_args = {
"name": "my_custom_name",
"description": "my_custom_description",
"deprecation_reason": "my_custom_deprecation_reason",
}
node_field = Node.Field(**field_args)
for field_arg, value in field_args.items():
assert getattr(node_field, field_arg) == value
def test_node_field_only_type():
executed = schema.execute(
'{ onlyNode(id:"%s") { __typename, name } } ' % Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {"onlyNode": {"__typename": "MyNode", "name": "1"}}
def test_node_field_only_type_wrong():
executed = schema.execute(
'{ onlyNode(id:"%s") { __typename, name } } '
% Node.to_global_id("MyOtherNode", 1)
)
assert len(executed.errors) == 1
assert str(executed.errors[0]).startswith("Must receive a MyNode id.")
assert executed.data == {"onlyNode": None}
def test_node_field_only_lazy_type():
executed = schema.execute(
'{ onlyNodeLazy(id:"%s") { __typename, name } } '
% Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {"onlyNodeLazy": {"__typename": "MyNode", "name": "1"}}
def test_node_field_only_lazy_type_wrong():
executed = schema.execute(
'{ onlyNodeLazy(id:"%s") { __typename, name } } '
% Node.to_global_id("MyOtherNode", 1)
)
assert len(executed.errors) == 1
assert str(executed.errors[0]).startswith("Must receive a MyNode id.")
assert executed.data == {"onlyNodeLazy": None}
def test_str_schema():
assert (
str(schema).strip()
== dedent(
'''
schema {
query: RootQuery
}
type MyNode implements Node {
"""The ID of the object"""
id: ID!
name: String
}
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type MyOtherNode implements Node {
"""The ID of the object"""
id: ID!
shared: String
somethingElse: String
extraField: String
}
type RootQuery {
first: String
node(
"""The ID of the object"""
id: ID!
): Node
onlyNode(
"""The ID of the object"""
id: ID!
): MyNode
onlyNodeLazy(
"""The ID of the object"""
id: ID!
): MyNode
}
'''
).strip()
)

View File

@@ -0,0 +1,313 @@
from textwrap import dedent
from graphql import graphql_sync
from ...types import Interface, ObjectType, Schema
from ...types.scalars import Int, String
from ..node import Node
class CustomNode(Node):
class Meta:
name = "Node"
@staticmethod
def to_global_id(type_, id):
return id
@staticmethod
def get_node_from_global_id(info, id, only_type=None):
assert info.schema is graphql_schema
if id in user_data:
return user_data.get(id)
else:
return photo_data.get(id)
class BasePhoto(Interface):
width = Int(description="The width of the photo in pixels")
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String(description="The full name of the user")
class Photo(ObjectType):
class Meta:
interfaces = [CustomNode, BasePhoto]
user_data = {"1": User(id="1", name="John Doe"), "2": User(id="2", name="Jane Smith")}
photo_data = {"3": Photo(id="3", width=300), "4": Photo(id="4", width=400)}
class RootQuery(ObjectType):
node = CustomNode.Field()
schema = Schema(query=RootQuery, types=[User, Photo])
graphql_schema = schema.graphql_schema
def test_str_schema_correct():
assert (
str(schema).strip()
== dedent(
'''
schema {
query: RootQuery
}
type User implements Node {
"""The ID of the object"""
id: ID!
"""The full name of the user"""
name: String
}
interface Node {
"""The ID of the object"""
id: ID!
}
type Photo implements Node & BasePhoto {
"""The ID of the object"""
id: ID!
"""The width of the photo in pixels"""
width: Int
}
interface BasePhoto {
"""The width of the photo in pixels"""
width: Int
}
type RootQuery {
node(
"""The ID of the object"""
id: ID!
): Node
}
'''
).strip()
)
def test_gets_the_correct_id_for_users():
query = """
{
node(id: "1") {
id
}
}
"""
expected = {"node": {"id": "1"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_id_for_photos():
query = """
{
node(id: "4") {
id
}
}
"""
expected = {"node": {"id": "4"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_name_for_users():
query = """
{
node(id: "1") {
id
... on User {
name
}
}
}
"""
expected = {"node": {"id": "1", "name": "John Doe"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_width_for_photos():
query = """
{
node(id: "4") {
id
... on Photo {
width
}
}
}
"""
expected = {"node": {"id": "4", "width": 400}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_typename_for_users():
query = """
{
node(id: "1") {
id
__typename
}
}
"""
expected = {"node": {"id": "1", "__typename": "User"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_typename_for_photos():
query = """
{
node(id: "4") {
id
__typename
}
}
"""
expected = {"node": {"id": "4", "__typename": "Photo"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_ignores_photo_fragments_on_user():
query = """
{
node(id: "1") {
id
... on Photo {
width
}
}
}
"""
expected = {"node": {"id": "1"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_returns_null_for_bad_ids():
query = """
{
node(id: "5") {
id
}
}
"""
expected = {"node": None}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_have_correct_node_interface():
query = """
{
__type(name: "Node") {
name
kind
fields {
name
type {
kind
ofType {
name
kind
}
}
}
}
}
"""
expected = {
"__type": {
"name": "Node",
"kind": "INTERFACE",
"fields": [
{
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {"name": "ID", "kind": "SCALAR"},
},
}
],
}
}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_has_correct_node_root_field():
query = """
{
__schema {
queryType {
fields {
name
type {
name
kind
}
args {
name
type {
kind
ofType {
name
kind
}
}
}
}
}
}
}
"""
expected = {
"__schema": {
"queryType": {
"fields": [
{
"name": "node",
"type": {"name": "Node", "kind": "INTERFACE"},
"args": [
{
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {"name": "ID", "kind": "SCALAR"},
},
}
],
}
]
}
}
}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected

View File

@@ -0,0 +1,39 @@
from graphql.error import GraphQLError
from graphene.types.schema import Schema
def default_format_error(error):
if isinstance(error, GraphQLError):
return error.formatted
return {"message": str(error)}
def format_execution_result(execution_result, format_error):
if execution_result:
response = {}
if execution_result.errors:
response["errors"] = [format_error(e) for e in execution_result.errors]
response["data"] = execution_result.data
return response
class Client:
def __init__(self, schema, format_error=None, **execute_options):
assert isinstance(schema, Schema)
self.schema = schema
self.execute_options = execute_options
self.format_error = format_error or default_format_error
def format_result(self, result):
return format_execution_result(result, self.format_error)
def execute(self, *args, **kwargs):
executed = self.schema.execute(*args, **dict(self.execute_options, **kwargs))
return self.format_result(executed)
async def execute_async(self, *args, **kwargs):
executed = await self.schema.execute_async(
*args, **dict(self.execute_options, **kwargs)
)
return self.format_result(executed)

View File

@@ -0,0 +1,41 @@
# https://github.com/graphql-python/graphene/issues/1293
from datetime import datetime, timezone
import graphene
from graphql.utilities import print_schema
class Filters(graphene.InputObjectType):
datetime_after = graphene.DateTime(
required=False,
default_value=datetime.fromtimestamp(1434549820.776, timezone.utc),
)
datetime_before = graphene.DateTime(
required=False,
default_value=datetime.fromtimestamp(1444549820.776, timezone.utc),
)
class SetDatetime(graphene.Mutation):
class Arguments:
filters = Filters(required=True)
ok = graphene.Boolean()
def mutate(root, info, filters):
return SetDatetime(ok=True)
class Query(graphene.ObjectType):
goodbye = graphene.String()
class Mutations(graphene.ObjectType):
set_datetime = SetDatetime.Field()
def test_schema_printable_with_default_datetime_value():
schema = graphene.Schema(query=Query, mutation=Mutations)
schema_str = print_schema(schema.graphql_schema)
assert schema_str, "empty schema printed"

View File

@@ -0,0 +1,36 @@
from ...types import ObjectType, Schema, String, NonNull
class Query(ObjectType):
hello = String(input=NonNull(String))
def resolve_hello(self, info, input):
if input == "nothing":
return None
return f"Hello {input}!"
schema = Schema(query=Query)
def test_required_input_provided():
"""
Test that a required argument works when provided.
"""
input_value = "Potato"
result = schema.execute('{ hello(input: "%s") }' % input_value)
assert not result.errors
assert result.data == {"hello": "Hello Potato!"}
def test_required_input_missing():
"""
Test that a required argument raised an error if not provided.
"""
result = schema.execute("{ hello }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "Field 'hello' argument 'input' of type 'String!' is required, but it was not provided."
)

View File

@@ -0,0 +1,53 @@
import pytest
from ...types.base64 import Base64
from ...types.datetime import Date, DateTime
from ...types.decimal import Decimal
from ...types.generic import GenericScalar
from ...types.json import JSONString
from ...types.objecttype import ObjectType
from ...types.scalars import ID, BigInt, Boolean, Float, Int, String
from ...types.schema import Schema
from ...types.uuid import UUID
@pytest.mark.parametrize(
"input_type,input_value",
[
(Date, '"2022-02-02"'),
(GenericScalar, '"foo"'),
(Int, "1"),
(BigInt, "12345678901234567890"),
(Float, "1.1"),
(String, '"foo"'),
(Boolean, "true"),
(ID, "1"),
(DateTime, '"2022-02-02T11:11:11"'),
(UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'),
(Decimal, '"1.1"'),
(JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'),
(Base64, '"Q2hlbG8gd29ycmxkCg=="'),
],
)
def test_parse_literal_with_variables(input_type, input_value):
# input_b needs to be evaluated as literal while the variable dict for
# input_a is passed along.
class Query(ObjectType):
generic = GenericScalar(input_a=GenericScalar(), input_b=input_type())
def resolve_generic(self, info, input_a=None, input_b=None):
return input
schema = Schema(query=Query)
query = f"""
query Test($a: GenericScalar){{
generic(inputA: $a, inputB: {input_value})
}}
"""
result = schema.execute(
query,
variables={"a": "bar"},
)
assert not result.errors

View File

@@ -0,0 +1,57 @@
# https://github.com/graphql-python/graphene/issues/313
import graphene
class Query(graphene.ObjectType):
rand = graphene.String()
class Success(graphene.ObjectType):
yeah = graphene.String()
class Error(graphene.ObjectType):
message = graphene.String()
class CreatePostResult(graphene.Union):
class Meta:
types = [Success, Error]
class CreatePost(graphene.Mutation):
class Arguments:
text = graphene.String(required=True)
result = graphene.Field(CreatePostResult)
def mutate(self, info, text):
result = Success(yeah="yeah")
return CreatePost(result=result)
class Mutations(graphene.ObjectType):
create_post = CreatePost.Field()
# tests.py
def test_create_post():
query_string = """
mutation {
createPost(text: "Try this out") {
result {
__typename
}
}
}
"""
schema = graphene.Schema(query=Query, mutation=Mutations)
result = schema.execute(query_string)
assert not result.errors
assert result.data["createPost"]["result"]["__typename"] == "Success"

View File

@@ -0,0 +1,33 @@
# https://github.com/graphql-python/graphene/issues/356
from pytest import raises
import graphene
from graphene import relay
class SomeTypeOne(graphene.ObjectType):
pass
class SomeTypeTwo(graphene.ObjectType):
pass
class MyUnion(graphene.Union):
class Meta:
types = (SomeTypeOne, SomeTypeTwo)
def test_issue():
class Query(graphene.ObjectType):
things = relay.ConnectionField(MyUnion)
with raises(Exception) as exc_info:
graphene.Schema(query=Query)
assert str(exc_info.value) == (
"Query fields cannot be resolved."
" IterableConnectionField type has to be a subclass of Connection."
' Received "MyUnion".'
)

View File

@@ -0,0 +1,117 @@
# https://github.com/graphql-python/graphene/issues/425
# Adapted for Graphene 2.0
from graphene.types.enum import Enum, EnumOptions
from graphene.types.inputobjecttype import InputObjectType
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
# ObjectType
class SpecialOptions(ObjectTypeOptions):
other_attr = None
class SpecialObjectType(ObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
_meta = SpecialOptions(cls)
_meta.other_attr = other_attr
super(SpecialObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
def test_special_objecttype_could_be_subclassed():
class MyType(SpecialObjectType):
class Meta:
other_attr = "yeah!"
assert MyType._meta.other_attr == "yeah!"
def test_special_objecttype_could_be_subclassed_default():
class MyType(SpecialObjectType):
pass
assert MyType._meta.other_attr == "default"
def test_special_objecttype_inherit_meta_options():
class MyType(SpecialObjectType):
pass
assert MyType._meta.name == "MyType"
assert MyType._meta.default_resolver is None
assert MyType._meta.interfaces == ()
# InputObjectType
class SpecialInputObjectTypeOptions(ObjectTypeOptions):
other_attr = None
class SpecialInputObjectType(InputObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
_meta = SpecialInputObjectTypeOptions(cls)
_meta.other_attr = other_attr
super(SpecialInputObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
def test_special_inputobjecttype_could_be_subclassed():
class MyInputObjectType(SpecialInputObjectType):
class Meta:
other_attr = "yeah!"
assert MyInputObjectType._meta.other_attr == "yeah!"
def test_special_inputobjecttype_could_be_subclassed_default():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.other_attr == "default"
def test_special_inputobjecttype_inherit_meta_options():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.name == "MyInputObjectType"
# Enum
class SpecialEnumOptions(EnumOptions):
other_attr = None
class SpecialEnum(Enum):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
_meta = SpecialEnumOptions(cls)
_meta.other_attr = other_attr
super(SpecialEnum, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_special_enum_could_be_subclassed():
class MyEnum(SpecialEnum):
class Meta:
other_attr = "yeah!"
assert MyEnum._meta.other_attr == "yeah!"
def test_special_enum_could_be_subclassed_default():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.other_attr == "default"
def test_special_enum_inherit_meta_options():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.name == "MyEnum"

View File

@@ -0,0 +1,24 @@
# https://github.com/graphql-python/graphene/issues/313
import graphene
class Query(graphene.ObjectType):
some_field = graphene.String(from_=graphene.String(name="from"))
def resolve_some_field(self, info, from_=None):
return from_
def test_issue():
query_string = """
query myQuery {
someField(from: "Oh")
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(query_string)
assert not result.errors
assert result.data["someField"] == "Oh"

View File

@@ -0,0 +1,44 @@
# https://github.com/graphql-python/graphene/issues/720
# InputObjectTypes overwrite the "fields" attribute of the provided
# _meta object, so even if dynamic fields are provided with a standard
# InputObjectTypeOptions, they are ignored.
import graphene
class MyInputClass(graphene.InputObjectType):
@classmethod
def __init_subclass_with_meta__(
cls, container=None, _meta=None, fields=None, **options
):
if _meta is None:
_meta = graphene.types.inputobjecttype.InputObjectTypeOptions(cls)
_meta.fields = fields
super(MyInputClass, cls).__init_subclass_with_meta__(
container=container, _meta=_meta, **options
)
class MyInput(MyInputClass):
class Meta:
fields = dict(x=graphene.Field(graphene.Int))
class Query(graphene.ObjectType):
myField = graphene.Field(graphene.String, input=graphene.Argument(MyInput))
def resolve_myField(parent, info, input):
return "ok"
def test_issue():
query_string = """
query myQuery {
myField(input: {x: 1})
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(query_string)
assert not result.errors

View File

@@ -0,0 +1,27 @@
import pickle
from ...types.enum import Enum
class PickleEnum(Enum):
# is defined outside of test because pickle unable to dump class inside ot pytest function
A = "a"
B = 1
def test_enums_pickling():
a = PickleEnum.A
pickled = pickle.dumps(a)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert a == restored
assert a.value == restored.value
assert a.name == restored.name
b = PickleEnum.B
pickled = pickle.dumps(b)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert b == restored
assert b.value == restored.value
assert b.name == restored.name

View File

@@ -0,0 +1,8 @@
import graphene
def test_issue():
options = {"description": "This my enum", "deprecation_reason": "For the funs"}
new_enum = graphene.Enum("MyEnum", [("some", "data")], **options)
assert new_enum._meta.description == options["description"]
assert new_enum._meta.deprecation_reason == options["deprecation_reason"]

View File

@@ -0,0 +1,53 @@
from graphql import GraphQLResolveInfo as ResolveInfo
from .argument import Argument
from .base64 import Base64
from .context import Context
from .datetime import Date, DateTime, Time
from .decimal import Decimal
from .dynamic import Dynamic
from .enum import Enum
from .field import Field
from .inputfield import InputField
from .inputobjecttype import InputObjectType
from .interface import Interface
from .json import JSONString
from .mutation import Mutation
from .objecttype import ObjectType
from .scalars import ID, BigInt, Boolean, Float, Int, Scalar, String
from .schema import Schema
from .structures import List, NonNull
from .union import Union
from .uuid import UUID
__all__ = [
"Argument",
"Base64",
"BigInt",
"Boolean",
"Context",
"Date",
"DateTime",
"Decimal",
"Dynamic",
"Enum",
"Field",
"Float",
"ID",
"InputField",
"InputObjectType",
"Int",
"Interface",
"JSONString",
"List",
"Mutation",
"NonNull",
"ObjectType",
"ResolveInfo",
"Scalar",
"Schema",
"String",
"Time",
"UUID",
"Union",
]

View File

@@ -0,0 +1,120 @@
from itertools import chain
from graphql import Undefined
from .dynamic import Dynamic
from .mountedtype import MountedType
from .structures import NonNull
from .utils import get_type
class Argument(MountedType):
"""
Makes an Argument available on a Field in the GraphQL schema.
Arguments will be parsed and provided to resolver methods for fields as keyword arguments.
All ``arg`` and ``**extra_args`` for a ``graphene.Field`` are implicitly mounted as Argument
using the below parameters.
.. code:: python
from graphene import String, Boolean, Argument
age = String(
# Boolean implicitly mounted as Argument
dog_years=Boolean(description="convert to dog years"),
# Boolean explicitly mounted as Argument
decades=Argument(Boolean, default_value=False),
)
args:
type (class for a graphene.UnmountedType): must be a class (not an instance) of an
unmounted graphene type (ex. scalar or object) which is used for the type of this
argument in the GraphQL schema.
required (optional, bool): indicates this argument as not null in the graphql schema. Same behavior
as graphene.NonNull. Default False.
name (optional, str): the name of the GraphQL argument. Defaults to parameter name.
description (optional, str): the description of the GraphQL argument in the schema.
default_value (optional, Any): The value to be provided if the user does not set this argument in
the operation.
deprecation_reason (optional, str): Setting this value indicates that the argument is
depreciated and may provide instruction or reason on how for clients to proceed. Cannot be
set if the argument is required (see spec).
"""
def __init__(
self,
type_,
default_value=Undefined,
deprecation_reason=None,
description=None,
name=None,
required=False,
_creation_counter=None,
):
super(Argument, self).__init__(_creation_counter=_creation_counter)
if required:
assert (
deprecation_reason is None
), f"Argument {name} is required, cannot deprecate it."
type_ = NonNull(type_)
self.name = name
self._type = type_
self.default_value = default_value
self.description = description
self.deprecation_reason = deprecation_reason
@property
def type(self):
return get_type(self._type)
def __eq__(self, other):
return isinstance(other, Argument) and (
self.name == other.name
and self.type == other.type
and self.default_value == other.default_value
and self.description == other.description
and self.deprecation_reason == other.deprecation_reason
)
def to_arguments(args, extra_args=None):
from .unmountedtype import UnmountedType
from .field import Field
from .inputfield import InputField
if extra_args:
extra_args = sorted(extra_args.items(), key=lambda f: f[1])
else:
extra_args = []
iter_arguments = chain(args.items(), extra_args)
arguments = {}
for default_name, arg in iter_arguments:
if isinstance(arg, Dynamic):
arg = arg.get_type()
if arg is None:
# If the Dynamic type returned None
# then we skip the Argument
continue
if isinstance(arg, UnmountedType):
arg = Argument.mounted(arg)
if isinstance(arg, (InputField, Field)):
raise ValueError(
f"Expected {default_name} to be Argument, "
f"but received {type(arg).__name__}. Try using Argument({arg.type})."
)
if not isinstance(arg, Argument):
raise ValueError(f'Unknown argument "{default_name}".')
arg_name = default_name or arg.name
assert (
arg_name not in arguments
), f'More than one Argument have same name "{arg_name}".'
arguments[arg_name] = arg
return arguments

View File

@@ -0,0 +1,48 @@
from typing import Type, Optional
from ..utils.subclass_with_meta import SubclassWithMeta, SubclassWithMeta_Meta
from ..utils.trim_docstring import trim_docstring
class BaseOptions:
name: Optional[str] = None
description: Optional[str] = None
_frozen: bool = False
def __init__(self, class_type: Type):
self.class_type: Type = class_type
def freeze(self):
self._frozen = True
def __setattr__(self, name, value):
if not self._frozen:
super(BaseOptions, self).__setattr__(name, value)
else:
raise Exception(f"Can't modify frozen Options {self}")
def __repr__(self):
return f"<{self.__class__.__name__} name={repr(self.name)}>"
BaseTypeMeta = SubclassWithMeta_Meta
class BaseType(SubclassWithMeta):
@classmethod
def create_type(cls, class_name, **options):
return type(class_name, (cls,), {"Meta": options})
@classmethod
def __init_subclass_with_meta__(
cls, name=None, description=None, _meta=None, **_kwargs
):
assert "_meta" not in cls.__dict__, "Can't assign meta directly"
if not _meta:
return
_meta.name = name or cls.__name__
_meta.description = description or trim_docstring(cls.__doc__)
_meta.freeze()
cls._meta = _meta
super(BaseType, cls).__init_subclass_with_meta__()

View File

@@ -0,0 +1,43 @@
from binascii import Error as _Error
from base64 import b64decode, b64encode
from graphql.error import GraphQLError
from graphql.language import StringValueNode, print_ast
from .scalars import Scalar
class Base64(Scalar):
"""
The `Base64` scalar type represents a base64-encoded String.
"""
@staticmethod
def serialize(value):
if not isinstance(value, bytes):
if isinstance(value, str):
value = value.encode("utf-8")
else:
value = str(value).encode("utf-8")
return b64encode(value).decode("utf-8")
@classmethod
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"Base64 cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
if not isinstance(value, bytes):
if not isinstance(value, str):
raise GraphQLError(
f"Base64 cannot represent non-string value: {repr(value)}"
)
value = value.encode("utf-8")
try:
return b64decode(value, validate=True).decode("utf-8")
except _Error:
raise GraphQLError(f"Base64 cannot decode value: {repr(value)}")

View File

@@ -0,0 +1,25 @@
class Context:
"""
Context can be used to make a convenient container for attributes to provide
for execution for resolvers of a GraphQL operation like a query.
.. code:: python
from graphene import Context
context = Context(loaders=build_dataloaders(), request=my_web_request)
schema.execute('{ hello(name: "world") }', context=context)
def resolve_hello(parent, info, name):
info.context.request # value set in Context
info.context.loaders # value set in Context
# ...
args:
**params (Dict[str, Any]): values to make available on Context instance as attributes.
"""
def __init__(self, **params):
for key, value in params.items():
setattr(self, key, value)

View File

@@ -0,0 +1,111 @@
import datetime
from dateutil.parser import isoparse
from graphql.error import GraphQLError
from graphql.language import StringValueNode, print_ast
from .scalars import Scalar
class Date(Scalar):
"""
The `Date` scalar type represents a Date
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
@staticmethod
def serialize(date):
if isinstance(date, datetime.datetime):
date = date.date()
if not isinstance(date, datetime.date):
raise GraphQLError(f"Date cannot represent value: {repr(date)}")
return date.isoformat()
@classmethod
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"Date cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
if isinstance(value, datetime.date):
return value
if not isinstance(value, str):
raise GraphQLError(f"Date cannot represent non-string value: {repr(value)}")
try:
return datetime.date.fromisoformat(value)
except ValueError:
raise GraphQLError(f"Date cannot represent value: {repr(value)}")
class DateTime(Scalar):
"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
@staticmethod
def serialize(dt):
if not isinstance(dt, (datetime.datetime, datetime.date)):
raise GraphQLError(f"DateTime cannot represent value: {repr(dt)}")
return dt.isoformat()
@classmethod
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"DateTime cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
if isinstance(value, datetime.datetime):
return value
if not isinstance(value, str):
raise GraphQLError(
f"DateTime cannot represent non-string value: {repr(value)}"
)
try:
return isoparse(value)
except ValueError:
raise GraphQLError(f"DateTime cannot represent value: {repr(value)}")
class Time(Scalar):
"""
The `Time` scalar type represents a Time value as
specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
@staticmethod
def serialize(time):
if not isinstance(time, datetime.time):
raise GraphQLError(f"Time cannot represent value: {repr(time)}")
return time.isoformat()
@classmethod
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"Time cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@classmethod
def parse_value(cls, value):
if isinstance(value, datetime.time):
return value
if not isinstance(value, str):
raise GraphQLError(f"Time cannot represent non-string value: {repr(value)}")
try:
return datetime.time.fromisoformat(value)
except ValueError:
raise GraphQLError(f"Time cannot represent value: {repr(value)}")

View File

@@ -0,0 +1,34 @@
from decimal import Decimal as _Decimal
from graphql import Undefined
from graphql.language.ast import StringValueNode, IntValueNode
from .scalars import Scalar
class Decimal(Scalar):
"""
The `Decimal` scalar type represents a python Decimal.
"""
@staticmethod
def serialize(dec):
if isinstance(dec, str):
dec = _Decimal(dec)
assert isinstance(
dec, _Decimal
), f'Received not compatible Decimal "{repr(dec)}"'
return str(dec)
@classmethod
def parse_literal(cls, node, _variables=None):
if isinstance(node, (StringValueNode, IntValueNode)):
return cls.parse_value(node.value)
return Undefined
@staticmethod
def parse_value(value):
try:
return _Decimal(value)
except Exception:
return Undefined

View File

@@ -0,0 +1,62 @@
from enum import Enum as PyEnum
from graphql import (
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLUnionType,
)
class GrapheneGraphQLType:
"""
A class for extending the base GraphQLType with the related
graphene_type
"""
def __init__(self, *args, **kwargs):
self.graphene_type = kwargs.pop("graphene_type")
super(GrapheneGraphQLType, self).__init__(*args, **kwargs)
def __copy__(self):
result = GrapheneGraphQLType(graphene_type=self.graphene_type)
result.__dict__.update(self.__dict__)
return result
class GrapheneInterfaceType(GrapheneGraphQLType, GraphQLInterfaceType):
pass
class GrapheneUnionType(GrapheneGraphQLType, GraphQLUnionType):
pass
class GrapheneObjectType(GrapheneGraphQLType, GraphQLObjectType):
pass
class GrapheneScalarType(GrapheneGraphQLType, GraphQLScalarType):
pass
class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType):
def serialize(self, value):
if not isinstance(value, PyEnum):
enum = self.graphene_type._meta.enum
try:
# Try and get enum by value
value = enum(value)
except ValueError:
# Try and get enum by name
try:
value = enum[value]
except KeyError:
pass
return super(GrapheneEnumType, self).serialize(value)
class GrapheneInputObjectType(GrapheneGraphQLType, GraphQLInputObjectType):
pass

View File

@@ -0,0 +1,22 @@
import inspect
from functools import partial
from .mountedtype import MountedType
class Dynamic(MountedType):
"""
A Dynamic Type let us get the type in runtime when we generate
the schema. So we can have lazy fields.
"""
def __init__(self, type_, with_schema=False, _creation_counter=None):
super(Dynamic, self).__init__(_creation_counter=_creation_counter)
assert inspect.isfunction(type_) or isinstance(type_, partial)
self.type = type_
self.with_schema = with_schema
def get_type(self, schema=None):
if schema and self.with_schema:
return self.type(schema=schema)
return self.type()

View File

@@ -0,0 +1,118 @@
from enum import Enum as PyEnum
from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta
from .base import BaseOptions, BaseType
from .unmountedtype import UnmountedType
def eq_enum(self, other):
if isinstance(other, self.__class__):
return self is other
return self.value is other
def hash_enum(self):
return hash(self.name)
EnumType = type(PyEnum)
class EnumOptions(BaseOptions):
enum = None # type: Enum
deprecation_reason = None
class EnumMeta(SubclassWithMeta_Meta):
def __new__(cls, name_, bases, classdict, **options):
enum_members = dict(classdict, __eq__=eq_enum, __hash__=hash_enum)
# We remove the Meta attribute from the class to not collide
# with the enum values.
enum_members.pop("Meta", None)
enum = PyEnum(cls.__name__, enum_members)
obj = SubclassWithMeta_Meta.__new__(
cls, name_, bases, dict(classdict, __enum__=enum), **options
)
globals()[name_] = obj.__enum__
return obj
def get(cls, value):
return cls._meta.enum(value)
def __getitem__(cls, value):
return cls._meta.enum[value]
def __prepare__(name, bases, **kwargs): # noqa: N805
return {}
def __call__(cls, *args, **kwargs): # noqa: N805
if cls is Enum:
description = kwargs.pop("description", None)
deprecation_reason = kwargs.pop("deprecation_reason", None)
return cls.from_enum(
PyEnum(*args, **kwargs),
description=description,
deprecation_reason=deprecation_reason,
)
return super(EnumMeta, cls).__call__(*args, **kwargs)
# return cls._meta.enum(*args, **kwargs)
def __iter__(cls):
return cls._meta.enum.__iter__()
def from_enum(cls, enum, name=None, description=None, deprecation_reason=None): # noqa: N805
name = name or enum.__name__
description = description or enum.__doc__ or "An enumeration."
meta_dict = {
"enum": enum,
"description": description,
"deprecation_reason": deprecation_reason,
}
meta_class = type("Meta", (object,), meta_dict)
return type(name, (Enum,), {"Meta": meta_class})
class Enum(UnmountedType, BaseType, metaclass=EnumMeta):
"""
Enum type definition
Defines a static set of values that can be provided as a Field, Argument or InputField.
.. code:: python
from graphene import Enum
class NameFormat(Enum):
FIRST_LAST = "first_last"
LAST_FIRST = "last_first"
Meta:
enum (optional, Enum): Python enum to use as a base for GraphQL Enum.
name (optional, str): Name of the GraphQL type (must be unique in schema). Defaults to class
name.
description (optional, str): Description of the GraphQL type in the schema. Defaults to class
docstring.
deprecation_reason (optional, str): Setting this value indicates that the enum is
depreciated and may provide instruction or reason on how for clients to proceed.
"""
@classmethod
def __init_subclass_with_meta__(cls, enum=None, _meta=None, **options):
if not _meta:
_meta = EnumOptions(cls)
_meta.enum = enum or cls.__enum__
_meta.deprecation_reason = options.pop("deprecation_reason", None)
for key, value in _meta.enum.__members__.items():
setattr(cls, key, value)
super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod
def get_type(cls):
"""
This function is called when the unmounted type (Enum instance)
is mounted (as a Field, InputField or Argument)
"""
return cls

View File

@@ -0,0 +1,139 @@
import inspect
from collections.abc import Mapping
from functools import partial
from .argument import Argument, to_arguments
from .mountedtype import MountedType
from .resolver import default_resolver
from .structures import NonNull
from .unmountedtype import UnmountedType
from .utils import get_type
from ..utils.deprecated import warn_deprecation
base_type = type
def source_resolver(source, root, info, **args):
resolved = default_resolver(source, None, root, info, **args)
if inspect.isfunction(resolved) or inspect.ismethod(resolved):
return resolved()
return resolved
class Field(MountedType):
"""
Makes a field available on an ObjectType in the GraphQL schema. Any type can be mounted as a
Field:
- Object Type
- Scalar Type
- Enum
- Interface
- Union
All class attributes of ``graphene.ObjectType`` are implicitly mounted as Field using the below
arguments.
.. code:: python
class Person(ObjectType):
first_name = graphene.String(required=True) # implicitly mounted as Field
last_name = graphene.Field(String, description='Surname') # explicitly mounted as Field
args:
type (class for a graphene.UnmountedType): Must be a class (not an instance) of an
unmounted graphene type (ex. scalar or object) which is used for the type of this
field in the GraphQL schema. You can provide a dotted module import path (string)
to the class instead of the class itself (e.g. to avoid circular import issues).
args (optional, Dict[str, graphene.Argument]): Arguments that can be input to the field.
Prefer to use ``**extra_args``, unless you use an argument name that clashes with one
of the Field arguments presented here (see :ref:`example<ResolverParamGraphQLArguments>`).
resolver (optional, Callable): A function to get the value for a Field from the parent
value object. If not set, the default resolver method for the schema is used.
source (optional, str): attribute name to resolve for this field from the parent value
object. Alternative to resolver (cannot set both source and resolver).
deprecation_reason (optional, str): Setting this value indicates that the field is
depreciated and may provide instruction or reason on how for clients to proceed.
required (optional, bool): indicates this field as not null in the graphql schema. Same behavior as
graphene.NonNull. Default False.
name (optional, str): the name of the GraphQL field (must be unique in a type). Defaults to attribute
name.
description (optional, str): the description of the GraphQL field in the schema.
default_value (optional, Any): Default value to resolve if none set from schema.
**extra_args (optional, Dict[str, Union[graphene.Argument, graphene.UnmountedType]): any
additional arguments to mount on the field.
"""
def __init__(
self,
type_,
args=None,
resolver=None,
source=None,
deprecation_reason=None,
name=None,
description=None,
required=False,
_creation_counter=None,
default_value=None,
**extra_args,
):
super(Field, self).__init__(_creation_counter=_creation_counter)
assert not args or isinstance(
args, Mapping
), f'Arguments in a field have to be a mapping, received "{args}".'
assert not (
source and resolver
), "A Field cannot have a source and a resolver in at the same time."
assert not callable(
default_value
), f'The default value can not be a function but received "{base_type(default_value)}".'
if required:
type_ = NonNull(type_)
# Check if name is actually an argument of the field
if isinstance(name, (Argument, UnmountedType)):
extra_args["name"] = name
name = None
# Check if source is actually an argument of the field
if isinstance(source, (Argument, UnmountedType)):
extra_args["source"] = source
source = None
self.name = name
self._type = type_
self.args = to_arguments(args or {}, extra_args)
if source:
resolver = partial(source_resolver, source)
self.resolver = resolver
self.deprecation_reason = deprecation_reason
self.description = description
self.default_value = default_value
@property
def type(self):
return get_type(self._type)
get_resolver = None
def wrap_resolve(self, parent_resolver):
"""
Wraps a function resolver, using the ObjectType resolve_{FIELD_NAME}
(parent_resolver) if the Field definition has no resolver.
"""
if self.get_resolver is not None:
warn_deprecation(
"The get_resolver method is being deprecated, please rename it to wrap_resolve."
)
return self.get_resolver(parent_resolver)
return self.resolver or parent_resolver
def wrap_subscribe(self, parent_subscribe):
"""
Wraps a function subscribe, using the ObjectType subscribe_{FIELD_NAME}
(parent_subscribe) if the Field definition has no subscribe.
"""
return parent_subscribe

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