From d29cc6f8fd204681803b5e4c6ae67f8280c57bf9 Mon Sep 17 00:00:00 2001 From: Manuel Bojato <30560560+KingDarBoja@users.noreply.github.com> Date: Tue, 25 Feb 2020 03:31:43 -0500 Subject: [PATCH 1/7] Update context and root executor options (#74) * Pass context and root value to executor opts * Bump graphql-core minimum version to 2.3 * Fix graphqlview custom context test * Pass app fixture on graphiqlview --- flask_graphql/graphqlview.py | 6 +++--- setup.py | 6 +++++- tests/test_graphiqlview.py | 4 ++-- tests/test_graphqlview.py | 22 +++++++++++----------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index ff257b3..e38e1ab 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -38,7 +38,7 @@ def __init__(self, **kwargs): def get_root_value(self): return self.root_value - def get_context(self): + def get_context_value(self): return request def get_middleware(self): @@ -89,8 +89,8 @@ def dispatch_request(self): backend=self.get_backend(), # Execute options - root=self.get_root_value(), - context=self.get_context(), + root_value=self.get_root_value(), + context_value=self.get_context_value(), middleware=self.get_middleware(), **extra_options ) diff --git a/setup.py b/setup.py index 0429c9e..0e2d779 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,10 @@ from setuptools import setup, find_packages -required_packages = ["graphql-core>=2.1,<3", "flask>=0.7.0", "graphql-server-core>=1.1,<2"] +required_packages = [ + "graphql-core>=2.3,<3", + "flask>=0.7.0", + "graphql-server-core>=1.1,<2", +] setup( name="Flask-GraphQL", diff --git a/tests/test_graphiqlview.py b/tests/test_graphiqlview.py index 3a3698a..fd7469a 100644 --- a/tests/test_graphiqlview.py +++ b/tests/test_graphiqlview.py @@ -23,7 +23,7 @@ def test_graphiql_renders_pretty(client): ' "test": "Hello World"\n' ' }\n' '}' - ).replace("\"","\\\"").replace("\n","\\n") + ).replace("\"", "\\\"").replace("\n", "\\n") assert pretty_response in response.data.decode('utf-8') @@ -34,6 +34,6 @@ def test_graphiql_default_title(client): @pytest.mark.parametrize('app', [create_app(graphiql=True, graphiql_html_title="Awesome")]) -def test_graphiql_custom_title(client): +def test_graphiql_custom_title(app, client): response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) assert 'Awesome' in response.data.decode('utf-8') diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 77626d4..71b4df6 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -19,6 +19,7 @@ def app(): return create_app() + def url_string(**url_params): string = url_for('graphql') @@ -328,7 +329,7 @@ def test_allows_post_with_get_operation_name(client): @pytest.mark.parametrize('app', [create_app(pretty=True)]) -def test_supports_pretty_printing(client): +def test_supports_pretty_printing(app, client): response = client.get(url_string(query='{test}')) assert response.data.decode() == ( @@ -341,7 +342,7 @@ def test_supports_pretty_printing(client): @pytest.mark.parametrize('app', [create_app(pretty=False)]) -def test_not_pretty_by_default(client): +def test_not_pretty_by_default(app, client): response = client.get(url_string(query='{test}')) assert response.data.decode() == ( @@ -451,11 +452,10 @@ def test_passes_request_into_request_context(client): } -@pytest.mark.parametrize('app', [create_app(get_context=lambda:"CUSTOM CONTEXT")]) -def test_supports_pretty_printing(client): +@pytest.mark.parametrize('app', [create_app(get_context_value=lambda:"CUSTOM CONTEXT")]) +def test_supports_pretty_printing_with_custom_context(app, client): response = client.get(url_string(query='{context}')) - assert response.status_code == 200 assert response_json(response) == { 'data': { @@ -468,7 +468,7 @@ def test_post_multipart_data(client): query = 'mutation TestMutation { writeTest { test } }' response = client.post( url_string(), - data= { + data={ 'query': query, 'file': (StringIO(), 'text1.txt'), }, @@ -480,7 +480,7 @@ def test_post_multipart_data(client): @pytest.mark.parametrize('app', [create_app(batch=True)]) -def test_batch_allows_post_with_json_encoding(client): +def test_batch_allows_post_with_json_encoding(app, client): response = client.post( url_string(), data=jl( @@ -498,7 +498,7 @@ def test_batch_allows_post_with_json_encoding(client): @pytest.mark.parametrize('app', [create_app(batch=True)]) -def test_batch_supports_post_json_query_with_json_variables(client): +def test_batch_supports_post_json_query_with_json_variables(app, client): response = client.post( url_string(), data=jl( @@ -514,10 +514,10 @@ def test_batch_supports_post_json_query_with_json_variables(client): # 'id': 1, 'data': {'test': "Hello Dolly"} }] - - + + @pytest.mark.parametrize('app', [create_app(batch=True)]) -def test_batch_allows_post_with_operation_name(client): +def test_batch_allows_post_with_operation_name(app, client): response = client.post( url_string(), data=jl( From 88089b6ca532a0e8148a1b49e3cec14af6263086 Mon Sep 17 00:00:00 2001 From: Manuel Bojato <30560560+KingDarBoja@users.noreply.github.com> Date: Sat, 14 Mar 2020 02:41:06 -0500 Subject: [PATCH 2/7] Contributing docs and refactor tox config (#77) * chore: contributing docs and tox config * chore: add pytest-cov and syntax fixes * chore: sort imports on graphqlview * chore: comment out tox-conda on tox file * chore: remove readme rst * chore: add check manifest to dev dependencies * chore: remove matrix jobs on travis * chore: add pyversions to Travis CI * chore: remove py33 and py34 from classifiers --- .coveragerc | 2 + .travis.yml | 34 ++++---- CONTRIBUTING.md | 49 +++++++++++ MANIFEST.in | 11 +++ Makefile | 5 ++ README.md | 3 + README.rst | 63 -------------- flask_graphql/graphqlview.py | 4 +- setup.py | 29 +++++-- tests/test_graphiqlview.py | 31 +++++-- tests/test_graphqlview.py | 157 +++++++++++++++++++++-------------- tox.ini | 33 ++++---- 12 files changed, 242 insertions(+), 179 deletions(-) create mode 100644 .coveragerc create mode 100644 CONTRIBUTING.md create mode 100644 Makefile delete mode 100644 README.rst diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..87df271 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[report] +omit = */tests/* diff --git a/.travis.yml b/.travis.yml index 4a129cb..c59b86e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,23 @@ language: python sudo: false -matrix: - include: - - python: pypy - env: TOX_ENV=pypy - - python: '2.7' - env: TOX_ENV=py27 - - python: '3.5' - env: TOX_ENV=py35 - - python: '3.6' - env: TOX_ENV=py36 - - python: '3.6' - env: TOX_ENV=import-order,flake8 -cache: - directories: - - $HOME/.cache/pip - - $TRAVIS_BUILD_DIR/.tox +python: + - 2.7 + - 3.5 + - 3.6 + - 3.7 + # - 3.8 +cache: pip + install: -- pip install tox coveralls + - pip install tox-travis + script: -- tox -e $TOX_ENV -- --cov=flask_graphql + - tox + after_success: -- coveralls + - pip install coveralls + - coveralls + deploy: provider: pypi user: syrusakbary diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9525b09 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing + +Thanks for helping to make flask-graphql 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/flask-graphql/issues) and [projects](https://github.com/graphql-python/flask-graphql/projects) in progress - someone could already be working on something similar and you can help out. + + +## Project setup + +After cloning this repo, ensure dependencies are installed by running: + +```sh +make dev-setup +``` + +## Running tests + +After developing, the full test suite can be evaluated by running: + +```sh +make tests +``` + +## Development on Conda + +In order to run `tox` command on conda, you must create a new env (e.g. `flask-grapqhl-dev`) with the following command: + +```sh +conda create -n flask-grapqhl-dev python=3.8 +``` + +Then activate the environment with `conda activate flask-grapqhl-dev` and install [tox-conda](https://github.com/tox-dev/tox-conda): + +```sh +conda install -c conda-forge tox-conda +``` + +Uncomment the `requires = tox-conda` line on `tox.ini` file and that's it! Run `tox` and you will see all the environments being created and all passing tests. :rocket: + diff --git a/MANIFEST.in b/MANIFEST.in index 497302a..29c54bf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,14 @@ +include LICENSE include README.md +include CONTRIBUTING.md + recursive-include flask_graphql/static * recursive-include flask_graphql/templates * +recursive-include tests *.py + +include Makefile + +include .coveragerc +include tox.ini + +global-exclude *.py[co] __pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6f33e3 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +dev-setup: + python pip install -e ".[test]" + +tests: + py.test tests --cov=flask_graphql -vv \ No newline at end of file diff --git a/README.md b/README.md index 8c918ae..48f5ea8 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,6 @@ class UserRootValue(GraphQLView): return request.user ``` + +## Contributing +See [CONTRIBUTING.md](contributing.md) diff --git a/README.rst b/README.rst deleted file mode 100644 index a2c5ef7..0000000 --- a/README.rst +++ /dev/null @@ -1,63 +0,0 @@ -Flask-GraphQL -============= - -|Build Status| |Coverage Status| |PyPI version| - -Adds GraphQL support to your Flask application. - -Usage ------ - -Just use the ``GraphQLView`` view from ``flask_graphql`` - -.. code:: python - - from flask_graphql import GraphQLView - - app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)) - - # Optional, for adding batch query support (used in Apollo-Client) - app.add_url_rule('/graphql/batch', view_func=GraphQLView.as_view('graphql', schema=schema, batch=True)) - -This will add ``/graphql`` and ``/graphiql`` endpoints to your app. - -Supported options -~~~~~~~~~~~~~~~~~ - -- ``schema``: The ``GraphQLSchema`` object that you want the view to - execute when it gets a valid request. -- ``context``: A value to pass as the ``context`` to the ``graphql()`` - function. -- ``root_value``: The ``root_value`` you want to provide to - ``executor.execute``. -- ``pretty``: Whether or not you want the response to be pretty printed - JSON. -- ``executor``: The ``Executor`` that you want to use to execute - queries. -- ``graphiql``: If ``True``, may present - `GraphiQL `__ when loaded - directly from a browser (a useful tool for debugging and - exploration). -- ``graphiql_template``: Inject a Jinja template string to customize - GraphiQL. -- ``batch``: Set the GraphQL view as batch (for using in - `Apollo-Client `__ - or - `ReactRelayNetworkLayer `__) - -You can also subclass ``GraphQLView`` and overwrite -``get_root_value(self, request)`` to have a dynamic root value per -request. - -.. code:: python - - class UserRootValue(GraphQLView): - def get_root_value(self, request): - return request.user - -.. |Build Status| image:: https://travis-ci.org/graphql-python/flask-graphql.svg?branch=master - :target: https://travis-ci.org/graphql-python/flask-graphql -.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/flask-graphql/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/graphql-python/flask-graphql?branch=master -.. |PyPI version| image:: https://badge.fury.io/py/flask-graphql.svg - :target: https://badge.fury.io/py/flask-graphql diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py index e38e1ab..25e382f 100644 --- a/flask_graphql/graphqlview.py +++ b/flask_graphql/graphqlview.py @@ -2,12 +2,12 @@ from flask import Response, request from flask.views import View - -from graphql.type.schema import GraphQLSchema from graphql_server import (HttpQueryError, default_format_error, encode_execution_results, json_encode, load_json_body, run_http_query) +from graphql.type.schema import GraphQLSchema + from .render_graphiql import render_graphiql diff --git a/setup.py b/setup.py index 0e2d779..a851be7 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,29 @@ from setuptools import setup, find_packages -required_packages = [ - "graphql-core>=2.3,<3", +install_requires = [ "flask>=0.7.0", + "graphql-core>=2.3,<3", "graphql-server-core>=1.1,<2", ] +tests_requires = [ + 'pytest>=2.7.2', + 'pytest-cov==2.8.1', + 'pytest-flask>=0.10.0', +] + +dev_requires = [ + 'flake8==3.7.9', + 'isort<4.0.0', + 'check-manifest>=0.40,<1', +] + tests_requires + setup( name="Flask-GraphQL", version="2.0.1", description="Adds GraphQL support to your Flask application", - long_description=open("README.rst").read(), + long_description=open("README.md").read(), + long_description_content_type="text/markdown", url="https://github.com/graphql-python/flask-graphql", download_url="https://github.com/graphql-python/flask-graphql/releases", author="Syrus Akbary", @@ -23,8 +36,6 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", @@ -33,8 +44,12 @@ ], keywords="api graphql protocol rest flask", packages=find_packages(exclude=["tests"]), - install_requires=required_packages, - tests_require=["pytest>=2.7.3"], + install_requires=install_requires, + tests_require=tests_requires, + extras_require={ + 'test': tests_requires, + 'dev': dev_requires, + }, include_package_data=True, zip_safe=False, platforms="any", diff --git a/tests/test_graphiqlview.py b/tests/test_graphiqlview.py index fd7469a..aa92202 100644 --- a/tests/test_graphiqlview.py +++ b/tests/test_graphiqlview.py @@ -6,16 +6,29 @@ @pytest.fixture def app(): - return create_app(graphiql=True) + # import app factory pattern + app = create_app(graphiql=True) + # pushes an application context manually + ctx = app.app_context() + ctx.push() + return app -def test_graphiql_is_enabled(client): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) + +@pytest.fixture +def client(app): + return app.test_client() + + +def test_graphiql_is_enabled(app, client): + with app.test_request_context(): + response = client.get(url_for('graphql', externals=False), headers={'Accept': 'text/html'}) assert response.status_code == 200 -def test_graphiql_renders_pretty(client): - response = client.get(url_for('graphql', query='{test}'), headers={'Accept': 'text/html'}) +def test_graphiql_renders_pretty(app, client): + with app.test_request_context(): + response = client.get(url_for('graphql', query='{test}'), headers={'Accept': 'text/html'}) assert response.status_code == 200 pretty_response = ( '{\n' @@ -28,12 +41,14 @@ def test_graphiql_renders_pretty(client): assert pretty_response in response.data.decode('utf-8') -def test_graphiql_default_title(client): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) +def test_graphiql_default_title(app, client): + with app.test_request_context(): + response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) assert 'GraphiQL' in response.data.decode('utf-8') @pytest.mark.parametrize('app', [create_app(graphiql=True, graphiql_html_title="Awesome")]) def test_graphiql_custom_title(app, client): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) + with app.test_request_context(): + response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) assert 'Awesome' in response.data.decode('utf-8') diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 71b4df6..3e49e55 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -16,12 +16,24 @@ @pytest.fixture -def app(): - return create_app() +def app(request): + # import app factory pattern + app = create_app() + # pushes an application context manually + ctx = app.app_context() + ctx.push() + return app -def url_string(**url_params): - string = url_for('graphql') + +@pytest.fixture +def client(app): + return app.test_client() + + +def url_string(app, **url_params): + with app.test_request_context(): + string = url_for('graphql') if url_params: string += '?' + urlencode(url_params) @@ -33,12 +45,16 @@ def response_json(response): return json.loads(response.data.decode()) -j = lambda **kwargs: json.dumps(kwargs) -jl = lambda **kwargs: json.dumps([kwargs]) +def json_dump_kwarg(**kwargs): + return json.dumps(kwargs) + + +def json_dump_kwarg_list(**kwargs): + return json.dumps([kwargs]) -def test_allows_get_with_query_param(client): - response = client.get(url_string(query='{test}')) +def test_allows_get_with_query_param(app, client): + response = client.get(url_string(app, query='{test}')) assert response.status_code == 200 assert response_json(response) == { @@ -46,8 +62,9 @@ def test_allows_get_with_query_param(client): } -def test_allows_get_with_variable_values(client): +def test_allows_get_with_variable_values(app, client): response = client.get(url_string( + app, query='query helloWho($who: String){ test(who: $who) }', variables=json.dumps({'who': "Dolly"}) )) @@ -58,8 +75,9 @@ def test_allows_get_with_variable_values(client): } -def test_allows_get_with_operation_name(client): +def test_allows_get_with_operation_name(app, client): response = client.get(url_string( + app, query=''' query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } @@ -80,8 +98,9 @@ def test_allows_get_with_operation_name(client): } -def test_reports_validation_errors(client): +def test_reports_validation_errors(app, client): response = client.get(url_string( + app, query='{ test, unknownOne, unknownTwo }' )) @@ -100,8 +119,9 @@ def test_reports_validation_errors(client): } -def test_errors_when_missing_operation_name(client): +def test_errors_when_missing_operation_name(app, client): response = client.get(url_string( + app, query=''' query TestQuery { test } mutation TestMutation { writeTest { test } } @@ -118,8 +138,9 @@ def test_errors_when_missing_operation_name(client): } -def test_errors_when_sending_a_mutation_via_get(client): +def test_errors_when_sending_a_mutation_via_get(app, client): response = client.get(url_string( + app, query=''' mutation TestMutation { writeTest { test } } ''' @@ -134,8 +155,9 @@ def test_errors_when_sending_a_mutation_via_get(client): } -def test_errors_when_selecting_a_mutation_within_a_get(client): +def test_errors_when_selecting_a_mutation_within_a_get(app, client): response = client.get(url_string( + app, query=''' query TestQuery { test } mutation TestMutation { writeTest { test } } @@ -153,8 +175,9 @@ def test_errors_when_selecting_a_mutation_within_a_get(client): } -def test_allows_mutation_to_exist_within_a_get(client): +def test_allows_mutation_to_exist_within_a_get(app, client): response = client.get(url_string( + app, query=''' query TestQuery { test } mutation TestMutation { writeTest { test } } @@ -168,8 +191,8 @@ def test_allows_mutation_to_exist_within_a_get(client): } -def test_allows_post_with_json_encoding(client): - response = client.post(url_string(), data=j(query='{test}'), content_type='application/json') +def test_allows_post_with_json_encoding(app, client): + response = client.post(url_string(app), data=json_dump_kwarg(query='{test}'), content_type='application/json') assert response.status_code == 200 assert response_json(response) == { @@ -177,8 +200,8 @@ def test_allows_post_with_json_encoding(client): } -def test_allows_sending_a_mutation_via_post(client): - response = client.post(url_string(), data=j(query='mutation TestMutation { writeTest { test } }'), content_type='application/json') +def test_allows_sending_a_mutation_via_post(app, client): + response = client.post(url_string(app), data=json_dump_kwarg(query='mutation TestMutation { writeTest { test } }'), content_type='application/json') assert response.status_code == 200 assert response_json(response) == { @@ -186,8 +209,8 @@ def test_allows_sending_a_mutation_via_post(client): } -def test_allows_post_with_url_encoding(client): - response = client.post(url_string(), data=urlencode(dict(query='{test}')), content_type='application/x-www-form-urlencoded') +def test_allows_post_with_url_encoding(app, client): + response = client.post(url_string(app), data=urlencode(dict(query='{test}')), content_type='application/x-www-form-urlencoded') assert response.status_code == 200 assert response_json(response) == { @@ -208,8 +231,8 @@ def test_allows_post_with_url_encoding(client): # } -def test_supports_post_json_query_with_string_variables(client): - response = client.post(url_string(), data=j( +def test_supports_post_json_query_with_string_variables(app, client): + response = client.post(url_string(app), data=json_dump_kwarg( query='query helloWho($who: String){ test(who: $who) }', variables=json.dumps({'who': "Dolly"}) ), content_type='application/json') @@ -220,8 +243,8 @@ def test_supports_post_json_query_with_string_variables(client): } -def test_supports_post_json_query_with_json_variables(client): - response = client.post(url_string(), data=j( +def test_supports_post_json_query_with_json_variables(app, client): + response = client.post(url_string(app), data=json_dump_kwarg( query='query helloWho($who: String){ test(who: $who) }', variables={'who': "Dolly"} ), content_type='application/json') @@ -232,8 +255,8 @@ def test_supports_post_json_query_with_json_variables(client): } -def test_supports_post_url_encoded_query_with_string_variables(client): - response = client.post(url_string(), data=urlencode(dict( +def test_supports_post_url_encoded_query_with_string_variables(app, client): + response = client.post(url_string(app), data=urlencode(dict( query='query helloWho($who: String){ test(who: $who) }', variables=json.dumps({'who': "Dolly"}) )), content_type='application/x-www-form-urlencoded') @@ -244,10 +267,11 @@ def test_supports_post_url_encoded_query_with_string_variables(client): } -def test_supports_post_json_quey_with_get_variable_values(client): +def test_supports_post_json_quey_with_get_variable_values(app, client): response = client.post(url_string( + app, variables=json.dumps({'who': "Dolly"}) - ), data=j( + ), data=json_dump_kwarg( query='query helloWho($who: String){ test(who: $who) }', ), content_type='application/json') @@ -257,8 +281,9 @@ def test_supports_post_json_quey_with_get_variable_values(client): } -def test_post_url_encoded_query_with_get_variable_values(client): +def test_post_url_encoded_query_with_get_variable_values(app, client): response = client.post(url_string( + app, variables=json.dumps({'who': "Dolly"}) ), data=urlencode(dict( query='query helloWho($who: String){ test(who: $who) }', @@ -270,8 +295,9 @@ def test_post_url_encoded_query_with_get_variable_values(client): } -def test_supports_post_raw_text_query_with_get_variable_values(client): +def test_supports_post_raw_text_query_with_get_variable_values(app, client): response = client.post(url_string( + app, variables=json.dumps({'who': "Dolly"}) ), data='query helloWho($who: String){ test(who: $who) }', @@ -284,8 +310,8 @@ def test_supports_post_raw_text_query_with_get_variable_values(client): } -def test_allows_post_with_operation_name(client): - response = client.post(url_string(), data=j( +def test_allows_post_with_operation_name(app, client): + response = client.post(url_string(app), data=json_dump_kwarg( query=''' query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } @@ -306,8 +332,9 @@ def test_allows_post_with_operation_name(client): } -def test_allows_post_with_get_operation_name(client): +def test_allows_post_with_get_operation_name(app, client): response = client.post(url_string( + app, operationName='helloWorld' ), data=''' query helloYou { test(who: "You"), ...shared } @@ -330,7 +357,7 @@ def test_allows_post_with_get_operation_name(client): @pytest.mark.parametrize('app', [create_app(pretty=True)]) def test_supports_pretty_printing(app, client): - response = client.get(url_string(query='{test}')) + response = client.get(url_string(app, query='{test}')) assert response.data.decode() == ( '{\n' @@ -343,15 +370,15 @@ def test_supports_pretty_printing(app, client): @pytest.mark.parametrize('app', [create_app(pretty=False)]) def test_not_pretty_by_default(app, client): - response = client.get(url_string(query='{test}')) + response = client.get(url_string(app, query='{test}')) assert response.data.decode() == ( '{"data":{"test":"Hello World"}}' ) -def test_supports_pretty_printing_by_request(client): - response = client.get(url_string(query='{test}', pretty='1')) +def test_supports_pretty_printing_by_request(app, client): + response = client.get(url_string(app, query='{test}', pretty='1')) assert response.data.decode() == ( '{\n' @@ -362,8 +389,8 @@ def test_supports_pretty_printing_by_request(client): ) -def test_handles_field_errors_caught_by_graphql(client): - response = client.get(url_string(query='{thrower}')) +def test_handles_field_errors_caught_by_graphql(app, client): + response = client.get(url_string(app, query='{thrower}')) assert response.status_code == 200 assert response_json(response) == { 'data': None, @@ -371,8 +398,8 @@ def test_handles_field_errors_caught_by_graphql(client): } -def test_handles_syntax_errors_caught_by_graphql(client): - response = client.get(url_string(query='syntaxerror')) +def test_handles_syntax_errors_caught_by_graphql(app, client): + response = client.get(url_string(app, query='syntaxerror')) assert response.status_code == 400 assert response_json(response) == { 'errors': [{'locations': [{'column': 1, 'line': 1}], @@ -381,8 +408,8 @@ def test_handles_syntax_errors_caught_by_graphql(client): } -def test_handles_errors_caused_by_a_lack_of_query(client): - response = client.get(url_string()) +def test_handles_errors_caused_by_a_lack_of_query(app, client): + response = client.get(url_string(app)) assert response.status_code == 400 assert response_json(response) == { @@ -390,8 +417,8 @@ def test_handles_errors_caused_by_a_lack_of_query(client): } -def test_handles_batch_correctly_if_is_disabled(client): - response = client.post(url_string(), data='[]', content_type='application/json') +def test_handles_batch_correctly_if_is_disabled(app, client): + response = client.post(url_string(app), data='[]', content_type='application/json') assert response.status_code == 400 assert response_json(response) == { @@ -399,8 +426,8 @@ def test_handles_batch_correctly_if_is_disabled(client): } -def test_handles_incomplete_json_bodies(client): - response = client.post(url_string(), data='{"query":', content_type='application/json') +def test_handles_incomplete_json_bodies(app, client): + response = client.post(url_string(app), data='{"query":', content_type='application/json') assert response.status_code == 400 assert response_json(response) == { @@ -408,8 +435,9 @@ def test_handles_incomplete_json_bodies(client): } -def test_handles_plain_post_text(client): +def test_handles_plain_post_text(app, client): response = client.post(url_string( + app, variables=json.dumps({'who': "Dolly"}) ), data='query helloWho($who: String){ test(who: $who) }', @@ -421,8 +449,9 @@ def test_handles_plain_post_text(client): } -def test_handles_poorly_formed_variables(client): +def test_handles_poorly_formed_variables(app, client): response = client.get(url_string( + app, query='query helloWho($who: String){ test(who: $who) }', variables='who:You' )) @@ -432,8 +461,8 @@ def test_handles_poorly_formed_variables(client): } -def test_handles_unsupported_http_methods(client): - response = client.put(url_string(query='{test}')) +def test_handles_unsupported_http_methods(app, client): + response = client.put(url_string(app, query='{test}')) assert response.status_code == 405 assert response.headers['Allow'] in ['GET, POST', 'HEAD, GET, POST, OPTIONS'] assert response_json(response) == { @@ -441,8 +470,8 @@ def test_handles_unsupported_http_methods(client): } -def test_passes_request_into_request_context(client): - response = client.get(url_string(query='{request}', q='testing')) +def test_passes_request_into_request_context(app, client): + response = client.get(url_string(app, query='{request}', q='testing')) assert response.status_code == 200 assert response_json(response) == { @@ -453,8 +482,8 @@ def test_passes_request_into_request_context(client): @pytest.mark.parametrize('app', [create_app(get_context_value=lambda:"CUSTOM CONTEXT")]) -def test_supports_pretty_printing_with_custom_context(app, client): - response = client.get(url_string(query='{context}')) +def test_passes_custom_context_into_context(app, client): + response = client.get(url_string(app, query='{context}')) assert response.status_code == 200 assert response_json(response) == { @@ -464,10 +493,10 @@ def test_supports_pretty_printing_with_custom_context(app, client): } -def test_post_multipart_data(client): +def test_post_multipart_data(app, client): query = 'mutation TestMutation { writeTest { test } }' response = client.post( - url_string(), + url_string(app), data={ 'query': query, 'file': (StringIO(), 'text1.txt'), @@ -482,8 +511,8 @@ def test_post_multipart_data(client): @pytest.mark.parametrize('app', [create_app(batch=True)]) def test_batch_allows_post_with_json_encoding(app, client): response = client.post( - url_string(), - data=jl( + url_string(app), + data=json_dump_kwarg_list( # id=1, query='{test}' ), @@ -500,8 +529,8 @@ def test_batch_allows_post_with_json_encoding(app, client): @pytest.mark.parametrize('app', [create_app(batch=True)]) def test_batch_supports_post_json_query_with_json_variables(app, client): response = client.post( - url_string(), - data=jl( + url_string(app), + data=json_dump_kwarg_list( # id=1, query='query helloWho($who: String){ test(who: $who) }', variables={'who': "Dolly"} @@ -519,8 +548,8 @@ def test_batch_supports_post_json_query_with_json_variables(app, client): @pytest.mark.parametrize('app', [create_app(batch=True)]) def test_batch_allows_post_with_operation_name(app, client): response = client.post( - url_string(), - data=jl( + url_string(app), + data=json_dump_kwarg_list( # id=1, query=''' query helloYou { test(who: "You"), ...shared } diff --git a/tox.ini b/tox.ini index 303a459..fb4b51e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,32 @@ [tox] -envlist = flake8,import-order,py35,py27,py33,py34,py36,py37,pypy -skipsdist = true +envlist = + py{27,35,36,37} + flake8,import-order,manifest +; requires = tox-conda [testenv] +passenv = * setenv = PYTHONPATH = {toxinidir} -deps = - pytest>=2.7.2 - pytest-flask>=0.10.0 - graphql-core>=2.1,<3 - graphql-server-core>=1.1,<2 - Flask>=0.10.0 - pytest-cov -commands = - py{py,27,34,35,36,37}: py.test tests {posargs} +install_command = python -m pip install --ignore-installed {opts} {packages} +deps = -e.[test] +commands = + pytest --cov=flask_graphql tests {posargs} [testenv:flake8] basepython=python3.6 -deps = flake8 +deps = -e.[dev] commands = flake8 flask_graphql [testenv:import-order] basepython=python3.6 -deps = - isort - graphql-core>=2.1 - Flask>=0.10.0 +deps = -e.[dev] commands = isort --check-only flask_graphql/ -rc + +[testenv:manifest] +basepython = python3.6 +deps = -e.[dev] +commands = + check-manifest -v \ No newline at end of file From 9691bafb6d98405cf0b283dbb272ad690b643643 Mon Sep 17 00:00:00 2001 From: Manuel Bojato <30560560+KingDarBoja@users.noreply.github.com> Date: Sat, 14 Mar 2020 10:36:17 -0500 Subject: [PATCH 3/7] Add anaconda cloud badge to flask-graphql (#73) * Add anaconda cloud badge to flask-graphql * docs: update readme and contributing link --- CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++++-------- README.md | 18 +++++++++++--- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9525b09..db93994 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,38 +12,83 @@ We welcome all kinds of contributions: ## Getting started -If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/flask-graphql/issues) and [projects](https://github.com/graphql-python/flask-graphql/projects) in progress - someone could already be working on something similar and you can help out. +If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/flask-graphql/issues) and [pull requests](https://github.com/graphql-python/flask-graphql/pulls) in progress - someone could already be working on something similar and you can help out. ## Project setup -After cloning this repo, ensure dependencies are installed by running: +### Development with virtualenv (recommended) + +After cloning this repo, create a virtualenv: + +```console +virtualenv flask-graphql-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. `flask-graphql-dev`) with the following command: ```sh -make dev-setup +conda create -n flask-graphql-dev python=3.8 ``` +Then activate the environment with `conda activate flask-graphql-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 -make tests +pytest tests --cov=flask_graphql -vv ``` -## Development on Conda +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. -In order to run `tox` command on conda, you must create a new env (e.g. `flask-grapqhl-dev`) with the following command: +You can also test on several python environments by using tox. -```sh -conda create -n flask-grapqhl-dev python=3.8 +### Running tox on virtualenv + +Install tox: + +```console +pip install tox ``` -Then activate the environment with `conda activate flask-grapqhl-dev` and install [tox-conda](https://github.com/tox-dev/tox-conda): +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 ``` -Uncomment the `requires = tox-conda` line on `tox.ini` file and that's it! Run `tox` and you will see all the environments being created and all passing tests. :rocket: +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: diff --git a/README.md b/README.md index 48f5ea8..af4e1ac 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,21 @@ # Flask-GraphQL -[![Build Status](https://travis-ci.org/graphql-python/flask-graphql.svg?branch=master)](https://travis-ci.org/graphql-python/flask-graphql) [![Coverage Status](https://coveralls.io/repos/graphql-python/flask-graphql/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/flask-graphql?branch=master) [![PyPI version](https://badge.fury.io/py/flask-graphql.svg)](https://badge.fury.io/py/flask-graphql) - Adds GraphQL support to your Flask application. +[![travis][travis-image]][travis-url] +[![pypi][pypi-image]][pypi-url] +[![Anaconda-Server Badge][conda-image]][conda-url] +[![coveralls][coveralls-image]][coveralls-url] + +[travis-image]: https://travis-ci.org/graphql-python/flask-graphql.svg?branch=master +[travis-url]: https://travis-ci.org/graphql-python/flask-graphql +[pypi-image]: https://img.shields.io/pypi/v/flask-graphql.svg?style=flat +[pypi-url]: https://pypi.org/project/flask-graphql/ +[coveralls-image]: https://coveralls.io/repos/graphql-python/flask-graphql/badge.svg?branch=master&service=github +[coveralls-url]: https://coveralls.io/github/graphql-python/flask-graphql?branch=master +[conda-image]: https://img.shields.io/conda/vn/conda-forge/flask-graphql.svg +[conda-url]: https://anaconda.org/conda-forge/flask-graphql + ## Usage Just use the `GraphQLView` view from `flask_graphql` @@ -41,4 +53,4 @@ class UserRootValue(GraphQLView): ``` ## Contributing -See [CONTRIBUTING.md](contributing.md) +See [CONTRIBUTING.md](CONTRIBUTING.md) From 5f79ab8f69a6ea4cfec5963eacecaa56a966b28e Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 11:26:53 -0500 Subject: [PATCH 4/7] chore: use graphql-server to re-export flask --- .coveragerc | 2 - .gitignore | 209 +++++++++++- .travis.yml | 4 +- MANIFEST.in | 9 +- flask_graphql/__init__.py | 5 +- flask_graphql/blueprint.py | 17 - flask_graphql/graphqlview.py | 154 --------- flask_graphql/render_graphiql.py | 138 -------- setup.cfg | 4 +- setup.py | 26 +- tests/app.py | 15 +- tests/schema.py | 51 +-- tests/test_graphiqlview.py | 32 +- tests/test_graphqlview.py | 544 ++++++++++++++++--------------- tox.ini | 16 +- 15 files changed, 568 insertions(+), 658 deletions(-) delete mode 100644 .coveragerc delete mode 100644 flask_graphql/blueprint.py delete mode 100644 flask_graphql/graphqlview.py delete mode 100644 flask_graphql/render_graphiql.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 87df271..0000000 --- a/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[report] -omit = */tests/* diff --git a/.gitignore b/.gitignore index 8f0e0ae..642f015 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,204 @@ -*.pyc -.idea -.cache -.tox + +# Created by https://www.gitignore.io/api/python,intellij+all,visualstudiocode +# Edit at https://www.gitignore.io/?templates=python,intellij+all,visualstudiocode + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg *.egg -*.egg-info +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.venv/ .coverage -/build/ +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### VisualStudioCode ### +.vscode + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history -/dist/ -/.pytest_cache +# End of https://www.gitignore.io/api/python,intellij+all,visualstudiocode diff --git a/.travis.yml b/.travis.yml index c59b86e..52554b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,9 @@ language: python sudo: false python: - - 2.7 - - 3.5 - 3.6 - 3.7 - # - 3.8 + - 3.8 cache: pip install: diff --git a/MANIFEST.in b/MANIFEST.in index 29c54bf..5854051 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,13 +2,10 @@ include LICENSE include README.md include CONTRIBUTING.md -recursive-include flask_graphql/static * -recursive-include flask_graphql/templates * -recursive-include tests *.py - +include tox.ini include Makefile -include .coveragerc -include tox.ini +recursive-include flask_graphql *.py +recursive-include tests *.py global-exclude *.py[co] __pycache__ diff --git a/flask_graphql/__init__.py b/flask_graphql/__init__.py index 4c30423..5b3ee3c 100644 --- a/flask_graphql/__init__.py +++ b/flask_graphql/__init__.py @@ -1,4 +1,3 @@ -from .blueprint import GraphQL -from .graphqlview import GraphQLView +from graphql_server.flask.graphqlview import GraphQLView -__all__ = ['GraphQL', 'GraphQLView'] +__all__ = ['GraphQLView'] diff --git a/flask_graphql/blueprint.py b/flask_graphql/blueprint.py deleted file mode 100644 index b02266a..0000000 --- a/flask_graphql/blueprint.py +++ /dev/null @@ -1,17 +0,0 @@ -import warnings - -from flask import Blueprint - -from .graphqlview import GraphQLView - - -class GraphQL(object): - def __init__(self, app, schema, **options): - self.app = app - warnings.warn('GraphQL Blueprint is now deprecated, please use GraphQLView directly') - self.blueprint = Blueprint('graphql', __name__, - template_folder='templates') - - app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, **options)) - - self.app.register_blueprint(self.blueprint) diff --git a/flask_graphql/graphqlview.py b/flask_graphql/graphqlview.py deleted file mode 100644 index 25e382f..0000000 --- a/flask_graphql/graphqlview.py +++ /dev/null @@ -1,154 +0,0 @@ -from functools import partial - -from flask import Response, request -from flask.views import View -from graphql_server import (HttpQueryError, default_format_error, - encode_execution_results, json_encode, - load_json_body, run_http_query) - -from graphql.type.schema import GraphQLSchema - -from .render_graphiql import render_graphiql - - -class GraphQLView(View): - schema = None - executor = None - root_value = None - pretty = False - graphiql = False - backend = None - graphiql_version = None - graphiql_template = None - graphiql_html_title = None - middleware = None - batch = False - - methods = ['GET', 'POST', 'PUT', 'DELETE'] - - def __init__(self, **kwargs): - super(GraphQLView, self).__init__() - for key, value in kwargs.items(): - if hasattr(self, key): - setattr(self, key, value) - - assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' - - # noinspection PyUnusedLocal - def get_root_value(self): - return self.root_value - - def get_context_value(self): - return request - - def get_middleware(self): - return self.middleware - - def get_backend(self): - return self.backend - - def get_executor(self): - return self.executor - - def render_graphiql(self, params, result): - return render_graphiql( - params=params, - result=result, - graphiql_version=self.graphiql_version, - graphiql_template=self.graphiql_template, - graphiql_html_title=self.graphiql_html_title, - ) - - format_error = staticmethod(default_format_error) - encode = staticmethod(json_encode) - - def dispatch_request(self): - try: - request_method = request.method.lower() - data = self.parse_body() - - show_graphiql = request_method == 'get' and self.should_display_graphiql() - catch = show_graphiql - - pretty = self.pretty or show_graphiql or request.args.get('pretty') - - extra_options = {} - executor = self.get_executor() - if executor: - # We only include it optionally since - # executor is not a valid argument in all backends - extra_options['executor'] = executor - - execution_results, all_params = run_http_query( - self.schema, - request_method, - data, - query_data=request.args, - batch_enabled=self.batch, - catch=catch, - backend=self.get_backend(), - - # Execute options - root_value=self.get_root_value(), - context_value=self.get_context_value(), - middleware=self.get_middleware(), - **extra_options - ) - result, status_code = encode_execution_results( - execution_results, - is_batch=isinstance(data, list), - format_error=self.format_error, - encode=partial(self.encode, pretty=pretty) - ) - - if show_graphiql: - return self.render_graphiql( - params=all_params[0], - result=result - ) - - return Response( - result, - status=status_code, - content_type='application/json' - ) - - except HttpQueryError as e: - return Response( - self.encode({ - 'errors': [self.format_error(e)] - }), - status=e.status_code, - headers=e.headers, - content_type='application/json' - ) - - # Flask - # noinspection PyBroadException - def parse_body(self): - # We use mimetype here since we don't need the other - # information provided by content_type - content_type = request.mimetype - if content_type == 'application/graphql': - return {'query': request.data.decode('utf8')} - - elif content_type == 'application/json': - return load_json_body(request.data.decode('utf8')) - - elif content_type in ('application/x-www-form-urlencoded', 'multipart/form-data'): - return request.form - - return {} - - def should_display_graphiql(self): - if not self.graphiql or 'raw' in request.args: - return False - - return self.request_wants_html() - - def request_wants_html(self): - best = request.accept_mimetypes \ - .best_match(['application/json', 'text/html']) - return best == 'text/html' and \ - request.accept_mimetypes[best] > \ - request.accept_mimetypes['application/json'] diff --git a/flask_graphql/render_graphiql.py b/flask_graphql/render_graphiql.py deleted file mode 100644 index 6bfb2da..0000000 --- a/flask_graphql/render_graphiql.py +++ /dev/null @@ -1,138 +0,0 @@ -from flask import render_template_string - -GRAPHIQL_VERSION = '0.11.11' - -TEMPLATE = ''' - - - - {{graphiql_html_title|default("GraphiQL", true)}} - - - - - - - - - - - -''' - - -def render_graphiql(params, result, graphiql_version=None, - graphiql_template=None, graphiql_html_title=None): - graphiql_version = graphiql_version or GRAPHIQL_VERSION - template = graphiql_template or TEMPLATE - - return render_template_string( - template, - graphiql_version=graphiql_version, - graphiql_html_title=graphiql_html_title, - result=result, - params=params - ) diff --git a/setup.cfg b/setup.cfg index 34e51b5..b6ff204 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [flake8] exclude = tests,scripts,setup.py,docs -max-line-length = 160 +max-line-length = 88 [isort] known_first_party=graphql [tool:pytest] -norecursedirs = venv .tox .cache +norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache diff --git a/setup.py b/setup.py index a851be7..9a7c1cc 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,28 @@ from setuptools import setup, find_packages install_requires = [ - "flask>=0.7.0", - "graphql-core>=2.3,<3", - "graphql-server-core>=1.1,<2", + "graphql-server>=3.0.0b1", ] tests_requires = [ - 'pytest>=2.7.2', - 'pytest-cov==2.8.1', - 'pytest-flask>=0.10.0', + "pytest>=5.4,<5.5", + "pytest-cov>=2.8,<3", ] dev_requires = [ - 'flake8==3.7.9', - 'isort<4.0.0', - 'check-manifest>=0.40,<1', + "flake8>=3.7,<4", + "isort>=4,<5", + "check-manifest>=0.40,<1", ] + tests_requires +with open("README.md", encoding="utf-8") as readme_file: + readme = readme_file.read() + setup( name="Flask-GraphQL", version="2.0.1", description="Adds GraphQL support to your Flask application", - long_description=open("README.md").read(), + long_description=readme, long_description_content_type="text/markdown", url="https://github.com/graphql-python/flask-graphql", download_url="https://github.com/graphql-python/flask-graphql/releases", @@ -33,13 +33,9 @@ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: 3.8", "License :: OSI Approved :: MIT License", ], keywords="api graphql protocol rest flask", diff --git a/tests/app.py b/tests/app.py index 0d38502..84f1d23 100644 --- a/tests/app.py +++ b/tests/app.py @@ -1,19 +1,18 @@ from flask import Flask + from flask_graphql import GraphQLView -from .schema import Schema -from graphql import GraphQLCachedBackend -# from quiver.backend import GraphQLQuiverBackend +from tests.schema import Schema -def create_app(path='/graphql', **kwargs): - # backend = GraphQLCachedBackend(GraphQLQuiverBackend({"async_framework": "PROMISE"})) - backend = None +def create_app(path="/graphql", **kwargs): app = Flask(__name__) app.debug = True - app.add_url_rule(path, view_func=GraphQLView.as_view('graphql', schema=Schema, backend=backend, **kwargs)) + app.add_url_rule( + path, view_func=GraphQLView.as_view("graphql", schema=Schema, **kwargs) + ) return app -if __name__ == '__main__': +if __name__ == "__main__": app = create_app(graphiql=True) app.run() diff --git a/tests/schema.py b/tests/schema.py index f841672..fdb5c9a 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -1,4 +1,5 @@ -from graphql.type.definition import GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType +from graphql.type.definition import (GraphQLArgument, GraphQLField, + GraphQLNonNull, GraphQLObjectType) from graphql.type.scalars import GraphQLString from graphql.type.schema import GraphQLSchema @@ -8,31 +9,39 @@ def resolve_raises(*_): QueryRootType = GraphQLObjectType( - name='QueryRoot', + name="QueryRoot", fields={ - 'thrower': GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_raises), - 'request': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: info.context.args.get('q')), - 'context': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: info.context), - 'test': GraphQLField( - type=GraphQLString, - args={ - 'who': GraphQLArgument(GraphQLString) - }, - resolver=lambda obj, info, who='World': 'Hello %s' % who - ) - } + "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_raises), + "request": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"].args.get("q"), + ), + "context": GraphQLField( + GraphQLObjectType( + name="context", + fields={ + "session": GraphQLField(GraphQLString), + "request": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"], + ), + }, + ), + resolve=lambda obj, info: info.context, + ), + "test": GraphQLField( + type_=GraphQLString, + args={"who": GraphQLArgument(GraphQLString)}, + resolve=lambda obj, info, who="World": "Hello %s" % who, + ), + }, ) MutationRootType = GraphQLObjectType( - name='MutationRoot', + name="MutationRoot", fields={ - 'writeTest': GraphQLField( - type=QueryRootType, - resolver=lambda *_: QueryRootType - ) - } + "writeTest": GraphQLField(type_=QueryRootType, resolve=lambda *_: QueryRootType) + }, ) Schema = GraphQLSchema(QueryRootType, MutationRootType) diff --git a/tests/test_graphiqlview.py b/tests/test_graphiqlview.py index aa92202..4a55710 100644 --- a/tests/test_graphiqlview.py +++ b/tests/test_graphiqlview.py @@ -1,7 +1,7 @@ import pytest +from flask import url_for from .app import create_app -from flask import url_for @pytest.fixture @@ -22,33 +22,39 @@ def client(app): def test_graphiql_is_enabled(app, client): with app.test_request_context(): - response = client.get(url_for('graphql', externals=False), headers={'Accept': 'text/html'}) + response = client.get( + url_for("graphql", externals=False), headers={"Accept": "text/html"} + ) assert response.status_code == 200 def test_graphiql_renders_pretty(app, client): with app.test_request_context(): - response = client.get(url_for('graphql', query='{test}'), headers={'Accept': 'text/html'}) + response = client.get( + url_for("graphql", query="{test}"), headers={"Accept": "text/html"} + ) assert response.status_code == 200 pretty_response = ( - '{\n' + "{\n" ' "data": {\n' ' "test": "Hello World"\n' - ' }\n' - '}' - ).replace("\"", "\\\"").replace("\n", "\\n") + " }\n" + "}".replace('"', '\\"').replace("\n", "\\n") + ) - assert pretty_response in response.data.decode('utf-8') + assert pretty_response in response.data.decode("utf-8") def test_graphiql_default_title(app, client): with app.test_request_context(): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) - assert 'GraphiQL' in response.data.decode('utf-8') + response = client.get(url_for("graphql"), headers={"Accept": "text/html"}) + assert "GraphiQL" in response.data.decode("utf-8") -@pytest.mark.parametrize('app', [create_app(graphiql=True, graphiql_html_title="Awesome")]) +@pytest.mark.parametrize( + "app", [create_app(graphiql=True, graphiql_html_title="Awesome")] +) def test_graphiql_custom_title(app, client): with app.test_request_context(): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) - assert 'Awesome' in response.data.decode('utf-8') + response = client.get(url_for("graphql"), headers={"Accept": "text/html"}) + assert "Awesome" in response.data.decode("utf-8") diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index 3e49e55..961a8e0 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -1,18 +1,11 @@ -import pytest import json +from io import StringIO +from urllib.parse import urlencode -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode +import pytest +from flask import url_for from .app import create_app -from flask import url_for @pytest.fixture @@ -33,10 +26,10 @@ def client(app): def url_string(app, **url_params): with app.test_request_context(): - string = url_for('graphql') + string = url_for("graphql") if url_params: - string += '?' + urlencode(url_params) + string += "?" + urlencode(url_params) return string @@ -54,357 +47,354 @@ def json_dump_kwarg_list(**kwargs): def test_allows_get_with_query_param(app, client): - response = client.get(url_string(app, query='{test}')) + response = client.get(url_string(app, query="{test}")) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_get_with_variable_values(app, client): - response = client.get(url_string( - app, - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - )) + response = client.get( + url_string( + app, + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ) + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_allows_get_with_operation_name(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - operationName='helloWorld' - )) + """, + operationName="helloWorld", + ) + ) assert response.status_code == 200 assert response_json(response) == { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } + "data": {"test": "Hello World", "shared": "Hello Everyone"} } def test_reports_validation_errors(app, client): - response = client.get(url_string( - app, - query='{ test, unknownOne, unknownTwo }' - )) + response = client.get(url_string(app, query="{ test, unknownOne, unknownTwo }")) assert response.status_code == 400 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Cannot query field "unknownOne" on type "QueryRoot".', - 'locations': [{'line': 1, 'column': 9}] + "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", + "locations": [{"line": 1, "column": 9}], + "path": None, }, { - 'message': 'Cannot query field "unknownTwo" on type "QueryRoot".', - 'locations': [{'line': 1, 'column': 21}] - } + "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", + "locations": [{"line": 1, "column": 21}], + "path": None, + }, ] } def test_errors_when_missing_operation_name(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } - ''' - )) + """, + ) + ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Must provide operation name if query contains multiple operations.' + "message": "Must provide operation name if query contains multiple operations.", # noqa: E501 + "locations": None, + "path": None, } ] } def test_errors_when_sending_a_mutation_via_get(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" mutation TestMutation { writeTest { test } } - ''' - )) + """, + ) + ) assert response.status_code == 405 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Can only perform a mutation operation from a POST request.' + "message": "Can only perform a mutation operation from a POST request.", + "locations": None, + "path": None, } ] } def test_errors_when_selecting_a_mutation_within_a_get(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } - ''', - operationName='TestMutation' - )) + """, + operationName="TestMutation", + ) + ) assert response.status_code == 405 assert response_json(response) == { - 'errors': [ + "errors": [ { - 'message': 'Can only perform a mutation operation from a POST request.' + "message": "Can only perform a mutation operation from a POST request.", + "locations": None, + "path": None, } ] } def test_allows_mutation_to_exist_within_a_get(app, client): - response = client.get(url_string( - app, - query=''' + response = client.get( + url_string( + app, + query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } - ''', - operationName='TestQuery' - )) + """, + operationName="TestQuery", + ) + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_post_with_json_encoding(app, client): - response = client.post(url_string(app), data=json_dump_kwarg(query='{test}'), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg(query="{test}"), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_sending_a_mutation_via_post(app, client): - response = client.post(url_string(app), data=json_dump_kwarg(query='mutation TestMutation { writeTest { test } }'), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg(query="mutation TestMutation { writeTest { test } }"), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'writeTest': {'test': 'Hello World'}} - } + assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} def test_allows_post_with_url_encoding(app, client): - response = client.post(url_string(app), data=urlencode(dict(query='{test}')), content_type='application/x-www-form-urlencoded') + response = client.post( + url_string(app), + data=urlencode(dict(query="{test}")), + content_type="application/x-www-form-urlencoded", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello World"} - } - - -# def test_benchmark(client, benchmark): -# url = url_string() -# data = urlencode(dict(query='{test}')) -# def fun(): -# return client.post(url_string(), data=data, content_type='application/x-www-form-urlencoded') - -# response = benchmark(fun) -# assert response.status_code == 200 -# assert response_json(response) == { -# 'data': {'test': "Hello World"} -# } + assert response_json(response) == {"data": {"test": "Hello World"}} def test_supports_post_json_query_with_string_variables(app, client): - response = client.post(url_string(app), data=json_dump_kwarg( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - ), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_json_query_with_json_variables(app, client): - response = client.post(url_string(app), data=json_dump_kwarg( - query='query helloWho($who: String){ test(who: $who) }', - variables={'who': "Dolly"} - ), content_type='application/json') + response = client.post( + url_string(app), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + variables={"who": "Dolly"}, + ), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_url_encoded_query_with_string_variables(app, client): - response = client.post(url_string(app), data=urlencode(dict( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - )), content_type='application/x-www-form-urlencoded') + response = client.post( + url_string(app), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ) + ), + content_type="application/x-www-form-urlencoded", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_json_quey_with_get_variable_values(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), data=json_dump_kwarg( - query='query helloWho($who: String){ test(who: $who) }', - ), content_type='application/json') + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",), + content_type="application/json", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_post_url_encoded_query_with_get_variable_values(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), data=urlencode(dict( - query='query helloWho($who: String){ test(who: $who) }', - )), content_type='application/x-www-form-urlencoded') + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + content_type="application/x-www-form-urlencoded", + ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_raw_text_query_with_get_variable_values(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), - data='query helloWho($who: String){ test(who: $who) }', - content_type='application/graphql' + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data="query helloWho($who: String){ test(who: $who) }", + content_type="application/graphql", ) assert response.status_code == 200 - assert response_json(response) == { - 'data': {'test': "Hello Dolly"} - } + assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_allows_post_with_operation_name(app, client): - response = client.post(url_string(app), data=json_dump_kwarg( - query=''' + response = client.post( + url_string(app), + data=json_dump_kwarg( + query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - operationName='helloWorld' - ), content_type='application/json') + """, + operationName="helloWorld", + ), + content_type="application/json", + ) assert response.status_code == 200 assert response_json(response) == { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } + "data": {"test": "Hello World", "shared": "Hello Everyone"} } def test_allows_post_with_get_operation_name(app, client): - response = client.post(url_string( - app, - operationName='helloWorld' - ), data=''' + response = client.post( + url_string(app, operationName="helloWorld"), + data=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - content_type='application/graphql') + """, + content_type="application/graphql", + ) assert response.status_code == 200 assert response_json(response) == { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } + "data": {"test": "Hello World", "shared": "Hello Everyone"} } -@pytest.mark.parametrize('app', [create_app(pretty=True)]) +@pytest.mark.parametrize("app", [create_app(pretty=True)]) def test_supports_pretty_printing(app, client): - response = client.get(url_string(app, query='{test}')) + response = client.get(url_string(app, query="{test}")) assert response.data.decode() == ( - '{\n' - ' "data": {\n' - ' "test": "Hello World"\n' - ' }\n' - '}' + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) -@pytest.mark.parametrize('app', [create_app(pretty=False)]) +@pytest.mark.parametrize("app", [create_app(pretty=False)]) def test_not_pretty_by_default(app, client): - response = client.get(url_string(app, query='{test}')) + response = client.get(url_string(app, query="{test}")) - assert response.data.decode() == ( - '{"data":{"test":"Hello World"}}' - ) + assert response.data.decode() == '{"data":{"test":"Hello World"}}' def test_supports_pretty_printing_by_request(app, client): - response = client.get(url_string(app, query='{test}', pretty='1')) + response = client.get(url_string(app, query="{test}", pretty="1")) assert response.data.decode() == ( - '{\n' - ' "data": {\n' - ' "test": "Hello World"\n' - ' }\n' - '}' + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) def test_handles_field_errors_caught_by_graphql(app, client): - response = client.get(url_string(app, query='{thrower}')) + response = client.get(url_string(app, query="{thrower}")) assert response.status_code == 200 assert response_json(response) == { - 'data': None, - 'errors': [{'locations': [{'column': 2, 'line': 1}], 'path': ['thrower'], 'message': 'Throws!'}] + "errors": [ + { + "locations": [{"column": 2, "line": 1}], + "path": ["thrower"], + "message": "Throws!", + } + ], + "data": None, } def test_handles_syntax_errors_caught_by_graphql(app, client): - response = client.get(url_string(app, query='syntaxerror')) + response = client.get(url_string(app, query="syntaxerror")) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'locations': [{'column': 1, 'line': 1}], - 'message': 'Syntax Error GraphQL (1:1) ' - 'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n'}] + "errors": [ + { + "locations": [{"column": 1, "line": 1}], + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None, + } + ] } @@ -413,162 +403,196 @@ def test_handles_errors_caused_by_a_lack_of_query(app, client): assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Must provide query string.'}] + "errors": [ + {"message": "Must provide query string.", "locations": None, "path": None} + ] } def test_handles_batch_correctly_if_is_disabled(app, client): - response = client.post(url_string(app), data='[]', content_type='application/json') + response = client.post(url_string(app), data="[]", content_type="application/json") assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Batch GraphQL requests are not enabled.'}] + "errors": [ + { + "message": "Batch GraphQL requests are not enabled.", + "locations": None, + "path": None, + } + ] } def test_handles_incomplete_json_bodies(app, client): - response = client.post(url_string(app), data='{"query":', content_type='application/json') + response = client.post( + url_string(app), data='{"query":', content_type="application/json" + ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'POST body sent invalid JSON.'}] + "errors": [ + {"message": "POST body sent invalid JSON.", "locations": None, "path": None} + ] } def test_handles_plain_post_text(app, client): - response = client.post(url_string( - app, - variables=json.dumps({'who': "Dolly"}) - ), - data='query helloWho($who: String){ test(who: $who) }', - content_type='text/plain' + response = client.post( + url_string(app, variables=json.dumps({"who": "Dolly"})), + data="query helloWho($who: String){ test(who: $who) }", + content_type="text/plain", ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Must provide query string.'}] + "errors": [ + {"message": "Must provide query string.", "locations": None, "path": None} + ] } def test_handles_poorly_formed_variables(app, client): - response = client.get(url_string( - app, - query='query helloWho($who: String){ test(who: $who) }', - variables='who:You' - )) + response = client.get( + url_string( + app, + query="query helloWho($who: String){ test(who: $who) }", + variables="who:You", + ) + ) assert response.status_code == 400 assert response_json(response) == { - 'errors': [{'message': 'Variables are invalid JSON.'}] + "errors": [ + {"message": "Variables are invalid JSON.", "locations": None, "path": None} + ] } def test_handles_unsupported_http_methods(app, client): - response = client.put(url_string(app, query='{test}')) + response = client.put(url_string(app, query="{test}")) assert response.status_code == 405 - assert response.headers['Allow'] in ['GET, POST', 'HEAD, GET, POST, OPTIONS'] + assert response.headers["Allow"] in ["GET, POST", "HEAD, GET, POST, OPTIONS"] assert response_json(response) == { - 'errors': [{'message': 'GraphQL only supports GET and POST requests.'}] + "errors": [ + { + "message": "GraphQL only supports GET and POST requests.", + "locations": None, + "path": None, + } + ] } def test_passes_request_into_request_context(app, client): - response = client.get(url_string(app, query='{request}', q='testing')) + response = client.get(url_string(app, query="{request}", q="testing")) assert response.status_code == 200 - assert response_json(response) == { - 'data': { - 'request': 'testing' - } - } + assert response_json(response) == {"data": {"request": "testing"}} -@pytest.mark.parametrize('app', [create_app(get_context_value=lambda:"CUSTOM CONTEXT")]) +@pytest.mark.parametrize("app", [create_app(context={"session": "CUSTOM CONTEXT"})]) def test_passes_custom_context_into_context(app, client): - response = client.get(url_string(app, query='{context}')) + response = client.get(url_string(app, query="{context { session request }}")) assert response.status_code == 200 - assert response_json(response) == { - 'data': { - 'context': 'CUSTOM CONTEXT' - } - } + res = response_json(response) + assert "data" in res + assert "session" in res["data"]["context"] + assert "request" in res["data"]["context"] + assert "CUSTOM CONTEXT" in res["data"]["context"]["session"] + assert "Request" in res["data"]["context"]["request"] + + +@pytest.mark.parametrize("app", [create_app(context="CUSTOM CONTEXT")]) +def test_context_remapped_if_not_mapping(app, client): + response = client.get(url_string(app, query="{context { session request }}")) + + assert response.status_code == 200 + res = response_json(response) + assert "data" in res + assert "session" in res["data"]["context"] + assert "request" in res["data"]["context"] + assert "CUSTOM CONTEXT" not in res["data"]["context"]["request"] + assert "Request" in res["data"]["context"]["request"] def test_post_multipart_data(app, client): - query = 'mutation TestMutation { writeTest { test } }' + query = "mutation TestMutation { writeTest { test } }" response = client.post( url_string(app), - data={ - 'query': query, - 'file': (StringIO(), 'text1.txt'), - }, - content_type='multipart/form-data' + data={"query": query, "file": (StringIO(), "text1.txt")}, + content_type="multipart/form-data", ) assert response.status_code == 200 - assert response_json(response) == {'data': {u'writeTest': {u'test': u'Hello World'}}} + assert response_json(response) == { + "data": {u"writeTest": {u"test": u"Hello World"}} + } -@pytest.mark.parametrize('app', [create_app(batch=True)]) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_allows_post_with_json_encoding(app, client): response = client.post( url_string(app), data=json_dump_kwarg_list( # id=1, - query='{test}' + query="{test}" ), - content_type='application/json' + content_type="application/json", ) assert response.status_code == 200 - assert response_json(response) == [{ - # 'id': 1, - 'data': {'test': "Hello World"} - }] + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello World"} + } + ] -@pytest.mark.parametrize('app', [create_app(batch=True)]) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_supports_post_json_query_with_json_variables(app, client): response = client.post( url_string(app), data=json_dump_kwarg_list( # id=1, - query='query helloWho($who: String){ test(who: $who) }', - variables={'who': "Dolly"} + query="query helloWho($who: String){ test(who: $who) }", + variables={"who": "Dolly"}, ), - content_type='application/json' + content_type="application/json", ) assert response.status_code == 200 - assert response_json(response) == [{ - # 'id': 1, - 'data': {'test': "Hello Dolly"} - }] + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello Dolly"} + } + ] -@pytest.mark.parametrize('app', [create_app(batch=True)]) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_allows_post_with_operation_name(app, client): response = client.post( url_string(app), data=json_dump_kwarg_list( # id=1, - query=''' + query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } - ''', - operationName='helloWorld' + """, + operationName="helloWorld", ), - content_type='application/json' + content_type="application/json", ) assert response.status_code == 200 - assert response_json(response) == [{ - # 'id': 1, - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello World", "shared": "Hello Everyone"} } - }] + ] diff --git a/tox.ini b/tox.ini index fb4b51e..72ab365 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,35,36,37} + py{36,37,38} flake8,import-order,manifest ; requires = tox-conda @@ -8,25 +8,25 @@ envlist = passenv = * setenv = PYTHONPATH = {toxinidir} -install_command = python -m pip install --ignore-installed {opts} {packages} +install_command = python -m pip install --pre --ignore-installed {opts} {packages} deps = -e.[test] commands = - pytest --cov=flask_graphql tests {posargs} + pytest tests --cov-report=term-missing --cov=flask_graphql {posargs} [testenv:flake8] -basepython=python3.6 +basepython=python3.8 deps = -e.[dev] commands = - flake8 flask_graphql + flake8 setup.py flask_graphql tests [testenv:import-order] -basepython=python3.6 +basepython=python3.8 deps = -e.[dev] commands = - isort --check-only flask_graphql/ -rc + isort -rc flask_graphql/ tests/ [testenv:manifest] -basepython = python3.6 +basepython = python3.8 deps = -e.[dev] commands = check-manifest -v \ No newline at end of file From dfd863dd14902d91d9cc4427b60eb337ee64134c Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 11:31:41 -0500 Subject: [PATCH 5/7] docs: update readme with v3-beta notes --- README.md | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index af4e1ac..7670f48 100644 --- a/README.md +++ b/README.md @@ -21,26 +21,58 @@ Adds GraphQL support to your Flask application. Just use the `GraphQLView` view from `flask_graphql` ```python +from flask import Flask from flask_graphql import GraphQLView -app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)) +from schema import schema + +app = Flask(__name__) + +app.add_url_rule('/graphql', view_func=GraphQLView.as_view( + 'graphql', + schema=schema, + graphiql=True, +)) # Optional, for adding batch query support (used in Apollo-Client) -app.add_url_rule('/graphql/batch', view_func=GraphQLView.as_view('graphql', schema=schema, batch=True)) +app.add_url_rule('/graphql/batch', view_func=GraphQLView.as_view( + 'graphql', + schema=schema, + batch=True +)) + +if __name__ == '__main__': + app.run() ``` -This will add `/graphql` and `/graphiql` endpoints to your app. +This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. + +### Special Note for Graphene v3 + +If you are using the `Schema` type of [Graphene](https://github.com/graphql-python/graphene) library, be sure to use the `graphql_schema` attribute to pass as schema on the `GraphQLView` view. Otherwise, the `GraphQLSchema` from `graphql-core` is the way to go. + +More info at [Graphene v3 release notes](https://github.com/graphql-python/graphene/wiki/v3-release-notes#graphene-schema-no-longer-subclasses-graphqlschema-type) and [GraphQL-core 3 usage](https://github.com/graphql-python/graphql-core#usage). + + +### Supported options for GraphQLView -### Supported options * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. - * `context`: A value to pass as the `context` to the `graphql()` function. - * `root_value`: The `root_value` you want to provide to `executor.execute`. + * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. + * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. - * `executor`: The `Executor` that you want to use to execute queries. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). + * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. + * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). + * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). + * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. + * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws. + * `headers`: An optional GraphQL string to use as the initial displayed request headers, if not provided, the stored headers will be used. + * `default_query`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If not provided, GraphiQL will use its own default query. +* `header_editor_enabled`: An optional boolean which enables the header editor when true. Defaults to **false**. +* `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**. You can also subclass `GraphQLView` and overwrite `get_root_value(self, request)` to have a dynamic root value per request. From d1ac3a56d3fe6b5eb9c78e9786a3ffd4b1b9e633 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 11:40:39 -0500 Subject: [PATCH 6/7] chore: add flask as extra on install requires --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a7c1cc..d8a626e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages install_requires = [ - "graphql-server>=3.0.0b1", + "graphql-server[flask]>=3.0.0b1", ] tests_requires = [ From 2f627ed4028f7dbe986cbbe5a89486822a2c0b83 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 12:18:55 -0500 Subject: [PATCH 7/7] docs: update contribute link --- CONTRIBUTING.md | 94 ------------------------------------------------- MANIFEST.in | 1 - README.md | 2 +- 3 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index db93994..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,94 +0,0 @@ -# Contributing - -Thanks for helping to make flask-graphql 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/flask-graphql/issues) and [pull requests](https://github.com/graphql-python/flask-graphql/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 flask-graphql-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. `flask-graphql-dev`) with the following command: - -```sh -conda create -n flask-graphql-dev python=3.8 -``` - -Then activate the environment with `conda activate flask-graphql-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=flask_graphql -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: - diff --git a/MANIFEST.in b/MANIFEST.in index 5854051..0fa13a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include LICENSE include README.md -include CONTRIBUTING.md include tox.ini include Makefile diff --git a/README.md b/README.md index 7670f48..3546b1d 100644 --- a/README.md +++ b/README.md @@ -85,4 +85,4 @@ class UserRootValue(GraphQLView): ``` ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) +Since v3, `flask-graphql` code lives at [graphql-server](https://github.com/graphql-python/graphql-server) repository to keep any breaking change on the base package on sync with all other integrations. In order to contribute, please take a look at [CONTRIBUTING.md](https://github.com/graphql-python/graphql-server/blob/master/CONTRIBUTING.md). pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy