Skip to content

Commit 614ecf5

Browse files
committed
Bring connection subpackage up to date
1 parent 2f1078c commit 614ecf5

File tree

6 files changed

+849
-1028
lines changed

6 files changed

+849
-1028
lines changed

src/graphql_relay/__init__.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
from .version import version, version_info, version_js, version_info_js
2-
from .connection.connection import connection_args, connection_definitions
2+
from .connection.connectiontypes import (
3+
Connection, ConnectionArguments, ConnectionCursor, Edge, PageInfo
4+
)
5+
from .connection.connection import (
6+
backward_connection_args, connection_args, connection_definitions,
7+
forward_connection_args, GraphQLConnectionDefinitions)
38
from .connection.arrayconnection import (
4-
connection_from_list,
5-
connection_from_list_slice,
6-
cursor_for_object_in_connection)
9+
connection_from_array, connection_from_array_slice,
10+
cursor_for_object_in_connection,
11+
cursor_to_offset, get_offset_with_default, offset_to_cursor)
12+
from .mutation.mutation import mutation_with_client_mutation_id
713
from .node.node import (
814
node_definitions,
9-
from_global_id,
10-
to_global_id,
11-
global_id_field)
15+
from_global_id, global_id_field, to_global_id)
1216
from .node.plural import plural_identifying_root_field
13-
from .mutation.mutation import mutation_with_client_mutation_id
1417

1518
__version__ = version
1619
__version_info__ = version_info
@@ -20,17 +23,21 @@
2023
__all__ = [
2124
# The graphql-relay and graphql-relay-js version info
2225
'version', 'version_info', 'version_js', 'version_info_js',
26+
# Types for creating connection types in the schema
27+
'Connection', 'ConnectionArguments', 'ConnectionCursor', 'Edge', 'PageInfo',
2328
# Helpers for creating connection types in the schema
24-
'connection_args', 'connection_definitions',
29+
'backward_connection_args', 'connection_args', 'connection_definitions',
30+
'forward_connection_args', 'GraphQLConnectionDefinitions',
2531
# Helpers for creating connections from arrays
26-
'connection_from_list', 'connection_from_list_slice',
27-
'cursor_for_object_in_connection',
28-
# Helper for creating node definitions
29-
'node_definitions',
30-
# Utilities for creating global IDs in systems that don't have them
31-
'from_global_id', 'to_global_id', 'global_id_field',
32+
'connection_from_array', 'connection_from_array_slice',
33+
'cursor_for_object_in_connection', 'cursor_to_offset',
34+
'get_offset_with_default', 'offset_to_cursor',
3235
# Helper for creating mutations with client mutation IDs
3336
'mutation_with_client_mutation_id',
37+
# Helper for creating node definitions
38+
'node_definitions',
3439
# Helper for creating plural identifying root fields
35-
'plural_identifying_root_field'
40+
'plural_identifying_root_field',
41+
# Utilities for creating global IDs in systems that don't have them
42+
'from_global_id', 'global_id_field', 'to_global_id',
3643
]
Lines changed: 79 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,138 @@
11
import binascii
2+
from typing import Any, Collection, Optional, TypeVar
23

34
from ..utils.base64 import base64, unbase64
4-
from .connectiontypes import Connection, PageInfo, Edge
5+
from .connectiontypes import (
6+
Connection, ConnectionArguments, ConnectionCursor, Edge, PageInfo)
57

8+
__all__ = [
9+
'connection_from_array', 'connection_from_array_slice',
10+
'cursor_for_object_in_connection', 'cursor_to_offset',
11+
'get_offset_with_default', 'offset_to_cursor'
12+
]
613

7-
def connection_from_list(
8-
data, args=None,
9-
connection_type=None, edge_type=None, pageinfo_type=None):
10-
"""
11-
A simple function that accepts an array and connection arguments, and returns
12-
a connection object for use in GraphQL. It uses array offsets as pagination,
13-
so pagination will only work if the array is static.
14+
T = TypeVar('T')
15+
16+
17+
def connection_from_array(
18+
data: Collection, args: ConnectionArguments) -> Connection:
19+
"""Create a connection argument from a collection.
20+
21+
A simple function that accepts a collection (e.g. a Python list) and connection
22+
arguments, and returns a connection object for use in GraphQL. It uses array
23+
offsets as pagination, so pagination will only work if the array is static.
1424
"""
15-
_len = len(data)
16-
return connection_from_list_slice(
17-
data,
18-
args,
19-
connection_type=connection_type,
20-
edge_type=edge_type,
21-
pageinfo_type=pageinfo_type,
25+
return connection_from_array_slice(
26+
data, args,
2227
slice_start=0,
23-
list_length=_len,
24-
list_slice_length=_len,
28+
array_length=len(data),
2529
)
2630

2731

28-
def connection_from_list_slice(
29-
list_slice, args=None,
30-
connection_type=None, edge_type=None, pageinfo_type=None,
31-
slice_start=0, list_length=0, list_slice_length=None):
32-
"""
33-
Given a slice (subset) of an array, returns a connection object for use in
34-
GraphQL.
35-
This function is similar to `connectionFromArray`, but is intended for use
32+
def connection_from_array_slice(
33+
array_slice: Collection, args: ConnectionArguments,
34+
slice_start: int, array_length: int
35+
) -> Connection:
36+
"""Given a slice of a collection, returns a connection object for use in GraphQL.
37+
38+
This function is similar to `connection_from_array`, but is intended for use
3639
cases where you know the cardinality of the connection, consider it too large
37-
to materialize the entire array, and instead wish pass in a slice of the
40+
to materialize the entire collection, and instead wish pass in a slice of the
3841
total result large enough to cover the range specified in `args`.
3942
"""
40-
connection_type = connection_type or Connection
41-
edge_type = edge_type or Edge
42-
pageinfo_type = pageinfo_type or PageInfo
43-
44-
args = args or {}
45-
4643
before = args.get('before')
4744
after = args.get('after')
4845
first = args.get('first')
4946
last = args.get('last')
50-
if list_slice_length is None:
51-
list_slice_length = len(list_slice)
52-
slice_end = slice_start + list_slice_length
53-
before_offset = get_offset_with_default(before, list_length)
47+
slice_end = slice_start + len(array_slice)
48+
before_offset = get_offset_with_default(before, array_length)
5449
after_offset = get_offset_with_default(after, -1)
5550

56-
start_offset = max(
57-
slice_start - 1,
58-
after_offset,
59-
-1
60-
) + 1
61-
end_offset = min(
62-
slice_end,
63-
before_offset,
64-
list_length
65-
)
51+
start_offset = max(slice_start - 1, after_offset, -1) + 1
52+
end_offset = min(slice_end, before_offset, array_length)
53+
6654
if isinstance(first, int):
67-
end_offset = min(
68-
end_offset,
69-
start_offset + first
70-
)
55+
if first < 0:
56+
raise ValueError("Argument 'first' must be a non-negative integer.")
57+
58+
end_offset = min(end_offset, start_offset + first)
7159
if isinstance(last, int):
72-
start_offset = max(
73-
start_offset,
74-
end_offset - last
75-
)
60+
if last < 0:
61+
raise ValueError("Argument 'last' must be a non-negative integer.")
62+
63+
start_offset = max(start_offset, end_offset - last)
7664

7765
# If supplied slice is too large, trim it down before mapping over it.
78-
_slice = list_slice[
66+
trimmed_slice = array_slice[
7967
max(start_offset - slice_start, 0):
80-
list_slice_length - (slice_end - end_offset)
68+
len(array_slice) - (slice_end - end_offset)
8169
]
70+
8271
edges = [
83-
edge_type(
84-
node=node,
85-
cursor=offset_to_cursor(start_offset + i)
72+
Edge(
73+
node=value,
74+
cursor=offset_to_cursor(start_offset + index)
8675
)
87-
for i, node in enumerate(_slice)
76+
for index, value in enumerate(trimmed_slice)
8877
]
8978

9079
first_edge_cursor = edges[0].cursor if edges else None
9180
last_edge_cursor = edges[-1].cursor if edges else None
9281
lower_bound = after_offset + 1 if after else 0
93-
upper_bound = before_offset if before else list_length
82+
upper_bound = before_offset if before else array_length
9483

95-
return connection_type(
84+
return Connection(
9685
edges=edges,
97-
page_info=pageinfo_type(
98-
start_cursor=first_edge_cursor,
99-
end_cursor=last_edge_cursor,
100-
has_previous_page=isinstance(last, int) and start_offset > lower_bound,
101-
has_next_page=isinstance(first, int) and end_offset < upper_bound
86+
pageInfo=PageInfo(
87+
startCursor=first_edge_cursor,
88+
endCursor=last_edge_cursor,
89+
hasPreviousPage=isinstance(last, int) and start_offset > lower_bound,
90+
hasNextPage=isinstance(first, int) and end_offset < upper_bound
10291
)
10392
)
10493

10594

10695
PREFIX = 'arrayconnection:'
10796

10897

109-
def offset_to_cursor(offset):
110-
"""
111-
Creates the cursor string from an offset.
112-
"""
113-
return base64(PREFIX + str(offset))
98+
def offset_to_cursor(offset: int) -> ConnectionCursor:
99+
"""Create the cursor string from an offset."""
100+
return base64(f"{PREFIX}{offset}")
114101

115102

116-
def cursor_to_offset(cursor):
117-
"""
118-
Rederives the offset from the cursor string.
119-
"""
103+
def cursor_to_offset(cursor: ConnectionCursor) -> Optional[int]:
104+
"""Rederive the offset from the cursor string."""
120105
try:
121106
return int(unbase64(cursor)[len(PREFIX):])
122107
except binascii.Error:
123108
return None
124109

125110

126-
def cursor_for_object_in_connection(data, _object):
127-
"""
128-
Return the cursor associated with an object in an array.
129-
"""
130-
if _object not in data:
111+
def cursor_for_object_in_connection(data: Collection, obj: T) -> Optional[Any]:
112+
"""Return the cursor associated with an object in a collection."""
113+
try:
114+
# noinspection PyUnresolvedReferences
115+
offset = data.index(obj) # type: ignore
116+
except AttributeError: # collection does not have an index method
117+
for offset, value in data:
118+
if value == obj:
119+
return offset
120+
return None
121+
except ValueError:
131122
return None
123+
else:
124+
return offset_to_cursor(offset)
132125

133-
offset = data.index(_object)
134-
return offset_to_cursor(offset)
135126

127+
def get_offset_with_default(cursor: ConnectionCursor = None, default_offset=0) -> int:
128+
"""Get offset from a given cursor and a default.
136129
137-
def get_offset_with_default(cursor=None, default_offset=0):
138-
"""
139-
Given an optional cursor and a default offset, returns the offset
140-
to use; if the cursor contains a valid offset, that will be used,
130+
Given an optional cursor and a default offset, return the offset to use;
131+
if the cursor contains a valid offset, that will be used,
141132
otherwise it will be the default.
142133
"""
143134
if not isinstance(cursor, str):
144135
return default_offset
145136

146137
offset = cursor_to_offset(cursor)
147-
try:
148-
return int(offset)
149-
except (TypeError, ValueError):
150-
return default_offset
138+
return default_offset if offset is None else offset

src/graphql_relay/connection/connection.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
from typing import Any
1+
from typing import Any, NamedTuple
22

33
from graphql.type import (
44
GraphQLArgument,
5+
GraphQLArgumentMap,
56
GraphQLBoolean,
67
GraphQLField,
8+
GraphQLFieldMap,
9+
GraphQLFieldResolver,
710
GraphQLInt,
811
GraphQLList,
912
GraphQLNonNull,
@@ -12,24 +15,59 @@
1215
Thunk
1316
)
1417

15-
connection_args = {
16-
'before': GraphQLArgument(GraphQLString),
18+
__all__ = [
19+
'connection_definitions',
20+
'forward_connection_args', 'backward_connection_args', 'connection_args',
21+
'GraphQLConnectionDefinitions'
22+
]
23+
24+
25+
# Returns a GraphQLArgumentMap appropriate to include on a field
26+
# whose return type is a connection type with forward pagination.
27+
forward_connection_args: GraphQLArgumentMap = {
1728
'after': GraphQLArgument(GraphQLString),
1829
'first': GraphQLArgument(GraphQLInt),
30+
}
31+
32+
# Returns a GraphQLArgumentMap appropriate to include on a field
33+
# whose return type is a connection type with backward pagination.
34+
backward_connection_args: GraphQLArgumentMap = {
35+
'before': GraphQLArgument(GraphQLString),
1936
'last': GraphQLArgument(GraphQLInt),
2037
}
2138

39+
# Returns a GraphQLArgumentMap appropriate to include on a field
40+
# whose return type is a connection type with bidirectional pagination.
41+
connection_args = {
42+
**forward_connection_args, **backward_connection_args
43+
}
44+
45+
46+
class GraphQLConnectionDefinitions(NamedTuple):
47+
edge_type: GraphQLObjectType
48+
connection_type: GraphQLObjectType
49+
2250

2351
def resolve_maybe_thunk(thing_or_thunk: Thunk) -> Any:
2452
return thing_or_thunk() if callable(thing_or_thunk) else thing_or_thunk
2553

2654

2755
def connection_definitions(
28-
name, node_type,
29-
resolve_node=None, resolve_cursor=None,
30-
edge_fields=None, connection_fields=None):
56+
node_type: GraphQLObjectType,
57+
name: str = None,
58+
resolve_node: GraphQLFieldResolver = None,
59+
resolve_cursor: GraphQLFieldResolver = None,
60+
edge_fields: Thunk[GraphQLFieldMap] = None,
61+
connection_fields: Thunk[GraphQLFieldMap] = None
62+
) -> GraphQLConnectionDefinitions:
63+
"""Return GraphQLObjectTypes for a connection with the given name.
64+
65+
The nodes of the returned object types will be of the specified type.
66+
"""
67+
name = name or node_type.name
3168
edge_fields = edge_fields or {}
3269
connection_fields = connection_fields or {}
70+
3371
edge_type = GraphQLObjectType(
3472
name + 'Edge',
3573
description='An edge in a connection.',
@@ -60,7 +98,7 @@ def connection_definitions(
6098
),
6199
**resolve_maybe_thunk(connection_fields)})
62100

63-
return edge_type, connection_type
101+
return GraphQLConnectionDefinitions(edge_type, connection_type)
64102

65103

66104
# The common page info type used by all connections.

0 commit comments

Comments
 (0)