From abc2c2a78418364c0f0f5c7c154cbae1ad34997b Mon Sep 17 00:00:00 2001 From: TheMelter Date: Thu, 19 Dec 2019 23:02:45 -0800 Subject: [PATCH 01/21] Fix typo in execute.rst (#1115) --- docs/execution/execute.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index 74300a82..f0ea8853 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -4,7 +4,7 @@ Executing a query ================= -For executing a query a schema, you can directly call the ``execute`` method on it. +For executing a query against a schema, you can directly call the ``execute`` method on it. .. code:: python From e31b93d1fdda810d70d3050c73c6638b29219d12 Mon Sep 17 00:00:00 2001 From: Yu Mochizuki Date: Thu, 26 Dec 2019 20:27:55 +0900 Subject: [PATCH 02/21] Increase the allowed version of aniso8601 (#1072) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3e22b7c4..d50aeba1 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ setup( install_requires=[ "graphql-core>=3.0.0a0,<4", "graphql-relay>=3.0.0a0,<4", - "aniso8601>=6,<8", + "aniso8601>=6,<9", ], tests_require=tests_require, extras_require={"test": tests_require}, From c0fbcba97a459e4a28e72d17755f6a1a21cbd74a Mon Sep 17 00:00:00 2001 From: Iman Date: Thu, 26 Dec 2019 23:32:28 +0330 Subject: [PATCH 03/21] Update quickstart.rst (#1090) A miss letter --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4ff0dfa2..2f0d54f9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -30,7 +30,7 @@ Compare Graphene's *code-first* approach to building a GraphQL API with *schema- .. _Ariadne: https://ariadne.readthedocs.io -Graphene is fully featured with integrations for the most popular web frameworks and ORMs. Graphene produces schemas tha are fully compliant with the GraphQL spec and provides tools and patterns for building a Relay-Compliant API as well. +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 ---------------------- From 482c7fcc65e98ba5f96ea5de3546ea95e7b1cdc7 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 26 Dec 2019 20:02:57 +0000 Subject: [PATCH 04/21] Add file uploading docs (#1084) --- docs/execution/fileuploading.rst | 8 ++++++++ docs/execution/index.rst | 1 + 2 files changed, 9 insertions(+) create mode 100644 docs/execution/fileuploading.rst diff --git a/docs/execution/fileuploading.rst b/docs/execution/fileuploading.rst new file mode 100644 index 00000000..d92174c0 --- /dev/null +++ b/docs/execution/fileuploading.rst @@ -0,0 +1,8 @@ +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 libary: `graphene-file-upload `_ which enhances Graphene to add file +uploads and conforms to the unoffical GraphQL `multipart request spec `_. diff --git a/docs/execution/index.rst b/docs/execution/index.rst index 00d98ffb..93a02845 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -8,3 +8,4 @@ Execution execute middleware dataloader + fileuploading From 81d61f82c5f0d60ee6aa4135a7f83f9c38ebf186 Mon Sep 17 00:00:00 2001 From: Tom Paoletti Date: Thu, 26 Dec 2019 12:05:14 -0800 Subject: [PATCH 05/21] Fix objecttypes DefaultResolver example (#1087) (#1088) * Create namedtuple as expected * Access result.data instead of result['data'] * Refer to field with camel-case name --- docs/types/objecttypes.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index f56cad9b..7919941a 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -212,7 +212,7 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a from graphene import ObjectType, String, Field, Schema - PersonValueObject = namedtuple('Person', 'first_name', 'last_name') + PersonValueObject = namedtuple('Person', ['first_name', 'last_name']) class Person(ObjectType): first_name = String() @@ -238,10 +238,10 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a } ''') # With default resolvers we can resolve attributes from an object.. - assert result['data']['me'] == {"firstName": "Luke", "lastName": "Skywalker"} + assert result.data['me'] == {"firstName": "Luke", "lastName": "Skywalker"} # With default resolvers, we can also resolve keys from a dictionary.. - assert result['data']['my_best_friend'] == {"firstName": "R2", "lastName": "D2"} + assert result.data['myBestFriend'] == {"firstName": "R2", "lastName": "D2"} Advanced ~~~~~~~~ @@ -280,7 +280,7 @@ An error will be thrown: TypeError: resolve_hello() missing 1 required positional argument: 'name' -You can fix this error in serveral ways. Either by combining all keyword arguments +You can fix this error in several ways. Either by combining all keyword arguments into a dict: .. code:: python From bd6d8d086dc350ab23f2ba56aee16de12bad53cf Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Tue, 31 Dec 2019 14:08:30 +0000 Subject: [PATCH 06/21] Fix tests (#1119) * Fix tests * Add extra folders to make test command * Update snapshots * Add python 3.8 to test matrix * Add black command to makefile and black dependency to setup.py * Add lint command * Run format * Remove 3.8 from test matrix * Add Python 3.8 to test matrix * Update setup.py --- .travis.yml | 1 + Makefile | 12 +++++++++-- .../snap_test_objectidentification.py | 8 ++++--- graphene/relay/tests/test_node.py | 21 +++++++++++-------- graphene/relay/tests/test_node_custom.py | 6 ++++-- setup.py | 9 +++++--- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5d15f2d..e1e55119 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ dist: xenial python: - "3.6" - "3.7" + - "3.8" install: - pip install tox tox-travis diff --git a/Makefile b/Makefile index b4e6c928..df3b4118 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ help: .PHONY: install-dev ## Install development dependencies install-dev: - pip install -e ".[test]" + pip install -e ".[dev]" test: - py.test graphene + py.test graphene examples tests_asyncio .PHONY: docs ## Generate docs docs: install-dev @@ -17,3 +17,11 @@ docs: install-dev .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 tests_asyncio + +.PHONY: lint +lint: + flake8 graphene examples setup.py tests_asyncio diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index cb57709a..02e61c39 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from snapshottest import Snapshot + snapshots = Snapshot() snapshots["test_correctly_fetches_id_name_rebels 1"] = { @@ -84,9 +85,10 @@ type PageInfo { type Query { rebels: Faction empire: Faction - - """The ID of the object""" - node(id: ID!): Node + node( + """The ID of the object""" + id: ID! + ): Node } """A ship in the Star Wars saga""" diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index c322b1a3..62fd31a3 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -183,15 +183,18 @@ def test_str_schema(): type RootQuery { first: String - - """The ID of the object""" - node(id: ID!): Node - - """The ID of the object""" - onlyNode(id: ID!): MyNode - - """The ID of the object""" - onlyNodeLazy(id: ID!): MyNode + 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 } ''' ) diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index 773be48f..6f28eb66 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -78,8 +78,10 @@ def test_str_schema_correct(): } type RootQuery { - """The ID of the object""" - node(id: ID!): Node + node( + """The ID of the object""" + id: ID! + ): Node } type User implements Node { diff --git a/setup.py b/setup.py index d50aeba1..58ec7345 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,8 @@ tests_require = [ "iso8601", ] +dev_requires = ["black==19.3b0", "flake8==3.7.7"] + tests_require + setup( name="graphene", version=version, @@ -76,15 +78,16 @@ setup( "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "tests.*", "examples"]), install_requires=[ - "graphql-core>=3.0.0a0,<4", - "graphql-relay>=3.0.0a0,<4", + "graphql-core>=3.0.0,<4", + "graphql-relay>=3.0.0,<4", "aniso8601>=6,<9", ], tests_require=tests_require, - extras_require={"test": tests_require}, + extras_require={"test": tests_require, "dev": dev_requires}, cmdclass={"test": PyTest}, ) From f82b8113776123afbec68ee9018891cb856a470e Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 30 Jan 2020 12:18:00 +0000 Subject: [PATCH 07/21] Fix example code (#1120) --- docs/types/objecttypes.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 7919941a..77ab130b 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -101,7 +101,7 @@ When we execute a query against that schema. query_string = "{ me { fullName } }" result = schema.execute(query_string) - assert result["data"]["me"] == {"fullName": "Luke Skywalker") + assert result.data["me"] == {"fullName": "Luke Skywalker") Then we go through the following steps to resolve this query: @@ -212,7 +212,7 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a from graphene import ObjectType, String, Field, Schema - PersonValueObject = namedtuple('Person', ['first_name', 'last_name']) + PersonValueObject = namedtuple("Person", ["first_name", "last_name"]) class Person(ObjectType): first_name = String() @@ -224,7 +224,7 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a def resolve_me(parent, info): # always pass an object for `me` field - return PersonValueObject(first_name='Luke', last_name='Skywalker') + return PersonValueObject(first_name="Luke", last_name="Skywalker") def resolve_my_best_friend(parent, info): # always pass a dictionary for `my_best_fiend_field` @@ -238,10 +238,10 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a } ''') # With default resolvers we can resolve attributes from an object.. - assert result.data['me'] == {"firstName": "Luke", "lastName": "Skywalker"} + 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"} + assert result.data["myBestFriend"] == {"firstName": "R2", "lastName": "D2"} Advanced ~~~~~~~~ From 55a03ba716ca6be431a42c817d9eca2154f6661c Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 30 Jan 2020 16:17:12 +0000 Subject: [PATCH 08/21] Update readme (#1130) * Add slack link and dev notice to the README * Fix formatting * Update formatting * Add notice to documentation --- README.md | 15 ++++++++------- docs/index.rst | 6 ++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9e84f819..e7bc5a60 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ +# ![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) + +[💬 Join the community on Slack](https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM) + **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) +**The below readme is the documentation for the `dev` (prerelease) version of Graphene. To view the documentation for the latest stable Graphene version go to the [v2 docs](https://docs.graphene-python.org/en/stable/)** + +--- ## Introduction -[Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily. +[Graphene](http://graphene-python.org) is an opinionated Python library for building GraphQL schemas/types fast and easily. - **Easy to use:** Graphene helps you use GraphQL in Python without effort. - **Relay:** Graphene has builtin support for Relay. @@ -23,7 +29,6 @@ Graphene has multiple integrations with different frameworks: | 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). @@ -35,10 +40,6 @@ For instaling graphene, just run this command in your shell 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: diff --git a/docs/index.rst b/docs/index.rst index 8db02a6e..dfaab1d5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,12 @@ Graphene ======== +------------ + +The documentation below is for the ``dev`` (prerelease) version of Graphene. To view the documentation for the latest stable Graphene version go to the `v2 docs `_. + +------------ + Contents: .. toctree:: From 9a19447213c53f7ba19f9dad5206a462ab31fcf0 Mon Sep 17 00:00:00 2001 From: Henry Baldursson Date: Sat, 8 Feb 2020 17:21:25 +0000 Subject: [PATCH 09/21] Use unidecode to handle unicode characters in constant names (#1080) --- graphene/utils/str_converters.py | 3 ++- graphene/utils/tests/test_str_converters.py | 4 ++++ setup.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/graphene/utils/str_converters.py b/graphene/utils/str_converters.py index 216b0547..9ac8461f 100644 --- a/graphene/utils/str_converters.py +++ b/graphene/utils/str_converters.py @@ -1,4 +1,5 @@ import re +from unidecode import unidecode # Adapted from this response in Stackoverflow @@ -18,4 +19,4 @@ def to_snake_case(name): def to_const(string): - return re.sub(r"[\W|^]+", "_", string).upper() # noqa + return re.sub(r"[\W|^]+", "_", unidecode(string)).upper() diff --git a/graphene/utils/tests/test_str_converters.py b/graphene/utils/tests/test_str_converters.py index 786149d9..d765906c 100644 --- a/graphene/utils/tests/test_str_converters.py +++ b/graphene/utils/tests/test_str_converters.py @@ -21,3 +21,7 @@ def test_camel_case(): def test_to_const(): assert to_const('snakes $1. on a "#plane') == "SNAKES_1_ON_A_PLANE" + + +def test_to_const_unicode(): + assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" diff --git a/setup.py b/setup.py index 58ec7345..d7077f0c 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ setup( "graphql-core>=3.0.0,<4", "graphql-relay>=3.0.0,<4", "aniso8601>=6,<9", + "unidecode>=1.1.1,<2", ], tests_require=tests_require, extras_require={"test": tests_require, "dev": dev_requires}, From ad0b3a529cbe006284dfdb1c01d1b68b60c3cd18 Mon Sep 17 00:00:00 2001 From: Jean-Louis Fuchs Date: Sat, 8 Feb 2020 21:24:58 +0100 Subject: [PATCH 10/21] The default_value of InputField should be INVALID (#1111) * The default_value of InputField should be INVALID Since GraphQL 3.0 there is a distinction between None and INVALID (no value). The tests captured the bug and are updated. * Update minimum graphql-core version * Use Undefined instead of INVALID Co-authored-by: Jonathan Kim --- .../tests/snapshots/snap_test_objectidentification.py | 2 +- graphene/relay/tests/test_mutation.py | 4 ++-- graphene/types/inputfield.py | 3 ++- graphene/types/tests/test_query.py | 9 +++------ setup.py | 2 +- tests_asyncio/test_relay_mutation.py | 4 ++-- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index 02e61c39..e42260f8 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -46,7 +46,7 @@ type Faction implements Node { input IntroduceShipInput { shipName: String! factionId: String! - clientMutationId: String = null + clientMutationId: String } type IntroduceShipPayload { diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 5fb1c468..e079ab4e 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -80,11 +80,11 @@ class OtherMutation(ClientIDMutation): @staticmethod def mutate_and_get_payload( - self, info, shared, additional_field, client_mutation_id=None + self, info, shared="", additional_field="", client_mutation_id=None ): edge_type = MyEdge return OtherMutation( - name=(shared or "") + (additional_field or ""), + name=shared + additional_field, my_node_edge=edge_type(cursor="1", node=MyNode(name="name")), ) diff --git a/graphene/types/inputfield.py b/graphene/types/inputfield.py index b0e0915a..bf3538e3 100644 --- a/graphene/types/inputfield.py +++ b/graphene/types/inputfield.py @@ -1,3 +1,4 @@ +from graphql import Undefined from .mountedtype import MountedType from .structures import NonNull from .utils import get_type @@ -48,7 +49,7 @@ class InputField(MountedType): self, type, name=None, - default_value=None, + default_value=Undefined, deprecation_reason=None, description=None, required=False, diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 004d53c8..fe9f39fc 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -262,17 +262,14 @@ def test_query_input_field(): result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', "Source!") assert not result.errors - assert result.data == { - "test": '["Source!",{"a_input":{"a_field":"String!","recursive_field":null}}]' - } + assert result.data == {"test": '["Source!",{"a_input":{"a_field":"String!"}}]'} result = test_schema.execute( '{ test(aInput: {recursiveField: {aField: "String!"}}) }', "Source!" ) assert not result.errors assert result.data == { - "test": '["Source!",{"a_input":{"a_field":null,"recursive_field":' - '{"a_field":"String!","recursive_field":null}}}]' + "test": '["Source!",{"a_input":{"recursive_field":{"a_field":"String!"}}}]' } @@ -408,7 +405,7 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark( - benchmark + benchmark, ): class Container(ObjectType): x = Int() diff --git a/setup.py b/setup.py index d7077f0c..977eba5d 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "tests.*", "examples"]), install_requires=[ - "graphql-core>=3.0.0,<4", + "graphql-core>=3.0.3,<4", "graphql-relay>=3.0.0,<4", "aniso8601>=6,<9", "unidecode>=1.1.1,<2", diff --git a/tests_asyncio/test_relay_mutation.py b/tests_asyncio/test_relay_mutation.py index 7b083dbf..4308a614 100644 --- a/tests_asyncio/test_relay_mutation.py +++ b/tests_asyncio/test_relay_mutation.py @@ -42,11 +42,11 @@ class OtherMutation(ClientIDMutation): @staticmethod def mutate_and_get_payload( - self, info, shared, additional_field, client_mutation_id=None + self, info, shared="", additional_field="", client_mutation_id=None ): edge_type = MyEdge return OtherMutation( - name=(shared or "") + (additional_field or ""), + name=shared + additional_field, my_node_edge=edge_type(cursor="1", node=MyNode(name="name")), ) From 23bb52a770e8b696770cdd29b76dbd23c4a5e749 Mon Sep 17 00:00:00 2001 From: James <33908344+allen-munsch@users.noreply.github.com> Date: Mon, 10 Feb 2020 16:16:11 -0600 Subject: [PATCH 11/21] Add a helpful message to when a global_id fails to parse. (#1074) * Add a helpful message to when a global_id fails to parse. * Update test_node to have errors on test_node_query_incorrect_id * Black the node.py file * Remove func wrapper used in debugging get_resolver partial * Update node.py * Expand error messages Co-authored-by: Jonathan Kim --- graphene/relay/node.py | 27 +++++++++++++++++++++++---- graphene/relay/tests/test_node.py | 18 +++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 1a5c5bdb..f8927ab7 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -90,9 +90,24 @@ class Node(AbstractNode): def get_node_from_global_id(cls, info, global_id, only_type=None): try: _type, _id = cls.from_global_id(global_id) - graphene_type = info.schema.get_type(_type).graphene_type - except Exception: - return None + except Exception as e: + raise Exception( + ( + 'Unable to parse global ID "{global_id}". ' + 'Make sure it is a base64 encoded string in the format: "TypeName:id". ' + "Exception message: {exception}".format( + global_id=global_id, exception=str(e) + ) + ) + ) + + graphene_type = info.schema.get_type(_type) + if graphene_type is None: + raise Exception( + 'Relay Node "{_type}" not found in schema'.format(_type=_type) + ) + + graphene_type = graphene_type.graphene_type if only_type: assert graphene_type == only_type, ("Must receive a {} id.").format( @@ -101,7 +116,11 @@ class Node(AbstractNode): # We make sure the ObjectType implements the "Node" interface if cls not in graphene_type._meta.interfaces: - return None + raise Exception( + 'ObjectType "{_type}" does not implement the "{cls}" interface.'.format( + _type=_type, cls=cls + ) + ) get_node = getattr(graphene_type, "get_node", None) if get_node: diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index 62fd31a3..de1802e9 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -1,3 +1,4 @@ +import re from graphql_relay import to_global_id from graphql.pyutils import dedent @@ -83,6 +84,20 @@ 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} @@ -90,7 +105,8 @@ def test_node_query_incorrect_id(): executed = schema.execute( '{ node(id:"%s") { ... on MyNode { name } } }' % "something:2" ) - assert not executed.errors + assert executed.errors + assert re.match(r"Unable to parse global ID .*", executed.errors[0].message) assert executed.data == {"node": None} From 03bd6984dd19750c8b472b2f15b6ba99feaaab9b Mon Sep 17 00:00:00 2001 From: David Sanders Date: Mon, 10 Feb 2020 14:17:16 -0800 Subject: [PATCH 12/21] fix example middleware class in docs (#1134) --- docs/execution/middleware.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index ad109e44..2a5e20f7 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(next, root, info, **args): + def resolve(self, next, root, info, **args): if info.field_name == 'user': return None return next(root, info, **args) From be97a369f7a08444540fba36d9d01ac4ecbdfc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9C=A0=EC=84=9D?= Date: Tue, 18 Feb 2020 17:53:48 +0900 Subject: [PATCH 13/21] fix typo in class 'Interface' (#1135) --- graphene/types/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/interface.py b/graphene/types/interface.py index def0d040..77086dab 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -68,4 +68,4 @@ class Interface(BaseType): return type(instance) def __init__(self, *args, **kwargs): - raise Exception("An Interface cannot be intitialized") + raise Exception("An Interface cannot be initialized") From ba5b7dd3d7b94c27359ca5d85c4320eff4ce6012 Mon Sep 17 00:00:00 2001 From: Lem Ko Date: Fri, 21 Feb 2020 19:15:51 +0800 Subject: [PATCH 14/21] Fix example query in quickstart doc (#1139) --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 2f0d54f9..d2ac83be 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -127,7 +127,7 @@ Then we can start querying our **Schema** by passing a GraphQL query string to ` query_string = '{ hello }' result = schema.execute(query_string) print(result.data['hello']) - # "Hello stranger" + # "Hello stranger!" # or passing the argument in the query query_with_argument = '{ hello(name: "GraphQL") }' From ac98be78363b98def729e129484a06c26324dccd Mon Sep 17 00:00:00 2001 From: Jayden Windle Date: Wed, 26 Feb 2020 14:18:13 -0600 Subject: [PATCH 15/21] Use Undefined instead of the now deprecated INVALID (#1143) --- graphene/types/datetime.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index c533d23e..25a1248e 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import datetime from aniso8601 import parse_date, parse_datetime, parse_time -from graphql.error import INVALID +from graphql import Undefined from graphql.language import StringValueNode from .scalars import Scalar @@ -38,7 +38,7 @@ class Date(Scalar): elif isinstance(value, str): return parse_date(value) except ValueError: - return INVALID + return Undefined class DateTime(Scalar): @@ -68,7 +68,7 @@ class DateTime(Scalar): elif isinstance(value, str): return parse_datetime(value) except ValueError: - return INVALID + return Undefined class Time(Scalar): @@ -98,4 +98,4 @@ class Time(Scalar): elif isinstance(value, str): return parse_time(value) except ValueError: - return INVALID + return Undefined From 98e10f0db834d2898ed9652f3e052feaea4c2de1 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 27 Feb 2020 20:51:59 +0000 Subject: [PATCH 16/21] Replace INVALID with Undefined (#1146) --- graphene/types/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 3249c6f6..79b5315b 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -22,7 +22,7 @@ from graphql import ( GraphQLObjectType, GraphQLSchema, GraphQLString, - INVALID, + Undefined, ) from ..utils.str_converters import to_camel_case @@ -357,7 +357,7 @@ class GrapheneGraphQLSchema(GraphQLSchema): arg_type, out_name=arg_name, description=arg.description, - default_value=INVALID + default_value=Undefined if isinstance(arg.type, NonNull) else arg.default_value, ) From 796880fc5cc1d79976f95390af00b0798201c9c3 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Wed, 4 Mar 2020 11:24:42 +0100 Subject: [PATCH 17/21] Update dependencies --- setup.py | 31 +++++++++++++++---------------- tox.ini | 10 +++++----- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index 977eba5d..084c7707 100644 --- a/setup.py +++ b/setup.py @@ -45,21 +45,20 @@ class PyTest(TestCommand): tests_require = [ - "pytest", - "pytest-benchmark", - "pytest-cov", - "pytest-mock", - "pytest-asyncio", - "snapshottest", - "coveralls", - "promise", - "six", - "mock", - "pytz", - "iso8601", + "pytest>=5.3,<6", + "pytest-benchmark>=3.2,<4", + "pytest-cov>=2.8,<3", + "pytest-mock>=2,<3", + "pytest-asyncio>=0.10,<2", + "snapshottest>=0.5,<1", + "coveralls>=1.11,<2", + "promise>=2.3,<3", + "mock>=4.0,<5", + "pytz==2019.3", + "iso8601>=0.1,<2", ] -dev_requires = ["black==19.3b0", "flake8==3.7.7"] + tests_require +dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require setup( name="graphene", @@ -83,9 +82,9 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "tests.*", "examples"]), install_requires=[ - "graphql-core>=3.0.3,<4", - "graphql-relay>=3.0.0,<4", - "aniso8601>=6,<9", + "graphql-core>=3.0.3,<3.1", + "graphql-relay>=3.0,<4", + "aniso8601>=8,<9", "unidecode>=1.1.1,<2", ], tests_require=tests_require, diff --git a/tox.ini b/tox.ini index 090cca07..468f5fbc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py36,py37,pre-commit,mypy +envlist = flake8,py36,py37,py38,pre-commit,mypy skipsdist = true [testenv] @@ -8,12 +8,12 @@ deps = setenv = PYTHONPATH = .:{envdir} commands = - py{36,37}: py.test --cov=graphene graphene examples tests_asyncio {posargs} + py{36,37}: pytest --cov=graphene graphene examples tests_asyncio {posargs} [testenv:pre-commit] basepython=python3.7 deps = - pre-commit>0.12.0 + pre-commit>=2,<3 setenv = LC_CTYPE=en_US.UTF-8 commands = @@ -22,12 +22,12 @@ commands = [testenv:mypy] basepython=python3.7 deps = - mypy>=0.720 + mypy>=0.761,<1 commands = mypy graphene [testenv:flake8] -basepython=python3.6 +basepython=python3.7 deps = flake8>=3.7,<4 commands = From ffb77014662d0585e5096218440525431dcf05b5 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Wed, 4 Mar 2020 11:37:00 +0100 Subject: [PATCH 18/21] Create another alpha release --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index f667e014..efc333ce 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -42,7 +42,7 @@ from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import -VERSION = (3, 0, 0, "alpha", 0) +VERSION = (3, 0, 0, "alpha", 1) __version__ = get_version(VERSION) From 5e6f68957e94ca9b15831d2eed689ce1eeada426 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Wed, 4 Mar 2020 12:23:40 +0100 Subject: [PATCH 19/21] Use latest graphql-core 3.1.0b1 instead of 3.0.3 Adapt Schema, because there is no type reducer in core 3.1 any more. --- .isort.cfg | 2 +- .../snap_test_objectidentification.py | 72 ++-- graphene/__init__.py | 2 +- graphene/relay/tests/test_node.py | 12 +- graphene/relay/tests/test_node_custom.py | 22 +- graphene/tests/issues/test_356.py | 2 +- graphene/types/datetime.py | 81 ++-- graphene/types/inputfield.py | 1 + graphene/types/schema.py | 357 ++++++++---------- graphene/types/tests/test_datetime.py | 23 +- graphene/types/tests/test_schema.py | 18 +- graphene/types/tests/test_type_map.py | 27 +- setup.py | 2 +- 13 files changed, 293 insertions(+), 328 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index d4ed37be..76c6f842 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,2 +1,2 @@ [settings] -known_third_party = aniso8601,graphql,graphql_relay,promise,pytest,pytz,pyutils,setuptools,six,snapshottest,sphinx_graphene_theme +known_third_party = aniso8601,graphql,graphql_relay,promise,pytest,pytz,pyutils,setuptools,snapshottest,sphinx_graphene_theme diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index e42260f8..7bce5ba3 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -31,7 +31,16 @@ snapshots["test_correctly_refetches_xwing 1"] = { snapshots[ "test_str_schema 1" -] = '''"""A faction in the Star Wars saga""" +] = '''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! @@ -43,28 +52,20 @@ type Faction implements Node { ships(before: String = null, after: String = null, first: Int = null, last: Int = null): ShipConnection } -input IntroduceShipInput { - shipName: String! - factionId: String! - clientMutationId: String -} - -type IntroduceShipPayload { - ship: Ship - faction: Faction - clientMutationId: String -} - -type Mutation { - introduceShip(input: IntroduceShipInput!): IntroduceShipPayload -} - """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. """ @@ -82,13 +83,13 @@ type PageInfo { endCursor: String } -type Query { - rebels: Faction - empire: Faction - node( - """The ID of the object""" - id: ID! - ): Node +"""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""" @@ -100,20 +101,19 @@ type Ship implements Node { name: String } -type ShipConnection { - """Pagination data for this connection.""" - pageInfo: PageInfo! - - """Contains the nodes in this connection.""" - edges: [ShipEdge]! +type Mutation { + introduceShip(input: IntroduceShipInput!): IntroduceShipPayload } -"""A Relay edge containing a `Ship` and its cursor.""" -type ShipEdge { - """The item at the end of the edge""" - node: Ship +type IntroduceShipPayload { + ship: Ship + faction: Faction + clientMutationId: String +} - """A cursor for use in pagination""" - cursor: String! +input IntroduceShipInput { + shipName: String! + factionId: String! + clientMutationId: String } ''' diff --git a/graphene/__init__.py b/graphene/__init__.py index efc333ce..876c3085 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -42,7 +42,7 @@ from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import -VERSION = (3, 0, 0, "alpha", 1) +VERSION = (3, 0, 0, "beta", 0) __version__ = get_version(VERSION) diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index de1802e9..92d85105 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -183,6 +183,12 @@ def test_str_schema(): name: String } + """An object with an ID""" + interface Node { + """The ID of the object""" + id: ID! + } + type MyOtherNode implements Node { """The ID of the object""" id: ID! @@ -191,12 +197,6 @@ def test_str_schema(): extraField: String } - """An object with an ID""" - interface Node { - """The ID of the object""" - id: ID! - } - type RootQuery { first: String node( diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index 6f28eb66..cba7366b 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -59,9 +59,12 @@ def test_str_schema_correct(): query: RootQuery } - interface BasePhoto { - """The width of the photo in pixels""" - width: Int + type User implements Node { + """The ID of the object""" + id: ID! + + """The full name of the user""" + name: String } interface Node { @@ -77,20 +80,17 @@ def test_str_schema_correct(): width: Int } + interface BasePhoto { + """The width of the photo in pixels""" + width: Int + } + type RootQuery { node( """The ID of the object""" id: ID! ): Node } - - type User implements Node { - """The ID of the object""" - id: ID! - - """The full name of the user""" - name: String - } ''' ) diff --git a/graphene/tests/issues/test_356.py b/graphene/tests/issues/test_356.py index 0e7daa09..480c5cd1 100644 --- a/graphene/tests/issues/test_356.py +++ b/graphene/tests/issues/test_356.py @@ -27,7 +27,7 @@ def test_issue(): graphene.Schema(query=Query) assert str(exc_info.value) == ( - "Query fields cannot be resolved:" + "Query fields cannot be resolved." " IterableConnectionField type has to be a subclass of Connection." ' Received "MyUnion".' ) diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 25a1248e..c152668f 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -3,8 +3,8 @@ from __future__ import absolute_import import datetime from aniso8601 import parse_date, parse_datetime, parse_time -from graphql import Undefined -from graphql.language import StringValueNode +from graphql.error import GraphQLError +from graphql.language import StringValueNode, print_ast from .scalars import Scalar @@ -20,25 +20,30 @@ class Date(Scalar): def serialize(date): if isinstance(date, datetime.datetime): date = date.date() - assert isinstance( - date, datetime.date - ), 'Received not compatible date "{}"'.format(repr(date)) + if not isinstance(date, datetime.date): + raise GraphQLError("Date cannot represent value: {}".format(repr(date))) return date.isoformat() @classmethod def parse_literal(cls, node): - if isinstance(node, StringValueNode): - return cls.parse_value(node.value) + if not isinstance(node, StringValueNode): + raise GraphQLError( + "Date cannot represent non-string value: {}".format(print_ast(node)) + ) + return cls.parse_value(node.value) @staticmethod def parse_value(value): + if isinstance(value, datetime.date): + return value + if not isinstance(value, str): + raise GraphQLError( + "Date cannot represent non-string value: {}".format(repr(value)) + ) try: - if isinstance(value, datetime.date): - return value - elif isinstance(value, str): - return parse_date(value) + return parse_date(value) except ValueError: - return Undefined + raise GraphQLError("Date cannot represent value: {}".format(repr(value))) class DateTime(Scalar): @@ -50,25 +55,32 @@ class DateTime(Scalar): @staticmethod def serialize(dt): - assert isinstance( - dt, (datetime.datetime, datetime.date) - ), 'Received not compatible datetime "{}"'.format(repr(dt)) + if not isinstance(dt, (datetime.datetime, datetime.date)): + raise GraphQLError("DateTime cannot represent value: {}".format(repr(dt))) return dt.isoformat() @classmethod def parse_literal(cls, node): - if isinstance(node, StringValueNode): - return cls.parse_value(node.value) + if not isinstance(node, StringValueNode): + raise GraphQLError( + "DateTime cannot represent non-string value: {}".format(print_ast(node)) + ) + return cls.parse_value(node.value) @staticmethod def parse_value(value): + if isinstance(value, datetime.datetime): + return value + if not isinstance(value, str): + raise GraphQLError( + "DateTime cannot represent non-string value: {}".format(repr(value)) + ) try: - if isinstance(value, datetime.datetime): - return value - elif isinstance(value, str): - return parse_datetime(value) + return parse_datetime(value) except ValueError: - return Undefined + raise GraphQLError( + "DateTime cannot represent value: {}".format(repr(value)) + ) class Time(Scalar): @@ -80,22 +92,27 @@ class Time(Scalar): @staticmethod def serialize(time): - assert isinstance( - time, datetime.time - ), 'Received not compatible time "{}"'.format(repr(time)) + if not isinstance(time, datetime.time): + raise GraphQLError("Time cannot represent value: {}".format(repr(time))) return time.isoformat() @classmethod def parse_literal(cls, node): - if isinstance(node, StringValueNode): - return cls.parse_value(node.value) + if not isinstance(node, StringValueNode): + raise GraphQLError( + "Time cannot represent non-string value: {}".format(print_ast(node)) + ) + return cls.parse_value(node.value) @classmethod def parse_value(cls, value): + if isinstance(value, datetime.time): + return value + if not isinstance(value, str): + raise GraphQLError( + "Time cannot represent non-string value: {}".format(repr(value)) + ) try: - if isinstance(value, datetime.time): - return value - elif isinstance(value, str): - return parse_time(value) + return parse_time(value) except ValueError: - return Undefined + raise GraphQLError("Time cannot represent value: {}".format(repr(value))) diff --git a/graphene/types/inputfield.py b/graphene/types/inputfield.py index bf3538e3..24d84b6c 100644 --- a/graphene/types/inputfield.py +++ b/graphene/types/inputfield.py @@ -1,4 +1,5 @@ from graphql import Undefined + from .mountedtype import MountedType from .structures import NonNull from .utils import get_type diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 79b5315b..d54f112a 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -7,7 +7,6 @@ from graphql import ( graphql, graphql_sync, introspection_types, - is_type, print_schema, GraphQLArgument, GraphQLBoolean, @@ -71,118 +70,74 @@ def is_graphene_type(type_): return True -def resolve_type(resolve_type_func, map_, type_name, root, info, _type): - type_ = resolve_type_func(root, info) - - if not type_: - return_type = map_[type_name] - return default_type_resolver(root, info, return_type) - - if inspect.isclass(type_) and issubclass(type_, ObjectType): - graphql_type = map_.get(type_._meta.name) - assert graphql_type, "Can't find type {} in schema".format(type_._meta.name) - assert graphql_type.graphene_type == type_, ( - "The type {} does not match with the associated graphene type {}." - ).format(type_, graphql_type.graphene_type) - return graphql_type - - return type_ - - def is_type_of_from_possible_types(possible_types, root, _info): return isinstance(root, possible_types) -class GrapheneGraphQLSchema(GraphQLSchema): - """A GraphQLSchema that can deal with Graphene types as well.""" - +class TypeMap(dict): def __init__( self, query=None, mutation=None, subscription=None, types=None, - directives=None, auto_camelcase=True, ): assert_valid_root_type(query) assert_valid_root_type(mutation) assert_valid_root_type(subscription) + if types is None: + types = [] + for type_ in types: + assert is_graphene_type(type_) self.auto_camelcase = auto_camelcase - super().__init__(query, mutation, subscription, types, directives) - if query: - self.query_type = self.get_type( - query.name if isinstance(query, GraphQLObjectType) else query._meta.name - ) - if mutation: - self.mutation_type = self.get_type( - mutation.name - if isinstance(mutation, GraphQLObjectType) - else mutation._meta.name - ) - if subscription: - self.subscription_type = self.get_type( - subscription.name - if isinstance(subscription, GraphQLObjectType) - else subscription._meta.name - ) + create_graphql_type = self.add_type - def get_graphql_type(self, _type): - if not _type: - return _type - if is_type(_type): - return _type - if is_graphene_type(_type): - graphql_type = self.get_type(_type._meta.name) - assert graphql_type, "Type {} not found in this schema.".format( - _type._meta.name + self.query = create_graphql_type(query) if query else None + self.mutation = create_graphql_type(mutation) if mutation else None + self.subscription = create_graphql_type(subscription) if subscription else None + + self.types = [create_graphql_type(graphene_type) for graphene_type in types] + + def add_type(self, graphene_type): + if inspect.isfunction(graphene_type): + graphene_type = graphene_type() + if isinstance(graphene_type, List): + return GraphQLList(self.add_type(graphene_type.of_type)) + if isinstance(graphene_type, NonNull): + return GraphQLNonNull(self.add_type(graphene_type.of_type)) + try: + name = graphene_type._meta.name + except AttributeError: + raise TypeError( + "Expected Graphene type, but received: {}.".format(graphene_type) ) - assert graphql_type.graphene_type == _type + graphql_type = self.get(name) + if graphql_type: return graphql_type - raise Exception("{} is not a valid GraphQL type.".format(_type)) - - # noinspection PyMethodOverriding - def type_map_reducer(self, map_, type_): - if not type_: - return map_ - if inspect.isfunction(type_): - type_ = type_() - if is_graphene_type(type_): - return self.graphene_reducer(map_, type_) - return super().type_map_reducer(map_, type_) - - def graphene_reducer(self, map_, type_): - if isinstance(type_, (List, NonNull)): - return self.type_map_reducer(map_, type_.of_type) - if type_._meta.name in map_: - _type = map_[type_._meta.name] - if isinstance(_type, GrapheneGraphQLType): - assert _type.graphene_type == type_, ( - "Found different types with the same name in the schema: {}, {}." - ).format(_type.graphene_type, type_) - return map_ - - if issubclass(type_, ObjectType): - internal_type = self.construct_objecttype(map_, type_) - elif issubclass(type_, InputObjectType): - internal_type = self.construct_inputobjecttype(map_, type_) - elif issubclass(type_, Interface): - internal_type = self.construct_interface(map_, type_) - elif issubclass(type_, Scalar): - internal_type = self.construct_scalar(type_) - elif issubclass(type_, Enum): - internal_type = self.construct_enum(type_) - elif issubclass(type_, Union): - internal_type = self.construct_union(map_, type_) + if issubclass(graphene_type, ObjectType): + graphql_type = self.create_objecttype(graphene_type) + elif issubclass(graphene_type, InputObjectType): + graphql_type = self.create_inputobjecttype(graphene_type) + elif issubclass(graphene_type, Interface): + graphql_type = self.create_interface(graphene_type) + elif issubclass(graphene_type, Scalar): + graphql_type = self.create_scalar(graphene_type) + elif issubclass(graphene_type, Enum): + graphql_type = self.create_enum(graphene_type) + elif issubclass(graphene_type, Union): + graphql_type = self.construct_union(graphene_type) else: - raise Exception("Expected Graphene type, but received: {}.".format(type_)) - - return super().type_map_reducer(map_, internal_type) + raise TypeError( + "Expected Graphene type, but received: {}.".format(graphene_type) + ) + self[name] = graphql_type + return graphql_type @staticmethod - def construct_scalar(type_): + def create_scalar(graphene_type): # We have a mapping to the original GraphQL types # so there are no collisions. _scalars = { @@ -192,29 +147,31 @@ class GrapheneGraphQLSchema(GraphQLSchema): Boolean: GraphQLBoolean, ID: GraphQLID, } - if type_ in _scalars: - return _scalars[type_] + if graphene_type in _scalars: + return _scalars[graphene_type] return GrapheneScalarType( - graphene_type=type_, - name=type_._meta.name, - description=type_._meta.description, - serialize=getattr(type_, "serialize", None), - parse_value=getattr(type_, "parse_value", None), - parse_literal=getattr(type_, "parse_literal", None), + graphene_type=graphene_type, + name=graphene_type._meta.name, + description=graphene_type._meta.description, + serialize=getattr(graphene_type, "serialize", None), + parse_value=getattr(graphene_type, "parse_value", None), + parse_literal=getattr(graphene_type, "parse_literal", None), ) @staticmethod - def construct_enum(type_): + def create_enum(graphene_type): values = {} - for name, value in type_._meta.enum.__members__.items(): + for name, value in graphene_type._meta.enum.__members__.items(): description = getattr(value, "description", None) deprecation_reason = getattr(value, "deprecation_reason", None) - if not description and callable(type_._meta.description): - description = type_._meta.description(value) + if not description and callable(graphene_type._meta.description): + description = graphene_type._meta.description(value) - if not deprecation_reason and callable(type_._meta.deprecation_reason): - deprecation_reason = type_._meta.deprecation_reason(value) + if not deprecation_reason and callable( + graphene_type._meta.deprecation_reason + ): + deprecation_reason = graphene_type._meta.deprecation_reason(value) values[name] = GraphQLEnumValue( value=value.value, @@ -223,107 +180,98 @@ class GrapheneGraphQLSchema(GraphQLSchema): ) type_description = ( - type_._meta.description(None) - if callable(type_._meta.description) - else type_._meta.description + graphene_type._meta.description(None) + if callable(graphene_type._meta.description) + else graphene_type._meta.description ) return GrapheneEnumType( - graphene_type=type_, + graphene_type=graphene_type, values=values, - name=type_._meta.name, + name=graphene_type._meta.name, description=type_description, ) - def construct_objecttype(self, map_, type_): - if type_._meta.name in map_: - _type = map_[type_._meta.name] - if isinstance(_type, GrapheneGraphQLType): - assert _type.graphene_type == type_, ( - "Found different types with the same name in the schema: {}, {}." - ).format(_type.graphene_type, type_) - return _type + def create_objecttype(self, graphene_type): + create_graphql_type = self.add_type def interfaces(): interfaces = [] - for interface in type_._meta.interfaces: - self.graphene_reducer(map_, interface) - internal_type = map_[interface._meta.name] - assert internal_type.graphene_type == interface - interfaces.append(internal_type) + for graphene_interface in graphene_type._meta.interfaces: + interface = create_graphql_type(graphene_interface) + assert interface.graphene_type == graphene_interface + interfaces.append(interface) return interfaces - if type_._meta.possible_types: + if graphene_type._meta.possible_types: is_type_of = partial( - is_type_of_from_possible_types, type_._meta.possible_types + is_type_of_from_possible_types, graphene_type._meta.possible_types ) else: - is_type_of = type_.is_type_of + is_type_of = graphene_type.is_type_of return GrapheneObjectType( - graphene_type=type_, - name=type_._meta.name, - description=type_._meta.description, - fields=partial(self.construct_fields_for_type, map_, type_), + graphene_type=graphene_type, + name=graphene_type._meta.name, + description=graphene_type._meta.description, + fields=partial(self.create_fields_for_type, graphene_type), is_type_of=is_type_of, interfaces=interfaces, ) - def construct_interface(self, map_, type_): - if type_._meta.name in map_: - _type = map_[type_._meta.name] - if isinstance(_type, GrapheneInterfaceType): - assert _type.graphene_type == type_, ( - "Found different types with the same name in the schema: {}, {}." - ).format(_type.graphene_type, type_) - return _type - - _resolve_type = None - if type_.resolve_type: - _resolve_type = partial( - resolve_type, type_.resolve_type, map_, type_._meta.name + def create_interface(self, graphene_type): + resolve_type = ( + partial( + self.resolve_type, graphene_type.resolve_type, graphene_type._meta.name ) - return GrapheneInterfaceType( - graphene_type=type_, - name=type_._meta.name, - description=type_._meta.description, - fields=partial(self.construct_fields_for_type, map_, type_), - resolve_type=_resolve_type, + if graphene_type.resolve_type + else None ) - def construct_inputobjecttype(self, map_, type_): + return GrapheneInterfaceType( + graphene_type=graphene_type, + name=graphene_type._meta.name, + description=graphene_type._meta.description, + fields=partial(self.create_fields_for_type, graphene_type), + resolve_type=resolve_type, + ) + + def create_inputobjecttype(self, graphene_type): return GrapheneInputObjectType( - graphene_type=type_, - name=type_._meta.name, - description=type_._meta.description, - out_type=type_._meta.container, + graphene_type=graphene_type, + name=graphene_type._meta.name, + description=graphene_type._meta.description, + out_type=graphene_type._meta.container, fields=partial( - self.construct_fields_for_type, map_, type_, is_input_type=True + self.create_fields_for_type, graphene_type, is_input_type=True ), ) - def construct_union(self, map_, type_): - _resolve_type = None - if type_.resolve_type: - _resolve_type = partial( - resolve_type, type_.resolve_type, map_, type_._meta.name - ) + def construct_union(self, graphene_type): + create_graphql_type = self.add_type def types(): union_types = [] - for objecttype in type_._meta.types: - self.graphene_reducer(map_, objecttype) - internal_type = map_[objecttype._meta.name] - assert internal_type.graphene_type == objecttype - union_types.append(internal_type) + for graphene_objecttype in graphene_type._meta.types: + object_type = create_graphql_type(graphene_objecttype) + assert object_type.graphene_type == graphene_objecttype + union_types.append(object_type) return union_types + resolve_type = ( + partial( + self.resolve_type, graphene_type.resolve_type, graphene_type._meta.name + ) + if graphene_type.resolve_type + else None + ) + return GrapheneUnionType( - graphene_type=type_, - name=type_._meta.name, - description=type_._meta.description, + graphene_type=graphene_type, + name=graphene_type._meta.name, + description=graphene_type._meta.description, types=types, - resolve_type=_resolve_type, + resolve_type=resolve_type, ) def get_name(self, name): @@ -331,15 +279,16 @@ class GrapheneGraphQLSchema(GraphQLSchema): return to_camel_case(name) return name - def construct_fields_for_type(self, map_, type_, is_input_type=False): + def create_fields_for_type(self, graphene_type, is_input_type=False): + create_graphql_type = self.add_type + fields = {} - for name, field in type_._meta.fields.items(): + for name, field in graphene_type._meta.fields.items(): if isinstance(field, Dynamic): field = get_field_as(field.get_type(self), _as=Field) if not field: continue - map_ = self.type_map_reducer(map_, field.type) - field_type = self.get_field_type(map_, field.type) + field_type = create_graphql_type(field.type) if is_input_type: _field = GraphQLInputField( field_type, @@ -350,8 +299,7 @@ class GrapheneGraphQLSchema(GraphQLSchema): else: args = {} for arg_name, arg in field.args.items(): - map_ = self.type_map_reducer(map_, arg.type) - arg_type = self.get_field_type(map_, arg.type) + arg_type = create_graphql_type(arg.type) processed_arg_name = arg.name or self.get_name(arg_name) args[processed_arg_name] = GraphQLArgument( arg_type, @@ -361,12 +309,13 @@ class GrapheneGraphQLSchema(GraphQLSchema): if isinstance(arg.type, NonNull) else arg.default_value, ) + resolve = field.get_resolver( + self.get_resolver(graphene_type, name, field.default_value) + ) _field = GraphQLField( field_type, args=args, - resolve=field.get_resolver( - self.get_resolver_for_type(type_, name, field.default_value) - ), + resolve=resolve, deprecation_reason=field.deprecation_reason, description=field.description, ) @@ -374,15 +323,32 @@ class GrapheneGraphQLSchema(GraphQLSchema): fields[field_name] = _field return fields - def get_resolver_for_type(self, type_, name, default_value): - if not issubclass(type_, ObjectType): + def resolve_type(self, resolve_type_func, type_name, root, info, _type): + type_ = resolve_type_func(root, info) + + if not type_: + return_type = self[type_name] + return default_type_resolver(root, info, return_type) + + if inspect.isclass(type_) and issubclass(type_, ObjectType): + graphql_type = self.get(type_._meta.name) + assert graphql_type, "Can't find type {} in schema".format(type_._meta.name) + assert graphql_type.graphene_type == type_, ( + "The type {} does not match with the associated graphene type {}." + ).format(type_, graphql_type.graphene_type) + return graphql_type + + return type_ + + def get_resolver(self, graphene_type, name, default_value): + if not issubclass(graphene_type, ObjectType): return - resolver = getattr(type_, "resolve_{}".format(name), None) + resolver = getattr(graphene_type, "resolve_{}".format(name), None) if not resolver: # If we don't find the resolver in the ObjectType class, then try to # find it in each of the interfaces interface_resolver = None - for interface in type_._meta.interfaces: + for interface in graphene_type._meta.interfaces: if name not in interface._meta.fields: continue interface_resolver = getattr(interface, "resolve_{}".format(name), None) @@ -394,16 +360,11 @@ class GrapheneGraphQLSchema(GraphQLSchema): if resolver: return get_unbound_function(resolver) - default_resolver = type_._meta.default_resolver or get_default_resolver() + default_resolver = ( + graphene_type._meta.default_resolver or get_default_resolver() + ) return partial(default_resolver, name, default_value) - def get_field_type(self, map_, type_): - if isinstance(type_, List): - return GraphQLList(self.get_field_type(map_, type_.of_type)) - if isinstance(type_, NonNull): - return GraphQLNonNull(self.get_field_type(map_, type_.of_type)) - return map_.get(type_._meta.name) - class Schema: """Schema Definition. @@ -419,11 +380,11 @@ class Schema: fields to *create, update or delete* data in your API. subscription (ObjectType, optional): Root subscription *ObjectType*. Describes entry point for fields to receive continuous updates. + types (List[ObjectType], optional): List of any types to include in schema that + may not be introspected through root types. directives (List[GraphQLDirective], optional): List of custom directives to include in the GraphQL schema. Defaults to only include directives defined by GraphQL spec (@include and @skip) [GraphQLIncludeDirective, GraphQLSkipDirective]. - types (List[GraphQLType], optional): List of any types to include in schema that - may not be introspected through root types. auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case to camelCase (preferred by GraphQL standard). Default True. """ @@ -440,13 +401,15 @@ class Schema: self.query = query self.mutation = mutation self.subscription = subscription - self.graphql_schema = GrapheneGraphQLSchema( - query, - mutation, - subscription, - types, + type_map = TypeMap( + query, mutation, subscription, types, auto_camelcase=auto_camelcase + ) + self.graphql_schema = GraphQLSchema( + type_map.query, + type_map.mutation, + type_map.subscription, + type_map.types, directives, - auto_camelcase=auto_camelcase, ) def __str__(self): diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index bfd56c6c..8bc20a41 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -3,7 +3,7 @@ import datetime import pytz from graphql import GraphQLError -from pytest import fixture, mark +from pytest import fixture from ..datetime import Date, DateTime, Time from ..objecttype import ObjectType @@ -84,8 +84,9 @@ def test_bad_datetime_query(): assert result.errors and len(result.errors) == 1 error = result.errors[0] assert isinstance(error, GraphQLError) - assert error.message == ( - 'Expected type DateTime, found "Some string that\'s not a datetime".' + assert ( + error.message == "DateTime cannot represent value:" + ' "Some string that\'s not a datetime"' ) assert result.data is None @@ -97,8 +98,9 @@ def test_bad_date_query(): error = result.errors[0] assert isinstance(error, GraphQLError) - assert error.message == ( - 'Expected type Date, found "Some string that\'s not a date".' + assert ( + error.message == "Date cannot represent value:" + ' "Some string that\'s not a date"' ) assert result.data is None @@ -110,8 +112,9 @@ def test_bad_time_query(): error = result.errors[0] assert isinstance(error, GraphQLError) - assert error.message == ( - 'Expected type Time, found "Some string that\'s not a time".' + assert ( + error.message == "Time cannot represent value:" + ' "Some string that\'s not a time"' ) assert result.data is None @@ -174,9 +177,6 @@ def test_time_query_variable(sample_time): assert result.data == {"time": isoformat} -@mark.xfail( - reason="creating the error message fails when un-parsable object is not JSON serializable." -) def test_bad_variables(sample_date, sample_datetime, sample_time): def _test_bad_variables(type_, input_): result = schema.execute( @@ -185,8 +185,6 @@ def test_bad_variables(sample_date, sample_datetime, sample_time): ), variables={"input": input_}, ) - # when `input` is not JSON serializable formatting the error message in - # `graphql.utils.is_valid_value` line 79 fails with a TypeError assert isinstance(result.errors, list) assert len(result.errors) == 1 assert isinstance(result.errors[0], GraphQLError) @@ -205,7 +203,6 @@ def test_bad_variables(sample_date, sample_datetime, sample_time): ("DateTime", time), ("Date", not_a_date), ("Date", not_a_date_str), - ("Date", now), ("Date", time), ("Time", not_a_date), ("Time", not_a_date_str), diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index 29581122..7a1c299a 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,5 +1,6 @@ from pytest import raises +from graphql.type import GraphQLObjectType, GraphQLSchema from graphql.pyutils import dedent from ..field import Field @@ -17,8 +18,13 @@ class Query(ObjectType): def test_schema(): - schema = Schema(Query).graphql_schema - assert schema.query_type == schema.get_graphql_type(Query) + schema = Schema(Query) + graphql_schema = schema.graphql_schema + assert isinstance(graphql_schema, GraphQLSchema) + query_type = graphql_schema.query_type + assert isinstance(query_type, GraphQLObjectType) + assert query_type.name == "Query" + assert query_type.graphene_type is Query def test_schema_get_type(): @@ -39,13 +45,13 @@ def test_schema_str(): schema = Schema(Query) assert str(schema) == dedent( """ - type MyOtherType { - field: String - } - type Query { inner: MyOtherType } + + type MyOtherType { + field: String + } """ ) diff --git a/graphene/types/tests/test_type_map.py b/graphene/types/tests/test_type_map.py index 0ef3af1b..2dbbe6bb 100644 --- a/graphene/types/tests/test_type_map.py +++ b/graphene/types/tests/test_type_map.py @@ -1,5 +1,3 @@ -from pytest import raises - from graphql.type import ( GraphQLArgument, GraphQLEnumType, @@ -21,13 +19,13 @@ from ..interface import Interface from ..objecttype import ObjectType from ..scalars import Int, String from ..structures import List, NonNull -from ..schema import GrapheneGraphQLSchema, resolve_type +from ..schema import Schema def create_type_map(types, auto_camelcase=True): - query = GraphQLObjectType("Query", {}) - schema = GrapheneGraphQLSchema(query, types=types, auto_camelcase=auto_camelcase) - return schema.type_map + query = type("Query", (ObjectType,), {}) + schema = Schema(query, types=types, auto_camelcase=auto_camelcase) + return schema.graphql_schema.type_map def test_enum(): @@ -272,20 +270,3 @@ def test_objecttype_with_possible_types(): assert graphql_type.is_type_of assert graphql_type.is_type_of({}, None) is True assert graphql_type.is_type_of(MyObjectType(), None) is False - - -def test_resolve_type_with_missing_type(): - class MyObjectType(ObjectType): - foo_bar = String() - - class MyOtherObjectType(ObjectType): - fizz_buzz = String() - - def resolve_type_func(root, info): - return MyOtherObjectType - - type_map = create_type_map([MyObjectType]) - with raises(AssertionError) as excinfo: - resolve_type(resolve_type_func, type_map, "MyOtherObjectType", {}, {}, None) - - assert "MyOtherObjectTyp" in str(excinfo.value) diff --git a/setup.py b/setup.py index 084c7707..4b336989 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "tests.*", "examples"]), install_requires=[ - "graphql-core>=3.0.3,<3.1", + "graphql-core>=3.1.0b1,<4", "graphql-relay>=3.0,<4", "aniso8601>=8,<9", "unidecode>=1.1.1,<2", From 5d97c848e00d71863c270ecc686597ec46e3a0b5 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Wed, 4 Mar 2020 12:44:53 +0100 Subject: [PATCH 20/21] Remove misleading comment The comment originally referred to the __metaclass__ attribute which is gone now. --- graphene/utils/subclass_with_meta.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index c6ba2d3f..09f08a88 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -19,7 +19,6 @@ class SubclassWithMeta_Meta(InitSubclassMeta): class SubclassWithMeta(metaclass=SubclassWithMeta_Meta): """This class improves __init_subclass__ to receive automatically the options from meta""" - # We will only have the metaclass in Python 2 def __init_subclass__(cls, **meta_options): """This method just terminates the super() chain""" _Meta = getattr(cls, "Meta", None) From 88f79b2850e8fc38254d2c1d8f3900bf3f55dea1 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Wed, 4 Mar 2020 15:26:09 +0100 Subject: [PATCH 21/21] Fix types in Schema docstring (#1100) --- graphene/types/schema.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index d54f112a..f1d1337e 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -374,13 +374,13 @@ class Schema: questions about the types through introspection. Args: - query (ObjectType): Root query *ObjectType*. Describes entry point for fields to *read* + query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* data in your Schema. - mutation (ObjectType, optional): Root mutation *ObjectType*. Describes entry point for + mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for fields to *create, update or delete* data in your API. - subscription (ObjectType, optional): Root subscription *ObjectType*. Describes entry point + subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point for fields to receive continuous updates. - types (List[ObjectType], optional): List of any types to include in schema that + types (Optional[List[Type[ObjectType]]]): List of any types to include in schema that may not be introspected through root types. directives (List[GraphQLDirective], optional): List of custom directives to include in the GraphQL schema. Defaults to only include directives defined by GraphQL spec (@include