From 8b3684147ee949439a66c7416e1335ecd45cc0ab Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 12:55:14 -0500 Subject: [PATCH] chore: use graphql-server to re-export aiohttp --- .flake8 | 4 - .gitignore | 176 ++++++++++- .pylintrc | 413 ------------------------ .travis.yml | 36 +-- MANIFEST.in | 5 +- Makefile | 5 + README.md | 79 ++--- aiohttp_graphql/__init__.py | 2 +- aiohttp_graphql/graphqlview.py | 214 ------------- aiohttp_graphql/render_graphiql.py | 208 ------------ setup.cfg | 18 +- setup.py | 41 ++- tests/__init__.py | 1 - tests/app.py | 22 ++ tests/conftest.py | 57 ---- tests/schema.py | 54 ++-- tests/test_graphiqlview.py | 124 ++++---- tests/test_graphqlview.py | 488 ++++++++++++++--------------- tox.ini | 42 ++- 19 files changed, 609 insertions(+), 1380 deletions(-) delete mode 100644 .flake8 delete mode 100644 .pylintrc create mode 100644 Makefile delete mode 100644 aiohttp_graphql/graphqlview.py delete mode 100644 aiohttp_graphql/render_graphiql.py create mode 100644 tests/app.py delete mode 100644 tests/conftest.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 4a13650..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -ignore = E203,E501,W503,W504 -exclude = .git,.pytest_cache,.tox,.venv,__pycache__,build,dist -max-line-length = 88 diff --git a/.gitignore b/.gitignore index cd9f747..642f015 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,104 @@ -# Compiled Python files + +# 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 -venv -venv3[3-9] -.venv -.venv3[3-9] -env -.env build/ develop-eggs/ dist/ @@ -22,11 +111,16 @@ parts/ sdist/ var/ wheels/ +pip-wheel-metadata/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +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 @@ -37,20 +131,74 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ +.venv/ .coverage .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 -# IntelliJ -.idea -*.iml +# 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 -# Visual Studio -/.vscode +### VisualStudioCode Patch ### +# Ignore all local history of files +.history -# OS X -.DS_Store +# End of https://www.gitignore.io/api/python,intellij+all,visualstudiocode diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 77d5501..0000000 --- a/.pylintrc +++ /dev/null @@ -1,413 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS,snapshots - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. This option is deprecated -# and it will be removed in Pylint 2.0. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable= - fixme, - bad-continuation, - missing-docstring, - too-few-public-methods, - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". This option is deprecated -# and it will be removed in Pylint 2.0. -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# Good variable names which should always be accepted, separated by a comma -good-names=_,f,i,j,k,v,id,pk,db,tm,tx - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -# DERAULT: min-similarity-lines=4 -min-similarity-lines=15 - -# Ignore comments when computing similarities. -# DEFAULT: ignore-comments=yes - -# Ignore docstrings when computing similarities. -# DEFAULT: ignore-docstrings=yes - -# Ignore imports when computing similarities. -# DEFAULT: ignore-imports=no -ignore-imports=yes - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,future.builtins - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=6 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=18 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/.travis.yml b/.travis.yml index e07d9a4..7223ca8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,16 @@ language: python -matrix: - include: - - name: Code quality tests - env: TOXENV=black,flake8,pylint,manifest - python: 3.8 - - name: Unit tests with Python 3.8 - env: TOXENV=py38 - python: 3.8 - - name: Unit tests with Python 3.7 - env: TOXENV=py37 - python: 3.7 - - name: Unit tests with Python 3.6 - env: TOXENV=py36 - python: 3.6 - - name: Unit tests with PyPy 3 - env: TOXENV=pypy3 - python: pypy3 -cache: - directories: - - "$HOME/.cache/pip" - - "$TRAVIS_BUILD_DIR/.tox" +sudo: false +python: + - 3.6 + - 3.7 + - 3.8 +cache: pip + install: - - pip install tox - - pip install -e .[test] - - pip install pytest-cov codecov + - pip install tox-travis + script: - - tox -e $TOXENV -- --cov-branch --cov-report=term-missing --cov=aiohttp_graphql + - tox + after_success: - codecov diff --git a/MANIFEST.in b/MANIFEST.in index 093db67..0e5c7e4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,11 +3,8 @@ include MANIFEST.in include LICENSE include README.md -include .flake8 -include .pylintrc - include tox.ini -include travis.yml +include Makefile graft aiohttp_graphql graft tests diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb0db43 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +dev-setup: + python pip install -e ".[test]" + +tests: + py.test tests --cov=aiohttp_graphql -vv \ No newline at end of file diff --git a/README.md b/README.md index 57667e5..c44bad6 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,33 @@ Based on [flask-graphql] by [Syrus Akbary] and [sanic-graphql] by [Sergey Poriva [![Coverage Status](https://codecov.io/gh/graphql-python/aiohttp-graphql/branch/master/graph/badge.svg)](https://github.com/graphql-python/aiohttp-graphql) ## Usage -Just use the `GraphQLView` view from `aiohttp_graphql` + +Use the `GraphQLView` view from `aiohttp_graphql` ```python +from aiohttp import web from aiohttp_graphql import GraphQLView -GraphQLView.attach(app, schema=Schema, graphiql=True) +from schema import schema + +app = web.Application() + +GraphQLView.attach(app, schema=schema, graphiql=True) # Optional, for adding batch query support (used in Apollo-Client) -GraphQLView.attach(app, schema=Schema, batch=True) +GraphQLView.attach(app, schema=schema, batch=True, route_path="/graphql/batch") + +if __name__ == '__main__': + web.run_app(app) ``` -This will add a `/graphql` endpoint to your app (customizable by passing `route_path='/mypath'` to `GraphQLView.attach`). +This will add `/graphql` endpoint to your app (customizable by passing `route_path='/mypath'` to `GraphQLView.attach`) and enable the GraphiQL IDE. Note: `GraphQLView.attach` is just a convenience function, and the same functionality can be achieved with ```python -gql_view = GraphQLView(schema=Schema, **kwargs) -app.router.add_route('*', gql_view, name='graphql') +gql_view = GraphQLView(schema=schema, graphiql=True) +app.router.add_route('*', '/graphql', gql_view, name='graphql') ``` It's worth noting that the the "view function" of `GraphQLView` is contained in `GraphQLView.__call__`. So, when you create an instance, that instance is callable with the request object as the sole positional argument. To illustrate: @@ -35,39 +44,33 @@ gql_view = GraphQLView(schema=Schema, **kwargs) gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Request` object. ``` -### Supported options -- `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. -- `executor`: The `Executor` that you want to use to execute queries. If an `AsyncioExecutor` instance is provided, performs queries asynchronously within executor’s loop. -- `root_value`: The `root_value` you want to provide to `executor.execute`. -- `context`: A value to pass as the `context` to the `graphql()` function. By default is set to `dict` with request object at key `request`. -- `pretty`: Whether or not you want the response to be pretty printed JSON. -- `graphiql`: If `True`, may present [GraphiQL] when loaded directly from a browser (a useful tool for debugging and exploration). -- `graphiql_version`: The version of the provided `graphiql` package. -- `graphiql_template`: Inject a Jinja template string to customize GraphiQL. -- `middleware`: Custom middleware for [graphql-python]. -- `batch`: Set the GraphQL view as batch (for using in [Apollo-Client] or [ReactRelayNetworkLayer]) -- `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses +### Supported options for GraphQLView + + * `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_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. + * `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"**. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses `Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. -- `max_age`: sets the response header `Access-Control-Max-Age` for preflight requests -- `encoder`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`) -- `error_formatter`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`) -- `enable_async`: whether `async` mode will be enabled. -- `subscriptions`: The [GraphiQL] socket endpoint for using subscriptions in [graphql-ws]. - - -## Testing -Testing is done with `pytest`. - -```bash -git clone https://github.com/graphql-python/aiohttp-graphql -cd aiohttp-graphql -# Create a virtualenv -python3.6 -m venv env && source env/bin/activate # for example -pip install -e .[test] -pytest -``` - -The tests, while modeled after sanic-graphql's tests, have been entirely refactored to take advantage of `pytest-asyncio`, conform with PEP-8, and increase readability with pytest fixtures. For usage tests, please check them out. + * `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/). + * `max_age`: Sets the response header Access-Control-Max-Age for preflight requests. + * `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`. + * `enable_async`: whether `async` mode will be enabled. + * `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**. + + +## Contributing +Since v3, `aiohttp-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). ## License diff --git a/aiohttp_graphql/__init__.py b/aiohttp_graphql/__init__.py index 8f5beaf..bd85dbb 100644 --- a/aiohttp_graphql/__init__.py +++ b/aiohttp_graphql/__init__.py @@ -1,3 +1,3 @@ -from .graphqlview import GraphQLView +from graphql_server.aiohttp.graphqlview import GraphQLView __all__ = ["GraphQLView"] diff --git a/aiohttp_graphql/graphqlview.py b/aiohttp_graphql/graphqlview.py deleted file mode 100644 index dfe27fc..0000000 --- a/aiohttp_graphql/graphqlview.py +++ /dev/null @@ -1,214 +0,0 @@ -from collections.abc import Mapping -from functools import partial - -from promise import Promise -from aiohttp import web -from graphql.type.schema import GraphQLSchema -from graphql.execution.executors.asyncio import AsyncioExecutor -from graphql_server import ( - HttpQueryError, - default_format_error, - encode_execution_results, - json_encode, - load_json_body, - run_http_query, -) - -from .render_graphiql import render_graphiql - - -class GraphQLView: # pylint: disable = too-many-instance-attributes - def __init__( - self, - schema=None, - executor=None, - root_value=None, - context=None, - pretty=False, - graphiql=False, - graphiql_version=None, - graphiql_template=None, - middleware=None, - batch=False, - jinja_env=None, - max_age=86400, - encoder=None, - error_formatter=None, - enable_async=True, - subscriptions=None, - **execution_options, - ): - # pylint: disable=too-many-arguments - - self.schema = schema - self.executor = executor - self.root_value = root_value - self.context = context - self.pretty = pretty - self.graphiql = graphiql - self.graphiql_version = graphiql_version - self.graphiql_template = graphiql_template - self.middleware = middleware - self.batch = batch - self.jinja_env = jinja_env - self.max_age = max_age - self.encoder = encoder or json_encode - self.error_formatter = error_formatter or default_format_error - self.enable_async = enable_async and isinstance(self.executor, AsyncioExecutor) - self.subscriptions = subscriptions - self.execution_options = execution_options - assert isinstance( - self.schema, GraphQLSchema - ), "A Schema is required to be provided to GraphQLView." - - def get_context(self, request): - if self.context and isinstance(self.context, Mapping): - context = self.context.copy() - else: - context = {} - - if isinstance(context, Mapping) and "request" not in context: - context.update({"request": request}) - return context - - async def parse_body(self, request): - if request.content_type == "application/graphql": - r_text = await request.text() - return {"query": r_text} - - if request.content_type == "application/json": - text = await request.text() - return load_json_body(text) - - if request.content_type in ( - "application/x-www-form-urlencoded", - "multipart/form-data", - ): - # TODO: seems like a multidict would be more appropriate - # than casting it and de-duping variables. Alas, it's what - # graphql-python wants. - return dict(await request.post()) - - return {} - - def render_graphiql(self, params, result): - return render_graphiql( - jinja_env=self.jinja_env, - params=params, - result=result, - graphiql_version=self.graphiql_version, - graphiql_template=self.graphiql_template, - subscriptions=self.subscriptions, - ) - - def is_graphiql(self, request): - return all( - [ - self.graphiql, - request.method.lower() == "get", - "raw" not in request.query, - any( - [ - "text/html" in request.headers.get("accept", {}), - "*/*" in request.headers.get("accept", {}), - ] - ), - ] - ) - - def is_pretty(self, request): - return any( - [self.pretty, self.is_graphiql(request), request.query.get("pretty")] - ) - - async def __call__(self, request): - try: - data = await self.parse_body(request) - request_method = request.method.lower() - is_graphiql = self.is_graphiql(request) - is_pretty = self.is_pretty(request) - - if request_method == "options": - return self.process_preflight(request) - - execution_results, all_params = run_http_query( - self.schema, - request_method, - data, - query_data=request.query, - batch_enabled=self.batch, - catch=is_graphiql, - # Execute options - return_promise=self.enable_async, - root_value=self.root_value, - context_value=self.get_context(request), - middleware=self.middleware, - executor=self.executor, - **self.execution_options, - ) - - if is_graphiql and self.enable_async: - # catch errors like run_http_query does when async - execution_results = [ - result.catch(lambda value: None) for result in execution_results - ] - awaited_execution_results = await Promise.all(execution_results) - result, status_code = encode_execution_results( - awaited_execution_results, - is_batch=isinstance(data, list), - format_error=self.error_formatter, - encode=partial(self.encoder, pretty=is_pretty), - ) - - if is_graphiql: - return await self.render_graphiql(params=all_params[0], result=result,) - - return web.Response( - text=result, status=status_code, content_type="application/json", - ) - - except HttpQueryError as err: - return web.Response( - text=self.encoder({"errors": [self.error_formatter(err)]}), - status=err.status_code, - headers=err.headers, - content_type="application/json", - ) - - def process_preflight(self, request): - """ Preflight request support for apollo-client - https://www.w3.org/TR/cors/#resource-preflight-requests """ - headers = request.headers - origin = headers.get("Origin", "") - method = headers.get("Access-Control-Request-Method", "").upper() - - accepted_methods = ["GET", "POST", "PUT", "DELETE"] - if method and method in accepted_methods: - return web.Response( - status=200, - headers={ - "Access-Control-Allow-Origin": origin, - "Access-Control-Allow-Methods": ", ".join(accepted_methods), - "Access-Control-Max-Age": str(self.max_age), - }, - ) - return web.Response(status=400) - - @classmethod - def attach(cls, app, *, route_path="/graphql", route_name="graphql", **kwargs): - view = cls(**kwargs) - app.router.add_route("*", route_path, _asyncify(view), name=route_name) - - -def _asyncify(handler): - """Return an async version of the given handler. - - This is mainly here because ``aiohttp`` can't infer the async definition of - :py:meth:`.GraphQLView.__call__` and raises a :py:class:`DeprecationWarning` - in tests. Wrapping it into an async function avoids the noisy warning. - """ - - async def _dispatch(request): - return await handler(request) - - return _dispatch diff --git a/aiohttp_graphql/render_graphiql.py b/aiohttp_graphql/render_graphiql.py deleted file mode 100644 index 9da47d3..0000000 --- a/aiohttp_graphql/render_graphiql.py +++ /dev/null @@ -1,208 +0,0 @@ -import json -import re - -from aiohttp import web - -GRAPHIQL_VERSION = "0.17.5" - -TEMPLATE = """ - - - - - - - - - - - - - - - - -""" - - -def escape_js_value(value): - quotation = False - if value.startswith('"') and value.endswith('"'): - quotation = True - value = value[1:-1] - - value = value.replace("\\\\n", "\\\\\\n").replace("\\n", "\\\\n") - if quotation: - value = '"' + value.replace('\\\\"', '"').replace('"', '\\"') + '"' - - return value - - -def process_var(template, name, value, jsonify=False): - pattern = r"{{\s*" + name + r"(\s*|[^}]+)*\s*}}" - if jsonify and value not in ["null", "undefined"]: - value = json.dumps(value) - value = escape_js_value(value) - - return re.sub(pattern, value, template) - - -def simple_renderer(template, **values): - replace = ["graphiql_version", "subscriptions"] - replace_jsonify = ["query", "result", "variables", "operation_name"] - - for rep in replace: - template = process_var(template, rep, values.get(rep, "")) - - for rep in replace_jsonify: - template = process_var(template, rep, values.get(rep, ""), True) - - return template - - -async def render_graphiql( - jinja_env=None, - graphiql_version=None, - graphiql_template=None, - params=None, - result=None, - subscriptions=None, -): - graphiql_version = graphiql_version or GRAPHIQL_VERSION - template = graphiql_template or TEMPLATE - template_vars = { - "graphiql_version": graphiql_version, - "query": params and params.query, - "variables": params and params.variables, - "operation_name": params and params.operation_name, - "result": result, - "subscriptions": subscriptions or "", - } - - if jinja_env: - template = jinja_env.from_string(template) - if jinja_env.is_async: - source = await template.render_async(**template_vars) - else: - source = template.render(**template_vars) - else: - source = simple_renderer(template, **template_vars) - - return web.Response(text=source, content_type="text/html") diff --git a/setup.cfg b/setup.cfg index 493cbdb..61e4d59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,10 @@ -[metadata] -description-file = README.md +[flake8] +exclude = tests,scripts,setup.py,docs +max-line-length = 88 + +[isort] +known_first_party=graphql [tool:pytest] -norecursedirs = - __pycache__ - *.egg-info - *.egg - .cache - .git - .tox -testpaths = tests/ +norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache +markers = asyncio diff --git a/setup.py b/setup.py index 0179668..ea2c921 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,30 @@ from setuptools import setup, find_packages +install_requires = [ + "graphql-server[aiohttp]>=3.0.0b1", +] + +tests_requires = [ + "pytest>=5.4,<5.5", + "pytest-asyncio>=0.11.0", + "pytest-cov>=2.8,<3", + "Jinja2>=2.10.1,<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="aiohttp-graphql", version="1.1.0", description="Adds GraphQL support to your aiohttp application", - long_description=open("README.md").read(), + long_description=readme, long_description_content_type="text/markdown", url="https://github.com/graphql-python/aiohttp-graphql", download_url="https://github.com/graphql-python/aiohttp-graphql/releases", @@ -22,22 +42,13 @@ ], keywords="api graphql protocol aiohttp", packages=find_packages(exclude=["tests"]), - install_requires=[ - "graphql-core>=2.3,<3", - "graphql-server-core>=1.2,<2", - "aiohttp>=3,<4", - ], + install_requires=install_requires, + tests_require=tests_requires, extras_require={ - "test": [ - "pytest>=5.4,<5.5", - # Note: tests do not work with pytest-asyncio 0.11, see - # https://github.com/pytest-dev/pytest-asyncio/issues/158 - "pytest-asyncio>=0.10,<0.11", - "pytest-cov>=2.8,<3", - "jinja2>=2.11,<3", - "yarl>1.4,<1.5", - ], + 'test': tests_requires, + 'dev': dev_requires, }, include_package_data=True, + zip_safe=False, platforms="any", ) diff --git a/tests/__init__.py b/tests/__init__.py index 943d58f..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ -# aiohttp-graphql tests diff --git a/tests/app.py b/tests/app.py new file mode 100644 index 0000000..5f4a973 --- /dev/null +++ b/tests/app.py @@ -0,0 +1,22 @@ +from urllib.parse import urlencode + +from aiohttp import web + +from aiohttp_graphql import GraphQLView +from tests.schema import Schema + + +def create_app(schema=Schema, **kwargs): + app = web.Application() + # Only needed to silence aiohttp deprecation warnings + GraphQLView.attach(app, schema=schema, **kwargs) + return app + + +def url_string(**url_params): + base_url = "/graphql" + + if url_params: + return f"{base_url}?{urlencode(url_params)}" + + return base_url diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 8d305c7..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,57 +0,0 @@ -from urllib.parse import urlencode - -import pytest - -from aiohttp import web -from aiohttp.test_utils import TestClient, TestServer -from graphql.execution.executors.asyncio import AsyncioExecutor - -from aiohttp_graphql import GraphQLView - -# pylint: disable=redefined-outer-name - - -# GraphQL Fixtures -@pytest.fixture(params=[True, False], ids=["async", "sync"]) -def executor(request): - if request.param: - return AsyncioExecutor() - return None - - -# GraphQLView Fixtures -@pytest.fixture -def view_kwargs(): - return {} - - -# aiohttp Fixtures -@pytest.fixture -def app(executor, view_kwargs): - app = web.Application() - GraphQLView.attach(app, executor=executor, **view_kwargs) - return app - - -@pytest.fixture -async def client(app): - client = TestClient(TestServer(app)) - await client.start_server() - yield client - await client.close() - - -# URL Fixtures -@pytest.fixture -def base_url(): - return "/graphql" - - -@pytest.fixture -def url_builder(base_url): - def builder(**url_params): - if url_params: - return "{}?{}".format(base_url, urlencode(url_params)) - return base_url - - return builder diff --git a/tests/schema.py b/tests/schema.py index de1adb5..59e7ff1 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -1,17 +1,12 @@ import asyncio -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 -def resolve_raises(*args): - # pylint: disable=unused-argument +def resolve_raises(*_): raise Exception("Throws!") @@ -19,22 +14,28 @@ def resolve_raises(*args): QueryRootType = GraphQLObjectType( name="QueryRoot", fields={ - "thrower": GraphQLField( - GraphQLNonNull(GraphQLString), resolver=resolve_raises, - ), + "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_raises,), "request": GraphQLField( GraphQLNonNull(GraphQLString), - resolver=lambda obj, info, *args: info.context["request"].query.get("q"), + resolve=lambda obj, info, *args: info.context["request"].query.get("q"), ), "context": GraphQLField( - GraphQLNonNull(GraphQLString), - resolver=lambda obj, info, *args: info.context, + 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, + type_=GraphQLString, args={"who": GraphQLArgument(GraphQLString)}, - resolver=lambda obj, info, **args: "Hello %s" - % (args.get("who") or "World"), + resolve=lambda obj, info, who=None: "Hello %s" % (who or "World"), ), }, ) @@ -44,7 +45,7 @@ def resolve_raises(*args): name="MutationRoot", fields={ "writeTest": GraphQLField( - type=QueryRootType, resolver=lambda *args: QueryRootType + type_=QueryRootType, resolve=lambda *args: QueryRootType ) }, ) @@ -53,7 +54,7 @@ def resolve_raises(*args): name="SubscriptionsRoot", fields={ "subscriptionsTest": GraphQLField( - type=QueryRootType, resolver=lambda *args: QueryRootType + type_=QueryRootType, resolve=lambda *args: QueryRootType ) }, ) @@ -62,29 +63,26 @@ def resolve_raises(*args): # Schema with async methods -async def resolver(context, *args): - # pylint: disable=unused-argument +async def resolver_field_async_1(_obj, info): await asyncio.sleep(0.001) return "hey" -async def resolver_2(context, *args): - # pylint: disable=unused-argument +async def resolver_field_async_2(_obj, info): await asyncio.sleep(0.003) return "hey2" -def resolver_3(context, *args): - # pylint: disable=unused-argument +def resolver_field_sync(_obj, info): return "hey3" AsyncQueryType = GraphQLObjectType( "AsyncQueryType", { - "a": GraphQLField(GraphQLString, resolver=resolver), - "b": GraphQLField(GraphQLString, resolver=resolver_2), - "c": GraphQLField(GraphQLString, resolver=resolver_3), + "a": GraphQLField(GraphQLString, resolve=resolver_field_async_1), + "b": GraphQLField(GraphQLString, resolve=resolver_field_async_2), + "c": GraphQLField(GraphQLString, resolve=resolver_field_sync), }, ) diff --git a/tests/test_graphiqlview.py b/tests/test_graphiqlview.py index 7cd6a13..b1dbd18 100644 --- a/tests/test_graphiqlview.py +++ b/tests/test_graphiqlview.py @@ -1,27 +1,23 @@ -from graphql.execution.executors.asyncio import AsyncioExecutor - +import pytest +from aiohttp.test_utils import TestClient, TestServer from jinja2 import Environment -import pytest +from tests.app import create_app, url_string +from tests.schema import AsyncSchema, Schema -from .schema import ( - AsyncSchema, - Schema, -) -# pylint: disable=invalid-name -# pylint: disable=redefined-outer-name +@pytest.fixture +def app(): + app = create_app() + return app @pytest.fixture -def pretty_response(): - return ( - "{\n" - ' "data": {\n' - ' "test": "Hello World"\n' - " }\n" - "}".replace('"', '\\"').replace("\n", "\\n") - ) +async def client(app): + client = TestClient(TestServer(app)) + await client.start_server() + yield client + await client.close() @pytest.fixture @@ -32,48 +28,60 @@ def view_kwargs(): } +@pytest.fixture +def pretty_response(): + return ( + "{\n" + ' "data": {\n' + ' "test": "Hello World"\n' + " }\n" + "}".replace('"', '\\"').replace("\n", "\\n") + ) + + @pytest.mark.asyncio -async def test_graphiql_is_enabled(client, base_url): - response = await client.get(base_url, headers={"Accept": "text/html"}) +@pytest.mark.parametrize("app", [create_app(graphiql=True)]) +async def test_graphiql_is_enabled(app, client): + response = await client.get( + url_string(query="{test}"), headers={"Accept": "text/html"} + ) assert response.status == 200 @pytest.mark.asyncio -async def test_graphiql_simple_renderer(client, url_builder, pretty_response): +@pytest.mark.parametrize("app", [create_app(graphiql=True)]) +async def test_graphiql_simple_renderer(app, client, pretty_response): response = await client.get( - url_builder(query="{test}"), headers={"Accept": "text/html"}, + url_string(query="{test}"), headers={"Accept": "text/html"}, ) assert response.status == 200 assert pretty_response in await response.text() class TestJinjaEnv: - @pytest.fixture(params=[True, False], ids=["async_jinja2", "sync_jinja2"]) - def view_kwargs(self, request, view_kwargs): - # pylint: disable=no-self-use - # pylint: disable=redefined-outer-name - view_kwargs.update(jinja_env=Environment(enable_async=request.param)) - return view_kwargs - @pytest.mark.asyncio - async def test_graphiql_jinja_renderer(self, client, url_builder, pretty_response): + @pytest.mark.parametrize( + "app", [create_app(graphiql=True, jinja_env=Environment(enable_async=True))] + ) + async def test_graphiql_jinja_renderer_async(self, app, client, pretty_response): response = await client.get( - url_builder(query="{test}"), headers={"Accept": "text/html"}, + url_string(query="{test}"), headers={"Accept": "text/html"}, ) assert response.status == 200 assert pretty_response in await response.text() @pytest.mark.asyncio -async def test_graphiql_html_is_not_accepted(client, base_url): - response = await client.get(base_url, headers={"Accept": "application/json"},) +async def test_graphiql_html_is_not_accepted(client): + response = await client.get("/graphql", headers={"Accept": "application/json"},) assert response.status == 400 @pytest.mark.asyncio -async def test_graphiql_get_mutation(client, url_builder): +@pytest.mark.parametrize("app", [create_app(graphiql=True)]) +async def test_graphiql_get_mutation(app, client): response = await client.get( - url_builder(query="mutation TestMutation { writeTest { test } }"), + url_string(query="mutation TestMutation { writeTest { test } }"), headers={"Accept": "text/html"}, ) assert response.status == 200 @@ -81,9 +89,10 @@ async def test_graphiql_get_mutation(client, url_builder): @pytest.mark.asyncio -async def test_graphiql_get_subscriptions(client, url_builder): +@pytest.mark.parametrize("app", [create_app(graphiql=True)]) +async def test_graphiql_get_subscriptions(app, client): response = await client.get( - url_builder( + url_string( query="subscription TestSubscriptions { subscriptionsTest { test } }" ), headers={"Accept": "text/html"}, @@ -92,39 +101,12 @@ async def test_graphiql_get_subscriptions(client, url_builder): assert "response: null" in await response.text() -class TestAsyncSchema: - @pytest.fixture - def executor(self, event_loop): - # pylint: disable=no-self-use - # Only need to test with the AsyncExecutor - return AsyncioExecutor(loop=event_loop) - - @pytest.fixture - def view_kwargs(self, view_kwargs): - # pylint: disable=no-self-use - # pylint: disable=redefined-outer-name - view_kwargs.update(schema=AsyncSchema) - return view_kwargs - - @pytest.mark.asyncio - async def test_graphiql_asyncio_schema(self, client, url_builder): - response = await client.get( - url_builder(query="{a,b,c}"), headers={"Accept": "text/html"}, - ) - - expected_response = ( - ( - "{\n" - ' "data": {\n' - ' "a": "hey",\n' - ' "b": "hey2",\n' - ' "c": "hey3"\n' - " }\n" - "}" - ) - .replace('"', '\\"') - .replace("\n", "\\n") - ) +@pytest.mark.asyncio +@pytest.mark.parametrize("app", [create_app(schema=AsyncSchema, enable_async=True)]) +async def test_graphiql_async_schema(app, client): + response = await client.get( + url_string(query="{a,b,c}"), headers={"Accept": "text/html"}, + ) - assert response.status == 200 - assert expected_response in await response.text() + assert response.status == 200 + assert await response.json() == {"data": {"a": "hey", "b": "hey2", "c": "hey3"}} diff --git a/tests/test_graphqlview.py b/tests/test_graphqlview.py index f73ad1e..0a940f9 100644 --- a/tests/test_graphqlview.py +++ b/tests/test_graphqlview.py @@ -1,50 +1,40 @@ import json - from urllib.parse import urlencode import pytest - from aiohttp import FormData -from graphql.execution.executors.asyncio import AsyncioExecutor -from graphql.execution.executors.sync import SyncExecutor -from aiohttp_graphql import GraphQLView - -from .schema import Schema, AsyncSchema - +from aiohttp.test_utils import TestClient, TestServer -# pylint: disable=invalid-name -# pylint: disable=protected-access +from .app import create_app, url_string +from .schema import AsyncSchema @pytest.fixture -def view_kwargs(): - return {"schema": Schema} +def app(): + app = create_app() + return app -@pytest.mark.parametrize( - "view,expected", - [ - (GraphQLView(schema=Schema), False), - (GraphQLView(schema=Schema, executor=SyncExecutor()), False), - (GraphQLView(schema=Schema, executor=AsyncioExecutor()), True), - ], -) -def test_eval(view, expected): - assert view.enable_async == expected +@pytest.fixture +async def client(app): + client = TestClient(TestServer(app)) + await client.start_server() + yield client + await client.close() @pytest.mark.asyncio -async def test_allows_get_with_query_param(client, url_builder): - response = await client.get(url_builder(query="{test}")) +async def test_allows_get_with_query_param(client): + response = await client.get(url_string(query="{test}")) assert response.status == 200 assert await response.json() == {"data": {"test": "Hello World"}} @pytest.mark.asyncio -async def test_allows_get_with_variable_values(client, url_builder): +async def test_allows_get_with_variable_values(client): response = await client.get( - url_builder( + url_string( query="query helloWho($who: String) { test(who: $who) }", variables=json.dumps({"who": "Dolly"}), ) @@ -55,9 +45,9 @@ async def test_allows_get_with_variable_values(client, url_builder): @pytest.mark.asyncio -async def test_allows_get_with_operation_name(client, url_builder): +async def test_allows_get_with_operation_name(client): response = await client.get( - url_builder( + url_string( query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } @@ -77,28 +67,30 @@ async def test_allows_get_with_operation_name(client, url_builder): @pytest.mark.asyncio -async def test_reports_validation_errors(client, url_builder): - response = await client.get(url_builder(query="{ test, unknownOne, unknownTwo }")) +async def test_reports_validation_errors(client): + response = await client.get(url_string(query="{ test, unknownOne, unknownTwo }")) assert response.status == 400 assert await response.json() == { "errors": [ { - "message": 'Cannot query field "unknownOne" on type "QueryRoot".', + "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], + "path": None, }, { - "message": 'Cannot query field "unknownTwo" on type "QueryRoot".', + "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], + "path": None, }, ], } @pytest.mark.asyncio -async def test_errors_when_missing_operation_name(client, url_builder): +async def test_errors_when_missing_operation_name(client): response = await client.get( - url_builder( + url_string( query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } @@ -115,15 +107,17 @@ async def test_errors_when_missing_operation_name(client, url_builder): "Must provide operation name if query contains multiple " "operations." ), + "locations": None, + "path": None, }, ] } @pytest.mark.asyncio -async def test_errors_when_sending_a_mutation_via_get(client, url_builder): +async def test_errors_when_sending_a_mutation_via_get(client): response = await client.get( - url_builder( + url_string( query=""" mutation TestMutation { writeTest { test } } """ @@ -132,15 +126,19 @@ async def test_errors_when_sending_a_mutation_via_get(client, url_builder): assert response.status == 405 assert await response.json() == { "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, + }, ], } @pytest.mark.asyncio -async def test_errors_when_selecting_a_mutation_within_a_get(client, url_builder): +async def test_errors_when_selecting_a_mutation_within_a_get(client): response = await client.get( - url_builder( + url_string( query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } @@ -152,17 +150,19 @@ async def test_errors_when_selecting_a_mutation_within_a_get(client, url_builder assert response.status == 405 assert await response.json() == { "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, + }, ], } @pytest.mark.asyncio -async def test_errors_when_selecting_a_subscription_within_a_get( - client, url_builder, -): +async def test_errors_when_selecting_a_subscription_within_a_get(client): response = await client.get( - url_builder( + url_string( query=""" subscription TestSubscriptions { subscriptionsTest { test } } """, @@ -174,18 +174,19 @@ async def test_errors_when_selecting_a_subscription_within_a_get( assert await response.json() == { "errors": [ { - "message": ( - "Can only perform a subscription operation from a POST " "request." - ) + "message": "Can only perform a subscription operation from a POST " + "request.", + "locations": None, + "path": None, }, ], } @pytest.mark.asyncio -async def test_allows_mutation_to_exist_within_a_get(client, url_builder): +async def test_allows_mutation_to_exist_within_a_get(client): response = await client.get( - url_builder( + url_string( query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } @@ -199,9 +200,9 @@ async def test_allows_mutation_to_exist_within_a_get(client, url_builder): @pytest.mark.asyncio -async def test_allows_post_with_json_encoding(client, base_url): +async def test_allows_post_with_json_encoding(client): response = await client.post( - base_url, + "/graphql", data=json.dumps(dict(query="{test}")), headers={"content-type": "application/json"}, ) @@ -211,9 +212,9 @@ async def test_allows_post_with_json_encoding(client, base_url): @pytest.mark.asyncio -async def test_allows_sending_a_mutation_via_post(client, base_url): +async def test_allows_sending_a_mutation_via_post(client): response = await client.post( - base_url, + "/graphql", data=json.dumps(dict(query="mutation TestMutation { writeTest { test } }",)), headers={"content-type": "application/json"}, ) @@ -223,38 +224,11 @@ async def test_allows_sending_a_mutation_via_post(client, base_url): @pytest.mark.asyncio -async def test_errors_when_sending_a_subscription_without_allow(client, base_url): - response = await client.post( - base_url, - data=json.dumps( - dict( - query=""" - subscription TestSubscriptions { subscriptionsTest { test } } - """, - ) - ), - headers={"content-type": "application/json"}, - ) - - assert response.status == 200 - assert await response.json() == { - "data": None, - "errors": [ - { - "message": "Subscriptions are not allowed. You will need to " - "either use the subscribe function or pass " - "allow_subscriptions=True" - }, - ], - } - - -@pytest.mark.asyncio -async def test_allows_post_with_url_encoding(client, base_url): +async def test_allows_post_with_url_encoding(client): data = FormData() data.add_field("query", "{test}") response = await client.post( - base_url, + "/graphql", data=data(), headers={"content-type": "application/x-www-form-urlencoded"}, ) @@ -264,9 +238,9 @@ async def test_allows_post_with_url_encoding(client, base_url): @pytest.mark.asyncio -async def test_supports_post_json_query_with_string_variables(client, base_url): +async def test_supports_post_json_query_with_string_variables(client): response = await client.post( - base_url, + "/graphql", data=json.dumps( dict( query="query helloWho($who: String){ test(who: $who) }", @@ -281,9 +255,9 @@ async def test_supports_post_json_query_with_string_variables(client, base_url): @pytest.mark.asyncio -async def test_supports_post_json_query_with_json_variables(client, base_url): +async def test_supports_post_json_query_with_json_variables(client): response = await client.post( - base_url, + "/graphql", data=json.dumps( dict( query="query helloWho($who: String){ test(who: $who) }", @@ -298,9 +272,9 @@ async def test_supports_post_json_query_with_json_variables(client, base_url): @pytest.mark.asyncio -async def test_supports_post_url_encoded_query_with_string_variables(client, base_url): +async def test_supports_post_url_encoded_query_with_string_variables(client): response = await client.post( - base_url, + "/graphql", data=urlencode( dict( query="query helloWho($who: String){ test(who: $who) }", @@ -315,9 +289,9 @@ async def test_supports_post_url_encoded_query_with_string_variables(client, bas @pytest.mark.asyncio -async def test_supports_post_json_quey_with_get_variable_values(client, url_builder): +async def test_supports_post_json_quey_with_get_variable_values(client): response = await client.post( - url_builder(variables=json.dumps({"who": "Dolly"})), + url_string(variables=json.dumps({"who": "Dolly"})), data=json.dumps(dict(query="query helloWho($who: String){ test(who: $who) }",)), headers={"content-type": "application/json"}, ) @@ -327,9 +301,9 @@ async def test_supports_post_json_quey_with_get_variable_values(client, url_buil @pytest.mark.asyncio -async def test_post_url_encoded_query_with_get_variable_values(client, url_builder): +async def test_post_url_encoded_query_with_get_variable_values(client): response = await client.post( - url_builder(variables=json.dumps({"who": "Dolly"})), + url_string(variables=json.dumps({"who": "Dolly"})), data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), headers={"content-type": "application/x-www-form-urlencoded"}, ) @@ -339,11 +313,9 @@ async def test_post_url_encoded_query_with_get_variable_values(client, url_build @pytest.mark.asyncio -async def test_supports_post_raw_text_query_with_get_variable_values( - client, url_builder -): +async def test_supports_post_raw_text_query_with_get_variable_values(client): response = await client.post( - url_builder(variables=json.dumps({"who": "Dolly"})), + url_string(variables=json.dumps({"who": "Dolly"})), data="query helloWho($who: String){ test(who: $who) }", headers={"content-type": "application/graphql"}, ) @@ -353,9 +325,9 @@ async def test_supports_post_raw_text_query_with_get_variable_values( @pytest.mark.asyncio -async def test_allows_post_with_operation_name(client, base_url): +async def test_allows_post_with_operation_name(client): response = await client.post( - base_url, + "/graphql", data=json.dumps( dict( query=""" @@ -379,9 +351,9 @@ async def test_allows_post_with_operation_name(client, base_url): @pytest.mark.asyncio -async def test_allows_post_with_get_operation_name(client, url_builder): +async def test_allows_post_with_get_operation_name(client): response = await client.post( - url_builder(operationName="helloWorld"), + url_string(operationName="helloWorld"), data=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } @@ -400,23 +372,23 @@ async def test_allows_post_with_get_operation_name(client, url_builder): @pytest.mark.asyncio -async def test_supports_pretty_printing(client, url_builder): - response = await client.get(url_builder(query="{test}", pretty="1")) +async def test_supports_pretty_printing(client): + response = await client.get(url_string(query="{test}", pretty="1")) text = await response.text() - assert text == ("{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}") + assert text == "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" @pytest.mark.asyncio -async def test_not_pretty_by_default(client, url_builder): - response = await client.get(url_builder(query="{test}")) +async def test_not_pretty_by_default(client): + response = await client.get(url_string(query="{test}")) - assert await response.text() == ('{"data":{"test":"Hello World"}}') + assert await response.text() == '{"data":{"test":"Hello World"}}' @pytest.mark.asyncio -async def test_supports_pretty_printing_by_request(client, url_builder): - response = await client.get(url_builder(query="{test}", pretty="1")) +async def test_supports_pretty_printing_by_request(client): + response = await client.get(url_string(query="{test}", pretty="1")) assert await response.text() == ( "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" @@ -424,8 +396,8 @@ async def test_supports_pretty_printing_by_request(client, url_builder): @pytest.mark.asyncio -async def test_handles_field_errors_caught_by_graphql(client, url_builder): - response = await client.get(url_builder(query="{thrower}")) +async def test_handles_field_errors_caught_by_graphql(client): + response = await client.get(url_string(query="{thrower}")) assert response.status == 200 assert await response.json() == { "data": None, @@ -440,96 +412,119 @@ async def test_handles_field_errors_caught_by_graphql(client, url_builder): @pytest.mark.asyncio -async def test_handles_syntax_errors_caught_by_graphql(client, url_builder): - response = await client.get(url_builder(query="syntaxerror")) +async def test_handles_syntax_errors_caught_by_graphql(client): + response = await client.get(url_string(query="syntaxerror")) assert response.status == 400 assert await response.json() == { "errors": [ { "locations": [{"column": 1, "line": 1}], - "message": ( - "Syntax Error GraphQL (1:1) " - 'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n' - ), + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None, }, ], } @pytest.mark.asyncio -async def test_handles_errors_caused_by_a_lack_of_query(client, base_url): - response = await client.get(base_url) +async def test_handles_errors_caused_by_a_lack_of_query(client): + response = await client.get("/graphql") assert response.status == 400 assert await response.json() == { - "errors": [{"message": "Must provide query string."}] + "errors": [ + {"message": "Must provide query string.", "locations": None, "path": None} + ] } @pytest.mark.asyncio -async def test_handles_batch_correctly_if_is_disabled(client, base_url): +async def test_handles_batch_correctly_if_is_disabled(client): response = await client.post( - base_url, data="[]", headers={"content-type": "application/json"}, + "/graphql", data="[]", headers={"content-type": "application/json"}, ) assert response.status == 400 assert await response.json() == { - "errors": [{"message": "Batch GraphQL requests are not enabled."}] + "errors": [ + { + "message": "Batch GraphQL requests are not enabled.", + "locations": None, + "path": None, + } + ] } @pytest.mark.asyncio -async def test_handles_incomplete_json_bodies(client, base_url): +async def test_handles_incomplete_json_bodies(client): response = await client.post( - base_url, data='{"query":', headers={"content-type": "application/json"}, + "/graphql", data='{"query":', headers={"content-type": "application/json"}, ) assert response.status == 400 assert await response.json() == { - "errors": [{"message": "POST body sent invalid JSON."}] + "errors": [ + { + "message": "POST body sent invalid JSON.", + "locations": None, + "path": None, + } + ] } @pytest.mark.asyncio -async def test_handles_plain_post_text(client, url_builder): +async def test_handles_plain_post_text(client): response = await client.post( - url_builder(variables=json.dumps({"who": "Dolly"})), + url_string(variables=json.dumps({"who": "Dolly"})), data="query helloWho($who: String){ test(who: $who) }", headers={"content-type": "text/plain"}, ) assert response.status == 400 assert await response.json() == { - "errors": [{"message": "Must provide query string."}] + "errors": [ + {"message": "Must provide query string.", "locations": None, "path": None} + ] } @pytest.mark.asyncio -async def test_handles_poorly_formed_variables(client, url_builder): +async def test_handles_poorly_formed_variables(client): response = await client.get( - url_builder( + url_string( query="query helloWho($who: String){ test(who: $who) }", variables="who:You" ), ) assert response.status == 400 assert await response.json() == { - "errors": [{"message": "Variables are invalid JSON."}] + "errors": [ + {"message": "Variables are invalid JSON.", "locations": None, "path": None} + ] } @pytest.mark.asyncio -async def test_handles_unsupported_http_methods(client, url_builder): - response = await client.put(url_builder(query="{test}")) +async def test_handles_unsupported_http_methods(client): + response = await client.put(url_string(query="{test}")) assert response.status == 405 assert response.headers["Allow"] in ["GET, POST", "HEAD, GET, POST, OPTIONS"] assert await response.json() == { - "errors": [{"message": "GraphQL only supports GET and POST requests."}] + "errors": [ + { + "message": "GraphQL only supports GET and POST requests.", + "locations": None, + "path": None, + } + ] } @pytest.mark.asyncio -async def test_passes_request_into_request_context(client, url_builder): - response = await client.get(url_builder(query="{request}", q="testing")) +@pytest.mark.parametrize("app", [create_app()]) +async def test_passes_request_into_request_context(app, client): + response = await client.get(url_string(query="{request}", q="testing")) assert response.status == 200 assert await response.json() == { @@ -537,45 +532,46 @@ async def test_passes_request_into_request_context(client, url_builder): } -class TestCustomContext: - @pytest.fixture - def view_kwargs(self, request, view_kwargs): - # pylint: disable=no-self-use - # pylint: disable=redefined-outer-name - view_kwargs.update(context=request.param) - return view_kwargs +@pytest.mark.asyncio +@pytest.mark.parametrize("app", [create_app(context={"session": "CUSTOM CONTEXT"})]) +async def test_passes_custom_context_into_context(app, client): + response = await client.get(url_string(query="{context { session request }}")) - @pytest.mark.parametrize( - "view_kwargs", - ["CUSTOM CONTEXT", {"CUSTOM_CONTEXT": "test"}], - indirect=True, - ids=repr, - ) - @pytest.mark.asyncio - async def test_context_remapped(self, client, url_builder): - response = await client.get(url_builder(query="{context}")) + _json = await response.json() + assert response.status == 200 + assert "data" in _json + assert "session" in _json["data"]["context"] + assert "request" in _json["data"]["context"] + assert "CUSTOM CONTEXT" in _json["data"]["context"]["session"] + assert "Request" in _json["data"]["context"]["request"] - _json = await response.json() - assert response.status == 200 - assert "request" in _json["data"]["context"] - assert "CUSTOM CONTEXT" not in _json["data"]["context"] - @pytest.mark.parametrize( - "view_kwargs", [{"request": "test"}], indirect=True, ids=repr - ) - @pytest.mark.asyncio - async def test_request_not_replaced(self, client, url_builder): - response = await client.get(url_builder(query="{context}")) +@pytest.mark.asyncio +@pytest.mark.parametrize("app", [create_app(context="CUSTOM CONTEXT")]) +async def test_context_remapped_if_not_mapping(app, client): + response = await client.get(url_string(query="{context { session request }}")) - _json = await response.json() - assert response.status == 200 - assert "request" in _json["data"]["context"] - assert _json["data"]["context"] == str({"request": "test"}) + _json = await response.json() + assert response.status == 200 + assert "data" in _json + assert "session" in _json["data"]["context"] + assert "request" in _json["data"]["context"] + assert "CUSTOM CONTEXT" not in _json["data"]["context"]["request"] + assert "Request" in _json["data"]["context"]["request"] @pytest.mark.asyncio -async def test_post_multipart_data(client, base_url): - # pylint: disable=line-too-long +@pytest.mark.parametrize("app", [create_app(context={"request": "test"})]) +async def test_request_not_replaced(app, client): + response = await client.get(url_string(query="{context { request }}")) + + _json = await response.json() + assert response.status == 200 + assert _json["data"]["context"]["request"] == "test" + + +@pytest.mark.asyncio +async def test_post_multipart_data(client): query = "mutation TestMutation { writeTest { test } }" data = ( @@ -586,14 +582,14 @@ async def test_post_multipart_data(client, base_url): + "\r\n" + "------aiohttpgraphql--\r\n" + "Content-Type: text/plain; charset=utf-8\r\n" - + 'Content-Disposition: form-data; name="file"; filename="text1.txt"; filename*=utf-8\'\'text1.txt\r\n' + + 'Content-Disposition: form-data; name="file"; filename="text1.txt"; filename*=utf-8\'\'text1.txt\r\n' # noqa: ignore + "\r\n" + "\r\n" + "------aiohttpgraphql--\r\n" ) response = await client.post( - base_url, + "/graphql", data=data, headers={"content-type": "multipart/form-data; boundary=----aiohttpgraphql"}, ) @@ -602,110 +598,92 @@ async def test_post_multipart_data(client, base_url): assert await response.json() == {"data": {u"writeTest": {u"test": u"Hello World"}}} -class TestBatchExecutor: - @pytest.fixture - def view_kwargs(self, view_kwargs): - # pylint: disable=no-self-use - # pylint: disable=redefined-outer-name - view_kwargs.update(batch=True) - return view_kwargs - - @pytest.mark.asyncio - async def test_batch_allows_post_with_json_encoding(self, client, base_url): - response = await client.post( - base_url, - data=json.dumps([dict(id=1, query="{test}")]), - headers={"content-type": "application/json"}, - ) +@pytest.mark.asyncio +@pytest.mark.parametrize("app", [create_app(batch=True)]) +async def test_batch_allows_post_with_json_encoding(app, client): + response = await client.post( + "/graphql", + data=json.dumps([dict(id=1, query="{test}")]), + headers={"content-type": "application/json"}, + ) - assert response.status == 200 - assert await response.json() == [{"data": {"test": "Hello World"}}] - - @pytest.mark.asyncio - async def test_batch_supports_post_json_query_with_json_variables( - self, client, base_url - ): - response = await client.post( - base_url, - data=json.dumps( - [ - dict( - id=1, - query="query helloWho($who: String){ test(who: $who) }", - variables={"who": "Dolly"}, - ) - ] - ), - headers={"content-type": "application/json"}, - ) + assert response.status == 200 + assert await response.json() == [{"data": {"test": "Hello World"}}] - assert response.status == 200 - assert await response.json() == [{"data": {"test": "Hello Dolly"}}] - - @pytest.mark.asyncio - async def test_batch_allows_post_with_operation_name(self, client, base_url): - response = await client.post( - base_url, - data=json.dumps( - [ - dict( - 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", - ) - ] - ), - headers={"content-type": "application/json"}, - ) - assert response.status == 200 - assert await response.json() == [ - {"data": {"test": "Hello World", "shared": "Hello Everyone"}} - ] +@pytest.mark.asyncio +@pytest.mark.parametrize("app", [create_app(batch=True)]) +async def test_batch_supports_post_json_query_with_json_variables(app, client): + response = await client.post( + "/graphql", + data=json.dumps( + [ + dict( + id=1, + query="query helloWho($who: String){ test(who: $who) }", + variables={"who": "Dolly"}, + ) + ] + ), + headers={"content-type": "application/json"}, + ) + assert response.status == 200 + assert await response.json() == [{"data": {"test": "Hello Dolly"}}] -class TestAsyncSchema: - @pytest.fixture - def executor(self, event_loop): - # pylint: disable=no-self-use - # Only need to test with the AsyncExecutor - return AsyncioExecutor(loop=event_loop) - @pytest.fixture - def view_kwargs(self, view_kwargs): - # pylint: disable=no-self-use - # pylint: disable=redefined-outer-name - view_kwargs.update(schema=AsyncSchema) - return view_kwargs +@pytest.mark.asyncio +@pytest.mark.parametrize("app", [create_app(batch=True)]) +async def test_batch_allows_post_with_operation_name(app, client): + response = await client.post( + "/graphql", + data=json.dumps( + [ + dict( + 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", + ) + ] + ), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert await response.json() == [ + {"data": {"test": "Hello World", "shared": "Hello Everyone"}} + ] - @pytest.mark.asyncio - async def test_async_schema(self, client, url_builder): - response = await client.get(url_builder(query="{a,b,c}")) - assert response.status == 200 - assert await response.json() == {"data": {"a": "hey", "b": "hey2", "c": "hey3"}} +@pytest.mark.asyncio +@pytest.mark.parametrize("app", [create_app(schema=AsyncSchema, enable_async=True)]) +async def test_async_schema(app, client): + response = await client.get(url_string(query="{a,b,c}")) + + assert response.status == 200 + assert await response.json() == {"data": {"a": "hey", "b": "hey2", "c": "hey3"}} @pytest.mark.asyncio -async def test_preflight_request(client, base_url): +async def test_preflight_request(client): response = await client.options( - base_url, headers={"Access-Control-Request-Method": "POST"}, + "/graphql", headers={"Access-Control-Request-Method": "POST"}, ) assert response.status == 200 @pytest.mark.asyncio -async def test_preflight_incorrect_request(client, base_url): +async def test_preflight_incorrect_request(client): response = await client.options( - base_url, headers={"Access-Control-Request-Method": "OPTIONS"}, + "/graphql", headers={"Access-Control-Request-Method": "OPTIONS"}, ) assert response.status == 400 diff --git a/tox.ini b/tox.ini index 6a9d8d1..43f7e99 100644 --- a/tox.ini +++ b/tox.ini @@ -1,34 +1,32 @@ [tox] -envlist = black, flake8, pylint, manifest, py{36,37,38,py3} +envlist = + py{36,37,38} + flake8,import-order,manifest +; requires = tox-conda -[testenv:black] -basepython = python3.8 -deps = black==19.10b0 -commands = - black aiohttp_graphql tests setup.py --check +[testenv] +passenv = * +setenv = + PYTHONPATH = {toxinidir} +install_command = python -m pip install --pre --ignore-installed {opts} {packages} +deps = -e.[test] +commands = + pytest tests --cov-report=term-missing --cov=aiohttp_graphql {posargs} [testenv:flake8] -basepython = python3.8 -deps = flake8>=3.7,<4 +basepython=python3.8 +deps = -e.[dev] commands = - flake8 aiohttp_graphql tests setup.py + flake8 setup.py aiohttp_graphql tests -[testenv:pylint] -basepython = python3.8 -deps = pylint>=2.5,<2.6 +[testenv:import-order] +basepython=python3.8 +deps = -e.[dev] commands = - pylint -rn aiohttp_graphql tests setup.py + isort -rc aiohttp_graphql/ tests/ [testenv:manifest] basepython = python3.8 -deps = check-manifest>=0.40,<1 +deps = -e.[dev] commands = check-manifest -v - -[testenv] -setenv = - PYTHONPATH = {toxinidir} -extras = - test -commands = - pytest tests {posargs}