mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-22 01:26:49 +03:00
Merge branch 'master' into update-dataloaderdocs
This commit is contained in:
commit
8e1c3d3102
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: "\U0001F41B bug"
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note: for support questions, please use stackoverflow**. This repository's issues are reserved for feature requests and bug reports.
|
||||||
|
|
||||||
|
* **What is the current behavior?**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via
|
||||||
|
a github repo, https://repl.it or similar.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **What is the expected behavior?**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **What is the motivation / use case for changing the behavior?**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **Please tell us about your environment:**
|
||||||
|
|
||||||
|
- Version:
|
||||||
|
- Platform:
|
||||||
|
|
||||||
|
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow)
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: false
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: "✨ enhancement"
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
13
.github/stale.yml
vendored
13
.github/stale.yml
vendored
|
@ -1,7 +1,7 @@
|
||||||
# Number of days of inactivity before an issue becomes stale
|
# Number of days of inactivity before an issue becomes stale
|
||||||
daysUntilStale: 90
|
daysUntilStale: false
|
||||||
# Number of days of inactivity before a stale issue is closed
|
# Number of days of inactivity before a stale issue is closed
|
||||||
daysUntilClose: 14
|
daysUntilClose: false
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- pinned
|
- pinned
|
||||||
|
@ -15,9 +15,10 @@ exemptLabels:
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: wontfix
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: false
|
||||||
This issue has been automatically marked as stale because it has not had
|
# markComment: >
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
# This issue has been automatically marked as stale because it has not had
|
||||||
for your contributions.
|
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
# for your contributions.
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: false
|
closeComment: false
|
||||||
|
|
25
.github/workflows/coveralls.yml
vendored
Normal file
25
.github/workflows/coveralls.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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
|
26
.github/workflows/deploy.yml
vendored
Normal file
26
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: 🚀 Deploy to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Build wheel and source tarball
|
||||||
|
run: |
|
||||||
|
pip install wheel
|
||||||
|
python setup.py sdist bdist_wheel
|
||||||
|
- name: Publish a Python distribution to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@v1.1.0
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.pypi_password }}
|
26
.github/workflows/lint.yml
vendored
Normal file
26
.github/workflows/lint.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: 💅 Lint
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install tox
|
||||||
|
- name: Run lint
|
||||||
|
run: tox
|
||||||
|
env:
|
||||||
|
TOXENV: pre-commit
|
||||||
|
- name: Run mypy
|
||||||
|
run: tox
|
||||||
|
env:
|
||||||
|
TOXENV: mypy
|
66
.github/workflows/tests.yml
vendored
Normal file
66
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
name: 📄 Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- '*.x'
|
||||||
|
paths-ignore:
|
||||||
|
- 'docs/**'
|
||||||
|
- '*.md'
|
||||||
|
- '*.rst'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- '*.x'
|
||||||
|
paths-ignore:
|
||||||
|
- 'docs/**'
|
||||||
|
- '*.md'
|
||||||
|
- '*.rst'
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
# runs the test suite
|
||||||
|
name: ${{ matrix.name }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
|
||||||
|
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
|
||||||
|
- { 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:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
|
- name: update pip
|
||||||
|
run: |
|
||||||
|
pip install -U wheel
|
||||||
|
pip install -U setuptools
|
||||||
|
python -m pip install -U pip
|
||||||
|
|
||||||
|
- name: get pip cache dir
|
||||||
|
id: pip-cache
|
||||||
|
run: echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
|
||||||
|
- name: cache pip dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pip-cache.outputs.dir }}
|
||||||
|
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}
|
||||||
|
|
||||||
|
- run: pip install 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,6 +1,9 @@
|
||||||
|
default_language_version:
|
||||||
|
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.1.0
|
rev: v4.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-json
|
- id: check-json
|
||||||
|
@ -14,15 +17,14 @@ repos:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude: README.md
|
exclude: README.md
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v1.12.0
|
rev: v2.32.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
- repo: https://github.com/ambv/black
|
- repo: https://github.com/ambv/black
|
||||||
rev: 19.10b0
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 3.7.8
|
rev: 4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
|
|
42
.travis.yml
42
.travis.yml
|
@ -1,42 +0,0 @@
|
||||||
language: python
|
|
||||||
dist: xenial
|
|
||||||
|
|
||||||
python:
|
|
||||||
- "3.6"
|
|
||||||
- "3.7"
|
|
||||||
- "3.8"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install tox tox-travis
|
|
||||||
script: tox
|
|
||||||
after_success:
|
|
||||||
- pip install coveralls
|
|
||||||
- coveralls
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/.cache/pip
|
|
||||||
- $HOME/.cache/pre-commit
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- test
|
|
||||||
- name: deploy
|
|
||||||
if: tag IS present
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fast_finish: true
|
|
||||||
include:
|
|
||||||
- env: TOXENV=pre-commit
|
|
||||||
python: 3.7
|
|
||||||
- env: TOXENV=mypy
|
|
||||||
python: 3.7
|
|
||||||
- stage: deploy
|
|
||||||
python: 3.7
|
|
||||||
after_success: true
|
|
||||||
deploy:
|
|
||||||
provider: pypi
|
|
||||||
user: syrusakbary
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
password:
|
|
||||||
secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ=
|
|
||||||
distributions: "sdist bdist_wheel"
|
|
|
@ -1,3 +0,0 @@
|
||||||
* @ekampf @dan98765 @projectcheshire @jkimbo
|
|
||||||
/docs/ @dvndrsn @phalt @changeling
|
|
||||||
/examples/ @dvndrsn @phalt @changeling
|
|
6
Makefile
6
Makefile
|
@ -8,7 +8,7 @@ install-dev:
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
py.test graphene examples tests_asyncio
|
py.test graphene examples
|
||||||
|
|
||||||
.PHONY: docs ## Generate docs
|
.PHONY: docs ## Generate docs
|
||||||
docs: install-dev
|
docs: install-dev
|
||||||
|
@ -20,8 +20,8 @@ docs-live: install-dev
|
||||||
|
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
format:
|
format:
|
||||||
black graphene examples setup.py tests_asyncio
|
black graphene examples setup.py
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint:
|
lint:
|
||||||
flake8 graphene examples setup.py tests_asyncio
|
flake8 graphene examples setup.py
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -37,7 +31,7 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit
|
||||||
For instaling graphene, just run this command in your shell
|
For instaling graphene, just run this command in your shell
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install "graphene>=2.0"
|
pip install "graphene>=3.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
91
README.rst
91
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
|
||||||
|
@ -52,13 +53,7 @@ For instaling 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
|
||||||
|
|
|
@ -64,6 +64,8 @@ Graphene Scalars
|
||||||
|
|
||||||
.. autoclass:: graphene.JSONString()
|
.. autoclass:: graphene.JSONString()
|
||||||
|
|
||||||
|
.. autoclass:: graphene.Base64()
|
||||||
|
|
||||||
Enum
|
Enum
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
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.",
|
||||||
|
|
|
@ -25,10 +25,10 @@ Create loaders by providing a batch loading function.
|
||||||
|
|
||||||
A batch loading async function accepts a list of keys, and returns a list of ``values``.
|
A batch loading async function accepts a list of keys, and returns 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 event loop 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
|
||||||
|
@ -95,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!
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
Executing a query
|
Executing a query
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
|
||||||
For executing a query against a schema, you can directly call the ``execute`` method on it.
|
For executing a query against a schema, you can directly call the ``execute`` method on it.
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,43 +16,6 @@ For executing a query against a schema, you can directly call the ``execute`` me
|
||||||
``result`` represents the result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred.
|
``result`` represents the result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred.
|
||||||
|
|
||||||
|
|
||||||
For executing a subscription, you can directly call the ``subscribe`` method on it.
|
|
||||||
This method is async and must be awaited.
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
from graphene import ObjectType, String, Schema, Field
|
|
||||||
|
|
||||||
# All schema require a query.
|
|
||||||
class Query(ObjectType):
|
|
||||||
hello = String()
|
|
||||||
|
|
||||||
def resolve_hello(root, info):
|
|
||||||
return 'Hello, world!'
|
|
||||||
|
|
||||||
class Subscription(ObjectType):
|
|
||||||
time_of_day = Field(String)
|
|
||||||
|
|
||||||
async def subscribe_time_of_day(root, info):
|
|
||||||
while True:
|
|
||||||
yield { 'time_of_day': datetime.now().isoformat()}
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
SCHEMA = Schema(query=Query, subscription=Subscription)
|
|
||||||
|
|
||||||
async def main(schema):
|
|
||||||
|
|
||||||
subscription = 'subscription { timeOfDay }'
|
|
||||||
result = await schema.subscribe(subscription)
|
|
||||||
async for item in result:
|
|
||||||
print(item.data['timeOfDay'])
|
|
||||||
|
|
||||||
asyncio.run(main(SCHEMA))
|
|
||||||
|
|
||||||
The ``result`` is an async iterator which yields items in the same manner as a query.
|
|
||||||
|
|
||||||
.. _SchemaExecuteContext:
|
.. _SchemaExecuteContext:
|
||||||
|
|
||||||
Context
|
Context
|
||||||
|
@ -123,7 +85,7 @@ Value used for :ref:`ResolverParamParent` in root queries and mutations can be o
|
||||||
return {'id': root.id, 'firstName': root.name}
|
return {'id': root.id, 'firstName': root.name}
|
||||||
|
|
||||||
schema = Schema(Query)
|
schema = Schema(Query)
|
||||||
user_root = User(id=12, name='bob'}
|
user_root = User(id=12, name='bob')
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
'''
|
'''
|
||||||
query getUser {
|
query getUser {
|
||||||
|
@ -148,7 +110,7 @@ If there are multiple operations defined in a query string, ``operation_name`` s
|
||||||
from graphene import ObjectType, Field, Schema
|
from graphene import ObjectType, Field, Schema
|
||||||
|
|
||||||
class Query(ObjectType):
|
class Query(ObjectType):
|
||||||
me = Field(User)
|
user = Field(User)
|
||||||
|
|
||||||
def resolve_user(root, info):
|
def resolve_user(root, info):
|
||||||
return get_user_by_id(12)
|
return get_user_by_id(12)
|
||||||
|
|
|
@ -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>`_.
|
||||||
|
|
|
@ -9,3 +9,5 @@ Execution
|
||||||
middleware
|
middleware
|
||||||
dataloader
|
dataloader
|
||||||
fileuploading
|
fileuploading
|
||||||
|
subscriptions
|
||||||
|
queryvalidation
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
123
docs/execution/queryvalidation.rst
Normal file
123
docs/execution/queryvalidation.rst
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
Query Validation
|
||||||
|
==========
|
||||||
|
GraphQL uses query validators to check if Query AST is valid and can be executed. Every GraphQL server implements
|
||||||
|
standard query validators. For example, there is an validator that tests if queried field exists on queried type, that
|
||||||
|
makes query fail with "Cannot query field on type" error if it doesn't.
|
||||||
|
|
||||||
|
To help with common use cases, graphene provides a few validation rules out of the box.
|
||||||
|
|
||||||
|
|
||||||
|
Depth limit Validator
|
||||||
|
-----------------
|
||||||
|
The depth limit validator helps to prevent execution of malicious
|
||||||
|
queries. It takes in the following arguments.
|
||||||
|
|
||||||
|
- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document.
|
||||||
|
- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean
|
||||||
|
- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-------
|
||||||
|
|
||||||
|
Here is how you would implement depth-limiting on your schema.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphql import validate, parse
|
||||||
|
from graphene import ObjectType, Schema, String
|
||||||
|
from graphene.validation import depth_limit_validator
|
||||||
|
|
||||||
|
|
||||||
|
class MyQuery(ObjectType):
|
||||||
|
name = String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=MyQuery)
|
||||||
|
|
||||||
|
# queries which have a depth more than 20
|
||||||
|
# will not be executed.
|
||||||
|
|
||||||
|
validation_errors = validate(
|
||||||
|
schema=schema.graphql_schema,
|
||||||
|
document_ast=parse('THE QUERY'),
|
||||||
|
rules=(
|
||||||
|
depth_limit_validator(
|
||||||
|
max_depth=20
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Disable Introspection
|
||||||
|
---------------------
|
||||||
|
the disable introspection validation rule ensures that your schema cannot be introspected.
|
||||||
|
This is a useful security measure in production environments.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-------
|
||||||
|
|
||||||
|
Here is how you would disable introspection for your schema.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphql import validate, parse
|
||||||
|
from graphene import ObjectType, Schema, String
|
||||||
|
from graphene.validation import DisableIntrospection
|
||||||
|
|
||||||
|
|
||||||
|
class MyQuery(ObjectType):
|
||||||
|
name = String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=MyQuery)
|
||||||
|
|
||||||
|
# introspection queries will not be executed.
|
||||||
|
|
||||||
|
validation_errors = validate(
|
||||||
|
schema=schema.graphql_schema,
|
||||||
|
document_ast=parse('THE QUERY'),
|
||||||
|
rules=(
|
||||||
|
DisableIntrospection,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Implementing custom validators
|
||||||
|
------------------------------
|
||||||
|
All custom query validators should extend the `ValidationRule <https://github.com/graphql-python/graphql-core/blob/v3.0.5/src/graphql/validation/rules/__init__.py#L37>`_
|
||||||
|
base class importable from the graphql.validation.rules module. Query validators are visitor classes. They are
|
||||||
|
instantiated at the time of query validation with one required argument (context: ASTValidationContext). In order to
|
||||||
|
perform validation, your validator class should define one or more of enter_* and leave_* methods. For possible
|
||||||
|
enter/leave items as well as details on function documentation, please see contents of the visitor module. To make
|
||||||
|
validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure
|
||||||
|
reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation
|
||||||
|
if any of those fields are blacklisted:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphql import GraphQLError
|
||||||
|
from graphql.language import FieldNode
|
||||||
|
from graphql.validation import ValidationRule
|
||||||
|
|
||||||
|
|
||||||
|
my_blacklist = (
|
||||||
|
"disallowed_field",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_blacklisted_field(field_name: str):
|
||||||
|
return field_name.lower() in my_blacklist
|
||||||
|
|
||||||
|
|
||||||
|
class BlackListRule(ValidationRule):
|
||||||
|
def enter_field(self, node: FieldNode, *_args):
|
||||||
|
field_name = node.name.value
|
||||||
|
if not is_blacklisted_field(field_name):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.report_error(
|
||||||
|
GraphQLError(
|
||||||
|
f"Cannot query '{field_name}': field is blacklisted.", node,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
40
docs/execution/subscriptions.rst
Normal file
40
docs/execution/subscriptions.rst
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
.. _SchemaSubscription:
|
||||||
|
|
||||||
|
Subscriptions
|
||||||
|
=============
|
||||||
|
|
||||||
|
To create a subscription, you can directly call the ``subscribe`` method on the
|
||||||
|
schema. This method is async and must be awaited.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
from graphene import ObjectType, String, Schema, Field
|
||||||
|
|
||||||
|
# Every schema requires a query.
|
||||||
|
class Query(ObjectType):
|
||||||
|
hello = String()
|
||||||
|
|
||||||
|
def resolve_hello(root, info):
|
||||||
|
return "Hello, world!"
|
||||||
|
|
||||||
|
class Subscription(ObjectType):
|
||||||
|
time_of_day = String()
|
||||||
|
|
||||||
|
async def subscribe_time_of_day(root, info):
|
||||||
|
while True:
|
||||||
|
yield datetime.now().isoformat()
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
schema = Schema(query=Query, subscription=Subscription)
|
||||||
|
|
||||||
|
async def main(schema):
|
||||||
|
subscription = 'subscription { timeOfDay }'
|
||||||
|
result = await schema.subscribe(subscription)
|
||||||
|
async for item in result:
|
||||||
|
print(item.data['timeOfDay'])
|
||||||
|
|
||||||
|
asyncio.run(main(schema))
|
||||||
|
|
||||||
|
The ``result`` is an async iterator which yields items in the same manner as a query.
|
|
@ -60,14 +60,14 @@ Requirements
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
- Python (2.7, 3.4, 3.5, 3.6, pypy)
|
- Python (2.7, 3.4, 3.5, 3.6, 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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -103,7 +103,7 @@ For each **Field** in our **Schema**, we write a **Resolver** method to fetch da
|
||||||
Schema Definition Language (SDL)
|
Schema Definition Language (SDL)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
In the `GraphQL Schema Definition Language`_, we could describe the fields defined by our example code as show below.
|
In the `GraphQL Schema Definition Language`_, we could describe the fields defined by our example code as shown below.
|
||||||
|
|
||||||
.. _GraphQL Schema Definition Language: https://graphql.org/learn/schema/
|
.. _GraphQL Schema Definition Language: https://graphql.org/learn/schema/
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,8 @@ Useful links
|
||||||
- `Getting started with Relay`_
|
- `Getting started with Relay`_
|
||||||
- `Relay Global Identification Specification`_
|
- `Relay Global Identification Specification`_
|
||||||
- `Relay Cursor Connection Specification`_
|
- `Relay Cursor Connection Specification`_
|
||||||
- `Relay input Object Mutation`_
|
|
||||||
|
|
||||||
.. _Relay: https://facebook.github.io/relay/docs/en/graphql-server-specification.html
|
.. _Relay: https://relay.dev/docs/guides/graphql-server-specification/
|
||||||
.. _Relay specification: https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-root-field
|
.. _Getting started with Relay: https://relay.dev/docs/getting-started/step-by-step-guide/
|
||||||
.. _Getting started with Relay: https://facebook.github.io/relay/docs/en/quick-start-guide.html
|
.. _Relay Global Identification Specification: https://relay.dev/graphql/objectidentification.htm
|
||||||
.. _Relay Global Identification Specification: https://facebook.github.io/relay/graphql/objectidentification.htm
|
.. _Relay Cursor Connection Specification: https://relay.dev/graphql/connections.htm
|
||||||
.. _Relay Cursor Connection Specification: https://facebook.github.io/relay/graphql/connections.htm
|
|
||||||
.. _Relay input Object Mutation: https://facebook.github.io/relay/graphql/mutations.htm
|
|
||||||
|
|
|
@ -51,20 +51,20 @@ Example of a custom node:
|
||||||
name = 'Node'
|
name = 'Node'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_global_id(type, id):
|
def to_global_id(type_, id):
|
||||||
return f"{type}:{id}"
|
return f"{type_}:{id}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_node_from_global_id(info, global_id, only_type=None):
|
def get_node_from_global_id(info, global_id, only_type=None):
|
||||||
type, id = global_id.split(':')
|
type_, id = global_id.split(':')
|
||||||
if only_type:
|
if only_type:
|
||||||
# We assure that the node type that we want to retrieve
|
# We assure that the node type that we want to retrieve
|
||||||
# is the same that was indicated in the field type
|
# is the same that was indicated in the field type
|
||||||
assert type == only_type._meta.name, 'Received not compatible node.'
|
assert type_ == only_type._meta.name, 'Received not compatible node.'
|
||||||
|
|
||||||
if type == 'User':
|
if type_ == 'User':
|
||||||
return get_user(id)
|
return get_user(id)
|
||||||
elif type == 'Photo':
|
elif type_ == 'Photo':
|
||||||
return get_photo(id)
|
return get_photo(id)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -77,13 +77,13 @@ Snapshot testing
|
||||||
As our APIs evolve, we need to know when our changes introduce any breaking changes that might break
|
As our APIs evolve, we need to know when our changes introduce any breaking changes that might break
|
||||||
some of the clients of our GraphQL app.
|
some of the clients of our GraphQL app.
|
||||||
|
|
||||||
However, writing tests and replicate the same response we expect from our GraphQL application can be
|
However, writing tests and replicating the same response we expect from our GraphQL application can be a
|
||||||
tedious and repetitive task, and sometimes it's easier to skip this process.
|
tedious and repetitive task, and sometimes it's easier to skip this process.
|
||||||
|
|
||||||
Because of that, we recommend the usage of `SnapshotTest <https://github.com/syrusakbary/snapshottest/>`_.
|
Because of that, we recommend the usage of `SnapshotTest <https://github.com/syrusakbary/snapshottest/>`_.
|
||||||
|
|
||||||
SnapshotTest let us write all this tests in a breeze, as creates automatically the ``snapshots`` for us
|
SnapshotTest lets us write all these tests in a breeze, as it automatically creates the ``snapshots`` for us
|
||||||
the first time the test is executed.
|
the first time the test are executed.
|
||||||
|
|
||||||
|
|
||||||
Here is a simple example on how our tests will look if we use ``pytest``:
|
Here is a simple example on how our tests will look if we use ``pytest``:
|
||||||
|
|
|
@ -61,7 +61,8 @@ you can add description etc. to your enum without changing the original:
|
||||||
|
|
||||||
graphene.Enum.from_enum(
|
graphene.Enum.from_enum(
|
||||||
AlreadyExistingPyEnum,
|
AlreadyExistingPyEnum,
|
||||||
description=lambda v: return 'foo' if v == AlreadyExistingPyEnum.Foo else 'bar')
|
description=lambda v: return 'foo' if v == AlreadyExistingPyEnum.Foo else 'bar'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
|
@ -76,6 +77,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
class Color(Enum):
|
class Color(Enum):
|
||||||
RED = 1
|
RED = 1
|
||||||
GREEN = 2
|
GREEN = 2
|
||||||
|
@ -84,11 +86,12 @@ 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
|
||||||
|
|
||||||
from graphene import Enum
|
from graphene import Enum
|
||||||
|
|
||||||
class Color(Enum):
|
class Color(Enum):
|
||||||
RED = 1
|
RED = 1
|
||||||
GREEN = 2
|
GREEN = 2
|
||||||
|
|
|
@ -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
|
||||||
|
@ -104,7 +104,6 @@ To use an InputField you define an InputObjectType that specifies the structure
|
||||||
|
|
||||||
person = graphene.Field(Person)
|
person = graphene.Field(Person)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mutate(root, info, person_data=None):
|
def mutate(root, info, person_data=None):
|
||||||
person = Person(
|
person = Person(
|
||||||
name=person_data.name,
|
name=person_data.name,
|
||||||
|
@ -113,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:
|
||||||
|
|
||||||
|
@ -129,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
|
||||||
|
|
||||||
|
@ -161,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::
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ When we execute a query against that schema.
|
||||||
query_string = "{ me { fullName } }"
|
query_string = "{ me { fullName } }"
|
||||||
result = schema.execute(query_string)
|
result = schema.execute(query_string)
|
||||||
|
|
||||||
assert result.data["me"] == {"fullName": "Luke Skywalker")
|
assert result.data["me"] == {"fullName": "Luke Skywalker"}
|
||||||
|
|
||||||
Then we go through the following steps to resolve this query:
|
Then we go through the following steps to resolve this query:
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
Scalars
|
Scalars
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
Scalar types represent concrete values at the leaves of a query. There are
|
||||||
|
several built in types that Graphene provides out of the box which represent common
|
||||||
|
values in Python. You can also create your own Scalar types to better express
|
||||||
|
values that you might have in your data model.
|
||||||
|
|
||||||
All Scalar types accept the following arguments. All are optional:
|
All Scalar types accept the following arguments. All are optional:
|
||||||
|
|
||||||
``name``: *string*
|
``name``: *string*
|
||||||
|
@ -27,34 +32,39 @@ All Scalar types accept the following arguments. All are optional:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Base scalars
|
Built in scalars
|
||||||
------------
|
----------------
|
||||||
|
|
||||||
Graphene defines the following base Scalar Types:
|
Graphene defines the following base Scalar Types that match the default `GraphQL types <https://graphql.org/learn/schema/#scalar-types>`_:
|
||||||
|
|
||||||
``graphene.String``
|
``graphene.String``
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents textual data, represented as UTF-8
|
Represents textual data, represented as UTF-8
|
||||||
character sequences. The String type is most often used by GraphQL to
|
character sequences. The String type is most often used by GraphQL to
|
||||||
represent free-form human-readable text.
|
represent free-form human-readable text.
|
||||||
|
|
||||||
``graphene.Int``
|
``graphene.Int``
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents non-fractional signed whole numeric
|
Represents non-fractional signed whole numeric
|
||||||
values. Int is a signed 32‐bit integer per the
|
values. Int is a signed 32‐bit integer per the
|
||||||
`GraphQL spec <https://facebook.github.io/graphql/June2018/#sec-Int>`_
|
`GraphQL spec <https://facebook.github.io/graphql/June2018/#sec-Int>`_
|
||||||
|
|
||||||
``graphene.Float``
|
``graphene.Float``
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents signed double-precision fractional
|
Represents signed double-precision fractional
|
||||||
values as specified by
|
values as specified by
|
||||||
`IEEE 754 <http://en.wikipedia.org/wiki/IEEE_floating_point>`_.
|
`IEEE 754 <http://en.wikipedia.org/wiki/IEEE_floating_point>`_.
|
||||||
|
|
||||||
``graphene.Boolean``
|
``graphene.Boolean``
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents `true` or `false`.
|
Represents `true` or `false`.
|
||||||
|
|
||||||
``graphene.ID``
|
``graphene.ID``
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents a unique identifier, often used to
|
Represents a unique identifier, often used to
|
||||||
refetch an object or as key for a cache. The ID type appears in a JSON
|
refetch an object or as key for a cache. The ID type appears in a JSON
|
||||||
|
@ -62,24 +72,183 @@ Graphene defines the following base Scalar Types:
|
||||||
When expected as an input type, any string (such as `"4"`) or integer
|
When expected as an input type, any string (such as `"4"`) or integer
|
||||||
(such as `4`) input value will be accepted as an ID.
|
(such as `4`) input value will be accepted as an ID.
|
||||||
|
|
||||||
Graphene also provides custom scalars for Dates, Times, and JSON:
|
----
|
||||||
|
|
||||||
``graphene.types.datetime.Date``
|
Graphene also provides custom scalars for common values:
|
||||||
|
|
||||||
|
``graphene.Date``
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents a Date value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
|
Represents a Date value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
|
||||||
|
|
||||||
``graphene.types.datetime.DateTime``
|
.. code:: python
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from graphene import Schema, ObjectType, Date
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
one_week_from = Date(required=True, date_input=Date(required=True))
|
||||||
|
|
||||||
|
def resolve_one_week_from(root, info, date_input):
|
||||||
|
assert date_input == datetime.date(2006, 1, 2)
|
||||||
|
return date_input + datetime.timedelta(weeks=1)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("""
|
||||||
|
query {
|
||||||
|
oneWeekFrom(dateInput: "2006-01-02")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert results.data == {"oneWeekFrom": "2006-01-09"}
|
||||||
|
|
||||||
|
|
||||||
|
``graphene.DateTime``
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents a DateTime value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
|
Represents a DateTime value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
|
||||||
|
|
||||||
``graphene.types.datetime.Time``
|
.. code:: python
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from graphene import Schema, ObjectType, DateTime
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
one_hour_from = DateTime(required=True, datetime_input=DateTime(required=True))
|
||||||
|
|
||||||
|
def resolve_one_hour_from(root, info, datetime_input):
|
||||||
|
assert datetime_input == datetime.datetime(2006, 1, 2, 15, 4, 5)
|
||||||
|
return datetime_input + datetime.timedelta(hours=1)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("""
|
||||||
|
query {
|
||||||
|
oneHourFrom(datetimeInput: "2006-01-02T15:04:05")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert results.data == {"oneHourFrom": "2006-01-02T16:04:05"}
|
||||||
|
|
||||||
|
``graphene.Time``
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents a Time value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
|
Represents a Time value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
|
||||||
|
|
||||||
``graphene.types.json.JSONString``
|
.. code:: python
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from graphene import Schema, ObjectType, Time
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
one_hour_from = Time(required=True, time_input=Time(required=True))
|
||||||
|
|
||||||
|
def resolve_one_hour_from(root, info, time_input):
|
||||||
|
assert time_input == datetime.time(15, 4, 5)
|
||||||
|
tmp_time_input = datetime.datetime.combine(datetime.date(1, 1, 1), time_input)
|
||||||
|
return (tmp_time_input + datetime.timedelta(hours=1)).time()
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("""
|
||||||
|
query {
|
||||||
|
oneHourFrom(timeInput: "15:04:05")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert results.data == {"oneHourFrom": "16:04:05"}
|
||||||
|
|
||||||
|
``graphene.Decimal``
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Represents a Python Decimal value.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import decimal
|
||||||
|
from graphene import Schema, ObjectType, Decimal
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
add_one_to = Decimal(required=True, decimal_input=Decimal(required=True))
|
||||||
|
|
||||||
|
def resolve_add_one_to(root, info, decimal_input):
|
||||||
|
assert decimal_input == decimal.Decimal("10.50")
|
||||||
|
return decimal_input + decimal.Decimal("1")
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("""
|
||||||
|
query {
|
||||||
|
addOneTo(decimalInput: "10.50")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert results.data == {"addOneTo": "11.50"}
|
||||||
|
|
||||||
|
``graphene.JSONString``
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Represents a JSON string.
|
Represents a JSON string.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene import Schema, ObjectType, JSONString, String
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
update_json_key = JSONString(
|
||||||
|
required=True,
|
||||||
|
json_input=JSONString(required=True),
|
||||||
|
key=String(required=True),
|
||||||
|
value=String(required=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_update_json_key(root, info, json_input, key, value):
|
||||||
|
assert json_input == {"name": "Jane"}
|
||||||
|
json_input[key] = value
|
||||||
|
return json_input
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("""
|
||||||
|
query {
|
||||||
|
updateJsonKey(jsonInput: "{\\"name\\": \\"Jane\\"}", key: "name", value: "Beth")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert results.data == {"updateJsonKey": "{\"name\": \"Beth\"}"}
|
||||||
|
|
||||||
|
|
||||||
|
``graphene.Base64``
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Represents a Base64 encoded string.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene import Schema, ObjectType, Base64
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
increment_encoded_id = Base64(
|
||||||
|
required=True,
|
||||||
|
base64_input=Base64(required=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_increment_encoded_id(root, info, base64_input):
|
||||||
|
assert base64_input == "4"
|
||||||
|
return int(base64_input) + 1
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("""
|
||||||
|
query {
|
||||||
|
incrementEncodedId(base64Input: "NA==")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert results.data == {"incrementEncodedId": "NQ=="}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Custom scalars
|
Custom scalars
|
||||||
--------------
|
--------------
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
Schema
|
Schema
|
||||||
======
|
======
|
||||||
|
|
||||||
A GraphQL **Schema** defines the types and relationship between **Fields** in your API.
|
A GraphQL **Schema** defines the types and relationships between **Fields** in your API.
|
||||||
|
|
||||||
A Schema is created by supplying the root :ref:`ObjectType` of each operation, query (mandatory), mutation and subscription.
|
A Schema is created by supplying the root :ref:`ObjectType` of each operation, query (mandatory), mutation and subscription.
|
||||||
|
|
||||||
Schema will collect all type definitions related to the root operations and then supplied to the validator and executor.
|
Schema will collect all type definitions related to the root operations and then supply them to the validator and executor.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -15,11 +15,11 @@ Schema will collect all type definitions related to the root operations and then
|
||||||
subscription=MyRootSubscription
|
subscription=MyRootSubscription
|
||||||
)
|
)
|
||||||
|
|
||||||
A Root Query is just a special :ref:`ObjectType` that :ref:`defines the fields <Scalars>` that are the entrypoint for your API. Root Mutation and Root Subscription are similar to Root Query, but for different operation types:
|
A Root Query is just a special :ref:`ObjectType` that defines the fields that are the entrypoint for your API. Root Mutation and Root Subscription are similar to Root Query, but for different operation types:
|
||||||
|
|
||||||
* Query fetches data
|
* Query fetches data
|
||||||
* Mutation to changes data and retrieve the changes
|
* Mutation changes data and retrieves the changes
|
||||||
* Subscription to sends changes to clients in real time
|
* Subscription sends changes to clients in real-time
|
||||||
|
|
||||||
Review the `GraphQL documentation on Schema`_ for a brief overview of fields, schema and operations.
|
Review the `GraphQL documentation on Schema`_ for a brief overview of fields, schema and operations.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -56,14 +56,14 @@ In this case, we need to use the ``types`` argument when creating the Schema.
|
||||||
|
|
||||||
.. _SchemaAutoCamelCase:
|
.. _SchemaAutoCamelCase:
|
||||||
|
|
||||||
Auto CamelCase field names
|
Auto camelCase field names
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
By default all field and argument names (that are not
|
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):
|
||||||
|
|
|
@ -1,88 +1,88 @@
|
||||||
from .pyutils.version import get_version
|
from .pyutils.version import get_version
|
||||||
|
|
||||||
from .types import (
|
|
||||||
ObjectType,
|
|
||||||
InputObjectType,
|
|
||||||
Interface,
|
|
||||||
Mutation,
|
|
||||||
Field,
|
|
||||||
InputField,
|
|
||||||
Schema,
|
|
||||||
Scalar,
|
|
||||||
String,
|
|
||||||
ID,
|
|
||||||
Int,
|
|
||||||
Float,
|
|
||||||
Boolean,
|
|
||||||
Date,
|
|
||||||
DateTime,
|
|
||||||
Time,
|
|
||||||
Decimal,
|
|
||||||
JSONString,
|
|
||||||
UUID,
|
|
||||||
List,
|
|
||||||
NonNull,
|
|
||||||
Enum,
|
|
||||||
Argument,
|
|
||||||
Dynamic,
|
|
||||||
Union,
|
|
||||||
Context,
|
|
||||||
ResolveInfo,
|
|
||||||
)
|
|
||||||
from .relay import (
|
from .relay import (
|
||||||
Node,
|
|
||||||
is_node,
|
|
||||||
GlobalID,
|
|
||||||
ClientIDMutation,
|
ClientIDMutation,
|
||||||
Connection,
|
Connection,
|
||||||
ConnectionField,
|
ConnectionField,
|
||||||
|
GlobalID,
|
||||||
|
Node,
|
||||||
PageInfo,
|
PageInfo,
|
||||||
|
is_node,
|
||||||
|
)
|
||||||
|
from .types import (
|
||||||
|
ID,
|
||||||
|
UUID,
|
||||||
|
Argument,
|
||||||
|
Base64,
|
||||||
|
Boolean,
|
||||||
|
Context,
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Decimal,
|
||||||
|
Dynamic,
|
||||||
|
Enum,
|
||||||
|
Field,
|
||||||
|
Float,
|
||||||
|
InputField,
|
||||||
|
InputObjectType,
|
||||||
|
Int,
|
||||||
|
Interface,
|
||||||
|
JSONString,
|
||||||
|
List,
|
||||||
|
Mutation,
|
||||||
|
NonNull,
|
||||||
|
ObjectType,
|
||||||
|
ResolveInfo,
|
||||||
|
Scalar,
|
||||||
|
Schema,
|
||||||
|
String,
|
||||||
|
Time,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
from .utils.resolve_only_args import resolve_only_args
|
|
||||||
from .utils.module_loading import lazy_import
|
from .utils.module_loading import lazy_import
|
||||||
|
from .utils.resolve_only_args import resolve_only_args
|
||||||
|
|
||||||
|
VERSION = (3, 1, 0, "final", 0)
|
||||||
VERSION = (3, 0, 0, "beta", 1)
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = get_version(VERSION)
|
__version__ = get_version(VERSION)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
"ObjectType",
|
|
||||||
"InputObjectType",
|
|
||||||
"Interface",
|
|
||||||
"Mutation",
|
|
||||||
"Field",
|
|
||||||
"InputField",
|
|
||||||
"Schema",
|
|
||||||
"Scalar",
|
|
||||||
"String",
|
|
||||||
"ID",
|
|
||||||
"Int",
|
|
||||||
"Float",
|
|
||||||
"Enum",
|
|
||||||
"Boolean",
|
|
||||||
"Date",
|
|
||||||
"DateTime",
|
|
||||||
"Time",
|
|
||||||
"Decimal",
|
|
||||||
"JSONString",
|
|
||||||
"UUID",
|
|
||||||
"List",
|
|
||||||
"NonNull",
|
|
||||||
"Argument",
|
"Argument",
|
||||||
"Dynamic",
|
"Base64",
|
||||||
"Union",
|
"Boolean",
|
||||||
"resolve_only_args",
|
|
||||||
"Node",
|
|
||||||
"is_node",
|
|
||||||
"GlobalID",
|
|
||||||
"ClientIDMutation",
|
"ClientIDMutation",
|
||||||
"Connection",
|
"Connection",
|
||||||
"ConnectionField",
|
"ConnectionField",
|
||||||
"PageInfo",
|
|
||||||
"lazy_import",
|
|
||||||
"Context",
|
"Context",
|
||||||
|
"Date",
|
||||||
|
"DateTime",
|
||||||
|
"Decimal",
|
||||||
|
"Dynamic",
|
||||||
|
"Enum",
|
||||||
|
"Field",
|
||||||
|
"Float",
|
||||||
|
"GlobalID",
|
||||||
|
"ID",
|
||||||
|
"InputField",
|
||||||
|
"InputObjectType",
|
||||||
|
"Int",
|
||||||
|
"Interface",
|
||||||
|
"JSONString",
|
||||||
|
"List",
|
||||||
|
"Mutation",
|
||||||
|
"Node",
|
||||||
|
"NonNull",
|
||||||
|
"ObjectType",
|
||||||
|
"PageInfo",
|
||||||
"ResolveInfo",
|
"ResolveInfo",
|
||||||
|
"Scalar",
|
||||||
|
"Schema",
|
||||||
|
"String",
|
||||||
|
"Time",
|
||||||
|
"UUID",
|
||||||
|
"Union",
|
||||||
|
"is_node",
|
||||||
|
"lazy_import",
|
||||||
|
"resolve_only_args",
|
||||||
]
|
]
|
||||||
|
|
|
@ -291,14 +291,7 @@ class Field:
|
||||||
|
|
||||||
|
|
||||||
class _DataclassParams:
|
class _DataclassParams:
|
||||||
__slots__ = (
|
__slots__ = ("init", "repr", "eq", "order", "unsafe_hash", "frozen")
|
||||||
"init",
|
|
||||||
"repr",
|
|
||||||
"eq",
|
|
||||||
"order",
|
|
||||||
"unsafe_hash",
|
|
||||||
"frozen",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, init, repr, eq, order, unsafe_hash, frozen):
|
def __init__(self, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
self.init = init
|
self.init = init
|
||||||
|
@ -442,13 +435,11 @@ def _field_init(f, frozen, globals, self_name):
|
||||||
# This field does not need initialization. Signify that
|
# This field does not need initialization. Signify that
|
||||||
# to the caller by returning None.
|
# to the caller by returning None.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Only test this now, so that we can create variables for the
|
# Only test this now, so that we can create variables for the
|
||||||
# default. However, return None to signify that we're not going
|
# default. However, return None to signify that we're not going
|
||||||
# to actually do the assignment statement for InitVars.
|
# to actually do the assignment statement for InitVars.
|
||||||
if f._field_type == _FIELD_INITVAR:
|
if f._field_type == _FIELD_INITVAR:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Now, actually generate the field assignment.
|
# Now, actually generate the field assignment.
|
||||||
return _field_assign(frozen, f.name, value, self_name)
|
return _field_assign(frozen, f.name, value, self_name)
|
||||||
|
|
||||||
|
@ -490,7 +481,6 @@ def _init_fn(fields, frozen, has_post_init, self_name):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"non-default argument {f.name!r} " "follows default argument"
|
f"non-default argument {f.name!r} " "follows default argument"
|
||||||
)
|
)
|
||||||
|
|
||||||
globals = {"MISSING": MISSING, "_HAS_DEFAULT_FACTORY": _HAS_DEFAULT_FACTORY}
|
globals = {"MISSING": MISSING, "_HAS_DEFAULT_FACTORY": _HAS_DEFAULT_FACTORY}
|
||||||
|
|
||||||
body_lines = []
|
body_lines = []
|
||||||
|
@ -500,16 +490,13 @@ def _init_fn(fields, frozen, has_post_init, self_name):
|
||||||
# initialization (it's a pseudo-field). Just skip it.
|
# initialization (it's a pseudo-field). Just skip it.
|
||||||
if line:
|
if line:
|
||||||
body_lines.append(line)
|
body_lines.append(line)
|
||||||
|
|
||||||
# Does this class have a post-init function?
|
# Does this class have a post-init function?
|
||||||
if has_post_init:
|
if has_post_init:
|
||||||
params_str = ",".join(f.name for f in fields if f._field_type is _FIELD_INITVAR)
|
params_str = ",".join(f.name for f in fields if f._field_type is _FIELD_INITVAR)
|
||||||
body_lines.append(f"{self_name}.{_POST_INIT_NAME}({params_str})")
|
body_lines.append(f"{self_name}.{_POST_INIT_NAME}({params_str})")
|
||||||
|
|
||||||
# If no body lines, use 'pass'.
|
# If no body lines, use 'pass'.
|
||||||
if not body_lines:
|
if not body_lines:
|
||||||
body_lines = ["pass"]
|
body_lines = ["pass"]
|
||||||
|
|
||||||
locals = {f"_type_{f.name}": f.type for f in fields}
|
locals = {f"_type_{f.name}": f.type for f in fields}
|
||||||
return _create_fn(
|
return _create_fn(
|
||||||
"__init__",
|
"__init__",
|
||||||
|
@ -674,7 +661,6 @@ def _get_field(cls, a_name, a_type):
|
||||||
# This is a field in __slots__, so it has no default value.
|
# This is a field in __slots__, so it has no default value.
|
||||||
default = MISSING
|
default = MISSING
|
||||||
f = field(default=default)
|
f = field(default=default)
|
||||||
|
|
||||||
# Only at this point do we know the name and the type. Set them.
|
# Only at this point do we know the name and the type. Set them.
|
||||||
f.name = a_name
|
f.name = a_name
|
||||||
f.type = a_type
|
f.type = a_type
|
||||||
|
@ -705,7 +691,6 @@ def _get_field(cls, a_name, a_type):
|
||||||
and _is_type(f.type, cls, typing, typing.ClassVar, _is_classvar)
|
and _is_type(f.type, cls, typing, typing.ClassVar, _is_classvar)
|
||||||
):
|
):
|
||||||
f._field_type = _FIELD_CLASSVAR
|
f._field_type = _FIELD_CLASSVAR
|
||||||
|
|
||||||
# If the type is InitVar, or if it's a matching string annotation,
|
# If the type is InitVar, or if it's a matching string annotation,
|
||||||
# then it's an InitVar.
|
# then it's an InitVar.
|
||||||
if f._field_type is _FIELD:
|
if f._field_type is _FIELD:
|
||||||
|
@ -717,7 +702,6 @@ def _get_field(cls, a_name, a_type):
|
||||||
and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, _is_initvar)
|
and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, _is_initvar)
|
||||||
):
|
):
|
||||||
f._field_type = _FIELD_INITVAR
|
f._field_type = _FIELD_INITVAR
|
||||||
|
|
||||||
# Validations for individual fields. This is delayed until now,
|
# Validations for individual fields. This is delayed until now,
|
||||||
# instead of in the Field() constructor, since only here do we
|
# instead of in the Field() constructor, since only here do we
|
||||||
# know the field name, which allows for better error reporting.
|
# know the field name, which allows for better error reporting.
|
||||||
|
@ -731,14 +715,12 @@ def _get_field(cls, a_name, a_type):
|
||||||
# example, how about init=False (or really,
|
# example, how about init=False (or really,
|
||||||
# init=<not-the-default-init-value>)? It makes no sense for
|
# init=<not-the-default-init-value>)? It makes no sense for
|
||||||
# ClassVar and InitVar to specify init=<anything>.
|
# ClassVar and InitVar to specify init=<anything>.
|
||||||
|
|
||||||
# For real fields, disallow mutable defaults for known types.
|
# For real fields, disallow mutable defaults for known types.
|
||||||
if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)):
|
if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"mutable default {type(f.default)} for field "
|
f"mutable default {type(f.default)} for field "
|
||||||
f"{f.name} is not allowed: use default_factory"
|
f"{f.name} is not allowed: use default_factory"
|
||||||
)
|
)
|
||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
@ -827,7 +809,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
fields[f.name] = f
|
fields[f.name] = f
|
||||||
if getattr(b, _PARAMS).frozen:
|
if getattr(b, _PARAMS).frozen:
|
||||||
any_frozen_base = True
|
any_frozen_base = True
|
||||||
|
|
||||||
# Annotations that are defined in this class (not in base
|
# Annotations that are defined in this class (not in base
|
||||||
# classes). If __annotations__ isn't present, then this class
|
# classes). If __annotations__ isn't present, then this class
|
||||||
# adds no new annotations. We use this to compute fields that are
|
# adds no new annotations. We use this to compute fields that are
|
||||||
|
@ -845,7 +826,9 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
# Now find fields in our class. While doing so, validate some
|
# Now find fields in our class. While doing so, validate some
|
||||||
# things, and set the default values (as class attributes) where
|
# things, and set the default values (as class attributes) where
|
||||||
# we can.
|
# we can.
|
||||||
cls_fields = [_get_field(cls, name, type) for name, type in cls_annotations.items()]
|
cls_fields = [
|
||||||
|
_get_field(cls, name, type_) for name, type_ in cls_annotations.items()
|
||||||
|
]
|
||||||
for f in cls_fields:
|
for f in cls_fields:
|
||||||
fields[f.name] = f
|
fields[f.name] = f
|
||||||
|
|
||||||
|
@ -864,22 +847,18 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
delattr(cls, f.name)
|
delattr(cls, f.name)
|
||||||
else:
|
else:
|
||||||
setattr(cls, f.name, f.default)
|
setattr(cls, f.name, f.default)
|
||||||
|
|
||||||
# Do we have any Field members that don't also have annotations?
|
# Do we have any Field members that don't also have annotations?
|
||||||
for name, value in cls.__dict__.items():
|
for name, value in cls.__dict__.items():
|
||||||
if isinstance(value, Field) and not name in cls_annotations:
|
if isinstance(value, Field) and not name in cls_annotations:
|
||||||
raise TypeError(f"{name!r} is a field but has no type annotation")
|
raise TypeError(f"{name!r} is a field but has no type annotation")
|
||||||
|
|
||||||
# Check rules that apply if we are derived from any dataclasses.
|
# Check rules that apply if we are derived from any dataclasses.
|
||||||
if has_dataclass_bases:
|
if has_dataclass_bases:
|
||||||
# Raise an exception if any of our bases are frozen, but we're not.
|
# Raise an exception if any of our bases are frozen, but we're not.
|
||||||
if any_frozen_base and not frozen:
|
if any_frozen_base and not frozen:
|
||||||
raise TypeError("cannot inherit non-frozen dataclass from a " "frozen one")
|
raise TypeError("cannot inherit non-frozen dataclass from a " "frozen one")
|
||||||
|
|
||||||
# Raise an exception if we're frozen, but none of our bases are.
|
# Raise an exception if we're frozen, but none of our bases are.
|
||||||
if not any_frozen_base and frozen:
|
if not any_frozen_base and frozen:
|
||||||
raise TypeError("cannot inherit frozen dataclass from a " "non-frozen one")
|
raise TypeError("cannot inherit frozen dataclass from a " "non-frozen one")
|
||||||
|
|
||||||
# Remember all of the fields on our class (including bases). This
|
# Remember all of the fields on our class (including bases). This
|
||||||
# also marks this class as being a dataclass.
|
# also marks this class as being a dataclass.
|
||||||
setattr(cls, _FIELDS, fields)
|
setattr(cls, _FIELDS, fields)
|
||||||
|
@ -898,7 +877,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
# eq methods.
|
# eq methods.
|
||||||
if order and not eq:
|
if order and not eq:
|
||||||
raise ValueError("eq must be true if order is true")
|
raise ValueError("eq must be true if order is true")
|
||||||
|
|
||||||
if init:
|
if init:
|
||||||
# Does this class have a post-init function?
|
# Does this class have a post-init function?
|
||||||
has_post_init = hasattr(cls, _POST_INIT_NAME)
|
has_post_init = hasattr(cls, _POST_INIT_NAME)
|
||||||
|
@ -918,7 +896,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
"__dataclass_self__" if "self" in fields else "self",
|
"__dataclass_self__" if "self" in fields else "self",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the fields as a list, and include only real fields. This is
|
# Get the fields as a list, and include only real fields. This is
|
||||||
# used in all of the following methods.
|
# used in all of the following methods.
|
||||||
field_list = [f for f in fields.values() if f._field_type is _FIELD]
|
field_list = [f for f in fields.values() if f._field_type is _FIELD]
|
||||||
|
@ -926,7 +903,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
if repr:
|
if repr:
|
||||||
flds = [f for f in field_list if f.repr]
|
flds = [f for f in field_list if f.repr]
|
||||||
_set_new_attribute(cls, "__repr__", _repr_fn(flds))
|
_set_new_attribute(cls, "__repr__", _repr_fn(flds))
|
||||||
|
|
||||||
if eq:
|
if eq:
|
||||||
# Create _eq__ method. There's no need for a __ne__ method,
|
# Create _eq__ method. There's no need for a __ne__ method,
|
||||||
# since python will call __eq__ and negate it.
|
# since python will call __eq__ and negate it.
|
||||||
|
@ -936,7 +912,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
_set_new_attribute(
|
_set_new_attribute(
|
||||||
cls, "__eq__", _cmp_fn("__eq__", "==", self_tuple, other_tuple)
|
cls, "__eq__", _cmp_fn("__eq__", "==", self_tuple, other_tuple)
|
||||||
)
|
)
|
||||||
|
|
||||||
if order:
|
if order:
|
||||||
# Create and set the ordering methods.
|
# Create and set the ordering methods.
|
||||||
flds = [f for f in field_list if f.compare]
|
flds = [f for f in field_list if f.compare]
|
||||||
|
@ -956,7 +931,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
f"in class {cls.__name__}. Consider using "
|
f"in class {cls.__name__}. Consider using "
|
||||||
"functools.total_ordering"
|
"functools.total_ordering"
|
||||||
)
|
)
|
||||||
|
|
||||||
if frozen:
|
if frozen:
|
||||||
for fn in _frozen_get_del_attr(cls, field_list):
|
for fn in _frozen_get_del_attr(cls, field_list):
|
||||||
if _set_new_attribute(cls, fn.__name__, fn):
|
if _set_new_attribute(cls, fn.__name__, fn):
|
||||||
|
@ -964,7 +938,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
f"Cannot overwrite attribute {fn.__name__} "
|
f"Cannot overwrite attribute {fn.__name__} "
|
||||||
f"in class {cls.__name__}"
|
f"in class {cls.__name__}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Decide if/how we're going to create a hash function.
|
# Decide if/how we're going to create a hash function.
|
||||||
hash_action = _hash_action[
|
hash_action = _hash_action[
|
||||||
bool(unsafe_hash), bool(eq), bool(frozen), has_explicit_hash
|
bool(unsafe_hash), bool(eq), bool(frozen), has_explicit_hash
|
||||||
|
@ -973,11 +946,9 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
# No need to call _set_new_attribute here, since by the time
|
# No need to call _set_new_attribute here, since by the time
|
||||||
# we're here the overwriting is unconditional.
|
# we're here the overwriting is unconditional.
|
||||||
cls.__hash__ = hash_action(cls, field_list)
|
cls.__hash__ = hash_action(cls, field_list)
|
||||||
|
|
||||||
if not getattr(cls, "__doc__"):
|
if not getattr(cls, "__doc__"):
|
||||||
# Create a class doc-string.
|
# Create a class doc-string.
|
||||||
cls.__doc__ = cls.__name__ + str(inspect.signature(cls)).replace(" -> None", "")
|
cls.__doc__ = cls.__name__ + str(inspect.signature(cls)).replace(" -> None", "")
|
||||||
|
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
@ -1013,7 +984,6 @@ def dataclass(
|
||||||
if _cls is None:
|
if _cls is None:
|
||||||
# We're called with parens.
|
# We're called with parens.
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
# We're called as @dataclass without parens.
|
# We're called as @dataclass without parens.
|
||||||
return wrap(_cls)
|
return wrap(_cls)
|
||||||
|
|
||||||
|
@ -1030,7 +1000,6 @@ def fields(class_or_instance):
|
||||||
fields = getattr(class_or_instance, _FIELDS)
|
fields = getattr(class_or_instance, _FIELDS)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise TypeError("must be called with a dataclass type or instance")
|
raise TypeError("must be called with a dataclass type or instance")
|
||||||
|
|
||||||
# Exclude pseudo-fields. Note that fields is sorted by insertion
|
# Exclude pseudo-fields. Note that fields is sorted by insertion
|
||||||
# order, so the order of the tuple is as the fields were defined.
|
# order, so the order of the tuple is as the fields were defined.
|
||||||
return tuple(f for f in fields.values() if f._field_type is _FIELD)
|
return tuple(f for f in fields.values() if f._field_type is _FIELD)
|
||||||
|
@ -1172,7 +1141,6 @@ def make_dataclass(
|
||||||
else:
|
else:
|
||||||
# Copy namespace since we're going to mutate it.
|
# Copy namespace since we're going to mutate it.
|
||||||
namespace = namespace.copy()
|
namespace = namespace.copy()
|
||||||
|
|
||||||
# While we're looking through the field names, validate that they
|
# While we're looking through the field names, validate that they
|
||||||
# are identifiers, are not keywords, and not duplicates.
|
# are identifiers, are not keywords, and not duplicates.
|
||||||
seen = set()
|
seen = set()
|
||||||
|
@ -1182,23 +1150,20 @@ def make_dataclass(
|
||||||
name = item
|
name = item
|
||||||
tp = "typing.Any"
|
tp = "typing.Any"
|
||||||
elif len(item) == 2:
|
elif len(item) == 2:
|
||||||
name, tp, = item
|
(name, tp) = item
|
||||||
elif len(item) == 3:
|
elif len(item) == 3:
|
||||||
name, tp, spec = item
|
name, tp, spec = item
|
||||||
namespace[name] = spec
|
namespace[name] = spec
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Invalid field: {item!r}")
|
raise TypeError(f"Invalid field: {item!r}")
|
||||||
|
|
||||||
if not isinstance(name, str) or not name.isidentifier():
|
if not isinstance(name, str) or not name.isidentifier():
|
||||||
raise TypeError(f"Field names must be valid identifers: {name!r}")
|
raise TypeError(f"Field names must be valid identifers: {name!r}")
|
||||||
if keyword.iskeyword(name):
|
if keyword.iskeyword(name):
|
||||||
raise TypeError(f"Field names must not be keywords: {name!r}")
|
raise TypeError(f"Field names must not be keywords: {name!r}")
|
||||||
if name in seen:
|
if name in seen:
|
||||||
raise TypeError(f"Field name duplicated: {name!r}")
|
raise TypeError(f"Field name duplicated: {name!r}")
|
||||||
|
|
||||||
seen.add(name)
|
seen.add(name)
|
||||||
anns[name] = tp
|
anns[name] = tp
|
||||||
|
|
||||||
namespace["__annotations__"] = anns
|
namespace["__annotations__"] = anns
|
||||||
# We use `types.new_class()` instead of simply `type()` to allow dynamic creation
|
# We use `types.new_class()` instead of simply `type()` to allow dynamic creation
|
||||||
# of generic dataclassses.
|
# of generic dataclassses.
|
||||||
|
@ -1227,14 +1192,13 @@ def replace(obj, **changes):
|
||||||
c = C(1, 2)
|
c = C(1, 2)
|
||||||
c1 = replace(c, x=3)
|
c1 = replace(c, x=3)
|
||||||
assert c1.x == 3 and c1.y == 2
|
assert c1.x == 3 and c1.y == 2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We're going to mutate 'changes', but that's okay because it's a
|
# We're going to mutate 'changes', but that's okay because it's a
|
||||||
# new dict, even if called with 'replace(obj, **my_changes)'.
|
# new dict, even if called with 'replace(obj, **my_changes)'.
|
||||||
|
|
||||||
if not _is_dataclass_instance(obj):
|
if not _is_dataclass_instance(obj):
|
||||||
raise TypeError("replace() should be called on dataclass instances")
|
raise TypeError("replace() should be called on dataclass instances")
|
||||||
|
|
||||||
# It's an error to have init=False fields in 'changes'.
|
# It's an error to have init=False fields in 'changes'.
|
||||||
# If a field is not in 'changes', read its value from the provided obj.
|
# If a field is not in 'changes', read its value from the provided obj.
|
||||||
|
|
||||||
|
@ -1248,10 +1212,8 @@ def replace(obj, **changes):
|
||||||
"replace()"
|
"replace()"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if f.name not in changes:
|
if f.name not in changes:
|
||||||
changes[f.name] = getattr(obj, f.name)
|
changes[f.name] = getattr(obj, f.name)
|
||||||
|
|
||||||
# Create the new object, which calls __init__() and
|
# Create the new object, which calls __init__() and
|
||||||
# __post_init__() (if defined), using all of the init fields we've
|
# __post_init__() (if defined), using all of the init fields we've
|
||||||
# added and/or left in 'changes'. If there are values supplied in
|
# added and/or left in 'changes'. If there are values supplied in
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -117,19 +117,19 @@ def connection_adapter(cls, edges, pageInfo):
|
||||||
|
|
||||||
|
|
||||||
class IterableConnectionField(Field):
|
class IterableConnectionField(Field):
|
||||||
def __init__(self, type, *args, **kwargs):
|
def __init__(self, type_, *args, **kwargs):
|
||||||
kwargs.setdefault("before", String())
|
kwargs.setdefault("before", String())
|
||||||
kwargs.setdefault("after", String())
|
kwargs.setdefault("after", String())
|
||||||
kwargs.setdefault("first", Int())
|
kwargs.setdefault("first", Int())
|
||||||
kwargs.setdefault("last", Int())
|
kwargs.setdefault("last", Int())
|
||||||
super(IterableConnectionField, self).__init__(type, *args, **kwargs)
|
super(IterableConnectionField, self).__init__(type_, *args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
type = super(IterableConnectionField, self).type
|
type_ = super(IterableConnectionField, self).type
|
||||||
connection_type = type
|
connection_type = type_
|
||||||
if isinstance(type, NonNull):
|
if isinstance(type_, NonNull):
|
||||||
connection_type = type.of_type
|
connection_type = type_.of_type
|
||||||
|
|
||||||
if is_node(connection_type):
|
if is_node(connection_type):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
@ -140,7 +140,7 @@ class IterableConnectionField(Field):
|
||||||
assert issubclass(
|
assert issubclass(
|
||||||
connection_type, Connection
|
connection_type, Connection
|
||||||
), f'{self.__class__.__name__} type has to be a subclass of Connection. Received "{connection_type}".'
|
), f'{self.__class__.__name__} type has to be a subclass of Connection. Received "{connection_type}".'
|
||||||
return type
|
return type_
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_connection(cls, connection_type, args, resolved):
|
def resolve_connection(cls, connection_type, args, resolved):
|
||||||
|
@ -171,8 +171,8 @@ class IterableConnectionField(Field):
|
||||||
on_resolve = partial(cls.resolve_connection, connection_type, args)
|
on_resolve = partial(cls.resolve_connection, connection_type, args)
|
||||||
return maybe_thenable(resolved, on_resolve)
|
return maybe_thenable(resolved, on_resolve)
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def wrap_resolve(self, parent_resolver):
|
||||||
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)
|
resolver = super(IterableConnectionField, self).wrap_resolve(parent_resolver)
|
||||||
return partial(self.connection_resolver, resolver, self.type)
|
return partial(self.connection_resolver, resolver, self.type)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -37,7 +33,7 @@ class GlobalID(Field):
|
||||||
parent_type_name = parent_type_name or info.parent_type.name
|
parent_type_name = parent_type_name or info.parent_type.name
|
||||||
return node.to_global_id(parent_type_name, type_id) # root._meta.name
|
return node.to_global_id(parent_type_name, type_id) # root._meta.name
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def wrap_resolve(self, parent_resolver):
|
||||||
return partial(
|
return partial(
|
||||||
self.id_resolver,
|
self.id_resolver,
|
||||||
parent_resolver,
|
parent_resolver,
|
||||||
|
@ -47,20 +43,20 @@ class GlobalID(Field):
|
||||||
|
|
||||||
|
|
||||||
class NodeField(Field):
|
class NodeField(Field):
|
||||||
def __init__(self, node, type=False, **kwargs):
|
def __init__(self, node, type_=False, **kwargs):
|
||||||
assert issubclass(node, Node), "NodeField can only operate in Nodes"
|
assert issubclass(node, Node), "NodeField can only operate in Nodes"
|
||||||
self.node_type = node
|
self.node_type = node
|
||||||
self.field_type = type
|
self.field_type = type_
|
||||||
|
|
||||||
super(NodeField, self).__init__(
|
super(NodeField, self).__init__(
|
||||||
# If we don's specify a type, the field type will be the node
|
# If we don's specify a type, the field type will be the node
|
||||||
# interface
|
# interface
|
||||||
type or node,
|
type_ or node,
|
||||||
id=ID(required=True, description="The ID of the object"),
|
id=ID(required=True, description="The ID of the object"),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def wrap_resolve(self, parent_resolver):
|
||||||
return partial(self.node_type.node_resolver, get_type(self.field_type))
|
return partial(self.node_type.node_resolver, get_type(self.field_type))
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,13 +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: {e}"
|
||||||
f"Exception message: {str(e)}"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
graphene_type = info.schema.get_type(_type)
|
graphene_type = info.schema.get_type(_type)
|
||||||
|
@ -125,5 +121,5 @@ class Node(AbstractNode):
|
||||||
return from_global_id(global_id)
|
return from_global_id(global_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def to_global_id(cls, type, id):
|
def to_global_id(cls, type_, id):
|
||||||
return to_global_id(type, id)
|
return to_global_id(type_, id)
|
||||||
|
|
|
@ -51,10 +51,10 @@ letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter
|
||||||
def edges(selected_letters):
|
def edges(selected_letters):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"node": {"id": base64("Letter:%s" % l.id), "letter": l.letter},
|
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
|
||||||
"cursor": base64("arrayconnection:%s" % l.id),
|
"cursor": base64("arrayconnection:%s" % letter.id),
|
||||||
}
|
}
|
||||||
for l in [letters[i] for i in selected_letters]
|
for letter in [letters[i] for i in selected_letters]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -51,10 +51,10 @@ letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter
|
||||||
def edges(selected_letters):
|
def edges(selected_letters):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"node": {"id": base64("Letter:%s" % l.id), "letter": l.letter},
|
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
|
||||||
"cursor": base64("arrayconnection:%s" % l.id),
|
"cursor": base64("arrayconnection:%s" % letter.id),
|
||||||
}
|
}
|
||||||
for l in [letters[i] for i in selected_letters]
|
for letter in [letters[i] for i in selected_letters]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ def cursor_for(ltr):
|
||||||
async def execute(args=""):
|
async def execute(args=""):
|
||||||
if args:
|
if args:
|
||||||
args = "(" + args + ")"
|
args = "(" + args + ")"
|
||||||
|
|
||||||
return await schema.execute_async(
|
return await schema.execute_async(
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
|
@ -164,14 +163,14 @@ async def test_respects_first_and_after_and_before_too_few():
|
||||||
@mark.asyncio
|
@mark.asyncio
|
||||||
async def test_respects_first_and_after_and_before_too_many():
|
async def test_respects_first_and_after_and_before_too_many():
|
||||||
await check(
|
await check(
|
||||||
f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD",
|
f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@mark.asyncio
|
@mark.asyncio
|
||||||
async def test_respects_first_and_after_and_before_exactly_right():
|
async def test_respects_first_and_after_and_before_exactly_right():
|
||||||
await check(
|
await check(
|
||||||
f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD",
|
f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -187,14 +186,14 @@ async def test_respects_last_and_after_and_before_too_few():
|
||||||
@mark.asyncio
|
@mark.asyncio
|
||||||
async def test_respects_last_and_after_and_before_too_many():
|
async def test_respects_last_and_after_and_before_too_many():
|
||||||
await check(
|
await check(
|
||||||
f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD",
|
f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@mark.asyncio
|
@mark.asyncio
|
||||||
async def test_respects_last_and_after_and_before_exactly_right():
|
async def test_respects_last_and_after_and_before_exactly_right():
|
||||||
await check(
|
await check(
|
||||||
f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD",
|
f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ def test_global_id_allows_overriding_of_node_and_required():
|
||||||
def test_global_id_defaults_to_info_parent_type():
|
def test_global_id_defaults_to_info_parent_type():
|
||||||
my_id = "1"
|
my_id = "1"
|
||||||
gid = GlobalID()
|
gid = GlobalID()
|
||||||
id_resolver = gid.get_resolver(lambda *_: my_id)
|
id_resolver = gid.wrap_resolve(lambda *_: my_id)
|
||||||
my_global_id = id_resolver(None, Info(User))
|
my_global_id = id_resolver(None, Info(User))
|
||||||
assert my_global_id == to_global_id(User._meta.name, my_id)
|
assert my_global_id == to_global_id(User._meta.name, my_id)
|
||||||
|
|
||||||
|
@ -53,6 +53,6 @@ def test_global_id_defaults_to_info_parent_type():
|
||||||
def test_global_id_allows_setting_customer_parent_type():
|
def test_global_id_allows_setting_customer_parent_type():
|
||||||
my_id = "1"
|
my_id = "1"
|
||||||
gid = GlobalID(parent_type=User)
|
gid = GlobalID(parent_type=User)
|
||||||
id_resolver = gid.get_resolver(lambda *_: my_id)
|
id_resolver = gid.wrap_resolve(lambda *_: my_id)
|
||||||
my_global_id = id_resolver(None, None)
|
my_global_id = id_resolver(None, None)
|
||||||
assert my_global_id == to_global_id(User._meta.name, my_id)
|
assert my_global_id == to_global_id(User._meta.name, my_id)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
from graphql_relay import to_global_id
|
from textwrap import dedent
|
||||||
|
|
||||||
from graphql.pyutils 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,5 +1,6 @@
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
from graphql import graphql_sync
|
from graphql import graphql_sync
|
||||||
from graphql.pyutils import dedent
|
|
||||||
|
|
||||||
from ...types import Interface, ObjectType, Schema
|
from ...types import Interface, ObjectType, Schema
|
||||||
from ...types.scalars import Int, String
|
from ...types.scalars import Int, String
|
||||||
|
@ -11,7 +12,7 @@ class CustomNode(Node):
|
||||||
name = "Node"
|
name = "Node"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_global_id(type, id):
|
def to_global_id(type_, id):
|
||||||
return id
|
return id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -53,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
|
||||||
}
|
}
|
||||||
|
@ -92,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,52 +1,53 @@
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
from graphql import GraphQLResolveInfo as ResolveInfo
|
from graphql import GraphQLResolveInfo as ResolveInfo
|
||||||
|
|
||||||
from .objecttype import ObjectType
|
from .argument import Argument
|
||||||
from .interface import Interface
|
from .base64 import Base64
|
||||||
from .mutation import Mutation
|
from .context import Context
|
||||||
from .scalars import Scalar, String, ID, Int, Float, Boolean
|
|
||||||
from .datetime import Date, DateTime, Time
|
from .datetime import Date, DateTime, Time
|
||||||
from .decimal import Decimal
|
from .decimal import Decimal
|
||||||
from .json import JSONString
|
from .dynamic import Dynamic
|
||||||
from .uuid import UUID
|
|
||||||
from .schema import Schema
|
|
||||||
from .structures import List, NonNull
|
|
||||||
from .enum import Enum
|
from .enum import Enum
|
||||||
from .field import Field
|
from .field import Field
|
||||||
from .inputfield import InputField
|
from .inputfield import InputField
|
||||||
from .argument import Argument
|
|
||||||
from .inputobjecttype import InputObjectType
|
from .inputobjecttype import InputObjectType
|
||||||
from .dynamic import Dynamic
|
from .interface import Interface
|
||||||
|
from .json import JSONString
|
||||||
|
from .mutation import Mutation
|
||||||
|
from .objecttype import ObjectType
|
||||||
|
from .scalars import ID, Boolean, Float, Int, Scalar, String
|
||||||
|
from .schema import Schema
|
||||||
|
from .structures import List, NonNull
|
||||||
from .union import Union
|
from .union import Union
|
||||||
from .context import Context
|
from .uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ObjectType",
|
"Argument",
|
||||||
"InputObjectType",
|
"Base64",
|
||||||
"Interface",
|
"Boolean",
|
||||||
"Mutation",
|
"Context",
|
||||||
"Enum",
|
|
||||||
"Field",
|
|
||||||
"InputField",
|
|
||||||
"Schema",
|
|
||||||
"Scalar",
|
|
||||||
"String",
|
|
||||||
"ID",
|
|
||||||
"Int",
|
|
||||||
"Float",
|
|
||||||
"Date",
|
"Date",
|
||||||
"DateTime",
|
"DateTime",
|
||||||
"Time",
|
|
||||||
"Decimal",
|
"Decimal",
|
||||||
"JSONString",
|
|
||||||
"UUID",
|
|
||||||
"Boolean",
|
|
||||||
"List",
|
|
||||||
"NonNull",
|
|
||||||
"Argument",
|
|
||||||
"Dynamic",
|
"Dynamic",
|
||||||
"Union",
|
"Enum",
|
||||||
"Context",
|
"Field",
|
||||||
|
"Float",
|
||||||
|
"ID",
|
||||||
|
"InputField",
|
||||||
|
"InputObjectType",
|
||||||
|
"Int",
|
||||||
|
"Interface",
|
||||||
|
"JSONString",
|
||||||
|
"List",
|
||||||
|
"Mutation",
|
||||||
|
"NonNull",
|
||||||
|
"ObjectType",
|
||||||
"ResolveInfo",
|
"ResolveInfo",
|
||||||
|
"Scalar",
|
||||||
|
"Schema",
|
||||||
|
"String",
|
||||||
|
"Time",
|
||||||
|
"UUID",
|
||||||
|
"Union",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
@ -40,8 +41,8 @@ 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,
|
||||||
|
@ -50,10 +51,10 @@ class Argument(MountedType):
|
||||||
super(Argument, self).__init__(_creation_counter=_creation_counter)
|
super(Argument, self).__init__(_creation_counter=_creation_counter)
|
||||||
|
|
||||||
if required:
|
if required:
|
||||||
type = NonNull(type)
|
type_ = NonNull(type_)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self._type = type
|
self._type = type_
|
||||||
self.default_value = default_value
|
self.default_value = default_value
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ class BaseType(SubclassWithMeta):
|
||||||
def __init_subclass_with_meta__(
|
def __init_subclass_with_meta__(
|
||||||
cls, name=None, description=None, _meta=None, **_kwargs
|
cls, name=None, description=None, _meta=None, **_kwargs
|
||||||
):
|
):
|
||||||
assert "_meta" not in cls.__dict__, "Can't assign directly meta"
|
assert "_meta" not in cls.__dict__, "Can't assign meta directly"
|
||||||
if not _meta:
|
if not _meta:
|
||||||
return
|
return
|
||||||
_meta.name = name or cls.__name__
|
_meta.name = name or cls.__name__
|
||||||
|
|
43
graphene/types/base64.py
Normal file
43
graphene/types/base64.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from binascii import Error as _Error
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
|
from graphql.error import GraphQLError
|
||||||
|
from graphql.language import StringValueNode, print_ast
|
||||||
|
|
||||||
|
from .scalars import Scalar
|
||||||
|
|
||||||
|
|
||||||
|
class Base64(Scalar):
|
||||||
|
"""
|
||||||
|
The `Base64` scalar type represents a base64-encoded String.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serialize(value):
|
||||||
|
if not isinstance(value, bytes):
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.encode("utf-8")
|
||||||
|
else:
|
||||||
|
value = str(value).encode("utf-8")
|
||||||
|
return b64encode(value).decode("utf-8")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_literal(cls, node, _variables=None):
|
||||||
|
if not isinstance(node, StringValueNode):
|
||||||
|
raise GraphQLError(
|
||||||
|
f"Base64 cannot represent non-string value: {print_ast(node)}"
|
||||||
|
)
|
||||||
|
return cls.parse_value(node.value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_value(value):
|
||||||
|
if not isinstance(value, bytes):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise GraphQLError(
|
||||||
|
f"Base64 cannot represent non-string value: {repr(value)}"
|
||||||
|
)
|
||||||
|
value = value.encode("utf-8")
|
||||||
|
try:
|
||||||
|
return b64decode(value, validate=True).decode("utf-8")
|
||||||
|
except _Error:
|
||||||
|
raise GraphQLError(f"Base64 cannot decode value: {repr(value)}")
|
|
@ -25,7 +25,7 @@ class Date(Scalar):
|
||||||
return date.isoformat()
|
return date.isoformat()
|
||||||
|
|
||||||
@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"Date cannot represent non-string value: {print_ast(node)}"
|
f"Date cannot represent non-string value: {print_ast(node)}"
|
||||||
|
@ -58,7 +58,7 @@ class DateTime(Scalar):
|
||||||
return dt.isoformat()
|
return dt.isoformat()
|
||||||
|
|
||||||
@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"DateTime cannot represent non-string value: {print_ast(node)}"
|
f"DateTime cannot represent non-string value: {print_ast(node)}"
|
||||||
|
@ -93,7 +93,7 @@ class Time(Scalar):
|
||||||
return time.isoformat()
|
return time.isoformat()
|
||||||
|
|
||||||
@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"Time cannot represent non-string value: {print_ast(node)}"
|
f"Time cannot represent non-string value: {print_ast(node)}"
|
||||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import absolute_import
|
||||||
|
|
||||||
from decimal import Decimal as _Decimal
|
from decimal import Decimal as _Decimal
|
||||||
|
|
||||||
from graphql.language.ast import StringValueNode
|
from graphql.language.ast import StringValueNode, IntValueNode
|
||||||
|
|
||||||
from .scalars import Scalar
|
from .scalars import Scalar
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ 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):
|
if isinstance(node, (StringValueNode, IntValueNode)):
|
||||||
return cls.parse_value(node.value)
|
return cls.parse_value(node.value)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from enum import Enum as PyEnum
|
||||||
|
|
||||||
from graphql import (
|
from graphql import (
|
||||||
GraphQLEnumType,
|
GraphQLEnumType,
|
||||||
GraphQLInputObjectType,
|
GraphQLInputObjectType,
|
||||||
|
@ -36,7 +38,19 @@ class GrapheneScalarType(GrapheneGraphQLType, GraphQLScalarType):
|
||||||
|
|
||||||
|
|
||||||
class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType):
|
class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType):
|
||||||
pass
|
def serialize(self, value):
|
||||||
|
if not isinstance(value, PyEnum):
|
||||||
|
enum = self.graphene_type._meta.enum
|
||||||
|
try:
|
||||||
|
# Try and get enum by value
|
||||||
|
value = enum(value)
|
||||||
|
except ValueError:
|
||||||
|
# Try and get enum by name
|
||||||
|
try:
|
||||||
|
value = enum[value]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return super(GrapheneEnumType, self).serialize(value)
|
||||||
|
|
||||||
|
|
||||||
class GrapheneInputObjectType(GrapheneGraphQLType, GraphQLInputObjectType):
|
class GrapheneInputObjectType(GrapheneGraphQLType, GraphQLInputObjectType):
|
||||||
|
|
|
@ -10,10 +10,10 @@ class Dynamic(MountedType):
|
||||||
the schema. So we can have lazy fields.
|
the schema. So we can have lazy fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, type, with_schema=False, _creation_counter=None):
|
def __init__(self, type_, with_schema=False, _creation_counter=None):
|
||||||
super(Dynamic, self).__init__(_creation_counter=_creation_counter)
|
super(Dynamic, self).__init__(_creation_counter=_creation_counter)
|
||||||
assert inspect.isfunction(type) or isinstance(type, partial)
|
assert inspect.isfunction(type_) or isinstance(type_, partial)
|
||||||
self.type = type
|
self.type = type_
|
||||||
self.with_schema = with_schema
|
self.with_schema = with_schema
|
||||||
|
|
||||||
def get_type(self, schema=None):
|
def get_type(self, schema=None):
|
||||||
|
|
|
@ -21,14 +21,14 @@ class EnumOptions(BaseOptions):
|
||||||
|
|
||||||
|
|
||||||
class EnumMeta(SubclassWithMeta_Meta):
|
class EnumMeta(SubclassWithMeta_Meta):
|
||||||
def __new__(cls, name, bases, classdict, **options):
|
def __new__(cls, name_, bases, classdict, **options):
|
||||||
enum_members = dict(classdict, __eq__=eq_enum)
|
enum_members = dict(classdict, __eq__=eq_enum)
|
||||||
# We remove the Meta attribute from the class to not collide
|
# We remove the Meta attribute from the class to not collide
|
||||||
# with the enum values.
|
# with the enum values.
|
||||||
enum_members.pop("Meta", None)
|
enum_members.pop("Meta", None)
|
||||||
enum = PyEnum(cls.__name__, enum_members)
|
enum = PyEnum(cls.__name__, enum_members)
|
||||||
return SubclassWithMeta_Meta.__new__(
|
return SubclassWithMeta_Meta.__new__(
|
||||||
cls, name, bases, dict(classdict, __enum__=enum), **options
|
cls, name_, bases, dict(classdict, __enum__=enum), **options
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(cls, value):
|
def get(cls, 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):
|
||||||
|
|
|
@ -8,6 +8,7 @@ from .resolver import default_resolver
|
||||||
from .structures import NonNull
|
from .structures import NonNull
|
||||||
from .unmountedtype import UnmountedType
|
from .unmountedtype import UnmountedType
|
||||||
from .utils import get_type
|
from .utils import get_type
|
||||||
|
from ..utils.deprecated import warn_deprecation
|
||||||
|
|
||||||
base_type = type
|
base_type = type
|
||||||
|
|
||||||
|
@ -64,7 +65,7 @@ class Field(MountedType):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
type,
|
type_,
|
||||||
args=None,
|
args=None,
|
||||||
resolver=None,
|
resolver=None,
|
||||||
source=None,
|
source=None,
|
||||||
|
@ -88,7 +89,7 @@ class Field(MountedType):
|
||||||
), f'The default value can not be a function but received "{base_type(default_value)}".'
|
), f'The default value can not be a function but received "{base_type(default_value)}".'
|
||||||
|
|
||||||
if required:
|
if required:
|
||||||
type = NonNull(type)
|
type_ = NonNull(type_)
|
||||||
|
|
||||||
# Check if name is actually an argument of the field
|
# Check if name is actually an argument of the field
|
||||||
if isinstance(name, (Argument, UnmountedType)):
|
if isinstance(name, (Argument, UnmountedType)):
|
||||||
|
@ -101,7 +102,7 @@ class Field(MountedType):
|
||||||
source = None
|
source = None
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self._type = type
|
self._type = type_
|
||||||
self.args = to_arguments(args or {}, extra_args)
|
self.args = to_arguments(args or {}, extra_args)
|
||||||
if source:
|
if source:
|
||||||
resolver = partial(source_resolver, source)
|
resolver = partial(source_resolver, source)
|
||||||
|
@ -114,5 +115,24 @@ class Field(MountedType):
|
||||||
def type(self):
|
def type(self):
|
||||||
return get_type(self._type)
|
return get_type(self._type)
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
get_resolver = None
|
||||||
|
|
||||||
|
def wrap_resolve(self, parent_resolver):
|
||||||
|
"""
|
||||||
|
Wraps a function resolver, using the ObjectType resolve_{FIELD_NAME}
|
||||||
|
(parent_resolver) if the Field definition has no resolver.
|
||||||
|
"""
|
||||||
|
if self.get_resolver is not None:
|
||||||
|
warn_deprecation(
|
||||||
|
"The get_resolver method is being deprecated, please rename it to wrap_resolve."
|
||||||
|
)
|
||||||
|
return self.get_resolver(parent_resolver)
|
||||||
|
|
||||||
return self.resolver or parent_resolver
|
return self.resolver or parent_resolver
|
||||||
|
|
||||||
|
def wrap_subscribe(self, parent_subscribe):
|
||||||
|
"""
|
||||||
|
Wraps a function subscribe, using the ObjectType subscribe_{FIELD_NAME}
|
||||||
|
(parent_subscribe) if the Field definition has no subscribe.
|
||||||
|
"""
|
||||||
|
return parent_subscribe
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -48,7 +48,7 @@ class InputField(MountedType):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
type,
|
type_,
|
||||||
name=None,
|
name=None,
|
||||||
default_value=Undefined,
|
default_value=Undefined,
|
||||||
deprecation_reason=None,
|
deprecation_reason=None,
|
||||||
|
@ -60,8 +60,8 @@ class InputField(MountedType):
|
||||||
super(InputField, self).__init__(_creation_counter=_creation_counter)
|
super(InputField, self).__init__(_creation_counter=_creation_counter)
|
||||||
self.name = name
|
self.name = name
|
||||||
if required:
|
if required:
|
||||||
type = NonNull(type)
|
type_ = NonNull(type_)
|
||||||
self._type = type
|
self._type = type_
|
||||||
self.deprecation_reason = deprecation_reason
|
self.deprecation_reason = deprecation_reason
|
||||||
self.default_value = default_value
|
self.default_value = default_value
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -20,7 +20,7 @@ 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)
|
return json.loads(node.value)
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,6 @@ class Mutation(ObjectType):
|
||||||
):
|
):
|
||||||
if not _meta:
|
if not _meta:
|
||||||
_meta = MutationOptions(cls)
|
_meta = MutationOptions(cls)
|
||||||
|
|
||||||
output = output or getattr(cls, "Output", None)
|
output = output or getattr(cls, "Output", None)
|
||||||
fields = {}
|
fields = {}
|
||||||
|
|
||||||
|
@ -85,43 +84,32 @@ class Mutation(ObjectType):
|
||||||
interface, Interface
|
interface, Interface
|
||||||
), f'All interfaces of {cls.__name__} must be a subclass of Interface. Received "{interface}".'
|
), f'All interfaces of {cls.__name__} must be a subclass of Interface. Received "{interface}".'
|
||||||
fields.update(interface._meta.fields)
|
fields.update(interface._meta.fields)
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
# If output is defined, we don't need to get the fields
|
# If output is defined, we don't need to get the fields
|
||||||
fields = {}
|
fields = {}
|
||||||
for base in reversed(cls.__mro__):
|
for base in reversed(cls.__mro__):
|
||||||
fields.update(yank_fields_from_attrs(base.__dict__, _as=Field))
|
fields.update(yank_fields_from_attrs(base.__dict__, _as=Field))
|
||||||
output = cls
|
output = cls
|
||||||
|
|
||||||
if not arguments:
|
if not arguments:
|
||||||
input_class = getattr(cls, "Arguments", None)
|
input_class = getattr(cls, "Arguments", None)
|
||||||
if not input_class:
|
if not input_class:
|
||||||
input_class = getattr(cls, "Input", None)
|
input_class = getattr(cls, "Input", None)
|
||||||
if input_class:
|
if input_class:
|
||||||
warn_deprecation(
|
warn_deprecation(
|
||||||
(
|
f"Please use {cls.__name__}.Arguments instead of {cls.__name__}.Input."
|
||||||
f"Please use {cls.__name__}.Arguments instead of {cls.__name__}.Input."
|
" Input is now only used in ClientMutationID.\n"
|
||||||
" Input is now only used in ClientMutationID.\n"
|
"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"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
arguments = props(input_class) if input_class else {}
|
||||||
if input_class:
|
|
||||||
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"
|
||||||
resolver = get_unbound_function(mutate)
|
resolver = get_unbound_function(mutate)
|
||||||
|
|
||||||
if _meta.fields:
|
if _meta.fields:
|
||||||
_meta.fields.update(fields)
|
_meta.fields.update(fields)
|
||||||
else:
|
else:
|
||||||
_meta.fields = fields
|
_meta.fields = fields
|
||||||
|
|
||||||
_meta.interfaces = interfaces
|
_meta.interfaces = interfaces
|
||||||
_meta.output = output
|
_meta.output = output
|
||||||
_meta.resolver = resolver
|
_meta.resolver = resolver
|
||||||
|
@ -133,7 +121,7 @@ class Mutation(ObjectType):
|
||||||
def Field(
|
def Field(
|
||||||
cls, name=None, description=None, deprecation_reason=None, required=False
|
cls, name=None, description=None, deprecation_reason=None, required=False
|
||||||
):
|
):
|
||||||
""" Mount instance of mutation Field. """
|
"""Mount instance of mutation Field."""
|
||||||
return Field(
|
return Field(
|
||||||
cls._meta.output,
|
cls._meta.output,
|
||||||
args=cls._meta.arguments,
|
args=cls._meta.arguments,
|
||||||
|
|
|
@ -7,7 +7,6 @@ try:
|
||||||
from dataclasses import make_dataclass, field
|
from dataclasses import make_dataclass, field
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from ..pyutils.dataclasses import make_dataclass, field # type: ignore
|
from ..pyutils.dataclasses import make_dataclass, field # type: ignore
|
||||||
|
|
||||||
# For static type checking with Mypy
|
# For static type checking with Mypy
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
|
@ -20,12 +19,16 @@ class ObjectTypeOptions(BaseOptions):
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeMeta(BaseTypeMeta):
|
class ObjectTypeMeta(BaseTypeMeta):
|
||||||
def __new__(cls, name, bases, namespace):
|
def __new__(cls, name_, bases, namespace, **options):
|
||||||
|
# Note: it's safe to pass options as keyword arguments as they are still type-checked by ObjectTypeOptions.
|
||||||
|
|
||||||
# We create this type, to then overload it with the dataclass attrs
|
# We create this type, to then overload it with the dataclass attrs
|
||||||
class InterObjectType:
|
class InterObjectType:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
base_cls = super().__new__(cls, name, (InterObjectType,) + bases, namespace)
|
base_cls = super().__new__(
|
||||||
|
cls, name_, (InterObjectType,) + bases, namespace, **options
|
||||||
|
)
|
||||||
if base_cls._meta:
|
if base_cls._meta:
|
||||||
fields = [
|
fields = [
|
||||||
(
|
(
|
||||||
|
@ -39,7 +42,7 @@ class ObjectTypeMeta(BaseTypeMeta):
|
||||||
)
|
)
|
||||||
for key, field_value in base_cls._meta.fields.items()
|
for key, field_value in base_cls._meta.fields.items()
|
||||||
]
|
]
|
||||||
dataclass = make_dataclass(name, fields, bases=())
|
dataclass = make_dataclass(name_, fields, bases=())
|
||||||
InterObjectType.__init__ = dataclass.__init__
|
InterObjectType.__init__ = dataclass.__init__
|
||||||
InterObjectType.__eq__ = dataclass.__eq__
|
InterObjectType.__eq__ = dataclass.__eq__
|
||||||
InterObjectType.__repr__ = dataclass.__repr__
|
InterObjectType.__repr__ = dataclass.__repr__
|
||||||
|
@ -62,7 +65,7 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta):
|
||||||
Methods starting with ``resolve_<field_name>`` are bound as resolvers of the matching Field
|
Methods starting with ``resolve_<field_name>`` are bound as resolvers of the matching Field
|
||||||
name. If no resolver is provided, the default resolver is used.
|
name. If no resolver is provided, the default resolver is used.
|
||||||
|
|
||||||
Ambiguous types with Interface and Union can be determined through``is_type_of`` method and
|
Ambiguous types with Interface and Union can be determined through ``is_type_of`` method and
|
||||||
``Meta.possible_types`` attribute.
|
``Meta.possible_types`` attribute.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
@ -129,7 +132,6 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta):
|
||||||
):
|
):
|
||||||
if not _meta:
|
if not _meta:
|
||||||
_meta = ObjectTypeOptions(cls)
|
_meta = ObjectTypeOptions(cls)
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
|
|
||||||
for interface in interfaces:
|
for interface in interfaces:
|
||||||
|
@ -137,10 +139,8 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta):
|
||||||
interface, Interface
|
interface, Interface
|
||||||
), f'All interfaces of {cls.__name__} must be a subclass of Interface. Received "{interface}".'
|
), f'All interfaces of {cls.__name__} must be a subclass of Interface. Received "{interface}".'
|
||||||
fields.update(interface._meta.fields)
|
fields.update(interface._meta.fields)
|
||||||
|
|
||||||
for base in reversed(cls.__mro__):
|
for base in reversed(cls.__mro__):
|
||||||
fields.update(yank_fields_from_attrs(base.__dict__, _as=Field))
|
fields.update(yank_fields_from_attrs(base.__dict__, _as=Field))
|
||||||
|
|
||||||
assert not (possible_types and cls.is_type_of), (
|
assert not (possible_types and cls.is_type_of), (
|
||||||
f"{cls.__name__}.Meta.possible_types will cause type collision with {cls.__name__}.is_type_of. "
|
f"{cls.__name__}.Meta.possible_types will cause type collision with {cls.__name__}.is_type_of. "
|
||||||
"Please use one or other."
|
"Please use one or other."
|
||||||
|
@ -150,7 +150,6 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta):
|
||||||
_meta.fields.update(fields)
|
_meta.fields.update(fields)
|
||||||
else:
|
else:
|
||||||
_meta.fields = fields
|
_meta.fields = fields
|
||||||
|
|
||||||
if not _meta.interfaces:
|
if not _meta.interfaces:
|
||||||
_meta.interfaces = interfaces
|
_meta.interfaces = interfaces
|
||||||
_meta.possible_types = possible_types
|
_meta.possible_types = possible_types
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -75,13 +75,40 @@ class Int(Scalar):
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
class BigInt(Scalar):
|
||||||
|
"""
|
||||||
|
The `BigInt` scalar type represents non-fractional whole numeric values.
|
||||||
|
`BigInt` is not constrained to 32-bit like the `Int` type and thus is a less
|
||||||
|
compatible type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def coerce_int(value):
|
||||||
|
try:
|
||||||
|
num = int(value)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
num = int(float(value))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return num
|
||||||
|
|
||||||
|
serialize = coerce_int
|
||||||
|
parse_value = coerce_int
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_literal(ast, _variables=None):
|
||||||
|
if isinstance(ast, IntValueNode):
|
||||||
|
return int(ast.value)
|
||||||
|
|
||||||
|
|
||||||
class Float(Scalar):
|
class Float(Scalar):
|
||||||
"""
|
"""
|
||||||
The `Float` scalar type represents signed double-precision fractional
|
The `Float` scalar type represents signed double-precision fractional
|
||||||
|
@ -101,7 +128,7 @@ class Float(Scalar):
|
||||||
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)
|
||||||
|
|
||||||
|
@ -116,14 +143,14 @@ 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
|
||||||
|
|
||||||
|
@ -137,7 +164,7 @@ 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
|
||||||
|
|
||||||
|
@ -155,6 +182,6 @@ 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
|
||||||
|
|
|
@ -10,8 +10,11 @@ from graphql import (
|
||||||
parse,
|
parse,
|
||||||
print_schema,
|
print_schema,
|
||||||
subscribe,
|
subscribe,
|
||||||
|
validate,
|
||||||
|
ExecutionResult,
|
||||||
GraphQLArgument,
|
GraphQLArgument,
|
||||||
GraphQLBoolean,
|
GraphQLBoolean,
|
||||||
|
GraphQLError,
|
||||||
GraphQLEnumValue,
|
GraphQLEnumValue,
|
||||||
GraphQLField,
|
GraphQLField,
|
||||||
GraphQLFloat,
|
GraphQLFloat,
|
||||||
|
@ -23,7 +26,6 @@ from graphql import (
|
||||||
GraphQLObjectType,
|
GraphQLObjectType,
|
||||||
GraphQLSchema,
|
GraphQLSchema,
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
Undefined,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utils.str_converters import to_camel_case
|
from ..utils.str_converters import to_camel_case
|
||||||
|
@ -76,6 +78,11 @@ def is_type_of_from_possible_types(possible_types, root, _info):
|
||||||
return isinstance(root, possible_types)
|
return isinstance(root, possible_types)
|
||||||
|
|
||||||
|
|
||||||
|
# We use this resolver for subscriptions
|
||||||
|
def identity_resolve(root, info, **arguments):
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
class TypeMap(dict):
|
class TypeMap(dict):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -172,7 +179,7 @@ class TypeMap(dict):
|
||||||
deprecation_reason = graphene_type._meta.deprecation_reason(value)
|
deprecation_reason = graphene_type._meta.deprecation_reason(value)
|
||||||
|
|
||||||
values[name] = GraphQLEnumValue(
|
values[name] = GraphQLEnumValue(
|
||||||
value=value.value,
|
value=value,
|
||||||
description=description,
|
description=description,
|
||||||
deprecation_reason=deprecation_reason,
|
deprecation_reason=deprecation_reason,
|
||||||
)
|
)
|
||||||
|
@ -226,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -303,26 +319,41 @@ class TypeMap(dict):
|
||||||
arg_type,
|
arg_type,
|
||||||
out_name=arg_name,
|
out_name=arg_name,
|
||||||
description=arg.description,
|
description=arg.description,
|
||||||
default_value=Undefined
|
default_value=arg.default_value,
|
||||||
if isinstance(arg.type, NonNull)
|
|
||||||
else arg.default_value,
|
|
||||||
)
|
)
|
||||||
|
subscribe = field.wrap_subscribe(
|
||||||
|
self.get_function_for_type(
|
||||||
|
graphene_type, f"subscribe_{name}", name, field.default_value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we are in a subscription, we use (by default) an
|
||||||
|
# identity-based resolver for the root, rather than the
|
||||||
|
# default resolver for objects/dicts.
|
||||||
|
if subscribe:
|
||||||
|
field_default_resolver = identity_resolve
|
||||||
|
elif issubclass(graphene_type, ObjectType):
|
||||||
|
default_resolver = (
|
||||||
|
graphene_type._meta.default_resolver or get_default_resolver()
|
||||||
|
)
|
||||||
|
field_default_resolver = partial(
|
||||||
|
default_resolver, name, field.default_value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
field_default_resolver = None
|
||||||
|
|
||||||
|
resolve = field.wrap_resolve(
|
||||||
|
self.get_function_for_type(
|
||||||
|
graphene_type, f"resolve_{name}", name, field.default_value
|
||||||
|
)
|
||||||
|
or field_default_resolver
|
||||||
|
)
|
||||||
|
|
||||||
_field = GraphQLField(
|
_field = GraphQLField(
|
||||||
field_type,
|
field_type,
|
||||||
args=args,
|
args=args,
|
||||||
resolve=field.get_resolver(
|
resolve=resolve,
|
||||||
self.get_resolver_for_type(
|
subscribe=subscribe,
|
||||||
graphene_type, f"resolve_{name}", name, field.default_value
|
|
||||||
)
|
|
||||||
),
|
|
||||||
subscribe=field.get_resolver(
|
|
||||||
self.get_resolver_for_type(
|
|
||||||
graphene_type,
|
|
||||||
f"subscribe_{name}",
|
|
||||||
name,
|
|
||||||
field.default_value,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
deprecation_reason=field.deprecation_reason,
|
deprecation_reason=field.deprecation_reason,
|
||||||
description=field.description,
|
description=field.description,
|
||||||
)
|
)
|
||||||
|
@ -330,7 +361,8 @@ class TypeMap(dict):
|
||||||
fields[field_name] = _field
|
fields[field_name] = _field
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_resolver_for_type(self, graphene_type, func_name, name, default_value):
|
def get_function_for_type(self, graphene_type, func_name, name, default_value):
|
||||||
|
"""Gets a resolve or subscribe function for a given ObjectType"""
|
||||||
if not issubclass(graphene_type, ObjectType):
|
if not issubclass(graphene_type, ObjectType):
|
||||||
return
|
return
|
||||||
resolver = getattr(graphene_type, func_name, None)
|
resolver = getattr(graphene_type, func_name, None)
|
||||||
|
@ -350,36 +382,21 @@ class TypeMap(dict):
|
||||||
if resolver:
|
if resolver:
|
||||||
return get_unbound_function(resolver)
|
return get_unbound_function(resolver)
|
||||||
|
|
||||||
default_resolver = (
|
|
||||||
graphene_type._meta.default_resolver or get_default_resolver()
|
|
||||||
)
|
|
||||||
return partial(default_resolver, name, default_value)
|
|
||||||
|
|
||||||
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:
|
||||||
"""Schema Definition.
|
"""Schema Definition.
|
||||||
|
|
||||||
A Graphene Schema can execute operations (query, mutation, subscription) against the defined
|
A Graphene Schema can execute operations (query, mutation, subscription) against the defined
|
||||||
types. For advanced purposes, the schema can be used to lookup type definitions and answer
|
types. For advanced purposes, the schema can be used to lookup type definitions and answer
|
||||||
questions about the types through introspection.
|
questions about the types through introspection.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read*
|
query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read*
|
||||||
data in your Schema.
|
data in your Schema.
|
||||||
|
@ -426,7 +443,6 @@ class Schema:
|
||||||
"""
|
"""
|
||||||
This function let the developer select a type in a given schema
|
This function let the developer select a type in a given schema
|
||||||
by accessing its attrs.
|
by accessing its attrs.
|
||||||
|
|
||||||
Example: using schema.Query for accessing the "Query" type in the Schema
|
Example: using schema.Query for accessing the "Query" type in the Schema
|
||||||
"""
|
"""
|
||||||
_type = self.graphql_schema.get_type(type_name)
|
_type = self.graphql_schema.get_type(type_name)
|
||||||
|
@ -441,11 +457,9 @@ class Schema:
|
||||||
|
|
||||||
def execute(self, *args, **kwargs):
|
def execute(self, *args, **kwargs):
|
||||||
"""Execute a GraphQL query on the schema.
|
"""Execute a GraphQL query on the schema.
|
||||||
|
|
||||||
Use the `graphql_sync` function from `graphql-core` to provide the result
|
Use the `graphql_sync` function from `graphql-core` to provide the result
|
||||||
for a query string. Most of the time this method will be called by one of the Graphene
|
for a query string. Most of the time this method will be called by one of the Graphene
|
||||||
:ref:`Integrations` via a web request.
|
:ref:`Integrations` via a web request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request_string (str or Document): GraphQL request (query, mutation or subscription)
|
request_string (str or Document): GraphQL request (query, mutation or subscription)
|
||||||
as string or parsed AST form from `graphql-core`.
|
as string or parsed AST form from `graphql-core`.
|
||||||
|
@ -460,7 +474,8 @@ class Schema:
|
||||||
request_string, an operation name must be provided for the result to be provided.
|
request_string, an operation name must be provided for the result to be provided.
|
||||||
middleware (List[SupportsGraphQLMiddleware]): Supply request level middleware as
|
middleware (List[SupportsGraphQLMiddleware]): Supply request level middleware as
|
||||||
defined in `graphql-core`.
|
defined in `graphql-core`.
|
||||||
|
execution_context_class (ExecutionContext, optional): The execution context class
|
||||||
|
to use when resolving queries and mutations.
|
||||||
Returns:
|
Returns:
|
||||||
:obj:`ExecutionResult` containing any data and errors for the operation.
|
:obj:`ExecutionResult` containing any data and errors for the operation.
|
||||||
"""
|
"""
|
||||||
|
@ -469,14 +484,25 @@ class Schema:
|
||||||
|
|
||||||
async def execute_async(self, *args, **kwargs):
|
async def execute_async(self, *args, **kwargs):
|
||||||
"""Execute a GraphQL query on the schema asynchronously.
|
"""Execute a GraphQL query on the schema asynchronously.
|
||||||
|
|
||||||
Same as `execute`, but uses `graphql` instead of `graphql_sync`.
|
Same as `execute`, but uses `graphql` instead of `graphql_sync`.
|
||||||
"""
|
"""
|
||||||
kwargs = normalize_execute_kwargs(kwargs)
|
kwargs = normalize_execute_kwargs(kwargs)
|
||||||
return await graphql(self.graphql_schema, *args, **kwargs)
|
return await graphql(self.graphql_schema, *args, **kwargs)
|
||||||
|
|
||||||
async def subscribe(self, query, *args, **kwargs):
|
async def subscribe(self, query, *args, **kwargs):
|
||||||
document = parse(query)
|
"""Execute a GraphQL subscription on the schema asynchronously."""
|
||||||
|
# Do parsing
|
||||||
|
try:
|
||||||
|
document = parse(query)
|
||||||
|
except GraphQLError as error:
|
||||||
|
return ExecutionResult(data=None, errors=[error])
|
||||||
|
|
||||||
|
# Do validation
|
||||||
|
validation_errors = validate(self.graphql_schema, document)
|
||||||
|
if validation_errors:
|
||||||
|
return ExecutionResult(data=None, errors=validation_errors)
|
||||||
|
|
||||||
|
# Execute the query
|
||||||
kwargs = normalize_execute_kwargs(kwargs)
|
kwargs = normalize_execute_kwargs(kwargs)
|
||||||
return await subscribe(self.graphql_schema, document, *args, **kwargs)
|
return await subscribe(self.graphql_schema, document, *args, **kwargs)
|
||||||
|
|
||||||
|
|
97
graphene/types/tests/test_base64.py
Normal file
97
graphene/types/tests/test_base64.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from graphql import GraphQLError
|
||||||
|
|
||||||
|
from ..objecttype import ObjectType
|
||||||
|
from ..scalars import String
|
||||||
|
from ..schema import Schema
|
||||||
|
from ..base64 import Base64
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
base64 = Base64(_in=Base64(name="input"), _match=String(name="match"))
|
||||||
|
bytes_as_base64 = Base64()
|
||||||
|
string_as_base64 = Base64()
|
||||||
|
number_as_base64 = Base64()
|
||||||
|
|
||||||
|
def resolve_base64(self, info, _in=None, _match=None):
|
||||||
|
if _match:
|
||||||
|
assert _in == _match
|
||||||
|
return _in
|
||||||
|
|
||||||
|
def resolve_bytes_as_base64(self, info):
|
||||||
|
return b"Hello world"
|
||||||
|
|
||||||
|
def resolve_string_as_base64(self, info):
|
||||||
|
return "Spam and eggs"
|
||||||
|
|
||||||
|
def resolve_number_as_base64(self, info):
|
||||||
|
return 42
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_query():
|
||||||
|
base64_value = base64.b64encode(b"Random string").decode("utf-8")
|
||||||
|
result = schema.execute(
|
||||||
|
"""{{ base64(input: "{}", match: "Random string") }}""".format(base64_value)
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"base64": base64_value}
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_query_with_variable():
|
||||||
|
base64_value = base64.b64encode(b"Another string").decode("utf-8")
|
||||||
|
|
||||||
|
# test datetime variable in string representation
|
||||||
|
result = schema.execute(
|
||||||
|
"""
|
||||||
|
query GetBase64($base64: Base64) {
|
||||||
|
base64(input: $base64, match: "Another string")
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variables={"base64": base64_value},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"base64": base64_value}
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_query_none():
|
||||||
|
result = schema.execute("""{ base64 }""")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"base64": None}
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_query_invalid():
|
||||||
|
bad_inputs = [dict(), 123, "This is not valid base64"]
|
||||||
|
|
||||||
|
for input_ in bad_inputs:
|
||||||
|
result = schema.execute(
|
||||||
|
"""{ base64(input: $input) }""", variables={"input": input_}
|
||||||
|
)
|
||||||
|
assert isinstance(result.errors, list)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert isinstance(result.errors[0], GraphQLError)
|
||||||
|
assert result.data is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_from_bytes():
|
||||||
|
base64_value = base64.b64encode(b"Hello world").decode("utf-8")
|
||||||
|
result = schema.execute("""{ bytesAsBase64 }""")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"bytesAsBase64": base64_value}
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_from_string():
|
||||||
|
base64_value = base64.b64encode(b"Spam and eggs").decode("utf-8")
|
||||||
|
result = schema.execute("""{ stringAsBase64 }""")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"stringAsBase64": base64_value}
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_from_number():
|
||||||
|
base64_value = base64.b64encode(b"42").decode("utf-8")
|
||||||
|
result = schema.execute("""{ numberAsBase64 }""")
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"numberAsBase64": base64_value}
|
|
@ -60,6 +60,23 @@ def test_datetime_query(sample_datetime):
|
||||||
assert result.data == {"datetime": isoformat}
|
assert result.data == {"datetime": isoformat}
|
||||||
|
|
||||||
|
|
||||||
|
def test_datetime_query_with_variables(sample_datetime):
|
||||||
|
isoformat = sample_datetime.isoformat()
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
"""
|
||||||
|
query GetDate($datetime: DateTime) {
|
||||||
|
literal: datetime(in: "%s")
|
||||||
|
value: datetime(in: $datetime)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
% isoformat,
|
||||||
|
variable_values={"datetime": isoformat},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"literal": isoformat, "value": isoformat}
|
||||||
|
|
||||||
|
|
||||||
def test_date_query(sample_date):
|
def test_date_query(sample_date):
|
||||||
isoformat = sample_date.isoformat()
|
isoformat = sample_date.isoformat()
|
||||||
|
|
||||||
|
@ -68,6 +85,23 @@ def test_date_query(sample_date):
|
||||||
assert result.data == {"date": isoformat}
|
assert result.data == {"date": isoformat}
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_query_with_variables(sample_date):
|
||||||
|
isoformat = sample_date.isoformat()
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
"""
|
||||||
|
query GetDate($date: Date) {
|
||||||
|
literal: date(in: "%s")
|
||||||
|
value: date(in: $date)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
% isoformat,
|
||||||
|
variable_values={"date": isoformat},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"literal": isoformat, "value": isoformat}
|
||||||
|
|
||||||
|
|
||||||
def test_time_query(sample_time):
|
def test_time_query(sample_time):
|
||||||
isoformat = sample_time.isoformat()
|
isoformat = sample_time.isoformat()
|
||||||
|
|
||||||
|
@ -76,6 +110,23 @@ def test_time_query(sample_time):
|
||||||
assert result.data == {"time": isoformat}
|
assert result.data == {"time": isoformat}
|
||||||
|
|
||||||
|
|
||||||
|
def test_time_query_with_variables(sample_time):
|
||||||
|
isoformat = sample_time.isoformat()
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
"""
|
||||||
|
query GetTime($time: Time) {
|
||||||
|
literal: time(at: "%s")
|
||||||
|
value: time(at: $time)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
% isoformat,
|
||||||
|
variable_values={"time": isoformat},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"literal": isoformat, "value": isoformat}
|
||||||
|
|
||||||
|
|
||||||
def test_bad_datetime_query():
|
def test_bad_datetime_query():
|
||||||
not_a_date = "Some string that's not a datetime"
|
not_a_date = "Some string that's not a datetime"
|
||||||
|
|
||||||
|
|
|
@ -41,3 +41,11 @@ def test_bad_decimal_query():
|
||||||
result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal)
|
result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal)
|
||||||
assert len(result.errors) == 1
|
assert len(result.errors) == 1
|
||||||
assert result.data is None
|
assert result.data is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_decimal_string_query_integer():
|
||||||
|
decimal_value = 1
|
||||||
|
result = schema.execute("""{ decimal(input: %s) }""" % decimal_value)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"decimal": str(decimal_value)}
|
||||||
|
assert decimal.Decimal(result.data["decimal"]) == decimal_value
|
||||||
|
|
|
@ -234,10 +234,10 @@ def test_stringifies_simple_types():
|
||||||
# (InputObjectType, True)
|
# (InputObjectType, True)
|
||||||
# )
|
# )
|
||||||
|
|
||||||
# for type, answer in expected:
|
# for type_, answer in expected:
|
||||||
# assert is_input_type(type) == answer
|
# assert is_input_type(type_) == answer
|
||||||
# assert is_input_type(GraphQLList(type)) == answer
|
# assert is_input_type(GraphQLList(type_)) == answer
|
||||||
# assert is_input_type(GraphQLNonNull(type)) == answer
|
# assert is_input_type(GraphQLNonNull(type_)) == answer
|
||||||
|
|
||||||
|
|
||||||
# def test_identifies_output_types():
|
# def test_identifies_output_types():
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
from ..argument import Argument
|
from ..argument import Argument
|
||||||
from ..enum import Enum, PyEnum
|
from ..enum import Enum, PyEnum
|
||||||
from ..field import Field
|
from ..field import Field
|
||||||
from ..inputfield import InputField
|
from ..inputfield import InputField
|
||||||
|
from ..inputobjecttype import InputObjectType
|
||||||
|
from ..mutation import Mutation
|
||||||
|
from ..scalars import String
|
||||||
from ..schema import ObjectType, Schema
|
from ..schema import ObjectType, Schema
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,8 +26,8 @@ def test_enum_construction():
|
||||||
assert RGB._meta.description == "Description"
|
assert RGB._meta.description == "Description"
|
||||||
|
|
||||||
values = RGB._meta.enum.__members__.values()
|
values = RGB._meta.enum.__members__.values()
|
||||||
assert sorted([v.name for v in values]) == ["BLUE", "GREEN", "RED"]
|
assert sorted(v.name for v in values) == ["BLUE", "GREEN", "RED"]
|
||||||
assert sorted([v.description for v in values]) == [
|
assert sorted(v.description for v in values) == [
|
||||||
"Description BLUE",
|
"Description BLUE",
|
||||||
"Description GREEN",
|
"Description GREEN",
|
||||||
"Description RED",
|
"Description RED",
|
||||||
|
@ -47,7 +52,7 @@ def test_enum_instance_construction():
|
||||||
RGB = Enum("RGB", "RED,GREEN,BLUE")
|
RGB = Enum("RGB", "RED,GREEN,BLUE")
|
||||||
|
|
||||||
values = RGB._meta.enum.__members__.values()
|
values = RGB._meta.enum.__members__.values()
|
||||||
assert sorted([v.name for v in values]) == ["BLUE", "GREEN", "RED"]
|
assert sorted(v.name for v in values) == ["BLUE", "GREEN", "RED"]
|
||||||
|
|
||||||
|
|
||||||
def test_enum_from_builtin_enum():
|
def test_enum_from_builtin_enum():
|
||||||
|
@ -224,3 +229,292 @@ def test_enum_skip_meta_from_members():
|
||||||
"GREEN": RGB1.GREEN,
|
"GREEN": RGB1.GREEN,
|
||||||
"BLUE": RGB1.BLUE,
|
"BLUE": RGB1.BLUE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_types():
|
||||||
|
from enum import Enum as PyEnum
|
||||||
|
|
||||||
|
class Color(PyEnum):
|
||||||
|
"""Primary colors"""
|
||||||
|
|
||||||
|
RED = 1
|
||||||
|
YELLOW = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
GColor = Enum.from_enum(Color)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
color = GColor(required=True)
|
||||||
|
|
||||||
|
def resolve_color(_, info):
|
||||||
|
return Color.RED
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
str(schema).strip()
|
||||||
|
== dedent(
|
||||||
|
'''
|
||||||
|
type Query {
|
||||||
|
color: Color!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""Primary colors"""
|
||||||
|
enum Color {
|
||||||
|
RED
|
||||||
|
YELLOW
|
||||||
|
BLUE
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
).strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_resolver():
|
||||||
|
from enum import Enum as PyEnum
|
||||||
|
|
||||||
|
class Color(PyEnum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
GColor = Enum.from_enum(Color)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
color = GColor(required=True)
|
||||||
|
|
||||||
|
def resolve_color(_, info):
|
||||||
|
return Color.RED
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("query { color }")
|
||||||
|
assert not results.errors
|
||||||
|
|
||||||
|
assert results.data["color"] == Color.RED.name
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_resolver_compat():
|
||||||
|
from enum import Enum as PyEnum
|
||||||
|
|
||||||
|
class Color(PyEnum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
GColor = Enum.from_enum(Color)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
color = GColor(required=True)
|
||||||
|
color_by_name = GColor(required=True)
|
||||||
|
|
||||||
|
def resolve_color(_, info):
|
||||||
|
return Color.RED.value
|
||||||
|
|
||||||
|
def resolve_color_by_name(_, info):
|
||||||
|
return Color.RED.name
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute(
|
||||||
|
"""query {
|
||||||
|
color
|
||||||
|
colorByName
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
assert not results.errors
|
||||||
|
|
||||||
|
assert results.data["color"] == 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():
|
||||||
|
from enum import Enum as PyEnum
|
||||||
|
|
||||||
|
class Color(PyEnum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
GColor = Enum.from_enum(Color)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
color = GColor(required=True)
|
||||||
|
|
||||||
|
def resolve_color(_, info):
|
||||||
|
return "BLACK"
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute("query { color }")
|
||||||
|
assert results.errors
|
||||||
|
assert results.errors[0].message == "Enum 'Color' cannot represent value: 'BLACK'"
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_enum_argument():
|
||||||
|
class Color(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
class Brick(ObjectType):
|
||||||
|
color = Color(required=True)
|
||||||
|
|
||||||
|
color_filter = None
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
bricks_by_color = Field(Brick, color=Color(required=True))
|
||||||
|
|
||||||
|
def resolve_bricks_by_color(_, info, color):
|
||||||
|
nonlocal color_filter
|
||||||
|
color_filter = color
|
||||||
|
return Brick(color=color)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
results = schema.execute(
|
||||||
|
"""
|
||||||
|
query {
|
||||||
|
bricksByColor(color: RED) {
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert not results.errors
|
||||||
|
assert results.data == {"bricksByColor": {"color": "RED"}}
|
||||||
|
assert color_filter == Color.RED
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_enum_input():
|
||||||
|
class RGB(Enum):
|
||||||
|
"""Available colors"""
|
||||||
|
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
color_input = None
|
||||||
|
|
||||||
|
class CreatePaint(Mutation):
|
||||||
|
class Arguments:
|
||||||
|
color = RGB(required=True)
|
||||||
|
|
||||||
|
color = RGB(required=True)
|
||||||
|
|
||||||
|
def mutate(_, info, color):
|
||||||
|
nonlocal color_input
|
||||||
|
color_input = color
|
||||||
|
return CreatePaint(color=color)
|
||||||
|
|
||||||
|
class MyMutation(ObjectType):
|
||||||
|
create_paint = CreatePaint.Field()
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
a = String()
|
||||||
|
|
||||||
|
schema = Schema(query=Query, mutation=MyMutation)
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation MyMutation {
|
||||||
|
createPaint(color: RED) {
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"createPaint": {"color": "RED"}}
|
||||||
|
|
||||||
|
assert color_input == RGB.RED
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_enum_input_type():
|
||||||
|
class RGB(Enum):
|
||||||
|
"""Available colors"""
|
||||||
|
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
class ColorInput(InputObjectType):
|
||||||
|
color = RGB(required=True)
|
||||||
|
|
||||||
|
color_input_value = None
|
||||||
|
|
||||||
|
class CreatePaint(Mutation):
|
||||||
|
class Arguments:
|
||||||
|
color_input = ColorInput(required=True)
|
||||||
|
|
||||||
|
color = RGB(required=True)
|
||||||
|
|
||||||
|
def mutate(_, info, color_input):
|
||||||
|
nonlocal color_input_value
|
||||||
|
color_input_value = color_input.color
|
||||||
|
return CreatePaint(color=color_input.color)
|
||||||
|
|
||||||
|
class MyMutation(ObjectType):
|
||||||
|
create_paint = CreatePaint.Field()
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
a = String()
|
||||||
|
|
||||||
|
schema = Schema(query=Query, mutation=MyMutation)
|
||||||
|
result = schema.execute(
|
||||||
|
"""
|
||||||
|
mutation MyMutation {
|
||||||
|
createPaint(colorInput: { color: RED }) {
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"createPaint": {"color": "RED"}}
|
||||||
|
|
||||||
|
assert color_input_value == RGB.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():
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -295,3 +289,21 @@ def test_objecttype_meta_with_annotations():
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
schema = Schema(query=Query)
|
||||||
assert schema is not None
|
assert schema is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_objecttype_meta_arguments():
|
||||||
|
class MyInterface(Interface):
|
||||||
|
foo = String()
|
||||||
|
|
||||||
|
class MyType(ObjectType, interfaces=[MyInterface]):
|
||||||
|
bar = String()
|
||||||
|
|
||||||
|
assert MyType._meta.interfaces == [MyInterface]
|
||||||
|
assert list(MyType._meta.fields.keys()) == ["foo", "bar"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_objecttype_type_name():
|
||||||
|
class MyObjectType(ObjectType, name="FooType"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert MyObjectType._meta.name == "FooType"
|
||||||
|
|
|
@ -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,5 @@
|
||||||
from ..scalars import Scalar
|
from ..scalars import Scalar, Int, BigInt
|
||||||
|
from graphql.language.ast import IntValueNode
|
||||||
|
|
||||||
|
|
||||||
def test_scalar():
|
def test_scalar():
|
||||||
|
@ -7,3 +8,22 @@ def test_scalar():
|
||||||
|
|
||||||
assert JSONScalar._meta.name == "JSONScalar"
|
assert JSONScalar._meta.name == "JSONScalar"
|
||||||
assert JSONScalar._meta.description == "Documentation"
|
assert JSONScalar._meta.description == "Documentation"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ints():
|
||||||
|
assert Int.parse_value(2**31 - 1) is not None
|
||||||
|
assert Int.parse_value("2.0") is not None
|
||||||
|
assert Int.parse_value(2**31) is None
|
||||||
|
|
||||||
|
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_value(-(2**31)) is not None
|
||||||
|
assert Int.parse_value(-(2**31) - 1) is None
|
||||||
|
|
||||||
|
assert BigInt.parse_value(2**31) is not None
|
||||||
|
assert BigInt.parse_value("2.0") is not None
|
||||||
|
assert BigInt.parse_value(-(2**31) - 1) is not None
|
||||||
|
|
||||||
|
assert BigInt.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1
|
||||||
|
assert BigInt.parse_literal(IntValueNode(value=str(2**31))) == 2**31
|
||||||
|
|
|
@ -38,7 +38,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 textwrap import dedent
|
||||||
|
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
from graphql.type import GraphQLObjectType, GraphQLSchema
|
from graphql.type import GraphQLObjectType, GraphQLSchema
|
||||||
from graphql.pyutils import dedent
|
|
||||||
|
|
||||||
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,9 +56,19 @@ def test_schema_str():
|
||||||
field: String
|
field: String
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
).strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_schema_introspect():
|
def test_schema_introspect():
|
||||||
schema = Schema(Query)
|
schema = Schema(Query)
|
||||||
assert "__schema" in schema.introspect()
|
assert "__schema" in schema.introspect()
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_requires_query_type():
|
||||||
|
schema = Schema()
|
||||||
|
result = schema.execute("query {}")
|
||||||
|
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
error = result.errors[0]
|
||||||
|
assert error.message == "Query root type must be provided."
|
||||||
|
|
78
graphene/types/tests/test_subscribe_async.py
Normal file
78
graphene/types/tests/test_subscribe_async.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
from pytest import mark
|
||||||
|
|
||||||
|
from graphene import ObjectType, Int, String, Schema, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
hello = String()
|
||||||
|
|
||||||
|
def resolve_hello(root, info):
|
||||||
|
return "Hello, world!"
|
||||||
|
|
||||||
|
|
||||||
|
class Subscription(ObjectType):
|
||||||
|
count_to_ten = Field(Int)
|
||||||
|
|
||||||
|
async def subscribe_count_to_ten(root, info):
|
||||||
|
for count in range(1, 11):
|
||||||
|
yield count
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=Query, subscription=Subscription)
|
||||||
|
|
||||||
|
|
||||||
|
@mark.asyncio
|
||||||
|
async def test_subscription():
|
||||||
|
subscription = "subscription { countToTen }"
|
||||||
|
result = await schema.subscribe(subscription)
|
||||||
|
count = 0
|
||||||
|
async for item in result:
|
||||||
|
count = item.data["countToTen"]
|
||||||
|
assert count == 10
|
||||||
|
|
||||||
|
|
||||||
|
@mark.asyncio
|
||||||
|
async def test_subscription_fails_with_invalid_query():
|
||||||
|
# It fails if the provided query is invalid
|
||||||
|
subscription = "subscription { "
|
||||||
|
result = await schema.subscribe(subscription)
|
||||||
|
assert not result.data
|
||||||
|
assert result.errors
|
||||||
|
assert "Syntax Error: Expected Name, found <EOF>" in str(result.errors[0])
|
||||||
|
|
||||||
|
|
||||||
|
@mark.asyncio
|
||||||
|
async def test_subscription_fails_when_query_is_not_valid():
|
||||||
|
# It can't subscribe to two fields at the same time, triggering a
|
||||||
|
# validation error.
|
||||||
|
subscription = "subscription { countToTen, b: countToTen }"
|
||||||
|
result = await schema.subscribe(subscription)
|
||||||
|
assert not result.data
|
||||||
|
assert result.errors
|
||||||
|
assert "Anonymous Subscription must select only one top level field." in str(
|
||||||
|
result.errors[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mark.asyncio
|
||||||
|
async def test_subscription_with_args():
|
||||||
|
class Query(ObjectType):
|
||||||
|
hello = String()
|
||||||
|
|
||||||
|
class Subscription(ObjectType):
|
||||||
|
count_upwards = Field(Int, limit=Int(required=True))
|
||||||
|
|
||||||
|
async def subscribe_count_upwards(root, info, limit):
|
||||||
|
count = 0
|
||||||
|
while count < limit:
|
||||||
|
count += 1
|
||||||
|
yield count
|
||||||
|
|
||||||
|
schema = Schema(query=Query, subscription=Subscription)
|
||||||
|
|
||||||
|
subscription = "subscription { countUpwards(limit: 5) }"
|
||||||
|
result = await schema.subscribe(subscription)
|
||||||
|
count = 0
|
||||||
|
async for item in result:
|
||||||
|
count = item.data["countUpwards"]
|
||||||
|
assert count == 5
|
|
@ -1,3 +1,4 @@
|
||||||
|
from graphql import Undefined
|
||||||
from graphql.type import (
|
from graphql.type import (
|
||||||
GraphQLArgument,
|
GraphQLArgument,
|
||||||
GraphQLEnumType,
|
GraphQLEnumType,
|
||||||
|
@ -6,6 +7,7 @@ from graphql.type import (
|
||||||
GraphQLInputField,
|
GraphQLInputField,
|
||||||
GraphQLInputObjectType,
|
GraphQLInputObjectType,
|
||||||
GraphQLInterfaceType,
|
GraphQLInterfaceType,
|
||||||
|
GraphQLNonNull,
|
||||||
GraphQLObjectType,
|
GraphQLObjectType,
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
)
|
)
|
||||||
|
@ -94,6 +96,21 @@ def test_objecttype():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_required_argument_with_default_value():
|
||||||
|
class MyObjectType(ObjectType):
|
||||||
|
foo = String(bar=String(required=True, default_value="x"))
|
||||||
|
|
||||||
|
type_map = create_type_map([MyObjectType])
|
||||||
|
|
||||||
|
graphql_type = type_map["MyObjectType"]
|
||||||
|
foo_field = graphql_type.fields["foo"]
|
||||||
|
|
||||||
|
bar_argument = foo_field.args["bar"]
|
||||||
|
assert bar_argument.default_value == "x"
|
||||||
|
assert isinstance(bar_argument.type, GraphQLNonNull)
|
||||||
|
assert bar_argument.type.of_type == GraphQLString
|
||||||
|
|
||||||
|
|
||||||
def test_dynamic_objecttype():
|
def test_dynamic_objecttype():
|
||||||
class MyObjectType(ObjectType):
|
class MyObjectType(ObjectType):
|
||||||
"""Description"""
|
"""Description"""
|
||||||
|
@ -228,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"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -251,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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,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])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -41,3 +41,10 @@ def get_type(_type):
|
||||||
if inspect.isfunction(_type) or isinstance(_type, partial):
|
if inspect.isfunction(_type) or isinstance(_type, partial):
|
||||||
return _type()
|
return _type()
|
||||||
return _type
|
return _type
|
||||||
|
|
||||||
|
|
||||||
|
def get_underlying_type(_type):
|
||||||
|
"""Get the underlying type even if it is wrapped in structures like NonNull"""
|
||||||
|
while hasattr(_type, "of_type"):
|
||||||
|
_type = _type.of_type
|
||||||
|
return _type
|
||||||
|
|
|
@ -21,7 +21,7 @@ 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)
|
||||||
|
|
||||||
|
|
6
graphene/utils/is_introspection_key.py
Normal file
6
graphene/utils/is_introspection_key.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
def is_introspection_key(key):
|
||||||
|
# from: https://spec.graphql.org/June2018/#sec-Schema
|
||||||
|
# > All types and directives defined within a schema must not have a name which
|
||||||
|
# > begins with "__" (two underscores), as this is used exclusively
|
||||||
|
# > by GraphQL’s introspection system.
|
||||||
|
return str(key).startswith("__")
|
|
@ -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):
|
||||||
|
|
|
@ -36,4 +36,4 @@ class OrderedType:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash((self.creation_counter))
|
return hash(self.creation_counter)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import re
|
import re
|
||||||
from unidecode import unidecode
|
|
||||||
|
|
||||||
|
|
||||||
# Adapted from this response in Stackoverflow
|
# Adapted from this response in Stackoverflow
|
||||||
|
@ -16,7 +15,3 @@ def to_camel_case(snake_str):
|
||||||
def to_snake_case(name):
|
def to_snake_case(name):
|
||||||
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
||||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
||||||
|
|
||||||
|
|
||||||
def to_const(string):
|
|
||||||
return re.sub(r"[\W|^]+", "_", unidecode(string)).upper()
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from ..str_converters import to_camel_case, to_const, to_snake_case
|
from ..str_converters import to_camel_case, to_snake_case
|
||||||
|
|
||||||
|
|
||||||
def test_snake_case():
|
def test_snake_case():
|
||||||
|
@ -17,11 +17,3 @@ def test_camel_case():
|
||||||
assert to_camel_case("snakes_on_a__plane") == "snakesOnA_Plane"
|
assert to_camel_case("snakes_on_a__plane") == "snakesOnA_Plane"
|
||||||
assert to_camel_case("i_phone_hysteria") == "iPhoneHysteria"
|
assert to_camel_case("i_phone_hysteria") == "iPhoneHysteria"
|
||||||
assert to_camel_case("field_i18n") == "fieldI18n"
|
assert to_camel_case("field_i18n") == "fieldI18n"
|
||||||
|
|
||||||
|
|
||||||
def test_to_const():
|
|
||||||
assert to_const('snakes $1. on a "#plane') == "SNAKES_1_ON_A_PLANE"
|
|
||||||
|
|
||||||
|
|
||||||
def test_to_const_unicode():
|
|
||||||
assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF"
|
|
||||||
|
|
5
graphene/validation/__init__.py
Normal file
5
graphene/validation/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .depth_limit import depth_limit_validator
|
||||||
|
from .disable_introspection import DisableIntrospection
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["DisableIntrospection", "depth_limit_validator"]
|
195
graphene/validation/depth_limit.py
Normal file
195
graphene/validation/depth_limit.py
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
# This is a Python port of https://github.com/stems/graphql-depth-limit
|
||||||
|
# which is licensed under the terms of the MIT license, reproduced below.
|
||||||
|
#
|
||||||
|
# -----------
|
||||||
|
#
|
||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# Copyright (c) 2017 Stem
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
try:
|
||||||
|
from re import Pattern
|
||||||
|
except ImportError:
|
||||||
|
# backwards compatibility for v3.6
|
||||||
|
from typing import Pattern
|
||||||
|
from typing import Callable, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from graphql import GraphQLError
|
||||||
|
from graphql.validation import ValidationContext, ValidationRule
|
||||||
|
from graphql.language import (
|
||||||
|
DefinitionNode,
|
||||||
|
FieldNode,
|
||||||
|
FragmentDefinitionNode,
|
||||||
|
FragmentSpreadNode,
|
||||||
|
InlineFragmentNode,
|
||||||
|
Node,
|
||||||
|
OperationDefinitionNode,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..utils.is_introspection_key import is_introspection_key
|
||||||
|
|
||||||
|
|
||||||
|
IgnoreType = Union[Callable[[str], bool], Pattern, str]
|
||||||
|
|
||||||
|
|
||||||
|
def depth_limit_validator(
|
||||||
|
max_depth: int,
|
||||||
|
ignore: Optional[List[IgnoreType]] = None,
|
||||||
|
callback: Callable[[Dict[str, int]], None] = None,
|
||||||
|
):
|
||||||
|
class DepthLimitValidator(ValidationRule):
|
||||||
|
def __init__(self, validation_context: ValidationContext):
|
||||||
|
document = validation_context.document
|
||||||
|
definitions = document.definitions
|
||||||
|
|
||||||
|
fragments = get_fragments(definitions)
|
||||||
|
queries = get_queries_and_mutations(definitions)
|
||||||
|
query_depths = {}
|
||||||
|
|
||||||
|
for name in queries:
|
||||||
|
query_depths[name] = determine_depth(
|
||||||
|
node=queries[name],
|
||||||
|
fragments=fragments,
|
||||||
|
depth_so_far=0,
|
||||||
|
max_depth=max_depth,
|
||||||
|
context=validation_context,
|
||||||
|
operation_name=name,
|
||||||
|
ignore=ignore,
|
||||||
|
)
|
||||||
|
if callable(callback):
|
||||||
|
callback(query_depths)
|
||||||
|
super().__init__(validation_context)
|
||||||
|
|
||||||
|
return DepthLimitValidator
|
||||||
|
|
||||||
|
|
||||||
|
def get_fragments(
|
||||||
|
definitions: List[DefinitionNode],
|
||||||
|
) -> Dict[str, FragmentDefinitionNode]:
|
||||||
|
fragments = {}
|
||||||
|
for definition in definitions:
|
||||||
|
if isinstance(definition, FragmentDefinitionNode):
|
||||||
|
fragments[definition.name.value] = definition
|
||||||
|
return fragments
|
||||||
|
|
||||||
|
|
||||||
|
# This will actually get both queries and mutations.
|
||||||
|
# We can basically treat those the same
|
||||||
|
def get_queries_and_mutations(
|
||||||
|
definitions: List[DefinitionNode],
|
||||||
|
) -> Dict[str, OperationDefinitionNode]:
|
||||||
|
operations = {}
|
||||||
|
|
||||||
|
for definition in definitions:
|
||||||
|
if isinstance(definition, OperationDefinitionNode):
|
||||||
|
operation = definition.name.value if definition.name else "anonymous"
|
||||||
|
operations[operation] = definition
|
||||||
|
return operations
|
||||||
|
|
||||||
|
|
||||||
|
def determine_depth(
|
||||||
|
node: Node,
|
||||||
|
fragments: Dict[str, FragmentDefinitionNode],
|
||||||
|
depth_so_far: int,
|
||||||
|
max_depth: int,
|
||||||
|
context: ValidationContext,
|
||||||
|
operation_name: str,
|
||||||
|
ignore: Optional[List[IgnoreType]] = None,
|
||||||
|
) -> int:
|
||||||
|
if depth_so_far > max_depth:
|
||||||
|
context.report_error(
|
||||||
|
GraphQLError(
|
||||||
|
f"'{operation_name}' exceeds maximum operation depth of {max_depth}.",
|
||||||
|
[node],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return depth_so_far
|
||||||
|
if isinstance(node, FieldNode):
|
||||||
|
should_ignore = is_introspection_key(node.name.value) or is_ignored(
|
||||||
|
node, ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_ignore or not node.selection_set:
|
||||||
|
return 0
|
||||||
|
return 1 + max(
|
||||||
|
map(
|
||||||
|
lambda selection: determine_depth(
|
||||||
|
node=selection,
|
||||||
|
fragments=fragments,
|
||||||
|
depth_so_far=depth_so_far + 1,
|
||||||
|
max_depth=max_depth,
|
||||||
|
context=context,
|
||||||
|
operation_name=operation_name,
|
||||||
|
ignore=ignore,
|
||||||
|
),
|
||||||
|
node.selection_set.selections,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(node, FragmentSpreadNode):
|
||||||
|
return determine_depth(
|
||||||
|
node=fragments[node.name.value],
|
||||||
|
fragments=fragments,
|
||||||
|
depth_so_far=depth_so_far,
|
||||||
|
max_depth=max_depth,
|
||||||
|
context=context,
|
||||||
|
operation_name=operation_name,
|
||||||
|
ignore=ignore,
|
||||||
|
)
|
||||||
|
elif isinstance(
|
||||||
|
node, (InlineFragmentNode, FragmentDefinitionNode, OperationDefinitionNode)
|
||||||
|
):
|
||||||
|
return max(
|
||||||
|
map(
|
||||||
|
lambda selection: determine_depth(
|
||||||
|
node=selection,
|
||||||
|
fragments=fragments,
|
||||||
|
depth_so_far=depth_so_far,
|
||||||
|
max_depth=max_depth,
|
||||||
|
context=context,
|
||||||
|
operation_name=operation_name,
|
||||||
|
ignore=ignore,
|
||||||
|
),
|
||||||
|
node.selection_set.selections,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"Depth crawler cannot handle: {node.kind}."
|
||||||
|
) # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool:
|
||||||
|
if ignore is None:
|
||||||
|
return False
|
||||||
|
for rule in ignore:
|
||||||
|
field_name = node.name.value
|
||||||
|
if isinstance(rule, str):
|
||||||
|
if field_name == rule:
|
||||||
|
return True
|
||||||
|
elif isinstance(rule, Pattern):
|
||||||
|
if rule.match(field_name):
|
||||||
|
return True
|
||||||
|
elif callable(rule):
|
||||||
|
if rule(field_name):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid ignore option: {rule}.")
|
||||||
|
return False
|
16
graphene/validation/disable_introspection.py
Normal file
16
graphene/validation/disable_introspection.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from graphql import GraphQLError
|
||||||
|
from graphql.language import FieldNode
|
||||||
|
from graphql.validation import ValidationRule
|
||||||
|
|
||||||
|
from ..utils.is_introspection_key import is_introspection_key
|
||||||
|
|
||||||
|
|
||||||
|
class DisableIntrospection(ValidationRule):
|
||||||
|
def enter_field(self, node: FieldNode, *_args):
|
||||||
|
field_name = node.name.value
|
||||||
|
if is_introspection_key(field_name):
|
||||||
|
self.report_error(
|
||||||
|
GraphQLError(
|
||||||
|
f"Cannot query '{field_name}': introspection is disabled.", node
|
||||||
|
)
|
||||||
|
)
|
0
graphene/validation/tests/__init__.py
Normal file
0
graphene/validation/tests/__init__.py
Normal file
254
graphene/validation/tests/test_depth_limit_validator.py
Normal file
254
graphene/validation/tests/test_depth_limit_validator.py
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from pytest import raises
|
||||||
|
from graphql import parse, get_introspection_query, validate
|
||||||
|
|
||||||
|
from ...types import Schema, ObjectType, Interface
|
||||||
|
from ...types import String, Int, List, Field
|
||||||
|
from ..depth_limit import depth_limit_validator
|
||||||
|
|
||||||
|
|
||||||
|
class PetType(Interface):
|
||||||
|
name = String(required=True)
|
||||||
|
|
||||||
|
class meta:
|
||||||
|
name = "Pet"
|
||||||
|
|
||||||
|
|
||||||
|
class CatType(ObjectType):
|
||||||
|
class meta:
|
||||||
|
name = "Cat"
|
||||||
|
interfaces = (PetType,)
|
||||||
|
|
||||||
|
|
||||||
|
class DogType(ObjectType):
|
||||||
|
class meta:
|
||||||
|
name = "Dog"
|
||||||
|
interfaces = (PetType,)
|
||||||
|
|
||||||
|
|
||||||
|
class AddressType(ObjectType):
|
||||||
|
street = String(required=True)
|
||||||
|
number = Int(required=True)
|
||||||
|
city = String(required=True)
|
||||||
|
country = String(required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
name = "Address"
|
||||||
|
|
||||||
|
|
||||||
|
class HumanType(ObjectType):
|
||||||
|
name = String(required=True)
|
||||||
|
email = String(required=True)
|
||||||
|
address = Field(AddressType, required=True)
|
||||||
|
pets = List(PetType, required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
name = "Human"
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
user = Field(HumanType, required=True, name=String())
|
||||||
|
version = String(required=True)
|
||||||
|
user1 = Field(HumanType, required=True)
|
||||||
|
user2 = Field(HumanType, required=True)
|
||||||
|
user3 = Field(HumanType, required=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_user(root, info, name=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
|
||||||
|
def run_query(query: str, max_depth: int, ignore=None):
|
||||||
|
document = parse(query)
|
||||||
|
|
||||||
|
result = None
|
||||||
|
|
||||||
|
def callback(query_depths):
|
||||||
|
nonlocal result
|
||||||
|
result = query_depths
|
||||||
|
|
||||||
|
errors = validate(
|
||||||
|
schema=schema.graphql_schema,
|
||||||
|
document_ast=document,
|
||||||
|
rules=(
|
||||||
|
depth_limit_validator(
|
||||||
|
max_depth=max_depth, ignore=ignore, callback=callback
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors, result
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_count_depth_without_fragment():
|
||||||
|
query = """
|
||||||
|
query read0 {
|
||||||
|
version
|
||||||
|
}
|
||||||
|
query read1 {
|
||||||
|
version
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query read2 {
|
||||||
|
matt: user(name: "matt") {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
andy: user(name: "andy") {
|
||||||
|
email
|
||||||
|
address {
|
||||||
|
city
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query read3 {
|
||||||
|
matt: user(name: "matt") {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
andy: user(name: "andy") {
|
||||||
|
email
|
||||||
|
address {
|
||||||
|
city
|
||||||
|
}
|
||||||
|
pets {
|
||||||
|
name
|
||||||
|
owner {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3}
|
||||||
|
|
||||||
|
errors, result = run_query(query, 10)
|
||||||
|
assert not errors
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_count_with_fragments():
|
||||||
|
query = """
|
||||||
|
query read0 {
|
||||||
|
... on Query {
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query read1 {
|
||||||
|
version
|
||||||
|
user {
|
||||||
|
... on Human {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment humanInfo on Human {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
fragment petInfo on Pet {
|
||||||
|
name
|
||||||
|
owner {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query read2 {
|
||||||
|
matt: user(name: "matt") {
|
||||||
|
...humanInfo
|
||||||
|
}
|
||||||
|
andy: user(name: "andy") {
|
||||||
|
...humanInfo
|
||||||
|
address {
|
||||||
|
city
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query read3 {
|
||||||
|
matt: user(name: "matt") {
|
||||||
|
...humanInfo
|
||||||
|
}
|
||||||
|
andy: user(name: "andy") {
|
||||||
|
... on Human {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
address {
|
||||||
|
city
|
||||||
|
}
|
||||||
|
pets {
|
||||||
|
...petInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3}
|
||||||
|
|
||||||
|
errors, result = run_query(query, 10)
|
||||||
|
assert not errors
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_ignore_the_introspection_query():
|
||||||
|
errors, result = run_query(get_introspection_query(), 10)
|
||||||
|
assert not errors
|
||||||
|
assert result == {"IntrospectionQuery": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_catch_very_deep_query():
|
||||||
|
query = """{
|
||||||
|
user {
|
||||||
|
pets {
|
||||||
|
owner {
|
||||||
|
pets {
|
||||||
|
owner {
|
||||||
|
pets {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
errors, result = run_query(query, 4)
|
||||||
|
|
||||||
|
assert len(errors) == 1
|
||||||
|
assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4."
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_ignore_field():
|
||||||
|
query = """
|
||||||
|
query read1 {
|
||||||
|
user { address { city } }
|
||||||
|
}
|
||||||
|
query read2 {
|
||||||
|
user1 { address { city } }
|
||||||
|
user2 { address { city } }
|
||||||
|
user3 { address { city } }
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
errors, result = run_query(
|
||||||
|
query,
|
||||||
|
10,
|
||||||
|
ignore=["user1", re.compile("user2"), lambda field_name: field_name == "user3"],
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = {"read1": 2, "read2": 0}
|
||||||
|
assert not errors
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_raise_invalid_ignore():
|
||||||
|
query = """
|
||||||
|
query read1 {
|
||||||
|
user { address { city } }
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
with raises(ValueError, match="Invalid ignore option:"):
|
||||||
|
run_query(query, 10, ignore=[True])
|
37
graphene/validation/tests/test_disable_introspection.py
Normal file
37
graphene/validation/tests/test_disable_introspection.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from graphql import parse, validate
|
||||||
|
|
||||||
|
from ...types import Schema, ObjectType, String
|
||||||
|
from ..disable_introspection import DisableIntrospection
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
name = String(required=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_name(root, info):
|
||||||
|
return "Hello world!"
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
|
||||||
|
def run_query(query: str):
|
||||||
|
document = parse(query)
|
||||||
|
|
||||||
|
return validate(
|
||||||
|
schema=schema.graphql_schema,
|
||||||
|
document_ast=document,
|
||||||
|
rules=(DisableIntrospection,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disallows_introspection_queries():
|
||||||
|
errors = run_query("{ __schema { queryType { name } } }")
|
||||||
|
|
||||||
|
assert len(errors) == 1
|
||||||
|
assert errors[0].message == "Cannot query '__schema': introspection is disabled."
|
||||||
|
|
||||||
|
|
||||||
|
def test_allows_non_introspection_queries():
|
||||||
|
errors = run_query("{ name }")
|
||||||
|
assert len(errors) == 0
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user