Skip to content

Commit

Permalink
chore: make relay type fields extendable (#1499)
Browse files Browse the repository at this point in the history
  • Loading branch information
erikwrede authored Mar 13, 2023
1 parent b76e89c commit d33e38a
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 29 deletions.
69 changes: 41 additions & 28 deletions graphene/relay/connection.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
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
from .node import is_node, AbstractNode


def get_edge_class(
connection_class: Type["Connection"], _node: Type[AbstractNode], base_name: str
):
edge_class = getattr(connection_class, "Edge", None)

class EdgeBase:
node = Field(_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):
Expand Down Expand Up @@ -61,8 +83,9 @@ class Meta:
abstract = True

@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
def __init_subclass_with_meta__(cls, node=None, name=None, _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)
Expand All @@ -72,39 +95,29 @@ def __init_subclass_with_meta__(cls, node=None, name=None, **options):
if not name:
name = f"{base_name}Connection"

edge_class = getattr(cls, "Edge", None)
_node = node

class EdgeBase:
node = Field(_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."
options["name"] = name

edge_name = f"{base_name}Edge"
if edge_class:
edge_bases = (edge_class, EdgeBase, ObjectType)
else:
edge_bases = (EdgeBase, ObjectType)
_meta.node = node

edge = type(edge_name, edge_bases, {"Meta": EdgeMeta})
cls.Edge = edge
if not _meta.fields:
_meta.fields = {}

options["name"] = name
_meta.node = node
_meta.fields = {
"page_info": Field(
if "page_info" not in _meta.fields:
_meta.fields["page_info"] = Field(
PageInfo,
name="pageInfo",
required=True,
description="Pagination data for this connection.",
),
"edges": Field(
NonNull(List(edge)),
)

if "edges" not in _meta.fields:
edge_class = get_edge_class(cls, node, base_name) # type: ignore
cls.Edge = edge_class
_meta.fields["edges"] = Field(
NonNull(List(edge_class)),
description="Contains the nodes in this connection.",
),
}
)

return super(Connection, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
Expand Down
115 changes: 114 additions & 1 deletion graphene/relay/tests/test_connection.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import re

from pytest import raises

from ...types import Argument, Field, Int, List, NonNull, ObjectType, Schema, String
from ..connection import Connection, ConnectionField, PageInfo
from ..connection import (
Connection,
ConnectionField,
PageInfo,
ConnectionOptions,
get_edge_class,
)
from ..node import Node


Expand Down Expand Up @@ -51,6 +59,111 @@ class Meta:
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"

Expand Down

0 comments on commit d33e38a

Please sign in to comment.