Merge branch 'master' into fix/ariadne-link

This commit is contained in:
Erik Wrede 2022-08-20 14:59:02 +02:00 committed by GitHub
commit d96ec55abb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 966 additions and 344 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.",

View File

@ -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)

View File

@ -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>`_.

View File

@ -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

View File

@ -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

View File

@ -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
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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

View File

@ -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::

View File

@ -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::

View File

@ -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")

View File

@ -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

View File

@ -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
------------- -------------

View File

@ -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
} }'''
'''

View File

@ -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):

View File

@ -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)

View File

@ -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])

View File

@ -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)

View File

@ -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()
) )

View File

@ -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()
) )

View File

@ -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)}

View 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."
)

View 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

View File

@ -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"))

View File

@ -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,

View File

@ -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)}"

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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():

View File

@ -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"}}

View File

@ -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():

View File

@ -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)"
)

View File

@ -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):

View File

@ -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

View File

@ -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"
)

View File

@ -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():

View File

@ -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()
) )

View File

@ -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

View File

@ -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])

View File

@ -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."

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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=="},
}, },
] ]
} }

View File

@ -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

View File

@ -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 } } }")

View File

@ -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,

View 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
View File

@ -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