Compare commits

..

No commits in common. "master" and "v2.1.2" have entirely different histories.

186 changed files with 6130 additions and 9884 deletions

View File

@ -1,34 +0,0 @@
---
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)

View File

@ -1 +0,0 @@
blank_issues_enabled: false

View File

@ -1,20 +0,0 @@
---
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.

24
.github/stale.yml vendored
View File

@ -1,24 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: false
# Number of days of inactivity before a stale issue is closed
daysUntilClose: false
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- 🐛 bug
- 📖 documentation
- 🙋 help wanted
- ✨ enhancement
- good first issue
- work in progress
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: false
# markComment: >
# This issue has been automatically marked as stale because it has not had
# 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
closeComment: false

View File

@ -1,21 +0,0 @@
name: 📦 Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Building package
run: python3 -m build
- name: Check package with Twine
run: twine check dist/*

View File

@ -1,26 +0,0 @@
name: 🚀 Deploy to PyPI
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- 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 }}

View File

@ -1,26 +0,0 @@
name: 💅 Lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- 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

View File

@ -1,64 +0,0 @@
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.13', python: '3.13', os: ubuntu-latest, tox: py313}
- {name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312}
- {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311}
- {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}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: update pip
run: |
python -m pip install --upgrade pip
pip install --upgrade setuptools wheel
- name: get pip cache dir
id: pip-cache
run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- 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@v4
with:
name: graphene-coverage
path: coverage.xml
if-no-files-found: error
- name: Upload coverage.xml to codecov
if: ${{ matrix.python == '3.10' }}
uses: codecov/codecov-action@v4

13
.gitignore vendored
View File

@ -10,6 +10,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
@ -44,8 +45,7 @@ htmlcov/
.pytest_cache
nosetests.xml
coverage.xml
*.cover
.pytest_cache/
*,cover
# Translations
*.mo
@ -60,14 +60,6 @@ docs/_build/
# PyBuilder
target/
# VirtualEnv
.env
.venv
env/
venv/
# Typing
.mypy_cache/
/tests/django.sqlite
@ -90,4 +82,3 @@ venv/
*.sqlite3
.vscode
.mypy_cache
.ruff_cache

2
.isort.cfg Normal file
View File

@ -0,0 +1,2 @@
[settings]
known_third_party = aniso8601,graphql,graphql_relay,promise,pytest,pytz,pyutils,setuptools,six,snapshottest,sphinx_graphene_theme

View File

@ -1,29 +1,31 @@
default_language_version:
python: python3.10
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
- repo: git://github.com/pre-commit/pre-commit-hooks
rev: v1.3.0
hooks:
- id: check-merge-conflict
- id: autopep8-wrapper
args:
- -i
- --ignore=E128,E309,E501
exclude: ^docs/.*$
- id: check-json
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: ^docs/.*$
- id: trailing-whitespace
- id: pretty-format-json
args:
- --autofix
- id: trailing-whitespace
exclude: README.md
- id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v2.37.3
rev: v1.2.0
hooks:
- id: pyupgrade
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.0
- repo: https://github.com/asottile/seed-isort-config
rev: v1.0.0
hooks:
- id: ruff
- id: ruff-format
args: [ --check ]
- id: seed-isort-config
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.4
hooks:
- id: isort

69
.travis.yml Normal file
View File

@ -0,0 +1,69 @@
language: python
sudo: false
python:
- 2.7
- 3.5
- 3.6
# - "pypy-5.3.1"
before_install:
- |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
export PYPY_VERSION="4.0.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
fi
install:
- |
if [ "$TEST_TYPE" = build ]; then
pip install -e .[test]
python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then
pip install flake8
elif [ "$TEST_TYPE" = mypy ]; then
pip install mypy
fi
script:
- |
if [ "$TEST_TYPE" = lint ]; then
echo "Checking Python code lint."
flake8 graphene
exit
elif [ "$TEST_TYPE" = mypy ]; then
echo "Checking Python types."
mypy graphene
exit
elif [ "$TEST_TYPE" = build ]; then
py.test --cov=graphene graphene examples
fi
after_success:
- |
if [ "$TEST_TYPE" = build ]; then
coveralls
fi
env:
matrix:
- TEST_TYPE=build
global:
secure: SQC0eCWCWw8bZxbLE8vQn+UjJOp3Z1m779s9SMK3lCLwJxro/VCLBZ7hj4xsrq1MtcFO2U2Kqf068symw4Hr/0amYI3HFTCFiwXAC3PAKXeURca03eNO2heku+FtnQcOjBanExTsIBQRLDXMOaUkf3MIztpLJ4LHqMfUupKmw9YSB0v40jDbSN8khBnndFykmOnVVHznFp8USoN5F0CiPpnfEvHnJkaX76lNf7Kc9XNShBTTtJsnsHMhuYQeInt0vg9HSjoIYC38Tv2hmMj1myNdzyrHF+LgRjI6ceGi50ApAnGepXC/DNRhXROfECKez+LON/ZSqBGdJhUILqC8A4WmWmIjNcwitVFp3JGBqO7LULS0BI96EtSLe8rD1rkkdTbjivajkbykM1Q0Tnmg1adzGwLxRUbTq9tJQlTTkHBCuXIkpKb1mAtb/TY7A6BqfnPi2xTc/++qEawUG7ePhscdTj0IBrUfZsUNUYZqD8E8XbSWKIuS3SHE+cZ+s/kdAsm4q+FFAlpZKOYGxIkwvgyfu4/Plfol4b7X6iAP9J3r1Kv0DgBVFst5CXEwzZs19/g0CgokQbCXf1N+xeNnUELl6/fImaR3RKP22EaABoil4z8vzl4EqxqVoH1nfhE+WlpryXsuSaF/1R+WklR7aQ1FwoCk8V8HxM2zrj4tI8k=
matrix:
fast_finish: true
include:
- python: '2.7'
env: TEST_TYPE=lint
- python: '3.6'
env: TEST_TYPE=mypy
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"

View File

@ -1,28 +0,0 @@
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
.PHONY: install-dev ## Install development dependencies
install-dev:
pip install -e ".[dev]"
.PHONY: test ## Run tests
test:
py.test graphene examples
.PHONY: docs ## Generate docs
docs: install-dev
cd docs && make install && make html
.PHONY: docs-live ## Generate docs with live reloading
docs-live: install-dev
cd docs && make install && make livehtml
.PHONY: format
format:
black graphene examples setup.py
.PHONY: lint
lint:
flake8 graphene examples setup.py

View File

@ -1,47 +1,50 @@
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) [![](https://dcbadge.vercel.app/api/server/T6Gp6NFYHe?style=flat)](https://discord.gg/T6Gp6NFYHe)
Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade to Graphene `2.0`.
[💬 Join the community on Discord](https://discord.gg/T6Gp6NFYHe)
---
**We are looking for contributors**! Please check the current issues to see how you can help ❤️
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master)
## 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 a Python library for building GraphQL schemas/types fast and easily.
- **Easy to use:** Graphene helps you use GraphQL in Python without effort.
- **Relay:** Graphene has builtin support for Relay.
- **Data agnostic:** Graphene supports any kind of data source: SQL (Django, SQLAlchemy), Mongo, custom Python objects, etc.
- **Data agnostic:** Graphene supports any kind of data source: SQL (Django, SQLAlchemy), NoSQL, custom Python objects, etc.
We believe that by providing a complete API you could plug Graphene anywhere your data lives and make your data available
through GraphQL.
## Integrations
Graphene has multiple integrations with different frameworks:
| integration | Package |
| ----------------- | --------------------------------------------------------------------------------------- |
| SQLAlchemy | [graphene-sqlalchemy](https://github.com/graphql-python/graphene-sqlalchemy/) |
| Mongo | [graphene-mongo](https://github.com/graphql-python/graphene-mongo/) |
| Apollo Federation | [graphene-federation](https://github.com/graphql-python/graphene-federation/) |
| Django | [graphene-django](https://github.com/graphql-python/graphene-django/) |
| integration | Package |
|---------------|-------------------|
| Django | [graphene-django](https://github.com/graphql-python/graphene-django/) |
| SQLAlchemy | [graphene-sqlalchemy](https://github.com/graphql-python/graphene-sqlalchemy/) |
| Google App Engine | [graphene-gae](https://github.com/graphql-python/graphene-gae/) |
| Peewee | *In progress* ([Tracking Issue](https://github.com/graphql-python/graphene/issues/289)) |
Also, Graphene is fully compatible with the GraphQL spec, working seamlessly with all GraphQL clients, such as [Relay](https://github.com/facebook/relay), [Apollo](https://github.com/apollographql/apollo-client) and [gql](https://github.com/graphql-python/gql).
## Installation
To install `graphene`, just run this command in your shell
For instaling graphene, just run this command in your shell
```bash
pip install "graphene>=3.1"
pip install "graphene>=2.0"
```
## 2.0 Upgrade Guide
Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade.
## Examples
Here is one example for you to get started:
```python
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(description='A typical hello world')
@ -64,13 +67,9 @@ result = schema.execute(query)
If you want to learn even more, you can also check the following [examples](examples/):
- **Basic Schema**: [Starwars example](examples/starwars)
- **Relay Schema**: [Starwars Relay example](examples/starwars_relay)
* **Basic Schema**: [Starwars example](examples/starwars)
* **Relay Schema**: [Starwars Relay example](examples/starwars_relay)
## Documentation
Documentation and links to additional resources are available at
https://docs.graphene-python.org/en/latest/
## Contributing
@ -85,46 +84,43 @@ pip install -e ".[test]"
Well-written tests and maintaining good test coverage is important to this project. While developing, run new and existing tests with:
```sh
pytest graphene/relay/tests/test_node.py # Single file
pytest graphene/relay # All tests in directory
py.test PATH/TO/MY/DIR/test_test.py # Single file
py.test PATH/TO/MY/DIR/ # All tests in directory
```
Add the `-s` flag if you have introduced breakpoints into the code for debugging.
Add the `-v` ("verbose") flag to get more detailed test output. For even more detailed output, use `-vv`.
Check out the [pytest documentation](https://docs.pytest.org/en/latest/) for more options and test running controls.
Regularly ensure your `pre-commit` hooks are up to date and enabled:
```sh
pre-commit install
```
Check out the [pytest documentation](https://docs.pytest.org/en/latest/) for more options and test running controls.
You can also run the benchmarks with:
```sh
pytest graphene --benchmark-only
py.test graphene --benchmark-only
```
Graphene supports several versions of Python. To make sure that changes do not break compatibility with any of those versions, we use `tox` to create virtualenvs for each Python version and run tests with that version. To run against all Python versions defined in the `tox.ini` config file, just run:
Graphene supports several versions of Python. To make sure that changes do not break compatibility with any of those versions, we use `tox` to create virtualenvs for each python version and run tests with that version. To run against all python versions defined in the `tox.ini` config file, just run:
```sh
tox
```
If you wish to run against a specific version defined in the `tox.ini` file:
```sh
tox -e py39
tox -e py36
```
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 same tests and report the results, so there is no need for potential contributors to try to install every single version of python on their own system ahead of time. We appreciate opening issues and pull requests to make graphene even more stable & useful!
Tox can only use whatever versions of Python are installed on your system. When you create a pull request, GitHub Actions pipelines will also be running the same tests and report the results, so there is no need for potential contributors to try to install every single version of Python on their own system ahead of time. We appreciate opening issues and pull requests to make graphene even more stable & useful!
### Building Documentation
### Documentation
The documentation is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
An HTML version of the documentation is produced by running:
The documentation dependencies are installed by running:
```sh
make docs
cd docs
pip install -r requirements.txt
```
Then to produce a HTML version of the documentation:
```sh
make html
```

137
README.rst Normal file
View File

@ -0,0 +1,137 @@
Please read `UPGRADE-v2.0.md </UPGRADE-v2.0.md>`__ to learn how to
upgrade to Graphene ``2.0``.
--------------
|Graphene Logo| `Graphene <http://graphene-python.org>`__ |Build Status| |PyPI version| |Coverage Status|
=========================================================================================================
`Graphene <http://graphene-python.org>`__ is a Python library for
building GraphQL schemas/types fast and easily.
- **Easy to use:** Graphene helps you use GraphQL in Python without
effort.
- **Relay:** Graphene has builtin support for Relay.
- **Data agnostic:** Graphene supports any kind of data source: SQL
(Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe
that by providing a complete API you could plug Graphene anywhere
your data lives and make your data available through GraphQL.
Integrations
------------
Graphene has multiple integrations with different frameworks:
+---------------------+----------------------------------------------------------------------------------------------+
| integration | Package |
+=====================+==============================================================================================+
| Django | `graphene-django <https://github.com/graphql-python/graphene-django/>`__ |
+---------------------+----------------------------------------------------------------------------------------------+
| SQLAlchemy | `graphene-sqlalchemy <https://github.com/graphql-python/graphene-sqlalchemy/>`__ |
+---------------------+----------------------------------------------------------------------------------------------+
| Google App Engine | `graphene-gae <https://github.com/graphql-python/graphene-gae/>`__ |
+---------------------+----------------------------------------------------------------------------------------------+
| Peewee | *In progress* (`Tracking Issue <https://github.com/graphql-python/graphene/issues/289>`__) |
+---------------------+----------------------------------------------------------------------------------------------+
Also, Graphene is fully compatible with the GraphQL spec, working
seamlessly with all GraphQL clients, such as
`Relay <https://github.com/facebook/relay>`__,
`Apollo <https://github.com/apollographql/apollo-client>`__ and
`gql <https://github.com/graphql-python/gql>`__.
Installation
------------
For instaling graphene, just run this command in your shell
.. code:: bash
pip install "graphene>=2.0"
2.0 Upgrade Guide
-----------------
Please read `UPGRADE-v2.0.md </UPGRADE-v2.0.md>`__ to learn how to
upgrade.
Examples
--------
Here is one example for you to get started:
.. code:: python
class Query(graphene.ObjectType):
hello = graphene.String(description='A typical hello world')
def resolve_hello(self, info):
return 'World'
schema = graphene.Schema(query=Query)
Then Querying ``graphene.Schema`` is as simple as:
.. code:: python
query = '''
query SayHello {
hello
}
'''
result = schema.execute(query)
If you want to learn even more, you can also check the following
`examples <examples/>`__:
- **Basic Schema**: `Starwars example <examples/starwars>`__
- **Relay Schema**: `Starwars Relay
example <examples/starwars_relay>`__
Contributing
------------
After cloning this repo, ensure dependencies are installed by running:
.. code:: sh
pip install -e ".[test]"
After developing, the full test suite can be evaluated by running:
.. code:: sh
py.test graphene --cov=graphene --benchmark-skip # Use -v -s for verbose mode
You can also run the benchmarks with:
.. code:: sh
py.test graphene --benchmark-only
Documentation
~~~~~~~~~~~~~
The documentation is generated using the excellent
`Sphinx <http://www.sphinx-doc.org/>`__ and a custom theme.
The documentation dependencies are installed by running:
.. code:: sh
cd docs
pip install -r requirements.txt
Then to produce a HTML version of the documentation:
.. code:: sh
make html
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master
:target: https://travis-ci.org/graphql-python/graphene
.. |PyPI version| image:: https://badge.fury.io/py/graphene.svg
:target: https://badge.fury.io/py/graphene
.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/graphql-python/graphene?branch=master

View File

@ -1,15 +0,0 @@
# Security Policy
## Supported Versions
Support for security issues is currently provided for Graphene 3.0 and above. Support on earlier versions cannot be guaranteed by the maintainers of this library, but community PRs may be accepted in critical cases.
The preferred mitigation strategy is via an upgrade to Graphene 3.
| Version | Supported |
| ------- | ------------------ |
| 3.x | :white_check_mark: |
| <3.x | :x: |
## Reporting a Vulnerability
Please use responsible disclosure by contacting a core maintainer via Discord or E-Mail.

View File

@ -2,29 +2,30 @@
Big changes from v0.10.x to 1.0. While on the surface a lot of this just looks like shuffling around API, the entire codebase has been rewritten to handle some really great use cases and improved performance.
## Backwards Compatibility and Deprecation Warnings
This has been a community project from the start, we need your help making the upgrade as smooth as possible for everybody!
We have done our best to provide backwards compatibility with deprecated APIs.
## Deprecations
- `with_context` is no longer needed. Resolvers now always take the context argument.
* `with_context` is no longer needed. Resolvers now always take the context argument.
Before:
```python
def resolve_xxx(root, args, info):
def resolve_xxx(self, args, info):
# ...
```
With 1.0:
```python
def resolve_xxx(root, args, context, info):
def resolve_xxx(self, args, context, info):
# ...
```
- `ObjectType` and `Interface` no longer accept the `abstract` option in the `Meta`.
* `ObjectType` and `Interface` no longer accept the `abstract` option in the `Meta`.
Inheriting fields should be now achieved using `AbstractType` inheritance.
Before:
@ -41,7 +42,6 @@ We have done our best to provide backwards compatibility with deprecated APIs.
```
With 1.0:
```python
class MyBaseQuery(graphene.AbstractType):
my_field = String()
@ -50,9 +50,9 @@ We have done our best to provide backwards compatibility with deprecated APIs.
pass
```
- The `type_name` option in the Meta in types is now `name`
* The `type_name` option in the Meta in types is now `name`
- Type references no longer work with strings, but with functions.
* Type references no longer work with strings, but with functions.
Before:
@ -70,6 +70,7 @@ We have done our best to provide backwards compatibility with deprecated APIs.
users = graphene.List(lambda: User)
```
## Schema
Schemas in graphene `1.0` are `Immutable`, that means that once you create a `graphene.Schema` any
@ -79,6 +80,7 @@ The `name` argument is removed from the Schema.
The arguments `executor` and `middlewares` are also removed from the `Schema` definition.
You can still use them, but by calling explicitly in the `execute` method in `graphql`.
```python
# Old way
schema = graphene.Schema(name='My Schema')
@ -92,6 +94,7 @@ schema = graphene.Schema(
)
```
## Interfaces
For implementing an Interface in an ObjectType, you have to add it onto `Meta.interfaces`.
@ -128,7 +131,7 @@ class ReverseString(Mutation):
reversed = String()
def mutate(root, args, context, info):
def mutate(self, args, context, info):
reversed = args.get('input')[::-1]
return ReverseString(reversed=reversed)
@ -153,15 +156,16 @@ class Query(ObjectType):
```
Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it
explicitly.
explicity.
## Django
The Django integration with Graphene now has an independent package: `graphene-django`.
For installing, you have to replace the old `graphene[django]` with `graphene-django`.
- As the package is now independent, you now have to import from `graphene_django`.
- **DjangoNode no longer exists**, please use `relay.Node` instead:
* As the package is now independent, you now have to import from `graphene_django`.
* **DjangoNode no longer exists**, please use `relay.Node` instead:
```python
from graphene.relay import Node
@ -177,8 +181,8 @@ For installing, you have to replace the old `graphene[django]` with `graphene-dj
The SQLAlchemy integration with Graphene now has an independent package: `graphene-sqlalchemy`.
For installing, you have to replace the old `graphene[sqlalchemy]` with `graphene-sqlalchemy`.
- As the package is now independent, you have to import now from `graphene_sqlalchemy`.
- **SQLAlchemyNode no longer exists**, please use `relay.Node` instead:
* As the package is now independent, you have to import now from `graphene_sqlalchemy`.
* **SQLAlchemyNode no longer exists**, please use `relay.Node` instead:
```python
from graphene.relay import Node

View File

@ -7,22 +7,20 @@ It also improves the field resolvers, [simplifying the code](#simpler-resolvers)
developer has to write to use them.
**Deprecations:**
- [`AbstractType`](#abstracttype-deprecated)
- [`resolve_only_args`](#resolve_only_args)
- [`Mutation.Input`](#mutationinput)
* [`AbstractType`](#abstracttype-deprecated)
* [`resolve_only_args`](#resolve_only_args)
* [`Mutation.Input`](#mutationinput)
**Breaking changes:**
- [`Simpler Resolvers`](#simpler-resolvers)
- [`Node Connections`](#node-connections)
* [`Simpler Resolvers`](#simpler-resolvers)
* [`Node Connections`](#node-connections)
**New Features!**
* [`InputObjectType`](#inputobjecttype)
* [`Meta as Class arguments`](#meta-ass-class-arguments) (_only available for Python 3_)
- [`InputObjectType`](#inputobjecttype)
- [`Meta as Class arguments`](#meta-as-class-arguments) (_only available for Python 3_)
> The type metaclasses are now deleted as they are no longer necessary. If your code was depending
> The type metaclasses are now deleted as they are no longer necessary. If your code was depending
> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/v2.0.0/graphene/tests/issues/test_425.py).
## Deprecations
@ -51,7 +49,7 @@ class Pet(CommonFields, Interface):
pass
```
### resolve_only_args
### resolve\_only\_args
`resolve_only_args` is now deprecated as the resolver API has been simplified.
@ -62,8 +60,8 @@ class User(ObjectType):
name = String()
@resolve_only_args
def resolve_name(root):
return root.name
def resolve_name(self):
return self.name
```
With 2.0:
@ -72,8 +70,8 @@ With 2.0:
class User(ObjectType):
name = String()
def resolve_name(root, info):
return root.name
def resolve_name(self, info):
return self.name
```
### Mutation.Input
@ -96,6 +94,7 @@ class User(Mutation):
name = String()
```
## Breaking Changes
### Simpler resolvers
@ -109,7 +108,7 @@ Before:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(root, args, context, info):
def resolve_my_field(self, args, context, info):
my_arg = args.get('my_arg')
return ...
```
@ -119,15 +118,15 @@ With 2.0:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(root, info, my_arg):
def resolve_my_field(self, info, my_arg):
return ...
```
**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).**
**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).**
You may need something like this:
```python
def resolve_my_field(root, info, known_field1, known_field2, **args): ## get other args with: args.get('arg_key')
def resolve_my_field(self, info, known_field1, known_field2, **args): ## get other args with: args.get('arg_key')
```
And, if you need the context in the resolver, you can use `info.context`:
@ -135,7 +134,7 @@ And, if you need the context in the resolver, you can use `info.context`:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(root, info, my_arg):
def resolve_my_field(self, info, my_arg):
context = info.context
return ...
```
@ -189,7 +188,6 @@ class MyObject(ObjectType):
```
To:
```python
class MyObject(ObjectType):
class Meta:
@ -205,32 +203,30 @@ class MyObject(ObjectType):
The parameters' order of `get_node_from_global_id` method has changed. You may need to adjust your [Node Root Field](http://docs.graphene-python.org/en/latest/relay/nodes/#node-root-field) and maybe other places that uses this method to obtain an object.
Before:
```python
class RootQuery(object):
...
node = Field(relay.Node, id=ID(required=True))
def resolve_node(root, args, context, info):
def resolve_node(self, args, context, info):
node = relay.Node.get_node_from_global_id(args['id'], context, info)
return node
```
Now:
```python
class RootQuery(object):
...
node = Field(relay.Node, id=ID(required=True))
def resolve_node(root, info, id):
def resolve_node(self, info, id):
node = relay.Node.get_node_from_global_id(info, id)
return node
```
## Mutation.mutate
Now only receives (`root`, `info`, `**kwargs`) and is not a @classmethod
Now only receives (`self`, `info`, `**args`) and is not a @classmethod
Before:
@ -249,7 +245,7 @@ With 2.0:
class SomeMutation(Mutation):
...
def mutate(root, info, **args):
def mutate(self, info, **args):
...
```
@ -262,14 +258,17 @@ class SomeMutation(Mutation):
last_name = String(required=True)
...
def mutate(root, info, first_name, last_name):
def mutate(self, info, first_name, last_name):
...
```
## ClientIDMutation.mutate_and_get_payload
Now only receives (`root`, `info`, `**input`)
### Middlewares
If you are using Middelwares, you need to some adjustments:
@ -277,7 +276,7 @@ If you are using Middelwares, you need to some adjustments:
Before:
```python
class MyGrapheneMiddleware(object):
class MyGrapheneMiddleware(object):
def resolve(self, next_mw, root, args, context, info):
## Middleware code
@ -288,16 +287,17 @@ class MyGrapheneMiddleware(object):
With 2.0:
```python
class MyGrapheneMiddleware(object):
class MyGrapheneMiddleware(object):
def resolve(self, next_mw, root, info, **args):
context = info.context
## Middleware code
info.context = context
       return next_mw(root, info, **args)
       return next_mw(root, info, **args)```
```
## New Features
### InputObjectType
@ -321,7 +321,7 @@ class Query(ObjectType):
user = graphene.Field(User, input=UserInput())
@resolve_only_args
def resolve_user(root, input):
def resolve_user(self, input):
user_id = input.get('id')
if is_valid_input(user_id):
return get_user(user_id)
@ -334,17 +334,18 @@ class UserInput(InputObjectType):
id = ID(required=True)
@property
def is_valid(root):
return root.id.startswith('userid_')
def is_valid(self):
return self.id.startswith('userid_')
class Query(ObjectType):
user = graphene.Field(User, input=UserInput())
def resolve_user(root, info, input):
def resolve_user(self, info, input):
if input.is_valid:
return get_user(input.id)
```
### Meta as Class arguments
Now you can use the meta options as class arguments (**ONLY PYTHON 3**).
@ -365,6 +366,7 @@ class Dog(ObjectType, interfaces=[Pet]):
name = String()
```
### Abstract types
Now you can create abstact types super easily, without the need of subclassing the meta.
@ -376,8 +378,11 @@ class Base(ObjectType):
id = ID()
def resolve_id(root, info):
return f"{root.__class__.__name__}_{root.id}"
def resolve_id(self, info):
return "{type}_{id}".format(
type=self.__class__.__name__,
id=self.id
)
```
### UUID Scalar

7
bin/autolinter Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
# Install the required scripts with
# pip install autoflake autopep8 isort
autoflake ./examples/ ./graphene/ -r --remove-unused-variables --remove-all-unused-imports --in-place
autopep8 ./examples/ ./graphene/ -r --in-place --experimental --aggressive --max-line-length 120
isort -rc ./examples/ ./graphene/

View File

@ -17,54 +17,75 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: install ## to install all documentation related requirements
install:
pip install -r requirements.txt
.PHONY: clean ## to remove all built documentation
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html ## to make standalone HTML files
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml ## to make HTML files named index.html in directories
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml ## to make a single large HTML file
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle ## to make pickle files
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json ## to make JSON files
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp ## to make HTML files and a HTML help project
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp ## to make HTML files and a qthelp project
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@ -74,7 +95,7 @@ qthelp:
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Graphene.qhc"
.PHONY: applehelp ## to make an Apple Help Book
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@ -83,7 +104,7 @@ applehelp:
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp ## to make HTML files and a Devhelp project
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@ -93,19 +114,19 @@ devhelp:
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Graphene"
@echo "# devhelp"
.PHONY: epub ## to make an epub
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3 ## to make an epub3
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex ## to make LaTeX files, you can set PAPER=a4 or PAPER=letter
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@ -113,33 +134,33 @@ latex:
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf ## to make LaTeX files and run them through pdflatex
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja ## to make LaTeX files and run them through platex/dvipdfmx
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text ## to make text files
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man ## to make manual pages
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo ## to make Texinfo files
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@ -147,62 +168,62 @@ texinfo:
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info ## to make Texinfo files and run them through makeinfo
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext ## to make PO message catalogs
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes ## to make an overview of all changed/added/deprecated items
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck ## to check all external links for integrity
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest ## to run all doctests embedded in the documentation (if enabled)
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage ## to run coverage check of the documentation (if enabled)
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml ## to make Docutils-native XML files
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml ## to make pseudoxml-XML files for display purposes
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy ## to check syntax errors of document sources
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."
.PHONY: livehtml ## to build and serve live-reloading documentation
.PHONY: livehtml
livehtml:
sphinx-autobuild -b html --watch ../graphene $(ALLSPHINXOPTS) $(BUILDDIR)/html
sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html

View File

@ -1,110 +0,0 @@
API Reference
=============
Schema
------
.. autoclass:: graphene.types.schema.Schema
:members:
.. Uncomment sections / types as API documentation is fleshed out
.. in each class
Object types
------------
.. autoclass:: graphene.ObjectType
.. autoclass:: graphene.InputObjectType
.. autoclass:: graphene.Mutation
:members:
.. _fields-mounted-types:
Fields (Mounted Types)
----------------------
.. autoclass:: graphene.Field
.. autoclass:: graphene.Argument
.. autoclass:: graphene.InputField
Fields (Unmounted Types)
------------------------
.. autoclass:: graphene.types.unmountedtype.UnmountedType
GraphQL Scalars
---------------
.. autoclass:: graphene.Int()
.. autoclass:: graphene.Float()
.. autoclass:: graphene.String()
.. autoclass:: graphene.Boolean()
.. autoclass:: graphene.ID()
Graphene Scalars
----------------
.. autoclass:: graphene.Date()
.. autoclass:: graphene.DateTime()
.. autoclass:: graphene.Time()
.. autoclass:: graphene.Decimal()
.. autoclass:: graphene.UUID()
.. autoclass:: graphene.JSONString()
.. autoclass:: graphene.Base64()
Enum
----
.. autoclass:: graphene.Enum()
Structures
----------
.. autoclass:: graphene.List
.. autoclass:: graphene.NonNull
Type Extension
--------------
.. autoclass:: graphene.Interface()
.. autoclass:: graphene.Union()
Execution Metadata
------------------
.. autoclass:: graphene.ResolveInfo
.. autoclass:: graphene.Context
.. autoclass:: graphql.ExecutionResult
.. Relay
.. -----
.. .. autoclass:: graphene.Node
.. .. autoclass:: graphene.GlobalID
.. .. autoclass:: graphene.ClientIDMutation
.. .. autoclass:: graphene.Connection
.. .. autoclass:: graphene.ConnectionField
.. .. autoclass:: graphene.PageInfo

View File

@ -1,9 +1,8 @@
import os
import sys
import sphinx_graphene_theme
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# -*- coding: utf-8 -*-
#
@ -23,8 +22,9 @@ on_rtd = os.environ.get("READTHEDOCS", None) == "True"
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
sys.path.insert(0, os.path.abspath(".."))
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
@ -36,52 +36,53 @@ sys.path.insert(0, os.path.abspath(".."))
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
]
if not on_rtd:
extensions += ["sphinx.ext.githubpages"]
extensions += [
'sphinx.ext.githubpages',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = "index"
master_doc = 'index'
# General information about the project.
project = "Graphene"
copyright = "Graphene 2016"
author = "Syrus Akbary"
project = u'Graphene'
copyright = u'Graphene 2016'
author = u'Syrus Akbary'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = "1.0"
version = u'1.0'
# The full version, including alpha/beta/rc tags.
release = "1.0"
release = u'1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
# language = None
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@ -95,7 +96,7 @@ release = "1.0"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
@ -117,7 +118,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@ -174,7 +175,7 @@ html_theme_path = [sphinx_graphene_theme.get_html_theme_path()]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
@ -254,30 +255,34 @@ html_static_path = ["_static"]
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = "Graphenedoc"
htmlhelp_basename = 'Graphenedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual")
(master_doc, 'Graphene.tex', u'Graphene Documentation',
u'Syrus Akbary', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@ -317,7 +322,10 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "graphene", "Graphene Documentation", [author], 1)]
man_pages = [
(master_doc, 'graphene', u'Graphene Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
@ -330,15 +338,9 @@ man_pages = [(master_doc, "graphene", "Graphene Documentation", [author], 1)]
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"Graphene",
"Graphene Documentation",
author,
"Graphene",
"One line description of project.",
"Miscellaneous",
)
(master_doc, 'Graphene', u'Graphene Documentation',
author, 'Graphene', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
@ -412,7 +414,7 @@ epub_copyright = copyright
# epub_post_files = []
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
epub_exclude_files = ['search.html']
# The depth of the table of contents in toc.ncx.
#
@ -445,14 +447,9 @@ epub_exclude_files = ["search.html"]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
"https://docs.python.org/": None,
"python": ("https://docs.python.org/", None),
"graphene_django": (
"http://docs.graphene-python.org/projects/django/en/latest/",
None,
),
"graphene_sqlalchemy": (
"http://docs.graphene-python.org/projects/sqlalchemy/en/latest/",
None,
),
'https://docs.python.org/': None,
'python': ('https://docs.python.org/', None),
'graphene_django': ('http://docs.graphene-python.org/projects/django/en/latest/', None),
'graphene_sqlalchemy': ('http://docs.graphene-python.org/projects/sqlalchemy/en/latest/', None),
'graphene_gae': ('http://docs.graphene-python.org/projects/gae/en/latest/', None),
}

View File

@ -4,7 +4,7 @@ Dataloader
DataLoader is a generic utility to be used as part of your application's
data fetching layer to provide a simplified and consistent API over
various remote data sources such as databases or web services via batching
and caching. It is provided by a separate package `aiodataloader <https://pypi.org/project/aiodataloader/>`.
and caching.
Batching
@ -15,49 +15,38 @@ Create loaders by providing a batch loading function.
.. code:: python
from aiodataloader import DataLoader
from promise import Promise
from promise.dataloader import DataLoader
class UserLoader(DataLoader):
async def batch_load_fn(self, keys):
# Here we call a function to return a user for each key in keys
return [get_user(id=key) for key in keys]
def batch_load_fn(self, keys):
# Here we return a promise that will result on the
# corresponding user for each key in keys
return Promise.resolve([get_user(id=key) for key in keys])
A batch loading async function accepts a list of keys, and returns a list of ``values``.
A batch loading function accepts an list of keys, and returns a ``Promise``
which resolves to an 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
the wrapping promise is resolved) and then call your batch function with all
requested keys.
``DataLoader`` will coalesce all individual loads which occur within a
single frame of execution (executed once the wrapping event loop is resolved)
and then call your batch function with all requested keys.
.. code:: python
user_loader = UserLoader()
user1 = await user_loader.load(1)
user1_best_friend = await user_loader.load(user1.best_friend_id)
user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id))
user2 = await user_loader.load(2)
user2_best_friend = await user_loader.load(user2.best_friend_id)
user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id))
A naive application may have issued *four* round-trips to a backend for the
required information, but with ``DataLoader`` this application will make at most *two*.
Note that loaded values are one-to-one with the keys and must have the same
order. This means that if you load all values from a single query, you must
make sure that you then order the query result for the results to match the keys:
.. code:: python
class UserLoader(DataLoader):
async def batch_load_fn(self, keys):
users = {user.id: user for user in User.objects.filter(id__in=keys)}
return [users.get(user_id) for user_id in keys]
``DataLoader`` allows you to decouple unrelated parts of your application without
sacrificing the performance of batch data-loading. While the loader presents
an API that loads individual values, all concurrent requests will be coalesced
@ -95,7 +84,7 @@ Consider the following GraphQL request:
}
If ``me``, ``bestFriend`` and ``friends`` each need to send a request to the backend,
Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend,
there could be at most 13 database requests!
@ -110,8 +99,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac
best_friend = graphene.Field(lambda: User)
friends = graphene.List(lambda: User)
async def resolve_best_friend(root, info):
return await user_loader.load(root.best_friend_id)
def resolve_best_friend(self, info):
return user_loader.load(self.best_friend_id)
async def resolve_friends(root, info):
return await user_loader.load_many(root.friend_ids)
def resolve_friends(self, info):
return user_loader.load_many(self.friend_ids)

View File

@ -1,138 +1,59 @@
.. _SchemaExecute:
Executing a query
=================
For executing a query against a schema, you can directly call the ``execute`` method on it.
For executing a query a schema, you can directly call the ``execute`` method on it.
.. code:: python
from graphene import Schema
schema = Schema(...)
schema = graphene.Schema(...)
result = schema.execute('{ name }')
``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.
.. _SchemaExecuteContext:
Context
_______
You can pass context to a query via ``context``.
You can pass context to a query via ``context_value``.
.. code:: python
from graphene import ObjectType, String, Schema
class Query(graphene.ObjectType):
name = graphene.String()
class Query(ObjectType):
name = String()
def resolve_name(root, info):
def resolve_name(self, info):
return info.context.get('name')
schema = Schema(Query)
result = schema.execute('{ name }', context={'name': 'Syrus'})
assert result.data['name'] == 'Syrus'
schema = graphene.Schema(Query)
result = schema.execute('{ name }', context_value={'name': 'Syrus'})
Variables
_________
_______
You can pass variables to a query via ``variables``.
You can pass variables to a query via ``variable_values``.
.. code:: python
from graphene import ObjectType, Field, ID, Schema
class Query(graphene.ObjectType):
user = graphene.Field(User)
class Query(ObjectType):
user = Field(User, id=ID(required=True))
def resolve_user(self, info):
return info.context.get('user')
def resolve_user(root, info, id):
return get_user_by_id(id)
schema = Schema(Query)
schema = graphene.Schema(Query)
result = schema.execute(
'''
query getUser($id: ID) {
'''query getUser($id: ID) {
user(id: $id) {
id
firstName
lastName
}
}
''',
variables={'id': 12},
)
Root Value
__________
Value used for :ref:`ResolverParamParent` in root queries and mutations can be overridden using ``root`` parameter.
.. code:: python
from graphene import ObjectType, Field, Schema
class Query(ObjectType):
me = Field(User)
def resolve_user(root, info):
return {'id': root.id, 'firstName': root.name}
schema = Schema(Query)
user_root = User(id=12, name='bob')
result = schema.execute(
'''
query getUser {
user {
id
firstName
lastName
}
}
''',
root=user_root
}''',
variable_values={'id': 12},
)
assert result.data['user']['id'] == user_root.id
Operation Name
______________
If there are multiple operations defined in a query string, ``operation_name`` should be used to indicate which should be executed.
.. code:: python
from graphene import ObjectType, Field, Schema
class Query(ObjectType):
user = Field(User)
def resolve_user(root, info):
return get_user_by_id(12)
schema = Schema(Query)
query_string = '''
query getUserWithFirstName {
user {
id
firstName
lastName
}
}
query getUserWithFullName {
user {
id
fullName
}
}
'''
result = schema.execute(
query_string,
operation_name='getUserWithFullName'
)
assert result.data['user']['fullName']

View File

@ -1,8 +0,0 @@
File uploading
==============
File uploading is not part of the official GraphQL spec yet and is not natively
implemented in Graphene.
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>`_.

View File

@ -8,6 +8,3 @@ Execution
execute
middleware
dataloader
fileuploading
subscriptions
queryvalidation

View File

@ -16,7 +16,7 @@ Resolve arguments
Middlewares ``resolve`` is invoked with several arguments:
- ``next`` represents the execution chain. Call ``next`` to continue evaluation.
- ``next`` represents the execution chain. Call ``next`` to continue evalution.
- ``root`` is the root value object passed throughout the query.
- ``info`` is the resolver info.
- ``args`` is the dict of arguments passed to the field.
@ -41,14 +41,12 @@ And then execute it with:
result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()])
If the ``middleware`` argument includes multiple middlewares,
these middlewares will be executed bottom-up, i.e. from last to first.
Functional example
------------------
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
@ -57,9 +55,12 @@ logs the time it takes to resolve each field:
def timing_middleware(next, root, info, **args):
start = timer()
return_value = next(root, info, **args)
duration = round((timer() - start) * 1000, 2)
parent_type_name = root._meta.name if root and hasattr(root, '_meta') else ''
logger.debug(f"{parent_type_name}.{info.field_name}: {duration} ms")
duration = timer() - start
logger.debug("{parent_type}.{field_name}: {duration} ms".format(
parent_type=root._meta.name if root and hasattr(root, '_meta') else '',
field_name=info.field_name,
duration=round(duration * 1000, 2)
))
return return_value

View File

@ -1,123 +0,0 @@
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,
)
)

View File

@ -1,40 +0,0 @@
.. _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.

View File

@ -11,16 +11,11 @@ Contents:
execution/index
relay/index
testing/index
api/index
.. _Integrations:
Integrations
------------
-----
* `Graphene-Django <http://docs.graphene-python.org/projects/django/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-django/>`_)
* Flask-Graphql (`source <https://github.com/graphql-python/flask-graphql>`_)
* `Graphene-SQLAlchemy <http://docs.graphene-python.org/projects/sqlalchemy/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-sqlalchemy/>`_)
* `Graphene-GAE <http://docs.graphene-python.org/projects/gae/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-gae/>`_)
* `Graphene-Mongo <http://graphene-mongo.readthedocs.io/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-mongo>`_)
* `Starlette <https://www.starlette.io/graphql/>`_ (`source <https://github.com/encode/starlette>`_)
* `FastAPI <https://fastapi.tiangolo.com/advanced/graphql/>`_ (`source <https://github.com/tiangolo/fastapi>`_)

View File

@ -1,144 +1,57 @@
Getting started
===============
Introduction
------------
What is GraphQL?
~~~~~~~~~~~~~~~~
----------------
GraphQL is a query language for your API.
It provides a standard way to:
* *describe data provided by a server* in a statically typed **Schema**
* *request data* in a **Query** which exactly describes your data requirements and
* *receive data* in a **Response** containing only the data you requested.
For an introduction to GraphQL and an overview of its concepts, please refer to `the official GraphQL documentation`_.
.. _the official GraphQL documentation: http://graphql.org/learn/
What is Graphene?
~~~~~~~~~~~~~~~~~
Graphene is a library that provides tools to implement a GraphQL API in Python using a *code-first* approach.
Compare Graphene's *code-first* approach to building a GraphQL API with *schema-first* approaches like `Apollo Server`_ (JavaScript) or Ariadne_ (Python). Instead of writing GraphQL **Schema Definition Language (SDL)**, we write Python code to describe the data provided by your server.
.. _Apollo Server: https://www.apollographql.com/docs/apollo-server/
.. _Ariadne: https://ariadnegraphql.org/
Graphene is fully featured with integrations for the most popular web frameworks and ORMs. Graphene produces schemas that are fully compliant with the GraphQL spec and provides tools and patterns for building a Relay-Compliant API as well.
An example in Graphene
----------------------
Lets build a basic GraphQL schema to say "hello" and "goodbye" in Graphene.
When we send a **Query** requesting only one **Field**, ``hello``, and specify a value for the ``firstName`` **Argument**...
.. code::
{
hello(firstName: "friend")
}
...we would expect the following Response containing only the data requested (the ``goodbye`` field is not resolved).
.. code::
{
"data": {
"hello": "Hello friend!"
}
}
For an introduction to GraphQL and an overview of its concepts, please refer
to `the official introduction <http://graphql.org/learn/>`_.
Lets build a basic GraphQL schema from scratch.
Requirements
~~~~~~~~~~~~
------------
- Python (3.8, 3.9, 3.10, 3.11, 3.12, pypy)
- Graphene (3.0)
- Python (2.7, 3.4, 3.5, 3.6, pypy)
- Graphene (2.0)
Project setup
~~~~~~~~~~~~~
-------------
.. code:: bash
pip install "graphene>=3.0"
pip install "graphene>=2.0"
Creating a basic Schema
~~~~~~~~~~~~~~~~~~~~~~~
-----------------------
In Graphene, we can define a simple schema using the following code:
A GraphQL schema describes your data model, and provides a GraphQL
server with an associated set of resolve methods that know how to fetch
data.
We are going to create a very simple schema, with a ``Query`` with only
one field: ``hello`` and an input name. And when we query it, it should return ``"Hello {name}"``.
.. code:: python
from graphene import ObjectType, String, Schema
import graphene
class Query(ObjectType):
# this defines a Field `hello` in our Schema with a single Argument `first_name`
# By default, the argument name will automatically be camel-based into firstName in the generated schema
hello = String(first_name=String(default_value="stranger"))
goodbye = String()
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
# our Resolver method takes the GraphQL context (root, info) as well as
# Argument (first_name) for the Field and returns data for the query Response
def resolve_hello(root, info, first_name):
return f'Hello {first_name}!'
def resolve_hello(self, info, name):
return 'Hello ' + name
def resolve_goodbye(root, info):
return 'See ya!'
schema = Schema(query=Query)
A GraphQL **Schema** describes each **Field** in the data model provided by the server using scalar types like *String*, *Int* and *Enum* and compound types like *List* and *Object*. For more details refer to the Graphene :ref:`TypesReference`.
Our schema can also define any number of **Arguments** for our **Fields**. This is a powerful way for a **Query** to describe the exact data requirements for each **Field**.
For each **Field** in our **Schema**, we write a **Resolver** method to fetch data requested by a client's **Query** using the current context and **Arguments**. For more details, refer to this section on :ref:`Resolvers`.
Schema Definition Language (SDL)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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/
.. code::
type Query {
hello(firstName: String = "stranger"): String
goodbye: String
}
Further examples in this documentation will use SDL to describe schema created by ObjectTypes and other fields.
schema = graphene.Schema(query=Query)
Querying
~~~~~~~~
--------
Then we can start querying our **Schema** by passing a GraphQL query string to ``execute``:
Then we can start querying our schema:
.. code:: python
# we can query for our field (with the default argument)
query_string = '{ hello }'
result = schema.execute(query_string)
print(result.data['hello'])
# "Hello stranger!"
result = schema.execute('{ hello }')
print(result.data['hello']) # "Hello stranger"
# or passing the argument in the query
query_with_argument = '{ hello(firstName: "GraphQL") }'
result = schema.execute(query_with_argument)
print(result.data['hello'])
# "Hello GraphQL!"
Next steps
~~~~~~~~~~
Congrats! You got your first Graphene schema working!
Normally, we don't need to directly execute a query string against our schema as Graphene provides many useful Integrations with popular web frameworks like Flask and Django. Check out :ref:`Integrations` for more information on how to get started serving your GraphQL API.
Congrats! You got your first graphene schema working!

View File

@ -41,5 +41,5 @@ that implements ``Node`` will have a default Connection.
name = graphene.String()
ships = relay.ConnectionField(ShipConnection)
def resolve_ships(root, info):
def resolve_ships(self, info):
return []

View File

@ -19,8 +19,11 @@ Useful links
- `Getting started with Relay`_
- `Relay Global Identification Specification`_
- `Relay Cursor Connection Specification`_
- `Relay input Object Mutation`_
.. _Relay: https://relay.dev/docs/guides/graphql-server-specification/
.. _Getting started with Relay: https://relay.dev/docs/getting-started/step-by-step-guide/
.. _Relay Global Identification Specification: https://relay.dev/graphql/objectidentification.htm
.. _Relay Cursor Connection Specification: https://relay.dev/graphql/connections.htm
.. _Relay: https://facebook.github.io/relay/docs/en/graphql-server-specification.html
.. _Relay specification: https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-root-field
.. _Getting started with Relay: https://facebook.github.io/relay/docs/en/quick-start-guide.html
.. _Relay Global Identification Specification: https://facebook.github.io/relay/graphql/objectidentification.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

View File

@ -51,20 +51,20 @@ Example of a custom node:
name = 'Node'
@staticmethod
def to_global_id(type_, id):
return f"{type_}:{id}"
def to_global_id(type, id):
return '{}:{}'.format(type, id)
@staticmethod
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:
# We assure that the node type that we want to retrieve
# 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)
elif type_ == 'Photo':
elif type == 'Photo':
return get_photo(id)

View File

@ -1,5 +1,4 @@
# Required library
Sphinx==6.1.3
sphinx-autobuild==2021.3.14
Sphinx==1.5.3
# Docs template
http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -54,7 +54,7 @@ Execute parameters
~~~~~~~~~~~~~~~~~~
You can also add extra keyword arguments to the ``execute`` method, such as
``context``, ``root``, ``variables``, ...:
``context_value``, ``root_value``, ``variable_values``, ...:
.. code:: python
@ -63,9 +63,49 @@ You can also add extra keyword arguments to the ``execute`` method, such as
def test_hey():
client = Client(my_schema)
executed = client.execute('''{ hey }''', context={'user': 'Peter'})
executed = client.execute('''{ hey }''', context_value={'user': 'Peter'})
assert executed == {
'data': {
'hey': 'hello Peter!'
}
}
Snapshot testing
~~~~~~~~~~~~~~~~
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.
However, writing tests and replicate the same response we expect from our GraphQL application can be
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/>`_.
SnapshotTest let us write all this tests in a breeze, as creates automatically the ``snapshots`` for us
the first time the test is executed.
Here is a simple example on how our tests will look if we use ``pytest``:
.. code:: python
def test_hey(snapshot):
client = Client(my_schema)
# This will create a snapshot dir and a snapshot file
# the first time the test is executed, with the response
# of the execution.
snapshot.assert_match(client.execute('''{ hey }'''))
If we are using ``unittest``:
.. code:: python
from snapshottest import TestCase
class APITestCase(TestCase):
def test_api_me(self):
"""Testing the API for /me"""
client = Client(my_schema)
self.assertMatchSnapshot(client.execute('''{ hey }'''))

View File

@ -0,0 +1,43 @@
AbstractTypes
=============
An AbstractType contains fields that can be shared among
``graphene.ObjectType``, ``graphene.Interface``,
``graphene.InputObjectType`` or other ``graphene.AbstractType``.
The basics:
- Each AbstractType is a Python class that inherits from ``graphene.AbstractType``.
- Each attribute of the AbstractType represents a field (a ``graphene.Field`` or
``graphene.InputField`` depending on where it is mounted)
Quick example
-------------
In this example UserFields is an ``AbstractType`` with a name. ``User`` and
``UserInput`` are two types that have their own fields
plus the ones defined in ``UserFields``.
.. code:: python
import graphene
class UserFields(graphene.AbstractType):
name = graphene.String()
class User(graphene.ObjectType, UserFields):
pass
class UserInput(graphene.InputObjectType, UserFields):
pass
.. code::
type User {
name: String
}
inputtype UserInput {
name: String
}

View File

@ -1,7 +1,7 @@
Enums
=====
An ``Enum`` is a special ``GraphQL`` type that represents a set of
A ``Enum`` is a special ``GraphQL`` type that represents a set of
symbolic names (members) bound to unique, constant values.
Definition
@ -59,10 +59,7 @@ you can add description etc. to your enum without changing the original:
.. code:: python
graphene.Enum.from_enum(
AlreadyExistingPyEnum,
description=lambda v: return 'foo' if v == AlreadyExistingPyEnum.Foo else 'bar'
)
graphene.Enum.from_enum(AlreadyExistingPyEnum, description=lambda value: return 'foo' if value == AlreadyExistingPyEnum.Foo else 'bar')
Notes
@ -77,7 +74,6 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu
.. code:: python
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
@ -86,12 +82,11 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu
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
from graphene import Enum
class Color(Enum):
RED = 1
GREEN = 2

View File

@ -1,5 +1,3 @@
.. _TypesReference:
===============
Types Reference
===============
@ -7,11 +5,12 @@ Types Reference
.. toctree::
:maxdepth: 1
schema
enums
scalars
list-and-nonnull
objecttypes
enums
interfaces
abstracttypes
unions
objecttypes
schema
mutations

View File

@ -1,172 +1,60 @@
.. _Interfaces:
Interfaces
==========
An *Interface* is an abstract type that defines a certain set of fields that a
type must include to implement the interface.
An Interface contains the essential fields that will be implemented by
multiple ObjectTypes.
For example, you can define an Interface ``Character`` that represents any
character in the Star Wars trilogy:
The basics:
- Each Interface is a Python class that inherits from ``graphene.Interface``.
- Each attribute of the Interface represents a GraphQL field.
Quick example
-------------
This example model defines a ``Character`` interface with a name. ``Human``
and ``Droid`` are two implementations of that interface.
.. code:: python
import graphene
class Character(graphene.Interface):
id = graphene.ID(required=True)
name = graphene.String(required=True)
friends = graphene.List(lambda: Character)
Any ObjectType that implements ``Character`` will have these exact fields, with
these arguments and return types.
For example, here are some types that might implement ``Character``:
.. code:: python
name = graphene.String()
# Human is a Character implementation
class Human(graphene.ObjectType):
class Meta:
interfaces = (Character, )
starships = graphene.List(Starship)
home_planet = graphene.String()
born_in = graphene.String()
# Droid is a Character implementation
class Droid(graphene.ObjectType):
class Meta:
interfaces = (Character, )
primary_function = graphene.String()
function = graphene.String()
Both of these types have all of the fields from the ``Character`` interface,
but also bring in extra fields, ``home_planet``, ``starships`` and
``primary_function``, that are specific to that particular type of character.
``name`` is a field on the ``Character`` interface that will also exist on both
the ``Human`` and ``Droid`` ObjectTypes (as those implement the ``Character``
interface). Each ObjectType may define additional fields.
The full GraphQL schema definition will look like this:
The above types have the following representation in a schema:
.. code::
interface Character {
id: ID!
name: String!
friends: [Character]
}
type Human implements Character {
id: ID!
name: String!
friends: [Character]
starships: [Starship]
homePlanet: String
name: String
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
primaryFunction: String
name: String
function: String
}
Interfaces are useful when you want to return an object or set of objects,
which might be of several different types.
For example, you can define a field ``hero`` that resolves to any
``Character``, depending on the episode, like this:
.. code:: python
class Query(graphene.ObjectType):
hero = graphene.Field(
Character,
required=True,
episode=graphene.Int(required=True)
)
def resolve_hero(root, info, episode):
# Luke is the hero of Episode V
if episode == 5:
return get_human(name='Luke Skywalker')
return get_droid(name='R2-D2')
schema = graphene.Schema(query=Query, types=[Human, Droid])
This allows you to directly query for fields that exist on the Character interface
as well as selecting specific fields on any type that implements the interface
using `inline fragments <https://graphql.org/learn/queries/#inline-fragments>`_.
For example, the following query:
.. code::
query HeroForEpisode($episode: Int!) {
hero(episode: $episode) {
__typename
name
... on Droid {
primaryFunction
}
... on Human {
homePlanet
}
}
type Human implements Character {
name: String
bornIn: String
}
Will return the following data with variables ``{ "episode": 4 }``:
.. code:: json
{
"data": {
"hero": {
"__typename": "Droid",
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
And different data with the variables ``{ "episode": 5 }``:
.. code:: json
{
"data": {
"hero": {
"__typename": "Human",
"name": "Luke Skywalker",
"homePlanet": "Tatooine"
}
}
}
Resolving data objects to types
-------------------------------
As you build out your schema in Graphene it's common for your resolvers to
return objects that represent the data backing your GraphQL types rather than
instances of the Graphene types (e.g. Django or SQLAlchemy models). This works
well with ``ObjectType`` and ``Scalar`` fields, however when you start using
Interfaces you might come across this error:
.. code::
"Abstract type Character must resolve to an Object type at runtime for field Query.hero ..."
This happens because Graphene doesn't have enough information to convert the
data object into a Graphene type needed to resolve the ``Interface``. To solve
this you can define a ``resolve_type`` class method on the ``Interface`` which
maps a data object to a Graphene type:
.. code:: python
class Character(graphene.Interface):
id = graphene.ID(required=True)
name = graphene.String(required=True)
@classmethod
def resolve_type(cls, instance, info):
if instance.type == 'DROID':
return Droid
return Human

View File

@ -19,21 +19,20 @@ This example defines a Mutation:
ok = graphene.Boolean()
person = graphene.Field(lambda: Person)
def mutate(root, info, name):
def mutate(self, info, name):
person = Person(name=name)
ok = True
return CreatePerson(person=person, ok=ok)
**person** and **ok** are the output fields of the Mutation when it is
**person** and **ok** are the output fields of the Mutation when is
resolved.
**Arguments** attributes are the arguments that the Mutation
**Input** attributes are the arguments that the Mutation
``CreatePerson`` needs for resolving, in this case **name** will be the
only argument for the mutation.
**mutate** is the function that will be applied once the mutation is
called. This method is just a special resolver that we can change
data within. It takes the same arguments as the standard query :ref:`ResolverArguments`.
called.
So, we can finish our schema like this:
@ -77,7 +76,7 @@ We should receive:
{
"createPerson": {
"person" : {
"name": "Peter"
name: "Peter"
},
"ok": true
}
@ -85,9 +84,9 @@ We should receive:
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
@ -104,6 +103,7 @@ To use an InputField you define an InputObjectType that specifies the structure
person = graphene.Field(Person)
@staticmethod
def mutate(root, info, person_data=None):
person = Person(
name=person_data.name,
@ -112,11 +112,11 @@ To use an InputField you define an InputObjectType that specifies the structure
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:
.. code::
.. code:: json
mutation myFirstMutation {
createPerson(personData: {name:"Peter", age: 24}) {
@ -128,7 +128,7 @@ Using the above mutation your new query would look like this:
}
InputObjectTypes can also be fields of InputObjectTypes allowing you to have
as complex of input data as you need:
as complex of input data as you need
.. code:: python
@ -143,41 +143,3 @@ as complex of input data as you need:
name = graphene.String()
latlng = graphene.InputField(LatLngInput)
Output type example
-------------------
To return an existing ObjectType instead of a mutation-specific type, set the **Output** attribute to the desired ObjectType:
.. code:: python
import graphene
class CreatePerson(graphene.Mutation):
class Arguments:
name = graphene.String()
Output = Person
def mutate(root, info, name):
return Person(name=name)
Then, if we query (``schema.execute(query_str)``) with the following:
.. code::
mutation myFirstMutation {
createPerson(name:"Peter") {
name
__typename
}
}
We should receive:
.. code:: json
{
"createPerson": {
"name": "Peter",
"__typename": "Person"
}
}

View File

@ -1,15 +1,15 @@
.. _ObjectType:
ObjectTypes
===========
ObjectType
==========
A Graphene *ObjectType* is the building block used to define the relationship between **Fields** in your **Schema** and how their data is retrieved.
An ObjectType is the single, definitive source of information about your
data. It contains the essential fields and behaviors of the data youre
querying.
The basics:
- Each ObjectType is a Python class that inherits from ``graphene.ObjectType``.
- Each ObjectType is a Python class that inherits from
``graphene.ObjectType``.
- Each attribute of the ObjectType represents a ``Field``.
- Each ``Field`` has a :ref:`resolver method<Resolvers>` to fetch data (or :ref:`DefaultResolver`).
Quick example
-------------
@ -18,17 +18,19 @@ This example model defines a Person, with a first and a last name:
.. code:: python
from graphene import ObjectType, String
import graphene
class Person(ObjectType):
first_name = String()
last_name = String()
full_name = String()
class Person(graphene.ObjectType):
first_name = graphene.String()
last_name = graphene.String()
full_name = graphene.String()
def resolve_full_name(parent, info):
return f"{parent.first_name} {parent.last_name}"
def resolve_full_name(self, info):
return '{} {}'.format(self.first_name, self.last_name)
This *ObjectType* defines the field **first\_name**, **last\_name**, and **full\_name**. Each field is specified as a class attribute, and each attribute maps to a Field. Data is fetched by our ``resolve_full_name`` :ref:`resolver method<Resolvers>` for ``full_name`` field and the :ref:`DefaultResolver` for other fields.
**first\_name** and **last\_name** are fields of the ObjectType. Each
field is specified as a class attribute, and each attribute maps to a
Field.
The above ``Person`` ObjectType has the following schema representation:
@ -40,396 +42,65 @@ The above ``Person`` ObjectType has the following schema representation:
fullName: String
}
.. _Resolvers:
Resolvers
---------
A **Resolver** is a method that helps us answer **Queries** by fetching data for a **Field** in our **Schema**.
A resolver is a method that resolves certain fields within a
``ObjectType``. If not specififed otherwise, the resolver of a
field is the ``resolve_{field_name}`` method on the ``ObjectType``.
Resolvers are lazily executed, so if a field is not included in a query, its resolver will not be executed.
By default resolvers take the arguments ``args``, ``context`` and ``info``.
Each field on an *ObjectType* in Graphene should have a corresponding resolver method to fetch data. This resolver method should match the field name. For example, in the ``Person`` type above, the ``full_name`` field is resolved by the method ``resolve_full_name``.
NOTE: The resolvers on a ``ObjectType`` are always treated as ``staticmethod``\ s,
so the first argument to the resolver method ``self`` (or ``root``) need
not be an actual instance of the ``ObjectType``.
Each resolver method takes the parameters:
* :ref:`ResolverParamParent` for the value object use to resolve most fields
* :ref:`ResolverParamInfo` for query and schema meta information and per-request context
* :ref:`ResolverParamGraphQLArguments` as defined on the **Field**.
Quick example
~~~~~~~~~~~~~
.. _ResolverArguments:
Resolver Parameters
~~~~~~~~~~~~~~~~~~~
.. _ResolverParamParent:
Parent Value Object (*parent*)
******************************
This parameter is typically used to derive the values for most fields on an *ObjectType*.
The first parameter of a resolver method (*parent*) is the value object returned from the resolver of the parent field. If there is no parent field, such as a root Query field, then the value for *parent* is set to the ``root_value`` configured while executing the query (default ``None``). See :ref:`SchemaExecute` for more details on executing queries.
Resolver example
^^^^^^^^^^^^^^^^
If we have a schema with Person type and one field on the root query.
This example model defines a ``Query`` type, which has a reverse field
that reverses the given ``word`` argument using the ``resolve_reverse``
method in the class.
.. code:: python
from graphene import ObjectType, String, Field
import graphene
def get_human(name):
first_name, last_name = name.split()
return Person(first_name, last_name)
class Query(graphene.ObjectType):
reverse = graphene.String(word=graphene.String())
class Person(ObjectType):
full_name = String()
def resolve_full_name(parent, info):
return f"{parent.first_name} {parent.last_name}"
class Query(ObjectType):
me = Field(Person)
def resolve_me(parent, info):
# returns an object that represents a Person
return get_human(name="Luke Skywalker")
When we execute a query against that schema.
.. code:: python
schema = Schema(query=Query)
query_string = "{ me { fullName } }"
result = schema.execute(query_string)
assert result.data["me"] == {"fullName": "Luke Skywalker"}
Then we go through the following steps to resolve this query:
* ``parent`` is set with the root_value from query execution (None).
* ``Query.resolve_me`` called with ``parent`` None which returns a value object ``Person("Luke", "Skywalker")``.
* This value object is then used as ``parent`` while calling ``Person.resolve_full_name`` to resolve the scalar String value "Luke Skywalker".
* The scalar value is serialized and sent back in the query response.
Each resolver returns the next :ref:`ResolverParamParent` to be used in executing the following resolver in the chain. If the Field is a Scalar type, that value will be serialized and sent in the **Response**. Otherwise, while resolving Compound types like *ObjectType*, the value be passed forward as the next :ref:`ResolverParamParent`.
Naming convention
^^^^^^^^^^^^^^^^^
This :ref:`ResolverParamParent` is sometimes named ``obj``, ``parent``, or ``source`` in other GraphQL documentation. It can also be named after the value object being resolved (ex. ``root`` for a root Query or Mutation, and ``person`` for a Person value object). Sometimes this argument will be named ``self`` in Graphene code, but this can be misleading due to :ref:`ResolverImplicitStaticMethod` while executing queries in Graphene.
.. _ResolverParamInfo:
GraphQL Execution Info (*info*)
*******************************
The second parameter provides two things:
* reference to meta information about the execution of the current GraphQL Query (fields, schema, parsed query, etc.)
* access to per-request ``context`` which can be used to store user authentication, data loader instances or anything else useful for resolving the query.
Only context will be required for most applications. See :ref:`SchemaExecuteContext` for more information about setting context.
.. _ResolverParamGraphQLArguments:
GraphQL Arguments (*\*\*kwargs*)
********************************
Any arguments that a field defines gets passed to the resolver function as
keyword arguments. For example:
.. code:: python
from graphene import ObjectType, Field, String
class Query(ObjectType):
human_by_name = Field(Human, name=String(required=True))
def resolve_human_by_name(parent, info, name):
return get_human(name=name)
You can then execute the following query:
.. code::
query {
humanByName(name: "Luke Skywalker") {
firstName
lastName
}
}
*Note:* There are several arguments to a field that are "reserved" by Graphene
(see :ref:`fields-mounted-types`).
You can still define an argument that clashes with one of these fields by using
the ``args`` parameter like so:
.. code:: python
from graphene import ObjectType, Field, String
class Query(ObjectType):
answer = String(args={'description': String()})
def resolve_answer(parent, info, description):
return description
Convenience Features of Graphene Resolvers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. _ResolverImplicitStaticMethod:
Implicit staticmethod
*********************
One surprising feature of Graphene is that all resolver methods are treated implicitly as staticmethods. This means that, unlike other methods in Python, the first argument of a resolver is *never* ``self`` while it is being executed by Graphene. Instead, the first argument is always :ref:`ResolverParamParent`. In practice, this is very convenient as, in GraphQL, we are almost always more concerned with the using the parent value object to resolve queries than attributes on the Python object itself.
The two resolvers in this example are effectively the same.
.. code:: python
from graphene import ObjectType, String
class Person(ObjectType):
first_name = String()
last_name = String()
@staticmethod
def resolve_first_name(parent, info):
'''
Decorating a Python method with `staticmethod` ensures that `self` will not be provided as an
argument. However, Graphene does not need this decorator for this behavior.
'''
return parent.first_name
def resolve_last_name(parent, info):
'''
Normally the first argument for this method would be `self`, but Graphene executes this as
a staticmethod implicitly.
'''
return parent.last_name
# ...
If you prefer your code to be more explicit, feel free to use ``@staticmethod`` decorators. Otherwise, your code may be cleaner without them!
.. _DefaultResolver:
Default Resolver
****************
If a resolver method is not defined for a **Field** attribute on our *ObjectType*, Graphene supplies a default resolver.
If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a dictionary key matching the field name. Otherwise, the resolver will get the attribute from the parent value object matching the field name.
.. code:: python
from collections import namedtuple
from graphene import ObjectType, String, Field, Schema
PersonValueObject = namedtuple("Person", ["first_name", "last_name"])
class Person(ObjectType):
first_name = String()
last_name = String()
class Query(ObjectType):
me = Field(Person)
my_best_friend = Field(Person)
def resolve_me(parent, info):
# always pass an object for `me` field
return PersonValueObject(first_name="Luke", last_name="Skywalker")
def resolve_my_best_friend(parent, info):
# always pass a dictionary for `my_best_fiend_field`
return {"first_name": "R2", "last_name": "D2"}
schema = Schema(query=Query)
result = schema.execute('''
{
me { firstName lastName }
myBestFriend { firstName lastName }
}
''')
# With default resolvers we can resolve attributes from an object..
assert result.data["me"] == {"firstName": "Luke", "lastName": "Skywalker"}
# With default resolvers, we can also resolve keys from a dictionary..
assert result.data["myBestFriend"] == {"firstName": "R2", "lastName": "D2"}
Advanced
~~~~~~~~
GraphQL Argument defaults
*************************
If you define an argument for a field that is not required (and in a query
execution it is not provided as an argument) it will not be passed to the
resolver function at all. This is so that the developer can differentiate
between a ``undefined`` value for an argument and an explicit ``null`` value.
For example, given this schema:
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(required=True, name=String())
def resolve_hello(parent, info, name):
return name if name else 'World'
And this query:
.. code::
query {
hello
}
An error will be thrown:
.. code::
TypeError: resolve_hello() missing 1 required positional argument: 'name'
You can fix this error in several ways. Either by combining all keyword arguments
into a dict:
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(required=True, name=String())
def resolve_hello(parent, info, **kwargs):
name = kwargs.get('name', 'World')
return f'Hello, {name}!'
Or by setting a default value for the keyword argument:
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(required=True, name=String())
def resolve_hello(parent, info, name='World'):
return f'Hello, {name}!'
One can also set a default value for an Argument in the GraphQL schema itself using Graphene!
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(
required=True,
name=String(default_value='World')
)
def resolve_hello(parent, info, name):
return f'Hello, {name}!'
def resolve_reverse(self, info, word):
return word[::-1]
Resolvers outside the class
***************************
~~~~~~~~~~~~~~~~~~~~~~~~~~~
A field can use a custom resolver from outside the class:
.. code:: python
from graphene import ObjectType, String
import graphene
def resolve_full_name(person, info):
return f"{person.first_name} {person.last_name}"
def reverse(root, info, word):
return word[::-1]
class Person(ObjectType):
first_name = String()
last_name = String()
full_name = String(resolver=resolve_full_name)
class Query(graphene.ObjectType):
reverse = graphene.String(word=graphene.String(), resolver=reverse)
Instances as value objects
**************************
Instances as data containers
----------------------------
Graphene ``ObjectType``\ s can act as value objects too. So with the
previous example you could use ``Person`` to capture data for each of the *ObjectType*'s fields.
Graphene ``ObjectType``\ s can act as containers too. So with the
previous example you could do:
.. code:: python
peter = Person(first_name='Peter', last_name='Griffin')
peter.first_name # prints "Peter"
peter.last_name # prints "Griffin"
Field camelcasing
*****************
Graphene automatically camelcases fields on *ObjectType* from ``field_name`` to ``fieldName`` to conform with GraphQL standards. See :ref:`SchemaAutoCamelCase` for more information.
*ObjectType* Configuration - Meta class
---------------------------------------
Graphene uses a Meta inner class on *ObjectType* to set different options.
GraphQL type name
~~~~~~~~~~~~~~~~~
By default the type name in the GraphQL schema will be the same as the class name
that defines the ``ObjectType``. This can be changed by setting the ``name``
property on the ``Meta`` class:
.. code:: python
from graphene import ObjectType
class MyGraphQlSong(ObjectType):
class Meta:
name = 'Song'
GraphQL Description
~~~~~~~~~~~~~~~~~~~
The schema description of an *ObjectType* can be set as a docstring on the Python object or on the Meta inner class.
.. code:: python
from graphene import ObjectType
class MyGraphQlSong(ObjectType):
''' We can set the schema description for an Object Type here on a docstring '''
class Meta:
description = 'But if we set the description in Meta, this value is used instead'
Interfaces & Possible Types
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Setting ``interfaces`` in Meta inner class specifies the GraphQL Interfaces that this Object implements.
Providing ``possible_types`` helps Graphene resolve ambiguous types such as interfaces or Unions.
See :ref:`Interfaces` for more information.
.. code:: python
from graphene import ObjectType, Node
Song = namedtuple('Song', ('title', 'artist'))
class MyGraphQlSong(ObjectType):
class Meta:
interfaces = (Node, )
possible_types = (Song, )
peter.first_name # prints "Peter"
peter.last_name # prints "Griffin"
.. _Interface: /docs/interfaces/

View File

@ -1,13 +1,6 @@
.. _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:
``name``: *string*
@ -20,7 +13,7 @@ All Scalar types accept the following arguments. All are optional:
``required``: *boolean*
If ``True``, the server will enforce a value for this field. See `NonNull <../list-and-nonnull.html#nonnull>`_. Default is ``False``.
If ``True``, the server will enforce a value for this field. See `NonNull <./list-and-nonnull.html#nonnull>`_. Default is ``False``.
``deprecation_reason``: *string*
@ -32,39 +25,35 @@ All Scalar types accept the following arguments. All are optional:
Built in scalars
----------------
Base scalars
------------
Graphene defines the following base Scalar Types that match the default `GraphQL types <https://graphql.org/learn/schema/#scalar-types>`_:
Graphene defines the following base Scalar Types:
``graphene.String``
^^^^^^^^^^^^^^^^^^^
Represents textual data, represented as UTF-8
character sequences. The String type is most often used by GraphQL to
represent free-form human-readable text.
``graphene.Int``
^^^^^^^^^^^^^^^^
Represents non-fractional signed whole numeric
values. Int is a signed 32bit integer per the
`GraphQL spec <https://facebook.github.io/graphql/June2018/#sec-Int>`_
values. Int can represent values between `-(2^53 - 1)` and `2^53 - 1` since
represented in JSON as double-precision floating point numbers specified
by `IEEE 754 <http://en.wikipedia.org/wiki/IEEE_floating_point>`_.
``graphene.Float``
^^^^^^^^^^^^^^^^^^
Represents signed double-precision fractional
values as specified by
`IEEE 754 <http://en.wikipedia.org/wiki/IEEE_floating_point>`_.
``graphene.Boolean``
^^^^^^^^^^^^^^^^^^^^
Represents `true` or `false`.
``graphene.ID``
^^^^^^^^^^^^^^^
Represents a unique identifier, often used to
refetch an object or as key for a cache. The ID type appears in a JSON
@ -72,183 +61,24 @@ Graphene defines the following base Scalar Types that match the default `GraphQL
When expected as an input type, any string (such as `"4"`) or integer
(such as `4`) input value will be accepted as an ID.
----
Graphene also provides custom scalars for Dates, Times, and JSON:
Graphene also provides custom scalars for common values:
``graphene.Date``
^^^^^^^^^^^^^^^^^
``graphene.types.datetime.Date``
Represents a Date value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
.. 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``
^^^^^^^^^^^^^^^^^^^^^
``graphene.types.datetime.DateTime``
Represents a DateTime value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
.. 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``
^^^^^^^^^^^^^^^^^
``graphene.types.datetime.Time``
Represents a Time value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
.. 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``
^^^^^^^^^^^^^^^^^^^^^^^
``graphene.types.json.JSONString``
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
--------------
@ -270,8 +100,8 @@ The following is an example for creating a DateTime scalar:
return dt.isoformat()
@staticmethod
def parse_literal(node, _variables=None):
if isinstance(node, ast.StringValueNode):
def parse_literal(node):
if isinstance(node, ast.StringValue):
return datetime.datetime.strptime(
node.value, "%Y-%m-%dT%H:%M:%S.%f")

View File

@ -1,42 +1,16 @@
Schema
======
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.
Schema will collect all type definitions related to the root operations and then supply them to the validator and executor.
A Schema is created by supplying the root types of each type of operation, query and mutation (optional).
A schema definition is then supplied to the validator and executor.
.. code:: python
my_schema = Schema(
query=MyRootQuery,
mutation=MyRootMutation,
subscription=MyRootSubscription
)
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
* Mutation changes data and retrieves the changes
* Subscription sends changes to clients in real-time
Review the `GraphQL documentation on Schema`_ for a brief overview of fields, schema and operations.
.. _GraphQL documentation on Schema: https://graphql.org/learn/schema/
Querying
--------
To query a schema, call the ``execute`` method on it. See :ref:`SchemaExecute` for more details.
.. code:: python
query_string = 'query whoIsMyBestFriend { myBestFriend { lastName } }'
my_schema.execute(query_string)
Types
-----
@ -44,7 +18,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
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
@ -54,16 +28,26 @@ In this case, we need to use the ``types`` argument when creating the Schema:
types=[SomeExtraObjectType, ]
)
.. _SchemaAutoCamelCase:
Auto camelCase field names
Querying
--------
To query a schema, call the ``execute`` method on it.
.. code:: python
my_schema.execute('{ lastName }')
Auto CamelCase field names
--------------------------
By default all field and argument names (that are not
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)
For example with the ObjectType the ``last_name`` field name is converted to ``lastName``:
For example with the ObjectType
.. code:: python
@ -71,10 +55,12 @@ For example with the ObjectType the ``last_name`` field name is converted to ``l
last_name = graphene.String()
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.
``other_name`` converts to ``_other_Name`` (without further transformations).
Your query should look like:
Your query should look like
.. code::
@ -84,7 +70,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

View File

@ -7,7 +7,7 @@ to specify any common fields between the types.
The basics:
- 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
-------------

View File

@ -7,7 +7,7 @@ class GeoInput(graphene.InputObjectType):
@property
def latlng(self):
return f"({self.lat},{self.lng})"
return "({},{})".format(self.lat, self.lng)
class Address(graphene.ObjectType):
@ -17,17 +17,18 @@ class Address(graphene.ObjectType):
class Query(graphene.ObjectType):
address = graphene.Field(Address, geo=GeoInput(required=True))
def resolve_address(root, info, geo):
def resolve_address(self, info, geo):
return Address(latlng=geo.latlng)
class CreateAddress(graphene.Mutation):
class Arguments:
geo = GeoInput(required=True)
Output = Address
def mutate(root, info, geo):
def mutate(self, info, geo):
return Address(latlng=geo.latlng)
@ -36,34 +37,42 @@ class Mutation(graphene.ObjectType):
schema = graphene.Schema(query=Query, mutation=Mutation)
query = """
query = '''
query something{
address(geo: {lat:32.2, lng:12}) {
latlng
}
}
"""
mutation = """
'''
mutation = '''
mutation addAddress{
createAddress(geo: {lat:32.2, lng:12}) {
latlng
}
}
"""
'''
def test_query():
result = schema.execute(query)
assert not result.errors
assert result.data == {"address": {"latlng": "(32.2,12.0)"}}
assert result.data == {
'address': {
'latlng': "(32.2,12.0)",
}
}
def test_mutation():
result = schema.execute(mutation)
assert not result.errors
assert result.data == {"createAddress": {"latlng": "(32.2,12.0)"}}
assert result.data == {
'createAddress': {
'latlng': "(32.2,12.0)",
}
}
if __name__ == "__main__":
if __name__ == '__main__':
result = schema.execute(query)
print(result.data["address"]["latlng"])
print(result.data['address']['latlng'])

View File

@ -9,27 +9,32 @@ class User(graphene.ObjectType):
class Query(graphene.ObjectType):
me = graphene.Field(User)
def resolve_me(root, info):
return info.context["user"]
def resolve_me(self, info):
return info.context['user']
schema = graphene.Schema(query=Query)
query = """
query = '''
query something{
me {
id
name
}
}
"""
'''
def test_query():
result = schema.execute(query, context={"user": User(id="1", name="Syrus")})
result = schema.execute(query, context_value={'user': User(id='1', name='Syrus')})
assert not result.errors
assert result.data == {"me": {"id": "1", "name": "Syrus"}}
assert result.data == {
'me': {
'id': '1',
'name': 'Syrus',
}
}
if __name__ == "__main__":
result = schema.execute(query, context={"user": User(id="X", name="Console")})
print(result.data["me"])
if __name__ == '__main__':
result = schema.execute(query, context_value={'user': User(id='X', name='Console')})
print(result.data['me'])

View File

@ -8,14 +8,15 @@ class Patron(graphene.ObjectType):
class Query(graphene.ObjectType):
patron = graphene.Field(Patron)
def resolve_patron(root, info):
return Patron(id=1, name="Syrus", age=27)
def resolve_patron(self, info):
return Patron(id=1, name='Syrus', age=27)
schema = graphene.Schema(query=Query)
query = """
query = '''
query something{
patron {
id
@ -23,15 +24,21 @@ query = """
age
}
}
"""
'''
def test_query():
result = schema.execute(query)
assert not result.errors
assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}}
assert result.data == {
'patron': {
'id': '1',
'name': 'Syrus',
'age': 27,
}
}
if __name__ == "__main__":
if __name__ == '__main__':
result = schema.execute(query)
print(result.data["patron"])
print(result.data['patron'])

View File

@ -4,73 +4,75 @@ droid_data = {}
def setup():
from .schema import Human, Droid
global human_data, droid_data
luke = Human(
id="1000",
name="Luke Skywalker",
friends=["1002", "1003", "2000", "2001"],
id='1000',
name='Luke Skywalker',
friends=['1002', '1003', '2000', '2001'],
appears_in=[4, 5, 6],
home_planet="Tatooine",
home_planet='Tatooine',
)
vader = Human(
id="1001",
name="Darth Vader",
friends=["1004"],
id='1001',
name='Darth Vader',
friends=['1004'],
appears_in=[4, 5, 6],
home_planet="Tatooine",
home_planet='Tatooine',
)
han = Human(
id="1002",
name="Han Solo",
friends=["1000", "1003", "2001"],
id='1002',
name='Han Solo',
friends=['1000', '1003', '2001'],
appears_in=[4, 5, 6],
home_planet=None,
)
leia = Human(
id="1003",
name="Leia Organa",
friends=["1000", "1002", "2000", "2001"],
id='1003',
name='Leia Organa',
friends=['1000', '1002', '2000', '2001'],
appears_in=[4, 5, 6],
home_planet="Alderaan",
home_planet='Alderaan',
)
tarkin = Human(
id="1004",
name="Wilhuff Tarkin",
friends=["1001"],
id='1004',
name='Wilhuff Tarkin',
friends=['1001'],
appears_in=[4],
home_planet=None,
)
human_data = {
"1000": luke,
"1001": vader,
"1002": han,
"1003": leia,
"1004": tarkin,
'1000': luke,
'1001': vader,
'1002': han,
'1003': leia,
'1004': tarkin,
}
c3po = Droid(
id="2000",
name="C-3PO",
friends=["1000", "1002", "1003", "2001"],
id='2000',
name='C-3PO',
friends=['1000', '1002', '1003', '2001'],
appears_in=[4, 5, 6],
primary_function="Protocol",
primary_function='Protocol',
)
r2d2 = Droid(
id="2001",
name="R2-D2",
friends=["1000", "1002", "1003"],
id='2001',
name='R2-D2',
friends=['1000', '1002', '1003'],
appears_in=[4, 5, 6],
primary_function="Astromech",
primary_function='Astromech',
)
droid_data = {"2000": c3po, "2001": r2d2}
droid_data = {
'2000': c3po,
'2001': r2d2,
}
def get_character(id):
@ -83,8 +85,8 @@ def get_friends(character):
def get_hero(episode):
if episode == 5:
return human_data["1000"]
return droid_data["2001"]
return human_data['1000']
return droid_data['2001']
def get_human(id):

View File

@ -21,31 +21,37 @@ class Character(graphene.Interface):
class Human(graphene.ObjectType):
class Meta:
interfaces = (Character,)
class Meta:
interfaces = (Character, )
home_planet = graphene.String()
class Droid(graphene.ObjectType):
class Meta:
interfaces = (Character,)
class Meta:
interfaces = (Character, )
primary_function = graphene.String()
class Query(graphene.ObjectType):
hero = graphene.Field(Character, episode=Episode())
human = graphene.Field(Human, id=graphene.String())
droid = graphene.Field(Droid, id=graphene.String())
hero = graphene.Field(Character,
episode=Episode()
)
human = graphene.Field(Human,
id=graphene.String()
)
droid = graphene.Field(Droid,
id=graphene.String()
)
def resolve_hero(root, info, episode=None):
def resolve_hero(self, info, episode=None):
return get_hero(episode)
def resolve_human(root, info, id):
def resolve_human(self, info, id):
return get_human(id)
def resolve_droid(root, info, id):
def resolve_droid(self, info, id):
return get_droid(id)

View File

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_hero_name_query 1'] = {
'data': {
'hero': {
'name': 'R2-D2'
}
}
}
snapshots['test_hero_name_and_friends_query 1'] = {
'data': {
'hero': {
'id': '2001',
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Han Solo'
},
{
'name': 'Leia Organa'
}
]
}
}
}
snapshots['test_nested_query 1'] = {
'data': {
'hero': {
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Han Solo'
},
{
'name': 'Leia Organa'
},
{
'name': 'C-3PO'
},
{
'name': 'R2-D2'
}
]
},
{
'name': 'Han Solo',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Leia Organa'
},
{
'name': 'R2-D2'
}
]
},
{
'name': 'Leia Organa',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Han Solo'
},
{
'name': 'C-3PO'
},
{
'name': 'R2-D2'
}
]
}
]
}
}
}
snapshots['test_fetch_luke_query 1'] = {
'data': {
'human': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_some_id_query 1'] = {
'data': {
'human': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_some_id_query2 1'] = {
'data': {
'human': {
'name': 'Han Solo'
}
}
}
snapshots['test_invalid_id_query 1'] = {
'data': {
'human': None
}
}
snapshots['test_fetch_luke_aliased 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_luke_and_leia_aliased 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker'
},
'leia': {
'name': 'Leia Organa'
}
}
}
snapshots['test_duplicate_fields 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine'
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan'
}
}
}
snapshots['test_use_fragment 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine'
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan'
}
}
}
snapshots['test_check_type_of_r2 1'] = {
'data': {
'hero': {
'__typename': 'Droid',
'name': 'R2-D2'
}
}
}
snapshots['test_check_type_of_luke 1'] = {
'data': {
'hero': {
'__typename': 'Human',
'name': 'Luke Skywalker'
}
}
}

View File

@ -8,19 +8,19 @@ setup()
client = Client(schema)
def test_hero_name_query():
result = client.execute("""
def test_hero_name_query(snapshot):
query = '''
query HeroNameQuery {
hero {
name
}
}
""")
assert result == {"data": {"hero": {"name": "R2-D2"}}}
'''
snapshot.assert_match(client.execute(query))
def test_hero_name_and_friends_query():
result = client.execute("""
def test_hero_name_and_friends_query(snapshot):
query = '''
query HeroNameAndFriendsQuery {
hero {
id
@ -30,24 +30,12 @@ def test_hero_name_and_friends_query():
}
}
}
""")
assert result == {
"data": {
"hero": {
"id": "2001",
"name": "R2-D2",
"friends": [
{"name": "Luke Skywalker"},
{"name": "Han Solo"},
{"name": "Leia Organa"},
],
}
}
}
'''
snapshot.assert_match(client.execute(query))
def test_nested_query():
result = client.execute("""
def test_nested_query(snapshot):
query = '''
query NestedQuery {
hero {
name
@ -60,113 +48,76 @@ def test_nested_query():
}
}
}
""")
assert result == {
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{"name": "Han Solo"},
{"name": "Leia Organa"},
{"name": "C-3PO"},
{"name": "R2-D2"},
],
},
{
"name": "Han Solo",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{"name": "Luke Skywalker"},
{"name": "Leia Organa"},
{"name": "R2-D2"},
],
},
{
"name": "Leia Organa",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{"name": "Luke Skywalker"},
{"name": "Han Solo"},
{"name": "C-3PO"},
{"name": "R2-D2"},
],
},
],
}
}
}
'''
snapshot.assert_match(client.execute(query))
def test_fetch_luke_query():
result = client.execute("""
def test_fetch_luke_query(snapshot):
query = '''
query FetchLukeQuery {
human(id: "1000") {
name
}
}
""")
assert result == {"data": {"human": {"name": "Luke Skywalker"}}}
'''
snapshot.assert_match(client.execute(query))
def test_fetch_some_id_query():
result = client.execute(
"""
def test_fetch_some_id_query(snapshot):
query = '''
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
""",
variables={"someId": "1000"},
)
assert result == {"data": {"human": {"name": "Luke Skywalker"}}}
'''
params = {
'someId': '1000',
}
snapshot.assert_match(client.execute(query, variable_values=params))
def test_fetch_some_id_query2():
result = client.execute(
"""
def test_fetch_some_id_query2(snapshot):
query = '''
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
""",
variables={"someId": "1002"},
)
assert result == {"data": {"human": {"name": "Han Solo"}}}
'''
params = {
'someId': '1002',
}
snapshot.assert_match(client.execute(query, variable_values=params))
def test_invalid_id_query():
result = client.execute(
"""
def test_invalid_id_query(snapshot):
query = '''
query humanQuery($id: String!) {
human(id: $id) {
name
}
}
""",
variables={"id": "not a valid id"},
)
assert result == {"data": {"human": None}}
'''
params = {
'id': 'not a valid id',
}
snapshot.assert_match(client.execute(query, variable_values=params))
def test_fetch_luke_aliased():
result = client.execute("""
def test_fetch_luke_aliased(snapshot):
query = '''
query FetchLukeAliased {
luke: human(id: "1000") {
name
}
}
""")
assert result == {"data": {"luke": {"name": "Luke Skywalker"}}}
'''
snapshot.assert_match(client.execute(query))
def test_fetch_luke_and_leia_aliased():
result = client.execute("""
def test_fetch_luke_and_leia_aliased(snapshot):
query = '''
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
name
@ -175,14 +126,12 @@ def test_fetch_luke_and_leia_aliased():
name
}
}
""")
assert result == {
"data": {"luke": {"name": "Luke Skywalker"}, "leia": {"name": "Leia Organa"}}
}
'''
snapshot.assert_match(client.execute(query))
def test_duplicate_fields():
result = client.execute("""
def test_duplicate_fields(snapshot):
query = '''
query DuplicateFields {
luke: human(id: "1000") {
name
@ -193,17 +142,12 @@ def test_duplicate_fields():
homePlanet
}
}
""")
assert result == {
"data": {
"luke": {"name": "Luke Skywalker", "homePlanet": "Tatooine"},
"leia": {"name": "Leia Organa", "homePlanet": "Alderaan"},
}
}
'''
snapshot.assert_match(client.execute(query))
def test_use_fragment():
result = client.execute("""
def test_use_fragment(snapshot):
query = '''
query UseFragment {
luke: human(id: "1000") {
...HumanFragment
@ -216,36 +160,29 @@ def test_use_fragment():
name
homePlanet
}
""")
assert result == {
"data": {
"luke": {"name": "Luke Skywalker", "homePlanet": "Tatooine"},
"leia": {"name": "Leia Organa", "homePlanet": "Alderaan"},
}
}
'''
snapshot.assert_match(client.execute(query))
def test_check_type_of_r2():
result = client.execute("""
def test_check_type_of_r2(snapshot):
query = '''
query CheckTypeOfR2 {
hero {
__typename
name
}
}
""")
assert result == {"data": {"hero": {"__typename": "Droid", "name": "R2-D2"}}}
'''
snapshot.assert_match(client.execute(query))
def test_check_type_of_luke():
result = client.execute("""
def test_check_type_of_luke(snapshot):
query = '''
query CheckTypeOfLuke {
hero(episode: EMPIRE) {
__typename
name
}
}
""")
assert result == {
"data": {"hero": {"__typename": "Human", "name": "Luke Skywalker"}}
}
'''
snapshot.assert_match(client.execute(query))

View File

@ -5,67 +5,101 @@ def setup():
global data
from .schema import Ship, Faction
xwing = Ship(
id='1',
name='X-Wing',
)
xwing = Ship(id="1", name="X-Wing")
ywing = Ship(
id='2',
name='Y-Wing',
)
ywing = Ship(id="2", name="Y-Wing")
awing = Ship(id="3", name="A-Wing")
awing = Ship(
id='3',
name='A-Wing',
)
# Yeah, technically it's Corellian. But it flew in the service of the rebels,
# so for the purposes of this demo it's a rebel ship.
falcon = Ship(id="4", name="Millennium Falcon")
homeOne = Ship(id="5", name="Home One")
tieFighter = Ship(id="6", name="TIE Fighter")
tieInterceptor = Ship(id="7", name="TIE Interceptor")
executor = Ship(id="8", name="Executor")
rebels = Faction(
id="1", name="Alliance to Restore the Republic", ships=["1", "2", "3", "4", "5"]
falcon = Ship(
id='4',
name='Millenium Falcon',
)
empire = Faction(id="2", name="Galactic Empire", ships=["6", "7", "8"])
homeOne = Ship(
id='5',
name='Home One',
)
tieFighter = Ship(
id='6',
name='TIE Fighter',
)
tieInterceptor = Ship(
id='7',
name='TIE Interceptor',
)
executor = Ship(
id='8',
name='Executor',
)
rebels = Faction(
id='1',
name='Alliance to Restore the Republic',
ships=['1', '2', '3', '4', '5']
)
empire = Faction(
id='2',
name='Galactic Empire',
ships=['6', '7', '8']
)
data = {
"Faction": {"1": rebels, "2": empire},
"Ship": {
"1": xwing,
"2": ywing,
"3": awing,
"4": falcon,
"5": homeOne,
"6": tieFighter,
"7": tieInterceptor,
"8": executor,
'Faction': {
'1': rebels,
'2': empire
},
'Ship': {
'1': xwing,
'2': ywing,
'3': awing,
'4': falcon,
'5': homeOne,
'6': tieFighter,
'7': tieInterceptor,
'8': executor
}
}
def create_ship(ship_name, faction_id):
from .schema import Ship
next_ship = len(data["Ship"].keys()) + 1
new_ship = Ship(id=str(next_ship), name=ship_name)
data["Ship"][new_ship.id] = new_ship
data["Faction"][faction_id].ships.append(new_ship.id)
next_ship = len(data['Ship'].keys()) + 1
new_ship = Ship(
id=str(next_ship),
name=ship_name
)
data['Ship'][new_ship.id] = new_ship
data['Faction'][faction_id].ships.append(new_ship.id)
return new_ship
def get_ship(_id):
return data["Ship"][_id]
return data['Ship'][_id]
def get_faction(_id):
return data["Faction"][_id]
return data['Faction'][_id]
def get_rebels():
return get_faction("1")
return get_faction('1')
def get_empire():
return get_faction("2")
return get_faction('2')

View File

@ -5,12 +5,12 @@ from .data import create_ship, get_empire, get_faction, get_rebels, get_ship
class Ship(graphene.ObjectType):
"""A ship in the Star Wars saga"""
'''A ship in the Star Wars saga'''
class Meta:
interfaces = (relay.Node,)
interfaces = (relay.Node, )
name = graphene.String(description="The name of the ship.")
name = graphene.String(description='The name of the ship.')
@classmethod
def get_node(cls, info, id):
@ -18,20 +18,19 @@ class Ship(graphene.ObjectType):
class ShipConnection(relay.Connection):
class Meta:
node = Ship
class Faction(graphene.ObjectType):
"""A faction in the Star Wars saga"""
'''A faction in the Star Wars saga'''
class Meta:
interfaces = (relay.Node,)
interfaces = (relay.Node, )
name = graphene.String(description="The name of the faction.")
ships = relay.ConnectionField(
ShipConnection, description="The ships used by the faction."
)
name = graphene.String(description='The name of the faction.')
ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.')
def resolve_ships(self, info, **args):
# Transform the instance ship_ids into real instances
@ -43,6 +42,7 @@ class Faction(graphene.ObjectType):
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
@ -51,9 +51,7 @@ class IntroduceShip(relay.ClientIDMutation):
faction = graphene.Field(Faction)
@classmethod
def mutate_and_get_payload(
cls, root, info, ship_name, faction_id, client_mutation_id=None
):
def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None):
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
@ -64,10 +62,10 @@ class Query(graphene.ObjectType):
empire = graphene.Field(Faction)
node = relay.Node.Field()
def resolve_rebels(root, info):
def resolve_rebels(self, info):
return get_rebels()
def resolve_empire(root, info):
def resolve_empire(self, info):
return get_empire()

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_correct_fetch_first_ship_rebels 1'] = {
'data': {
'rebels': {
'name': 'Alliance to Restore the Republic',
'ships': {
'pageInfo': {
'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'hasNextPage': True,
'hasPreviousPage': False
},
'edges': [
{
'cursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'node': {
'name': 'X-Wing'
}
}
]
}
}
}
}

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_mutations 1'] = {
'data': {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [
{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
},
{
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
},
{
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
},
{
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
},
{
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
},
{
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}
]
}
}
}
}
}

View File

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_correctly_fetches_id_name_rebels 1'] = {
'data': {
'rebels': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
}
snapshots['test_correctly_refetches_rebels 1'] = {
'data': {
'node': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
}
snapshots['test_correctly_fetches_id_name_empire 1'] = {
'data': {
'empire': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
}
snapshots['test_correctly_refetches_empire 1'] = {
'data': {
'node': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
}
snapshots['test_correctly_refetches_xwing 1'] = {
'data': {
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
}
snapshots['test_str_schema 1'] = '''schema {
query: Query
mutation: Mutation
}
type Faction implements Node {
id: ID!
name: String
ships(before: String, after: String, first: Int, last: Int): ShipConnection
}
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String
}
type IntroduceShipPayload {
ship: Ship
faction: Faction
clientMutationId: String
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
rebels: Faction
empire: Faction
node(id: ID!): Node
}
type Ship implements Node {
id: ID!
name: String
}
type ShipConnection {
pageInfo: PageInfo!
edges: [ShipEdge]!
}
type ShipEdge {
node: Ship
cursor: String!
}
'''

View File

@ -8,46 +8,26 @@ setup()
client = Client(schema)
def test_correct_fetch_first_ship_rebels():
result = client.execute("""
query RebelsShipsQuery {
rebels {
name,
ships(first: 1) {
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
name
}
}
def test_correct_fetch_first_ship_rebels(snapshot):
query = '''
query RebelsShipsQuery {
rebels {
name,
ships(first: 1) {
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
name
}
}
}
""")
assert result == {
"data": {
"rebels": {
"name": "Alliance to Restore the Republic",
"ships": {
"pageInfo": {
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"hasNextPage": True,
"hasPreviousPage": False,
},
"edges": [
{
"cursor": "YXJyYXljb25uZWN0aW9uOjA=",
"node": {"name": "X-Wing"},
}
],
},
}
}
}
}
'''
snapshot.assert_match(client.execute(query))

View File

@ -8,45 +8,26 @@ setup()
client = Client(schema)
def test_mutations():
result = client.execute("""
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
id
name
}
faction {
name
ships {
edges {
node {
id
name
}
}
def test_mutations(snapshot):
query = '''
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
id
name
}
faction {
name
ships {
edges {
node {
id
name
}
}
}
}
""")
assert result == {
"data": {
"introduceShip": {
"ship": {"id": "U2hpcDo5", "name": "Peter"},
"faction": {
"name": "Alliance to Restore the Republic",
"ships": {
"edges": [
{"node": {"id": "U2hpcDox", "name": "X-Wing"}},
{"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
{"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
{"node": {"id": "U2hpcDo0", "name": "Millennium Falcon"}},
{"node": {"id": "U2hpcDo1", "name": "Home One"}},
{"node": {"id": "U2hpcDo5", "name": "Peter"}},
]
},
},
}
}
}
}
'''
snapshot.assert_match(client.execute(query))

View File

@ -1,5 +1,3 @@
import textwrap
from graphene.test import Client
from ..data import setup
@ -10,115 +8,24 @@ setup()
client = Client(schema)
def test_str_schema():
assert str(schema).strip() == textwrap.dedent(
'''\
type Query {
rebels: Faction
empire: Faction
node(
"""The ID of the object"""
id: ID!
): Node
}
"""A faction in the Star Wars saga"""
type Faction implements Node {
"""The ID of the object"""
id: ID!
"""The name of the faction."""
name: String
"""The ships used by the faction."""
ships(before: String, after: String, first: Int, last: Int): ShipConnection
}
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type ShipConnection {
"""Pagination data for this connection."""
pageInfo: PageInfo!
"""Contains the nodes in this connection."""
edges: [ShipEdge]!
}
"""
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
"""
type PageInfo {
"""When paginating forwards, are there more items?"""
hasNextPage: Boolean!
"""When paginating backwards, are there more items?"""
hasPreviousPage: Boolean!
"""When paginating backwards, the cursor to continue."""
startCursor: String
"""When paginating forwards, the cursor to continue."""
endCursor: String
}
"""A Relay edge containing a `Ship` and its cursor."""
type ShipEdge {
"""The item at the end of the edge"""
node: Ship
"""A cursor for use in pagination"""
cursor: String!
}
"""A ship in the Star Wars saga"""
type Ship implements Node {
"""The ID of the object"""
id: ID!
"""The name of the ship."""
name: String
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
type IntroduceShipPayload {
ship: Ship
faction: Faction
clientMutationId: String
}
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String
}'''
)
def test_str_schema(snapshot):
snapshot.assert_match(str(schema))
def test_correctly_fetches_id_name_rebels():
result = client.execute("""
def test_correctly_fetches_id_name_rebels(snapshot):
query = '''
query RebelsQuery {
rebels {
id
name
}
}
""")
assert result == {
"data": {
"rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
}
'''
snapshot.assert_match(client.execute(query))
def test_correctly_refetches_rebels():
result = client.execute("""
def test_correctly_refetches_rebels(snapshot):
query = '''
query RebelsRefetchQuery {
node(id: "RmFjdGlvbjox") {
id
@ -127,30 +34,24 @@ def test_correctly_refetches_rebels():
}
}
}
""")
assert result == {
"data": {
"node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
}
'''
snapshot.assert_match(client.execute(query))
def test_correctly_fetches_id_name_empire():
result = client.execute("""
def test_correctly_fetches_id_name_empire(snapshot):
query = '''
query EmpireQuery {
empire {
id
name
}
}
""")
assert result == {
"data": {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
}
'''
snapshot.assert_match(client.execute(query))
def test_correctly_refetches_empire():
result = client.execute("""
def test_correctly_refetches_empire(snapshot):
query = '''
query EmpireRefetchQuery {
node(id: "RmFjdGlvbjoy") {
id
@ -159,14 +60,12 @@ def test_correctly_refetches_empire():
}
}
}
""")
assert result == {
"data": {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
}
'''
snapshot.assert_match(client.execute(query))
def test_correctly_refetches_xwing():
result = client.execute("""
def test_correctly_refetches_xwing(snapshot):
query = '''
query XWingRefetchQuery {
node(id: "U2hpcDox") {
id
@ -175,5 +74,5 @@ def test_correctly_refetches_xwing():
}
}
}
""")
assert result == {"data": {"node": {"id": "U2hpcDox", "name": "X-Wing"}}}
'''
snapshot.assert_match(client.execute(query))

View File

@ -1,98 +1,82 @@
from .pyutils.version import get_version
from .types import (
AbstractType,
ObjectType,
InputObjectType,
Interface,
Mutation,
Field,
InputField,
Schema,
Scalar,
String, ID, Int, Float, Boolean,
Date, DateTime, Time,
JSONString,
UUID,
List, NonNull,
Enum,
Argument,
Dynamic,
Union,
Context,
ResolveInfo
)
from .relay import (
BaseGlobalIDType,
Node,
is_node,
GlobalID,
ClientIDMutation,
Connection,
ConnectionField,
DefaultGlobalIDType,
GlobalID,
Node,
PageInfo,
SimpleGlobalIDType,
UUIDGlobalIDType,
is_node,
PageInfo
)
from .types import (
ID,
UUID,
Argument,
Base64,
BigInt,
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.module_loading import lazy_import
from .utils.resolve_only_args import resolve_only_args
from .utils.module_loading import lazy_import
VERSION = (3, 4, 3, "final", 0)
VERSION = (2, 1, 2, 'final', 0)
__version__ = get_version(VERSION)
__all__ = [
"__version__",
"Argument",
"Base64",
"BigInt",
"BaseGlobalIDType",
"Boolean",
"ClientIDMutation",
"Connection",
"ConnectionField",
"Context",
"Date",
"DateTime",
"Decimal",
"DefaultGlobalIDType",
"Dynamic",
"Enum",
"Field",
"Float",
"GlobalID",
"ID",
"InputField",
"InputObjectType",
"Int",
"Interface",
"JSONString",
"List",
"Mutation",
"Node",
"NonNull",
"ObjectType",
"PageInfo",
"ResolveInfo",
"Scalar",
"Schema",
"SimpleGlobalIDType",
"String",
"Time",
"Union",
"UUID",
"UUIDGlobalIDType",
"is_node",
"lazy_import",
"resolve_only_args",
'__version__',
'ObjectType',
'InputObjectType',
'Interface',
'Mutation',
'Field',
'InputField',
'Schema',
'Scalar',
'String',
'ID',
'Int',
'Float',
'Enum',
'Boolean',
'Date',
'DateTime',
'Time',
'JSONString',
'UUID',
'List',
'NonNull',
'Argument',
'Dynamic',
'Union',
'resolve_only_args',
'Node',
'is_node',
'GlobalID',
'ClientIDMutation',
'Connection',
'ConnectionField',
'PageInfo',
'lazy_import',
'Context',
'ResolveInfo',
# Deprecated
'AbstractType',
]

View File

@ -0,0 +1,20 @@
from __future__ import absolute_import
import six
try:
from enum import Enum
except ImportError:
from .enum import Enum
try:
from inspect import signature
except ImportError:
from .signature import signature
if six.PY2:
def func_name(func):
return func.func_name
else:
def func_name(func):
return func.__name__

873
graphene/pyutils/enum.py Normal file
View File

@ -0,0 +1,873 @@
"""Python Enumerations"""
import sys as _sys
__all__ = ['Enum', 'IntEnum', 'unique']
version = 1, 1, 6
pyver = float('%s.%s' % _sys.version_info[:2])
try:
any
except NameError:
def any(iterable):
for element in iterable:
if element:
return True
return False
try:
from collections import OrderedDict
except ImportError:
OrderedDict = None
try:
basestring
except NameError:
# In Python 2 basestring is the ancestor of both str and unicode
# in Python 3 it's just str, but was missing in 3.1
basestring = str
try:
unicode
except NameError:
# In Python 3 unicode no longer exists (it's just str)
unicode = str
class _RouteClassAttributeToGetattr(object):
"""Route attribute access on a class to __getattr__.
This is a descriptor, used to define attributes that act differently when
accessed through an instance and through a class. Instance access remains
normal, but access to an attribute through a class will be routed to the
class's __getattr__ method; this is done by raising AttributeError.
"""
def __init__(self, fget=None):
self.fget = fget
def __get__(self, instance, ownerclass=None):
if instance is None:
raise AttributeError()
return self.fget(instance)
def __set__(self, instance, value):
raise AttributeError("can't set attribute")
def __delete__(self, instance):
raise AttributeError("can't delete attribute")
def _is_descriptor(obj):
"""Returns True if obj is a descriptor, False otherwise."""
return (
hasattr(obj, '__get__') or
hasattr(obj, '__set__') or
hasattr(obj, '__delete__'))
def _is_dunder(name):
"""Returns True if a __dunder__ name, False otherwise."""
return (len(name) > 4 and
name[:2] == name[-2:] == '__' and
name[2:3] != '_' and
name[-3:-2] != '_')
def _is_sunder(name):
"""Returns True if a _sunder_ name, False otherwise."""
return (len(name) > 2 and
name[0] == name[-1] == '_' and
name[1:2] != '_' and
name[-2:-1] != '_')
def _make_class_unpicklable(cls):
"""Make the given class un-picklable."""
def _break_on_call_reduce(self, protocol=None):
raise TypeError('%r cannot be pickled' % self)
cls.__reduce_ex__ = _break_on_call_reduce
cls.__module__ = '<unknown>'
class _EnumDict(OrderedDict):
"""Track enum member order and ensure member names are not reused.
EnumMeta will use the names found in self._member_names as the
enumeration member names.
"""
def __init__(self):
super(_EnumDict, self).__init__()
self._member_names = []
def __setitem__(self, key, value):
"""Changes anything not dundered or not a descriptor.
If a descriptor is added with the same name as an enum member, the name
is removed from _member_names (this may leave a hole in the numerical
sequence of values).
If an enum member name is used twice, an error is raised; duplicate
values are not checked for.
Single underscore (sunder) names are reserved.
Note: in 3.x __order__ is simply discarded as a not necessary piece
leftover from 2.x
"""
if pyver >= 3.0 and key in ('_order_', '__order__'):
return
elif key == '__order__':
key = '_order_'
if _is_sunder(key):
if key != '_order_':
raise ValueError('_names_ are reserved for future Enum use')
elif _is_dunder(key):
pass
elif key in self._member_names:
# descriptor overwriting an enum?
raise TypeError('Attempted to reuse key: %r' % key)
elif not _is_descriptor(value):
if key in self:
# enum overwriting a descriptor?
raise TypeError('Key already defined as: %r' % self[key])
self._member_names.append(key)
super(_EnumDict, self).__setitem__(key, value)
# Dummy value for Enum as EnumMeta explicity checks for it, but of course until
# EnumMeta finishes running the first time the Enum class doesn't exist. This
# is also why there are checks in EnumMeta like `if Enum is not None`
Enum = None
class EnumMeta(type):
"""Metaclass for Enum"""
@classmethod
def __prepare__(metacls, cls, bases):
return _EnumDict()
def __new__(metacls, cls, bases, classdict):
# an Enum class is final once enumeration items have been defined; it
# cannot be mixed with other types (int, float, etc.) if it has an
# inherited __new__ unless a new __new__ is defined (or the resulting
# class will fail).
if isinstance(classdict, dict):
original_dict = classdict
classdict = _EnumDict()
for k, v in original_dict.items():
classdict[k] = v
member_type, first_enum = metacls._get_mixins_(bases)
__new__, save_new, use_args = metacls._find_new_(classdict, member_type,
first_enum)
# save enum items into separate mapping so they don't get baked into
# the new class
members = {k: classdict[k] for k in classdict._member_names}
for name in classdict._member_names:
del classdict[name]
# py2 support for definition order
_order_ = classdict.get('_order_')
if _order_ is None:
if pyver < 3.0:
try:
_order_ = [name for (name, value) in sorted(members.items(), key=lambda item: item[1])]
except TypeError:
_order_ = [name for name in sorted(members.keys())]
else:
_order_ = classdict._member_names
else:
del classdict['_order_']
if pyver < 3.0:
_order_ = _order_.replace(',', ' ').split()
aliases = [name for name in members if name not in _order_]
_order_ += aliases
# check for illegal enum names (any others?)
invalid_names = set(members) & {'mro'}
if invalid_names:
raise ValueError('Invalid enum member name(s): %s' % (
', '.join(invalid_names), ))
# save attributes from super classes so we know if we can take
# the shortcut of storing members in the class dict
base_attributes = {a for b in bases for a in b.__dict__}
# create our new Enum type
enum_class = super(EnumMeta, metacls).__new__(metacls, cls, bases, classdict)
enum_class._member_names_ = [] # names in random order
if OrderedDict is not None:
enum_class._member_map_ = OrderedDict()
else:
enum_class._member_map_ = {} # name->value map
enum_class._member_type_ = member_type
# Reverse value->name map for hashable values.
enum_class._value2member_map_ = {}
# instantiate them, checking for duplicates as we go
# we instantiate first instead of checking for duplicates first in case
# a custom __new__ is doing something funky with the values -- such as
# auto-numbering ;)
if __new__ is None:
__new__ = enum_class.__new__
for member_name in _order_:
value = members[member_name]
if not isinstance(value, tuple):
args = (value, )
else:
args = value
if member_type is tuple: # special case for tuple enums
args = (args, ) # wrap it one more time
if not use_args or not args:
enum_member = __new__(enum_class)
if not hasattr(enum_member, '_value_'):
enum_member._value_ = value
else:
enum_member = __new__(enum_class, *args)
if not hasattr(enum_member, '_value_'):
enum_member._value_ = member_type(*args)
value = enum_member._value_
enum_member._name_ = member_name
enum_member.__objclass__ = enum_class
enum_member.__init__(*args)
# If another member with the same value was already defined, the
# new member becomes an alias to the existing one.
for name, canonical_member in enum_class._member_map_.items():
if canonical_member.value == enum_member._value_:
enum_member = canonical_member
break
else:
# Aliases don't appear in member names (only in __members__).
enum_class._member_names_.append(member_name)
# performance boost for any member that would not shadow
# a DynamicClassAttribute (aka _RouteClassAttributeToGetattr)
if member_name not in base_attributes:
setattr(enum_class, member_name, enum_member)
# now add to _member_map_
enum_class._member_map_[member_name] = enum_member
try:
# This may fail if value is not hashable. We can't add the value
# to the map, and by-value lookups for this value will be
# linear.
enum_class._value2member_map_[value] = enum_member
except TypeError:
pass
# If a custom type is mixed into the Enum, and it does not know how
# to pickle itself, pickle.dumps will succeed but pickle.loads will
# fail. Rather than have the error show up later and possibly far
# from the source, sabotage the pickle protocol for this class so
# that pickle.dumps also fails.
#
# However, if the new class implements its own __reduce_ex__, do not
# sabotage -- it's on them to make sure it works correctly. We use
# __reduce_ex__ instead of any of the others as it is preferred by
# pickle over __reduce__, and it handles all pickle protocols.
unpicklable = False
if '__reduce_ex__' not in classdict:
if member_type is not object:
methods = ('__getnewargs_ex__', '__getnewargs__',
'__reduce_ex__', '__reduce__')
if not any(m in member_type.__dict__ for m in methods):
_make_class_unpicklable(enum_class)
unpicklable = True
# double check that repr and friends are not the mixin's or various
# things break (such as pickle)
for name in ('__repr__', '__str__', '__format__', '__reduce_ex__'):
class_method = getattr(enum_class, name)
getattr(member_type, name, None)
enum_method = getattr(first_enum, name, None)
if name not in classdict and class_method is not enum_method:
if name == '__reduce_ex__' and unpicklable:
continue
setattr(enum_class, name, enum_method)
# method resolution and int's are not playing nice
# Python's less than 2.6 use __cmp__
if pyver < 2.6:
if issubclass(enum_class, int):
setattr(enum_class, '__cmp__', getattr(int, '__cmp__'))
elif pyver < 3.0:
if issubclass(enum_class, int):
for method in (
'__le__',
'__lt__',
'__gt__',
'__ge__',
'__eq__',
'__ne__',
'__hash__',
):
setattr(enum_class, method, getattr(int, method))
# replace any other __new__ with our own (as long as Enum is not None,
# anyway) -- again, this is to support pickle
if Enum is not None:
# if the user defined their own __new__, save it before it gets
# clobbered in case they subclass later
if save_new:
setattr(enum_class, '__member_new__', enum_class.__dict__['__new__'])
setattr(enum_class, '__new__', Enum.__dict__['__new__'])
return enum_class
def __bool__(cls):
"""
classes/types should always be True.
"""
return True
def __call__(cls, value, names=None, module=None, type=None, start=1):
"""Either returns an existing member, or creates a new enum class.
This method is used both when an enum class is given a value to match
to an enumeration member (i.e. Color(3)) and for the functional API
(i.e. Color = Enum('Color', names='red green blue')).
When used for the functional API: `module`, if set, will be stored in
the new class' __module__ attribute; `type`, if set, will be mixed in
as the first base class.
Note: if `module` is not set this routine will attempt to discover the
calling module by walking the frame stack; if this is unsuccessful
the resulting class will not be pickleable.
"""
if names is None: # simple value lookup
return cls.__new__(cls, value)
# otherwise, functional API: we're creating a new Enum type
return cls._create_(value, names, module=module, type=type, start=start)
def __contains__(cls, member):
return isinstance(member, cls) and member.name in cls._member_map_
def __delattr__(cls, attr):
# nicer error message when someone tries to delete an attribute
# (see issue19025).
if attr in cls._member_map_:
raise AttributeError(
"%s: cannot delete Enum member." % cls.__name__)
super(EnumMeta, cls).__delattr__(attr)
def __dir__(self):
return (['__class__', '__doc__', '__members__', '__module__'] +
self._member_names_)
@property
def __members__(cls):
"""Returns a mapping of member name->value.
This mapping lists all enum members, including aliases. Note that this
is a copy of the internal mapping.
"""
return cls._member_map_.copy()
def __getattr__(cls, name):
"""Return the enum member matching `name`
We use __getattr__ instead of descriptors or inserting into the enum
class' __dict__ in order to support `name` and `value` being both
properties for enum members (which live in the class' __dict__) and
enum members themselves.
"""
if _is_dunder(name):
raise AttributeError(name)
try:
return cls._member_map_[name]
except KeyError:
raise AttributeError(name)
def __getitem__(cls, name):
return cls._member_map_[name]
def __iter__(cls):
return (cls._member_map_[name] for name in cls._member_names_)
def __reversed__(cls):
return (cls._member_map_[name] for name in reversed(cls._member_names_))
def __len__(cls):
return len(cls._member_names_)
__nonzero__ = __bool__
def __repr__(cls):
return "<enum %r>" % cls.__name__
def __setattr__(cls, name, value):
"""Block attempts to reassign Enum members.
A simple assignment to the class namespace only changes one of the
several possible ways to get an Enum member from the Enum class,
resulting in an inconsistent Enumeration.
"""
member_map = cls.__dict__.get('_member_map_', {})
if name in member_map:
raise AttributeError('Cannot reassign members.')
super(EnumMeta, cls).__setattr__(name, value)
def _create_(cls, class_name, names=None, module=None, type=None, start=1):
"""Convenience method to create a new Enum class.
`names` can be:
* A string containing member names, separated either with spaces or
commas. Values are auto-numbered from 1.
* An iterable of member names. Values are auto-numbered from 1.
* An iterable of (member name, value) pairs.
* A mapping of member name -> value.
"""
if pyver < 3.0:
# if class_name is unicode, attempt a conversion to ASCII
if isinstance(class_name, unicode):
try:
class_name = class_name.encode('ascii')
except UnicodeEncodeError:
raise TypeError('%r is not representable in ASCII' % class_name)
metacls = cls.__class__
if type is None:
bases = (cls, )
else:
bases = (type, cls)
classdict = metacls.__prepare__(class_name, bases)
_order_ = []
# special processing needed for names?
if isinstance(names, basestring):
names = names.replace(',', ' ').split()
if isinstance(names, (tuple, list)) and isinstance(names[0], basestring):
names = [(e, i + start) for (i, e) in enumerate(names)]
# Here, names is either an iterable of (name, value) or a mapping.
item = None # in case names is empty
for item in names:
if isinstance(item, basestring):
member_name, member_value = item, names[item]
else:
member_name, member_value = item
classdict[member_name] = member_value
_order_.append(member_name)
# only set _order_ in classdict if name/value was not from a mapping
if not isinstance(item, basestring):
classdict['_order_'] = ' '.join(_order_)
enum_class = metacls.__new__(metacls, class_name, bases, classdict)
# TODO: replace the frame hack if a blessed way to know the calling
# module is ever developed
if module is None:
try:
module = _sys._getframe(2).f_globals['__name__']
except (AttributeError, ValueError):
pass
if module is None:
_make_class_unpicklable(enum_class)
else:
enum_class.__module__ = module
return enum_class
@staticmethod
def _get_mixins_(bases):
"""Returns the type for creating enum members, and the first inherited
enum class.
bases: the tuple of bases that was given to __new__
"""
if not bases or Enum is None:
return object, Enum
# double check that we are not subclassing a class with existing
# enumeration members; while we're at it, see if any other data
# type has been mixed in so we can use the correct __new__
member_type = first_enum = None
for base in bases:
if (base is not Enum and
issubclass(base, Enum) and
base._member_names_):
raise TypeError("Cannot extend enumerations")
# base is now the last base in bases
if not issubclass(base, Enum):
raise TypeError("new enumerations must be created as "
"`ClassName([mixin_type,] enum_type)`")
# get correct mix-in type (either mix-in type of Enum subclass, or
# first base if last base is Enum)
if not issubclass(bases[0], Enum):
member_type = bases[0] # first data type
first_enum = bases[-1] # enum type
else:
for base in bases[0].__mro__:
# most common: (IntEnum, int, Enum, object)
# possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
# <class 'int'>, <Enum 'Enum'>,
# <class 'object'>)
if issubclass(base, Enum):
if first_enum is None:
first_enum = base
else:
if member_type is None:
member_type = base
return member_type, first_enum
if pyver < 3.0:
@staticmethod
def _find_new_(classdict, member_type, first_enum):
"""Returns the __new__ to be used for creating the enum members.
classdict: the class dictionary given to __new__
member_type: the data type whose __new__ will be used by default
first_enum: enumeration to check for an overriding __new__
"""
# now find the correct __new__, checking to see of one was defined
# by the user; also check earlier enum classes in case a __new__ was
# saved as __member_new__
__new__ = classdict.get('__new__', None)
if __new__:
return None, True, True # __new__, save_new, use_args
N__new__ = getattr(None, '__new__')
O__new__ = getattr(object, '__new__')
if Enum is None:
E__new__ = N__new__
else:
E__new__ = Enum.__dict__['__new__']
# check all possibles for __member_new__ before falling back to
# __new__
for method in ('__member_new__', '__new__'):
for possible in (member_type, first_enum):
try:
target = possible.__dict__[method]
except (AttributeError, KeyError):
target = getattr(possible, method, None)
if target not in [
None,
N__new__,
O__new__,
E__new__,
]:
if method == '__member_new__':
classdict['__new__'] = target
return None, False, True
if isinstance(target, staticmethod):
target = target.__get__(member_type)
__new__ = target
break
if __new__ is not None:
break
else:
__new__ = object.__new__
# if a non-object.__new__ is used then whatever value/tuple was
# assigned to the enum member name will be passed to __new__ and to the
# new enum member's __init__
if __new__ is object.__new__:
use_args = False
else:
use_args = True
return __new__, False, use_args
else:
@staticmethod
def _find_new_(classdict, member_type, first_enum):
"""Returns the __new__ to be used for creating the enum members.
classdict: the class dictionary given to __new__
member_type: the data type whose __new__ will be used by default
first_enum: enumeration to check for an overriding __new__
"""
# now find the correct __new__, checking to see of one was defined
# by the user; also check earlier enum classes in case a __new__ was
# saved as __member_new__
__new__ = classdict.get('__new__', None)
# should __new__ be saved as __member_new__ later?
save_new = __new__ is not None
if __new__ is None:
# check all possibles for __member_new__ before falling back to
# __new__
for method in ('__member_new__', '__new__'):
for possible in (member_type, first_enum):
target = getattr(possible, method, None)
if target not in (
None,
None.__new__,
object.__new__,
Enum.__new__,
):
__new__ = target
break
if __new__ is not None:
break
else:
__new__ = object.__new__
# if a non-object.__new__ is used then whatever value/tuple was
# assigned to the enum member name will be passed to __new__ and to the
# new enum member's __init__
if __new__ is object.__new__:
use_args = False
else:
use_args = True
return __new__, save_new, use_args
########################################################
# In order to support Python 2 and 3 with a single
# codebase we have to create the Enum methods separately
# and then use the `type(name, bases, dict)` method to
# create the class.
########################################################
temp_enum_dict = {}
temp_enum_dict['__doc__'] = "Generic enumeration.\n\n Derive from this class to define new enumerations.\n\n"
def __new__(cls, value):
# all enum instances are actually created during class construction
# without calling this method; this method is called by the metaclass'
# __call__ (i.e. Color(3) ), and by pickle
if isinstance(value, cls):
# For lookups like Color(Color.red)
value = value.value
# return value
# by-value search for a matching enum member
# see if it's in the reverse mapping (for hashable values)
try:
if value in cls._value2member_map_:
return cls._value2member_map_[value]
except TypeError:
# not there, now do long search -- O(n) behavior
for member in cls._member_map_.values():
if member.value == value:
return member
raise ValueError("%s is not a valid %s" % (value, cls.__name__))
temp_enum_dict['__new__'] = __new__
del __new__
def __repr__(self):
return "<%s.%s: %r>" % (
self.__class__.__name__, self._name_, self._value_)
temp_enum_dict['__repr__'] = __repr__
del __repr__
def __str__(self):
return "%s.%s" % (self.__class__.__name__, self._name_)
temp_enum_dict['__str__'] = __str__
del __str__
if pyver >= 3.0:
def __dir__(self):
added_behavior = [
m
for cls in self.__class__.mro()
for m in cls.__dict__
if m[0] != '_' and m not in self._member_map_
]
return (['__class__', '__doc__', '__module__', ] + added_behavior)
temp_enum_dict['__dir__'] = __dir__
del __dir__
def __format__(self, format_spec):
# mixed-in Enums should use the mixed-in type's __format__, otherwise
# we can get strange results with the Enum name showing up instead of
# the value
# pure Enum branch
if self._member_type_ is object:
cls = str
val = str(self)
# mix-in branch
else:
cls = self._member_type_
val = self.value
return cls.__format__(val, format_spec)
temp_enum_dict['__format__'] = __format__
del __format__
####################################
# Python's less than 2.6 use __cmp__
if pyver < 2.6:
def __cmp__(self, other):
if isinstance(other, self.__class__):
if self is other:
return 0
return -1
return NotImplemented
raise TypeError("unorderable types: %s() and %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__cmp__'] = __cmp__
del __cmp__
else:
def __le__(self, other):
raise TypeError("unorderable types: %s() <= %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__le__'] = __le__
del __le__
def __lt__(self, other):
raise TypeError("unorderable types: %s() < %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__lt__'] = __lt__
del __lt__
def __ge__(self, other):
raise TypeError("unorderable types: %s() >= %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__ge__'] = __ge__
del __ge__
def __gt__(self, other):
raise TypeError("unorderable types: %s() > %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__gt__'] = __gt__
del __gt__
def __eq__(self, other):
if isinstance(other, self.__class__):
return self is other
return NotImplemented
temp_enum_dict['__eq__'] = __eq__
del __eq__
def __ne__(self, other):
if isinstance(other, self.__class__):
return self is not other
return NotImplemented
temp_enum_dict['__ne__'] = __ne__
del __ne__
def __hash__(self):
return hash(self._name_)
temp_enum_dict['__hash__'] = __hash__
del __hash__
def __reduce_ex__(self, proto):
return self.__class__, (self._value_, )
temp_enum_dict['__reduce_ex__'] = __reduce_ex__
del __reduce_ex__
# _RouteClassAttributeToGetattr is used to provide access to the `name`
# and `value` properties of enum members while keeping some measure of
# protection from modification, while still allowing for an enumeration
# to have members named `name` and `value`. This works because enumeration
# members are not set directly on the enum class -- __getattr__ is
# used to look them up.
@_RouteClassAttributeToGetattr
def name(self):
return self._name_
temp_enum_dict['name'] = name
del name
@_RouteClassAttributeToGetattr
def value(self):
return self._value_
temp_enum_dict['value'] = value
del value
@classmethod
def _convert(cls, name, module, filter, source=None):
"""
Create a new Enum subclass that replaces a collection of global constants
"""
# convert all constants from source (or module) that pass filter() to
# a new Enum called name, and export the enum and its members back to
# module;
# also, replace the __reduce_ex__ method so unpickling works in
# previous Python versions
module_globals = vars(_sys.modules[module])
if source:
source = vars(source)
else:
source = module_globals
members = {name: value for name, value in source.items() if filter(name)}
cls = cls(name, members, module=module)
cls.__reduce_ex__ = _reduce_ex_by_name
module_globals.update(cls.__members__)
module_globals[name] = cls
return cls
temp_enum_dict['_convert'] = _convert
del _convert
Enum = EnumMeta('Enum', (object, ), temp_enum_dict)
del temp_enum_dict
# Enum has now been created
###########################
class IntEnum(int, Enum):
"""Enum where members are also (and must be) ints"""
def _reduce_ex_by_name(self, proto):
return self.name
def unique(enumeration):
"""Class decorator that ensures only unique members exist in an enumeration."""
duplicates = []
for name, member in enumeration.__members__.items():
if name != member.name:
duplicates.append((name, member.name))
if duplicates:
duplicate_names = ', '.join(
["%s -> %s" % (alias, name) for (alias, name) in duplicates]
)
raise ValueError('duplicate names found in %r: %s' %
(enumeration, duplicate_names)
)
return enumeration

View File

@ -0,0 +1,19 @@
is_init_subclass_available = hasattr(object, '__init_subclass__')
if not is_init_subclass_available:
class InitSubclassMeta(type):
"""Metaclass that implements PEP 487 protocol"""
def __new__(cls, name, bases, ns, **kwargs):
__init_subclass__ = ns.pop('__init_subclass__', None)
if __init_subclass__:
__init_subclass__ = classmethod(__init_subclass__)
ns['__init_subclass__'] = __init_subclass__
return super(InitSubclassMeta, cls).__new__(cls, name, bases, ns, **kwargs)
def __init__(cls, name, bases, ns, **kwargs):
super(InitSubclassMeta, cls).__init__(name, bases, ns)
super_class = super(cls, cls)
if hasattr(super_class, '__init_subclass__'):
super_class.__init_subclass__.__func__(cls, **kwargs)
else:
InitSubclassMeta = type # type: ignore

View File

@ -0,0 +1,808 @@
# Copyright 2001-2013 Python Software Foundation; All Rights Reserved
"""Function signature objects for callables
Back port of Python 3.3's function signature tools from the inspect module,
modified to be compatible with Python 2.7 and 3.2+.
"""
from __future__ import absolute_import, division, print_function
import functools
import itertools
import re
import types
from collections import OrderedDict
__version__ = "0.4"
__all__ = ['BoundArguments', 'Parameter', 'Signature', 'signature']
_WrapperDescriptor = type(type.__call__)
_MethodWrapper = type(all.__call__)
_NonUserDefinedCallables = (_WrapperDescriptor,
_MethodWrapper,
types.BuiltinFunctionType)
def formatannotation(annotation, base_module=None):
if isinstance(annotation, type):
if annotation.__module__ in ('builtins', '__builtin__', base_module):
return annotation.__name__
return annotation.__module__ + '.' + annotation.__name__
return repr(annotation)
def _get_user_defined_method(cls, method_name, *nested):
try:
if cls is type:
return
meth = getattr(cls, method_name)
for name in nested:
meth = getattr(meth, name, meth)
except AttributeError:
return
else:
if not isinstance(meth, _NonUserDefinedCallables):
# Once '__signature__' will be added to 'C'-level
# callables, this check won't be necessary
return meth
def signature(obj):
'''Get a signature object for the passed callable.'''
if not callable(obj):
raise TypeError('{!r} is not a callable object'.format(obj))
if isinstance(obj, types.MethodType):
sig = signature(obj.__func__)
if obj.__self__ is None:
# Unbound method: the first parameter becomes positional-only
if sig.parameters:
first = sig.parameters.values()[0].replace(
kind=_POSITIONAL_ONLY)
return sig.replace(
parameters=(first,) + tuple(sig.parameters.values())[1:])
else:
return sig
else:
# In this case we skip the first parameter of the underlying
# function (usually `self` or `cls`).
return sig.replace(parameters=tuple(sig.parameters.values())[1:])
try:
sig = obj.__signature__
except AttributeError:
pass
else:
if sig is not None:
return sig
try:
# Was this function wrapped by a decorator?
wrapped = obj.__wrapped__
except AttributeError:
pass
else:
return signature(wrapped)
if isinstance(obj, types.FunctionType):
return Signature.from_function(obj)
if isinstance(obj, functools.partial):
sig = signature(obj.func)
new_params = OrderedDict(sig.parameters.items())
partial_args = obj.args or ()
partial_keywords = obj.keywords or {}
try:
ba = sig.bind_partial(*partial_args, **partial_keywords)
except TypeError as ex:
msg = 'partial object {!r} has incorrect arguments'.format(obj)
raise ValueError(msg)
for arg_name, arg_value in ba.arguments.items():
param = new_params[arg_name]
if arg_name in partial_keywords:
# We set a new default value, because the following code
# is correct:
#
# >>> def foo(a): print(a)
# >>> print(partial(partial(foo, a=10), a=20)())
# 20
# >>> print(partial(partial(foo, a=10), a=20)(a=30))
# 30
#
# So, with 'partial' objects, passing a keyword argument is
# like setting a new default value for the corresponding
# parameter
#
# We also mark this parameter with '_partial_kwarg'
# flag. Later, in '_bind', the 'default' value of this
# parameter will be added to 'kwargs', to simulate
# the 'functools.partial' real call.
new_params[arg_name] = param.replace(default=arg_value,
_partial_kwarg=True)
elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
not param._partial_kwarg):
new_params.pop(arg_name)
return sig.replace(parameters=new_params.values())
sig = None
if isinstance(obj, type):
# obj is a class or a metaclass
# First, let's see if it has an overloaded __call__ defined
# in its metaclass
call = _get_user_defined_method(type(obj), '__call__')
if call is not None:
sig = signature(call)
else:
# Now we check if the 'obj' class has a '__new__' method
new = _get_user_defined_method(obj, '__new__')
if new is not None:
sig = signature(new)
else:
# Finally, we should have at least __init__ implemented
init = _get_user_defined_method(obj, '__init__')
if init is not None:
sig = signature(init)
elif not isinstance(obj, _NonUserDefinedCallables):
# An object with __call__
# We also check that the 'obj' is not an instance of
# _WrapperDescriptor or _MethodWrapper to avoid
# infinite recursion (and even potential segfault)
call = _get_user_defined_method(type(obj), '__call__', 'im_func')
if call is not None:
sig = signature(call)
if sig is not None:
# For classes and objects we skip the first parameter of their
# __call__, __new__, or __init__ methods
return sig.replace(parameters=tuple(sig.parameters.values())[1:])
if isinstance(obj, types.BuiltinFunctionType):
# Raise a nicer error message for builtins
msg = 'no signature found for builtin function {!r}'.format(obj)
raise ValueError(msg)
raise ValueError('callable {!r} is not supported by signature'.format(obj))
class _void(object):
'''A private marker - used in Parameter & Signature'''
class _empty(object):
pass
class _ParameterKind(int):
def __new__(self, *args, **kwargs):
obj = int.__new__(self, *args)
obj._name = kwargs['name']
return obj
def __str__(self):
return self._name
def __repr__(self):
return '<_ParameterKind: {!r}>'.format(self._name)
_POSITIONAL_ONLY = _ParameterKind(0, name='POSITIONAL_ONLY')
_POSITIONAL_OR_KEYWORD = _ParameterKind(1, name='POSITIONAL_OR_KEYWORD')
_VAR_POSITIONAL = _ParameterKind(2, name='VAR_POSITIONAL')
_KEYWORD_ONLY = _ParameterKind(3, name='KEYWORD_ONLY')
_VAR_KEYWORD = _ParameterKind(4, name='VAR_KEYWORD')
class Parameter(object):
'''Represents a parameter in a function signature.
Has the following public attributes:
* name : str
The name of the parameter as a string.
* default : object
The default value for the parameter if specified. If the
parameter has no default value, this attribute is not set.
* annotation
The annotation for the parameter if specified. If the
parameter has no annotation, this attribute is not set.
* kind : str
Describes how argument values are bound to the parameter.
Possible values: `Parameter.POSITIONAL_ONLY`,
`Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`,
`Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
'''
__slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg')
POSITIONAL_ONLY = _POSITIONAL_ONLY
POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD
VAR_POSITIONAL = _VAR_POSITIONAL
KEYWORD_ONLY = _KEYWORD_ONLY
VAR_KEYWORD = _VAR_KEYWORD
empty = _empty
def __init__(self, name, kind, default=_empty, annotation=_empty,
_partial_kwarg=False):
if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD,
_VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD):
raise ValueError("invalid value for 'Parameter.kind' attribute")
self._kind = kind
if default is not _empty:
if kind in (_VAR_POSITIONAL, _VAR_KEYWORD):
msg = '{} parameters cannot have default values'.format(kind)
raise ValueError(msg)
self._default = default
self._annotation = annotation
if name is None:
if kind != _POSITIONAL_ONLY:
raise ValueError("None is not a valid name for a "
"non-positional-only parameter")
self._name = name
else:
name = str(name)
if kind != _POSITIONAL_ONLY and not re.match(r'[a-z_]\w*$', name, re.I):
msg = '{!r} is not a valid parameter name'.format(name)
raise ValueError(msg)
self._name = name
self._partial_kwarg = _partial_kwarg
@property
def name(self):
return self._name
@property
def default(self):
return self._default
@property
def annotation(self):
return self._annotation
@property
def kind(self):
return self._kind
def replace(self, name=_void, kind=_void, annotation=_void,
default=_void, _partial_kwarg=_void):
'''Creates a customized copy of the Parameter.'''
if name is _void:
name = self._name
if kind is _void:
kind = self._kind
if annotation is _void:
annotation = self._annotation
if default is _void:
default = self._default
if _partial_kwarg is _void:
_partial_kwarg = self._partial_kwarg
return type(self)(name, kind, default=default, annotation=annotation,
_partial_kwarg=_partial_kwarg)
def __str__(self):
kind = self.kind
formatted = self._name
if kind == _POSITIONAL_ONLY:
if formatted is None:
formatted = ''
formatted = '<{}>'.format(formatted)
# Add annotation and default value
if self._annotation is not _empty:
formatted = '{}:{}'.format(formatted,
formatannotation(self._annotation))
if self._default is not _empty:
formatted = '{}={}'.format(formatted, repr(self._default))
if kind == _VAR_POSITIONAL:
formatted = '*' + formatted
elif kind == _VAR_KEYWORD:
formatted = '**' + formatted
return formatted
def __repr__(self):
return '<{} at {:#x} {!r}>'.format(self.__class__.__name__,
id(self), self.name)
def __hash__(self):
msg = "unhashable type: '{}'".format(self.__class__.__name__)
raise TypeError(msg)
def __eq__(self, other):
return (issubclass(other.__class__, Parameter) and
self._name == other._name and
self._kind == other._kind and
self._default == other._default and
self._annotation == other._annotation)
def __ne__(self, other):
return not self.__eq__(other)
class BoundArguments(object):
'''Result of `Signature.bind` call. Holds the mapping of arguments
to the function's parameters.
Has the following public attributes:
* arguments : OrderedDict
An ordered mutable mapping of parameters' names to arguments' values.
Does not contain arguments' default values.
* signature : Signature
The Signature object that created this instance.
* args : tuple
Tuple of positional arguments values.
* kwargs : dict
Dict of keyword arguments values.
'''
def __init__(self, signature, arguments):
self.arguments = arguments
self._signature = signature
@property
def signature(self):
return self._signature
@property
def args(self):
args = []
for param_name, param in self._signature.parameters.items():
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
param._partial_kwarg):
# Keyword arguments mapped by 'functools.partial'
# (Parameter._partial_kwarg is True) are mapped
# in 'BoundArguments.kwargs', along with VAR_KEYWORD &
# KEYWORD_ONLY
break
try:
arg = self.arguments[param_name]
except KeyError:
# We're done here. Other arguments
# will be mapped in 'BoundArguments.kwargs'
break
else:
if param.kind == _VAR_POSITIONAL:
# *args
args.extend(arg)
else:
# plain argument
args.append(arg)
return tuple(args)
@property
def kwargs(self):
kwargs = {}
kwargs_started = False
for param_name, param in self._signature.parameters.items():
if not kwargs_started:
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
param._partial_kwarg):
kwargs_started = True
else:
if param_name not in self.arguments:
kwargs_started = True
continue
if not kwargs_started:
continue
try:
arg = self.arguments[param_name]
except KeyError:
pass
else:
if param.kind == _VAR_KEYWORD:
# **kwargs
kwargs.update(arg)
else:
# plain keyword argument
kwargs[param_name] = arg
return kwargs
def __hash__(self):
msg = "unhashable type: '{}'".format(self.__class__.__name__)
raise TypeError(msg)
def __eq__(self, other):
return (issubclass(other.__class__, BoundArguments) and
self.signature == other.signature and
self.arguments == other.arguments)
def __ne__(self, other):
return not self.__eq__(other)
class Signature(object):
'''A Signature object represents the overall signature of a function.
It stores a Parameter object for each parameter accepted by the
function, as well as information specific to the function itself.
A Signature object has the following public attributes and methods:
* parameters : OrderedDict
An ordered mapping of parameters' names to the corresponding
Parameter objects (keyword-only arguments are in the same order
as listed in `code.co_varnames`).
* return_annotation : object
The annotation for the return type of the function if specified.
If the function has no annotation for its return type, this
attribute is not set.
* bind(*args, **kwargs) -> BoundArguments
Creates a mapping from positional and keyword arguments to
parameters.
* bind_partial(*args, **kwargs) -> BoundArguments
Creates a partial mapping from positional and keyword arguments
to parameters (simulating 'functools.partial' behavior.)
'''
__slots__ = ('_return_annotation', '_parameters')
_parameter_cls = Parameter
_bound_arguments_cls = BoundArguments
empty = _empty
def __init__(self, parameters=None, return_annotation=_empty,
__validate_parameters__=True):
'''Constructs Signature from the given list of Parameter
objects and 'return_annotation'. All arguments are optional.
'''
if parameters is None:
params = OrderedDict()
else:
if __validate_parameters__:
params = OrderedDict()
top_kind = _POSITIONAL_ONLY
for idx, param in enumerate(parameters):
kind = param.kind
if kind < top_kind:
msg = 'wrong parameter order: {0} before {1}'
msg = msg.format(top_kind, param.kind)
raise ValueError(msg)
else:
top_kind = kind
name = param.name
if name is None:
name = str(idx)
param = param.replace(name=name)
if name in params:
msg = 'duplicate parameter name: {!r}'.format(name)
raise ValueError(msg)
params[name] = param
else:
params = OrderedDict(((param.name, param)
for param in parameters))
self._parameters = params
self._return_annotation = return_annotation
@classmethod
def from_function(cls, func):
'''Constructs Signature for the given python function'''
if not isinstance(func, types.FunctionType):
raise TypeError('{!r} is not a Python function'.format(func))
Parameter = cls._parameter_cls
# Parameter information.
func_code = func.__code__
pos_count = func_code.co_argcount
arg_names = func_code.co_varnames
positional = tuple(arg_names[:pos_count])
keyword_only_count = getattr(func_code, 'co_kwonlyargcount', 0)
keyword_only = arg_names[pos_count:(pos_count + keyword_only_count)]
annotations = getattr(func, '__annotations__', {})
defaults = func.__defaults__
kwdefaults = getattr(func, '__kwdefaults__', None)
if defaults:
pos_default_count = len(defaults)
else:
pos_default_count = 0
parameters = []
# Non-keyword-only parameters w/o defaults.
non_default_count = pos_count - pos_default_count
for name in positional[:non_default_count]:
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_POSITIONAL_OR_KEYWORD))
# ... w/ defaults.
for offset, name in enumerate(positional[non_default_count:]):
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_POSITIONAL_OR_KEYWORD,
default=defaults[offset]))
# *args
if func_code.co_flags & 0x04:
name = arg_names[pos_count + keyword_only_count]
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_VAR_POSITIONAL))
# Keyword-only parameters.
for name in keyword_only:
default = _empty
if kwdefaults is not None:
default = kwdefaults.get(name, _empty)
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_KEYWORD_ONLY,
default=default))
# **kwargs
if func_code.co_flags & 0x08:
index = pos_count + keyword_only_count
if func_code.co_flags & 0x04:
index += 1
name = arg_names[index]
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_VAR_KEYWORD))
return cls(parameters,
return_annotation=annotations.get('return', _empty),
__validate_parameters__=False)
@property
def parameters(self):
try:
return types.MappingProxyType(self._parameters)
except AttributeError:
return OrderedDict(self._parameters.items())
@property
def return_annotation(self):
return self._return_annotation
def replace(self, parameters=_void, return_annotation=_void):
'''Creates a customized copy of the Signature.
Pass 'parameters' and/or 'return_annotation' arguments
to override them in the new copy.
'''
if parameters is _void:
parameters = self.parameters.values()
if return_annotation is _void:
return_annotation = self._return_annotation
return type(self)(parameters,
return_annotation=return_annotation)
def __hash__(self):
msg = "unhashable type: '{}'".format(self.__class__.__name__)
raise TypeError(msg)
def __eq__(self, other):
if (not issubclass(type(other), Signature) or
self.return_annotation != other.return_annotation or
len(self.parameters) != len(other.parameters)):
return False
other_positions = {param: idx
for idx, param in enumerate(other.parameters.keys())}
for idx, (param_name, param) in enumerate(self.parameters.items()):
if param.kind == _KEYWORD_ONLY:
try:
other_param = other.parameters[param_name]
except KeyError:
return False
else:
if param != other_param:
return False
else:
try:
other_idx = other_positions[param_name]
except KeyError:
return False
else:
if (idx != other_idx or
param != other.parameters[param_name]):
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def _bind(self, args, kwargs, partial=False):
'''Private method. Don't use directly.'''
arguments = OrderedDict()
parameters = iter(self.parameters.values())
parameters_ex = ()
arg_vals = iter(args)
if partial:
# Support for binding arguments to 'functools.partial' objects.
# See 'functools.partial' case in 'signature()' implementation
# for details.
for param_name, param in self.parameters.items():
if (param._partial_kwarg and param_name not in kwargs):
# Simulating 'functools.partial' behavior
kwargs[param_name] = param.default
while True:
# Let's iterate through the positional arguments and corresponding
# parameters
try:
arg_val = next(arg_vals)
except StopIteration:
# No more positional arguments
try:
param = next(parameters)
except StopIteration:
# No more parameters. That's it. Just need to check that
# we have no `kwargs` after this while loop
break
else:
if param.kind == _VAR_POSITIONAL:
# That's OK, just empty *args. Let's start parsing
# kwargs
break
elif param.name in kwargs:
if param.kind == _POSITIONAL_ONLY:
msg = '{arg!r} parameter is positional only, ' \
'but was passed as a keyword'
msg = msg.format(arg=param.name)
raise TypeError(msg)
parameters_ex = (param,)
break
elif (param.kind == _VAR_KEYWORD or
param.default is not _empty):
# That's fine too - we have a default value for this
# parameter. So, lets start parsing `kwargs`, starting
# with the current parameter
parameters_ex = (param,)
break
else:
if partial:
parameters_ex = (param,)
break
else:
msg = '{arg!r} parameter lacking default value'
msg = msg.format(arg=param.name)
raise TypeError(msg)
else:
# We have a positional argument to process
try:
param = next(parameters)
except StopIteration:
raise TypeError('too many positional arguments')
else:
if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
# Looks like we have no parameter for this positional
# argument
raise TypeError('too many positional arguments')
if param.kind == _VAR_POSITIONAL:
# We have an '*args'-like argument, let's fill it with
# all positional arguments we have left and move on to
# the next phase
values = [arg_val]
values.extend(arg_vals)
arguments[param.name] = tuple(values)
break
if param.name in kwargs:
raise TypeError('multiple values for argument '
'{arg!r}'.format(arg=param.name))
arguments[param.name] = arg_val
# Now, we iterate through the remaining parameters to process
# keyword arguments
kwargs_param = None
for param in itertools.chain(parameters_ex, parameters):
if param.kind == _POSITIONAL_ONLY:
# This should never happen in case of a properly built
# Signature object (but let's have this check here
# to ensure correct behaviour just in case)
raise TypeError('{arg!r} parameter is positional only, '
'but was passed as a keyword'.
format(arg=param.name))
if param.kind == _VAR_KEYWORD:
# Memorize that we have a '**kwargs'-like parameter
kwargs_param = param
continue
param_name = param.name
try:
arg_val = kwargs.pop(param_name)
except KeyError:
# We have no value for this parameter. It's fine though,
# if it has a default value, or it is an '*args'-like
# parameter, left alone by the processing of positional
# arguments.
if (not partial and param.kind != _VAR_POSITIONAL and
param.default is _empty):
raise TypeError('{arg!r} parameter lacking default value'.
format(arg=param_name))
else:
arguments[param_name] = arg_val
if kwargs:
if kwargs_param is not None:
# Process our '**kwargs'-like parameter
arguments[kwargs_param.name] = kwargs
else:
raise TypeError('too many keyword arguments')
return self._bound_arguments_cls(self, arguments)
def bind(self, *args, **kwargs):
'''Get a BoundArguments object, that maps the passed `args`
and `kwargs` to the function's signature. Raises `TypeError`
if the passed arguments can not be bound.
'''
return self._bind(args, kwargs)
def bind_partial(self, *args, **kwargs):
'''Get a BoundArguments object, that partially maps the
passed `args` and `kwargs` to the function's signature.
Raises `TypeError` if the passed arguments can not be bound.
'''
return self._bind(args, kwargs, partial=True)
def __str__(self):
result = []
render_kw_only_separator = True
for idx, param in enumerate(self.parameters.values()):
formatted = str(param)
kind = param.kind
if kind == _VAR_POSITIONAL:
# OK, we have an '*args'-like parameter, so we won't need
# a '*' to separate keyword-only arguments
render_kw_only_separator = False
elif kind == _KEYWORD_ONLY and render_kw_only_separator:
# We have a keyword-only parameter to render and we haven't
# rendered an '*args'-like parameter before, so add a '*'
# separator to the parameters list ("foo(arg1, *, arg2)" case)
result.append('*')
# This condition should be only triggered once, so
# reset the flag
render_kw_only_separator = False
result.append(formatted)
rendered = '({})'.format(', '.join(result))
if self.return_annotation is not _empty:
anno = formatannotation(self.return_annotation)
rendered += ' -> {}'.format(anno)
return rendered

View File

View File

@ -0,0 +1,42 @@
from ..enum import _is_dunder, _is_sunder
def test__is_dunder():
dunder_names = [
'__i__',
'__test__',
]
non_dunder_names = [
'test',
'__test',
'_test',
'_test_',
'test__',
'',
]
for name in dunder_names:
assert _is_dunder(name) is True
for name in non_dunder_names:
assert _is_dunder(name) is False
def test__is_sunder():
sunder_names = [
'_i_',
'_test_',
]
non_sunder_names = [
'__i__',
'_i__',
'__i_',
'',
]
for name in sunder_names:
assert _is_sunder(name) is True
for name in non_sunder_names:
assert _is_sunder(name) is False

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import datetime
import os
import subprocess
@ -14,12 +16,15 @@ def get_version(version=None):
main = get_main_version(version)
sub = ""
if version[3] == "alpha" and version[4] == 0:
sub = ''
if version[3] == 'alpha' and version[4] == 0:
git_changeset = get_git_changeset()
sub = ".dev%s" % git_changeset if git_changeset else ".dev"
elif version[3] != "final":
mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
if git_changeset:
sub = '.dev%s' % git_changeset
else:
sub = '.dev'
elif version[3] != 'final':
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
sub = mapping[version[3]] + str(version[4])
return str(main + sub)
@ -29,7 +34,7 @@ def get_main_version(version=None):
"Returns main version (X.Y[.Z]) from VERSION."
version = get_complete_version(version)
parts = 2 if version[2] == 0 else 3
return ".".join(str(x) for x in version[:parts])
return '.'.join(str(x) for x in version[:parts])
def get_complete_version(version=None):
@ -40,17 +45,17 @@ def get_complete_version(version=None):
from graphene import VERSION as version
else:
assert len(version) == 5
assert version[3] in ("alpha", "beta", "rc", "final")
assert version[3] in ('alpha', 'beta', 'rc', 'final')
return version
def get_docs_version(version=None):
version = get_complete_version(version)
if version[3] != "final":
return "dev"
if version[3] != 'final':
return 'dev'
else:
return "%d.%d" % version[:2]
return '%d.%d' % version[:2]
def get_git_changeset():
@ -62,15 +67,12 @@ def get_git_changeset():
repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
try:
git_log = subprocess.Popen(
"git log --pretty=format:%ct --quiet -1 HEAD",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
cwd=repo_dir,
universal_newlines=True,
'git log --pretty=format:%ct --quiet -1 HEAD',
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, cwd=repo_dir, universal_newlines=True,
)
timestamp = git_log.communicate()[0]
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
except Exception:
except:
return None
return timestamp.strftime("%Y%m%d%H%M%S")
return timestamp.strftime('%Y%m%d%H%M%S')

View File

@ -1,23 +1,13 @@
from .node import Node, is_node, GlobalID
from .mutation import ClientIDMutation
from .connection import Connection, ConnectionField, PageInfo
from .id_type import (
BaseGlobalIDType,
DefaultGlobalIDType,
SimpleGlobalIDType,
UUIDGlobalIDType,
)
__all__ = [
"BaseGlobalIDType",
"ClientIDMutation",
"Connection",
"ConnectionField",
"DefaultGlobalIDType",
"GlobalID",
"Node",
"PageInfo",
"SimpleGlobalIDType",
"UUIDGlobalIDType",
"is_node",
'Node',
'is_node',
'GlobalID',
'ClientIDMutation',
'Connection',
'ConnectionField',
'PageInfo',
]

View File

@ -1,82 +1,38 @@
import re
from collections.abc import Iterable
from collections import Iterable, OrderedDict
from functools import partial
from typing import Type
from graphql_relay import connection_from_array
from graphql_relay import connection_from_list
from promise import Promise, is_thenable
from ..types import Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union
from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar,
String, Union)
from ..types.field import Field
from ..types.objecttype import ObjectType, ObjectTypeOptions
from ..utils.thenables import maybe_thenable
from .node import is_node, AbstractNode
def get_edge_class(
connection_class: Type["Connection"],
_node: Type[AbstractNode],
base_name: str,
strict_types: bool = False,
):
edge_class = getattr(connection_class, "Edge", None)
class EdgeBase:
node = Field(
NonNull(_node) if strict_types else _node,
description="The item at the end of the edge",
)
cursor = String(required=True, description="A cursor for use in pagination")
class EdgeMeta:
description = f"A Relay edge containing a `{base_name}` and its cursor."
edge_name = f"{base_name}Edge"
edge_bases = [edge_class, EdgeBase] if edge_class else [EdgeBase]
if not isinstance(edge_class, ObjectType):
edge_bases = [*edge_bases, ObjectType]
return type(edge_name, tuple(edge_bases), {"Meta": EdgeMeta})
from .node import is_node
class PageInfo(ObjectType):
class Meta:
description = (
"The Relay compliant `PageInfo` type, containing data necessary to"
" paginate this connection."
)
has_next_page = Boolean(
required=True,
name="hasNextPage",
description="When paginating forwards, are there more items?",
name='hasNextPage',
description='When paginating forwards, are there more items?',
)
has_previous_page = Boolean(
required=True,
name="hasPreviousPage",
description="When paginating backwards, are there more items?",
name='hasPreviousPage',
description='When paginating backwards, are there more items?',
)
start_cursor = String(
name="startCursor",
description="When paginating backwards, the cursor to continue.",
name='startCursor',
description='When paginating backwards, the cursor to continue.',
)
end_cursor = String(
name="endCursor",
description="When paginating forwards, the cursor to continue.",
)
# noinspection PyPep8Naming
def page_info_adapter(startCursor, endCursor, hasPreviousPage, hasNextPage):
"""Adapter for creating PageInfo instances"""
return PageInfo(
start_cursor=startCursor,
end_cursor=endCursor,
has_previous_page=hasPreviousPage,
has_next_page=hasNextPage,
name='endCursor',
description='When paginating forwards, the cursor to continue.',
)
@ -85,83 +41,77 @@ class ConnectionOptions(ObjectTypeOptions):
class Connection(ObjectType):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(
cls, node=None, name=None, strict_types=False, _meta=None, **options
):
if not _meta:
_meta = ConnectionOptions(cls)
assert node, f"You have to provide a node in {cls.__name__}.Meta"
assert isinstance(node, NonNull) or issubclass(
node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)
), f'Received incompatible node "{node}" for Connection {cls.__name__}.'
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
assert node, 'You have to provide a node in {}.Meta'.format(cls.__name__)
assert issubclass(node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), (
'Received incompatible node "{}" for Connection {}.'
).format(node, cls.__name__)
base_name = re.sub("Connection$", "", name or cls.__name__) or node._meta.name
base_name = re.sub('Connection$', '', name or cls.__name__) or node._meta.name
if not name:
name = f"{base_name}Connection"
name = '{}Connection'.format(base_name)
options["name"] = name
edge_class = getattr(cls, 'Edge', None)
_node = node
class EdgeBase(object):
node = Field(_node, description='The item at the end of the edge')
cursor = String(required=True, description='A cursor for use in pagination')
edge_name = '{}Edge'.format(base_name)
if edge_class:
edge_bases = (edge_class, EdgeBase, ObjectType,)
else:
edge_bases = (EdgeBase, ObjectType,)
edge = type(edge_name, edge_bases, {})
cls.Edge = edge
options['name'] = name
_meta.node = node
if not _meta.fields:
_meta.fields = {}
if "page_info" not in _meta.fields:
_meta.fields["page_info"] = Field(
PageInfo,
name="pageInfo",
required=True,
description="Pagination data for this connection.",
)
if "edges" not in _meta.fields:
edge_class = get_edge_class(cls, node, base_name, strict_types) # type: ignore
cls.Edge = edge_class
_meta.fields["edges"] = Field(
NonNull(List(NonNull(edge_class) if strict_types else edge_class)),
description="Contains the nodes in this connection.",
)
return super(Connection, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
# noinspection PyPep8Naming
def connection_adapter(cls, edges, pageInfo):
"""Adapter for creating Connection instances"""
return cls(edges=edges, page_info=pageInfo)
_meta.fields = OrderedDict([
('page_info', Field(PageInfo, name='pageInfo', required=True)),
('edges', Field(NonNull(List(edge)))),
])
return super(Connection, cls).__init_subclass_with_meta__(_meta=_meta, **options)
class IterableConnectionField(Field):
def __init__(self, type_, *args, **kwargs):
kwargs.setdefault("before", String())
kwargs.setdefault("after", String())
kwargs.setdefault("first", Int())
kwargs.setdefault("last", Int())
super(IterableConnectionField, self).__init__(type_, *args, **kwargs)
def __init__(self, type, *args, **kwargs):
kwargs.setdefault('before', String())
kwargs.setdefault('after', String())
kwargs.setdefault('first', Int())
kwargs.setdefault('last', Int())
super(IterableConnectionField, self).__init__(
type,
*args,
**kwargs
)
@property
def type(self):
type_ = super(IterableConnectionField, self).type
connection_type = type_
if isinstance(type_, NonNull):
connection_type = type_.of_type
type = super(IterableConnectionField, self).type
connection_type = type
if isinstance(type, NonNull):
connection_type = type.of_type
if is_node(connection_type):
raise Exception(
"ConnectionFields now need a explicit ConnectionType for Nodes.\n"
"ConnectionField's now need a explicit ConnectionType for Nodes.\n"
"Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#node-connections"
)
assert issubclass(
connection_type, Connection
), f'{self.__class__.__name__} type has to be a subclass of Connection. Received "{connection_type}".'
return type_
assert issubclass(connection_type, Connection), (
'{} type have to be a subclass of Connection. Received "{}".'
).format(self.__class__.__name__, connection_type)
return type
@classmethod
def resolve_connection(cls, connection_type, args, resolved):
@ -169,15 +119,15 @@ class IterableConnectionField(Field):
return resolved
assert isinstance(resolved, Iterable), (
f"Resolved value from the connection field has to be an iterable or instance of {connection_type}. "
f'Received "{resolved}"'
)
connection = connection_from_array(
'Resolved value from the connection field have to be iterable or instance of {}. '
'Received "{}"'
).format(connection_type, resolved)
connection = connection_from_list(
resolved,
args,
connection_type=partial(connection_adapter, connection_type),
connection_type=connection_type,
edge_type=connection_type.Edge,
page_info_type=page_info_adapter,
pageinfo_type=PageInfo
)
connection.iterable = resolved
return connection
@ -190,10 +140,13 @@ class IterableConnectionField(Field):
connection_type = connection_type.of_type
on_resolve = partial(cls.resolve_connection, connection_type, args)
return maybe_thenable(resolved, on_resolve)
if is_thenable(resolved):
return Promise.resolve(resolved).then(on_resolve)
def wrap_resolve(self, parent_resolver):
resolver = super(IterableConnectionField, self).wrap_resolve(parent_resolver)
return on_resolve(resolved)
def get_resolver(self, parent_resolver):
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)
return partial(self.connection_resolver, resolver, self.type)

View File

@ -1,87 +0,0 @@
from graphql_relay import from_global_id, to_global_id
from ..types import ID, UUID
from ..types.base import BaseType
from typing import Type
class BaseGlobalIDType:
"""
Base class that define the required attributes/method for a type.
"""
graphene_type: Type[BaseType] = ID
@classmethod
def resolve_global_id(cls, info, global_id):
# return _type, _id
raise NotImplementedError
@classmethod
def to_global_id(cls, _type, _id):
# return _id
raise NotImplementedError
class DefaultGlobalIDType(BaseGlobalIDType):
"""
Default global ID type: base64 encoded version of "<node type name>: <node id>".
"""
graphene_type = ID
@classmethod
def resolve_global_id(cls, info, global_id):
try:
_type, _id = from_global_id(global_id)
if not _type:
raise ValueError("Invalid Global ID")
return _type, _id
except Exception as e:
raise Exception(
f'Unable to parse global ID "{global_id}". '
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
f"Exception message: {e}"
)
@classmethod
def to_global_id(cls, _type, _id):
return to_global_id(_type, _id)
class SimpleGlobalIDType(BaseGlobalIDType):
"""
Simple global ID type: simply the id of the object.
To be used carefully as the user is responsible for ensuring that the IDs are indeed global
(otherwise it could cause request caching issues).
"""
graphene_type = ID
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id
class UUIDGlobalIDType(BaseGlobalIDType):
"""
UUID global ID type.
By definition UUID are global so they are used as they are.
"""
graphene_type = UUID
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id

View File

@ -1,66 +1,72 @@
import re
from collections import OrderedDict
from promise import Promise, is_thenable
from ..types import Field, InputObjectType, String
from ..types.mutation import Mutation
from ..utils.thenables import maybe_thenable
class ClientIDMutation(Mutation):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(
cls, output=None, input_fields=None, arguments=None, name=None, **options
):
input_class = getattr(cls, "Input", None)
base_name = re.sub("Payload$", "", name or cls.__name__)
def __init_subclass_with_meta__(cls, output=None, input_fields=None,
arguments=None, name=None, **options):
input_class = getattr(cls, 'Input', None)
base_name = re.sub('Payload$', '', name or cls.__name__)
assert not output, "Can't specify any output"
assert not arguments, "Can't specify any arguments"
bases = (InputObjectType,)
bases = (InputObjectType, )
if input_class:
bases += (input_class,)
bases += (input_class, )
if not input_fields:
input_fields = {}
cls.Input = type(
f"{base_name}Input",
'{}Input'.format(base_name),
bases,
dict(input_fields, client_mutation_id=String(name="clientMutationId")),
OrderedDict(input_fields, client_mutation_id=String(
name='clientMutationId'))
)
arguments = dict(
arguments = OrderedDict(
input=cls.Input(required=True)
# 'client_mutation_id': String(name='clientMutationId')
)
mutate_and_get_payload = getattr(cls, "mutate_and_get_payload", None)
mutate_and_get_payload = getattr(cls, 'mutate_and_get_payload', None)
if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__:
assert mutate_and_get_payload, (
f"{name or cls.__name__}.mutate_and_get_payload method is required"
" in a ClientIDMutation."
)
"{name}.mutate_and_get_payload method is required"
" in a ClientIDMutation.").format(name=name or cls.__name__)
if not name:
name = f"{base_name}Payload"
name = '{}Payload'.format(base_name)
super(ClientIDMutation, cls).__init_subclass_with_meta__(
output=None, arguments=arguments, name=name, **options
output=None, arguments=arguments, name=name, **options)
cls._meta.fields['client_mutation_id'] = (
Field(String, name='clientMutationId')
)
cls._meta.fields["client_mutation_id"] = Field(String, name="clientMutationId")
@classmethod
def mutate(cls, root, info, input):
def on_resolve(payload):
try:
payload.client_mutation_id = input.get("client_mutation_id")
payload.client_mutation_id = input.get('client_mutation_id')
except Exception:
raise Exception(
f"Cannot set client_mutation_id in the payload object {repr(payload)}"
)
('Cannot set client_mutation_id in the payload object {}'
).format(repr(payload)))
return payload
result = cls.mutate_and_get_payload(root, info, **input)
return maybe_thenable(result, on_resolve)
if is_thenable(result):
return Promise.resolve(result).then(on_resolve)
return on_resolve(result)

View File

@ -1,38 +1,35 @@
from collections import OrderedDict
from functools import partial
from inspect import isclass
from ..types import Field, Interface, ObjectType
from graphql_relay import from_global_id, to_global_id
from ..types import ID, Field, Interface, ObjectType
from ..types.interface import InterfaceOptions
from ..types.utils import get_type
from .id_type import BaseGlobalIDType, DefaultGlobalIDType
def is_node(objecttype):
"""
'''
Check if the given objecttype has Node as an interface
"""
'''
if not isclass(objecttype):
return False
if not issubclass(objecttype, ObjectType):
return False
return any(issubclass(i, Node) for i in objecttype._meta.interfaces)
for i in objecttype._meta.interfaces:
if issubclass(i, Node):
return True
return False
class GlobalID(Field):
def __init__(
self,
node=None,
parent_type=None,
required=True,
global_id_type=DefaultGlobalIDType,
*args,
**kwargs,
):
super(GlobalID, self).__init__(
global_id_type.graphene_type, required=required, *args, **kwargs
)
def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs):
super(GlobalID, self).__init__(ID, required=required, *args, **kwargs)
self.node = node or Node
self.parent_type_name = parent_type._meta.name if parent_type else None
@ -42,60 +39,49 @@ class GlobalID(Field):
parent_type_name = parent_type_name or info.parent_type.name
return node.to_global_id(parent_type_name, type_id) # root._meta.name
def wrap_resolve(self, parent_resolver):
def get_resolver(self, parent_resolver):
return partial(
self.id_resolver,
parent_resolver,
self.node,
parent_type_name=self.parent_type_name,
self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name
)
class NodeField(Field):
def __init__(self, node, type_=False, **kwargs):
assert issubclass(node, Node), "NodeField can only operate in Nodes"
def __init__(self, node, type=False, deprecation_reason=None,
name=None, **kwargs):
assert issubclass(node, Node), 'NodeField can only operate in Nodes'
self.node_type = node
self.field_type = type_
global_id_type = node._meta.global_id_type
self.field_type = type
super(NodeField, self).__init__(
# If we don't specify a type, the field type will be the node interface
type_ or node,
id=global_id_type.graphene_type(
required=True, description="The ID of the object"
),
**kwargs,
# If we don's specify a type, the field type will be the node
# interface
type or node,
description='The ID of the object',
id=ID(required=True)
)
def wrap_resolve(self, parent_resolver):
def get_resolver(self, parent_resolver):
return partial(self.node_type.node_resolver, get_type(self.field_type))
class AbstractNode(Interface):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, global_id_type=DefaultGlobalIDType, **options):
assert issubclass(
global_id_type, BaseGlobalIDType
), "Custom ID type need to be implemented as a subclass of BaseGlobalIDType."
def __init_subclass_with_meta__(cls, **options):
_meta = InterfaceOptions(cls)
_meta.global_id_type = global_id_type
_meta.fields = {
"id": GlobalID(
cls, global_id_type=global_id_type, description="The ID of the object"
)
}
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod
def resolve_global_id(cls, info, global_id):
return cls._meta.global_id_type.resolve_global_id(info, global_id)
_meta.fields = OrderedDict(
id=GlobalID(cls, description='The ID of the object.')
)
super(AbstractNode, cls).__init_subclass_with_meta__(
_meta=_meta, **options)
class Node(AbstractNode):
"""An object with an ID"""
'''An object with an ID'''
@classmethod
def Field(cls, *args, **kwargs): # noqa: N802
@ -107,29 +93,29 @@ class Node(AbstractNode):
@classmethod
def get_node_from_global_id(cls, info, global_id, only_type=None):
_type, _id = cls.resolve_global_id(info, global_id)
graphene_type = info.schema.get_type(_type)
if graphene_type is None:
raise Exception(f'Relay Node "{_type}" not found in schema')
graphene_type = graphene_type.graphene_type
try:
_type, _id = cls.from_global_id(global_id)
graphene_type = info.schema.get_type(_type).graphene_type
except Exception:
return None
if only_type:
assert (
graphene_type == only_type
), f"Must receive a {only_type._meta.name} id."
assert graphene_type == only_type, (
'Must receive a {} id.'
).format(only_type._meta.name)
# We make sure the ObjectType implements the "Node" interface
if cls not in graphene_type._meta.interfaces:
raise Exception(
f'ObjectType "{_type}" does not implement the "{cls}" interface.'
)
return None
get_node = getattr(graphene_type, "get_node", None)
get_node = getattr(graphene_type, 'get_node', None)
if get_node:
return get_node(info, _id)
@classmethod
def to_global_id(cls, type_, id):
return cls._meta.global_id_type.to_global_id(type_, id)
def from_global_id(cls, global_id):
return from_global_id(global_id)
@classmethod
def to_global_id(cls, type, id):
return to_global_id(type, id)

View File

@ -1,22 +1,15 @@
import re
import pytest
from pytest import raises
from ...types import Argument, Field, Int, List, NonNull, ObjectType, Schema, String
from ..connection import (
Connection,
ConnectionField,
PageInfo,
ConnectionOptions,
get_edge_class,
)
from ...types import (Argument, Field, Int, List, NonNull, ObjectType, Schema,
String)
from ..connection import Connection, ConnectionField, PageInfo
from ..node import Node
class MyObject(ObjectType):
class Meta:
interfaces = [Node]
field = String()
@ -30,11 +23,11 @@ def test_connection():
class Edge:
other = String()
assert MyObjectConnection._meta.name == "MyObjectConnection"
assert MyObjectConnection._meta.name == 'MyObjectConnection'
fields = MyObjectConnection._meta.fields
assert list(fields) == ["page_info", "edges", "extra"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
assert list(fields.keys()) == ['page_info', 'edges', 'extra']
edge_field = fields['edges']
pageinfo_field = fields['page_info']
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
@ -47,130 +40,27 @@ def test_connection():
def test_connection_inherit_abstracttype():
class BaseConnection:
class BaseConnection(object):
extra = String()
class MyObjectConnection(BaseConnection, Connection):
class Meta:
node = MyObject
assert MyObjectConnection._meta.name == "MyObjectConnection"
assert MyObjectConnection._meta.name == 'MyObjectConnection'
fields = MyObjectConnection._meta.fields
assert list(fields) == ["page_info", "edges", "extra"]
def test_connection_extra_abstract_fields():
class ConnectionWithNodes(Connection):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
_meta.fields = {
"nodes": Field(
NonNull(List(node)),
description="Contains all the nodes in this connection.",
),
}
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
node=node, name=name, _meta=_meta, **options
)
class MyObjectConnection(ConnectionWithNodes):
class Meta:
node = MyObject
class Edge:
other = String()
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["nodes", "page_info", "edges"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
nodes_field = fields["nodes"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
assert isinstance(edge_field.type.of_type, List)
assert edge_field.type.of_type.of_type == MyObjectConnection.Edge
assert isinstance(pageinfo_field, Field)
assert isinstance(pageinfo_field.type, NonNull)
assert pageinfo_field.type.of_type == PageInfo
assert isinstance(nodes_field, Field)
assert isinstance(nodes_field.type, NonNull)
assert isinstance(nodes_field.type.of_type, List)
assert nodes_field.type.of_type.of_type == MyObject
def test_connection_override_fields():
class ConnectionWithNodes(Connection):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
base_name = (
re.sub("Connection$", "", name or cls.__name__) or node._meta.name
)
edge_class = get_edge_class(cls, node, base_name)
_meta.fields = {
"page_info": Field(
NonNull(
PageInfo,
name="pageInfo",
required=True,
description="Pagination data for this connection.",
)
),
"edges": Field(
NonNull(List(NonNull(edge_class))),
description="Contains the nodes in this connection.",
),
}
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
node=node, name=name, _meta=_meta, **options
)
class MyObjectConnection(ConnectionWithNodes):
class Meta:
node = MyObject
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["page_info", "edges"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
assert isinstance(edge_field.type.of_type, List)
assert isinstance(edge_field.type.of_type.of_type, NonNull)
assert edge_field.type.of_type.of_type.of_type.__name__ == "MyObjectEdge"
# This page info is NonNull
assert isinstance(pageinfo_field, Field)
assert isinstance(edge_field.type, NonNull)
assert pageinfo_field.type.of_type == PageInfo
assert list(fields.keys()) == ['page_info', 'edges', 'extra']
def test_connection_name():
custom_name = "MyObjectCustomNameConnection"
class BaseConnection:
class BaseConnection(object):
extra = String()
class MyObjectConnection(BaseConnection, Connection):
class Meta:
node = MyObject
name = custom_name
@ -180,6 +70,7 @@ def test_connection_name():
def test_edge():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
@ -187,22 +78,23 @@ def test_edge():
other = String()
Edge = MyObjectConnection.Edge
assert Edge._meta.name == "MyObjectEdge"
assert Edge._meta.name == 'MyObjectEdge'
edge_fields = Edge._meta.fields
assert list(edge_fields) == ["node", "cursor", "other"]
assert list(edge_fields.keys()) == ['node', 'cursor', 'other']
assert isinstance(edge_fields["node"], Field)
assert edge_fields["node"].type == MyObject
assert isinstance(edge_fields['node'], Field)
assert edge_fields['node'].type == MyObject
assert isinstance(edge_fields["other"], Field)
assert edge_fields["other"].type == String
assert isinstance(edge_fields['other'], Field)
assert edge_fields['other'].type == String
def test_edge_with_bases():
class BaseEdge:
class BaseEdge(object):
extra = String()
class MyObjectConnection(Connection):
class Meta:
node = MyObject
@ -210,82 +102,65 @@ def test_edge_with_bases():
other = String()
Edge = MyObjectConnection.Edge
assert Edge._meta.name == "MyObjectEdge"
assert Edge._meta.name == 'MyObjectEdge'
edge_fields = Edge._meta.fields
assert list(edge_fields) == ["node", "cursor", "extra", "other"]
assert list(edge_fields.keys()) == ['node', 'cursor', 'extra', 'other']
assert isinstance(edge_fields["node"], Field)
assert edge_fields["node"].type == MyObject
assert isinstance(edge_fields['node'], Field)
assert edge_fields['node'].type == MyObject
assert isinstance(edge_fields["other"], Field)
assert edge_fields["other"].type == String
def test_edge_with_nonnull_node():
class MyObjectConnection(Connection):
class Meta:
node = NonNull(MyObject)
edge_fields = MyObjectConnection.Edge._meta.fields
assert isinstance(edge_fields["node"], Field)
assert isinstance(edge_fields["node"].type, NonNull)
assert edge_fields["node"].type.of_type == MyObject
assert isinstance(edge_fields['other'], Field)
assert edge_fields['other'].type == String
def test_pageinfo():
assert PageInfo._meta.name == "PageInfo"
assert PageInfo._meta.name == 'PageInfo'
fields = PageInfo._meta.fields
assert list(fields) == [
"has_next_page",
"has_previous_page",
"start_cursor",
"end_cursor",
]
assert list(fields.keys()) == ['has_next_page', 'has_previous_page', 'start_cursor', 'end_cursor']
def test_connectionfield():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
field = ConnectionField(MyObjectConnection)
assert field.args == {
"before": Argument(String),
"after": Argument(String),
"first": Argument(Int),
"last": Argument(Int),
'before': Argument(String),
'after': Argument(String),
'first': Argument(Int),
'last': Argument(Int),
}
def test_connectionfield_node_deprecated():
field = ConnectionField(MyObject)
with raises(Exception) as exc_info:
with pytest.raises(Exception) as exc_info:
field.type
assert "ConnectionFields now need a explicit ConnectionType for Nodes." in str(
exc_info.value
)
assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str(exc_info.value)
def test_connectionfield_custom_args():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
field = ConnectionField(
MyObjectConnection, before=String(required=True), extra=String()
)
field = ConnectionField(MyObjectConnection, before=String(required=True), extra=String())
assert field.args == {
"before": Argument(NonNull(String)),
"after": Argument(String),
"first": Argument(Int),
"last": Argument(Int),
"extra": Argument(String),
'before': Argument(NonNull(String)),
'after': Argument(String),
'first': Argument(Int),
'last': Argument(Int),
'extra': Argument(String),
}
def test_connectionfield_required():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
@ -296,23 +171,8 @@ def test_connectionfield_required():
return []
schema = Schema(query=Query)
executed = schema.execute("{ testConnection { edges { cursor } } }")
executed = schema.execute(
'{ testConnection { edges { cursor } } }'
)
assert not executed.errors
assert executed.data == {"testConnection": {"edges": []}}
def test_connectionfield_strict_types():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
strict_types = True
connection_field = ConnectionField(MyObjectConnection)
edges_field_type = connection_field.type._meta.fields["edges"].type
assert isinstance(edges_field_type, NonNull)
edges_list_element_type = edges_field_type.of_type.of_type
assert isinstance(edges_list_element_type, NonNull)
node_field = edges_list_element_type.of_type._meta.fields["node"]
assert isinstance(node_field.type, NonNull)
assert executed.data == {'testConnection': {'edges': []}}

View File

@ -1,121 +0,0 @@
from pytest import mark
from graphql_relay.utils import base64
from graphene.types import ObjectType, Schema, String
from graphene.relay.connection import Connection, ConnectionField, PageInfo
from graphene.relay.node import Node
letter_chars = ["A", "B", "C", "D", "E"]
class Letter(ObjectType):
class Meta:
interfaces = (Node,)
letter = String()
class LetterConnection(Connection):
class Meta:
node = Letter
class Query(ObjectType):
letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(LetterConnection)
async_letters = ConnectionField(LetterConnection)
node = Node.Field()
def resolve_letters(self, info, **args):
return list(letters.values())
async def resolve_async_letters(self, info, **args):
return list(letters.values())
def resolve_connection_letters(self, info, **args):
return LetterConnection(
page_info=PageInfo(has_next_page=True, has_previous_page=False),
edges=[
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
],
)
schema = Schema(Query)
letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter_chars)}
def edges(selected_letters):
return [
{
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
"cursor": base64("arrayconnection:%s" % letter.id),
}
for letter in [letters[i] for i in selected_letters]
]
def cursor_for(ltr):
letter = letters[ltr]
return base64("arrayconnection:%s" % letter.id)
def execute(args=""):
if args:
args = "(" + args + ")"
return schema.execute(
"""
{
letters%s {
edges {
node {
id
letter
}
cursor
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
"""
% args
)
@mark.asyncio
async def test_connection_async():
result = await schema.execute_async(
"""
{
asyncLetters(first:1) {
edges {
node {
id
letter
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
"""
)
assert not result.errors
assert result.data == {
"asyncLetters": {
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}

View File

@ -1,22 +1,25 @@
from pytest import mark
from collections import OrderedDict
from graphql_relay.utils import base64
from promise import Promise
from ...types import ObjectType, Schema, String
from ..connection import Connection, ConnectionField, PageInfo
from ..node import Node
letter_chars = ["A", "B", "C", "D", "E"]
letter_chars = ['A', 'B', 'C', 'D', 'E']
class Letter(ObjectType):
class Meta:
interfaces = (Node,)
interfaces = (Node, )
letter = String()
class LetterConnection(Connection):
class Meta:
node = Letter
@ -24,50 +27,61 @@ class LetterConnection(Connection):
class Query(ObjectType):
letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(LetterConnection)
async_letters = ConnectionField(LetterConnection)
promise_letters = ConnectionField(LetterConnection)
node = Node.Field()
def resolve_letters(self, info, **args):
return list(letters.values())
async def resolve_async_letters(self, info, **args):
return list(letters.values())
def resolve_promise_letters(self, info, **args):
return Promise.resolve(list(letters.values()))
def resolve_connection_letters(self, info, **args):
return LetterConnection(
page_info=PageInfo(has_next_page=True, has_previous_page=False),
page_info=PageInfo(
has_next_page=True,
has_previous_page=False
),
edges=[
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
],
LetterConnection.Edge(
node=Letter(id=0, letter='A'),
cursor='a-cursor'
),
]
)
schema = Schema(Query)
letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter_chars)}
letters = OrderedDict()
for i, letter in enumerate(letter_chars):
letters[letter] = Letter(id=i, letter=letter)
def edges(selected_letters):
return [
{
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
"cursor": base64("arrayconnection:%s" % letter.id),
'node': {
'id': base64('Letter:%s' % l.id),
'letter': l.letter
},
'cursor': base64('arrayconnection:%s' % l.id)
}
for letter in [letters[i] for i in selected_letters]
for l in [letters[i] for i in selected_letters]
]
def cursor_for(ltr):
letter = letters[ltr]
return base64("arrayconnection:%s" % letter.id)
return base64('arrayconnection:%s' % letter.id)
async def execute(args=""):
def execute(args=''):
if args:
args = "(" + args + ")"
return await schema.execute_async(
"""
args = '(' + args + ')'
return schema.execute('''
{
letters%s {
edges {
@ -85,148 +99,112 @@ async def execute(args=""):
}
}
}
"""
% args
)
''' % args)
async def check(args, letters, has_previous_page=False, has_next_page=False):
result = await execute(args)
def check(args, letters, has_previous_page=False, has_next_page=False):
result = execute(args)
expected_edges = edges(letters)
expected_page_info = {
"hasPreviousPage": has_previous_page,
"hasNextPage": has_next_page,
"endCursor": expected_edges[-1]["cursor"] if expected_edges else None,
"startCursor": expected_edges[0]["cursor"] if expected_edges else None,
'hasPreviousPage': has_previous_page,
'hasNextPage': has_next_page,
'endCursor': expected_edges[-1]['cursor'] if expected_edges else None,
'startCursor': expected_edges[0]['cursor'] if expected_edges else None
}
assert not result.errors
assert result.data == {
"letters": {"edges": expected_edges, "pageInfo": expected_page_info}
'letters': {
'edges': expected_edges,
'pageInfo': expected_page_info
}
}
@mark.asyncio
async def test_returns_all_elements_without_filters():
await check("", "ABCDE")
def test_returns_all_elements_without_filters():
check('', 'ABCDE')
@mark.asyncio
async def test_respects_a_smaller_first():
await check("first: 2", "AB", has_next_page=True)
def test_respects_a_smaller_first():
check('first: 2', 'AB', has_next_page=True)
@mark.asyncio
async def test_respects_an_overly_large_first():
await check("first: 10", "ABCDE")
def test_respects_an_overly_large_first():
check('first: 10', 'ABCDE')
@mark.asyncio
async def test_respects_a_smaller_last():
await check("last: 2", "DE", has_previous_page=True)
def test_respects_a_smaller_last():
check('last: 2', 'DE', has_previous_page=True)
@mark.asyncio
async def test_respects_an_overly_large_last():
await check("last: 10", "ABCDE")
def test_respects_an_overly_large_last():
check('last: 10', 'ABCDE')
@mark.asyncio
async def test_respects_first_and_after():
await check(f'first: 2, after: "{cursor_for("B")}"', "CD", has_next_page=True)
def test_respects_first_and_after():
check('first: 2, after: "{}"'.format(cursor_for('B')), 'CD', has_next_page=True)
@mark.asyncio
async def test_respects_first_and_after_with_long_first():
await check(f'first: 10, after: "{cursor_for("B")}"', "CDE")
def test_respects_first_and_after_with_long_first():
check('first: 10, after: "{}"'.format(cursor_for('B')), 'CDE')
@mark.asyncio
async def test_respects_last_and_before():
await check(f'last: 2, before: "{cursor_for("D")}"', "BC", has_previous_page=True)
def test_respects_last_and_before():
check('last: 2, before: "{}"'.format(cursor_for('D')), 'BC', has_previous_page=True)
@mark.asyncio
async def test_respects_last_and_before_with_long_last():
await check(f'last: 10, before: "{cursor_for("D")}"', "ABC")
def test_respects_last_and_before_with_long_last():
check('last: 10, before: "{}"'.format(cursor_for('D')), 'ABC')
@mark.asyncio
async def test_respects_first_and_after_and_before_too_few():
await check(
f'first: 2, after: "{cursor_for("A")}", before: "{cursor_for("E")}"',
"BC",
has_next_page=True,
)
def test_respects_first_and_after_and_before_too_few():
check('first: 2, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BC', has_next_page=True)
@mark.asyncio
async def test_respects_first_and_after_and_before_too_many():
await check(
f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_respects_first_and_after_and_before_too_many():
check('first: 4, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD')
@mark.asyncio
async def test_respects_first_and_after_and_before_exactly_right():
await check(
f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_respects_first_and_after_and_before_exactly_right():
check('first: 3, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), "BCD")
@mark.asyncio
async def test_respects_last_and_after_and_before_too_few():
await check(
f'last: 2, after: "{cursor_for("A")}", before: "{cursor_for("E")}"',
"CD",
has_previous_page=True,
)
def test_respects_last_and_after_and_before_too_few():
check('last: 2, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'CD', has_previous_page=True)
@mark.asyncio
async def test_respects_last_and_after_and_before_too_many():
await check(
f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_respects_last_and_after_and_before_too_many():
check('last: 4, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD')
@mark.asyncio
async def test_respects_last_and_after_and_before_exactly_right():
await check(
f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_respects_last_and_after_and_before_exactly_right():
check('last: 3, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD')
@mark.asyncio
async def test_returns_no_elements_if_first_is_0():
await check("first: 0", "", has_next_page=True)
def test_returns_no_elements_if_first_is_0():
check('first: 0', '', has_next_page=True)
@mark.asyncio
async def test_returns_all_elements_if_cursors_are_invalid():
await check('before: "invalid" after: "invalid"', "ABCDE")
def test_returns_all_elements_if_cursors_are_invalid():
check('before: "invalid" after: "invalid"', 'ABCDE')
@mark.asyncio
async def test_returns_all_elements_if_cursors_are_on_the_outside():
await check(
f'before: "{base64("arrayconnection:%s" % 6)}" after: "{base64("arrayconnection:%s" % -1)}"',
"ABCDE",
)
def test_returns_all_elements_if_cursors_are_on_the_outside():
check(
'before: "{}" after: "{}"'.format(
base64(
'arrayconnection:%s' % 6),
base64(
'arrayconnection:%s' % -1)),
'ABCDE')
@mark.asyncio
async def test_returns_no_elements_if_cursors_cross():
await check(
f'before: "{base64("arrayconnection:%s" % 2)}" after: "{base64("arrayconnection:%s" % 4)}"',
"",
)
def test_returns_no_elements_if_cursors_cross():
check('before: "{}" after: "{}"'.format(base64('arrayconnection:%s' % 2), base64('arrayconnection:%s' % 4)), '')
@mark.asyncio
async def test_connection_type_nodes():
result = await schema.execute_async(
"""
def test_connection_type_nodes():
result = schema.execute('''
{
connectionLetters {
edges {
@ -242,26 +220,30 @@ async def test_connection_type_nodes():
}
}
}
"""
)
''')
assert not result.errors
assert result.data == {
"connectionLetters": {
"edges": [
{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}, "cursor": "a-cursor"}
],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
'connectionLetters': {
'edges': [{
'node': {
'id': 'TGV0dGVyOjA=',
'letter': 'A',
},
'cursor': 'a-cursor',
}],
'pageInfo': {
'hasPreviousPage': False,
'hasNextPage': True,
}
}
}
@mark.asyncio
async def test_connection_async():
result = await schema.execute_async(
"""
def test_connection_promise():
result = schema.execute('''
{
asyncLetters(first:1) {
promiseLetters(first:1) {
edges {
node {
id
@ -274,13 +256,20 @@ async def test_connection_async():
}
}
}
"""
)
''')
assert not result.errors
assert result.data == {
"asyncLetters": {
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
'promiseLetters': {
'edges': [{
'node': {
'id': 'TGV0dGVyOjA=',
'letter': 'A',
},
}],
'pageInfo': {
'hasPreviousPage': False,
'hasNextPage': True,
}
}
}

View File

@ -1,325 +0,0 @@
import re
from uuid import uuid4
from graphql import graphql_sync
from ..id_type import BaseGlobalIDType, SimpleGlobalIDType, UUIDGlobalIDType
from ..node import Node
from ...types import Int, ObjectType, Schema, String
class TestUUIDGlobalID:
def setup_method(self):
self.user_list = [
{"id": uuid4(), "name": "First"},
{"id": uuid4(), "name": "Second"},
{"id": uuid4(), "name": "Third"},
{"id": uuid4(), "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomNode(Node):
class Meta:
global_id_type = UUIDGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: UUID!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: UUID!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query userById($id: UUID!) {
user(id: $id) {
id
name
}
}"""
# UUID need to be converted to string for serialization
result = graphql_sync(
self.graphql_schema,
query,
variable_values={"id": str(self.user_list[0]["id"])},
)
assert not result.errors
assert result.data["user"]["id"] == str(self.user_list[0]["id"])
assert result.data["user"]["name"] == self.user_list[0]["name"]
class TestSimpleGlobalID:
def setup_method(self):
self.user_list = [
{"id": "my global primary key in clear 1", "name": "First"},
{"id": "my global primary key in clear 2", "name": "Second"},
{"id": "my global primary key in clear 3", "name": "Third"},
{"id": "my global primary key in clear 4", "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomNode(Node):
class Meta:
global_id_type = SimpleGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: ID!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: ID!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query {
user(id: "my global primary key in clear 3") {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["id"] == self.user_list[2]["id"]
assert result.data["user"]["name"] == self.user_list[2]["name"]
class TestCustomGlobalID:
def setup_method(self):
self.user_list = [
{"id": 1, "name": "First"},
{"id": 2, "name": "Second"},
{"id": 3, "name": "Third"},
{"id": 4, "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomGlobalIDType(BaseGlobalIDType):
"""
Global id that is simply and integer in clear.
"""
graphene_type = Int
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: Int!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: Int!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["id"] == self.user_list[1]["id"]
assert result.data["user"]["name"] == self.user_list[1]["name"]
class TestIncompleteCustomGlobalID:
def setup_method(self):
self.user_list = [
{"id": 1, "name": "First"},
{"id": 2, "name": "Second"},
{"id": 3, "name": "Third"},
{"id": 4, "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
def test_must_define_to_global_id(self):
"""
Test that if the `to_global_id` method is not defined, we can query the object, but we can't request its ID.
"""
class CustomGlobalIDType(BaseGlobalIDType):
graphene_type = Int
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
query = """query {
user(id: 2) {
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["name"] == self.user_list[1]["name"]
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert result.errors is not None
assert len(result.errors) == 1
assert result.errors[0].path == ["user", "id"]
def test_must_define_resolve_global_id(self):
"""
Test that if the `resolve_global_id` method is not defined, we can't query the object by ID.
"""
class CustomGlobalIDType(BaseGlobalIDType):
graphene_type = Int
@classmethod
def to_global_id(cls, _type, _id):
return _id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert result.errors is not None
assert len(result.errors) == 1
assert result.errors[0].path == ["user"]

View File

@ -6,18 +6,20 @@ from ..node import GlobalID, Node
class CustomNode(Node):
class Meta:
name = "Node"
name = 'Node'
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
class Info:
class Info(object):
def __init__(self, parent_type):
self.parent_type = GrapheneObjectType(
graphene_type=parent_type,
@ -25,7 +27,7 @@ class Info:
description=parent_type._meta.description,
fields=None,
is_type_of=parent_type.is_type_of,
interfaces=None,
interfaces=None
)
@ -43,16 +45,16 @@ def test_global_id_allows_overriding_of_node_and_required():
def test_global_id_defaults_to_info_parent_type():
my_id = "1"
my_id = '1'
gid = GlobalID()
id_resolver = gid.wrap_resolve(lambda *_: my_id)
id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, Info(User))
assert my_global_id == to_global_id(User._meta.name, my_id)
def test_global_id_allows_setting_customer_parent_type():
my_id = "1"
my_id = '1'
gid = GlobalID(parent_type=User)
id_resolver = gid.wrap_resolve(lambda *_: my_id)
id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, None)
assert my_global_id == to_global_id(User._meta.name, my_id)

View File

@ -1,20 +1,13 @@
from pytest import mark, raises
import pytest
from promise import Promise
from ...types import (
ID,
Argument,
Field,
InputField,
InputObjectType,
NonNull,
ObjectType,
Schema,
)
from ...types import (ID, Argument, Field, InputField, InputObjectType,
NonNull, ObjectType, Schema)
from ...types.scalars import String
from ..mutation import ClientIDMutation
class SharedFields:
class SharedFields(object):
shared = String()
@ -26,6 +19,7 @@ class MyNode(ObjectType):
class SaySomething(ClientIDMutation):
class Input:
what = String()
@ -36,14 +30,15 @@ class SaySomething(ClientIDMutation):
return SaySomething(phrase=str(what))
class FixedSaySomething:
__slots__ = ("phrase",)
class FixedSaySomething(object):
__slots__ = 'phrase',
def __init__(self, phrase):
self.phrase = phrase
class SaySomethingFixed(ClientIDMutation):
class Input:
what = String()
@ -54,15 +49,16 @@ class SaySomethingFixed(ClientIDMutation):
return FixedSaySomething(phrase=str(what))
class SaySomethingAsync(ClientIDMutation):
class SaySomethingPromise(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomething(phrase=str(what))
def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return Promise.resolve(SaySomething(phrase=str(what)))
# MyEdge = MyNode.Connection.Edge
@ -72,6 +68,7 @@ class MyEdge(ObjectType):
class OtherMutation(ClientIDMutation):
class Input(SharedFields):
additional_field = String()
@ -79,14 +76,11 @@ class OtherMutation(ClientIDMutation):
my_node_edge = Field(MyEdge)
@staticmethod
def mutate_and_get_payload(
self, info, shared="", additional_field="", client_mutation_id=None
):
def mutate_and_get_payload(self, info, shared='', additional_field='', client_mutation_id=None):
edge_type = MyEdge
return OtherMutation(
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)
my_node_edge=edge_type(cursor='1', node=MyNode(name='name')))
class RootQuery(ObjectType):
@ -96,7 +90,7 @@ class RootQuery(ObjectType):
class Mutation(ObjectType):
say = SaySomething.Field()
say_fixed = SaySomethingFixed.Field()
say_async = SaySomethingAsync.Field()
say_promise = SaySomethingPromise.Field()
other = OtherMutation.Field()
@ -104,67 +98,69 @@ schema = Schema(query=RootQuery, mutation=Mutation)
def test_no_mutate_and_get_payload():
with raises(AssertionError) as excinfo:
with pytest.raises(AssertionError) as excinfo:
class MyMutation(ClientIDMutation):
pass
assert (
"MyMutation.mutate_and_get_payload method is required in a ClientIDMutation."
== str(excinfo.value)
)
assert "MyMutation.mutate_and_get_payload method is required in a ClientIDMutation." == str(
excinfo.value)
def test_mutation():
fields = SaySomething._meta.fields
assert list(fields) == ["phrase", "client_mutation_id"]
assert list(fields.keys()) == ['phrase', 'client_mutation_id']
assert SaySomething._meta.name == "SaySomethingPayload"
assert isinstance(fields["phrase"], Field)
assert isinstance(fields['phrase'], Field)
field = SaySomething.Field()
assert field.type == SaySomething
assert list(field.args) == ["input"]
assert isinstance(field.args["input"], Argument)
assert isinstance(field.args["input"].type, NonNull)
assert field.args["input"].type.of_type == SaySomething.Input
assert isinstance(fields["client_mutation_id"], Field)
assert fields["client_mutation_id"].name == "clientMutationId"
assert fields["client_mutation_id"].type == String
assert list(field.args.keys()) == ['input']
assert isinstance(field.args['input'], Argument)
assert isinstance(field.args['input'].type, NonNull)
assert field.args['input'].type.of_type == SaySomething.Input
assert isinstance(fields['client_mutation_id'], Field)
assert fields['client_mutation_id'].name == 'clientMutationId'
assert fields['client_mutation_id'].type == String
def test_mutation_input():
Input = SaySomething.Input
assert issubclass(Input, InputObjectType)
fields = Input._meta.fields
assert list(fields) == ["what", "client_mutation_id"]
assert isinstance(fields["what"], InputField)
assert fields["what"].type == String
assert isinstance(fields["client_mutation_id"], InputField)
assert fields["client_mutation_id"].type == String
assert list(fields.keys()) == ['what', 'client_mutation_id']
assert isinstance(fields['what'], InputField)
assert fields['what'].type == String
assert isinstance(fields['client_mutation_id'], InputField)
assert fields['client_mutation_id'].type == String
def test_subclassed_mutation():
fields = OtherMutation._meta.fields
assert list(fields) == ["name", "my_node_edge", "client_mutation_id"]
assert isinstance(fields["name"], Field)
assert list(fields.keys()) == [
'name', 'my_node_edge', 'client_mutation_id'
]
assert isinstance(fields['name'], Field)
field = OtherMutation.Field()
assert field.type == OtherMutation
assert list(field.args) == ["input"]
assert isinstance(field.args["input"], Argument)
assert isinstance(field.args["input"].type, NonNull)
assert field.args["input"].type.of_type == OtherMutation.Input
assert list(field.args.keys()) == ['input']
assert isinstance(field.args['input'], Argument)
assert isinstance(field.args['input'].type, NonNull)
assert field.args['input'].type.of_type == OtherMutation.Input
def test_subclassed_mutation_input():
Input = OtherMutation.Input
assert issubclass(Input, InputObjectType)
fields = Input._meta.fields
assert list(fields) == ["shared", "additional_field", "client_mutation_id"]
assert isinstance(fields["shared"], InputField)
assert fields["shared"].type == String
assert isinstance(fields["additional_field"], InputField)
assert fields["additional_field"].type == String
assert isinstance(fields["client_mutation_id"], InputField)
assert fields["client_mutation_id"].type == String
assert list(fields.keys()) == [
'shared', 'additional_field', 'client_mutation_id'
]
assert isinstance(fields['shared'], InputField)
assert fields['shared'].type == String
assert isinstance(fields['additional_field'], InputField)
assert fields['additional_field'].type == String
assert isinstance(fields['client_mutation_id'], InputField)
assert fields['client_mutation_id'].type == String
def test_node_query():
@ -172,25 +168,22 @@ def test_node_query():
'mutation a { say(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert not executed.errors
assert executed.data == {"say": {"phrase": "hello"}}
assert executed.data == {'say': {'phrase': 'hello'}}
def test_node_query_fixed():
executed = schema.execute(
'mutation a { sayFixed(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert "Cannot set client_mutation_id in the payload object" in str(
executed.errors[0]
)
assert "Cannot set client_mutation_id in the payload object" in str(executed.errors[0])
@mark.asyncio
async def test_node_query_async():
executed = await schema.execute_async(
'mutation a { sayAsync(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
def test_node_query_promise():
executed = schema.execute(
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert not executed.errors
assert executed.data == {"sayAsync": {"phrase": "hello"}}
assert executed.data == {'sayPromise': {'phrase': 'hello'}}
def test_edge_query():
@ -199,8 +192,13 @@ def test_edge_query():
)
assert not executed.errors
assert dict(executed.data) == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
'other': {
'clientMutationId': '1',
'myNodeEdge': {
'cursor': '1',
'node': {
'name': 'name'
}
}
}
}

View File

@ -1,90 +0,0 @@
from pytest import mark
from graphene.types import ID, Field, ObjectType, Schema
from graphene.types.scalars import String
from graphene.relay.mutation import ClientIDMutation
from graphene.test import Client
class SharedFields(object):
shared = String()
class MyNode(ObjectType):
# class Meta:
# interfaces = (Node, )
id = ID()
name = String()
class SaySomethingAsync(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomethingAsync(phrase=str(what))
# MyEdge = MyNode.Connection.Edge
class MyEdge(ObjectType):
node = Field(MyNode)
cursor = String()
class OtherMutation(ClientIDMutation):
class Input(SharedFields):
additional_field = String()
name = String()
my_node_edge = Field(MyEdge)
@staticmethod
def mutate_and_get_payload(
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)
class RootQuery(ObjectType):
something = String()
class Mutation(ObjectType):
say_promise = SaySomethingAsync.Field()
other = OtherMutation.Field()
schema = Schema(query=RootQuery, mutation=Mutation)
client = Client(schema)
@mark.asyncio
async def test_node_query_promise():
executed = await client.execute_async(
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert isinstance(executed, dict)
assert "errors" not in executed
assert executed["data"] == {"sayPromise": {"phrase": "hello"}}
@mark.asyncio
async def test_edge_query():
executed = await client.execute_async(
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }'
)
assert isinstance(executed, dict)
assert "errors" not in executed
assert executed["data"] == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
}
}

View File

@ -1,5 +1,4 @@
import re
from textwrap import dedent
from collections import OrderedDict
from graphql_relay import to_global_id
@ -7,18 +6,19 @@ from ...types import ObjectType, Schema, String
from ..node import Node, is_node
class SharedNodeFields:
class SharedNodeFields(object):
shared = String()
something_else = String()
def resolve_something_else(*_):
return "----"
return '----'
class MyNode(ObjectType):
class Meta:
interfaces = (Node,)
class Meta:
interfaces = (Node, )
name = String()
@staticmethod
@ -30,10 +30,10 @@ class MyOtherNode(SharedNodeFields, ObjectType):
extra_field = String()
class Meta:
interfaces = (Node,)
interfaces = (Node, )
def resolve_extra_field(self, *_):
return "extra field info."
return 'extra field info.'
@staticmethod
def get_node(info, id):
@ -51,10 +51,9 @@ schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode])
def test_node_good():
assert "id" in MyNode._meta.fields
assert 'id' in MyNode._meta.fields
assert is_node(MyNode)
assert not is_node(object)
assert not is_node("node")
def test_node_query():
@ -62,52 +61,33 @@ def test_node_query():
'{ node(id:"%s") { ... on MyNode { name } } }' % Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {"node": {"name": "1"}}
assert executed.data == {'node': {'name': '1'}}
def test_subclassed_node_query():
executed = schema.execute(
'{ node(id:"%s") { ... on MyOtherNode { shared, extraField, somethingElse } } }'
% to_global_id("MyOtherNode", 1)
)
'{ node(id:"%s") { ... on MyOtherNode { shared, extraField, somethingElse } } }' %
to_global_id("MyOtherNode", 1))
assert not executed.errors
assert executed.data == {
"node": {
"shared": "1",
"extraField": "extra field info.",
"somethingElse": "----",
}
}
assert executed.data == OrderedDict({'node': OrderedDict(
[('shared', '1'), ('extraField', 'extra field info.'), ('somethingElse', '----')])})
def test_node_requesting_non_node():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("RootQuery", 1)
)
assert executed.errors
assert re.match(
r"ObjectType .* does not implement the .* interface.",
executed.errors[0].message,
)
assert executed.data == {"node": None}
def test_node_requesting_unknown_type():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("UnknownType", 1)
)
assert executed.errors
assert re.match(r"Relay Node .* not found in schema", executed.errors[0].message)
assert executed.data == {"node": None}
assert executed.data == {
'node': None
}
def test_node_query_incorrect_id():
executed = schema.execute(
'{ node(id:"%s") { ... on MyNode { name } } }' % "something:2"
)
assert executed.errors
assert re.match(r"Unable to parse global ID .*", executed.errors[0].message)
assert executed.data == {"node": None}
assert not executed.errors
assert executed.data == {'node': None}
def test_node_field():
@ -122,98 +102,66 @@ def test_node_field_custom():
assert node_field.node_type == Node
def test_node_field_args():
field_args = {
"name": "my_custom_name",
"description": "my_custom_description",
"deprecation_reason": "my_custom_deprecation_reason",
}
node_field = Node.Field(**field_args)
for field_arg, value in field_args.items():
assert getattr(node_field, field_arg) == value
def test_node_field_only_type():
executed = schema.execute(
'{ onlyNode(id:"%s") { __typename, name } } ' % Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {"onlyNode": {"__typename": "MyNode", "name": "1"}}
assert executed.data == {'onlyNode': {'__typename': 'MyNode', 'name': '1'}}
def test_node_field_only_type_wrong():
executed = schema.execute(
'{ onlyNode(id:"%s") { __typename, name } } '
% Node.to_global_id("MyOtherNode", 1)
'{ onlyNode(id:"%s") { __typename, name } } ' % Node.to_global_id("MyOtherNode", 1)
)
assert len(executed.errors) == 1
assert str(executed.errors[0]).startswith("Must receive a MyNode id.")
assert executed.data == {"onlyNode": None}
assert str(executed.errors[0]) == 'Must receive a MyNode id.'
assert executed.data == {'onlyNode': None}
def test_node_field_only_lazy_type():
executed = schema.execute(
'{ onlyNodeLazy(id:"%s") { __typename, name } } '
% Node.to_global_id("MyNode", 1)
'{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {"onlyNodeLazy": {"__typename": "MyNode", "name": "1"}}
assert executed.data == {'onlyNodeLazy': {'__typename': 'MyNode', 'name': '1'}}
def test_node_field_only_lazy_type_wrong():
executed = schema.execute(
'{ onlyNodeLazy(id:"%s") { __typename, name } } '
% Node.to_global_id("MyOtherNode", 1)
'{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyOtherNode", 1)
)
assert len(executed.errors) == 1
assert str(executed.errors[0]).startswith("Must receive a MyNode id.")
assert executed.data == {"onlyNodeLazy": None}
assert str(executed.errors[0]) == 'Must receive a MyNode id.'
assert executed.data == {'onlyNodeLazy': None}
def test_str_schema():
assert (
str(schema).strip()
== dedent(
'''
schema {
query: RootQuery
}
assert str(schema) == """
schema {
query: RootQuery
}
type MyNode implements Node {
"""The ID of the object"""
id: ID!
name: String
}
type MyNode implements Node {
id: ID!
name: String
}
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type MyOtherNode implements Node {
id: ID!
shared: String
somethingElse: String
extraField: String
}
type MyOtherNode implements Node {
"""The ID of the object"""
id: ID!
shared: String
somethingElse: String
extraField: String
}
interface Node {
id: ID!
}
type RootQuery {
first: String
node(
"""The ID of the object"""
id: ID!
): Node
onlyNode(
"""The ID of the object"""
id: ID!
): MyNode
onlyNodeLazy(
"""The ID of the object"""
id: ID!
): MyNode
}
'''
).strip()
)
type RootQuery {
first: String
node(id: ID!): Node
onlyNode(id: ID!): MyNode
onlyNodeLazy(id: ID!): MyNode
}
""".lstrip()

View File

@ -1,6 +1,4 @@
from textwrap import dedent
from graphql import graphql_sync
from graphql import graphql
from ...types import Interface, ObjectType, Schema
from ...types.scalars import Int, String
@ -8,16 +6,17 @@ from ..node import Node
class CustomNode(Node):
class Meta:
name = "Node"
name = 'Node'
@staticmethod
def to_global_id(type_, id):
def to_global_id(type, id):
return id
@staticmethod
def get_node_from_global_id(info, id, only_type=None):
assert info.schema is graphql_schema
assert info.schema == schema
if id in user_data:
return user_data.get(id)
else:
@ -25,24 +24,31 @@ class CustomNode(Node):
class BasePhoto(Interface):
width = Int(description="The width of the photo in pixels")
width = Int()
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String(description="The full name of the user")
name = String()
class Photo(ObjectType):
class Meta:
interfaces = [CustomNode, BasePhoto]
user_data = {"1": User(id="1", name="John Doe"), "2": User(id="2", name="Jane Smith")}
user_data = {
'1': User(id='1', name='John Doe'),
'2': User(id='2', name='Jane Smith'),
}
photo_data = {"3": Photo(id="3", width=300), "4": Photo(id="4", width=400)}
photo_data = {
'3': Photo(id='3', width=300),
'4': Photo(id='4', width=400),
}
class RootQuery(ObjectType):
@ -50,85 +56,75 @@ class RootQuery(ObjectType):
schema = Schema(query=RootQuery, types=[User, Photo])
graphql_schema = schema.graphql_schema
def test_str_schema_correct():
assert (
str(schema).strip()
== dedent(
'''
schema {
query: RootQuery
}
assert str(schema) == '''schema {
query: RootQuery
}
type User implements Node {
"""The ID of the object"""
id: ID!
interface BasePhoto {
width: Int
}
"""The full name of the user"""
name: String
}
interface Node {
id: ID!
}
interface Node {
"""The ID of the object"""
id: ID!
}
type Photo implements Node, BasePhoto {
id: ID!
width: Int
}
type Photo implements Node & BasePhoto {
"""The ID of the object"""
id: ID!
type RootQuery {
node(id: ID!): Node
}
"""The width of the photo in pixels"""
width: Int
}
interface BasePhoto {
"""The width of the photo in pixels"""
width: Int
}
type RootQuery {
node(
"""The ID of the object"""
id: ID!
): Node
}
'''
).strip()
)
type User implements Node {
id: ID!
name: String
}
'''
def test_gets_the_correct_id_for_users():
query = """
query = '''
{
node(id: "1") {
id
}
}
"""
expected = {"node": {"id": "1"}}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': {
'id': '1',
}
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_id_for_photos():
query = """
query = '''
{
node(id: "4") {
id
}
}
"""
expected = {"node": {"id": "4"}}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': {
'id': '4',
}
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_name_for_users():
query = """
query = '''
{
node(id: "1") {
id
@ -137,15 +133,20 @@ def test_gets_the_correct_name_for_users():
}
}
}
"""
expected = {"node": {"id": "1", "name": "John Doe"}}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': {
'id': '1',
'name': 'John Doe'
}
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_width_for_photos():
query = """
query = '''
{
node(id: "4") {
id
@ -154,45 +155,60 @@ def test_gets_the_correct_width_for_photos():
}
}
}
"""
expected = {"node": {"id": "4", "width": 400}}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': {
'id': '4',
'width': 400
}
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_typename_for_users():
query = """
query = '''
{
node(id: "1") {
id
__typename
}
}
"""
expected = {"node": {"id": "1", "__typename": "User"}}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': {
'id': '1',
'__typename': 'User'
}
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_typename_for_photos():
query = """
query = '''
{
node(id: "4") {
id
__typename
}
}
"""
expected = {"node": {"id": "4", "__typename": "Photo"}}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': {
'id': '4',
'__typename': 'Photo'
}
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_ignores_photo_fragments_on_user():
query = """
query = '''
{
node(id: "1") {
id
@ -201,29 +217,35 @@ def test_ignores_photo_fragments_on_user():
}
}
}
"""
expected = {"node": {"id": "1"}}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': {
'id': '1',
}
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_returns_null_for_bad_ids():
query = """
query = '''
{
node(id: "5") {
id
}
}
"""
expected = {"node": None}
result = graphql_sync(graphql_schema, query)
'''
expected = {
'node': None
}
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_have_correct_node_interface():
query = """
query = '''
{
__type(name: "Node") {
name
@ -240,29 +262,32 @@ def test_have_correct_node_interface():
}
}
}
"""
'''
expected = {
"__type": {
"name": "Node",
"kind": "INTERFACE",
"fields": [
'__type': {
'name': 'Node',
'kind': 'INTERFACE',
'fields': [
{
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {"name": "ID", "kind": "SCALAR"},
},
'name': 'id',
'type': {
'kind': 'NON_NULL',
'ofType': {
'name': 'ID',
'kind': 'SCALAR'
}
}
}
],
]
}
}
result = graphql_sync(graphql_schema, query)
result = graphql(schema, query)
assert not result.errors
assert result.data == expected
def test_has_correct_node_root_field():
query = """
query = '''
{
__schema {
queryType {
@ -286,28 +311,34 @@ def test_has_correct_node_root_field():
}
}
}
"""
'''
expected = {
"__schema": {
"queryType": {
"fields": [
'__schema': {
'queryType': {
'fields': [
{
"name": "node",
"type": {"name": "Node", "kind": "INTERFACE"},
"args": [
'name': 'node',
'type': {
'name': 'Node',
'kind': 'INTERFACE'
},
'args': [
{
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {"name": "ID", "kind": "SCALAR"},
},
'name': 'id',
'type': {
'kind': 'NON_NULL',
'ofType': {
'name': 'ID',
'kind': 'SCALAR'
}
}
}
],
]
}
]
}
}
}
result = graphql_sync(graphql_schema, query)
result = graphql(schema, query)
assert not result.errors
assert result.data == expected

View File

@ -1,3 +1,6 @@
from promise import Promise, is_thenable
import six
from graphql.error import format_error as format_graphql_error
from graphql.error import GraphQLError
from graphene.types.schema import Schema
@ -5,20 +8,28 @@ from graphene.types.schema import Schema
def default_format_error(error):
if isinstance(error, GraphQLError):
return error.formatted
return {"message": str(error)}
return format_graphql_error(error)
return {'message': six.text_type(error)}
def format_execution_result(execution_result, format_error):
if execution_result:
response = {}
if execution_result.errors:
response["errors"] = [format_error(e) for e in execution_result.errors]
response["data"] = execution_result.data
response['errors'] = [
format_error(e) for e in execution_result.errors
]
if not execution_result.invalid:
response['data'] = execution_result.data
return response
class Client:
class Client(object):
def __init__(self, schema, format_error=None, **execute_options):
assert isinstance(schema, Schema)
self.schema = schema
@ -29,11 +40,9 @@ class Client:
return format_execution_result(result, self.format_error)
def execute(self, *args, **kwargs):
executed = self.schema.execute(*args, **dict(self.execute_options, **kwargs))
return self.format_result(executed)
executed = self.schema.execute(*args,
**dict(self.execute_options, **kwargs))
if is_thenable(executed):
return Promise.resolve(executed).then(self.format_result)
async def execute_async(self, *args, **kwargs):
executed = await self.schema.execute_async(
*args, **dict(self.execute_options, **kwargs)
)
return self.format_result(executed)

View File

@ -1,41 +0,0 @@
# https://github.com/graphql-python/graphene/issues/1293
from datetime import datetime, timezone
import graphene
from graphql.utilities import print_schema
class Filters(graphene.InputObjectType):
datetime_after = graphene.DateTime(
required=False,
default_value=datetime.fromtimestamp(1434549820.776, timezone.utc),
)
datetime_before = graphene.DateTime(
required=False,
default_value=datetime.fromtimestamp(1444549820.776, timezone.utc),
)
class SetDatetime(graphene.Mutation):
class Arguments:
filters = Filters(required=True)
ok = graphene.Boolean()
def mutate(root, info, filters):
return SetDatetime(ok=True)
class Query(graphene.ObjectType):
goodbye = graphene.String()
class Mutations(graphene.ObjectType):
set_datetime = SetDatetime.Field()
def test_schema_printable_with_default_datetime_value():
schema = graphene.Schema(query=Query, mutation=Mutations)
schema_str = print_schema(schema.graphql_schema)
assert schema_str, "empty schema printed"

View File

@ -1,36 +0,0 @@
from ...types import ObjectType, Schema, String, NonNull
class Query(ObjectType):
hello = String(input=NonNull(String))
def resolve_hello(self, info, input):
if input == "nothing":
return None
return f"Hello {input}!"
schema = Schema(query=Query)
def test_required_input_provided():
"""
Test that a required argument works when provided.
"""
input_value = "Potato"
result = schema.execute('{ hello(input: "%s") }' % input_value)
assert not result.errors
assert result.data == {"hello": "Hello Potato!"}
def test_required_input_missing():
"""
Test that a required argument raised an error if not provided.
"""
result = schema.execute("{ hello }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "Field 'hello' argument 'input' of type 'String!' is required, but it was not provided."
)

View File

@ -1,53 +0,0 @@
import pytest
from ...types.base64 import Base64
from ...types.datetime import Date, DateTime
from ...types.decimal import Decimal
from ...types.generic import GenericScalar
from ...types.json import JSONString
from ...types.objecttype import ObjectType
from ...types.scalars import ID, BigInt, Boolean, Float, Int, String
from ...types.schema import Schema
from ...types.uuid import UUID
@pytest.mark.parametrize(
"input_type,input_value",
[
(Date, '"2022-02-02"'),
(GenericScalar, '"foo"'),
(Int, "1"),
(BigInt, "12345678901234567890"),
(Float, "1.1"),
(String, '"foo"'),
(Boolean, "true"),
(ID, "1"),
(DateTime, '"2022-02-02T11:11:11"'),
(UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'),
(Decimal, '"1.1"'),
(JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'),
(Base64, '"Q2hlbG8gd29ycmxkCg=="'),
],
)
def test_parse_literal_with_variables(input_type, input_value):
# input_b needs to be evaluated as literal while the variable dict for
# input_a is passed along.
class Query(ObjectType):
generic = GenericScalar(input_a=GenericScalar(), input_b=input_type())
def resolve_generic(self, info, input_a=None, input_b=None):
return input
schema = Schema(query=Query)
query = f"""
query Test($a: GenericScalar){{
generic(inputA: $a, inputB: {input_value})
}}
"""
result = schema.execute(
query,
variables={"a": "bar"},
)
assert not result.errors

View File

@ -16,18 +16,20 @@ class Error(graphene.ObjectType):
class CreatePostResult(graphene.Union):
class Meta:
types = [Success, Error]
class CreatePost(graphene.Mutation):
class Arguments:
class Input:
text = graphene.String(required=True)
result = graphene.Field(CreatePostResult)
def mutate(self, info, text):
result = Success(yeah="yeah")
result = Success(yeah='yeah')
return CreatePost(result=result)
@ -35,12 +37,11 @@ class CreatePost(graphene.Mutation):
class Mutations(graphene.ObjectType):
create_post = CreatePost.Field()
# tests.py
def test_create_post():
query_string = """
query_string = '''
mutation {
createPost(text: "Try this out") {
result {
@ -48,10 +49,10 @@ def test_create_post():
}
}
}
"""
'''
schema = graphene.Schema(query=Query, mutation=Mutations)
result = schema.execute(query_string)
assert not result.errors
assert result.data["createPost"]["result"]["__typename"] == "Success"
assert result.data['createPost']['result']['__typename'] == 'Success'

View File

@ -1,6 +1,6 @@
# https://github.com/graphql-python/graphene/issues/356
from pytest import raises
import pytest
import graphene
from graphene import relay
@ -15,6 +15,7 @@ class SomeTypeTwo(graphene.ObjectType):
class MyUnion(graphene.Union):
class Meta:
types = (SomeTypeOne, SomeTypeTwo)
@ -23,11 +24,10 @@ def test_issue():
class Query(graphene.ObjectType):
things = relay.ConnectionField(MyUnion)
with raises(Exception) as exc_info:
with pytest.raises(Exception) as exc_info:
graphene.Schema(query=Query)
assert str(exc_info.value) == (
"Query fields cannot be resolved."
" IterableConnectionField type has to be a subclass of Connection."
' Received "MyUnion".'
'IterableConnectionField type have to be a subclass of Connection. '
'Received "MyUnion".'
)

View File

@ -12,35 +12,35 @@ class SpecialOptions(ObjectTypeOptions):
class SpecialObjectType(ObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
def __init_subclass_with_meta__(cls, other_attr='default', **options):
_meta = SpecialOptions(cls)
_meta.other_attr = other_attr
super(SpecialObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
super(SpecialObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_special_objecttype_could_be_subclassed():
class MyType(SpecialObjectType):
class Meta:
other_attr = "yeah!"
assert MyType._meta.other_attr == "yeah!"
class Meta:
other_attr = 'yeah!'
assert MyType._meta.other_attr == 'yeah!'
def test_special_objecttype_could_be_subclassed_default():
class MyType(SpecialObjectType):
pass
assert MyType._meta.other_attr == "default"
assert MyType._meta.other_attr == 'default'
def test_special_objecttype_inherit_meta_options():
class MyType(SpecialObjectType):
pass
assert MyType._meta.name == "MyType"
assert MyType._meta.name == 'MyType'
assert MyType._meta.default_resolver is None
assert MyType._meta.interfaces == ()
@ -51,35 +51,35 @@ class SpecialInputObjectTypeOptions(ObjectTypeOptions):
class SpecialInputObjectType(InputObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
def __init_subclass_with_meta__(cls, other_attr='default', **options):
_meta = SpecialInputObjectTypeOptions(cls)
_meta.other_attr = other_attr
super(SpecialInputObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
super(SpecialInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_special_inputobjecttype_could_be_subclassed():
class MyInputObjectType(SpecialInputObjectType):
class Meta:
other_attr = "yeah!"
assert MyInputObjectType._meta.other_attr == "yeah!"
class Meta:
other_attr = 'yeah!'
assert MyInputObjectType._meta.other_attr == 'yeah!'
def test_special_inputobjecttype_could_be_subclassed_default():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.other_attr == "default"
assert MyInputObjectType._meta.other_attr == 'default'
def test_special_inputobjecttype_inherit_meta_options():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.name == "MyInputObjectType"
assert MyInputObjectType._meta.name == 'MyInputObjectType'
# Enum
@ -88,8 +88,9 @@ class SpecialEnumOptions(EnumOptions):
class SpecialEnum(Enum):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
def __init_subclass_with_meta__(cls, other_attr='default', **options):
_meta = SpecialEnumOptions(cls)
_meta.other_attr = other_attr
super(SpecialEnum, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@ -97,21 +98,22 @@ class SpecialEnum(Enum):
def test_special_enum_could_be_subclassed():
class MyEnum(SpecialEnum):
class Meta:
other_attr = "yeah!"
assert MyEnum._meta.other_attr == "yeah!"
class Meta:
other_attr = 'yeah!'
assert MyEnum._meta.other_attr == 'yeah!'
def test_special_enum_could_be_subclassed_default():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.other_attr == "default"
assert MyEnum._meta.other_attr == 'default'
def test_special_enum_inherit_meta_options():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.name == "MyEnum"
assert MyEnum._meta.name == 'MyEnum'

View File

@ -11,14 +11,14 @@ class Query(graphene.ObjectType):
def test_issue():
query_string = """
query_string = '''
query myQuery {
someField(from: "Oh")
}
"""
'''
schema = graphene.Schema(query=Query)
result = schema.execute(query_string)
assert not result.errors
assert result.data["someField"] == "Oh"
assert result.data['someField'] == 'Oh'

View File

@ -7,19 +7,19 @@ import graphene
class MyInputClass(graphene.InputObjectType):
@classmethod
def __init_subclass_with_meta__(
cls, container=None, _meta=None, fields=None, **options
):
cls, container=None, _meta=None, fields=None, **options):
if _meta is None:
_meta = graphene.types.inputobjecttype.InputObjectTypeOptions(cls)
_meta.fields = fields
super(MyInputClass, cls).__init_subclass_with_meta__(
container=container, _meta=_meta, **options
)
container=container, _meta=_meta, **options)
class MyInput(MyInputClass):
class Meta:
fields = dict(x=graphene.Field(graphene.Int))
@ -28,15 +28,15 @@ class Query(graphene.ObjectType):
myField = graphene.Field(graphene.String, input=graphene.Argument(MyInput))
def resolve_myField(parent, info, input):
return "ok"
return 'ok'
def test_issue():
query_string = """
query_string = '''
query myQuery {
myField(input: {x: 1})
}
"""
'''
schema = graphene.Schema(query=Query)
result = schema.execute(query_string)

View File

@ -1,27 +0,0 @@
import pickle
from ...types.enum import Enum
class PickleEnum(Enum):
# is defined outside of test because pickle unable to dump class inside ot pytest function
A = "a"
B = 1
def test_enums_pickling():
a = PickleEnum.A
pickled = pickle.dumps(a)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert a == restored
assert a.value == restored.value
assert a.name == restored.name
b = PickleEnum.B
pickled = pickle.dumps(b)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert b == restored
assert b.value == restored.value
assert b.name == restored.name

View File

@ -1,8 +0,0 @@
import graphene
def test_issue():
options = {"description": "This my enum", "deprecation_reason": "For the funs"}
new_enum = graphene.Enum("MyEnum", [("some", "data")], **options)
assert new_enum._meta.description == options["description"]
assert new_enum._meta.deprecation_reason == options["deprecation_reason"]

View File

@ -1,53 +1,56 @@
from graphql import GraphQLResolveInfo as ResolveInfo
# flake8: noqa
from graphql import ResolveInfo
from .argument import Argument
from .base64 import Base64
from .context import Context
from .objecttype import ObjectType
from .interface import Interface
from .mutation import Mutation
from .scalars import Scalar, String, ID, Int, Float, Boolean
from .datetime import Date, DateTime, Time
from .decimal import Decimal
from .dynamic import Dynamic
from .json import JSONString
from .uuid import UUID
from .schema import Schema
from .structures import List, NonNull
from .enum import Enum
from .field import Field
from .inputfield import InputField
from .argument import Argument
from .inputobjecttype import InputObjectType
from .interface import Interface
from .json import JSONString
from .mutation import Mutation
from .objecttype import ObjectType
from .scalars import ID, BigInt, Boolean, Float, Int, Scalar, String
from .schema import Schema
from .structures import List, NonNull
from .dynamic import Dynamic
from .union import Union
from .uuid import UUID
from .context import Context
# Deprecated
from .abstracttype import AbstractType
__all__ = [
"Argument",
"Base64",
"BigInt",
"Boolean",
"Context",
"Date",
"DateTime",
"Decimal",
"Dynamic",
"Enum",
"Field",
"Float",
"ID",
"InputField",
"InputObjectType",
"Int",
"Interface",
"JSONString",
"List",
"Mutation",
"NonNull",
"ObjectType",
"ResolveInfo",
"Scalar",
"Schema",
"String",
"Time",
"UUID",
"Union",
'ObjectType',
'InputObjectType',
'Interface',
'Mutation',
'Enum',
'Field',
'InputField',
'Schema',
'Scalar',
'String',
'ID',
'Int',
'Float',
'Date',
'DateTime',
'Time',
'JSONString',
'UUID',
'Boolean',
'List',
'NonNull',
'Argument',
'Dynamic',
'Union',
'Context',
'ResolveInfo',
# Deprecated
'AbstractType',
]

View File

@ -0,0 +1,12 @@
from ..utils.deprecated import warn_deprecation
from ..utils.subclass_with_meta import SubclassWithMeta
class AbstractType(SubclassWithMeta):
def __init_subclass__(cls, *args, **kwargs):
warn_deprecation(
"Abstract type is deprecated, please use normal object inheritance instead.\n"
"See more: https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md#deprecations"
)
super(AbstractType, cls).__init_subclass__(*args, **kwargs)

View File

@ -1,5 +1,5 @@
from collections import OrderedDict
from itertools import chain
from graphql import Undefined
from .dynamic import Dynamic
from .mountedtype import MountedType
@ -8,63 +8,17 @@ from .utils import get_type
class Argument(MountedType):
"""
Makes an Argument available on a Field in the GraphQL schema.
Arguments will be parsed and provided to resolver methods for fields as keyword arguments.
All ``arg`` and ``**extra_args`` for a ``graphene.Field`` are implicitly mounted as Argument
using the below parameters.
.. code:: python
from graphene import String, Boolean, Argument
age = String(
# Boolean implicitly mounted as Argument
dog_years=Boolean(description="convert to dog years"),
# Boolean explicitly mounted as Argument
decades=Argument(Boolean, default_value=False),
)
args:
type (class for a graphene.UnmountedType): must be a class (not an instance) of an
unmounted graphene type (ex. scalar or object) which is used for the type of this
argument in the GraphQL schema.
required (optional, bool): indicates this argument as not null in the graphql schema. Same behavior
as graphene.NonNull. Default False.
name (optional, str): the name of the GraphQL argument. Defaults to parameter name.
description (optional, str): the description of the GraphQL argument in the schema.
default_value (optional, Any): The value to be provided if the user does not set this argument in
the operation.
deprecation_reason (optional, str): Setting this value indicates that the argument is
depreciated and may provide instruction or reason on how for clients to proceed. Cannot be
set if the argument is required (see spec).
"""
def __init__(
self,
type_,
default_value=Undefined,
deprecation_reason=None,
description=None,
name=None,
required=False,
_creation_counter=None,
):
def __init__(self, type, default_value=None, description=None, name=None, required=False, _creation_counter=None):
super(Argument, self).__init__(_creation_counter=_creation_counter)
if required:
assert (
deprecation_reason is None
), f"Argument {name} is required, cannot deprecate it."
type_ = NonNull(type_)
type = NonNull(type)
self.name = name
self._type = type_
self._type = type
self.default_value = default_value
self.description = description
self.deprecation_reason = deprecation_reason
@property
def type(self):
@ -72,11 +26,10 @@ class Argument(MountedType):
def __eq__(self, other):
return isinstance(other, Argument) and (
self.name == other.name
and self.type == other.type
and self.default_value == other.default_value
and self.description == other.description
and self.deprecation_reason == other.deprecation_reason
self.name == other.name and
self.type == other.type and
self.default_value == other.default_value and
self.description == other.description
)
@ -84,13 +37,12 @@ def to_arguments(args, extra_args=None):
from .unmountedtype import UnmountedType
from .field import Field
from .inputfield import InputField
if extra_args:
extra_args = sorted(extra_args.items(), key=lambda f: f[1])
else:
extra_args = []
iter_arguments = chain(args.items(), extra_args)
arguments = {}
arguments = OrderedDict()
for default_name, arg in iter_arguments:
if isinstance(arg, Dynamic):
arg = arg.get_type()
@ -103,18 +55,17 @@ def to_arguments(args, extra_args=None):
arg = Argument.mounted(arg)
if isinstance(arg, (InputField, Field)):
raise ValueError(
f"Expected {default_name} to be Argument, "
f"but received {type(arg).__name__}. Try using Argument({arg.type})."
)
raise ValueError('Expected {} to be Argument, but received {}. Try using Argument({}).'.format(
default_name,
type(arg).__name__,
arg.type
))
if not isinstance(arg, Argument):
raise ValueError(f'Unknown argument "{default_name}".')
raise ValueError('Unknown argument "{}".'.format(default_name))
arg_name = default_name or arg.name
assert (
arg_name not in arguments
), f'More than one Argument have same name "{arg_name}".'
assert arg_name not in arguments, 'More than one Argument have same name "{}".'.format(arg_name)
arguments[arg_name] = arg
return arguments

View File

@ -1,17 +1,15 @@
from typing import Type, Optional
from ..utils.subclass_with_meta import SubclassWithMeta, SubclassWithMeta_Meta
from ..utils.subclass_with_meta import SubclassWithMeta
from ..utils.trim_docstring import trim_docstring
class BaseOptions:
name: Optional[str] = None
description: Optional[str] = None
class BaseOptions(object):
name = None # type: str
description = None # type: str
_frozen: bool = False
_frozen = False # type: bool
def __init__(self, class_type: Type):
self.class_type: Type = class_type
def __init__(self, class_type):
self.class_type = class_type # type: Type
def freeze(self):
self._frozen = True
@ -20,25 +18,21 @@ class BaseOptions:
if not self._frozen:
super(BaseOptions, self).__setattr__(name, value)
else:
raise Exception(f"Can't modify frozen Options {self}")
raise Exception("Can't modify frozen Options {}".format(self))
def __repr__(self):
return f"<{self.__class__.__name__} name={repr(self.name)}>"
BaseTypeMeta = SubclassWithMeta_Meta
return "<{} name={}>".format(self.__class__.__name__, repr(self.name))
class BaseType(SubclassWithMeta):
@classmethod
def create_type(cls, class_name, **options):
return type(class_name, (cls,), {"Meta": options})
@classmethod
def __init_subclass_with_meta__(
cls, name=None, description=None, _meta=None, **_kwargs
):
assert "_meta" not in cls.__dict__, "Can't assign meta directly"
def create_type(cls, class_name, **options):
return type(class_name, (cls, ), {'Meta': options})
@classmethod
def __init_subclass_with_meta__(cls, name=None, description=None, _meta=None):
assert "_meta" not in cls.__dict__, "Can't assign directly meta"
if not _meta:
return
_meta.name = name or cls.__name__

View File

@ -1,43 +0,0 @@
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)}")

Some files were not shown because too many files have changed in this diff Show More