diff --git a/gql/__init__.py b/gql/__init__.py index cd6c0088..571c7371 100644 --- a/gql/__init__.py +++ b/gql/__init__.py @@ -1,4 +1,4 @@ -from .gql import gql from .client import Client +from .gql import gql __all__ = ["gql", "Client"] diff --git a/gql/client.py b/gql/client.py index a6d45f7e..62129803 100644 --- a/gql/client.py +++ b/gql/client.py @@ -29,19 +29,21 @@ def __init__( ): assert not ( type_def and introspection - ), "Cant provide introspection type definition at the same time" + ), "Cannot provide introspection type definition at the same time." if transport and fetch_schema_from_transport: assert ( not schema - ), "Cant fetch the schema from transport if is already provided" + ), "Cannot fetch the schema from transport if is already provided" introspection = transport.execute(parse(introspection_query)).data if introspection: - assert not schema, "Cant provide introspection and schema at the same time" + assert ( + not schema + ), "Cannot provide introspection and schema at the same time." schema = build_client_schema(introspection) elif type_def: assert ( not schema - ), "Cant provide Type definition and schema at the same time" + ), "Cannot provide type definition and schema at the same time." type_def_ast = parse(type_def) schema = build_ast_schema(type_def_ast) elif schema and not transport: @@ -61,7 +63,7 @@ def __init__( def validate(self, document): if not self.schema: raise Exception( - "Cannot validate locally the document, you need to pass a schema." + "Cannot validate the document locally, you need to pass a schema." ) validation_errors = validate(self.schema, document) if validation_errors: diff --git a/gql/transport/requests.py b/gql/transport/requests.py index e27848f4..5a9ac491 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -76,19 +76,27 @@ def __init__( for prefix in "http://", "https://": self.session.mount(prefix, adapter) - def execute(self, document, variable_values=None, timeout=None): - # type: (Document, Dict, int) -> ExecutionResult + def execute( + self, document, variable_values=None, operation_name=None, timeout=None + ): + # type: (Document, Dict, str, int) -> ExecutionResult """Execute the provided document AST against the configured remote server. This uses the requests library to perform a HTTP POST request to the remote server. :param document: GraphQL query as AST Node object. :param variable_values: Dictionary of input parameters (Default: None). + :param operation_name: Name of the operation that shall be executed. + Only required in multi-operation documents (Default: None). :param timeout: Specifies a default timeout for requests (Default: None). - :return: The result of execution. `data` is the result of executing the query, `errors` is null if no errors - occurred, and is a non-empty array if an error occurred. + :return: The result of execution. `data` is the result of executing the query, + `errors` is null if no errors occurred, and is a non-empty array if an error occurred. """ query_str = print_ast(document) - payload = {"query": query_str, "variables": variable_values or {}} + payload = {"query": query_str} # type: Dict + if variable_values: + payload["variables"] = variable_values + if operation_name: + payload["operationName"] = operation_name data_key = "json" if self.use_json else "data" post_args = { diff --git a/setup.cfg b/setup.cfg index 4a307fe7..fc748510 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,15 @@ universal=1 max-line-length = 120 [isort] -known_first_party=gql +known_standard_library = ssl +known_first_party = gql +multi_line_output = 3 +include_trailing_comma = True +line_length = 88 +not_skip = __init__.py + +[mypy] +ignore_missing_imports = true [tool:pytest] norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache diff --git a/setup.py b/setup.py index a9682283..251f4345 100644 --- a/setup.py +++ b/setup.py @@ -29,16 +29,16 @@ ) dev_requires = [ - "flake8==3.7.9", - "isort<4.0.0", + "flake8==3.8.1", + "isort==4.3.21", "black==19.10b0", "mypy==0.770", - "check-manifest>=0.40,<1", + "check-manifest>=0.42,<1", ] + tests_require setup( name="gql", - version="0.5.0", + version="2.0.0", description="GraphQL client for Python", long_description=open("README.md").read(), long_description_content_type="text/markdown", @@ -63,7 +63,7 @@ packages=find_packages(include=["gql*"]), install_requires=install_requires, tests_require=tests_require, - extras_require={"test": tests_require, "dev": dev_requires,}, + extras_require={"test": tests_require, "dev": dev_requires}, include_package_data=True, zip_safe=False, platforms="any", diff --git a/tests/fixtures/vcr_cassettes/client.yaml b/tests/fixtures/vcr_cassettes/client.yaml index b9b550f4..78c15e15 100644 --- a/tests/fixtures/vcr_cassettes/client.yaml +++ b/tests/fixtures/vcr_cassettes/client.yaml @@ -4,14 +4,10 @@ interactions: headers: Accept: - text/html - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive Host: - swapi.graphene-python.org User-Agent: - - python-requests/2.22.0 + - python-requests/2.23.0 method: GET uri: http://127.0.0.1:8000/graphql response: @@ -68,39 +64,33 @@ interactions: - '3808' Content-Type: - text/html; charset=utf-8 - Date: - - Tue, 03 Dec 2019 08:22:54 GMT - Server: - - WSGIServer/0.1 Python/2.7.16 Set-Cookie: - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz; - expires=Tue, 01-Dec-2020 08:22:54 GMT; Max-Age=31449600; Path=/ - Vary: - - Cookie - X-Frame-Options: - - SAMEORIGIN + Max-Age=31449600; Path=/ status: code: 200 message: OK - request: - body: query=query+IntrospectionQuery+%7B%0A++__schema+%7B%0A++++queryType+%7B%0A++++++name%0A++++%7D%0A++++mutationType+%7B%0A++++++name%0A++++%7D%0A++++subscriptionType+%7B%0A++++++name%0A++++%7D%0A++++types+%7B%0A++++++...FullType%0A++++%7D%0A++++directives+%7B%0A++++++name%0A++++++description%0A++++++locations%0A++++++args+%7B%0A++++++++...InputValue%0A++++++%7D%0A++++%7D%0A++%7D%0A%7D%0A%0Afragment+FullType+on+__Type+%7B%0A++kind%0A++name%0A++description%0A++fields%28includeDeprecated%3A+true%29+%7B%0A++++name%0A++++description%0A++++args+%7B%0A++++++...InputValue%0A++++%7D%0A++++type+%7B%0A++++++...TypeRef%0A++++%7D%0A++++isDeprecated%0A++++deprecationReason%0A++%7D%0A++inputFields+%7B%0A++++...InputValue%0A++%7D%0A++interfaces+%7B%0A++++...TypeRef%0A++%7D%0A++enumValues%28includeDeprecated%3A+true%29+%7B%0A++++name%0A++++description%0A++++isDeprecated%0A++++deprecationReason%0A++%7D%0A++possibleTypes+%7B%0A++++...TypeRef%0A++%7D%0A%7D%0A%0Afragment+InputValue+on+__InputValue+%7B%0A++name%0A++description%0A++type+%7B%0A++++...TypeRef%0A++%7D%0A++defaultValue%0A%7D%0A%0Afragment+TypeRef+on+__Type+%7B%0A++kind%0A++name%0A++ofType+%7B%0A++++kind%0A++++name%0A++++ofType+%7B%0A++++++kind%0A++++++name%0A++++++ofType+%7B%0A++++++++kind%0A++++++++name%0A++++++++ofType+%7B%0A++++++++++kind%0A++++++++++name%0A++++++++++ofType+%7B%0A++++++++++++kind%0A++++++++++++name%0A++++++++++++ofType+%7B%0A++++++++++++++kind%0A++++++++++++++name%0A++++++++++++++ofType+%7B%0A++++++++++++++++kind%0A++++++++++++++++name%0A++++++++++++++%7D%0A++++++++++++%7D%0A++++++++++%7D%0A++++++++%7D%0A++++++%7D%0A++++%7D%0A++%7D%0A%7D%0A + body: '{"query": "query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType + {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives + {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment + FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: + true) {\n name\n description\n args {\n ...InputValue\n }\n type + {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields + {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: + true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes + {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type + {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType + {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType + {\n kind\n name\n ofType {\n kind\n name\n ofType + {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}' headers: Accept: - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '1625' Content-Type: - - application/x-www-form-urlencoded - Cookie: - - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + - application/json User-Agent: - - python-requests/2.22.0 - x-csrftoken: - - hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + - python-requests/2.23.0 method: POST uri: http://127.0.0.1:8000/graphql response: @@ -113,7 +103,7 @@ interactions: ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Planet","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"starship","description":"The ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Starship","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"hero","description":"The ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Hero","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"node","description":"The - ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"INTERFACE","name":"Node","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"viewer","description":null,"args":[],"type":{"kind":"OBJECT","name":"Query","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"__debug","description":null,"args":[],"type":{"kind":"OBJECT","name":"DjangoDebug","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"FilmConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination + ID of the object","args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"INTERFACE","name":"Node","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"viewer","description":null,"args":[],"type":{"kind":"OBJECT","name":"Query","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"FilmConnection","description":null,"fields":[{"name":"pageInfo","description":"Pagination data for this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","description":"Contains the nodes in this connection.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"FilmEdge","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"totalCount","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"PageInfo","description":"The Relay compliant `PageInfo` type, containing data necessary to paginate this @@ -254,20 +244,9 @@ interactions: when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]}]}}}' headers: Content-Length: - - '69554' + - '69400' Content-Type: - application/json - Date: - - Tue, 03 Dec 2019 08:22:54 GMT - Server: - - WSGIServer/0.1 Python/2.7.16 - Set-Cookie: - - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz; - expires=Tue, 01-Dec-2020 08:22:54 GMT; Max-Age=31449600; Path=/ - Vary: - - Cookie - X-Frame-Options: - - SAMEORIGIN status: code: 200 message: OK diff --git a/tests/fixtures/vcr_cassettes/execute.yaml b/tests/fixtures/vcr_cassettes/execute.yaml deleted file mode 100644 index a6728a33..00000000 --- a/tests/fixtures/vcr_cassettes/execute.yaml +++ /dev/null @@ -1,47 +0,0 @@ -interactions: -- request: - body: query=%7B%0A++myFavoriteFilm%3A+film%28id%3A+%22RmlsbToz%22%29+%7B%0A++++id%0A++++title%0A++++episodeId%0A++++characters%28first%3A+5%29+%7B%0A++++++edges+%7B%0A++++++++node+%7B%0A++++++++++name%0A++++++++%7D%0A++++++%7D%0A++++%7D%0A++%7D%0A%7D%0A - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '247' - Content-Type: - - application/x-www-form-urlencoded - Cookie: - - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz - User-Agent: - - python-requests/2.22.0 - x-csrftoken: - - hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz - method: POST - uri: http://127.0.0.1:8000/graphql - response: - body: - string: '{"data":{"myFavoriteFilm":{"id":"RmlsbToz","title":"Return of the Jedi","episodeId":6,"characters":{"edges":[{"node":{"name":"Luke - Skywalker"}},{"node":{"name":"C-3PO"}},{"node":{"name":"R2-D2"}},{"node":{"name":"Darth - Vader"}},{"node":{"name":"Leia Organa"}}]}}}}' - headers: - Content-Length: - - '264' - Content-Type: - - application/json - Date: - - Tue, 03 Dec 2019 08:23:58 GMT - Server: - - WSGIServer/0.1 Python/2.7.16 - Set-Cookie: - - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz; - expires=Tue, 01-Dec-2020 08:23:58 GMT; Max-Age=31449600; Path=/ - Vary: - - Cookie - X-Frame-Options: - - SAMEORIGIN - status: - code: 200 - message: OK -version: 1 diff --git a/tests/fixtures/vcr_cassettes/queries.yaml b/tests/fixtures/vcr_cassettes/queries.yaml new file mode 100644 index 00000000..8f0bcde5 --- /dev/null +++ b/tests/fixtures/vcr_cassettes/queries.yaml @@ -0,0 +1,143 @@ +interactions: +- request: + body: '{"query": "{\n myFavoriteFilm: film(id: \"RmlsbToz\") {\n id\n title\n episodeId\n characters(first: + 5) {\n edges {\n node {\n name\n }\n }\n }\n }\n}\n"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '204' + Content-Type: + - application/json + Cookie: + - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + User-Agent: + - python-requests/2.23.0 + x-csrftoken: + - hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '{"data":{"myFavoriteFilm":{"id":"RmlsbToz","title":"Return of the Jedi","episodeId":6,"characters":{"edges":[{"node":{"name":"Luke + Skywalker"}},{"node":{"name":"C-3PO"}},{"node":{"name":"R2-D2"}},{"node":{"name":"Darth + Vader"}},{"node":{"name":"Leia Organa"}}]}}}}' + headers: + Content-Length: + - '264' + Content-Type: + - application/json + Date: + - Sat, 16 May 2020 15:28:52 GMT + Server: + - WSGIServer/0.2 CPython/3.8.3 + Set-Cookie: + - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz; + expires=Sat, 15 May 2021 15:28:52 GMT; Max-Age=31449600; Path=/; SameSite=Lax + Vary: + - Cookie + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: '{"query": "query Planet($id: ID!) {\n planet(id: $id) {\n id\n name\n }\n}\n", + "variables": {"id": "UGxhbmV0OjEw"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '123' + Content-Type: + - application/json + Cookie: + - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + User-Agent: + - python-requests/2.23.0 + x-csrftoken: + - hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '{"data":{"planet":{"id":"UGxhbmV0OjEw","name":"Kamino"}}}' + headers: + Content-Length: + - '57' + Content-Type: + - application/json + Date: + - Sat, 16 May 2020 15:28:52 GMT + Server: + - WSGIServer/0.2 CPython/3.8.3 + Set-Cookie: + - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz; + expires=Sat, 15 May 2021 15:28:52 GMT; Max-Age=31449600; Path=/; SameSite=Lax + Vary: + - Cookie + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: '{"query": "query Planet1 {\n planet(id: \"UGxhbmV0OjEw\") {\n id\n name\n }\n}\n\nquery + Planet2 {\n planet(id: \"UGxhbmV0OjEx\") {\n id\n name\n }\n}\n", "operationName": + "Planet2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '197' + Content-Type: + - application/json + Cookie: + - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + User-Agent: + - python-requests/2.23.0 + x-csrftoken: + - hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz + method: POST + uri: http://127.0.0.1:8000/graphql + response: + body: + string: '{"data":{"planet":{"id":"UGxhbmV0OjEx","name":"Geonosis"}}}' + headers: + Content-Length: + - '59' + Content-Type: + - application/json + Date: + - Sat, 16 May 2020 15:28:52 GMT + Server: + - WSGIServer/0.2 CPython/3.8.3 + Set-Cookie: + - csrftoken=hRIez34v4hg2Wbl8XhrbshvDIB3HmLR2L9WNTJ3SdrIQHxAKtoukxiuwQlwRJewz; + expires=Sat, 15 May 2021 15:28:52 GMT; Max-Age=31449600; Path=/; SameSite=Lax + Vary: + - Cookie + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/tests/starwars/test_query.py b/tests/starwars/test_query.py index 6ac5cc07..b4ec234d 100644 --- a/tests/starwars/test_query.py +++ b/tests/starwars/test_query.py @@ -18,7 +18,7 @@ def test_hero_name_query(client): name } } - """ + """ ) expected = {"hero": {"name": "R2-D2"}} result = client.execute(query) @@ -37,7 +37,7 @@ def test_hero_name_and_friends_query(client): } } } - """ + """ ) expected = { "hero": { @@ -69,7 +69,7 @@ def test_nested_query(client): } } } - """ + """ ) expected = { "hero": { @@ -119,7 +119,7 @@ def test_fetch_luke_query(client): name } } - """ + """ ) expected = {"human": {"name": "Luke Skywalker"}} result = client.execute(query) @@ -134,7 +134,7 @@ def test_fetch_some_id_query(client): name } } - """ + """ ) params = { "someId": "1000", @@ -152,7 +152,7 @@ def test_fetch_some_id_query2(client): name } } - """ + """ ) params = { "someId": "1002", @@ -170,7 +170,7 @@ def test_invalid_id_query(client): name } } - """ + """ ) params = { "id": "not a valid id", @@ -188,7 +188,7 @@ def test_fetch_luke_aliased(client): name } } - """ + """ ) expected = {"luke": {"name": "Luke Skywalker"}} result = client.execute(query) @@ -206,7 +206,7 @@ def test_fetch_luke_and_leia_aliased(client): name } } - """ + """ ) expected = {"luke": {"name": "Luke Skywalker"}, "leia": {"name": "Leia Organa"}} result = client.execute(query) @@ -226,7 +226,7 @@ def test_duplicate_fields(client): homePlanet } } - """ + """ ) expected = { "luke": {"name": "Luke Skywalker", "homePlanet": "Tatooine"}, @@ -251,7 +251,7 @@ def test_use_fragment(client): name homePlanet } - """ + """ ) expected = { "luke": {"name": "Luke Skywalker", "homePlanet": "Tatooine"}, @@ -270,7 +270,7 @@ def test_check_type_of_r2(client): name } } - """ + """ ) expected = {"hero": {"__typename": "Droid", "name": "R2-D2"}} result = client.execute(query) @@ -286,7 +286,7 @@ def test_check_type_of_luke(client): name } } - """ + """ ) expected = {"hero": {"__typename": "Human", "name": "Luke Skywalker"}} result = client.execute(query) @@ -294,14 +294,12 @@ def test_check_type_of_luke(client): def test_parse_error(client): - result = None with pytest.raises(Exception) as exc_info: - query = gql( + gql( """ qeury - """ + """ ) - result = client.execute(query) error = exc_info.value formatted_error = format_error(error) assert formatted_error["locations"] == [{"column": 13, "line": 2}] @@ -309,7 +307,6 @@ def test_parse_error(client): 'Syntax Error GraphQL request (2:13) Unexpected Name "qeury"' in formatted_error["message"] ) - assert not result def test_mutation_result(client): @@ -321,7 +318,7 @@ def test_mutation_result(client): commentary } } - """ + """ ) params = { "ep": "JEDI", diff --git a/tests/test_client.py b/tests/test_client.py index 19fad303..53105419 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -40,19 +40,19 @@ def test_retries(execute_mock): client = Client( retries=expected_retries, - transport=RequestsHTTPTransport(url="http://swapi.graphene-python.org/graphql"), + transport=RequestsHTTPTransport(url="http://127.0.0.1:8000/graphql"), ) query = gql( """ - { - myFavoriteFilm: film(id:"RmlsbToz") { - id - title - episodeId - } - } - """ + { + myFavoriteFilm: film(id:"RmlsbToz") { + id + title + episodeId + } + } + """ ) with pytest.raises(Exception): @@ -73,20 +73,20 @@ def test_retries_on_transport(execute_mock): "Should be HTTPConnection", "Fake connection error" ) transport = RequestsHTTPTransport( - url="http://localhost:9999", retries=expected_retries, + url="http://127.0.0.1:8000/graphql", retries=expected_retries, ) client = Client(transport=transport) query = gql( """ - { - myFavoriteFilm: film(id:"RmlsbToz") { - id - title - episodeId - } - } - """ + { + myFavoriteFilm: film(id:"RmlsbToz") { + id + title + episodeId + } + } + """ ) with client: # We're using the client as context manager with pytest.raises(Exception): @@ -101,7 +101,7 @@ def test_no_schema_exception(): with pytest.raises(Exception) as exc_info: client = Client() client.validate("") - assert "Cannot validate locally the document, you need to pass a schema." in str( + assert "Cannot validate the document locally, you need to pass a schema." in str( exc_info.value ) @@ -116,14 +116,14 @@ def test_execute_result_error(): failing_query = gql( """ - query getContinents { - continents { - code - name - id - } - } - """ + query getContinents { + continents { + code + name + id + } + } + """ ) with pytest.raises(Exception) as exc_info: @@ -206,7 +206,7 @@ def test_gql(): username } } - """ + """ ) result = client.execute(query) client.close() diff --git a/tests/test_transport.py b/tests/test_transport.py index b415bb8a..823059e1 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1,3 +1,5 @@ +import os + import pytest import requests import vcr @@ -5,19 +7,31 @@ from gql import Client, gql from gql.transport.requests import RequestsHTTPTransport -# https://github.com/graphql-python/swapi-graphene +# We serve https://github.com/graphql-python/swapi-graphene locally: URL = "http://127.0.0.1:8000/graphql" +query_vcr = vcr.VCR( + cassette_library_dir=os.path.join( + os.path.dirname(__file__), "fixtures", "vcr_cassettes" + ), + record_mode="new_episodes", + match_on=["uri", "method", "body"], +) + + +def use_cassette(name): + return query_vcr.use_cassette(name + ".yaml") + + @pytest.fixture def client(): - with vcr.use_cassette("tests/fixtures/vcr_cassettes/client.yaml"): - request = requests.get( + with use_cassette("client"): + response = requests.get( URL, headers={"Host": "swapi.graphene-python.org", "Accept": "text/html"} ) - request.raise_for_status() - csrf = request.cookies["csrftoken"] - + response.raise_for_status() + csrf = response.cookies["csrftoken"] return Client( transport=RequestsHTTPTransport( url=URL, cookies={"csrftoken": csrf}, headers={"x-csrftoken": csrf} @@ -29,21 +43,21 @@ def client(): def test_hero_name_query(client): query = gql( """ - { - myFavoriteFilm: film(id:"RmlsbToz") { - id - title - episodeId - characters(first:5) { - edges { - node { - name + { + myFavoriteFilm: film(id:"RmlsbToz") { + id + title + episodeId + characters(first:5) { + edges { + node { + name + } + } } } } - } - } - """ + """ ) expected = { "myFavoriteFilm": { @@ -61,6 +75,46 @@ def test_hero_name_query(client): }, } } - with vcr.use_cassette("tests/fixtures/vcr_cassettes/execute.yaml"): + with use_cassette("queries"): result = client.execute(query) - assert result == expected + assert result == expected + + +def test_query_with_variable(client): + query = gql( + """ + query Planet($id: ID!) { + planet(id: $id) { + id + name + } + } + """ + ) + expected = {"planet": {"id": "UGxhbmV0OjEw", "name": "Kamino"}} + with use_cassette("queries"): + result = client.execute(query, variable_values={"id": "UGxhbmV0OjEw"}) + assert result == expected + + +def test_named_query(client): + query = gql( + """ + query Planet1 { + planet(id: "UGxhbmV0OjEw") { + id + name + } + } + query Planet2 { + planet(id: "UGxhbmV0OjEx") { + id + name + } + } + """ + ) + expected = {"planet": {"id": "UGxhbmV0OjEx", "name": "Geonosis"}} + with use_cassette("queries"): + result = client.execute(query, operation_name="Planet2") + assert result == expected diff --git a/tests_py36/schema.py b/tests_py36/schema.py index 0de3e27a..cb0d2c1d 100644 --- a/tests_py36/schema.py +++ b/tests_py36/schema.py @@ -1,28 +1,34 @@ -from graphql import GraphQLField, GraphQLArgument, GraphQLObjectType, GraphQLSchema +from graphql import GraphQLArgument, GraphQLField, GraphQLObjectType, GraphQLSchema -from tests.starwars.schema import reviewType, episodeEnum, queryType, mutationType, humanType, droidType, \ - reviewInputType +from tests.starwars.schema import ( + droidType, + episodeEnum, + humanType, + mutationType, + queryType, + reviewInputType, + reviewType, +) from tests_py36.fixtures import reviewAdded subscriptionType = GraphQLObjectType( - 'Subscription', + "Subscription", fields=lambda: { - 'reviewAdded': GraphQLField( + "reviewAdded": GraphQLField( reviewType, args={ - 'episode': GraphQLArgument( - description='Episode to review', - type_=episodeEnum, + "episode": GraphQLArgument( + description="Episode to review", type_=episodeEnum, # type: ignore ) }, - resolver=lambda root, info, **args: reviewAdded(args.get('episode')), + resolver=lambda root, info, **args: reviewAdded(args.get("episode")), ) - } + }, ) StarWarsSchema = GraphQLSchema( query=queryType, mutation=mutationType, subscription=subscriptionType, - types=[humanType, droidType, reviewType, reviewInputType] + types=[humanType, droidType, reviewType, reviewInputType], ) diff --git a/tests_py36/test_query.py b/tests_py36/test_query.py index 937ca2b0..aca7bfca 100644 --- a/tests_py36/test_query.py +++ b/tests_py36/test_query.py @@ -20,15 +20,15 @@ def __aiter__(self): async def __anext__(self): type_, val = await self.queue.get() - if type_ in ('E', 'C'): + if type_ in ("E", "C"): raise StopAsyncIteration() return val async def __aenter__(self): self.disposable = self.observable.subscribe( - on_next=lambda val: self.queue.put_nowait(('N', val)), - on_error=lambda exc: self.queue.put_nowait(('E', exc)), - on_completed=lambda: self.queue.put_nowait(('C', None)), + on_next=lambda val: self.queue.put_nowait(("N", val)), + on_error=lambda exc: self.queue.put_nowait(("E", exc)), + on_completed=lambda: self.queue.put_nowait(("C", None)), ) return self @@ -38,7 +38,8 @@ async def __aexit__(self, exc_type, exc_value, traceback): @pytest.mark.asyncio async def test_subscription_support(): - subs = gql(''' + subs = gql( + """ subscription ListenEpisodeReviews($ep: Episode!) { reviewAdded(episode: $ep) { stars, @@ -46,19 +47,18 @@ async def test_subscription_support(): episode } } - ''') - params = { - 'ep': 'JEDI' - } + """ + ) + params = {"ep": "JEDI"} expected_one = { - 'stars': 3, - 'commentary': 'Was expecting more stuff', - 'episode': 'JEDI' + "stars": 3, + "commentary": "Was expecting more stuff", + "episode": "JEDI", } expected_two = { - 'stars': 5, - 'commentary': 'This is a great movie!', - 'episode': 'JEDI' + "stars": 5, + "commentary": "This is a great movie!", + "episode": "JEDI", } # For asyncio, requires set return_promise=True as stated on the following comment # https://github.com/graphql-python/graphql-core/issues/63#issuecomment-568270864 @@ -68,12 +68,12 @@ async def test_subscription_support(): document_ast=subs, return_promise=True, variable_values=params, - executor=AsyncioExecutor(loop=loop) + executor=AsyncioExecutor(loop=loop), ) expected = [] async with ObservableAsyncIterable(execution_result) as oai: async for i in oai: review = i.to_dict() - expected.append(review['data']['reviewAdded']) + expected.append(review["data"]["reviewAdded"]) assert expected[0] == expected_one assert expected[1] == expected_two diff --git a/tox.ini b/tox.ini index 878017f7..344702ea 100644 --- a/tox.ini +++ b/tox.ini @@ -28,25 +28,25 @@ commands = basepython=python3.8 deps = -e.[dev] commands = - black --check gql tests + black --check gql tests tests_py36 [testenv:flake8] basepython = python3.8 deps = -e.[dev] commands = - flake8 gql tests + flake8 gql tests tests_py36 [testenv:import-order] basepython=python3.8 deps = -e.[dev] commands = - isort -rc gql/ tests/ + isort --recursive --check-only --diff gql tests tests_py36 [testenv:mypy] basepython=python3.8 deps = -e.[dev] commands = - mypy gql tests --ignore-missing-imports + mypy gql tests tests_py36 [testenv:manifest] basepython = python3.8