diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f72385c..93ab2e6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,28 @@ repos: - repo: git://github.com/pre-commit/pre-commit-hooks - rev: v1.3.0 + rev: v2.1.0 hooks: + - id: check-merge-conflict - id: check-json - id: check-yaml - id: debug-statements - id: end-of-file-fixer exclude: ^docs/.*$ - - id: trailing-whitespace - exclude: README.md - id: pretty-format-json args: - --autofix - - id: flake8 + - id: trailing-whitespace + exclude: README.md - repo: https://github.com/asottile/pyupgrade - rev: v1.4.0 + rev: v1.12.0 hooks: - id: pyupgrade - repo: https://github.com/ambv/black - rev: 18.6b4 + rev: 18.9b0 hooks: - id: black - language_version: python3.6 + language_version: python3 +- repo: https://github.com/PyCQA/flake8 + rev: 3.7.7 + hooks: + - id: flake8 diff --git a/.travis.yml b/.travis.yml index 399ce134..53878e74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,24 @@ language: python matrix: include: - - env: TOXENV=py27 - python: 2.7 - - env: TOXENV=py34 - python: 3.4 - - env: TOXENV=py35 - python: 3.5 - - env: TOXENV=py36 - python: 3.6 - - env: TOXENV=pypy - python: pypy-5.7.1 - - env: TOXENV=pre-commit - python: 3.6 - - env: TOXENV=mypy - python: 3.6 + - env: TOXENV=py27 + python: 2.7 + - env: TOXENV=py34 + python: 3.4 + - env: TOXENV=py35 + python: 3.5 + - env: TOXENV=py36 + python: 3.6 + - env: TOXENV=py37 + python: 3.7 + dist: xenial + sudo: true + - env: TOXENV=pypy + python: pypy-5.7.1 + - env: TOXENV=pre-commit + python: 3.6 + - env: TOXENV=mypy + python: 3.6 install: - pip install coveralls tox script: tox diff --git a/BACKERS.md b/BACKERS.md new file mode 100644 index 00000000..a1264060 --- /dev/null +++ b/BACKERS.md @@ -0,0 +1,97 @@ +

Sponsors & Backers

+ +Graphene is an MIT-licensed open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome [backers](https://github.com/graphql-python/graphene/blob/master/BACKERS.md). If you'd like to join them, please consider: + +- [Become a backer or sponsor on Patreon](https://www.patreon.com/syrusakbary). +- [One-time donation via PayPal.](https://graphene-python.org/support-graphene/) + +

+ + + +

Platinum via Patreon

+ + + + + + + + +
+ + + +
+ +

Gold via Patreon

+ + + + + + + + +
+ + + +
+ + +

Silver via Patreon

+ + + + + + + + +
+ + + +
+ + +

Bronze via Patreon

+ + + + + + + + +
+ + + +
+ + +

Generous Backers via Patreon ($50+)

+ + + +- [Lee Benson](https://github.com/leebenson) +- [Become a Patron](https://www.patreon.com/join/syrusakbary) + + +

Backers via Patreon

+ + + +- [Become a Patron](https://www.patreon.com/join/syrusakbary) + diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..26bb1aff --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +/ @syrusakbary @ekampf @dan98765 @projectcheshire diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c1a00054 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.PHONY: help +help: + @echo "Please use \`make ' where 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: docs ## Generate docs +docs: + @cd docs &&\ + pip install -r requirements.txt &&\ + make html &&\ + cd - diff --git a/README.md b/README.md index 1ffb4aea..8b5a83e3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,77 @@ -Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade to Graphene `2.0`. +**We are looking for contributors**! Please check the [ROADMAP](https://github.com/graphql-python/graphene/blob/master/ROADMAP.md) to see how you can help ❀️ --- # ![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) +

Supporting Graphene Python

+ +Graphene is an MIT-licensed open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome [backers](https://github.com/graphql-python/graphene/blob/master/BACKERS.md). If you'd like to join them, please consider: + +- [Become a backer or sponsor on Patreon](https://www.patreon.com/syrusakbary). +- [One-time donation via PayPal.](https://graphene-python.org/support-graphene/) + + + +

Platinum via Patreon

+ + + + + + + + +
+ + + +
+ +

Gold via Patreon

+ + + + + + + + +
+ + + +
+ + +

Silver via Patreon

+ + + + + + + + +
+ + + +
+ + +--- + +## Introduction [Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily. @@ -13,17 +81,16 @@ Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade to Graph 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)) | +| 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). @@ -39,7 +106,6 @@ pip install "graphene>=2.0" 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: @@ -67,9 +133,13 @@ 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 @@ -84,13 +154,13 @@ 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 -py.test PATH/TO/MY/DIR/test_test.py # Single file -py.test PATH/TO/MY/DIR/ # All tests in directory +py.test graphene/relay/tests/test_node.py # Single file +py.test graphene/relay # 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. +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: @@ -99,28 +169,25 @@ 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: + ```sh tox ``` + If you wish to run against a specific version defined in the `tox.ini` file: + ```sh 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! -### Documentation +### Building 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: +An HTML version of the documentation is produced by running: ```sh -cd docs -pip install -r requirements.txt -``` - -Then to produce a HTML version of the documentation: - -```sh -make html +make docs ``` diff --git a/README.rst b/README.rst index bea6c4d4..5b3f3747 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,190 @@ -Please read `UPGRADE-v2.0.md `__ to learn how to -upgrade to Graphene ``2.0``. +**We are looking for contributors**! Please check the +`ROADMAP `__ +to see how you can help ❀️ -------------- |Graphene Logo| `Graphene `__ |Build Status| |PyPI version| |Coverage Status| ========================================================================================================= +.. raw:: html + +

+ +Supporting Graphene Python + +.. raw:: html + +

+ +Graphene is an MIT-licensed open source project. It's an independent +project with its ongoing development made possible entirely thanks to +the support by these awesome +`backers `__. +If you'd like to join them, please consider: + +- `Become a backer or sponsor on + Patreon `__. +- `One-time donation via + PayPal. `__ + + + +.. raw:: html + +

+ +Platinum via Patreon + +.. raw:: html + +

+ +.. raw:: html + + + +.. raw:: html + + + +.. raw:: html + + + +:: + + + + + +.. raw:: html + + + +.. raw:: html + +
+ + + +
+ +.. raw:: html + +

+ +Gold via Patreon + +.. raw:: html + +

+ +.. raw:: html + + + +.. raw:: html + + + +.. raw:: html + + + +:: + + + + + +.. raw:: html + + + +.. raw:: html + +
+ + + +
+ +.. raw:: html + + + +.. raw:: html + +

+ +Silver via Patreon + +.. raw:: html + +

+ +.. raw:: html + + + +.. raw:: html + + + +.. raw:: html + + + +:: + + + + + +.. raw:: html + + + +.. raw:: html + +
+ + + +
+ +.. raw:: html + + + +-------------- + +Introduction +------------ + `Graphene `__ is a Python library for building GraphQL schemas/types fast and easily. @@ -88,20 +267,38 @@ If you want to learn even more, you can also check the following - **Relay Schema**: `Starwars Relay example `__ +Documentation +------------- + +Documentation and links to additional resources are available at +https://docs.graphene-python.org/en/latest/ + Contributing ------------ -After cloning this repo, ensure dependencies are installed by running: +After cloning this repo, create a +`virtualenv `__ and ensure +dependencies are installed by running: .. code:: sh + virtualenv venv + source venv/bin/activate pip install -e ".[test]" -After developing, the full test suite can be evaluated by running: +Well-written tests and maintaining good test coverage is important to +this project. While developing, run new and existing tests with: .. code:: sh - py.test graphene --cov=graphene --benchmark-skip # Use -v -s for verbose mode + py.test graphene/relay/tests/test_node.py # Single file + py.test graphene/relay # 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 `__ for more +options and test running controls. You can also run the benchmarks with: @@ -109,24 +306,41 @@ You can also run the benchmarks with: py.test graphene --benchmark-only -Documentation +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: + +.. code:: sh + + tox + +If you wish to run against a specific version defined in the ``tox.ini`` +file: + +.. code:: sh + + 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! + +Building Documentation ~~~~~~~~~~~~~ The documentation is generated using the excellent `Sphinx `__ and a custom theme. -The documentation dependencies are installed by running: +An HTML version of the documentation is produced by running: .. code:: sh - cd docs - pip install -r requirements.txt - -Then to produce a HTML version of the documentation: - -.. code:: sh - - make html + make docs .. |Graphene Logo| image:: http://graphene-python.org/favicon.png .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..24f542e0 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,33 @@ +# Graphene Roadmap + +In order to move Graphene and the GraphQL Python ecosystem forward I realized is essential to be clear with the community on next steps, so we can move uniformly. + +There are few key points that need to happen in the short/mid term, divided into two main sections: + +- [Community](#community) +- [Graphene 3](#graphene-3) + +_πŸ‘‹ If you have more ideas on how to move the Graphene ecosystem forward, don't hesistate to [open a PR](https://github.com/graphql-python/graphene/edit/master/ROADMAP.md)_ + +## Community + +The goal is to improve adoption and sustainability of the project. + +- πŸ’Ž Add Commercial Support for Graphene - [See issue](https://github.com/graphql-python/graphene/issues/813) + - Create [Patreon page](https://www.patreon.com/syrusakbary) + - Add [/support-graphene page](https://graphene-python.org/support-graphene/) in Graphene website +- πŸ“˜ Vastly improve documentation - [See issue](https://github.com/graphql-python/graphene/issues/823) +- ~~πŸ’° Apply for [Mozilla MOSS](https://www.mozilla.org/en-US/moss/) sponsorship~~ (not for now) + +## Graphene 3 + +The goal is to summarize the different improvements that Graphene will need to accomplish for version 3. + +In a nushell, Graphene 3 should take the Python 3 integration one step forward while still maintaining compatibility with Python 2. + +- πŸš€ [graphql-core-next](https://github.com/graphql-python/graphql-core-next) GraphQL engine support (almost same API as graphql-core) +- πŸ”Έ GraphQL types from type annotations - [See issue](https://github.com/graphql-python/graphene/issues/729) +- πŸ“„ Schema creation from SDL (API TBD) +- ✨ Improve connections structure +- πŸ“— Improve function documentation +- πŸ”€ Add support for coroutines in Connection, Mutation (abstracting out Promise requirement) - [See PR](https://github.com/graphql-python/graphene/pull/824) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 32b28d8b..d9d48005 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -17,7 +17,7 @@ developer has to write to use them. **New Features!** * [`InputObjectType`](#inputobjecttype) -* [`Meta as Class arguments`](#meta-ass-class-arguments) (_only available for Python 3_) +* [`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 @@ -276,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 @@ -287,7 +287,7 @@ class MyGrapheneMiddleware(object): With 2.0: ```python -class MyGrapheneMiddleware(object): +class MyGrapheneMiddleware(object): def resolve(self, next_mw, root, info, **args): context = info.context diff --git a/docs/Makefile b/docs/Makefile index 2973acec..3eb7e638 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -17,75 +17,50 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" - @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" + @grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' .PHONY: clean clean: rm -rf $(BUILDDIR)/* -.PHONY: html +.PHONY: html ## to make standalone HTML files html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -.PHONY: dirhtml +.PHONY: dirhtml ## to make HTML files named index.html in directories dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." -.PHONY: singlehtml +.PHONY: singlehtml ## to make a single large HTML file singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." -.PHONY: pickle +.PHONY: pickle ## to make pickle files pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." -.PHONY: json +.PHONY: json ## to make JSON files json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." -.PHONY: htmlhelp +.PHONY: htmlhelp ## to make HTML files and a HTML help project 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 +.PHONY: qthelp ## to make HTML files and a qthelp project qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @@ -95,7 +70,7 @@ qthelp: @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Graphene.qhc" -.PHONY: applehelp +.PHONY: applehelp ## to make an Apple Help Book applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @@ -104,7 +79,7 @@ applehelp: "~/Library/Documentation/Help or install it in your application" \ "bundle." -.PHONY: devhelp +.PHONY: devhelp ## to make HTML files and a Devhelp project devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @@ -114,19 +89,19 @@ devhelp: @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Graphene" @echo "# devhelp" -.PHONY: epub +.PHONY: epub ## to make an epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." -.PHONY: epub3 +.PHONY: epub3 ## to make an epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." -.PHONY: latex +.PHONY: latex ## to make LaTeX files, you can set PAPER=a4 or PAPER=letter latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @@ -134,33 +109,33 @@ latex: @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." -.PHONY: latexpdf +.PHONY: latexpdf ## to make LaTeX files and run them through pdflatex 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 +.PHONY: latexpdfja ## to make LaTeX files and run them through platex/dvipdfmx 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 +.PHONY: text ## to make text files text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." -.PHONY: man +.PHONY: man ## to make manual pages man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." -.PHONY: texinfo +.PHONY: texinfo ## to make Texinfo files texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @@ -168,57 +143,57 @@ texinfo: @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." -.PHONY: info +.PHONY: info ## to make Texinfo files and run them through makeinfo 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 +.PHONY: gettext ## to make PO message catalogs gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." -.PHONY: changes +.PHONY: changes ## to make an overview of all changed/added/deprecated items changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." -.PHONY: linkcheck +.PHONY: linkcheck ## to check all external links for integrity 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 +.PHONY: doctest ## to run all doctests embedded in the documentation (if enabled) 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 +.PHONY: coverage ## to run coverage check of the documentation (if enabled) 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 +.PHONY: xml ## to make Docutils-native XML files xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." -.PHONY: pseudoxml +.PHONY: pseudoxml ## to make pseudoxml-XML files for display purposes pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." -.PHONY: dummy +.PHONY: dummy ## to check syntax errors of document sources dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 522af161..3cd36fcc 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -25,8 +25,8 @@ Create loaders by providing a batch loading function. return Promise.resolve([get_user(id=key) for key in keys]) -A batch loading function accepts an list of keys, and returns a ``Promise`` -which resolves to an list of ``values``. +A batch loading function accepts a list of keys, and returns a ``Promise`` +which resolves to a list of ``values``. Then load individual values from the loader. ``DataLoader`` will coalesce all individual loads which occur within a single frame of execution (executed once @@ -34,7 +34,6 @@ the wrapping promise is resolved) and then call your batch function with all requested keys. - .. code:: python user_loader = UserLoader() @@ -47,6 +46,19 @@ requested keys. 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): + def batch_load_fn(self, keys): + users = {user.id: user for user in User.objects.filter(id__in=keys)} + return Promise.resolve([users.get(user_id) for user_id in keys]) + + ``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 diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index 327ce230..21345aa3 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -16,7 +16,7 @@ For executing a query a schema, you can directly call the ``execute`` method on Context _______ -You can pass context to a query via ``context_value``. +You can pass context to a query via ``context``. .. code:: python @@ -28,14 +28,14 @@ You can pass context to a query via ``context_value``. return info.context.get('name') schema = graphene.Schema(Query) - result = schema.execute('{ name }', context_value={'name': 'Syrus'}) + result = schema.execute('{ name }', context={'name': 'Syrus'}) Variables _______ -You can pass variables to a query via ``variable_values``. +You can pass variables to a query via ``variables``. .. code:: python @@ -55,5 +55,5 @@ You can pass variables to a query via ``variable_values``. lastName } }''', - variable_values={'id': 12}, + variables={'id': 12}, ) diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 55efe730..f7dac708 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -29,7 +29,7 @@ This middleware only continues evaluation if the ``field_name`` is not ``'user'` .. code:: python class AuthorizationMiddleware(object): - def resolve(self, next, root, info, **args): + def resolve(next, root, info, **args): if info.field_name == 'user': return None return next(root, info, **args) diff --git a/docs/index.rst b/docs/index.rst index 3e9577ad..aff3960f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,3 +19,5 @@ Integrations * `Graphene-SQLAlchemy `_ (`source `_) * `Graphene-GAE `_ (`source `_) * `Graphene-Mongo `_ (`source `_) +* `Starlette `_ (`source `_) +* `FastAPI `_ (`source `_) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a2d39481..fc333fae 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -30,17 +30,18 @@ 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}"``. +one field: ``hello`` and an input name. And when we query it, it should return ``"Hello +{argument}"``. .. code:: python import graphene class Query(graphene.ObjectType): - hello = graphene.String(name=graphene.String(default_value="stranger")) + hello = graphene.String(argument=graphene.String(default_value="stranger")) - def resolve_hello(self, info, name): - return 'Hello ' + name + def resolve_hello(self, info, argument): + return 'Hello ' + argument schema = graphene.Schema(query=Query) @@ -54,4 +55,8 @@ Then we can start querying our schema: result = schema.execute('{ hello }') print(result.data['hello']) # "Hello stranger" + # or passing the argument in the query + result = schema.execute('{ hello (argument: "graph") }') + print(result.data['hello']) # "Hello graph" + Congrats! You got your first graphene schema working! diff --git a/docs/testing/index.rst b/docs/testing/index.rst index 0263a9aa..0103779c 100644 --- a/docs/testing/index.rst +++ b/docs/testing/index.rst @@ -54,7 +54,7 @@ Execute parameters ~~~~~~~~~~~~~~~~~~ You can also add extra keyword arguments to the ``execute`` method, such as -``context_value``, ``root_value``, ``variable_values``, ...: +``context``, ``root``, ``variables``, ...: .. code:: python @@ -63,7 +63,7 @@ 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_value={'user': 'Peter'}) + executed = client.execute('''{ hey }''', context={'user': 'Peter'}) assert executed == { 'data': { 'hey': 'hello Peter!' diff --git a/docs/types/enums.rst b/docs/types/enums.rst index 6e730628..4e019ee7 100644 --- a/docs/types/enums.rst +++ b/docs/types/enums.rst @@ -1,7 +1,7 @@ Enums ===== -A ``Enum`` is a special ``GraphQL`` type that represents a set of +An ``Enum`` is a special ``GraphQL`` type that represents a set of symbolic names (members) bound to unique, constant values. Definition diff --git a/docs/types/interfaces.rst b/docs/types/interfaces.rst index 21cf2173..f2073d6a 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -91,7 +91,7 @@ For example, you can define a field ``hero`` that resolves to any 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 implments the interface +as well as selecting specific fields on any type that implements the interface using `inline fragments `_. For example, the following query: diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index 6949008f..8532595e 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -27,7 +27,7 @@ This example defines a Mutation: **person** and **ok** are the output fields of the Mutation when it is resolved. -**Input** attributes are the arguments that the Mutation +**Arguments** attributes are the arguments that the Mutation ``CreatePerson`` needs for resolving, in this case **name** will be the only argument for the mutation. diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 69c8c08a..b6eb3087 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -25,8 +25,8 @@ This example model defines a Person, with a first and a last name: last_name = graphene.String() full_name = graphene.String() - def resolve_full_name(self, info): - return '{} {}'.format(self.first_name, self.last_name) + def resolve_full_name(root, info): + return '{} {}'.format(root.first_name, root.last_name) **first\_name** and **last\_name** are fields of the ObjectType. Each field is specified as a class attribute, and each attribute maps to a @@ -46,33 +46,158 @@ The above ``Person`` ObjectType has the following schema representation: Resolvers --------- -A resolver is a method that resolves certain fields within a -``ObjectType``. If not specififed otherwise, the resolver of a +A resolver is a method that resolves certain fields within an +``ObjectType``. If not specified otherwise, the resolver of a field is the ``resolve_{field_name}`` method on the ``ObjectType``. By default resolvers take the arguments ``info`` and ``*args``. -NOTE: The resolvers on a ``ObjectType`` are always treated as ``staticmethod``\ s, +NOTE: The resolvers on an ``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``. +If an explicit resolver is not defined on the ``ObjectType`` then Graphene will +attempt to use a property with the same name on the object that is passed to the +``ObjectType``. -Quick example -~~~~~~~~~~~~~ +.. code:: python -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. + import graphene + + class Person(graphene.ObjectType): + first_name = graphene.String() + last_name = graphene.String() + + class Query(graphene.ObjectType): + me = graphene.Field(Person) + + def resolve_me(_, info): + # returns an object that represents a Person + return get_human(name='Luke Skywalker') + +If you are passing a dict instead of an object to your ``ObjectType`` you can +change the default resolver in the ``Meta`` class like this: + +.. code:: python + + import graphene + from graphene.types.resolver import dict_resolver + + class Person(graphene.ObjectType): + class Meta: + default_resolver = dict_resolver + + first_name = graphene.String() + last_name = graphene.String() + + class Query(graphene.ObjectType): + me = graphene.Field(Person) + + def resolve_me(_, info): + return { + "first_name": "Luke", + "last_name": "Skywalker", + } + +Or you can change the default resolver globally by calling ``set_default_resolver`` +before executing a query. + +.. code:: python + + import graphene + from graphene.types.resolver import dict_resolver, set_default_resolver + + set_default_resolver(dict_resolver) + + schema = graphene.Schema(query=Query) + result = schema.execute(''' + query { + me { + firstName + } + } + ''') + + +Resolvers with arguments +~~~~~~~~~~~~~~~~~~~~~~~~ + +Any arguments that a field defines gets passed to the resolver function as +kwargs. For example: .. code:: python import graphene class Query(graphene.ObjectType): - reverse = graphene.String(word=graphene.String()) + human_by_name = graphene.Field(Human, name=graphene.String(required=True)) + + def resolve_human_by_name(_, info, name): + return get_human(name=name) + +You can then execute the following query: + +.. code:: + + query { + humanByName(name: "Luke Skywalker") { + firstName + lastName + } + } + +NOTE: 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 differenciate +between a ``undefined`` value for an argument and an explicit ``null`` value. + +For example, given this schema: + +.. code:: python + + import graphene + + class Query(graphene.ObjectType): + hello = graphene.String(required=True, name=graphene.String()) + + def resolve_hello(_, 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 2 ways. Either by combining all keyword arguments +into a dict: + +.. code:: python + + class Query(graphene.ObjectType): + hello = graphene.String(required=True, name=graphene.String()) + + def resolve_hello(_, info, **args): + return args.get('name', 'World') + +Or by setting a default value for the keyword argument: + +.. code:: python + + class Query(graphene.ObjectType): + hello = graphene.String(required=True, name=graphene.String()) + + def resolve_hello(_, info, name='World'): + return name - def resolve_reverse(self, info, word): - return word[::-1] Resolvers outside the class ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -83,11 +208,13 @@ A field can use a custom resolver from outside the class: import graphene - def reverse(root, info, word): - return word[::-1] + def resolve_full_name(person, info): + return '{} {}'.format(person.first_name, person.last_name) - class Query(graphene.ObjectType): - reverse = graphene.String(word=graphene.String(), resolver=reverse) + class Person(graphene.ObjectType): + first_name = graphene.String() + last_name = graphene.String() + full_name = graphene.String(resolver=resolve_full_name) Instances as data containers diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index b6e8f654..b4552cfb 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -39,9 +39,8 @@ Graphene defines the following base Scalar Types: ``graphene.Int`` Represents non-fractional signed whole numeric - 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 `_. + values. Int is a signed 32‐bit integer per the + `GraphQL spec `_ ``graphene.Float`` diff --git a/examples/context_example.py b/examples/context_example.py index 5fd7647d..9b5fd1a5 100644 --- a/examples/context_example.py +++ b/examples/context_example.py @@ -25,11 +25,11 @@ query = """ def test_query(): - result = schema.execute(query, context_value={"user": User(id="1", name="Syrus")}) + result = schema.execute(query, context={"user": User(id="1", name="Syrus")}) assert not result.errors assert result.data == {"me": {"id": "1", "name": "Syrus"}} if __name__ == "__main__": - result = schema.execute(query, context_value={"user": User(id="X", name="Console")}) + result = schema.execute(query, context={"user": User(id="X", name="Console")}) print(result.data["me"]) diff --git a/examples/starwars/tests/test_query.py b/examples/starwars/tests/test_query.py index b26374b6..88934b0e 100644 --- a/examples/starwars/tests/test_query.py +++ b/examples/starwars/tests/test_query.py @@ -72,7 +72,7 @@ def test_fetch_some_id_query(snapshot): } """ params = {"someId": "1000"} - snapshot.assert_match(client.execute(query, variable_values=params)) + snapshot.assert_match(client.execute(query, variables=params)) def test_fetch_some_id_query2(snapshot): @@ -84,7 +84,7 @@ def test_fetch_some_id_query2(snapshot): } """ params = {"someId": "1002"} - snapshot.assert_match(client.execute(query, variable_values=params)) + snapshot.assert_match(client.execute(query, variables=params)) def test_invalid_id_query(snapshot): @@ -96,7 +96,7 @@ def test_invalid_id_query(snapshot): } """ params = {"id": "not a valid id"} - snapshot.assert_match(client.execute(query, variable_values=params)) + snapshot.assert_match(client.execute(query, variables=params)) def test_fetch_luke_aliased(snapshot): diff --git a/graphene/pyutils/signature.py b/graphene/pyutils/signature.py index c66c2563..7757d9d0 100644 --- a/graphene/pyutils/signature.py +++ b/graphene/pyutils/signature.py @@ -707,7 +707,10 @@ class Signature(object): 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 = ( + "{arg!r} parameter is positional only, " + "but was passed as a keyword" + ) msg = msg.format(arg=param.name) raise TypeError(msg) parameters_ex = (param,) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index f23849f2..927dfcc4 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -3,15 +3,21 @@ from collections import Iterable, OrderedDict from functools import partial 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.field import Field from ..types.objecttype import ObjectType, ObjectTypeOptions +from ..utils.thenables import maybe_thenable 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", @@ -64,21 +70,40 @@ class Connection(ObjectType): node = Field(_node, description="The item at the end of the edge") cursor = String(required=True, description="A cursor for use in pagination") + class EdgeMeta: + description = "A Relay edge containing a `{}` and its cursor.".format( + base_name + ) + 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, {}) + edge = type(edge_name, edge_bases, {"Meta": EdgeMeta}) cls.Edge = edge options["name"] = name _meta.node = node _meta.fields = OrderedDict( [ - ("page_info", Field(PageInfo, name="pageInfo", required=True)), - ("edges", Field(NonNull(List(edge)))), + ( + "page_info", + Field( + PageInfo, + name="pageInfo", + required=True, + description="Pagination data for this connection.", + ), + ), + ( + "edges", + Field( + NonNull(List(edge)), + description="Contains the nodes in this connection.", + ), + ), ] ) return super(Connection, cls).__init_subclass_with_meta__( @@ -103,7 +128,7 @@ class IterableConnectionField(Field): if is_node(connection_type): raise Exception( - "ConnectionField's now need a explicit ConnectionType for Nodes.\n" + "ConnectionFields 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" ) @@ -139,10 +164,7 @@ class IterableConnectionField(Field): connection_type = connection_type.of_type on_resolve = partial(cls.resolve_connection, connection_type, args) - if is_thenable(resolved): - return Promise.resolve(resolved).then(on_resolve) - - return on_resolve(resolved) + return maybe_thenable(resolved, on_resolve) def get_resolver(self, parent_resolver): resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 1b8b855e..ee758e78 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -1,10 +1,9 @@ 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): @@ -69,7 +68,4 @@ class ClientIDMutation(Mutation): return payload result = cls.mutate_and_get_payload(root, info, **input) - if is_thenable(result): - return Promise.resolve(result).then(on_resolve) - - return on_resolve(result) + return maybe_thenable(result, on_resolve) diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 061d6ec9..ca43f397 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -138,7 +138,7 @@ def test_connectionfield_node_deprecated(): with pytest.raises(Exception) as exc_info: field.type - assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str( + assert "ConnectionFields now need a explicit ConnectionType for Nodes." in str( exc_info.value ) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 9c75bcee..bf304608 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -75,9 +75,7 @@ def to_arguments(args, extra_args=None): arg_name = default_name or arg.name assert ( arg_name not in arguments - ), 'More than one Argument have same name "{}".'.format( - arg_name - ) + ), 'More than one Argument have same name "{}".'.format(arg_name) arguments[arg_name] = arg return arguments diff --git a/graphene/types/base.py b/graphene/types/base.py index aa97ed22..75685d98 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -1,5 +1,9 @@ from ..utils.subclass_with_meta import SubclassWithMeta from ..utils.trim_docstring import trim_docstring +import six + +if six.PY3: + from typing import Type class BaseOptions(object): diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index aa8f86bb..dd09e461 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -85,7 +85,7 @@ class Mutation(ObjectType): args=cls._meta.arguments, resolver=cls._meta.resolver, name=name, - description=description, + description=description or cls._meta.description, deprecation_reason=deprecation_reason, required=required, ) diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index dfb63e52..c5f43787 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -4,6 +4,9 @@ from graphql.language.ast import BooleanValue, FloatValue, IntValue, StringValue from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType +if six.PY3: + from typing import Any + class ScalarOptions(BaseOptions): pass diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index 5165aa61..98e5e7ab 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -90,7 +90,7 @@ def test_datetime_query_variable(): result = schema.execute( """query Test($date: DateTime){ datetime(in: $date) }""", - variable_values={"date": isoformat}, + variables={"date": isoformat}, ) assert not result.errors assert result.data == {"datetime": isoformat} @@ -101,8 +101,7 @@ def test_date_query_variable(): isoformat = now.isoformat() result = schema.execute( - """query Test($date: Date){ date(in: $date) }""", - variable_values={"date": isoformat}, + """query Test($date: Date){ date(in: $date) }""", variables={"date": isoformat} ) assert not result.errors assert result.data == {"date": isoformat} @@ -114,8 +113,7 @@ def test_time_query_variable(): isoformat = time.isoformat() result = schema.execute( - """query Test($time: Time){ time(at: $time) }""", - variable_values={"time": isoformat}, + """query Test($time: Time){ time(at: $time) }""", variables={"time": isoformat} ) assert not result.errors assert result.data == {"time": isoformat} diff --git a/graphene/types/tests/test_decimal.py b/graphene/types/tests/test_decimal.py index abc4a6c4..fd77f482 100644 --- a/graphene/types/tests/test_decimal.py +++ b/graphene/types/tests/test_decimal.py @@ -28,7 +28,7 @@ def test_decimal_string_query_variable(): result = schema.execute( """query Test($decimal: Decimal){ decimal(input: $decimal) }""", - variable_values={"decimal": decimal_value}, + variables={"decimal": decimal_value}, ) assert not result.errors assert result.data == {"decimal": str(decimal_value)} diff --git a/graphene/types/tests/test_definition.py b/graphene/types/tests/test_definition.py index 347de9c9..549847d5 100644 --- a/graphene/types/tests/test_definition.py +++ b/graphene/types/tests/test_definition.py @@ -1,5 +1,3 @@ - - from ..argument import Argument from ..enum import Enum from ..field import Field diff --git a/graphene/types/tests/test_generic.py b/graphene/types/tests/test_generic.py index 83e9bc88..338da982 100644 --- a/graphene/types/tests/test_generic.py +++ b/graphene/types/tests/test_generic.py @@ -39,7 +39,7 @@ def test_generic_query_variable(): ]: result = schema.execute( """query Test($generic: GenericScalar){ generic(input: $generic) }""", - variable_values={"generic": generic_value}, + variables={"generic": generic_value}, ) assert not result.errors assert result.data == {"generic": generic_value} diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index d565ff40..dc557b94 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -1,4 +1,3 @@ - from ..argument import Argument from ..field import Field from ..inputfield import InputField diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index c6b93586..b5537180 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -1,4 +1,3 @@ - from ..json import JSONString from ..objecttype import ObjectType from ..schema import Schema @@ -28,7 +27,7 @@ def test_jsonstring_query_variable(): result = schema.execute( """query Test($json: JSONString){ json(input: $json) }""", - variable_values={"json": json_value}, + variables={"json": json_value}, ) assert not result.errors assert result.data == {"json": json_value} diff --git a/graphene/types/tests/test_mountedtype.py b/graphene/types/tests/test_mountedtype.py index 787bee56..b964233e 100644 --- a/graphene/types/tests/test_mountedtype.py +++ b/graphene/types/tests/test_mountedtype.py @@ -1,4 +1,3 @@ - from ..field import Field from ..scalars import String diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 9755ad23..8558dc7a 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -139,17 +139,41 @@ def test_mutation_allow_to_have_custom_args(): class MyMutation(ObjectType): create_user = CreateUser.Field( + name="createUser", description="Create a user", deprecation_reason="Is deprecated", required=True, ) field = MyMutation._meta.fields["create_user"] + assert field.name == "createUser" assert field.description == "Create a user" assert field.deprecation_reason == "Is deprecated" assert field.type == NonNull(CreateUser) +def test_mutation_default_args_output(): + class CreateUser(Mutation): + """Description.""" + + class Arguments: + name = String() + + name = String() + + def mutate(self, info, name): + return CreateUser(name=name) + + class MyMutation(ObjectType): + create_user = CreateUser.Field() + + field = MyMutation._meta.fields["create_user"] + assert field.name is None + assert field.description == "Description." + assert field.deprecation_reason is None + assert field.type == CreateUser + + def test_mutation_as_subclass(): class BaseCreateUser(Mutation): class Arguments: diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 9f693a52..8681e462 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -464,7 +464,7 @@ def test_query_annotated_resolvers(): assert not result.errors assert result.data == {"annotated": "base-self"} - result = test_schema.execute("{ context }", "base", context_value=context) + result = test_schema.execute("{ context }", "base", context=context) assert not result.errors assert result.data == {"context": "base-context"} diff --git a/graphene/types/tests/test_resolver.py b/graphene/types/tests/test_resolver.py index 3cc44539..a03cf187 100644 --- a/graphene/types/tests/test_resolver.py +++ b/graphene/types/tests/test_resolver.py @@ -1,4 +1,3 @@ - from ..resolver import ( attr_resolver, dict_resolver, diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py index 1ec986cd..559c0ce6 100644 --- a/graphene/types/tests/test_scalar.py +++ b/graphene/types/tests/test_scalar.py @@ -1,4 +1,3 @@ - from ..scalars import Scalar diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py index 9b3f93a0..2280b41f 100644 --- a/graphene/types/tests/test_uuid.py +++ b/graphene/types/tests/test_uuid.py @@ -25,7 +25,7 @@ def test_uuidstring_query_variable(): result = schema.execute( """query Test($uuid: UUID){ uuid(input: $uuid) }""", - variable_values={"uuid": uuid_value}, + variables={"uuid": uuid_value}, ) assert not result.errors assert result.data == {"uuid": uuid_value} diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index dce04445..9edb8518 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -255,6 +255,7 @@ class TypeMap(GraphQLTypeMap): return GrapheneUnionType( graphene_type=type, name=type._meta.name, + description=type._meta.description, types=types, resolve_type=_resolve_type, ) diff --git a/graphene/utils/str_converters.py b/graphene/utils/str_converters.py index d8804038..216b0547 100644 --- a/graphene/utils/str_converters.py +++ b/graphene/utils/str_converters.py @@ -18,4 +18,4 @@ def to_snake_case(name): def to_const(string): - return re.sub("[\W|^]+", "_", string).upper() + return re.sub(r"[\W|^]+", "_", string).upper() # noqa diff --git a/graphene/utils/thenables.py b/graphene/utils/thenables.py new file mode 100644 index 00000000..a3089595 --- /dev/null +++ b/graphene/utils/thenables.py @@ -0,0 +1,42 @@ +""" +This file is used mainly as a bridge for thenable abstractions. +This includes: +- Promises +- Asyncio Coroutines +""" + +try: + from promise import Promise, is_thenable # type: ignore +except ImportError: + + class Promise(object): # type: ignore + pass + + def is_thenable(obj): # type: ignore + return False + + +try: + from inspect import isawaitable + from .thenables_asyncio import await_and_execute +except ImportError: + + def isawaitable(obj): # type: ignore + return False + + +def maybe_thenable(obj, on_resolve): + """ + Execute a on_resolve function once the thenable is resolved, + returning the same type of object inputed. + If the object is not thenable, it should return on_resolve(obj) + """ + if isawaitable(obj) and not isinstance(obj, Promise): + return await_and_execute(obj, on_resolve) + + if is_thenable(obj): + return Promise.resolve(obj).then(on_resolve) + + # If it's not awaitable not a Promise, return + # the function executed over the object + return on_resolve(obj) diff --git a/graphene/utils/thenables_asyncio.py b/graphene/utils/thenables_asyncio.py new file mode 100644 index 00000000..d5f93182 --- /dev/null +++ b/graphene/utils/thenables_asyncio.py @@ -0,0 +1,5 @@ +def await_and_execute(obj, on_resolve): + async def build_resolve_async(): + return on_resolve(await obj) + + return build_resolve_async() diff --git a/setup.py b/setup.py index be2111f8..48e5be35 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ import ast +import codecs import re import sys @@ -50,6 +51,7 @@ tests_require = [ "pytest-mock", "snapshottest", "coveralls", + "promise", "six", "mock", "pytz", @@ -60,7 +62,9 @@ setup( name="graphene", version=version, description="GraphQL Framework for Python", - long_description=open("README.rst").read(), + long_description=codecs.open( + "README.rst", "r", encoding="ascii", errors="replace" + ).read(), url="https://github.com/graphql-python/graphene", author="Syrus Akbary", author_email="me@syrusakbary.com", @@ -84,7 +88,6 @@ setup( "six>=1.10.0,<2", "graphql-core>=2.1,<3", "graphql-relay>=0.4.5,<1", - "promise>=2.1,<3", "aniso8601>=3,<4", ], tests_require=tests_require, diff --git a/tests_asyncio/test_relay_connection.py b/tests_asyncio/test_relay_connection.py new file mode 100644 index 00000000..ec86fef6 --- /dev/null +++ b/tests_asyncio/test_relay_connection.py @@ -0,0 +1,128 @@ +import pytest + +from collections import OrderedDict +from graphql.execution.executors.asyncio import AsyncioExecutor + +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) + promise_letters = ConnectionField(LetterConnection) + + node = Node.Field() + + def resolve_letters(self, info, **args): + return list(letters.values()) + + async def resolve_promise_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 = 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" % l.id), "letter": l.letter}, + "cursor": base64("arrayconnection:%s" % l.id), + } + for l 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 + ) + + +@pytest.mark.asyncio +async def test_connection_promise(): + result = await schema.execute( + """ + { + promiseLetters(first:1) { + edges { + node { + id + letter + } + } + pageInfo { + hasPreviousPage + hasNextPage + } + } + } + """, + executor=AsyncioExecutor(), + return_promise=True, + ) + + assert not result.errors + assert result.data == { + "promiseLetters": { + "edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}], + "pageInfo": {"hasPreviousPage": False, "hasNextPage": True}, + } + } diff --git a/tests_asyncio/test_relay_mutation.py b/tests_asyncio/test_relay_mutation.py new file mode 100644 index 00000000..42ea5fc7 --- /dev/null +++ b/tests_asyncio/test_relay_mutation.py @@ -0,0 +1,91 @@ +import pytest +from graphql.execution.executors.asyncio import AsyncioExecutor + +from graphene.types import ID, Field, ObjectType, Schema +from graphene.types.scalars import String +from graphene.relay.mutation import ClientIDMutation + + +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) + + +@pytest.mark.asyncio +async def test_node_query_promise(): + executed = await schema.execute( + 'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }', + executor=AsyncioExecutor(), + return_promise=True, + ) + assert not executed.errors + assert executed.data == {"sayPromise": {"phrase": "hello"}} + + +@pytest.mark.asyncio +async def test_edge_query(): + executed = await schema.execute( + 'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }', + executor=AsyncioExecutor(), + return_promise=True, + ) + assert not executed.errors + assert dict(executed.data) == { + "other": { + "clientMutationId": "1", + "myNodeEdge": {"cursor": "1", "node": {"name": "name"}}, + } + } diff --git a/tox.ini b/tox.ini index f8e6f347..2b7ae59c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,16 @@ [tox] -envlist = flake8,py27,py33,py34,py35,py36,pre-commit,pypy,mypy +envlist = flake8,py27,py34,py35,py36,py37,pre-commit,pypy,mypy skipsdist = true [testenv] -deps = .[test] +deps = + .[test] + py{35,36,37}: pytest-asyncio setenv = PYTHONPATH = .:{envdir} -commands= - py.test --cov=graphene graphene examples +commands = + py{27,34,py}: py.test --cov=graphene graphene examples {posargs} + py{35,36,37}: py.test --cov=graphene graphene examples tests_asyncio {posargs} [testenv:pre-commit] basepython=python3.6