diff --git a/.gitignore b/.gitignore index 89b1585..642f015 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +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/ +# End of https://www.gitignore.io/api/python,intellij+all,visualstudiocode diff --git a/.travis.yml b/.travis.yml index 824cf2f..52554b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,21 @@ language: python sudo: false -matrix: - include: - - python: pypy - env: TOX_ENV=pypy - - python: '2.7' - env: TOX_ENV=py27 - - python: '3.3' - env: TOX_ENV=py33 - - python: '3.4' - env: TOX_ENV=py34 - - python: '3.5' - env: TOX_ENV=py35,import-order,flake8 -cache: - directories: - - $HOME/.cache/pip - - $TRAVIS_BUILD_DIR/.tox +python: + - 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/MANIFEST.in b/MANIFEST.in index 497302a..0fa13a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,10 @@ +include LICENSE include README.md -recursive-include flask_graphql/static * -recursive-include flask_graphql/templates * + +include tox.ini +include Makefile + +recursive-include flask_graphql *.py +recursive-include tests *.py + +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 3e8f3d3..3546b1d 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,78 @@ # 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` ```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. @@ -37,3 +83,6 @@ class UserRootValue(GraphQLView): return request.user ``` + +## Contributing +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). diff --git a/README.rst b/README.rst deleted file mode 100644 index 00e0852..0000000 --- a/README.rst +++ /dev/null @@ -1,53 +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)) - -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][https://github.com/graphql/graphiql] when loaded directly - from a browser (a useful tool for debugging and exploration). - -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/graphql-flask/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/graphql-python/graphql-flask?branch=master -.. |PyPI version| image:: https://badge.fury.io/py/graphql-flask.svg - :target: https://badge.fury.io/py/graphql-flask 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 40f074b..0000000 --- a/flask_graphql/graphqlview.py +++ /dev/null @@ -1,258 +0,0 @@ -import json - -import six -from flask import Response, request -from flask.views import View -from werkzeug.exceptions import BadRequest, MethodNotAllowed - -from graphql import Source, execute, parse, validate -from graphql.error import format_error as format_graphql_error -from graphql.error import GraphQLError -from graphql.execution import ExecutionResult -from graphql.type.schema import GraphQLSchema -from graphql.utils.get_operation_ast import get_operation_ast - -from .render_graphiql import render_graphiql - - -class HttpError(Exception): - def __init__(self, response, message=None, *args, **kwargs): - self.response = response - self.message = message = message or response.description - super(HttpError, self).__init__(message, *args, **kwargs) - - -class GraphQLView(View): - schema = None - executor = None - root_value = None - context = None - pretty = False - graphiql = False - graphiql_version = None - graphiql_template = 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 not all((self.graphiql, self.batch)), 'Use either graphiql or batch processing' - assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' - - # noinspection PyUnusedLocal - def get_root_value(self, request): - return self.root_value - - def get_context(self, request): - if self.context is not None: - return self.context - return request - - def get_middleware(self, request): - return self.middleware - - def get_executor(self, request): - return self.executor - - def dispatch_request(self): - try: - if request.method.lower() not in ('get', 'post'): - raise HttpError(MethodNotAllowed(['GET', 'POST'], 'GraphQL only supports GET and POST requests.')) - - data = self.parse_body(request) - show_graphiql = self.graphiql and self.can_display_graphiql(data) - - if self.batch: - responses = [self.get_response(request, entry) for entry in data] - result = '[{}]'.format(','.join([response[0] for response in responses])) - status_code = max(responses, key=lambda response: response[1])[1] - else: - result, status_code = self.get_response(request, data, show_graphiql) - - if show_graphiql: - query, variables, operation_name, id = self.get_graphql_params(request, data) - return render_graphiql( - graphiql_version=self.graphiql_version, - graphiql_template=self.graphiql_template, - query=query, - variables=variables, - operation_name=operation_name, - result=result - ) - - return Response( - status=status_code, - response=result, - content_type='application/json' - ) - - except HttpError as e: - return Response( - self.json_encode(request, { - 'errors': [self.format_error(e)] - }), - status=e.response.code, - headers={'Allow': ['GET, POST']}, - content_type='application/json' - ) - - def get_response(self, request, data, show_graphiql=False): - query, variables, operation_name, id = self.get_graphql_params(request, data) - - execution_result = self.execute_graphql_request( - data, - query, - variables, - operation_name, - show_graphiql - ) - - status_code = 200 - if execution_result: - response = {} - - if execution_result.errors: - response['errors'] = [self.format_error(e) for e in execution_result.errors] - - if execution_result.invalid: - status_code = 400 - else: - status_code = 200 - response['data'] = execution_result.data - - if self.batch: - response = { - 'id': id, - 'payload': response, - 'status': status_code, - } - - result = self.json_encode(request, response, show_graphiql) - else: - result = None - - return result, status_code - - def json_encode(self, request, d, show_graphiql=False): - pretty = self.pretty or show_graphiql or request.args.get('pretty') - if not pretty: - return json.dumps(d, separators=(',', ':')) - - return json.dumps(d, sort_keys=True, - indent=2, separators=(',', ': ')) - - # noinspection PyBroadException - def parse_body(self, request): - content_type = self.get_content_type(request) - if content_type == 'application/graphql': - return {'query': request.data.decode()} - - elif content_type == 'application/json': - try: - request_json = json.loads(request.data.decode('utf8')) - if self.batch: - assert isinstance(request_json, list) - else: - assert isinstance(request_json, dict) - return request_json - except: - raise HttpError(BadRequest('POST body sent invalid JSON.')) - - elif content_type == 'application/x-www-form-urlencoded': - return request.form - - elif content_type == 'multipart/form-data': - return request.form - - return {} - - def execute(self, *args, **kwargs): - return execute(self.schema, *args, **kwargs) - - def execute_graphql_request(self, data, query, variables, operation_name, show_graphiql=False): - if not query: - if show_graphiql: - return None - raise HttpError(BadRequest('Must provide query string.')) - - try: - source = Source(query, name='GraphQL request') - ast = parse(source) - validation_errors = validate(self.schema, ast) - if validation_errors: - return ExecutionResult( - errors=validation_errors, - invalid=True, - ) - except Exception as e: - return ExecutionResult(errors=[e], invalid=True) - - if request.method.lower() == 'get': - operation_ast = get_operation_ast(ast, operation_name) - if operation_ast and operation_ast.operation != 'query': - if show_graphiql: - return None - raise HttpError(MethodNotAllowed( - ['POST'], 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation) - )) - - try: - return self.execute( - ast, - root_value=self.get_root_value(request), - variable_values=variables or {}, - operation_name=operation_name, - context_value=self.get_context(request), - middleware=self.get_middleware(request), - executor=self.get_executor(request) - ) - except Exception as e: - return ExecutionResult(errors=[e], invalid=True) - - @classmethod - def can_display_graphiql(cls, data): - raw = 'raw' in request.args or 'raw' in data - return not raw and cls.request_wants_html(request) - - @classmethod - def request_wants_html(cls, request): - best = request.accept_mimetypes \ - .best_match(['application/json', 'text/html']) - return best == 'text/html' and \ - request.accept_mimetypes[best] > \ - request.accept_mimetypes['application/json'] - - @staticmethod - def get_graphql_params(request, data): - query = request.args.get('query') or data.get('query') - variables = request.args.get('variables') or data.get('variables') - id = request.args.get('id') or data.get('id') - - if variables and isinstance(variables, six.text_type): - try: - variables = json.loads(variables) - except: - raise HttpError(BadRequest('Variables are invalid JSON.')) - - operation_name = request.args.get('operationName') or data.get('operationName') - - return query, variables, operation_name, id - - @staticmethod - def format_error(error): - if isinstance(error, GraphQLError): - return format_graphql_error(error) - - return {'message': six.text_type(error)} - - @staticmethod - def get_content_type(request): - # We use mimetype here since we don't need the other - # information provided by content_type - return request.mimetype diff --git a/flask_graphql/render_graphiql.py b/flask_graphql/render_graphiql.py deleted file mode 100644 index 12d8f4c..0000000 --- a/flask_graphql/render_graphiql.py +++ /dev/null @@ -1,131 +0,0 @@ -from flask import render_template_string - - -GRAPHIQL_VERSION = '0.7.1' - -TEMPLATE = ''' - - - - - - - - - - - - - -''' - - -def render_graphiql(graphiql_version=None, graphiql_template=None, **kwargs): - graphiql_version = graphiql_version or GRAPHIQL_VERSION - template = graphiql_template or TEMPLATE - - return render_template_string( - template, graphiql_version=graphiql_version, **kwargs) diff --git a/setup.cfg b/setup.cfg index bccff8a..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 -[pytest] -norecursedirs = venv .tox .cache +[tool:pytest] +norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache diff --git a/setup.py b/setup.py index f4d3cd7..d8a626e 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,52 @@ from setuptools import setup, find_packages -required_packages = ['graphql-core>=1.0', 'flask>=0.7.0'] +install_requires = [ + "graphql-server[flask]>=3.0.0b1", +] + +tests_requires = [ + "pytest>=5.4,<5.5", + "pytest-cov>=2.8,<3", +] + +dev_requires = [ + "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='1.4.0', - description='Adds GraphQL support to your Flask application', - long_description=open('README.rst').read(), - url='https://github.com/graphql-python/flask-graphql', - download_url='https://github.com/graphql-python/flask-graphql/releases', - author='Syrus Akbary', - author_email='me@syrusakbary.com', - license='MIT', + name="Flask-GraphQL", + version="2.0.1", + description="Adds GraphQL support to your Flask application", + 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", + author="Syrus Akbary", + author_email="me@syrusakbary.com", + license="MIT", classifiers=[ - '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.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: Implementation :: PyPy', - 'License :: OSI Approved :: MIT License', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: MIT License", ], - keywords='api graphql protocol rest flask', - packages=find_packages(exclude=['tests']), - install_requires=required_packages, - tests_require=['pytest>=2.7.3'], + keywords="api graphql protocol rest flask", + packages=find_packages(exclude=["tests"]), + 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', + platforms="any", ) diff --git a/tests/app.py b/tests/app.py index 9f11aee..84f1d23 100644 --- a/tests/app.py +++ b/tests/app.py @@ -1,15 +1,18 @@ from flask import Flask + from flask_graphql import GraphQLView -from .schema import Schema +from tests.schema import Schema -def create_app(path='/graphql', **kwargs): +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, **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 742ca09..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, args, context, info: context.args.get('q')), - 'context': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, args, context, info: context), - 'test': GraphQLField( - type=GraphQLString, - args={ - 'who': GraphQLArgument(GraphQLString) - }, - resolver=lambda obj, args, context, info: 'Hello %s' % (args.get('who') or 'World') - ) - } + "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 4a468e2..4a55710 100644 --- a/tests/test_graphiqlview.py +++ b/tests/test_graphiqlview.py @@ -1,28 +1,60 @@ import pytest +from flask import url_for from .app import create_app -from flask import url_for @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 + + +@pytest.fixture +def client(app): + return app.test_client() -def test_graphiql_is_enabled(client): - response = client.get(url_for('graphql'), headers={'Accept': 'text/html'}) +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' + "{\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") + + +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") + - assert pretty_response 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): + 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 1efcfbc..961a8e0 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -1,29 +1,35 @@ -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 -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 + + +@pytest.fixture +def client(app): + return app.test_client() -def url_string(**url_params): - string = url_for('graphql') + +def url_string(app, **url_params): + with app.test_request_context(): + string = url_for("graphql") if url_params: - string += '?' + urlencode(url_params) + string += "?" + urlencode(url_params) return string @@ -32,503 +38,561 @@ 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) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} -def test_allows_get_with_variable_values(client): - response = client.get(url_string( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - )) +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"}), + ) + ) 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(client): - response = client.get(url_string( - query=''' +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 } 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(client): - response = client.get(url_string( - query='{ test, unknownOne, unknownTwo }' - )) +def test_reports_validation_errors(app, client): + 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(client): - response = client.get(url_string( - query=''' +def test_errors_when_missing_operation_name(app, client): + 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(client): - response = client.get(url_string( - query=''' +def test_errors_when_sending_a_mutation_via_get(app, client): + 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(client): - response = client.get(url_string( - query=''' +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 } } - ''', - 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(client): - response = client.get(url_string( - query=''' +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 } } - ''', - 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(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) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} -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) == { - 'data': {'writeTest': {'test': 'Hello World'}} - } + assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} -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) == { - 'data': {'test': "Hello World"} - } + assert response_json(response) == {"data": {"test": "Hello World"}} -def test_supports_post_json_query_with_string_variables(client): - response = client.post(url_string(), data=j( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - ), content_type='application/json') +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", + ) 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(client): - response = client.post(url_string(), data=j( - query='query helloWho($who: String){ test(who: $who) }', - variables={'who': "Dolly"} - ), content_type='application/json') +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", + ) 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(client): - response = client.post(url_string(), data=urlencode(dict( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - )), content_type='application/x-www-form-urlencoded') +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", + ) 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(client): - response = client.post(url_string( - variables=json.dumps({'who': "Dolly"}) - ), data=j( - query='query helloWho($who: String){ test(who: $who) }', - ), content_type='application/json') +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", + ) 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(client): - response = client.post(url_string( - variables=json.dumps({'who': "Dolly"}) - ), data=urlencode(dict( - query='query helloWho($who: String){ test(who: $who) }', - )), content_type='application/x-www-form-urlencoded') +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", + ) 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(client): - response = client.post(url_string( - variables=json.dumps({'who': "Dolly"}) - ), - data='query helloWho($who: String){ test(who: $who) }', - content_type='application/graphql' +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", ) 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(client): - response = client.post(url_string(), data=j( - query=''' +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 } 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(client): - response = client.post(url_string( - operationName='helloWorld' - ), data=''' +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 } 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)]) -def test_supports_pretty_printing(client): - response = client.get(url_string(query='{test}')) +@pytest.mark.parametrize("app", [create_app(pretty=True)]) +def test_supports_pretty_printing(app, client): + 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)]) -def test_not_pretty_by_default(client): - response = client.get(url_string(query='{test}')) +@pytest.mark.parametrize("app", [create_app(pretty=False)]) +def test_not_pretty_by_default(app, client): + 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(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' - ' "data": {\n' - ' "test": "Hello World"\n' - ' }\n' - '}' + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) -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, - 'errors': [{'locations': [{'column': 2, 'line': 1}], 'message': 'Throws!'}] + "errors": [ + { + "locations": [{"column": 2, "line": 1}], + "path": ["thrower"], + "message": "Throws!", + } + ], + "data": None, } -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}], - 'message': 'Syntax Error GraphQL request (1:1) ' - 'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n'}] + "errors": [ + { + "locations": [{"column": 1, "line": 1}], + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None, + } + ] } -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) == { - 'errors': [{'message': 'Must provide query string.'}] + "errors": [ + {"message": "Must provide query string.", "locations": None, "path": None} + ] } -def test_handles_invalid_json_bodies(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) == { - 'errors': [{'message': 'POST body sent invalid JSON.'}] + "errors": [ + { + "message": "Batch GraphQL requests are not enabled.", + "locations": None, + "path": None, + } + ] } -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) == { - 'errors': [{'message': 'POST body sent invalid JSON.'}] + "errors": [ + {"message": "POST body sent invalid JSON.", "locations": None, "path": None} + ] } -def test_handles_plain_post_text(client): - response = client.post(url_string( - variables=json.dumps({'who': "Dolly"}) - ), - data='query helloWho($who: String){ test(who: $who) }', - content_type='text/plain' +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", ) 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(client): - response = client.get(url_string( - query='query helloWho($who: String){ test(who: $who) }', - variables='who:You' - )) +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", + ) + ) 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(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.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(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) == { - 'data': { - 'request': 'testing' - } - } + assert response_json(response) == {"data": {"request": "testing"}} + +@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 { session request }}")) -@pytest.mark.parametrize('app', [create_app(context="CUSTOM CONTEXT")]) -def test_supports_pretty_printing(client): - response = client.get(url_string(query='{context}')) + 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" 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 - 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" not in res["data"]["context"]["request"] + assert "Request" in res["data"]["context"]["request"] -def test_post_multipart_data(client): - query = 'mutation TestMutation { writeTest { test } }' +def test_post_multipart_data(app, client): + query = "mutation TestMutation { writeTest { test } }" response = client.post( - url_string(), - data= { - 'query': query, - 'file': (StringIO(), 'text1.txt'), - }, - content_type='multipart/form-data' + url_string(app), + 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)]) -def test_batch_allows_post_with_json_encoding(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(id=1, query='{test}'), - content_type='application/json' + url_string(app), + data=json_dump_kwarg_list( + # id=1, + query="{test}" + ), + content_type="application/json", ) assert response.status_code == 200 - assert response_json(response) == [{ - 'id': 1, - 'payload': { 'data': {'test': "Hello World"} }, - 'status': 200, - }] + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello World"} + } + ] -@pytest.mark.parametrize('app', [create_app(batch=True)]) -def test_batch_supports_post_json_query_with_json_variables(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( - id=1, - query='query helloWho($who: String){ test(who: $who) }', - variables={'who': "Dolly"} + url_string(app), + data=json_dump_kwarg_list( + # id=1, + 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, - 'payload': { 'data': {'test': "Hello Dolly"} }, - 'status': 200, - }] - - -@pytest.mark.parametrize('app', [create_app(batch=True)]) -def test_batch_allows_post_with_operation_name(client): + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello Dolly"} + } + ] + + +@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( - id=1, - query=''' + url_string(app), + data=json_dump_kwarg_list( + # id=1, + 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, - 'payload': { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } - }, - 'status': 200, - }] + assert response_json(response) == [ + { + # 'id': 1, + "data": {"test": "Hello World", "shared": "Hello Everyone"} + } + ] diff --git a/tox.ini b/tox.ini index 9d79799..72ab365 100644 --- a/tox.ini +++ b/tox.ini @@ -1,30 +1,32 @@ [tox] -envlist = flake8,import-order,py35,py27,py33,py34,pypy -skipsdist = true +envlist = + py{36,37,38} + flake8,import-order,manifest +; requires = tox-conda [testenv] +passenv = * setenv = PYTHONPATH = {toxinidir} -deps = - pytest>=2.7.2 - pytest-flask>=0.10.0 - graphql-core>=1.0 - Flask>=0.10.0 - pytest-cov -commands = - py{py,27,33,34,35}: py.test tests {posargs} +install_command = python -m pip install --pre --ignore-installed {opts} {packages} +deps = -e.[test] +commands = + pytest tests --cov-report=term-missing --cov=flask_graphql {posargs} [testenv:flake8] -basepython=python3.5 -deps = flake8 +basepython=python3.8 +deps = -e.[dev] commands = - flake8 flask_graphql + flake8 setup.py flask_graphql tests [testenv:import-order] -basepython=python3.5 -deps = - isort - graphql-core>=1.0 - Flask>=0.10.0 +basepython=python3.8 +deps = -e.[dev] +commands = + isort -rc flask_graphql/ tests/ + +[testenv:manifest] +basepython = python3.8 +deps = -e.[dev] commands = - isort --check-only flask_graphql/ -rc + check-manifest -v \ No newline at end of file