From cbcaac66d0dec374082f619b8b060e87f8fd18a0 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 1 Jul 2018 11:29:45 +0100 Subject: [PATCH 1/3] 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==', + }, + }, + ], + } From 56000394c4746246efd1a2846310f993c53eda8a Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 1 Jul 2018 11:32:16 +0100 Subject: [PATCH 2/3] Simplify code --- graphene/utils/deduplicator.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/graphene/utils/deduplicator.py b/graphene/utils/deduplicator.py index e381c0dd..fd5682b1 100644 --- a/graphene/utils/deduplicator.py +++ b/graphene/utils/deduplicator.py @@ -1,8 +1,4 @@ -from collections import defaultdict, Mapping, OrderedDict - - -def nested_dict(): - return defaultdict(nested_dict) +from collections import Mapping, OrderedDict def deflate(node, index=None, path=None): @@ -13,24 +9,15 @@ def deflate(node, index=None, path=None): if node and 'id' in node and '__typename' in node: route = ','.join(path) + cache_key = ':'.join([route, str(node['__typename']), str(node['id'])]) - if ( - route in index and - node['__typename'] in index[route] and - index[route][node['__typename']].get(node['id']) - ): + if index.get(cache_key) is True: 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 + index[cache_key] = True field_names = node.keys() result = OrderedDict() From 9ce78e32a5db92022dfe4505a201d3d1c462e445 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 1 Jul 2018 20:52:08 +0100 Subject: [PATCH 3/3] Remove utf-8 characters --- graphene/utils/tests/test_deduplicator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphene/utils/tests/test_deduplicator.py b/graphene/utils/tests/test_deduplicator.py index dd3b54a2..d73fbfbe 100644 --- a/graphene/utils/tests/test_deduplicator.py +++ b/graphene/utils/tests/test_deduplicator.py @@ -169,8 +169,8 @@ TEST_DATA = { '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 " + "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..." ), }, @@ -238,8 +238,8 @@ def test_example_end_to_end(): '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 " + "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..." ), },