From cbcaac66d0dec374082f619b8b060e87f8fd18a0 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 1 Jul 2018 11:29:45 +0100 Subject: [PATCH] Add deduplicator utility --- graphene/utils/deduplicator.py | 51 +++++ graphene/utils/tests/test_deduplicator.py | 257 ++++++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 graphene/utils/deduplicator.py create mode 100644 graphene/utils/tests/test_deduplicator.py diff --git a/graphene/utils/deduplicator.py b/graphene/utils/deduplicator.py new file mode 100644 index 00000000..e381c0dd --- /dev/null +++ b/graphene/utils/deduplicator.py @@ -0,0 +1,51 @@ +from collections import defaultdict, Mapping, OrderedDict + + +def nested_dict(): + return defaultdict(nested_dict) + + +def deflate(node, index=None, path=None): + if index is None: + index = {} + if path is None: + path = [] + + if node and 'id' in node and '__typename' in node: + route = ','.join(path) + + if ( + route in index and + node['__typename'] in index[route] and + index[route][node['__typename']].get(node['id']) + ): + return { + '__typename': node['__typename'], + 'id': node['id'], + } + else: + if route not in index: + index[route] = {} + + if node['__typename'] not in index[route]: + index[route][node['__typename']] = {} + + index[route][node['__typename']][node['id']] = True + + field_names = node.keys() + result = OrderedDict() + + for field_name in field_names: + value = node[field_name] + + new_path = path + [field_name] + if isinstance(value, (list, tuple)): + result[field_name] = [ + deflate(child, index, new_path) for child in value + ] + elif isinstance(value, Mapping): + result[field_name] = deflate(value, index, new_path) + else: + result[field_name] = value + + return result diff --git a/graphene/utils/tests/test_deduplicator.py b/graphene/utils/tests/test_deduplicator.py new file mode 100644 index 00000000..dd3b54a2 --- /dev/null +++ b/graphene/utils/tests/test_deduplicator.py @@ -0,0 +1,257 @@ +import datetime +import graphene +from graphene import relay +from graphene.types.resolver import dict_resolver + +from ..deduplicator import deflate + + +def test_does_not_modify_object_without_typename_and_id(): + response = { + 'foo': 'bar', + } + + deflated_response = deflate(response) + assert deflated_response == { + 'foo': 'bar', + } + + +def test_does_not_modify_first_instance_of_an_object(): + response = { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + } + ] + } + + deflated_response = deflate(response) + + assert deflated_response == { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1 + } + ] + } + + +def test_does_not_modify_first_instance_of_an_object_nested(): + response = { + 'data': [ + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'bar2': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'id': 1 + }, + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'bar2': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'id': 2 + } + ] + } + + deflated_response = deflate(response) + + assert deflated_response == { + 'data': [ + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'bar2': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'id': 1 + }, + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1 + }, + 'bar2': { + '__typename': 'bar', + 'id': 1 + }, + 'id': 2 + } + ] + } + + +def test_does_not_modify_input(): + response = { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + } + ] + } + + deflate(response) + + assert response == { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + } + ] + } + + +TEST_DATA = { + 'events': [ + { + 'id': '568', + 'date': datetime.date(2017, 5, 19), + 'movie': '1198359', + }, + { + 'id': '234', + 'date': datetime.date(2017, 5, 20), + 'movie': '1198359', + }, + ], + 'movies': { + '1198359': { + 'name': 'King Arthur: Legend of the Sword', + 'synopsis': ( + "When the child Arthur’s father is murdered, Vortigern, " + "Arthur’s uncle, seizes the crown. Robbed of his birthright and " + "with no idea who he truly is..." + ), + }, + }, +} + + +def test_example_end_to_end(): + class Movie(graphene.ObjectType): + class Meta: + interfaces = (relay.Node,) + default_resolver = dict_resolver + + name = graphene.String(required=True) + synopsis = graphene.String(required=True) + + class Event(graphene.ObjectType): + class Meta: + interfaces = (relay.Node,) + default_resolver = dict_resolver + + movie = graphene.Field(Movie, required=True) + date = graphene.types.datetime.Date(required=True) + + def resolve_movie(event, info): + return TEST_DATA['movies'][event['movie']] + + class Query(graphene.ObjectType): + events = graphene.List( + graphene.NonNull(Event), + required=True + ) + + def resolve_events(_, info): + return TEST_DATA['events'] + + schema = graphene.Schema(query=Query) + query = """\ + { + events { + __typename + id + date + movie { + __typename + id + name + synopsis + } + } + } + """ + result = schema.execute(query) + assert not result.errors + + result.data = deflate(result.data) + assert result.data == { + 'events': [ + { + '__typename': 'Event', + 'id': 'RXZlbnQ6NTY4', + 'date': '2017-05-19', + 'movie': { + '__typename': 'Movie', + 'id': 'TW92aWU6Tm9uZQ==', + 'name': 'King Arthur: Legend of the Sword', + 'synopsis': ( + "When the child Arthur’s father is murdered, Vortigern, " + "Arthur’s uncle, seizes the crown. Robbed of his birthright and " + "with no idea who he truly is..." + ), + }, + }, + { + '__typename': 'Event', + 'id': 'RXZlbnQ6MjM0', + 'date': '2017-05-20', + 'movie': { + '__typename': 'Movie', + 'id': 'TW92aWU6Tm9uZQ==', + }, + }, + ], + }