Skip to content

Commit 124d4f7

Browse files
committed
Add extensions in arrayconnection back again
1 parent 614ecf5 commit 124d4f7

12 files changed

+430
-82
lines changed

README.md

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,10 @@ returning those types.
6262
they return a connection type.
6363
- `connection_definitions` returns a `connection_type` and its associated
6464
`edgeType`, given a name and a node type.
65-
- `connection_from_list` is a helper method that takes an array and the
65+
- `connection_from_array` is a helper method that takes an array and the
6666
arguments from `connection_args`, does pagination and filtering, and returns
6767
an object in the shape expected by a `connection_type`'s `resolver` function.
68-
- `connection_from_promised_list` is similar to `connection_from_list`, but
69-
it takes a promise that resolves to an array, and returns a promise that
70-
resolves to the expected shape by `connection_type`.
68+
7169
- `cursor_for_object_in_connection` is a helper method that takes an array and a
7270
member object, and returns a cursor for use in the mutation payload.
7371

@@ -89,7 +87,7 @@ factionType = GraphQLObjectType(
8987
ship_connection,
9088
description='The ships used by the faction.',
9189
args=connection_args,
92-
resolve=lambda faction, _info, **args: connection_from_list(
90+
resolve=lambda faction, _info, **args: connection_from_array(
9391
[getShip(ship) for ship in faction.ships], args),
9492
)
9593
},
@@ -98,28 +96,28 @@ factionType = GraphQLObjectType(
9896
```
9997

10098
This shows adding a `ships` field to the `Faction` object that is a connection.
101-
It uses `connection_definitions({name: 'Ship', nodeType: shipType})` to create
102-
the connection type, adds `connection_args` as arguments on this function, and
103-
then implements the resolver function by passing the array of ships and the
104-
arguments to `connection_from_list`.
99+
It uses `connection_definitions('Ship', shipType)` to create the connection
100+
type, adds `connection_args` as arguments on this function, and then implements
101+
the resolver function by passing the array of ships and the arguments to
102+
`connection_from_array`.
105103

106104
### Object Identification
107105

108106
Helper functions are provided for both building the GraphQL types
109107
for nodes and for implementing global IDs around local IDs.
110108

111109
- `node_definitions` returns the `Node` interface that objects can implement,
112-
and returns the `node` root field to include on the query type. To implement
113-
this, it takes a function to resolve an ID to an object, and to determine
114-
the type of a given object.
110+
and returns the `node` root field to include on the query type.
111+
To implement this, it takes a function to resolve an ID to an object,
112+
and to determine the type of a given object.
115113
- `to_global_id` takes a type name and an ID specific to that type name,
116-
and returns a "global ID" that is unique among all types.
117-
- `from_global_id` takes the "global ID" created by `toGlobalID`, and returns
118-
the type name and ID used to create it.
114+
and returns a "global ID" that is unique among all types.
115+
- `from_global_id` takes the "global ID" created by `to_global_id`, and
116+
returns the type name and ID used to create it.
119117
- `global_id_field` creates the configuration for an `id` field on a node.
120118
- `plural_identifying_root_field` creates a field that accepts a list of
121-
non-ID identifiers (like a username) and maps then to their corresponding
122-
objects.
119+
non-ID identifiers (like a username) and maps then to their corresponding
120+
objects.
123121

124122
An example usage of these methods from the [test schema](tests/starwars/schema.py):
125123

@@ -184,7 +182,7 @@ class IntroduceShipMutation:
184182
self.factionId = factionId
185183
self.clientMutationId = clientMutationId
186184

187-
def mutate_and_get_payload(_info, shipName, factionId, **_input):
185+
def mutate_and_get_payload(_info, shipName, factionId):
188186
newShip = createShip(shipName, factionId)
189187
return IntroduceShipMutation(shipId=newShip.id, factionId=factionId)
190188

@@ -221,9 +219,9 @@ mutationType = GraphQLObjectType(
221219

222220
This code creates a mutation named `IntroduceShip`, which takes a faction
223221
ID and a ship name as input. It outputs the `Faction` and the `Ship` in
224-
question. `mutate_and_get_payload` then gets an object with a property for
225-
each input field, performs the mutation by constructing the new ship, then
226-
returns an object that will be resolved by the output fields.
222+
question. `mutate_and_get_payload` then gets each input field as keyword
223+
parameter, performs the mutation by constructing the new ship, then returns
224+
an object that will be resolved by the output fields.
227225

228226
Our mutation type then creates the `introduceShip` field using the return
229227
value of `mutation_with_client_mutation_id`.

src/graphql_relay/connection/arrayconnection.py

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import binascii
2-
from typing import Any, Collection, Optional, TypeVar
2+
from typing import Any, Optional, Sequence
33

44
from ..utils.base64 import base64, unbase64
55
from .connectiontypes import (
@@ -11,40 +11,65 @@
1111
'get_offset_with_default', 'offset_to_cursor'
1212
]
1313

14-
T = TypeVar('T')
15-
1614

1715
def connection_from_array(
18-
data: Collection, args: ConnectionArguments) -> Connection:
19-
"""Create a connection argument from a collection.
16+
data: Sequence, args: ConnectionArguments = None,
17+
connection_type: Any = Connection,
18+
edge_type: Any = Edge, page_info_type: Any = PageInfo) -> Connection:
19+
"""Create a connection object from a sequence of objects.
20+
21+
Note that different from its JavaScript counterpart which expects an array,
22+
this function accepts any kind of sliceable object with a length.
2023
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.
24+
Given this `data` object representing the result set, and connection arguments,
25+
this simple function returns a connection object for use in GraphQL. It uses
26+
offsets as pagination, so pagination will only work if the data is static.
27+
28+
The result will use the default types provided in the `connectiontypes` module
29+
if you don't pass custom types as arguments.
2430
"""
2531
return connection_from_array_slice(
2632
data, args,
2733
slice_start=0,
2834
array_length=len(data),
35+
connection_type=connection_type,
36+
edge_type=edge_type, page_info_type=page_info_type,
2937
)
3038

3139

3240
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.
41+
array_slice: Sequence, args: ConnectionArguments = None,
42+
slice_start: int = 0, array_length: int = None, array_slice_length: int = None,
43+
connection_type: Any = Connection,
44+
edge_type: Any = Edge, page_info_type: Any = PageInfo) -> Connection:
45+
"""Create a connection object from a slice of the result set.
46+
47+
Note that different from its JavaScript counterpart which expects an array,
48+
this function accepts any kind of sliceable object. This object represents
49+
a slice of the full result set. You need to pass the start position of the
50+
slice as `slice start` and the length of the full result set as `array_length`.
51+
If the `array_slice` does not have a length, you need to provide it separately
52+
in `array_slice_length` as well.
3753
3854
This function is similar to `connection_from_array`, but is intended for use
3955
cases where you know the cardinality of the connection, consider it too large
40-
to materialize the entire collection, and instead wish pass in a slice of the
41-
total result large enough to cover the range specified in `args`.
56+
to materialize the entire result set, and instead wish to pass in only a slice
57+
of the total result large enough to cover the range specified in `args`.
58+
59+
If you do not provide a `slice_start`, we assume that the slice starts at
60+
the beginning of the result set, and if you do not provide an `array_length`,
61+
we assume that the slice ends at the end of the result set.
4262
"""
63+
args = args or {}
4364
before = args.get('before')
4465
after = args.get('after')
4566
first = args.get('first')
4667
last = args.get('last')
47-
slice_end = slice_start + len(array_slice)
68+
if array_slice_length is None:
69+
array_slice_length = len(array_slice)
70+
slice_end = slice_start + array_slice_length
71+
if array_length is None:
72+
array_length = slice_end
4873
before_offset = get_offset_with_default(before, array_length)
4974
after_offset = get_offset_with_default(after, -1)
5075

@@ -64,12 +89,11 @@ def connection_from_array_slice(
6489

6590
# If supplied slice is too large, trim it down before mapping over it.
6691
trimmed_slice = array_slice[
67-
max(start_offset - slice_start, 0):
68-
len(array_slice) - (slice_end - end_offset)
92+
start_offset - slice_start:array_slice_length - (slice_end - end_offset)
6993
]
7094

7195
edges = [
72-
Edge(
96+
edge_type(
7397
node=value,
7498
cursor=offset_to_cursor(start_offset + index)
7599
)
@@ -81,9 +105,9 @@ def connection_from_array_slice(
81105
lower_bound = after_offset + 1 if after else 0
82106
upper_bound = before_offset if before else array_length
83107

84-
return Connection(
108+
return connection_type(
85109
edges=edges,
86-
pageInfo=PageInfo(
110+
pageInfo=page_info_type(
87111
startCursor=first_edge_cursor,
88112
endCursor=last_edge_cursor,
89113
hasPreviousPage=isinstance(last, int) and start_offset > lower_bound,
@@ -108,16 +132,27 @@ def cursor_to_offset(cursor: ConnectionCursor) -> Optional[int]:
108132
return None
109133

110134

111-
def cursor_for_object_in_connection(data: Collection, obj: T) -> Optional[Any]:
112-
"""Return the cursor associated with an object in a collection."""
135+
def cursor_for_object_in_connection(
136+
data: Sequence, obj: Any) -> Optional[ConnectionCursor]:
137+
"""Return the cursor associated with an object in a sequence.
138+
139+
This function uses the `index` method of the sequence if it exists,
140+
otherwise searches the object by iterating via the `__getitem__` method.
141+
"""
113142
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
143+
offset = data.index(obj)
144+
except AttributeError:
145+
# data does not have an index method
146+
offset = 0
147+
try:
148+
while True:
149+
if data[offset] == obj:
150+
break
151+
offset += 1
152+
except IndexError:
153+
return None
154+
else:
155+
return offset_to_cursor(offset)
121156
except ValueError:
122157
return None
123158
else:

src/graphql_relay/connection/connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def connection_definitions(
5959
resolve_cursor: GraphQLFieldResolver = None,
6060
edge_fields: Thunk[GraphQLFieldMap] = None,
6161
connection_fields: Thunk[GraphQLFieldMap] = None
62-
) -> GraphQLConnectionDefinitions:
62+
) -> GraphQLConnectionDefinitions:
6363
"""Return GraphQLObjectTypes for a connection with the given name.
6464
6565
The nodes of the returned object types will be of the specified type.

src/graphql_relay/connection/connectiontypes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Connection(NamedTuple):
3535
"""A type describing the arguments a connection field receives in GraphQL.
3636
3737
The following kinds of arguments are expected (all optional):
38-
38+
3939
before: ConnectionCursor
4040
after: ConnectionCursor
4141
first: int

src/graphql_relay/mutation/mutation.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from inspect import isawaitable
2-
from typing import Any, Callable, Dict
2+
from typing import Any, Callable
33

44
from graphql.error import GraphQLError
55
from graphql.pyutils import AwaitableOrValue, inspect
@@ -17,7 +17,14 @@
1717
Thunk
1818
)
1919

20-
MutationFn = Callable[[Dict[str, Any], GraphQLResolveInfo], AwaitableOrValue[Any]]
20+
21+
# Note: Contrary to the Javascript implementation of MutationFn,
22+
# the context is passed as part of the GraphQLResolveInfo and any arguments
23+
# are passed individually as keyword arguments.
24+
MutationFnWithoutArgs = Callable[[GraphQLResolveInfo], AwaitableOrValue[Any]]
25+
# Unfortunately there is currently no syntax to indicate optional or keyword
26+
# arguments in Python, so we also allow any other Callable as a workaround:
27+
MutationFn = Callable[..., AwaitableOrValue[Any]]
2128

2229

2330
def resolve_maybe_thunk(thing_or_thunk: Thunk) -> Any:
@@ -40,9 +47,10 @@ def mutation_with_client_mutation_id(
4047
An input object will be created containing the input fields, and an
4148
object will be created containing the output fields.
4249
43-
mutate_and_get_payload will receive a dict with a key for each input field,
44-
and it should return an object (or a dict) with an attribute (or a key)
45-
for each output field. It may return synchronously or asynchronously.
50+
mutate_and_get_payload will receive a GraphQLResolveInfo as first argument,
51+
and the input fields as keyword arguments, and it should return an object
52+
(or a dict) with an attribute (or a key) for each output field.
53+
It may return synchronously or asynchronously.
4654
"""
4755
def augmented_input_fields() -> GraphQLInputFieldMap:
4856
return dict(
@@ -66,7 +74,7 @@ def augmented_output_fields() -> GraphQLFieldMap:
6674

6775
# noinspection PyShadowingBuiltins
6876
async def resolve(_root, info, input):
69-
payload = mutate_and_get_payload(input, info)
77+
payload = mutate_and_get_payload(info, **input)
7078
if isawaitable(payload):
7179
payload = await payload
7280
try:

0 commit comments

Comments
 (0)