mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-13 05:06:47 +03:00
Merge branch 'master' into fix/ariadne-link
This commit is contained in:
commit
d96ec55abb
25
.github/workflows/coveralls.yml
vendored
25
.github/workflows/coveralls.yml
vendored
|
@ -1,25 +0,0 @@
|
||||||
name: 📊 Check Coverage
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- '*.x'
|
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '*.md'
|
|
||||||
- '*.rst'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- '*.x'
|
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '*.md'
|
|
||||||
- '*.rst'
|
|
||||||
jobs:
|
|
||||||
coveralls_finish:
|
|
||||||
# check coverage increase/decrease
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Coveralls Finished
|
|
||||||
uses: AndreMiras/coveralls-python-action@develop
|
|
4
.github/workflows/deploy.yml
vendored
4
.github/workflows/deploy.yml
vendored
|
@ -11,10 +11,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.9
|
||||||
- name: Build wheel and source tarball
|
- name: Build wheel and source tarball
|
||||||
run: |
|
run: |
|
||||||
pip install wheel
|
pip install wheel
|
||||||
|
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
@ -8,10 +8,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.9
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|
24
.github/workflows/tests.yml
vendored
24
.github/workflows/tests.yml
vendored
|
@ -25,12 +25,14 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
- {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
|
||||||
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
|
||||||
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
|
- { name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38 }
|
||||||
|
- { name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37 }
|
||||||
|
- { name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36 }
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
|
@ -45,10 +47,20 @@ jobs:
|
||||||
run: echo "::set-output name=dir::$(pip cache dir)"
|
run: echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
|
||||||
- name: cache pip dependencies
|
- name: cache pip dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.pip-cache.outputs.dir }}
|
path: ${{ steps.pip-cache.outputs.dir }}
|
||||||
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}
|
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}
|
||||||
|
|
||||||
- run: pip install tox
|
- run: pip install tox
|
||||||
- run: tox -e ${{ matrix.tox }}
|
- run: tox -e ${{ matrix.tox }}
|
||||||
|
- name: Upload coverage.xml
|
||||||
|
if: ${{ matrix.python == '3.10' }}
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: graphene-sqlalchemy-coverage
|
||||||
|
path: coverage.xml
|
||||||
|
if-no-files-found: error
|
||||||
|
- name: Upload coverage.xml to codecov
|
||||||
|
if: ${{ matrix.python == '3.10' }}
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
default_language_version:
|
default_language_version:
|
||||||
python: python3.8
|
python: python3.9
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.3.0
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-json
|
- id: check-json
|
||||||
|
@ -16,15 +16,15 @@ repos:
|
||||||
- --autofix
|
- --autofix
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude: README.md
|
exclude: README.md
|
||||||
- repo: git://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.24.0
|
rev: v2.37.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
- repo: git://github.com/ambv/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 19.3b0
|
rev: 22.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: git://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 3.8.4
|
rev: 5.0.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
|
|
10
README.md
10
README.md
|
@ -4,12 +4,6 @@
|
||||||
|
|
||||||
**We are looking for contributors**! Please check the [ROADMAP](https://github.com/graphql-python/graphene/blob/master/ROADMAP.md) to see how you can help ❤️
|
**We are looking for contributors**! Please check the [ROADMAP](https://github.com/graphql-python/graphene/blob/master/ROADMAP.md) to see how you can help ❤️
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**The below readme is the documentation for the `dev` (prerelease) version of Graphene. To view the documentation for the latest stable Graphene version go to the [v2 docs](https://docs.graphene-python.org/en/stable/)**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
[Graphene](http://graphene-python.org) is an opinionated Python library for building GraphQL schemas/types fast and easily.
|
[Graphene](http://graphene-python.org) is an opinionated Python library for building GraphQL schemas/types fast and easily.
|
||||||
|
@ -34,10 +28,10 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For instaling graphene, just run this command in your shell
|
To install `graphene`, just run this command in your shell
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install "graphene>=2.0"
|
pip install "graphene>=3.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
93
README.rst
93
README.rst
|
@ -1,18 +1,18 @@
|
||||||
|
|Graphene Logo| `Graphene <http://graphene-python.org>`__ |Build Status| |PyPI version| |Coverage Status|
|
||||||
|
=========================================================================================================
|
||||||
|
|
||||||
|
`💬 Join the community on
|
||||||
|
Slack <https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM>`__
|
||||||
|
|
||||||
**We are looking for contributors**! Please check the
|
**We are looking for contributors**! Please check the
|
||||||
`ROADMAP <https://github.com/graphql-python/graphene/blob/master/ROADMAP.md>`__
|
`ROADMAP <https://github.com/graphql-python/graphene/blob/master/ROADMAP.md>`__
|
||||||
to see how you can help ❤️
|
to see how you can help ❤️
|
||||||
|
|
||||||
--------------
|
|
||||||
|
|
||||||
|Graphene Logo| `Graphene <http://graphene-python.org>`__ |Build Status| |PyPI version| |Coverage Status|
|
|
||||||
=========================================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
Introduction
|
Introduction
|
||||||
------------
|
------------
|
||||||
|
|
||||||
`Graphene <http://graphene-python.org>`__ is a Python library for
|
`Graphene <http://graphene-python.org>`__ is an opinionated Python
|
||||||
building GraphQL schemas/types fast and easily.
|
library for building GraphQL schemas/types fast and easily.
|
||||||
|
|
||||||
- **Easy to use:** Graphene helps you use GraphQL in Python without
|
- **Easy to use:** Graphene helps you use GraphQL in Python without
|
||||||
effort.
|
effort.
|
||||||
|
@ -27,17 +27,18 @@ Integrations
|
||||||
|
|
||||||
Graphene has multiple integrations with different frameworks:
|
Graphene has multiple integrations with different frameworks:
|
||||||
|
|
||||||
+---------------------+----------------------------------------------------------------------------------------------+
|
+-------------------+-------------------------------------------------+
|
||||||
| integration | Package |
|
| integration | Package |
|
||||||
+=====================+==============================================================================================+
|
+===================+=================================================+
|
||||||
| Django | `graphene-django <https://github.com/graphql-python/graphene-django/>`__ |
|
| Django | `graphene-django <https:/ |
|
||||||
+---------------------+----------------------------------------------------------------------------------------------+
|
| | /github.com/graphql-python/graphene-django/>`__ |
|
||||||
| SQLAlchemy | `graphene-sqlalchemy <https://github.com/graphql-python/graphene-sqlalchemy/>`__ |
|
+-------------------+-------------------------------------------------+
|
||||||
+---------------------+----------------------------------------------------------------------------------------------+
|
| SQLAlchemy | `graphene-sqlalchemy <https://git |
|
||||||
| Google App Engine | `graphene-gae <https://github.com/graphql-python/graphene-gae/>`__ |
|
| | hub.com/graphql-python/graphene-sqlalchemy/>`__ |
|
||||||
+---------------------+----------------------------------------------------------------------------------------------+
|
+-------------------+-------------------------------------------------+
|
||||||
| Peewee | *In progress* (`Tracking Issue <https://github.com/graphql-python/graphene/issues/289>`__) |
|
| Google App Engine | `graphene-gae <http |
|
||||||
+---------------------+----------------------------------------------------------------------------------------------+
|
| | s://github.com/graphql-python/graphene-gae/>`__ |
|
||||||
|
+-------------------+-------------------------------------------------+
|
||||||
|
|
||||||
Also, Graphene is fully compatible with the GraphQL spec, working
|
Also, Graphene is fully compatible with the GraphQL spec, working
|
||||||
seamlessly with all GraphQL clients, such as
|
seamlessly with all GraphQL clients, such as
|
||||||
|
@ -48,17 +49,11 @@ seamlessly with all GraphQL clients, such as
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
For instaling graphene, just run this command in your shell
|
To install `graphene`, just run this command in your shell
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
pip install "graphene>=2.0"
|
pip install "graphene>=3.0"
|
||||||
|
|
||||||
2.0 Upgrade Guide
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
Please read `UPGRADE-v2.0.md </UPGRADE-v2.0.md>`__ to learn how to
|
|
||||||
upgrade.
|
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -67,26 +62,26 @@ Here is one example for you to get started:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
hello = graphene.String(description='A typical hello world')
|
hello = graphene.String(description='A typical hello world')
|
||||||
|
|
||||||
def resolve_hello(self, info):
|
def resolve_hello(self, info):
|
||||||
return 'World'
|
return 'World'
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
Then Querying ``graphene.Schema`` is as simple as:
|
Then Querying ``graphene.Schema`` is as simple as:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
query = '''
|
query = '''
|
||||||
query SayHello {
|
query SayHello {
|
||||||
hello
|
hello
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
|
|
||||||
If you want to learn even more, you can also check the following
|
If you want to learn even more, you can also check the following
|
||||||
`examples <examples/>`__:
|
`examples <examples/>`__:
|
||||||
|
@ -110,20 +105,20 @@ dependencies are installed by running:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
virtualenv venv
|
virtualenv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install -e ".[test]"
|
pip install -e ".[test]"
|
||||||
|
|
||||||
Well-written tests and maintaining good test coverage is important to
|
Well-written tests and maintaining good test coverage is important to
|
||||||
this project. While developing, run new and existing tests with:
|
this project. While developing, run new and existing tests with:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
py.test graphene/relay/tests/test_node.py # Single file
|
py.test graphene/relay/tests/test_node.py # Single file
|
||||||
py.test graphene/relay # All tests in directory
|
py.test graphene/relay # All tests in directory
|
||||||
|
|
||||||
Add the ``-s`` flag if you have introduced breakpoints into the code for
|
Add the ``-s`` flag if you have introduced breakpoints into the code for
|
||||||
debugging. Add the ``-v`` ("verbose") flag to get more detailed test
|
debugging. Add the ``-v`` (“verbose”) flag to get more detailed test
|
||||||
output. For even more detailed output, use ``-vv``. Check out the
|
output. For even more detailed output, use ``-vv``. Check out the
|
||||||
`pytest documentation <https://docs.pytest.org/en/latest/>`__ for more
|
`pytest documentation <https://docs.pytest.org/en/latest/>`__ for more
|
||||||
options and test running controls.
|
options and test running controls.
|
||||||
|
@ -132,7 +127,7 @@ You can also run the benchmarks with:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
py.test graphene --benchmark-only
|
py.test graphene --benchmark-only
|
||||||
|
|
||||||
Graphene supports several versions of Python. To make sure that changes
|
Graphene supports several versions of Python. To make sure that changes
|
||||||
do not break compatibility with any of those versions, we use ``tox`` to
|
do not break compatibility with any of those versions, we use ``tox`` to
|
||||||
|
@ -142,14 +137,14 @@ config file, just run:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
tox
|
tox
|
||||||
|
|
||||||
If you wish to run against a specific version defined in the ``tox.ini``
|
If you wish to run against a specific version defined in the ``tox.ini``
|
||||||
file:
|
file:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
tox -e py36
|
tox -e py36
|
||||||
|
|
||||||
Tox can only use whatever versions of Python are installed on your
|
Tox can only use whatever versions of Python are installed on your
|
||||||
system. When you create a pull request, Travis will also be running the
|
system. When you create a pull request, Travis will also be running the
|
||||||
|
@ -168,7 +163,7 @@ An HTML version of the documentation is produced by running:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
make docs
|
make docs
|
||||||
|
|
||||||
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
|
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
|
||||||
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master
|
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master
|
||||||
|
|
|
@ -153,7 +153,7 @@ class Query(ObjectType):
|
||||||
```
|
```
|
||||||
|
|
||||||
Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it
|
Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it
|
||||||
explicity.
|
explicitly.
|
||||||
|
|
||||||
## Django
|
## Django
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,7 @@ def resolve_my_field(root, info, my_arg):
|
||||||
return ...
|
return ...
|
||||||
```
|
```
|
||||||
|
|
||||||
**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as stantard `Connection`'s arguments (first, before, after, before).**
|
**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as standard `Connection`'s arguments (first, last, after, before).**
|
||||||
You may need something like this:
|
You may need something like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
16
docs/conf.py
16
docs/conf.py
|
@ -64,18 +64,18 @@ source_suffix = ".rst"
|
||||||
master_doc = "index"
|
master_doc = "index"
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u"Graphene"
|
project = "Graphene"
|
||||||
copyright = u"Graphene 2016"
|
copyright = "Graphene 2016"
|
||||||
author = u"Syrus Akbary"
|
author = "Syrus Akbary"
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = u"1.0"
|
version = "1.0"
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = u"1.0"
|
release = "1.0"
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
@ -278,7 +278,7 @@ latex_elements = {
|
||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual")
|
(master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual")
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
@ -318,7 +318,7 @@ latex_documents = [
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [(master_doc, "graphene", u"Graphene Documentation", [author], 1)]
|
man_pages = [(master_doc, "graphene", "Graphene Documentation", [author], 1)]
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
# If true, show URL addresses after external links.
|
||||||
#
|
#
|
||||||
|
@ -334,7 +334,7 @@ texinfo_documents = [
|
||||||
(
|
(
|
||||||
master_doc,
|
master_doc,
|
||||||
"Graphene",
|
"Graphene",
|
||||||
u"Graphene Documentation",
|
"Graphene Documentation",
|
||||||
author,
|
author,
|
||||||
"Graphene",
|
"Graphene",
|
||||||
"One line description of project.",
|
"One line description of project.",
|
||||||
|
|
|
@ -4,7 +4,7 @@ Dataloader
|
||||||
DataLoader is a generic utility to be used as part of your application's
|
DataLoader is a generic utility to be used as part of your application's
|
||||||
data fetching layer to provide a simplified and consistent API over
|
data fetching layer to provide a simplified and consistent API over
|
||||||
various remote data sources such as databases or web services via batching
|
various remote data sources such as databases or web services via batching
|
||||||
and caching.
|
and caching. It is provided by a separate package `aiodataloader <https://pypi.org/project/aiodataloader/>`.
|
||||||
|
|
||||||
|
|
||||||
Batching
|
Batching
|
||||||
|
@ -15,32 +15,31 @@ Create loaders by providing a batch loading function.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from promise import Promise
|
from aiodataloader import DataLoader
|
||||||
from promise.dataloader import DataLoader
|
|
||||||
|
|
||||||
class UserLoader(DataLoader):
|
class UserLoader(DataLoader):
|
||||||
def batch_load_fn(self, keys):
|
async def batch_load_fn(self, keys):
|
||||||
# Here we return a promise that will result on the
|
# Here we call a function to return a user for each key in keys
|
||||||
# corresponding user for each key in keys
|
return [get_user(id=key) for key in keys]
|
||||||
return Promise.resolve([get_user(id=key) for key in keys])
|
|
||||||
|
|
||||||
|
|
||||||
A batch loading function accepts a list of keys, and returns a ``Promise``
|
A batch loading async function accepts a list of keys, and returns a list of ``values``.
|
||||||
which resolves to a list of ``values``.
|
|
||||||
|
|
||||||
Then load individual values from the loader. ``DataLoader`` will coalesce all
|
|
||||||
individual loads which occur within a single frame of execution (executed once
|
``DataLoader`` will coalesce all individual loads which occur within a
|
||||||
the wrapping promise is resolved) and then call your batch function with all
|
single frame of execution (executed once the wrapping event loop is resolved)
|
||||||
requested keys.
|
and then call your batch function with all requested keys.
|
||||||
|
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
user_loader = UserLoader()
|
user_loader = UserLoader()
|
||||||
|
|
||||||
user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id))
|
user1 = await user_loader.load(1)
|
||||||
|
user1_best_friend = await user_loader.load(user1.best_friend_id))
|
||||||
|
|
||||||
user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id))
|
user2 = await user_loader.load(2)
|
||||||
|
user2_best_friend = await user_loader.load(user2.best_friend_id))
|
||||||
|
|
||||||
|
|
||||||
A naive application may have issued *four* round-trips to a backend for the
|
A naive application may have issued *four* round-trips to a backend for the
|
||||||
|
@ -54,9 +53,9 @@ make sure that you then order the query result for the results to match the keys
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class UserLoader(DataLoader):
|
class UserLoader(DataLoader):
|
||||||
def batch_load_fn(self, keys):
|
async def batch_load_fn(self, keys):
|
||||||
users = {user.id: user for user in User.objects.filter(id__in=keys)}
|
users = {user.id: user for user in User.objects.filter(id__in=keys)}
|
||||||
return Promise.resolve([users.get(user_id) for user_id in keys])
|
return [users.get(user_id) for user_id in keys]
|
||||||
|
|
||||||
|
|
||||||
``DataLoader`` allows you to decouple unrelated parts of your application without
|
``DataLoader`` allows you to decouple unrelated parts of your application without
|
||||||
|
@ -96,7 +95,7 @@ Consider the following GraphQL request:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend,
|
If ``me``, ``bestFriend`` and ``friends`` each need to send a request to the backend,
|
||||||
there could be at most 13 database requests!
|
there could be at most 13 database requests!
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,8 +110,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac
|
||||||
best_friend = graphene.Field(lambda: User)
|
best_friend = graphene.Field(lambda: User)
|
||||||
friends = graphene.List(lambda: User)
|
friends = graphene.List(lambda: User)
|
||||||
|
|
||||||
def resolve_best_friend(root, info):
|
async def resolve_best_friend(root, info):
|
||||||
return user_loader.load(root.best_friend_id)
|
return await user_loader.load(root.best_friend_id)
|
||||||
|
|
||||||
def resolve_friends(root, info):
|
async def resolve_friends(root, info):
|
||||||
return user_loader.load_many(root.friend_ids)
|
return await user_loader.load_many(root.friend_ids)
|
||||||
|
|
|
@ -4,5 +4,5 @@ File uploading
|
||||||
File uploading is not part of the official GraphQL spec yet and is not natively
|
File uploading is not part of the official GraphQL spec yet and is not natively
|
||||||
implemented in Graphene.
|
implemented in Graphene.
|
||||||
|
|
||||||
If your server needs to support file uploading then you can use the libary: `graphene-file-upload <https://github.com/lmcgartland/graphene-file-upload>`_ which enhances Graphene to add file
|
If your server needs to support file uploading then you can use the library: `graphene-file-upload <https://github.com/lmcgartland/graphene-file-upload>`_ which enhances Graphene to add file
|
||||||
uploads and conforms to the unoffical GraphQL `multipart request spec <https://github.com/jaydenseric/graphql-multipart-request-spec>`_.
|
uploads and conforms to the unoffical GraphQL `multipart request spec <https://github.com/jaydenseric/graphql-multipart-request-spec>`_.
|
||||||
|
|
|
@ -46,7 +46,7 @@ Functional example
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Middleware can also be defined as a function. Here we define a middleware that
|
Middleware can also be defined as a function. Here we define a middleware that
|
||||||
logs the time it takes to resolve each field
|
logs the time it takes to resolve each field:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ Usage
|
||||||
Here is how you would implement depth-limiting on your schema.
|
Here is how you would implement depth-limiting on your schema.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from graphql import validate, parse
|
from graphql import validate, parse
|
||||||
from graphene import ObjectType, Schema, String
|
from graphene import ObjectType, Schema, String
|
||||||
from graphene.validation import depth_limit_validator
|
from graphene.validation import depth_limit_validator
|
||||||
|
@ -37,7 +38,7 @@ Here is how you would implement depth-limiting on your schema.
|
||||||
# will not be executed.
|
# will not be executed.
|
||||||
|
|
||||||
validation_errors = validate(
|
validation_errors = validate(
|
||||||
schema=schema,
|
schema=schema.graphql_schema,
|
||||||
document_ast=parse('THE QUERY'),
|
document_ast=parse('THE QUERY'),
|
||||||
rules=(
|
rules=(
|
||||||
depth_limit_validator(
|
depth_limit_validator(
|
||||||
|
@ -58,6 +59,7 @@ Usage
|
||||||
Here is how you would disable introspection for your schema.
|
Here is how you would disable introspection for your schema.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from graphql import validate, parse
|
from graphql import validate, parse
|
||||||
from graphene import ObjectType, Schema, String
|
from graphene import ObjectType, Schema, String
|
||||||
from graphene.validation import DisableIntrospection
|
from graphene.validation import DisableIntrospection
|
||||||
|
@ -72,7 +74,7 @@ Here is how you would disable introspection for your schema.
|
||||||
# introspection queries will not be executed.
|
# introspection queries will not be executed.
|
||||||
|
|
||||||
validation_errors = validate(
|
validation_errors = validate(
|
||||||
schema=schema,
|
schema=schema.graphql_schema,
|
||||||
document_ast=parse('THE QUERY'),
|
document_ast=parse('THE QUERY'),
|
||||||
rules=(
|
rules=(
|
||||||
DisableIntrospection,
|
DisableIntrospection,
|
||||||
|
@ -92,6 +94,7 @@ reason. Here is an example query validator that visits field definitions in Grap
|
||||||
if any of those fields are blacklisted:
|
if any of those fields are blacklisted:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from graphql import GraphQLError
|
from graphql import GraphQLError
|
||||||
from graphql.language import FieldNode
|
from graphql.language import FieldNode
|
||||||
from graphql.validation import ValidationRule
|
from graphql.validation import ValidationRule
|
||||||
|
|
|
@ -59,15 +59,15 @@ When we send a **Query** requesting only one **Field**, ``hello``, and specify a
|
||||||
Requirements
|
Requirements
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
- Python (2.7, 3.4, 3.5, 3.6, pypy)
|
- Python (3.6, 3.7, 3.8, 3.9, 3.10, pypy)
|
||||||
- Graphene (2.0)
|
- Graphene (3.0)
|
||||||
|
|
||||||
Project setup
|
Project setup
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
pip install "graphene>=2.0"
|
pip install "graphene>=3.0"
|
||||||
|
|
||||||
Creating a basic Schema
|
Creating a basic Schema
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -86,7 +86,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu
|
||||||
assert Color(1) == Color.RED
|
assert Color(1) == Color.RED
|
||||||
|
|
||||||
|
|
||||||
However, in Graphene ``Enum`` you need to call get to have the same effect:
|
However, in Graphene ``Enum`` you need to call `.get` to have the same effect:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ Both of these types have all of the fields from the ``Character`` interface,
|
||||||
but also bring in extra fields, ``home_planet``, ``starships`` and
|
but also bring in extra fields, ``home_planet``, ``starships`` and
|
||||||
``primary_function``, that are specific to that particular type of character.
|
``primary_function``, that are specific to that particular type of character.
|
||||||
|
|
||||||
The full GraphQL schema defition will look like this:
|
The full GraphQL schema definition will look like this:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
|
|
|
@ -85,9 +85,9 @@ We should receive:
|
||||||
|
|
||||||
InputFields and InputObjectTypes
|
InputFields and InputObjectTypes
|
||||||
----------------------------------
|
----------------------------------
|
||||||
InputFields are used in mutations to allow nested input data for mutations
|
InputFields are used in mutations to allow nested input data for mutations.
|
||||||
|
|
||||||
To use an InputField you define an InputObjectType that specifies the structure of your input data
|
To use an InputField you define an InputObjectType that specifies the structure of your input data:
|
||||||
|
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
@ -112,7 +112,7 @@ To use an InputField you define an InputObjectType that specifies the structure
|
||||||
return CreatePerson(person=person)
|
return CreatePerson(person=person)
|
||||||
|
|
||||||
|
|
||||||
Note that **name** and **age** are part of **person_data** now
|
Note that **name** and **age** are part of **person_data** now.
|
||||||
|
|
||||||
Using the above mutation your new query would look like this:
|
Using the above mutation your new query would look like this:
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ Using the above mutation your new query would look like this:
|
||||||
}
|
}
|
||||||
|
|
||||||
InputObjectTypes can also be fields of InputObjectTypes allowing you to have
|
InputObjectTypes can also be fields of InputObjectTypes allowing you to have
|
||||||
as complex of input data as you need
|
as complex of input data as you need:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ To return an existing ObjectType instead of a mutation-specific type, set the **
|
||||||
def mutate(root, info, name):
|
def mutate(root, info, name):
|
||||||
return Person(name=name)
|
return Person(name=name)
|
||||||
|
|
||||||
Then, if we query (``schema.execute(query_str)``) the following:
|
Then, if we query (``schema.execute(query_str)``) with the following:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
|
|
|
@ -270,7 +270,7 @@ The following is an example for creating a DateTime scalar:
|
||||||
return dt.isoformat()
|
return dt.isoformat()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(node):
|
def parse_literal(node, _variables=None):
|
||||||
if isinstance(node, ast.StringValue):
|
if isinstance(node, ast.StringValue):
|
||||||
return datetime.datetime.strptime(
|
return datetime.datetime.strptime(
|
||||||
node.value, "%Y-%m-%dT%H:%M:%S.%f")
|
node.value, "%Y-%m-%dT%H:%M:%S.%f")
|
||||||
|
|
|
@ -44,7 +44,7 @@ There are some cases where the schema cannot access all of the types that we pla
|
||||||
For example, when a field returns an ``Interface``, the schema doesn't know about any of the
|
For example, when a field returns an ``Interface``, the schema doesn't know about any of the
|
||||||
implementations.
|
implementations.
|
||||||
|
|
||||||
In this case, we need to use the ``types`` argument when creating the Schema.
|
In this case, we need to use the ``types`` argument when creating the Schema:
|
||||||
|
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
@ -63,7 +63,7 @@ By default all field and argument names (that are not
|
||||||
explicitly set with the ``name`` arg) will be converted from
|
explicitly set with the ``name`` arg) will be converted from
|
||||||
``snake_case`` to ``camelCase`` (as the API is usually being consumed by a js/mobile client)
|
``snake_case`` to ``camelCase`` (as the API is usually being consumed by a js/mobile client)
|
||||||
|
|
||||||
For example with the ObjectType
|
For example with the ObjectType the ``last_name`` field name is converted to ``lastName``:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -71,12 +71,10 @@ For example with the ObjectType
|
||||||
last_name = graphene.String()
|
last_name = graphene.String()
|
||||||
other_name = graphene.String(name='_other_Name')
|
other_name = graphene.String(name='_other_Name')
|
||||||
|
|
||||||
the ``last_name`` field name is converted to ``lastName``.
|
|
||||||
|
|
||||||
In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor.
|
In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor.
|
||||||
``other_name`` converts to ``_other_Name`` (without further transformations).
|
``other_name`` converts to ``_other_Name`` (without further transformations).
|
||||||
|
|
||||||
Your query should look like
|
Your query should look like:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
|
@ -86,7 +84,7 @@ Your query should look like
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation.
|
To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ to specify any common fields between the types.
|
||||||
The basics:
|
The basics:
|
||||||
|
|
||||||
- Each Union is a Python class that inherits from ``graphene.Union``.
|
- Each Union is a Python class that inherits from ``graphene.Union``.
|
||||||
- Unions don't have any fields on it, just links to the possible objecttypes.
|
- Unions don't have any fields on it, just links to the possible ObjectTypes.
|
||||||
|
|
||||||
Quick example
|
Quick example
|
||||||
-------------
|
-------------
|
||||||
|
|
|
@ -49,7 +49,7 @@ type Faction implements Node {
|
||||||
name: String
|
name: String
|
||||||
|
|
||||||
"""The ships used by the faction."""
|
"""The ships used by the faction."""
|
||||||
ships(before: String = null, after: String = null, first: Int = null, last: Int = null): ShipConnection
|
ships(before: String, after: String, first: Int, last: Int): ShipConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
"""An object with an ID"""
|
"""An object with an ID"""
|
||||||
|
@ -115,5 +115,4 @@ input IntroduceShipInput {
|
||||||
shipName: String!
|
shipName: String!
|
||||||
factionId: String!
|
factionId: String!
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
}
|
}'''
|
||||||
'''
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ client = Client(schema)
|
||||||
|
|
||||||
|
|
||||||
def test_str_schema(snapshot):
|
def test_str_schema(snapshot):
|
||||||
snapshot.assert_match(str(schema))
|
snapshot.assert_match(str(schema).strip())
|
||||||
|
|
||||||
|
|
||||||
def test_correctly_fetches_id_name_rebels(snapshot):
|
def test_correctly_fetches_id_name_rebels(snapshot):
|
||||||
|
|
|
@ -41,7 +41,7 @@ from .types import (
|
||||||
from .utils.module_loading import lazy_import
|
from .utils.module_loading import lazy_import
|
||||||
from .utils.resolve_only_args import resolve_only_args
|
from .utils.resolve_only_args import resolve_only_args
|
||||||
|
|
||||||
VERSION = (3, 0, 0, "beta", 7)
|
VERSION = (3, 1, 0, "final", 0)
|
||||||
|
|
||||||
|
|
||||||
__version__ = get_version(VERSION)
|
__version__ = get_version(VERSION)
|
||||||
|
|
|
@ -19,10 +19,7 @@ def get_version(version=None):
|
||||||
sub = ""
|
sub = ""
|
||||||
if version[3] == "alpha" and version[4] == 0:
|
if version[3] == "alpha" and version[4] == 0:
|
||||||
git_changeset = get_git_changeset()
|
git_changeset = get_git_changeset()
|
||||||
if git_changeset:
|
sub = ".dev%s" % git_changeset if git_changeset else ".dev"
|
||||||
sub = ".dev%s" % git_changeset
|
|
||||||
else:
|
|
||||||
sub = ".dev"
|
|
||||||
elif version[3] != "final":
|
elif version[3] != "final":
|
||||||
mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
|
mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
|
||||||
sub = mapping[version[3]] + str(version[4])
|
sub = mapping[version[3]] + str(version[4])
|
||||||
|
|
|
@ -18,11 +18,7 @@ def is_node(objecttype):
|
||||||
if not issubclass(objecttype, ObjectType):
|
if not issubclass(objecttype, ObjectType):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for i in objecttype._meta.interfaces:
|
return any(issubclass(i, Node) for i in objecttype._meta.interfaces)
|
||||||
if issubclass(i, Node):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class GlobalID(Field):
|
class GlobalID(Field):
|
||||||
|
@ -90,11 +86,13 @@ class Node(AbstractNode):
|
||||||
def get_node_from_global_id(cls, info, global_id, only_type=None):
|
def get_node_from_global_id(cls, info, global_id, only_type=None):
|
||||||
try:
|
try:
|
||||||
_type, _id = cls.from_global_id(global_id)
|
_type, _id = cls.from_global_id(global_id)
|
||||||
|
if not _type:
|
||||||
|
raise ValueError("Invalid Global ID")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f'Unable to parse global ID "{global_id}". '
|
f'Unable to parse global ID "{global_id}". '
|
||||||
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
|
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
|
||||||
f"Exception message: {str(e)}"
|
f"Exception message: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
graphene_type = info.schema.get_type(_type)
|
graphene_type = info.schema.get_type(_type)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
from graphql_relay import to_global_id
|
from textwrap import dedent
|
||||||
|
|
||||||
from graphene.tests.utils import dedent
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
from ...types import ObjectType, Schema, String
|
from ...types import ObjectType, Schema, String
|
||||||
from ..node import Node, is_node
|
from ..node import Node, is_node
|
||||||
|
@ -171,8 +171,10 @@ def test_node_field_only_lazy_type_wrong():
|
||||||
|
|
||||||
|
|
||||||
def test_str_schema():
|
def test_str_schema():
|
||||||
assert str(schema) == dedent(
|
assert (
|
||||||
'''
|
str(schema).strip()
|
||||||
|
== dedent(
|
||||||
|
'''
|
||||||
schema {
|
schema {
|
||||||
query: RootQuery
|
query: RootQuery
|
||||||
}
|
}
|
||||||
|
@ -213,4 +215,5 @@ def test_str_schema():
|
||||||
): MyNode
|
): MyNode
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
).strip()
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from graphql import graphql_sync
|
from textwrap import dedent
|
||||||
|
|
||||||
from graphene.tests.utils import dedent
|
from graphql import graphql_sync
|
||||||
|
|
||||||
from ...types import Interface, ObjectType, Schema
|
from ...types import Interface, ObjectType, Schema
|
||||||
from ...types.scalars import Int, String
|
from ...types.scalars import Int, String
|
||||||
|
@ -54,8 +54,10 @@ graphql_schema = schema.graphql_schema
|
||||||
|
|
||||||
|
|
||||||
def test_str_schema_correct():
|
def test_str_schema_correct():
|
||||||
assert str(schema) == dedent(
|
assert (
|
||||||
'''
|
str(schema).strip()
|
||||||
|
== dedent(
|
||||||
|
'''
|
||||||
schema {
|
schema {
|
||||||
query: RootQuery
|
query: RootQuery
|
||||||
}
|
}
|
||||||
|
@ -93,6 +95,7 @@ def test_str_schema_correct():
|
||||||
): Node
|
): Node
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
).strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from promise import Promise, is_thenable
|
from promise import Promise, is_thenable
|
||||||
from graphql.error import format_error as format_graphql_error
|
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
|
|
||||||
from graphene.types.schema import Schema
|
from graphene.types.schema import Schema
|
||||||
|
@ -7,7 +6,7 @@ from graphene.types.schema import Schema
|
||||||
|
|
||||||
def default_format_error(error):
|
def default_format_error(error):
|
||||||
if isinstance(error, GraphQLError):
|
if isinstance(error, GraphQLError):
|
||||||
return format_graphql_error(error)
|
return error.formatted
|
||||||
return {"message": str(error)}
|
return {"message": str(error)}
|
||||||
|
|
||||||
|
|
||||||
|
|
36
graphene/tests/issues/test_1394.py
Normal file
36
graphene/tests/issues/test_1394.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from ...types import ObjectType, Schema, String, NonNull
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
hello = String(input=NonNull(String))
|
||||||
|
|
||||||
|
def resolve_hello(self, info, input):
|
||||||
|
if input == "nothing":
|
||||||
|
return None
|
||||||
|
return f"Hello {input}!"
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
|
||||||
|
def test_required_input_provided():
|
||||||
|
"""
|
||||||
|
Test that a required argument works when provided.
|
||||||
|
"""
|
||||||
|
input_value = "Potato"
|
||||||
|
result = schema.execute('{ hello(input: "%s") }' % input_value)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"hello": "Hello Potato!"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_required_input_missing():
|
||||||
|
"""
|
||||||
|
Test that a required argument raised an error if not provided.
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ hello }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "Field 'hello' argument 'input' of type 'String!' is required, but it was not provided."
|
||||||
|
)
|
53
graphene/tests/issues/test_1419.py
Normal file
53
graphene/tests/issues/test_1419.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ...types.base64 import Base64
|
||||||
|
from ...types.datetime import Date, DateTime
|
||||||
|
from ...types.decimal import Decimal
|
||||||
|
from ...types.generic import GenericScalar
|
||||||
|
from ...types.json import JSONString
|
||||||
|
from ...types.objecttype import ObjectType
|
||||||
|
from ...types.scalars import ID, BigInt, Boolean, Float, Int, String
|
||||||
|
from ...types.schema import Schema
|
||||||
|
from ...types.uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"input_type,input_value",
|
||||||
|
[
|
||||||
|
(Date, '"2022-02-02"'),
|
||||||
|
(GenericScalar, '"foo"'),
|
||||||
|
(Int, "1"),
|
||||||
|
(BigInt, "12345678901234567890"),
|
||||||
|
(Float, "1.1"),
|
||||||
|
(String, '"foo"'),
|
||||||
|
(Boolean, "true"),
|
||||||
|
(ID, "1"),
|
||||||
|
(DateTime, '"2022-02-02T11:11:11"'),
|
||||||
|
(UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'),
|
||||||
|
(Decimal, '"1.1"'),
|
||||||
|
(JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'),
|
||||||
|
(Base64, '"Q2hlbG8gd29ycmxkCg=="'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_literal_with_variables(input_type, input_value):
|
||||||
|
# input_b needs to be evaluated as literal while the variable dict for
|
||||||
|
# input_a is passed along.
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
generic = GenericScalar(input_a=GenericScalar(), input_b=input_type())
|
||||||
|
|
||||||
|
def resolve_generic(self, info, input_a=None, input_b=None):
|
||||||
|
return input
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
query Test($a: GenericScalar){{
|
||||||
|
generic(inputA: $a, inputB: {input_value})
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
result = schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"a": "bar"},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
|
@ -1,9 +0,0 @@
|
||||||
from textwrap import dedent as _dedent
|
|
||||||
|
|
||||||
|
|
||||||
def dedent(text: str) -> str:
|
|
||||||
"""Fix indentation of given text by removing leading spaces and tabs.
|
|
||||||
Also removes leading newlines and trailing spaces and tabs, but keeps trailing
|
|
||||||
newlines.
|
|
||||||
"""
|
|
||||||
return _dedent(text.lstrip("\n").rstrip(" \t"))
|
|
|
@ -1,4 +1,5 @@
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
from graphql import Undefined
|
||||||
|
|
||||||
from .dynamic import Dynamic
|
from .dynamic import Dynamic
|
||||||
from .mountedtype import MountedType
|
from .mountedtype import MountedType
|
||||||
|
@ -41,7 +42,7 @@ class Argument(MountedType):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
type_,
|
type_,
|
||||||
default_value=None,
|
default_value=Undefined,
|
||||||
description=None,
|
description=None,
|
||||||
name=None,
|
name=None,
|
||||||
required=False,
|
required=False,
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Base64(Scalar):
|
||||||
return b64encode(value).decode("utf-8")
|
return b64encode(value).decode("utf-8")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_literal(cls, node):
|
def parse_literal(cls, node, _variables=None):
|
||||||
if not isinstance(node, StringValueNode):
|
if not isinstance(node, StringValueNode):
|
||||||
raise GraphQLError(
|
raise GraphQLError(
|
||||||
f"Base64 cannot represent non-string value: {print_ast(node)}"
|
f"Base64 cannot represent non-string value: {print_ast(node)}"
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
||||||
|
|
||||||
from decimal import Decimal as _Decimal
|
from decimal import Decimal as _Decimal
|
||||||
|
|
||||||
|
from graphql import Undefined
|
||||||
from graphql.language.ast import StringValueNode, IntValueNode
|
from graphql.language.ast import StringValueNode, IntValueNode
|
||||||
|
|
||||||
from .scalars import Scalar
|
from .scalars import Scalar
|
||||||
|
@ -22,13 +23,14 @@ class Decimal(Scalar):
|
||||||
return str(dec)
|
return str(dec)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_literal(cls, node):
|
def parse_literal(cls, node, _variables=None):
|
||||||
if isinstance(node, (StringValueNode, IntValueNode)):
|
if isinstance(node, (StringValueNode, IntValueNode)):
|
||||||
return cls.parse_value(node.value)
|
return cls.parse_value(node.value)
|
||||||
|
return Undefined
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_value(value):
|
def parse_value(value):
|
||||||
try:
|
try:
|
||||||
return _Decimal(value)
|
return _Decimal(value)
|
||||||
except ValueError:
|
except Exception:
|
||||||
return None
|
return Undefined
|
||||||
|
|
|
@ -7,7 +7,6 @@ from graphql import (
|
||||||
GraphQLObjectType,
|
GraphQLObjectType,
|
||||||
GraphQLScalarType,
|
GraphQLScalarType,
|
||||||
GraphQLUnionType,
|
GraphQLUnionType,
|
||||||
Undefined,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType):
|
||||||
try:
|
try:
|
||||||
value = enum[value]
|
value = enum[value]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return Undefined
|
pass
|
||||||
return super(GrapheneEnumType, self).serialize(value)
|
return super(GrapheneEnumType, self).serialize(value)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,10 @@ class EnumMeta(SubclassWithMeta_Meta):
|
||||||
return super(EnumMeta, cls).__call__(*args, **kwargs)
|
return super(EnumMeta, cls).__call__(*args, **kwargs)
|
||||||
# return cls._meta.enum(*args, **kwargs)
|
# return cls._meta.enum(*args, **kwargs)
|
||||||
|
|
||||||
def from_enum(cls, enum, description=None, deprecation_reason=None): # noqa: N805
|
def from_enum(
|
||||||
|
cls, enum, name=None, description=None, deprecation_reason=None
|
||||||
|
): # noqa: N805
|
||||||
|
name = name or enum.__name__
|
||||||
description = description or enum.__doc__
|
description = description or enum.__doc__
|
||||||
meta_dict = {
|
meta_dict = {
|
||||||
"enum": enum,
|
"enum": enum,
|
||||||
|
@ -60,7 +63,7 @@ class EnumMeta(SubclassWithMeta_Meta):
|
||||||
"deprecation_reason": deprecation_reason,
|
"deprecation_reason": deprecation_reason,
|
||||||
}
|
}
|
||||||
meta_class = type("Meta", (object,), meta_dict)
|
meta_class = type("Meta", (object,), meta_dict)
|
||||||
return type(meta_class.enum.__name__, (Enum,), {"Meta": meta_class})
|
return type(name, (Enum,), {"Meta": meta_class})
|
||||||
|
|
||||||
|
|
||||||
class Enum(UnmountedType, BaseType, metaclass=EnumMeta):
|
class Enum(UnmountedType, BaseType, metaclass=EnumMeta):
|
||||||
|
|
|
@ -29,7 +29,7 @@ class GenericScalar(Scalar):
|
||||||
parse_value = identity
|
parse_value = identity
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(ast):
|
def parse_literal(ast, _variables=None):
|
||||||
if isinstance(ast, (StringValueNode, BooleanValueNode)):
|
if isinstance(ast, (StringValueNode, BooleanValueNode)):
|
||||||
return ast.value
|
return ast.value
|
||||||
elif isinstance(ast, IntValueNode):
|
elif isinstance(ast, IntValueNode):
|
||||||
|
|
|
@ -5,11 +5,12 @@ from .utils import yank_fields_from_attrs
|
||||||
# For static type checking with Mypy
|
# For static type checking with Mypy
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from typing import Dict # NOQA
|
from typing import Dict, Iterable, Type # NOQA
|
||||||
|
|
||||||
|
|
||||||
class InterfaceOptions(BaseOptions):
|
class InterfaceOptions(BaseOptions):
|
||||||
fields = None # type: Dict[str, Field]
|
fields = None # type: Dict[str, Field]
|
||||||
|
interfaces = () # type: Iterable[Type[Interface]]
|
||||||
|
|
||||||
|
|
||||||
class Interface(BaseType):
|
class Interface(BaseType):
|
||||||
|
@ -45,7 +46,7 @@ class Interface(BaseType):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass_with_meta__(cls, _meta=None, **options):
|
def __init_subclass_with_meta__(cls, _meta=None, interfaces=(), **options):
|
||||||
if not _meta:
|
if not _meta:
|
||||||
_meta = InterfaceOptions(cls)
|
_meta = InterfaceOptions(cls)
|
||||||
|
|
||||||
|
@ -58,6 +59,9 @@ class Interface(BaseType):
|
||||||
else:
|
else:
|
||||||
_meta.fields = fields
|
_meta.fields = fields
|
||||||
|
|
||||||
|
if not _meta.interfaces:
|
||||||
|
_meta.interfaces = interfaces
|
||||||
|
|
||||||
super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options)
|
super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from graphql import Undefined
|
||||||
from graphql.language.ast import StringValueNode
|
from graphql.language.ast import StringValueNode
|
||||||
|
|
||||||
from .scalars import Scalar
|
from .scalars import Scalar
|
||||||
|
@ -20,9 +21,13 @@ class JSONString(Scalar):
|
||||||
return json.dumps(dt)
|
return json.dumps(dt)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(node):
|
def parse_literal(node, _variables=None):
|
||||||
if isinstance(node, StringValueNode):
|
if isinstance(node, StringValueNode):
|
||||||
return json.loads(node.value)
|
try:
|
||||||
|
return json.loads(node.value)
|
||||||
|
except Exception as error:
|
||||||
|
raise ValueError(f"Badly formed JSONString: {str(error)}")
|
||||||
|
return Undefined
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_value(value):
|
def parse_value(value):
|
||||||
|
|
|
@ -29,21 +29,21 @@ class Mutation(ObjectType):
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from graphene import Mutation, ObjectType, String, Boolean, Field
|
import graphene
|
||||||
|
|
||||||
class CreatePerson(Mutation):
|
class CreatePerson(graphene.Mutation):
|
||||||
class Arguments:
|
class Arguments:
|
||||||
name = String()
|
name = graphene.String()
|
||||||
|
|
||||||
ok = Boolean()
|
ok = graphene.Boolean()
|
||||||
person = Field(Person)
|
person = graphene.Field(Person)
|
||||||
|
|
||||||
def mutate(parent, info, name):
|
def mutate(parent, info, name):
|
||||||
person = Person(name=name)
|
person = Person(name=name)
|
||||||
ok = True
|
ok = True
|
||||||
return CreatePerson(person=person, ok=ok)
|
return CreatePerson(person=person, ok=ok)
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
class Mutation(graphene.ObjectType):
|
||||||
create_person = CreatePerson.Field()
|
create_person = CreatePerson.Field()
|
||||||
|
|
||||||
Meta class options (optional):
|
Meta class options (optional):
|
||||||
|
@ -101,10 +101,7 @@ class Mutation(ObjectType):
|
||||||
"Read more:"
|
"Read more:"
|
||||||
" https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input"
|
" https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input"
|
||||||
)
|
)
|
||||||
if input_class:
|
arguments = props(input_class) if input_class else {}
|
||||||
arguments = props(input_class)
|
|
||||||
else:
|
|
||||||
arguments = {}
|
|
||||||
if not resolver:
|
if not resolver:
|
||||||
mutate = getattr(cls, "mutate", None)
|
mutate = getattr(cls, "mutate", None)
|
||||||
assert mutate, "All mutations must define a mutate method in it"
|
assert mutate, "All mutations must define a mutate method in it"
|
||||||
|
|
|
@ -7,9 +7,7 @@ def dict_resolver(attname, default_value, root, info, **args):
|
||||||
|
|
||||||
|
|
||||||
def dict_or_attr_resolver(attname, default_value, root, info, **args):
|
def dict_or_attr_resolver(attname, default_value, root, info, **args):
|
||||||
resolver = attr_resolver
|
resolver = dict_resolver if isinstance(root, dict) else attr_resolver
|
||||||
if isinstance(root, dict):
|
|
||||||
resolver = dict_resolver
|
|
||||||
return resolver(attname, default_value, root, info, **args)
|
return resolver(attname, default_value, root, info, **args)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from graphql import Undefined
|
||||||
from graphql.language.ast import (
|
from graphql.language.ast import (
|
||||||
BooleanValueNode,
|
BooleanValueNode,
|
||||||
FloatValueNode,
|
FloatValueNode,
|
||||||
|
@ -67,19 +68,21 @@ class Int(Scalar):
|
||||||
try:
|
try:
|
||||||
num = int(float(value))
|
num = int(float(value))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return Undefined
|
||||||
if MIN_INT <= num <= MAX_INT:
|
if MIN_INT <= num <= MAX_INT:
|
||||||
return num
|
return num
|
||||||
|
return Undefined
|
||||||
|
|
||||||
serialize = coerce_int
|
serialize = coerce_int
|
||||||
parse_value = coerce_int
|
parse_value = coerce_int
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(ast):
|
def parse_literal(ast, _variables=None):
|
||||||
if isinstance(ast, IntValueNode):
|
if isinstance(ast, IntValueNode):
|
||||||
num = int(ast.value)
|
num = int(ast.value)
|
||||||
if MIN_INT <= num <= MAX_INT:
|
if MIN_INT <= num <= MAX_INT:
|
||||||
return num
|
return num
|
||||||
|
return Undefined
|
||||||
|
|
||||||
|
|
||||||
class BigInt(Scalar):
|
class BigInt(Scalar):
|
||||||
|
@ -97,16 +100,17 @@ class BigInt(Scalar):
|
||||||
try:
|
try:
|
||||||
num = int(float(value))
|
num = int(float(value))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return Undefined
|
||||||
return num
|
return num
|
||||||
|
|
||||||
serialize = coerce_int
|
serialize = coerce_int
|
||||||
parse_value = coerce_int
|
parse_value = coerce_int
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(ast):
|
def parse_literal(ast, _variables=None):
|
||||||
if isinstance(ast, IntValueNode):
|
if isinstance(ast, IntValueNode):
|
||||||
return int(ast.value)
|
return int(ast.value)
|
||||||
|
return Undefined
|
||||||
|
|
||||||
|
|
||||||
class Float(Scalar):
|
class Float(Scalar):
|
||||||
|
@ -122,15 +126,16 @@ class Float(Scalar):
|
||||||
try:
|
try:
|
||||||
return float(value)
|
return float(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return Undefined
|
||||||
|
|
||||||
serialize = coerce_float
|
serialize = coerce_float
|
||||||
parse_value = coerce_float
|
parse_value = coerce_float
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(ast):
|
def parse_literal(ast, _variables=None):
|
||||||
if isinstance(ast, (FloatValueNode, IntValueNode)):
|
if isinstance(ast, (FloatValueNode, IntValueNode)):
|
||||||
return float(ast.value)
|
return float(ast.value)
|
||||||
|
return Undefined
|
||||||
|
|
||||||
|
|
||||||
class String(Scalar):
|
class String(Scalar):
|
||||||
|
@ -143,16 +148,17 @@ class String(Scalar):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def coerce_string(value):
|
def coerce_string(value):
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
return u"true" if value else u"false"
|
return "true" if value else "false"
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
serialize = coerce_string
|
serialize = coerce_string
|
||||||
parse_value = coerce_string
|
parse_value = coerce_string
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(ast):
|
def parse_literal(ast, _variables=None):
|
||||||
if isinstance(ast, StringValueNode):
|
if isinstance(ast, StringValueNode):
|
||||||
return ast.value
|
return ast.value
|
||||||
|
return Undefined
|
||||||
|
|
||||||
|
|
||||||
class Boolean(Scalar):
|
class Boolean(Scalar):
|
||||||
|
@ -164,9 +170,10 @@ class Boolean(Scalar):
|
||||||
parse_value = bool
|
parse_value = bool
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(ast):
|
def parse_literal(ast, _variables=None):
|
||||||
if isinstance(ast, BooleanValueNode):
|
if isinstance(ast, BooleanValueNode):
|
||||||
return ast.value
|
return ast.value
|
||||||
|
return Undefined
|
||||||
|
|
||||||
|
|
||||||
class ID(Scalar):
|
class ID(Scalar):
|
||||||
|
@ -182,6 +189,7 @@ class ID(Scalar):
|
||||||
parse_value = str
|
parse_value = str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(ast):
|
def parse_literal(ast, _variables=None):
|
||||||
if isinstance(ast, (StringValueNode, IntValueNode)):
|
if isinstance(ast, (StringValueNode, IntValueNode)):
|
||||||
return ast.value
|
return ast.value
|
||||||
|
return Undefined
|
||||||
|
|
|
@ -233,11 +233,20 @@ class TypeMap(dict):
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def interfaces():
|
||||||
|
interfaces = []
|
||||||
|
for graphene_interface in graphene_type._meta.interfaces:
|
||||||
|
interface = self.add_type(graphene_interface)
|
||||||
|
assert interface.graphene_type == graphene_interface
|
||||||
|
interfaces.append(interface)
|
||||||
|
return interfaces
|
||||||
|
|
||||||
return GrapheneInterfaceType(
|
return GrapheneInterfaceType(
|
||||||
graphene_type=graphene_type,
|
graphene_type=graphene_type,
|
||||||
name=graphene_type._meta.name,
|
name=graphene_type._meta.name,
|
||||||
description=graphene_type._meta.description,
|
description=graphene_type._meta.description,
|
||||||
fields=partial(self.create_fields_for_type, graphene_type),
|
fields=partial(self.create_fields_for_type, graphene_type),
|
||||||
|
interfaces=interfaces,
|
||||||
resolve_type=resolve_type,
|
resolve_type=resolve_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -376,19 +385,11 @@ class TypeMap(dict):
|
||||||
def resolve_type(self, resolve_type_func, type_name, root, info, _type):
|
def resolve_type(self, resolve_type_func, type_name, root, info, _type):
|
||||||
type_ = resolve_type_func(root, info)
|
type_ = resolve_type_func(root, info)
|
||||||
|
|
||||||
if not type_:
|
|
||||||
return_type = self[type_name]
|
|
||||||
return default_type_resolver(root, info, return_type)
|
|
||||||
|
|
||||||
if inspect.isclass(type_) and issubclass(type_, ObjectType):
|
if inspect.isclass(type_) and issubclass(type_, ObjectType):
|
||||||
graphql_type = self.get(type_._meta.name)
|
return type_._meta.name
|
||||||
assert graphql_type, f"Can't find type {type_._meta.name} in schema"
|
|
||||||
assert (
|
|
||||||
graphql_type.graphene_type == type_
|
|
||||||
), f"The type {type_} does not match with the associated graphene type {graphql_type.graphene_type}."
|
|
||||||
return graphql_type
|
|
||||||
|
|
||||||
return type_
|
return_type = self[type_name]
|
||||||
|
return default_type_resolver(root, info, return_type)
|
||||||
|
|
||||||
|
|
||||||
class Schema:
|
class Schema:
|
||||||
|
|
|
@ -39,8 +39,25 @@ def test_bad_decimal_query():
|
||||||
not_a_decimal = "Nobody expects the Spanish Inquisition!"
|
not_a_decimal = "Nobody expects the Spanish Inquisition!"
|
||||||
|
|
||||||
result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal)
|
result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal)
|
||||||
|
assert result.errors
|
||||||
assert len(result.errors) == 1
|
assert len(result.errors) == 1
|
||||||
assert result.data is None
|
assert result.data is None
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "Expected value of type 'Decimal', found \"Nobody expects the Spanish Inquisition!\"."
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ decimal(input: true) }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.data is None
|
||||||
|
assert result.errors[0].message == "Expected value of type 'Decimal', found true."
|
||||||
|
|
||||||
|
result = schema.execute("{ decimal(input: 1.2) }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.data is None
|
||||||
|
assert result.errors[0].message == "Expected value of type 'Decimal', found 1.2."
|
||||||
|
|
||||||
|
|
||||||
def test_decimal_string_query_integer():
|
def test_decimal_string_query_integer():
|
||||||
|
|
|
@ -251,19 +251,22 @@ def test_enum_types():
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
assert str(schema) == dedent(
|
assert (
|
||||||
'''\
|
str(schema).strip()
|
||||||
type Query {
|
== dedent(
|
||||||
color: Color!
|
'''
|
||||||
}
|
type Query {
|
||||||
|
color: Color!
|
||||||
|
}
|
||||||
|
|
||||||
"""Primary colors"""
|
"""Primary colors"""
|
||||||
enum Color {
|
enum Color {
|
||||||
RED
|
RED
|
||||||
YELLOW
|
YELLOW
|
||||||
BLUE
|
BLUE
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
).strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -325,6 +328,52 @@ def test_enum_resolver_compat():
|
||||||
assert results.data["colorByName"] == Color.RED.name
|
assert results.data["colorByName"] == Color.RED.name
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_with_name():
|
||||||
|
from enum import Enum as PyEnum
|
||||||
|
|
||||||
|
class Color(PyEnum):
|
||||||
|
RED = 1
|
||||||
|
YELLOW = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
GColor = Enum.from_enum(Color, description="original colors")
|
||||||
|
UniqueGColor = Enum.from_enum(
|
||||||
|
Color, name="UniqueColor", description="unique colors"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
color = GColor(required=True)
|
||||||
|
unique_color = UniqueGColor(required=True)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
str(schema).strip()
|
||||||
|
== dedent(
|
||||||
|
'''
|
||||||
|
type Query {
|
||||||
|
color: Color!
|
||||||
|
uniqueColor: UniqueColor!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""original colors"""
|
||||||
|
enum Color {
|
||||||
|
RED
|
||||||
|
YELLOW
|
||||||
|
BLUE
|
||||||
|
}
|
||||||
|
|
||||||
|
"""unique colors"""
|
||||||
|
enum UniqueColor {
|
||||||
|
RED
|
||||||
|
YELLOW
|
||||||
|
BLUE
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
).strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_enum_resolver_invalid():
|
def test_enum_resolver_invalid():
|
||||||
from enum import Enum as PyEnum
|
from enum import Enum as PyEnum
|
||||||
|
|
||||||
|
@ -345,10 +394,7 @@ def test_enum_resolver_invalid():
|
||||||
|
|
||||||
results = schema.execute("query { color }")
|
results = schema.execute("query { color }")
|
||||||
assert results.errors
|
assert results.errors
|
||||||
assert (
|
assert results.errors[0].message == "Enum 'Color' cannot represent value: 'BLACK'"
|
||||||
results.errors[0].message
|
|
||||||
== "Expected a value of type 'Color' but received: 'BLACK'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_field_enum_argument():
|
def test_field_enum_argument():
|
||||||
|
@ -460,12 +506,13 @@ def test_mutation_enum_input_type():
|
||||||
|
|
||||||
schema = Schema(query=Query, mutation=MyMutation)
|
schema = Schema(query=Query, mutation=MyMutation)
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
""" mutation MyMutation {
|
"""
|
||||||
createPaint(colorInput: { color: RED }) {
|
mutation MyMutation {
|
||||||
color
|
createPaint(colorInput: { color: RED }) {
|
||||||
|
color
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
"""
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"createPaint": {"color": "RED"}}
|
assert result.data == {"createPaint": {"color": "RED"}}
|
||||||
|
|
|
@ -25,13 +25,18 @@ def test_generate_interface():
|
||||||
|
|
||||||
|
|
||||||
def test_generate_interface_with_meta():
|
def test_generate_interface_with_meta():
|
||||||
|
class MyFirstInterface(Interface):
|
||||||
|
pass
|
||||||
|
|
||||||
class MyInterface(Interface):
|
class MyInterface(Interface):
|
||||||
class Meta:
|
class Meta:
|
||||||
name = "MyOtherInterface"
|
name = "MyOtherInterface"
|
||||||
description = "Documentation"
|
description = "Documentation"
|
||||||
|
interfaces = [MyFirstInterface]
|
||||||
|
|
||||||
assert MyInterface._meta.name == "MyOtherInterface"
|
assert MyInterface._meta.name == "MyOtherInterface"
|
||||||
assert MyInterface._meta.description == "Documentation"
|
assert MyInterface._meta.description == "Documentation"
|
||||||
|
assert MyInterface._meta.interfaces == [MyFirstInterface]
|
||||||
|
|
||||||
|
|
||||||
def test_generate_interface_with_fields():
|
def test_generate_interface_with_fields():
|
||||||
|
|
|
@ -21,6 +21,10 @@ def test_jsonstring_query():
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"json": json_value}
|
assert result.data == {"json": json_value}
|
||||||
|
|
||||||
|
result = schema.execute("""{ json(input: "{}") }""")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"json": "{}"}
|
||||||
|
|
||||||
|
|
||||||
def test_jsonstring_query_variable():
|
def test_jsonstring_query_variable():
|
||||||
json_value = '{"key": "value"}'
|
json_value = '{"key": "value"}'
|
||||||
|
@ -31,3 +35,51 @@ def test_jsonstring_query_variable():
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"json": json_value}
|
assert result.data == {"json": json_value}
|
||||||
|
|
||||||
|
|
||||||
|
def test_jsonstring_optional_uuid_input():
|
||||||
|
"""
|
||||||
|
Test that we can provide a null value to an optional input
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ json(input: null) }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"json": None}
|
||||||
|
|
||||||
|
|
||||||
|
def test_jsonstring_invalid_query():
|
||||||
|
"""
|
||||||
|
Test that if an invalid type is provided we get an error
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ json(input: 1) }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Expected value of type 'JSONString', found 1."
|
||||||
|
|
||||||
|
result = schema.execute("{ json(input: {}) }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Expected value of type 'JSONString', found {}."
|
||||||
|
|
||||||
|
result = schema.execute('{ json(input: "a") }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == (
|
||||||
|
"Expected value of type 'JSONString', found \"a\"; "
|
||||||
|
"Badly formed JSONString: Expecting value: line 1 column 1 (char 0)"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("""{ json(input: "{\\'key\\': 0}") }""")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "Syntax Error: Invalid character escape sequence: '\\''."
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("""{ json(input: "{\\"key\\": 0,}") }""")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == (
|
||||||
|
'Expected value of type \'JSONString\', found "{\\"key\\": 0,}"; '
|
||||||
|
"Badly formed JSONString: Expecting property name enclosed in double quotes: line 1 column 11 (char 10)"
|
||||||
|
)
|
||||||
|
|
|
@ -191,21 +191,15 @@ def test_objecttype_as_container_all_kwargs():
|
||||||
|
|
||||||
|
|
||||||
def test_objecttype_as_container_extra_args():
|
def test_objecttype_as_container_extra_args():
|
||||||
with raises(TypeError) as excinfo:
|
msg = r"__init__\(\) takes from 1 to 3 positional arguments but 4 were given"
|
||||||
Container("1", "2", "3")
|
with raises(TypeError, match=msg):
|
||||||
|
Container("1", "2", "3") # type: ignore
|
||||||
assert "__init__() takes from 1 to 3 positional arguments but 4 were given" == str(
|
|
||||||
excinfo.value
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_objecttype_as_container_invalid_kwargs():
|
def test_objecttype_as_container_invalid_kwargs():
|
||||||
with raises(TypeError) as excinfo:
|
msg = r"__init__\(\) got an unexpected keyword argument 'unexisting_field'"
|
||||||
Container(unexisting_field="3")
|
with raises(TypeError, match=msg):
|
||||||
|
Container(unexisting_field="3") # type: ignore
|
||||||
assert "__init__() got an unexpected keyword argument 'unexisting_field'" == str(
|
|
||||||
excinfo.value
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_objecttype_container_benchmark(benchmark):
|
def test_objecttype_container_benchmark(benchmark):
|
||||||
|
|
|
@ -229,11 +229,11 @@ def test_query_arguments():
|
||||||
|
|
||||||
result = test_schema.execute("{ test }", None)
|
result = test_schema.execute("{ test }", None)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"test": '[null,{"a_str":null,"a_int":null}]'}
|
assert result.data == {"test": "[null,{}]"}
|
||||||
|
|
||||||
result = test_schema.execute('{ test(aStr: "String!") }', "Source!")
|
result = test_schema.execute('{ test(aStr: "String!") }', "Source!")
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"test": '["Source!",{"a_str":"String!","a_int":null}]'}
|
assert result.data == {"test": '["Source!",{"a_str":"String!"}]'}
|
||||||
|
|
||||||
result = test_schema.execute('{ test(aInt: -123, aStr: "String!") }', "Source!")
|
result = test_schema.execute('{ test(aInt: -123, aStr: "String!") }', "Source!")
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
|
@ -258,7 +258,7 @@ def test_query_input_field():
|
||||||
|
|
||||||
result = test_schema.execute("{ test }", None)
|
result = test_schema.execute("{ test }", None)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"test": '[null,{"a_input":null}]'}
|
assert result.data == {"test": "[null,{}]"}
|
||||||
|
|
||||||
result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', "Source!")
|
result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', "Source!")
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
from ..scalars import Scalar, Int, BigInt
|
from ..objecttype import ObjectType, Field
|
||||||
|
from ..scalars import Scalar, Int, BigInt, Float, String, Boolean
|
||||||
|
from ..schema import Schema
|
||||||
|
from graphql import Undefined
|
||||||
from graphql.language.ast import IntValueNode
|
from graphql.language.ast import IntValueNode
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,19 +14,295 @@ def test_scalar():
|
||||||
|
|
||||||
|
|
||||||
def test_ints():
|
def test_ints():
|
||||||
assert Int.parse_value(2 ** 31 - 1) is not None
|
assert Int.parse_value(2**31 - 1) is not Undefined
|
||||||
assert Int.parse_value("2.0") is not None
|
assert Int.parse_value("2.0") == 2
|
||||||
assert Int.parse_value(2 ** 31) is None
|
assert Int.parse_value(2**31) is Undefined
|
||||||
|
|
||||||
assert Int.parse_literal(IntValueNode(value=str(2 ** 31 - 1))) == 2 ** 31 - 1
|
assert Int.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1
|
||||||
assert Int.parse_literal(IntValueNode(value=str(2 ** 31))) is None
|
assert Int.parse_literal(IntValueNode(value=str(2**31))) is Undefined
|
||||||
|
|
||||||
assert Int.parse_value(-(2 ** 31)) is not None
|
assert Int.parse_value(-(2**31)) is not Undefined
|
||||||
assert Int.parse_value(-(2 ** 31) - 1) is None
|
assert Int.parse_value(-(2**31) - 1) is Undefined
|
||||||
|
|
||||||
assert BigInt.parse_value(2 ** 31) is not None
|
assert BigInt.parse_value(2**31) is not Undefined
|
||||||
assert BigInt.parse_value("2.0") is not None
|
assert BigInt.parse_value("2.0") == 2
|
||||||
assert BigInt.parse_value(-(2 ** 31) - 1) is not None
|
assert BigInt.parse_value(-(2**31) - 1) is not Undefined
|
||||||
|
|
||||||
assert BigInt.parse_literal(IntValueNode(value=str(2 ** 31 - 1))) == 2 ** 31 - 1
|
assert BigInt.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1
|
||||||
assert BigInt.parse_literal(IntValueNode(value=str(2 ** 31))) == 2 ** 31
|
assert BigInt.parse_literal(IntValueNode(value=str(2**31))) == 2**31
|
||||||
|
|
||||||
|
|
||||||
|
def return_input(_parent, _info, input):
|
||||||
|
return input
|
||||||
|
|
||||||
|
|
||||||
|
class Optional(ObjectType):
|
||||||
|
int = Int(input=Int(), resolver=return_input)
|
||||||
|
big_int = BigInt(input=BigInt(), resolver=return_input)
|
||||||
|
float = Float(input=Float(), resolver=return_input)
|
||||||
|
bool = Boolean(input=Boolean(), resolver=return_input)
|
||||||
|
string = String(input=String(), resolver=return_input)
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
optional = Field(Optional)
|
||||||
|
|
||||||
|
def resolve_optional(self, info):
|
||||||
|
return Optional()
|
||||||
|
|
||||||
|
def resolve_required(self, info, input):
|
||||||
|
return input
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInt:
|
||||||
|
def test_query(self):
|
||||||
|
"""
|
||||||
|
Test that a normal query works.
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { int(input: 20) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"int": 20}}
|
||||||
|
|
||||||
|
def test_optional_input(self):
|
||||||
|
"""
|
||||||
|
Test that we can provide a null value to an optional input
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { int(input: null) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"int": None}}
|
||||||
|
|
||||||
|
def test_invalid_input(self):
|
||||||
|
"""
|
||||||
|
Test that if an invalid type is provided we get an error
|
||||||
|
"""
|
||||||
|
result = schema.execute('{ optional { int(input: "20") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == 'Int cannot represent non-integer value: "20"'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute('{ optional { int(input: "a") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == 'Int cannot represent non-integer value: "a"'
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { int(input: true) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == "Int cannot represent non-integer value: true"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBigInt:
|
||||||
|
def test_query(self):
|
||||||
|
"""
|
||||||
|
Test that a normal query works.
|
||||||
|
"""
|
||||||
|
value = 2**31
|
||||||
|
result = schema.execute("{ optional { bigInt(input: %s) } }" % value)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"bigInt": value}}
|
||||||
|
|
||||||
|
def test_optional_input(self):
|
||||||
|
"""
|
||||||
|
Test that we can provide a null value to an optional input
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { bigInt(input: null) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"bigInt": None}}
|
||||||
|
|
||||||
|
def test_invalid_input(self):
|
||||||
|
"""
|
||||||
|
Test that if an invalid type is provided we get an error
|
||||||
|
"""
|
||||||
|
result = schema.execute('{ optional { bigInt(input: "20") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == "Expected value of type 'BigInt', found \"20\"."
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute('{ optional { bigInt(input: "a") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == "Expected value of type 'BigInt', found \"a\"."
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { bigInt(input: true) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == "Expected value of type 'BigInt', found true."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFloat:
|
||||||
|
def test_query(self):
|
||||||
|
"""
|
||||||
|
Test that a normal query works.
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { float(input: 20) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"float": 20.0}}
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { float(input: 20.2) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"float": 20.2}}
|
||||||
|
|
||||||
|
def test_optional_input(self):
|
||||||
|
"""
|
||||||
|
Test that we can provide a null value to an optional input
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { float(input: null) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"float": None}}
|
||||||
|
|
||||||
|
def test_invalid_input(self):
|
||||||
|
"""
|
||||||
|
Test that if an invalid type is provided we get an error
|
||||||
|
"""
|
||||||
|
result = schema.execute('{ optional { float(input: "20") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == 'Float cannot represent non numeric value: "20"'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute('{ optional { float(input: "a") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == 'Float cannot represent non numeric value: "a"'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { float(input: true) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == "Float cannot represent non numeric value: true"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBoolean:
|
||||||
|
def test_query(self):
|
||||||
|
"""
|
||||||
|
Test that a normal query works.
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { bool(input: true) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"bool": True}}
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { bool(input: false) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"bool": False}}
|
||||||
|
|
||||||
|
def test_optional_input(self):
|
||||||
|
"""
|
||||||
|
Test that we can provide a null value to an optional input
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { bool(input: null) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"bool": None}}
|
||||||
|
|
||||||
|
def test_invalid_input(self):
|
||||||
|
"""
|
||||||
|
Test that if an invalid type is provided we get an error
|
||||||
|
"""
|
||||||
|
result = schema.execute('{ optional { bool(input: "True") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== 'Boolean cannot represent a non boolean value: "True"'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute('{ optional { bool(input: "true") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== 'Boolean cannot represent a non boolean value: "true"'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute('{ optional { bool(input: "a") } }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== 'Boolean cannot represent a non boolean value: "a"'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { bool(input: 1) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "Boolean cannot represent a non boolean value: 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { bool(input: 0) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "Boolean cannot represent a non boolean value: 0"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestString:
|
||||||
|
def test_query(self):
|
||||||
|
"""
|
||||||
|
Test that a normal query works.
|
||||||
|
"""
|
||||||
|
result = schema.execute('{ optional { string(input: "something something") } }')
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"string": "something something"}}
|
||||||
|
|
||||||
|
result = schema.execute('{ optional { string(input: "True") } }')
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"string": "True"}}
|
||||||
|
|
||||||
|
result = schema.execute('{ optional { string(input: "0") } }')
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"string": "0"}}
|
||||||
|
|
||||||
|
def test_optional_input(self):
|
||||||
|
"""
|
||||||
|
Test that we can provide a null value to an optional input
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { string(input: null) } }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"optional": {"string": None}}
|
||||||
|
|
||||||
|
def test_invalid_input(self):
|
||||||
|
"""
|
||||||
|
Test that if an invalid type is provided we get an error
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ optional { string(input: 1) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message == "String cannot represent a non string value: 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { string(input: 3.2) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "String cannot represent a non string value: 3.2"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ optional { string(input: true) } }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "String cannot represent a non string value: true"
|
||||||
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from graphql import Undefined
|
||||||
from ..scalars import Boolean, Float, Int, String
|
from ..scalars import Boolean, Float, Int, String
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,12 +10,12 @@ def test_serializes_output_int():
|
||||||
assert Int.serialize(1.1) == 1
|
assert Int.serialize(1.1) == 1
|
||||||
assert Int.serialize(-1.1) == -1
|
assert Int.serialize(-1.1) == -1
|
||||||
assert Int.serialize(1e5) == 100000
|
assert Int.serialize(1e5) == 100000
|
||||||
assert Int.serialize(9876504321) is None
|
assert Int.serialize(9876504321) is Undefined
|
||||||
assert Int.serialize(-9876504321) is None
|
assert Int.serialize(-9876504321) is Undefined
|
||||||
assert Int.serialize(1e100) is None
|
assert Int.serialize(1e100) is Undefined
|
||||||
assert Int.serialize(-1e100) is None
|
assert Int.serialize(-1e100) is Undefined
|
||||||
assert Int.serialize("-1.1") == -1
|
assert Int.serialize("-1.1") == -1
|
||||||
assert Int.serialize("one") is None
|
assert Int.serialize("one") is Undefined
|
||||||
assert Int.serialize(False) == 0
|
assert Int.serialize(False) == 0
|
||||||
assert Int.serialize(True) == 1
|
assert Int.serialize(True) == 1
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ def test_serializes_output_float():
|
||||||
assert Float.serialize(1.1) == 1.1
|
assert Float.serialize(1.1) == 1.1
|
||||||
assert Float.serialize(-1.1) == -1.1
|
assert Float.serialize(-1.1) == -1.1
|
||||||
assert Float.serialize("-1.1") == -1.1
|
assert Float.serialize("-1.1") == -1.1
|
||||||
assert Float.serialize("one") is None
|
assert Float.serialize("one") is Undefined
|
||||||
assert Float.serialize(False) == 0
|
assert Float.serialize(False) == 0
|
||||||
assert Float.serialize(True) == 1
|
assert Float.serialize(True) == 1
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ def test_serializes_output_string():
|
||||||
assert String.serialize(-1.1) == "-1.1"
|
assert String.serialize(-1.1) == "-1.1"
|
||||||
assert String.serialize(True) == "true"
|
assert String.serialize(True) == "true"
|
||||||
assert String.serialize(False) == "false"
|
assert String.serialize(False) == "false"
|
||||||
assert String.serialize(u"\U0001F601") == u"\U0001F601"
|
assert String.serialize("\U0001F601") == "\U0001F601"
|
||||||
|
|
||||||
|
|
||||||
def test_serializes_output_boolean():
|
def test_serializes_output_boolean():
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from graphql.type import GraphQLObjectType, GraphQLSchema
|
from textwrap import dedent
|
||||||
|
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
from graphene.tests.utils import dedent
|
from graphql.type import GraphQLObjectType, GraphQLSchema
|
||||||
|
|
||||||
from ..field import Field
|
from ..field import Field
|
||||||
from ..objecttype import ObjectType
|
from ..objecttype import ObjectType
|
||||||
|
@ -43,8 +44,10 @@ def test_schema_get_type_error():
|
||||||
|
|
||||||
def test_schema_str():
|
def test_schema_str():
|
||||||
schema = Schema(Query)
|
schema = Schema(Query)
|
||||||
assert str(schema) == dedent(
|
assert (
|
||||||
"""
|
str(schema).strip()
|
||||||
|
== dedent(
|
||||||
|
"""
|
||||||
type Query {
|
type Query {
|
||||||
inner: MyOtherType
|
inner: MyOtherType
|
||||||
}
|
}
|
||||||
|
@ -53,6 +56,7 @@ def test_schema_str():
|
||||||
field: String
|
field: String
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
).strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,7 @@ class Subscription(ObjectType):
|
||||||
count_to_ten = Field(Int)
|
count_to_ten = Field(Int)
|
||||||
|
|
||||||
async def subscribe_count_to_ten(root, info):
|
async def subscribe_count_to_ten(root, info):
|
||||||
count = 0
|
for count in range(1, 11):
|
||||||
while count < 10:
|
|
||||||
count += 1
|
|
||||||
yield count
|
yield count
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from graphql import Undefined
|
||||||
from graphql.type import (
|
from graphql.type import (
|
||||||
GraphQLArgument,
|
GraphQLArgument,
|
||||||
GraphQLEnumType,
|
GraphQLEnumType,
|
||||||
|
@ -244,7 +245,9 @@ def test_objecttype_camelcase():
|
||||||
foo_field = fields["fooBar"]
|
foo_field = fields["fooBar"]
|
||||||
assert isinstance(foo_field, GraphQLField)
|
assert isinstance(foo_field, GraphQLField)
|
||||||
assert foo_field.args == {
|
assert foo_field.args == {
|
||||||
"barFoo": GraphQLArgument(GraphQLString, default_value=None, out_name="bar_foo")
|
"barFoo": GraphQLArgument(
|
||||||
|
GraphQLString, default_value=Undefined, out_name="bar_foo"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -267,7 +270,7 @@ def test_objecttype_camelcase_disabled():
|
||||||
assert isinstance(foo_field, GraphQLField)
|
assert isinstance(foo_field, GraphQLField)
|
||||||
assert foo_field.args == {
|
assert foo_field.args == {
|
||||||
"bar_foo": GraphQLArgument(
|
"bar_foo": GraphQLArgument(
|
||||||
GraphQLString, default_value=None, out_name="bar_foo"
|
GraphQLString, default_value=Undefined, out_name="bar_foo"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,3 +289,33 @@ def test_objecttype_with_possible_types():
|
||||||
assert graphql_type.is_type_of
|
assert graphql_type.is_type_of
|
||||||
assert graphql_type.is_type_of({}, None) is True
|
assert graphql_type.is_type_of({}, None) is True
|
||||||
assert graphql_type.is_type_of(MyObjectType(), None) is False
|
assert graphql_type.is_type_of(MyObjectType(), None) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_with_interfaces():
|
||||||
|
class FooInterface(Interface):
|
||||||
|
foo = String()
|
||||||
|
|
||||||
|
class BarInterface(Interface):
|
||||||
|
class Meta:
|
||||||
|
interfaces = [FooInterface]
|
||||||
|
|
||||||
|
foo = String()
|
||||||
|
bar = String()
|
||||||
|
|
||||||
|
type_map = create_type_map([FooInterface, BarInterface])
|
||||||
|
assert "FooInterface" in type_map
|
||||||
|
foo_graphql_type = type_map["FooInterface"]
|
||||||
|
assert isinstance(foo_graphql_type, GraphQLInterfaceType)
|
||||||
|
assert foo_graphql_type.name == "FooInterface"
|
||||||
|
|
||||||
|
assert "BarInterface" in type_map
|
||||||
|
bar_graphql_type = type_map["BarInterface"]
|
||||||
|
assert isinstance(bar_graphql_type, GraphQLInterfaceType)
|
||||||
|
assert bar_graphql_type.name == "BarInterface"
|
||||||
|
|
||||||
|
fields = bar_graphql_type.fields
|
||||||
|
assert list(fields) == ["foo", "bar"]
|
||||||
|
assert isinstance(fields["foo"], GraphQLField)
|
||||||
|
assert isinstance(fields["bar"], GraphQLField)
|
||||||
|
|
||||||
|
assert list(bar_graphql_type.interfaces) == list([foo_graphql_type])
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
from ..objecttype import ObjectType
|
from ..objecttype import ObjectType
|
||||||
from ..schema import Schema
|
from ..schema import Schema
|
||||||
from ..uuid import UUID
|
from ..uuid import UUID
|
||||||
|
from ..structures import NonNull
|
||||||
|
|
||||||
|
|
||||||
class Query(ObjectType):
|
class Query(ObjectType):
|
||||||
uuid = UUID(input=UUID())
|
uuid = UUID(input=UUID())
|
||||||
|
required_uuid = UUID(input=NonNull(UUID), required=True)
|
||||||
|
|
||||||
def resolve_uuid(self, info, input):
|
def resolve_uuid(self, info, input):
|
||||||
return input
|
return input
|
||||||
|
|
||||||
|
def resolve_required_uuid(self, info, input):
|
||||||
|
return input
|
||||||
|
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
@ -29,3 +34,35 @@ def test_uuidstring_query_variable():
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"uuid": uuid_value}
|
assert result.data == {"uuid": uuid_value}
|
||||||
|
|
||||||
|
|
||||||
|
def test_uuidstring_optional_uuid_input():
|
||||||
|
"""
|
||||||
|
Test that we can provide a null value to an optional input
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ uuid(input: null) }")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"uuid": None}
|
||||||
|
|
||||||
|
|
||||||
|
def test_uuidstring_invalid_query():
|
||||||
|
"""
|
||||||
|
Test that if an invalid type is provided we get an error
|
||||||
|
"""
|
||||||
|
result = schema.execute("{ uuid(input: 1) }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Expected value of type 'UUID', found 1."
|
||||||
|
|
||||||
|
result = schema.execute('{ uuid(input: "a") }')
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert (
|
||||||
|
result.errors[0].message
|
||||||
|
== "Expected value of type 'UUID', found \"a\"; badly formed hexadecimal UUID string"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute("{ requiredUuid(input: null) }")
|
||||||
|
assert result.errors
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Expected value of type 'UUID!', found null."
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Union(UnmountedType, BaseType):
|
||||||
to determine which type is actually used when the field is resolved.
|
to determine which type is actually used when the field is resolved.
|
||||||
|
|
||||||
The schema in this example can take a search text and return any of the GraphQL object types
|
The schema in this example can take a search text and return any of the GraphQL object types
|
||||||
indicated: Human, Droid or Startship.
|
indicated: Human, Droid or Starship.
|
||||||
|
|
||||||
Ambiguous return types can be resolved on each ObjectType through ``Meta.possible_types``
|
Ambiguous return types can be resolved on each ObjectType through ``Meta.possible_types``
|
||||||
attribute or ``is_type_of`` method. Or by implementing ``resolve_type`` class method on the
|
attribute or ``is_type_of`` method. Or by implementing ``resolve_type`` class method on the
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
||||||
from uuid import UUID as _UUID
|
from uuid import UUID as _UUID
|
||||||
|
|
||||||
from graphql.language.ast import StringValueNode
|
from graphql.language.ast import StringValueNode
|
||||||
|
from graphql import Undefined
|
||||||
|
|
||||||
from .scalars import Scalar
|
from .scalars import Scalar
|
||||||
|
|
||||||
|
@ -21,9 +22,10 @@ class UUID(Scalar):
|
||||||
return str(uuid)
|
return str(uuid)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_literal(node):
|
def parse_literal(node, _variables=None):
|
||||||
if isinstance(node, StringValueNode):
|
if isinstance(node, StringValueNode):
|
||||||
return _UUID(node.value)
|
return _UUID(node.value)
|
||||||
|
return Undefined
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_value(value):
|
def parse_value(value):
|
||||||
|
|
|
@ -27,19 +27,18 @@ def import_string(dotted_path, dotted_attributes=None):
|
||||||
|
|
||||||
if not dotted_attributes:
|
if not dotted_attributes:
|
||||||
return result
|
return result
|
||||||
else:
|
attributes = dotted_attributes.split(".")
|
||||||
attributes = dotted_attributes.split(".")
|
traveled_attributes = []
|
||||||
traveled_attributes = []
|
try:
|
||||||
try:
|
for attribute in attributes:
|
||||||
for attribute in attributes:
|
traveled_attributes.append(attribute)
|
||||||
traveled_attributes.append(attribute)
|
result = getattr(result, attribute)
|
||||||
result = getattr(result, attribute)
|
return result
|
||||||
return result
|
except AttributeError:
|
||||||
except AttributeError:
|
raise ImportError(
|
||||||
raise ImportError(
|
'Module "%s" does not define a "%s" attribute inside attribute/class "%s"'
|
||||||
'Module "%s" does not define a "%s" attribute inside attribute/class "%s"'
|
% (module_path, ".".join(traveled_attributes), class_name)
|
||||||
% (module_path, ".".join(traveled_attributes), class_name)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def lazy_import(dotted_path, dotted_attributes=None):
|
def lazy_import(dotted_path, dotted_attributes=None):
|
||||||
|
|
|
@ -94,6 +94,7 @@ TEST_DATA = {
|
||||||
],
|
],
|
||||||
"movies": {
|
"movies": {
|
||||||
"1198359": {
|
"1198359": {
|
||||||
|
"id": "1198359",
|
||||||
"name": "King Arthur: Legend of the Sword",
|
"name": "King Arthur: Legend of the Sword",
|
||||||
"synopsis": (
|
"synopsis": (
|
||||||
"When the child Arthur's father is murdered, Vortigern, "
|
"When the child Arthur's father is murdered, Vortigern, "
|
||||||
|
@ -159,7 +160,7 @@ def test_example_end_to_end():
|
||||||
"date": "2017-05-19",
|
"date": "2017-05-19",
|
||||||
"movie": {
|
"movie": {
|
||||||
"__typename": "Movie",
|
"__typename": "Movie",
|
||||||
"id": "TW92aWU6Tm9uZQ==",
|
"id": "TW92aWU6MTE5ODM1OQ==",
|
||||||
"name": "King Arthur: Legend of the Sword",
|
"name": "King Arthur: Legend of the Sword",
|
||||||
"synopsis": (
|
"synopsis": (
|
||||||
"When the child Arthur's father is murdered, Vortigern, "
|
"When the child Arthur's father is murdered, Vortigern, "
|
||||||
|
@ -172,7 +173,7 @@ def test_example_end_to_end():
|
||||||
"__typename": "Event",
|
"__typename": "Event",
|
||||||
"id": "RXZlbnQ6MjM0",
|
"id": "RXZlbnQ6MjM0",
|
||||||
"date": "2017-05-20",
|
"date": "2017-05-20",
|
||||||
"movie": {"__typename": "Movie", "id": "TW92aWU6Tm9uZQ=="},
|
"movie": {"__typename": "Movie", "id": "TW92aWU6MTE5ODM1OQ=="},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,4 @@ def test_orderedtype_non_orderabletypes():
|
||||||
|
|
||||||
assert one.__lt__(1) == NotImplemented
|
assert one.__lt__(1) == NotImplemented
|
||||||
assert one.__gt__(1) == NotImplemented
|
assert one.__gt__(1) == NotImplemented
|
||||||
assert not one == 1
|
assert one != 1
|
||||||
|
|
|
@ -18,14 +18,12 @@ schema = Schema(query=Query)
|
||||||
def run_query(query: str):
|
def run_query(query: str):
|
||||||
document = parse(query)
|
document = parse(query)
|
||||||
|
|
||||||
errors = validate(
|
return validate(
|
||||||
schema=schema.graphql_schema,
|
schema=schema.graphql_schema,
|
||||||
document_ast=document,
|
document_ast=document,
|
||||||
rules=(DisableIntrospection,),
|
rules=(DisableIntrospection,),
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def test_disallows_introspection_queries():
|
def test_disallows_introspection_queries():
|
||||||
errors = run_query("{ __schema { queryType { name } } }")
|
errors = run_query("{ __schema { queryType { name } } }")
|
||||||
|
|
29
setup.py
29
setup.py
|
@ -45,20 +45,21 @@ class PyTest(TestCommand):
|
||||||
|
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"pytest>=5.3,<6",
|
"pytest>=6,<7",
|
||||||
"pytest-benchmark>=3.2,<4",
|
"pytest-benchmark>=3.4,<4",
|
||||||
"pytest-cov>=2.8,<3",
|
"pytest-cov>=3,<4",
|
||||||
"pytest-mock>=2,<3",
|
"pytest-mock>=3,<4",
|
||||||
"pytest-asyncio>=0.10,<2",
|
"pytest-asyncio>=0.16,<2",
|
||||||
"snapshottest>=0.5,<1",
|
"snapshottest>=0.6,<1",
|
||||||
"coveralls>=1.11,<2",
|
"coveralls>=3.3,<4",
|
||||||
"promise>=2.3,<3",
|
"promise>=2.3,<3",
|
||||||
"mock>=4.0,<5",
|
"aiodataloader<1",
|
||||||
"pytz==2021.1",
|
"mock>=4,<5",
|
||||||
"iso8601>=0.1,<2",
|
"pytz==2022.1",
|
||||||
|
"iso8601>=1,<2",
|
||||||
]
|
]
|
||||||
|
|
||||||
dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require
|
dev_requires = ["black==22.3.0", "flake8>=4,<5"] + tests_require
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="graphene",
|
name="graphene",
|
||||||
|
@ -78,12 +79,14 @@ setup(
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
],
|
],
|
||||||
keywords="api graphql protocol rest relay graphene",
|
keywords="api graphql protocol rest relay graphene",
|
||||||
packages=find_packages(exclude=["examples*"]),
|
packages=find_packages(exclude=["examples*"]),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"graphql-core>=3.1.2,<4",
|
"graphql-core>=3.1,<3.3",
|
||||||
"graphql-relay>=3.0,<4",
|
"graphql-relay>=3.1,<3.3",
|
||||||
"aniso8601>=8,<10",
|
"aniso8601>=8,<10",
|
||||||
],
|
],
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
|
|
79
tests_asyncio/test_dataloader.py
Normal file
79
tests_asyncio/test_dataloader.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from collections import namedtuple
|
||||||
|
from unittest.mock import Mock
|
||||||
|
from pytest import mark
|
||||||
|
from aiodataloader import DataLoader
|
||||||
|
|
||||||
|
from graphene import ObjectType, String, Schema, Field, List
|
||||||
|
|
||||||
|
|
||||||
|
CHARACTERS = {
|
||||||
|
"1": {"name": "Luke Skywalker", "sibling": "3"},
|
||||||
|
"2": {"name": "Darth Vader", "sibling": None},
|
||||||
|
"3": {"name": "Leia Organa", "sibling": "1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get_character = Mock(side_effect=lambda character_id: CHARACTERS[character_id])
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterType(ObjectType):
|
||||||
|
name = String()
|
||||||
|
sibling = Field(lambda: CharacterType)
|
||||||
|
|
||||||
|
async def resolve_sibling(character, info):
|
||||||
|
if character["sibling"]:
|
||||||
|
return await info.context.character_loader.load(character["sibling"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
skywalker_family = List(CharacterType)
|
||||||
|
|
||||||
|
async def resolve_skywalker_family(_, info):
|
||||||
|
return await info.context.character_loader.load_many(["1", "2", "3"])
|
||||||
|
|
||||||
|
|
||||||
|
mock_batch_load_fn = Mock(
|
||||||
|
side_effect=lambda character_ids: [get_character(id) for id in character_ids]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterLoader(DataLoader):
|
||||||
|
async def batch_load_fn(self, character_ids):
|
||||||
|
return mock_batch_load_fn(character_ids)
|
||||||
|
|
||||||
|
|
||||||
|
Context = namedtuple("Context", "character_loader")
|
||||||
|
|
||||||
|
|
||||||
|
@mark.asyncio
|
||||||
|
async def test_basic_dataloader():
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
character_loader = CharacterLoader()
|
||||||
|
context = Context(character_loader=character_loader)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
{
|
||||||
|
skywalkerFamily {
|
||||||
|
name
|
||||||
|
sibling {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = await schema.execute_async(query, context=context)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {
|
||||||
|
"skywalkerFamily": [
|
||||||
|
{"name": "Luke Skywalker", "sibling": {"name": "Leia Organa"}},
|
||||||
|
{"name": "Darth Vader", "sibling": None},
|
||||||
|
{"name": "Leia Organa", "sibling": {"name": "Luke Skywalker"}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert mock_batch_load_fn.call_count == 1
|
||||||
|
assert get_character.call_count == 3
|
16
tox.ini
16
tox.ini
|
@ -1,5 +1,5 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = flake8,py36,py37,py38,pre-commit,mypy
|
envlist = py3{6,7,8,9,10}, flake8, mypy, pre-commit
|
||||||
skipsdist = true
|
skipsdist = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
@ -8,28 +8,28 @@ deps =
|
||||||
setenv =
|
setenv =
|
||||||
PYTHONPATH = .:{envdir}
|
PYTHONPATH = .:{envdir}
|
||||||
commands =
|
commands =
|
||||||
py{36,37,38}: pytest --cov=graphene graphene examples {posargs}
|
py{36,37,38,39,310}: pytest --cov=graphene graphene --cov-report=term --cov-report=xml examples {posargs}
|
||||||
|
|
||||||
[testenv:pre-commit]
|
[testenv:pre-commit]
|
||||||
basepython=python3.8
|
basepython = python3.9
|
||||||
deps =
|
deps =
|
||||||
pre-commit>=2,<3
|
pre-commit>=2.16,<3
|
||||||
setenv =
|
setenv =
|
||||||
LC_CTYPE=en_US.UTF-8
|
LC_CTYPE=en_US.UTF-8
|
||||||
commands =
|
commands =
|
||||||
pre-commit run --all-files --show-diff-on-failure
|
pre-commit run --all-files --show-diff-on-failure
|
||||||
|
|
||||||
[testenv:mypy]
|
[testenv:mypy]
|
||||||
basepython=python3.8
|
basepython = python3.9
|
||||||
deps =
|
deps =
|
||||||
mypy>=0.761,<1
|
mypy>=0.950,<1
|
||||||
commands =
|
commands =
|
||||||
mypy graphene
|
mypy graphene
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
basepython=python3.8
|
basepython = python3.9
|
||||||
deps =
|
deps =
|
||||||
flake8>=3.8,<4
|
flake8>=4,<5
|
||||||
commands =
|
commands =
|
||||||
pip install --pre -e .
|
pip install --pre -e .
|
||||||
flake8 graphene
|
flake8 graphene
|
||||||
|
|
Loading…
Reference in New Issue
Block a user