Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix Sequnce[Optional[UnionRef]] - make it work with Nothing type #160

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion hiku/denormalize/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,13 @@ def visit_link(self, obj: Link) -> None:
type_ref = type_.__item_type__.__type__
assert isinstance(type_ref, RefMeta)
self._type.append(get_type(self._types, type_ref))
items = []
items: t.List = []
for item in self._data[-1][obj.result_key]:
if item is None:
assert isinstance(type_.__item_type__, OptionalMeta)
items.append(None)
continue

self._res.append({})
self._data.append(item)
super().visit_link(obj)
Expand Down
25 changes: 21 additions & 4 deletions hiku/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,15 @@ def link_result_to_ids(
"{!r}".format(result)
)
return result
elif link_type is Many or link_type is MaybeMany:
elif link_type is Many:
result = list(chain.from_iterable(result))
if any(i is Nothing for i in result):
raise TypeError(
"Link of non-optional type should not contain Nothing: "
"{!r}".format(result)
)
return result
elif link_type is MaybeMany:
return list(chain.from_iterable(result))
else:
if link_type is Maybe:
Expand All @@ -588,7 +596,14 @@ def link_result_to_ids(
if result is Nothing:
raise TypeError("Non-optional link should not return Nothing")
return [result]
elif link_type is Many or link_type is MaybeMany:
elif link_type is Many:
if any(i is Nothing for i in result):
raise TypeError(
"Link of non-optional type should not contain Nothing: "
"{!r}".format(result)
)
return result
elif link_type is MaybeMany:
return result
raise TypeError(repr([from_list, link_type]))

Expand Down Expand Up @@ -770,8 +785,10 @@ def process_link(
LinkType.INTERFACE,
) and isinstance(to_ids, list):
grouped_ids = defaultdict(list)
for id_, type_ref in to_ids:
grouped_ids[type_ref.__type_name__].append(id_)
for ident in to_ids:
if ident is not Nothing:
id_, type_ref = ident
grouped_ids[type_ref.__type_name__].append(id_)

for type_name, type_ids in grouped_ids.items():
self.process_node(
Expand Down
4 changes: 2 additions & 2 deletions hiku/federation/denormalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from hiku.denormalize.graphql import DenormalizeGraphQL
from hiku.graph import Graph
from hiku.result import Proxy
from hiku.types import Record, Sequence, TypeRef
from hiku.types import Record, Sequence, TypeRef, Optional


class DenormalizeEntityGraphQL(DenormalizeGraphQL):
Expand All @@ -12,5 +12,5 @@ def __init__(
) -> None:
super().__init__(graph, result, "Query")
t.cast(Record, self._type[-1]).__field_types__["_entities"] = Sequence[
TypeRef[entity_type_name]
Optional[TypeRef[entity_type_name]]
]
20 changes: 16 additions & 4 deletions hiku/federation/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
Interface,
Link,
Node,
Nothing,
NothingType,
Option,
Root,
Union,
Expand Down Expand Up @@ -81,7 +83,7 @@ def visit_node(self, obj: Node) -> Node:
def entities_link(self) -> Link:
def entities_resolver(
options: t.Dict,
) -> t.List[t.Tuple[t.Any, t.Type[TypeRef]]]:
) -> t.List[t.Union[t.Tuple[t.Any, t.Type[TypeRef]], NothingType]]:
representations = options["representations"]
if not representations:
return []
Expand All @@ -94,9 +96,19 @@ def entities_resolver(
)

resolve_reference = self.type_to_resolve_reference_map[typ]
return [
(r, TypeRef[typ]) for r in resolve_reference(representations)
]
# TODO resolve_reference may be async!
references = resolve_reference(representations)

result: t.List[
t.Union[t.Tuple[t.Any, t.Type[TypeRef]], NothingType]
] = []
for ref in references:
if ref is Nothing:
result.append(Nothing)
else:
result.append((ref, TypeRef[typ]))

return result

def _asyncify(func: t.Callable) -> t.Callable:
async def wrapper(*args: t.Any, **kwargs: t.Any) -> t.List[t.Tuple]:
Expand Down
51 changes: 51 additions & 0 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from hiku.executors.sync import SyncExecutor
from hiku.graph import (Field, Graph, Interface, Link, Node, Nothing, Option,
Root, Union)
from hiku.readers.graphql import read
from hiku.result import denormalize
from hiku.sources.sqlalchemy import FieldsQuery
from hiku.types import (Boolean, Integer, InterfaceRef, Optional, Record, Sequence,
Expand Down Expand Up @@ -1283,6 +1284,56 @@ def get_fields(f, id_):
assert denormalize(graph, result) == {"aa": [{"a": 42}, None]}


def test_root_link_with_sequence_to_optional_union_ref():
def a_fields(fields, ids):
def get_fields(f, id_):
assert id_ is not None and id_ is not Nothing
if f.name == "a":
return 42
if f.name == "b":
return 24
raise AssertionError("Unexpected field: {}".format(f))

return [[get_fields(f, id_) for f in fields] for id_ in ids]

graph = Graph(
[
Node("A1", [Field("a", String, a_fields)]),
Node("A2", [Field("b", String, a_fields)]),
Root(
[
Link(
"aa",
Sequence[Optional[UnionRef["A"]]],
lambda: [
(1, TypeRef["A1"]),
Nothing
],
requires=None,
)
]
),
],
unions=[
Union('A', ['A1', 'A2']),
]
)

query = read("""
{ aa {
... on A1 { a }
... on A2 { b }
}
}
""")
result = execute(
graph,
query,
)

assert DenormalizeGraphQL(graph, result, "query").process(query) == {"aa": [{"a": 42}, None]}


# TODO: test typeref
def test_non_root_link_with_sequence_to_optional_type_ref():
def a_fields(fields, ids):
Expand Down
72 changes: 66 additions & 6 deletions tests/test_federation/test_engine.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import pytest
from hiku.graph import Graph
from hiku.graph import Graph, Nothing

from hiku.query import Node, Field, Link
import hiku.query
from hiku.executors.asyncio import AsyncIOExecutor
from hiku.federation.endpoint import denormalize_entities
from hiku.federation.engine import Engine
from hiku.federation.validate import validate
from hiku.executors.sync import SyncExecutor
from hiku.readers.graphql import read

from hiku.graph import (
Field,
Link,
Option,
Root,
)
from hiku.types import (
Integer,
TypeRef,
Optional,
)

from hiku.federation.graph import FederatedNode, Graph
from hiku.federation.directive import Key

from tests.test_federation.utils import (
GRAPH,
ASYNC_GRAPH,
data_types,
cart_resolver,
ids_resolver,
)


def execute(query: Node, graph: Graph, ctx=None):
def execute(query: hiku.query.Node, graph: Graph, ctx=None):
engine = Engine(SyncExecutor())
return engine.execute(graph, query, ctx=ctx)


async def execute_async(query: Node, graph: Graph, ctx=None):
async def execute_async(query: hiku.query.Node, graph: Graph, ctx=None):
engine = Engine(AsyncIOExecutor())
return await engine.execute(graph, query, ctx=ctx)

Expand All @@ -44,8 +62,8 @@ async def execute_async(query: Node, graph: Graph, ctx=None):
}


SDL_QUERY = Node(fields=[
Link('_service', Node(fields=[Field('sdl')]))
SDL_QUERY = hiku.query.Node(fields=[
hiku.query.Link('_service', hiku.query.Node(fields=[hiku.query.Field('sdl')]))
])


Expand Down Expand Up @@ -86,3 +104,45 @@ async def test_execute_async_executor():
{'status': {'id': 'ORDERED', 'title': 'ordered'}}
]
assert expect == data


def test_resolve_reference_optional():
"""Test when resolve_reference returns Nothing for one of the entities"""

def resolve_cart(representations):
# Lets assume that we found only one cart in storage, and the other one
# is missing so we return Nothing for it
ids = [r['id'] for r in representations]
return [ids[0], Nothing]

_GRAPH = Graph([
FederatedNode('Cart', [
Field('id', Integer, cart_resolver),
Field('status', TypeRef['Status'], cart_resolver),
], directives=[Key('id')], resolve_reference=resolve_cart),
Root([
Link(
'cart',
Optional[TypeRef['Cart']],
ids_resolver,
requires=None,
options=[
Option('id', Integer)
],
),
]),
], data_types=data_types)

query = read(ENTITIES_QUERY['query'], ENTITIES_QUERY['variables'])
result = execute(query, _GRAPH)
data = denormalize_entities(
_GRAPH,
query,
result,
)

expect = [
{'status': {'id': 'NEW', 'title': 'new'}},
None
]
assert expect == data
Loading