Skip to content

Commit d33e38a

Browse files
authored
chore: make relay type fields extendable (#1499)
1 parent b76e89c commit d33e38a

File tree

2 files changed

+155
-29
lines changed

2 files changed

+155
-29
lines changed

graphene/relay/connection.py

+41-28
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
import re
22
from collections.abc import Iterable
33
from functools import partial
4+
from typing import Type
45

56
from graphql_relay import connection_from_array
67

78
from ..types import Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union
89
from ..types.field import Field
910
from ..types.objecttype import ObjectType, ObjectTypeOptions
1011
from ..utils.thenables import maybe_thenable
11-
from .node import is_node
12+
from .node import is_node, AbstractNode
13+
14+
15+
def get_edge_class(
16+
connection_class: Type["Connection"], _node: Type[AbstractNode], base_name: str
17+
):
18+
edge_class = getattr(connection_class, "Edge", None)
19+
20+
class EdgeBase:
21+
node = Field(_node, description="The item at the end of the edge")
22+
cursor = String(required=True, description="A cursor for use in pagination")
23+
24+
class EdgeMeta:
25+
description = f"A Relay edge containing a `{base_name}` and its cursor."
26+
27+
edge_name = f"{base_name}Edge"
28+
29+
edge_bases = [edge_class, EdgeBase] if edge_class else [EdgeBase]
30+
if not isinstance(edge_class, ObjectType):
31+
edge_bases = [*edge_bases, ObjectType]
32+
33+
return type(edge_name, tuple(edge_bases), {"Meta": EdgeMeta})
1234

1335

1436
class PageInfo(ObjectType):
@@ -61,8 +83,9 @@ class Meta:
6183
abstract = True
6284

6385
@classmethod
64-
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
65-
_meta = ConnectionOptions(cls)
86+
def __init_subclass_with_meta__(cls, node=None, name=None, _meta=None, **options):
87+
if not _meta:
88+
_meta = ConnectionOptions(cls)
6689
assert node, f"You have to provide a node in {cls.__name__}.Meta"
6790
assert isinstance(node, NonNull) or issubclass(
6891
node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)
@@ -72,39 +95,29 @@ def __init_subclass_with_meta__(cls, node=None, name=None, **options):
7295
if not name:
7396
name = f"{base_name}Connection"
7497

75-
edge_class = getattr(cls, "Edge", None)
76-
_node = node
77-
78-
class EdgeBase:
79-
node = Field(_node, description="The item at the end of the edge")
80-
cursor = String(required=True, description="A cursor for use in pagination")
81-
82-
class EdgeMeta:
83-
description = f"A Relay edge containing a `{base_name}` and its cursor."
98+
options["name"] = name
8499

85-
edge_name = f"{base_name}Edge"
86-
if edge_class:
87-
edge_bases = (edge_class, EdgeBase, ObjectType)
88-
else:
89-
edge_bases = (EdgeBase, ObjectType)
100+
_meta.node = node
90101

91-
edge = type(edge_name, edge_bases, {"Meta": EdgeMeta})
92-
cls.Edge = edge
102+
if not _meta.fields:
103+
_meta.fields = {}
93104

94-
options["name"] = name
95-
_meta.node = node
96-
_meta.fields = {
97-
"page_info": Field(
105+
if "page_info" not in _meta.fields:
106+
_meta.fields["page_info"] = Field(
98107
PageInfo,
99108
name="pageInfo",
100109
required=True,
101110
description="Pagination data for this connection.",
102-
),
103-
"edges": Field(
104-
NonNull(List(edge)),
111+
)
112+
113+
if "edges" not in _meta.fields:
114+
edge_class = get_edge_class(cls, node, base_name) # type: ignore
115+
cls.Edge = edge_class
116+
_meta.fields["edges"] = Field(
117+
NonNull(List(edge_class)),
105118
description="Contains the nodes in this connection.",
106-
),
107-
}
119+
)
120+
108121
return super(Connection, cls).__init_subclass_with_meta__(
109122
_meta=_meta, **options
110123
)

graphene/relay/tests/test_connection.py

+114-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
import re
2+
13
from pytest import raises
24

35
from ...types import Argument, Field, Int, List, NonNull, ObjectType, Schema, String
4-
from ..connection import Connection, ConnectionField, PageInfo
6+
from ..connection import (
7+
Connection,
8+
ConnectionField,
9+
PageInfo,
10+
ConnectionOptions,
11+
get_edge_class,
12+
)
513
from ..node import Node
614

715

@@ -51,6 +59,111 @@ class Meta:
5159
assert list(fields) == ["page_info", "edges", "extra"]
5260

5361

62+
def test_connection_extra_abstract_fields():
63+
class ConnectionWithNodes(Connection):
64+
class Meta:
65+
abstract = True
66+
67+
@classmethod
68+
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
69+
_meta = ConnectionOptions(cls)
70+
71+
_meta.fields = {
72+
"nodes": Field(
73+
NonNull(List(node)),
74+
description="Contains all the nodes in this connection.",
75+
),
76+
}
77+
78+
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
79+
node=node, name=name, _meta=_meta, **options
80+
)
81+
82+
class MyObjectConnection(ConnectionWithNodes):
83+
class Meta:
84+
node = MyObject
85+
86+
class Edge:
87+
other = String()
88+
89+
assert MyObjectConnection._meta.name == "MyObjectConnection"
90+
fields = MyObjectConnection._meta.fields
91+
assert list(fields) == ["nodes", "page_info", "edges"]
92+
edge_field = fields["edges"]
93+
pageinfo_field = fields["page_info"]
94+
nodes_field = fields["nodes"]
95+
96+
assert isinstance(edge_field, Field)
97+
assert isinstance(edge_field.type, NonNull)
98+
assert isinstance(edge_field.type.of_type, List)
99+
assert edge_field.type.of_type.of_type == MyObjectConnection.Edge
100+
101+
assert isinstance(pageinfo_field, Field)
102+
assert isinstance(pageinfo_field.type, NonNull)
103+
assert pageinfo_field.type.of_type == PageInfo
104+
105+
assert isinstance(nodes_field, Field)
106+
assert isinstance(nodes_field.type, NonNull)
107+
assert isinstance(nodes_field.type.of_type, List)
108+
assert nodes_field.type.of_type.of_type == MyObject
109+
110+
111+
def test_connection_override_fields():
112+
class ConnectionWithNodes(Connection):
113+
class Meta:
114+
abstract = True
115+
116+
@classmethod
117+
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
118+
_meta = ConnectionOptions(cls)
119+
base_name = (
120+
re.sub("Connection$", "", name or cls.__name__) or node._meta.name
121+
)
122+
123+
edge_class = get_edge_class(cls, node, base_name)
124+
125+
_meta.fields = {
126+
"page_info": Field(
127+
NonNull(
128+
PageInfo,
129+
name="pageInfo",
130+
required=True,
131+
description="Pagination data for this connection.",
132+
)
133+
),
134+
"edges": Field(
135+
NonNull(List(NonNull(edge_class))),
136+
description="Contains the nodes in this connection.",
137+
),
138+
}
139+
140+
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
141+
node=node, name=name, _meta=_meta, **options
142+
)
143+
144+
class MyObjectConnection(ConnectionWithNodes):
145+
class Meta:
146+
node = MyObject
147+
148+
assert MyObjectConnection._meta.name == "MyObjectConnection"
149+
fields = MyObjectConnection._meta.fields
150+
assert list(fields) == ["page_info", "edges"]
151+
edge_field = fields["edges"]
152+
pageinfo_field = fields["page_info"]
153+
154+
assert isinstance(edge_field, Field)
155+
assert isinstance(edge_field.type, NonNull)
156+
assert isinstance(edge_field.type.of_type, List)
157+
assert isinstance(edge_field.type.of_type.of_type, NonNull)
158+
159+
assert edge_field.type.of_type.of_type.of_type.__name__ == "MyObjectEdge"
160+
161+
# This page info is NonNull
162+
assert isinstance(pageinfo_field, Field)
163+
assert isinstance(edge_field.type, NonNull)
164+
assert pageinfo_field.type.of_type == PageInfo
165+
166+
54167
def test_connection_name():
55168
custom_name = "MyObjectCustomNameConnection"
56169

0 commit comments

Comments
 (0)