From ed9799a7cfe95f26f6f26cf0cfc2e252631d2d13 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Thu, 27 Feb 2020 21:40:45 -0500 Subject: [PATCH 1/8] chore: display missing lines on cov report --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 89303f01..3f56bc77 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = -e.[test] ; Prevent installing issues: https://github.com/ContinuumIO/anaconda-issues/issues/542 commands = pip install -U setuptools - pytest --cov=gql tests {posargs} + pytest --cov-report=term-missing --cov=gql tests {posargs} [testenv:flake8] basepython = python3.8 From 4b8723132c006cdcb4801943f568e12b25f6013b Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Thu, 27 Feb 2020 23:38:10 -0500 Subject: [PATCH 2/8] test: http transport raise for status error --- tests/test_client.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 2d5bdfc1..9d413e5f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -64,3 +64,28 @@ def test_execute_result_error(): with pytest.raises(Exception) as excInfo: client.execute(query) assert "Cannot query field \"id\" on type \"Continent\"." in str(excInfo.value) + + +def test_http_transport_raise_for_status_error(): + client = Client( + transport=RequestsHTTPTransport( + url='https://countries.trevorblades.com/', + use_json=False, + headers={ + "Content-type": "application/json", + } + ) + ) + + query = gql(''' + query getContinents { + continents { + code + name + id + } + } + ''') + with pytest.raises(Exception) as excInfo: + client.execute(query) + assert "400 Client Error: Bad Request for url" in str(excInfo.value) From cca92008da9bb1658c0a6fef13a0f248dfcd9e0c Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Thu, 27 Feb 2020 23:41:07 -0500 Subject: [PATCH 3/8] refactor: remove unused var function on dsl --- gql/dsl.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index fd932d91..4420ec59 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -151,7 +151,3 @@ def get_arg_serializer(arg_type): if isinstance(arg_type, GraphQLEnumType): return lambda value: ast.EnumValue(value=arg_type.serialize(value)) return arg_type.serialize - - -def var(name): - return ast.Variable(name=name) From 8ffd8677360a6afe0e6d7dd5b40046965ac37e69 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 28 Feb 2020 00:04:53 -0500 Subject: [PATCH 4/8] fix: support mutation on dsl --- gql/dsl.py | 14 ++++++++------ tests/starwars/fixtures.py | 13 ++++++++++++- tests/starwars/schema.py | 32 ++++++++++++++++++++++++++++---- tests/starwars/test_dsl.py | 16 ++++++++++++++++ 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index 4420ec59..bb04bdae 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -4,8 +4,8 @@ import six from graphql.language import ast from graphql.language.printer import print_ast -from graphql.type import (GraphQLField, GraphQLList, - GraphQLNonNull, GraphQLEnumType) +from graphql.type import (GraphQLField, GraphQLList, GraphQLNonNull, GraphQLEnumType) +from graphql.utils.ast_from_value import ast_from_value from .utils import to_camel_case @@ -31,7 +31,7 @@ def query(self, *args, **kwargs): return self.execute(query(*args, **kwargs)) def mutate(self, *args, **kwargs): - return self.query(*args, operation='mutate', **kwargs) + return self.query(*args, operation='mutation', **kwargs) def execute(self, document): return self.client.execute(document) @@ -126,10 +126,12 @@ def field(field, **args): raise Exception('Received incompatible query field: "{}".'.format(field)) -def query(*fields): +def query(*fields, **kwargs): + if 'operation' not in kwargs: + kwargs['operation'] = 'query' return ast.Document( definitions=[ast.OperationDefinition( - operation='query', + operation=kwargs['operation'], selection_set=ast.SelectionSet( selections=list(selections(*fields)) ) @@ -150,4 +152,4 @@ def get_arg_serializer(arg_type): return partial(serialize_list, inner_serializer) if isinstance(arg_type, GraphQLEnumType): return lambda value: ast.EnumValue(value=arg_type.serialize(value)) - return arg_type.serialize + return lambda value: ast_from_value(arg_type.serialize(value)) diff --git a/tests/starwars/fixtures.py b/tests/starwars/fixtures.py index 51f29a59..a8ea8548 100644 --- a/tests/starwars/fixtures.py +++ b/tests/starwars/fixtures.py @@ -1,6 +1,6 @@ from collections import namedtuple -Human = namedtuple('Human', 'id name friends appearsIn homePlanet') +Human = namedtuple('Human', 'id name friends appearsIn homePlanet isAlive') luke = Human( id='1000', @@ -8,6 +8,7 @@ friends=['1002', '1003', '2000', '2001'], appearsIn=[4, 5, 6], homePlanet='Tatooine', + isAlive=True, ) vader = Human( @@ -16,6 +17,7 @@ friends=['1004'], appearsIn=[4, 5, 6], homePlanet='Tatooine', + isAlive=False, ) han = Human( @@ -24,6 +26,7 @@ friends=['1000', '1003', '2001'], appearsIn=[4, 5, 6], homePlanet=None, + isAlive=True, ) leia = Human( @@ -32,6 +35,7 @@ friends=['1000', '1002', '2000', '2001'], appearsIn=[4, 5, 6], homePlanet='Alderaan', + isAlive=True, ) tarkin = Human( @@ -40,6 +44,7 @@ friends=['1001'], appearsIn=[4], homePlanet=None, + isAlive=False, ) humanData = { @@ -94,3 +99,9 @@ def getHuman(id): def getDroid(id): return droidData.get(id) + + +def updateHumanAlive(id, status): + human = humanData.get(id) + human = human._replace(isAlive=status) + return human diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index 655f2871..afda634c 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -1,9 +1,9 @@ from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, GraphQLField, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, - GraphQLString) + GraphQLString, GraphQLBoolean) -from .fixtures import getDroid, getFriends, getHero, getHuman +from .fixtures import getDroid, getFriends, getHero, getHuman, updateHumanAlive episodeEnum = GraphQLEnumType( 'Episode', @@ -72,7 +72,11 @@ 'homePlanet': GraphQLField( GraphQLString, description='The home planet of the human, or null if unknown.', - ) + ), + 'isAlive': GraphQLField( + GraphQLBoolean, + description='The human is still alive.' + ), }, interfaces=[characterInterface] ) @@ -143,4 +147,24 @@ } ) -StarWarsSchema = GraphQLSchema(query=queryType, types=[humanType, droidType]) +mutationType = GraphQLObjectType( + 'Mutation', + fields=lambda: { + 'updateHumanAliveStatus': GraphQLField( + humanType, + args={ + 'id': GraphQLArgument( + description='id of the human', + type=GraphQLNonNull(GraphQLString), + ), + 'status': GraphQLArgument( + description='set alive status', + type=GraphQLNonNull(GraphQLBoolean), + ), + }, + resolver=lambda root, info, **args: updateHumanAlive(args['id'], args['status']), + ), + } +) + +StarWarsSchema = GraphQLSchema(query=queryType, mutation=mutationType, types=[humanType, droidType]) diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index e901009e..0c8fe8b1 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -122,3 +122,19 @@ def test_hero_name_query_result(ds): } } assert result == expected + + +def test_human_alive_mutation_result(ds): + result = ds.mutate( + ds.Mutation.updateHumanAliveStatus.args(id=1004, status=True).select( + ds.Human.name, + ds.Human.isAlive + ) + ) + expected = { + 'updateHumanAliveStatus': { + 'name': 'Wilhuff Tarkin', + 'isAlive': True + } + } + assert result == expected From d24a13d98cd362849085e67763245ed7b8a96fb1 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 28 Feb 2020 23:23:54 -0500 Subject: [PATCH 5/8] chore: add coveralls to dev dependencies --- .travis.yml | 5 ++--- setup.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index ab35bd1c..45d6bcaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,11 +11,10 @@ python: - pypy3 cache: pip install: - - pip install -e .[test] - - pip install flake8==3.7.9 + - pip install -e .[dev] script: - flake8 gql tests - - pytest --cov=gql tests + - pytest --cov-report=term-missing --cov=gql tests after_success: - coveralls deploy: diff --git a/setup.py b/setup.py index 14f97ab8..62d09e29 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ ] tests_require = [ + 'coveralls==1.11.1', 'pytest==4.6.9', 'pytest-cov==2.8.1', 'mock==3.0.5', From abfe9995a89d79531cb61b24c1a47a84a8746f47 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Sun, 1 Mar 2020 01:15:06 -0500 Subject: [PATCH 6/8] test: arg serializer for list and enum --- gql/dsl.py | 21 +++++++++++---------- tests/starwars/fixtures.py | 4 ++++ tests/starwars/schema.py | 12 +++++++++++- tests/starwars/test_dsl.py | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index bb04bdae..d0f0cfca 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -4,7 +4,7 @@ import six from graphql.language import ast from graphql.language.printer import print_ast -from graphql.type import (GraphQLField, GraphQLList, GraphQLNonNull, GraphQLEnumType) +from graphql.type import (GraphQLEnumType, GraphQLField, GraphQLList, GraphQLNonNull) from graphql.utils.ast_from_value import ast_from_value from .utils import to_camel_case @@ -59,7 +59,7 @@ def get_field(self, name): def selections(*fields): for _field in fields: - yield field(_field).ast + yield selection_field(_field).ast def get_ast_value(value): @@ -89,15 +89,15 @@ def select(self, *fields): self.ast_field.selection_set.selections.extend(selections(*fields)) return self - def __call__(self, *args, **kwargs): - return self.args(*args, **kwargs) + def __call__(self, **kwargs): + return self.args(**kwargs) def alias(self, alias): self.ast_field.alias = ast.Name(value=alias) return self - def args(self, **args): - for name, value in args.items(): + def args(self, **kwargs): + for name, value in kwargs.items(): arg = self.field.args.get(name) arg_type_serializer = get_arg_serializer(arg.type) value = arg_type_serializer(value) @@ -117,8 +117,9 @@ def __str__(self): return print_ast(self.ast_field) -def field(field, **args): +def selection_field(field, **args): if isinstance(field, GraphQLField): + # TODO: Pass name argument return DSLField(field).args(**args) elif isinstance(field, DSLField): return field @@ -139,9 +140,9 @@ def query(*fields, **kwargs): ) -def serialize_list(serializer, values): - assert isinstance(values, Iterable), 'Expected iterable, received "{}"'.format(repr(values)) - return [serializer(v) for v in values] +def serialize_list(serializer, list_values): + assert isinstance(list_values, Iterable), 'Expected iterable, received "{}"'.format(repr(list_values)) + return ast.ListValue(values=[serializer(v) for v in list_values]) def get_arg_serializer(arg_type): diff --git a/tests/starwars/fixtures.py b/tests/starwars/fixtures.py index a8ea8548..b386f1c7 100644 --- a/tests/starwars/fixtures.py +++ b/tests/starwars/fixtures.py @@ -83,6 +83,10 @@ def getCharacter(id): return humanData.get(id) or droidData.get(id) +def getCharacters(ids): + return map(getCharacter, ids) + + def getFriends(character): return map(getCharacter, character.friends) diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index afda634c..854dee84 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -3,7 +3,7 @@ GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLBoolean) -from .fixtures import getDroid, getFriends, getHero, getHuman, updateHumanAlive +from .fixtures import getCharacters, getDroid, getFriends, getHero, getHuman, updateHumanAlive episodeEnum = GraphQLEnumType( 'Episode', @@ -144,6 +144,16 @@ }, resolver=lambda root, info, **args: getDroid(args['id']), ), + 'characters': GraphQLField( + GraphQLList(characterInterface), + args={ + 'ids': GraphQLArgument( + description='list of character ids', + type=GraphQLList(GraphQLString), + ) + }, + resolver=lambda root, info, **args: getCharacters(args['ids']), + ), } ) diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index 0c8fe8b1..8cacde7b 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -124,6 +124,43 @@ def test_hero_name_query_result(ds): assert result == expected +# TODO: Keep the function name or switch to schema query names? +def test_arg_serializer_list(ds): + result = ds.query( + ds.Query.characters.args(ids=[1000, 1001, 1003]).select( + ds.Character.name, + ) + ) + expected = { + 'characters': [ + { + 'name': 'Luke Skywalker' + }, + { + 'name': 'Darth Vader' + }, + { + 'name': 'Leia Organa' + } + ] + } + assert result == expected + + +def test_arg_serializer_enum(ds): + result = ds.query( + ds.Query.hero.args(episode=5).select( + ds.Character.name + ) + ) + expected = { + 'hero': { + 'name': 'Luke Skywalker' + } + } + assert result == expected + + def test_human_alive_mutation_result(ds): result = ds.mutate( ds.Mutation.updateHumanAliveStatus.args(id=1004, status=True).select( From 2740513579be48a0c695ff8b2f5119da4945664f Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Sun, 1 Mar 2020 14:31:11 -0500 Subject: [PATCH 7/8] refactor: dsl get_ast_value removed --- gql/dsl.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index d0f0cfca..d0da1410 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -1,4 +1,3 @@ -import decimal from functools import partial import six @@ -62,20 +61,6 @@ def selections(*fields): yield selection_field(_field).ast -def get_ast_value(value): - if isinstance(value, ast.Node): - return value - if isinstance(value, six.string_types): - return ast.StringValue(value=value) - elif isinstance(value, bool): - return ast.BooleanValue(value=value) - elif isinstance(value, (float, decimal.Decimal)): - return ast.FloatValue(value=value) - elif isinstance(value, int): - return ast.IntValue(value=value) - return None - - class DSLField(object): def __init__(self, name, field): @@ -100,11 +85,11 @@ def args(self, **kwargs): for name, value in kwargs.items(): arg = self.field.args.get(name) arg_type_serializer = get_arg_serializer(arg.type) - value = arg_type_serializer(value) + serialized_value = arg_type_serializer(value) self.ast_field.arguments.append( ast.Argument( name=ast.Name(value=name), - value=get_ast_value(value) + value=serialized_value ) ) return self From f60a309812eba59f226b3346ec33f7a57e9e7a48 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Wed, 4 Mar 2020 16:23:09 -0500 Subject: [PATCH 8/8] refactor: remove if check for GraphQLField on dsl --- gql/dsl.py | 11 ++++------- tests/starwars/test_dsl.py | 3 +-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index d0da1410..bdadbef4 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -3,7 +3,7 @@ import six from graphql.language import ast from graphql.language.printer import print_ast -from graphql.type import (GraphQLEnumType, GraphQLField, GraphQLList, GraphQLNonNull) +from graphql.type import (GraphQLEnumType, GraphQLList, GraphQLNonNull) from graphql.utils.ast_from_value import ast_from_value from .utils import to_camel_case @@ -53,7 +53,7 @@ def get_field(self, name): if camel_cased_name in self.type.fields: return camel_cased_name, self.type.fields[camel_cased_name] - raise KeyError('Field {} doesnt exist in type {}.'.format(name, self.type.name)) + raise KeyError('Field {} does not exist in type {}.'.format(name, self.type.name)) def selections(*fields): @@ -102,11 +102,8 @@ def __str__(self): return print_ast(self.ast_field) -def selection_field(field, **args): - if isinstance(field, GraphQLField): - # TODO: Pass name argument - return DSLField(field).args(**args) - elif isinstance(field, DSLField): +def selection_field(field): + if isinstance(field, DSLField): return field raise Exception('Received incompatible query field: "{}".'.format(field)) diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index 8cacde7b..26dbcbf1 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -18,7 +18,7 @@ def test_invalid_field_on_type_query(ds): ds.Query.extras.select( ds.Character.name ) - assert "Field extras doesnt exist in type Query." in str(excInfo.value) + assert "Field extras does not exist in type Query." in str(excInfo.value) def test_incompatible_query_field(ds): @@ -124,7 +124,6 @@ def test_hero_name_query_result(ds): assert result == expected -# TODO: Keep the function name or switch to schema query names? def test_arg_serializer_list(ds): result = ds.query( ds.Query.characters.args(ids=[1000, 1001, 1003]).select(