diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..776c3080 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing + +Thanks for helping to make gql awesome! + +We welcome all kinds of contributions: + +- Bug fixes +- Documentation improvements +- New features +- Refactoring & tidying + + +## Getting started + +If you have a specific contribution in mind, be sure to check the +[issues](https://github.com/graphql-python/gql/issues) +and [pull requests](https://github.com/graphql-python/gql/pulls) +in progress - someone could already be working on something similar +and you can help out. + +## Project setup + +### Development with virtualenv (recommended) + +After cloning this repo, create a virtualenv: + +```console +virtualenv gql-dev +``` + +Activate the virtualenv and install dependencies by running: + +```console +python pip install -e ".[test]" +``` + +If you are using Linux or MacOS, you can make use of Makefile command +`make dev-setup`, which is a shortcut for the above python command. + +### Development on Conda + +You must create a new env (e.g. `gql-dev`) with the following command: + +```sh +conda create -n gql-dev python=3.8 +``` + +Then activate the environment with `conda activate gql-dev`. + +Proceed to install all dependencies by running: + +```console +python pip install -e ".[test]" +``` + +And you ready to start development! + + + +## Running tests + +After developing, the full test suite can be evaluated by running: + +```sh +pytest tests --cov=gql -vv +``` + +If you are using Linux or MacOS, you can make use of Makefile command +`make tests`, which is a shortcut for the above python command. + +You can also test on several python environments by using tox. + +### Running tox on virtualenv + +Install tox: +```console +pip install tox +``` + +Run `tox` on your virtualenv (do not forget to activate it!) +and that's it! + +### Running tox on Conda + +In order to run `tox` command on conda, install +[tox-conda](https://github.com/tox-dev/tox-conda): + +```sh +conda install -c conda-forge tox-conda +``` + +This install tox underneath so no need to install it before. + +Then uncomment the `requires = tox-conda` line on `tox.ini` file. + +Run `tox` and you will see all the environments being created +and all passing tests. :rocket: \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index e9e681eb..49e90f2e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,8 +3,10 @@ include MANIFEST.in include CODEOWNERS include LICENSE include README.md +include CONTRIBUTING.md include dev_requirements.txt +include Makefile include tox.ini diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..83609cef --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +dev-setup: + python pip install -e ".[test]" + +tests: + pytest tests --cov=gql -vv \ No newline at end of file diff --git a/README.md b/README.md index 60d0a70b..008561fb 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ client = Client(transport=RequestsHTTPTransport( url='/graphql', headers={'Authorization': 'token'}), schema=schema) ``` +## Contributing +See [CONTRIBUTING.md](contributing.md) + ## License [MIT License](https://github.com/graphql-python/gql/blob/master/LICENSE) diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 71399a55..20e2794f 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -35,12 +35,16 @@ def execute(self, document, variable_values=None, timeout=None): 'timeout': timeout or self.default_timeout, data_key: payload } - request = requests.post(self.url, **post_args) - request.raise_for_status() - - result = request.json() - assert 'errors' in result or 'data' in result, 'Received non-compatible response "{}"'.format(result) - return ExecutionResult( - errors=result.get('errors'), - data=result.get('data') - ) + + response = requests.post(self.url, **post_args) + try: + result = response.json() + if not isinstance(result, dict): + raise ValueError + except ValueError: + result = {} + + if 'errors' not in result and 'data' not in result: + response.raise_for_status() + raise requests.HTTPError("Server did not return a GraphQL result", response=response) + return ExecutionResult(errors=result.get('errors'), data=result.get('data')) diff --git a/gql/utils.py b/gql/utils.py index d5996497..5b1d15cc 100644 --- a/gql/utils.py +++ b/gql/utils.py @@ -1,4 +1,4 @@ -import re +"""Utilities to manipulate several python objects.""" # From this response in Stackoverflow @@ -8,14 +8,3 @@ def to_camel_case(snake_str): # We capitalize the first letter of each component except the first one # with the 'title' method and join them together. return components[0] + "".join(x.title() if x else '_' for x in components[1:]) - - -# From this response in Stackoverflow -# http://stackoverflow.com/a/1176023/1072990 -def to_snake_case(name): - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() - - -def to_const(string): - return re.sub(r'[\W|^]+', '_', string).upper() diff --git a/setup.py b/setup.py index c4cf4e52..14f97ab8 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,14 @@ 'pytest==4.6.9', 'pytest-cov==2.8.1', 'mock==3.0.5', - 'vcrpy==3.0.0' + 'vcrpy==3.0.0', ] +dev_requires = [ + 'flake8==3.7.9', + 'check-manifest>=0.40,<1', +] + tests_require + setup( name='gql', version='0.3.0', @@ -42,6 +47,10 @@ install_requires=install_requires, tests_require=tests_require, extras_require={ - 'test': tests_require - } + 'test': tests_require, + 'dev': dev_requires, + }, + include_package_data=True, + zip_safe=False, + platforms="any", ) diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index e8fa1b61..e901009e 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -13,6 +13,20 @@ def ds(): return ds +def test_invalid_field_on_type_query(ds): + with pytest.raises(KeyError) as excInfo: + ds.Query.extras.select( + ds.Character.name + ) + assert "Field extras doesnt exist in type Query." in str(excInfo.value) + + +def test_incompatible_query_field(ds): + with pytest.raises(Exception) as excInfo: + ds.query('hero') + assert "Received incompatible query field" in str(excInfo.value) + + def test_hero_name_query(ds): query = ''' hero { @@ -84,67 +98,6 @@ def test_fetch_luke_query(ds): assert query == str(query_dsl) -# def test_fetch_some_id_query(): -# query = ''' -# query FetchSomeIDQuery($someId: String!) { -# human(id: $someId) { -# name -# } -# } -# ''' -# params = { -# 'someId': '1000', -# } -# expected = { -# 'human': { -# 'name': 'Luke Skywalker', -# } -# } -# result = schema.execute(query, None, params) -# assert not result.errors -# assert result.data == expected - - -# def test_fetch_some_id_query2(): -# query = ''' -# query FetchSomeIDQuery($someId: String!) { -# human(id: $someId) { -# name -# } -# } -# ''' -# params = { -# 'someId': '1002', -# } -# expected = { -# 'human': { -# 'name': 'Han Solo', -# } -# } -# result = schema.execute(query, None, params) -# assert not result.errors -# assert result.data == expected - - -# def test_invalid_id_query(): -# query = ''' -# query humanQuery($id: String!) { -# human(id: $id) { -# name -# } -# } -# ''' -# params = { -# 'id': 'not a valid id', -# } -# expected = { -# 'human': None -# } -# result = schema.execute(query, None, params) -# assert not result.errors -# assert result.data == expected - - def test_fetch_luke_aliased(ds): query = ''' luke: human(id: "1000") { @@ -157,128 +110,6 @@ def test_fetch_luke_aliased(ds): assert query == str(query_dsl) -# def test_fetch_luke_and_leia_aliased(): -# query = ''' -# query FetchLukeAndLeiaAliased { -# luke: human(id: "1000") { -# name -# } -# leia: human(id: "1003") { -# name -# } -# } -# ''' -# expected = { -# 'luke': { -# 'name': 'Luke Skywalker', -# }, -# 'leia': { -# 'name': 'Leia Organa', -# } -# } -# result = schema.execute(query) -# assert not result.errors -# assert result.data == expected - - -# def test_duplicate_fields(): -# query = ''' -# query DuplicateFields { -# luke: human(id: "1000") { -# name -# homePlanet -# } -# leia: human(id: "1003") { -# name -# homePlanet -# } -# } -# ''' -# expected = { -# 'luke': { -# 'name': 'Luke Skywalker', -# 'homePlanet': 'Tatooine', -# }, -# 'leia': { -# 'name': 'Leia Organa', -# 'homePlanet': 'Alderaan', -# } -# } -# result = schema.execute(query) -# assert not result.errors -# assert result.data == expected - - -# def test_use_fragment(): -# query = ''' -# query UseFragment { -# luke: human(id: "1000") { -# ...HumanFragment -# } -# leia: human(id: "1003") { -# ...HumanFragment -# } -# } -# fragment HumanFragment on Human { -# name -# homePlanet -# } -# ''' -# expected = { -# 'luke': { -# 'name': 'Luke Skywalker', -# 'homePlanet': 'Tatooine', -# }, -# 'leia': { -# 'name': 'Leia Organa', -# 'homePlanet': 'Alderaan', -# } -# } -# result = schema.execute(query) -# assert not result.errors -# assert result.data == expected - - -# def test_check_type_of_r2(): -# query = ''' -# query CheckTypeOfR2 { -# hero { -# __typename -# name -# } -# } -# ''' -# expected = { -# 'hero': { -# '__typename': 'Droid', -# 'name': 'R2-D2', -# } -# } -# result = schema.execute(query) -# assert not result.errors -# assert result.data == expected - - -# def test_check_type_of_luke(): -# query = ''' -# query CheckTypeOfLuke { -# hero(episode: EMPIRE) { -# __typename -# name -# } -# } -# ''' -# expected = { -# 'hero': { -# '__typename': 'Human', -# 'name': 'Luke Skywalker', -# } -# } -# result = schema.execute(query) -# assert not result.errors -# assert result.data == expected - - def test_hero_name_query_result(ds): result = ds.query( ds.Query.hero.select( diff --git a/tests/starwars/test_validation.py b/tests/starwars/test_validation.py index e2699e95..e36c069c 100644 --- a/tests/starwars/test_validation.py +++ b/tests/starwars/test_validation.py @@ -76,6 +76,12 @@ def validation_errors(client, query): return True +def test_incompatible_request_gql(client): + with pytest.raises(Exception) as excInfo: + gql(123) + assert "Received incompatible request" in str(excInfo.value) + + def test_nested_query_with_fragment(client): query = ''' query NestedQueryWithFragment { diff --git a/tests/test_client.py b/tests/test_client.py index 14f06c43..2d5bdfc1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,3 +29,38 @@ def test_retries(execute_mock): client.execute(query) assert execute_mock.call_count == expected_retries + + +def test_no_schema_exception(): + with pytest.raises(Exception) as excInfo: + client = Client() + client.validate('') + assert "Cannot validate locally the document, you need to pass a schema." in str(excInfo.value) + + +def test_execute_result_error(): + expected_retries = 3 + + client = Client( + retries=expected_retries, + transport=RequestsHTTPTransport( + url='https://countries.trevorblades.com/', + use_json=True, + headers={ + "Content-type": "application/json", + } + ) + ) + + query = gql(''' + query getContinents { + continents { + code + name + id + } + } + ''') + with pytest.raises(Exception) as excInfo: + client.execute(query) + assert "Cannot query field \"id\" on type \"Continent\"." in str(excInfo.value) diff --git a/tox.ini b/tox.ini index 7d6b4c66..89303f01 100644 --- a/tox.ini +++ b/tox.ini @@ -1,24 +1,30 @@ [tox] -envlist = py{27,35,36,37,38,py,py3}, flake8, manifest +envlist = py{27,35,36,37,38}, flake8, manifest +; requires = tox-conda + +[testenv] +passenv = * +setenv = + PYTHONPATH = {toxinidir} + MULTIDICT_NO_EXTENSIONS = 1 ; Related to https://github.com/aio-libs/multidict + YARL_NO_EXTENSIONS = 1 ; Related to https://github.com/aio-libs/yarl +install_command = python -m pip install --ignore-installed {opts} {packages} +whitelist_externals = + python +deps = -e.[test] +; Prevent installing issues: https://github.com/ContinuumIO/anaconda-issues/issues/542 +commands = + pip install -U setuptools + pytest --cov=gql tests {posargs} [testenv:flake8] basepython = python3.8 -deps = flake8==3.7.9 +deps = -e.[dev] commands = flake8 gql tests [testenv:manifest] basepython = python3.8 -deps = check-manifest>=0.40,<1 +deps = -e.[dev] commands = check-manifest -v - -[testenv] -setenv = - PYTHONPATH = {toxinidir} - MULTIDICT_NO_EXTENSIONS = 1 - YARL_NO_EXTENSIONS = 1 -extras = - test -commands = - pytest --cov=gql tests {posargs}