From a0b522fa392925aac525f5ebf0a4e5b10a63d8f9 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 26 Apr 2020 13:16:51 +0100 Subject: [PATCH 01/37] Add aiodataloader to test deps --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d924f9f4..132876c8 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ tests_require = [ "mock>=4.0,<5", "pytz==2019.3", "iso8601>=0.1,<2", + "aiodataloader<1", ] dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require From 3e4305259b9f7b0842bac99c4d3a70014e01ac15 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 26 Apr 2020 13:17:00 +0100 Subject: [PATCH 02/37] Add basic test for aiodataloader --- tests_asyncio/test_dataloader.py | 79 ++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests_asyncio/test_dataloader.py diff --git a/tests_asyncio/test_dataloader.py b/tests_asyncio/test_dataloader.py new file mode 100644 index 00000000..fb8d1630 --- /dev/null +++ b/tests_asyncio/test_dataloader.py @@ -0,0 +1,79 @@ +from collections import namedtuple +from unittest.mock import Mock +from pytest import mark +from aiodataloader import DataLoader + +from graphene import ObjectType, String, Schema, Field, List + + +CHARACTERS = { + "1": {"name": "Luke Skywalker", "sibling": "3"}, + "2": {"name": "Darth Vader", "sibling": None}, + "3": {"name": "Leia Organa", "sibling": "1"}, +} + + +get_character = Mock(side_effect=lambda character_id: CHARACTERS[character_id]) + + +class CharacterType(ObjectType): + name = String() + sibling = Field(lambda: CharacterType) + + async def resolve_sibling(character, info): + if character["sibling"]: + return await info.context.character_loader.load(character["sibling"]) + return None + + +class Query(ObjectType): + skywalker_family = List(CharacterType) + + async def resolve_skywalker_family(_, info): + return await info.context.character_loader.load_many(["1", "2", "3"]) + + +mock_batch_load_fn = Mock( + side_effect=lambda character_ids: [get_character(id) for id in character_ids] +) + + +class CharacterLoader(DataLoader): + async def batch_load_fn(self, character_ids): + return mock_batch_load_fn(character_ids) + + +Context = namedtuple("Context", "character_loader") + + +@mark.asyncio +async def test_basic_dataloader(): + schema = Schema(query=Query) + + character_loader = CharacterLoader() + context = Context(character_loader=character_loader) + + query = """ + { + skywalkerFamily { + name + sibling { + name + } + } + } + """ + + result = await schema.execute_async(query, context=context) + + assert not result.errors + assert result.data == { + "skywalkerFamily": [ + {"name": "Luke Skywalker", "sibling": {"name": "Leia Organa"}}, + {"name": "Darth Vader", "sibling": None}, + {"name": "Leia Organa", "sibling": {"name": "Luke Skywalker"}}, + ] + } + + assert mock_batch_load_fn.call_count == 1 + assert get_character.call_count == 3 From 380166989d9112073c170d795801e2cd068ea5db Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 26 Apr 2020 13:22:09 +0100 Subject: [PATCH 03/37] Update dataloader docs --- docs/execution/dataloader.rst | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 3f693075..6e9ca7d8 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -4,7 +4,7 @@ Dataloader DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching -and caching. +and caching. It is provided by a seperate package `aiodataloader `. Batching @@ -15,22 +15,19 @@ Create loaders by providing a batch loading function. .. code:: python - from promise import Promise - from promise.dataloader import DataLoader + from aiodataloader import DataLoader class UserLoader(DataLoader): - def batch_load_fn(self, keys): - # Here we return a promise that will result on the - # corresponding user for each key in keys - return Promise.resolve([get_user(id=key) for key in keys]) + async def batch_load_fn(self, keys): + # Here we call a function to return a user for each key in keys + return [get_user(id=key) for key in keys] -A batch loading function accepts a list of keys, and returns a ``Promise`` -which resolves to a list of ``values``. +A batch loading async function accepts a list of keys, and returns a list of ``values``. Then load individual values from the loader. ``DataLoader`` will coalesce all individual loads which occur within a single frame of execution (executed once -the wrapping promise is resolved) and then call your batch function with all +the wrapping event loop is resolved) and then call your batch function with all requested keys. @@ -38,9 +35,11 @@ requested keys. user_loader = UserLoader() - user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id)) + user1 = await user_loader.load(1) + user1_best_friend = await user_loader.load(user1.best_friend_id)) - user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id)) + user2 = await user_loader.load(2) + user2_best_friend = await user_loader.load(user2.best_friend_id)) A naive application may have issued *four* round-trips to a backend for the @@ -54,9 +53,9 @@ 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): + async 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]) + return [users.get(user_id) for user_id in keys] ``DataLoader`` allows you to decouple unrelated parts of your application without @@ -111,8 +110,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac best_friend = graphene.Field(lambda: User) friends = graphene.List(lambda: User) - def resolve_best_friend(root, info): - return user_loader.load(root.best_friend_id) + async def resolve_best_friend(root, info): + return await user_loader.load(root.best_friend_id) - def resolve_friends(root, info): - return user_loader.load_many(root.friend_ids) + async def resolve_friends(root, info): + return await user_loader.load_many(root.friend_ids) From ae93499a37bdd9f7099b3c645ae36b7ed658f205 Mon Sep 17 00:00:00 2001 From: Alec Rosenbaum Date: Fri, 15 Jan 2021 13:01:43 -0500 Subject: [PATCH 04/37] add failing test for interface meta --- graphene/types/tests/test_interface.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graphene/types/tests/test_interface.py b/graphene/types/tests/test_interface.py index c30a8a3a..3dd4fc4f 100644 --- a/graphene/types/tests/test_interface.py +++ b/graphene/types/tests/test_interface.py @@ -25,13 +25,18 @@ def test_generate_interface(): def test_generate_interface_with_meta(): + class MyFirstInterface(Interface): + pass + class MyInterface(Interface): class Meta: name = "MyOtherInterface" description = "Documentation" + interfaces = [MyFirstInterface] assert MyInterface._meta.name == "MyOtherInterface" assert MyInterface._meta.description == "Documentation" + assert MyInterface._meta.interfaces == [MyFirstInterface] def test_generate_interface_with_fields(): From 86b7e6ac86a8a3095ef0bb4164c84cf727f4d9b8 Mon Sep 17 00:00:00 2001 From: Alec Rosenbaum Date: Fri, 15 Jan 2021 13:02:08 -0500 Subject: [PATCH 05/37] update InterfaceOptions to fix failing test --- graphene/types/interface.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/graphene/types/interface.py b/graphene/types/interface.py index 77086dab..6503b78b 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -5,11 +5,12 @@ from .utils import yank_fields_from_attrs # For static type checking with Mypy MYPY = False if MYPY: - from typing import Dict # NOQA + from typing import Dict, Iterable, Type # NOQA class InterfaceOptions(BaseOptions): fields = None # type: Dict[str, Field] + interfaces = () # type: Iterable[Type[Interface]] class Interface(BaseType): @@ -45,7 +46,7 @@ class Interface(BaseType): """ @classmethod - def __init_subclass_with_meta__(cls, _meta=None, **options): + def __init_subclass_with_meta__(cls, _meta=None, interfaces=(), **options): if not _meta: _meta = InterfaceOptions(cls) @@ -58,6 +59,9 @@ class Interface(BaseType): else: _meta.fields = fields + if not _meta.interfaces: + _meta.interfaces = interfaces + super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod From a17f63cf039939242167dbff46b6b9f24ac04802 Mon Sep 17 00:00:00 2001 From: Alec Rosenbaum Date: Fri, 15 Jan 2021 13:10:14 -0500 Subject: [PATCH 06/37] add failing type_map test, bar_graphql_type has no interfaces --- graphene/types/tests/test_type_map.py | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/graphene/types/tests/test_type_map.py b/graphene/types/tests/test_type_map.py index 334eb241..cc1992ac 100644 --- a/graphene/types/tests/test_type_map.py +++ b/graphene/types/tests/test_type_map.py @@ -270,3 +270,33 @@ 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_interface_with_interfaces(): + class FooInterface(Interface): + foo = String() + + class BarInterface(Interface): + class Meta: + interfaces = [FooInterface] + + foo = String() + bar = String() + + type_map = create_type_map([FooInterface, BarInterface]) + assert "FooInterface" in type_map + foo_graphql_type = type_map["FooInterface"] + assert isinstance(foo_graphql_type, GraphQLInterfaceType) + assert foo_graphql_type.name == "FooInterface" + + assert "BarInterface" in type_map + bar_graphql_type = type_map["BarInterface"] + assert isinstance(bar_graphql_type, GraphQLInterfaceType) + assert bar_graphql_type.name == "BarInterface" + + fields = bar_graphql_type.fields + assert list(fields) == ["foo", "bar"] + assert isinstance(fields["foo"], GraphQLField) + assert isinstance(fields["bar"], GraphQLField) + + assert bar_graphql_type.interfaces == [foo_graphql_type] From 7004515f06024227f3e6068150fc5d275b0c4392 Mon Sep 17 00:00:00 2001 From: Alec Rosenbaum Date: Fri, 15 Jan 2021 13:13:05 -0500 Subject: [PATCH 07/37] implement interface interfaces on TypeMap, fix failing test --- graphene/types/schema.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 4fd71769..8f27c9b9 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -236,11 +236,20 @@ class TypeMap(dict): else None ) + def interfaces(): + interfaces = [] + for graphene_interface in graphene_type._meta.interfaces: + interface = self.add_type(graphene_interface) + assert interface.graphene_type == graphene_interface + interfaces.append(interface) + return interfaces + return GrapheneInterfaceType( graphene_type=graphene_type, name=graphene_type._meta.name, description=graphene_type._meta.description, fields=partial(self.create_fields_for_type, graphene_type), + interfaces=interfaces, resolve_type=resolve_type, ) From f5321d619c0e6c296883e02e13cab97672c03067 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 22:33:14 -0700 Subject: [PATCH 08/37] Update interfaces.rst --- docs/types/interfaces.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/types/interfaces.rst b/docs/types/interfaces.rst index eb7172e9..4d4e32a5 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -44,7 +44,7 @@ Both of these types have all of the fields from the ``Character`` interface, but also bring in extra fields, ``home_planet``, ``starships`` and ``primary_function``, that are specific to that particular type of character. -The full GraphQL schema defition will look like this: +The full GraphQL schema definition will look like this: .. code:: From 17f6a45a47ba2bcf16c0d70a0078ea0a9f5af7b8 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 22:37:32 -0700 Subject: [PATCH 09/37] Update unions.rst --- docs/types/unions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/types/unions.rst b/docs/types/unions.rst index 2c5c5a75..16ac24e8 100644 --- a/docs/types/unions.rst +++ b/docs/types/unions.rst @@ -7,7 +7,7 @@ to specify any common fields between the types. The basics: - Each Union is a Python class that inherits from ``graphene.Union``. -- Unions don't have any fields on it, just links to the possible objecttypes. +- Unions don't have any fields on it, just links to the possible ObjectTypes. Quick example ------------- From 5acd04aa93a38bd7415aa33e4c593560420bc2e7 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 22:40:08 -0700 Subject: [PATCH 10/37] Update mutations.rst --- docs/types/mutations.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index f8c76f35..73866063 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -85,9 +85,9 @@ We should receive: InputFields and InputObjectTypes ---------------------------------- -InputFields are used in mutations to allow nested input data for mutations +InputFields are used in mutations to allow nested input data for mutations. -To use an InputField you define an InputObjectType that specifies the structure of your input data +To use an InputField you define an InputObjectType that specifies the structure of your input data: .. code:: python @@ -112,7 +112,7 @@ To use an InputField you define an InputObjectType that specifies the structure return CreatePerson(person=person) -Note that **name** and **age** are part of **person_data** now +Note that **name** and **age** are part of **person_data** now. Using the above mutation your new query would look like this: @@ -128,7 +128,7 @@ Using the above mutation your new query would look like this: } InputObjectTypes can also be fields of InputObjectTypes allowing you to have -as complex of input data as you need +as complex of input data as you need: .. code:: python @@ -160,7 +160,7 @@ To return an existing ObjectType instead of a mutation-specific type, set the ** def mutate(root, info, name): return Person(name=name) -Then, if we query (``schema.execute(query_str)``) the following: +Then, if we query (``schema.execute(query_str)``) with the following: .. code:: From a5fbb2e9e5a7b5cce1d7e017cfcb40b09f5bbc50 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 22:44:41 -0700 Subject: [PATCH 11/37] Update middleware.rst --- 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 0c5458b2..c0a8c792 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -46,7 +46,7 @@ Functional example ------------------ Middleware can also be defined as a function. Here we define a middleware that -logs the time it takes to resolve each field +logs the time it takes to resolve each field: .. code:: python From 3ed8273239dfcf21fd1a0c00d1658e4cd6ced322 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 22:48:53 -0700 Subject: [PATCH 12/37] Update dataloader.rst --- docs/execution/dataloader.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 3f693075..d2ab012a 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -28,10 +28,9 @@ Create loaders by providing a batch loading function. 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 -the wrapping promise is resolved) and then call your batch function with all -requested keys. +``DataLoader`` will coalesce all individual loads which occur within a +single frame of execution (executed once the wrapping promise is resolved) +and then call your batch function with all requested keys. .. code:: python @@ -96,7 +95,7 @@ Consider the following GraphQL request: } -Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend, +If ``me``, ``bestFriend`` and ``friends`` each need to send a request to the backend, there could be at most 13 database requests! From db9d9a08f2cf07311ab172820c893011a687c7c2 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 23:01:20 -0700 Subject: [PATCH 13/37] Update schema.rst --- docs/types/schema.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/types/schema.rst b/docs/types/schema.rst index 08ff27d0..c98684b7 100644 --- a/docs/types/schema.rst +++ b/docs/types/schema.rst @@ -44,7 +44,7 @@ There are some cases where the schema cannot access all of the types that we pla For example, when a field returns an ``Interface``, the schema doesn't know about any of the implementations. -In this case, we need to use the ``types`` argument when creating the Schema. +In this case, we need to use the ``types`` argument when creating the Schema: .. code:: python @@ -63,7 +63,7 @@ By default all field and argument names (that are not explicitly set with the ``name`` arg) will be converted from ``snake_case`` to ``camelCase`` (as the API is usually being consumed by a js/mobile client) -For example with the ObjectType +For example with the ObjectType the ``last_name`` field name is converted to ``lastName``: .. code:: python @@ -71,12 +71,12 @@ For example with the ObjectType last_name = graphene.String() other_name = graphene.String(name='_other_Name') -the ``last_name`` field name is converted to ``lastName``. + In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor. ``other_name`` converts to ``_other_Name`` (without further transformations). -Your query should look like +Your query should look like: .. code:: @@ -86,7 +86,7 @@ Your query should look like } -To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation. +To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation: .. code:: python From 12302b78f997669f84300ebc9058ff5380e43ca9 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 23:08:42 -0700 Subject: [PATCH 14/37] Update schema.rst --- docs/types/schema.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/types/schema.rst b/docs/types/schema.rst index c98684b7..a82addc9 100644 --- a/docs/types/schema.rst +++ b/docs/types/schema.rst @@ -71,8 +71,6 @@ For example with the ObjectType the ``last_name`` field name is converted to ``l last_name = graphene.String() other_name = graphene.String(name='_other_Name') - - In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor. ``other_name`` converts to ``_other_Name`` (without further transformations). From 002b769db4309b6d86c840a4b9277758be362653 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 23:32:11 -0700 Subject: [PATCH 15/37] Fixing Dataloader docs due to tox issue. --- docs/execution/dataloader.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index d2ab012a..8a8e2ae3 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -28,8 +28,8 @@ Create loaders by providing a batch loading function. A batch loading function accepts a list of keys, and returns a ``Promise`` which resolves to a list of ``values``. -``DataLoader`` will coalesce all individual loads which occur within a -single frame of execution (executed once the wrapping promise is resolved) +``DataLoader`` will coalesce all individual loads which occur within a +single frame of execution (executed once the wrapping promise is resolved) and then call your batch function with all requested keys. From fbac4d50925d3a573ef81100cecd863253c68ec2 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 23:01:20 -0700 Subject: [PATCH 16/37] Fixing grammar and spelling errors across a number of files. --- docs/execution/dataloader.rst | 9 ++++----- docs/execution/middleware.rst | 2 +- docs/types/mutations.rst | 10 +++++----- docs/types/schema.rst | 10 ++++------ docs/types/unions.rst | 2 +- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 3f693075..8a8e2ae3 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -28,10 +28,9 @@ Create loaders by providing a batch loading function. 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 -the wrapping promise is resolved) and then call your batch function with all -requested keys. +``DataLoader`` will coalesce all individual loads which occur within a +single frame of execution (executed once the wrapping promise is resolved) +and then call your batch function with all requested keys. .. code:: python @@ -96,7 +95,7 @@ Consider the following GraphQL request: } -Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend, +If ``me``, ``bestFriend`` and ``friends`` each need to send a request to the backend, there could be at most 13 database requests! diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 0c5458b2..c0a8c792 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -46,7 +46,7 @@ Functional example ------------------ Middleware can also be defined as a function. Here we define a middleware that -logs the time it takes to resolve each field +logs the time it takes to resolve each field: .. code:: python diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index f8c76f35..73866063 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -85,9 +85,9 @@ We should receive: InputFields and InputObjectTypes ---------------------------------- -InputFields are used in mutations to allow nested input data for mutations +InputFields are used in mutations to allow nested input data for mutations. -To use an InputField you define an InputObjectType that specifies the structure of your input data +To use an InputField you define an InputObjectType that specifies the structure of your input data: .. code:: python @@ -112,7 +112,7 @@ To use an InputField you define an InputObjectType that specifies the structure return CreatePerson(person=person) -Note that **name** and **age** are part of **person_data** now +Note that **name** and **age** are part of **person_data** now. Using the above mutation your new query would look like this: @@ -128,7 +128,7 @@ Using the above mutation your new query would look like this: } InputObjectTypes can also be fields of InputObjectTypes allowing you to have -as complex of input data as you need +as complex of input data as you need: .. code:: python @@ -160,7 +160,7 @@ To return an existing ObjectType instead of a mutation-specific type, set the ** def mutate(root, info, name): return Person(name=name) -Then, if we query (``schema.execute(query_str)``) the following: +Then, if we query (``schema.execute(query_str)``) with the following: .. code:: diff --git a/docs/types/schema.rst b/docs/types/schema.rst index 08ff27d0..a82addc9 100644 --- a/docs/types/schema.rst +++ b/docs/types/schema.rst @@ -44,7 +44,7 @@ There are some cases where the schema cannot access all of the types that we pla For example, when a field returns an ``Interface``, the schema doesn't know about any of the implementations. -In this case, we need to use the ``types`` argument when creating the Schema. +In this case, we need to use the ``types`` argument when creating the Schema: .. code:: python @@ -63,7 +63,7 @@ By default all field and argument names (that are not explicitly set with the ``name`` arg) will be converted from ``snake_case`` to ``camelCase`` (as the API is usually being consumed by a js/mobile client) -For example with the ObjectType +For example with the ObjectType the ``last_name`` field name is converted to ``lastName``: .. code:: python @@ -71,12 +71,10 @@ For example with the ObjectType last_name = graphene.String() other_name = graphene.String(name='_other_Name') -the ``last_name`` field name is converted to ``lastName``. - In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor. ``other_name`` converts to ``_other_Name`` (without further transformations). -Your query should look like +Your query should look like: .. code:: @@ -86,7 +84,7 @@ Your query should look like } -To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation. +To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation: .. code:: python diff --git a/docs/types/unions.rst b/docs/types/unions.rst index 2c5c5a75..16ac24e8 100644 --- a/docs/types/unions.rst +++ b/docs/types/unions.rst @@ -7,7 +7,7 @@ to specify any common fields between the types. The basics: - Each Union is a Python class that inherits from ``graphene.Union``. -- Unions don't have any fields on it, just links to the possible objecttypes. +- Unions don't have any fields on it, just links to the possible ObjectTypes. Quick example ------------- From b274a607f403b3e5758f4cd4fd9508116018e9a2 Mon Sep 17 00:00:00 2001 From: belkka Date: Mon, 11 Oct 2021 23:46:13 +0300 Subject: [PATCH 17/37] Avoid ambiguity in graphene.Mutation docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code example in docstring starts with `from graphene import Mutation` and defines a `class Mutation` later. This definition would shadow previously imported name and (which is more important) confuses a reader about usage of this class — one need to keep in mind that previous usage of `Mutation` is imported from graphene and have not been overridden yet. This PR changes an import-from statement to an import statement, so `graphene.Mutation` is used explicitly. This approach is consistent with other code examples in docs (e. g. https://docs.graphene-python.org/en/v2.1.9/types/mutations/). Another option is to change name of example class Mutation to something more clear (maybe SchemaMutation or RootMutation), but I'm not sure what name to choose. Only docstring is updated, no code changes. --- graphene/types/mutation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index ca87775a..0c55a8b6 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -29,21 +29,21 @@ class Mutation(ObjectType): .. code:: python - from graphene import Mutation, ObjectType, String, Boolean, Field + import graphene - class CreatePerson(Mutation): + class CreatePerson(graphene.Mutation): class Arguments: - name = String() + name = graphene.String() - ok = Boolean() - person = Field(Person) + ok = graphene.Boolean() + person = graphene.Field(Person) def mutate(parent, info, name): person = Person(name=name) ok = True return CreatePerson(person=person, ok=ok) - class Mutation(ObjectType): + class Mutation(graphene.ObjectType): create_person = CreatePerson.Field() Meta class options (optional): From 9e17044ddca17bc5d93f8c670d85030613008fcc Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Fri, 5 Nov 2021 02:21:14 +0100 Subject: [PATCH 18/37] Chore: Refactor Multi Expression Code --- graphene/pyutils/version.py | 5 +--- graphene/relay/node.py | 10 ++------ graphene/types/mutation.py | 5 +--- graphene/types/resolver.py | 4 +-- graphene/types/tests/test_subscribe_async.py | 4 +-- graphene/utils/module_loading.py | 25 +++++++++---------- graphene/utils/tests/test_orderedtype.py | 2 +- .../tests/test_disable_introspection.py | 4 +-- 8 files changed, 20 insertions(+), 39 deletions(-) diff --git a/graphene/pyutils/version.py b/graphene/pyutils/version.py index f2005442..8a3be07a 100644 --- a/graphene/pyutils/version.py +++ b/graphene/pyutils/version.py @@ -19,10 +19,7 @@ def get_version(version=None): sub = "" if version[3] == "alpha" and version[4] == 0: git_changeset = get_git_changeset() - if git_changeset: - sub = ".dev%s" % git_changeset - else: - sub = ".dev" + sub = ".dev%s" % git_changeset if git_changeset else ".dev" elif version[3] != "final": mapping = {"alpha": "a", "beta": "b", "rc": "rc"} sub = mapping[version[3]] + str(version[4]) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 8defefff..2a506aa2 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -18,11 +18,7 @@ def is_node(objecttype): if not issubclass(objecttype, ObjectType): return False - for i in objecttype._meta.interfaces: - if issubclass(i, Node): - return True - - return False + return any(issubclass(i, Node) for i in objecttype._meta.interfaces) class GlobalID(Field): @@ -92,9 +88,7 @@ class Node(AbstractNode): _type, _id = cls.from_global_id(global_id) except Exception as e: raise Exception( - f'Unable to parse global ID "{global_id}". ' - 'Make sure it is a base64 encoded string in the format: "TypeName:id". ' - f"Exception message: {str(e)}" + f'Unable to parse global ID "{global_id}". Make sure it is a base64 encoded string in the format: "TypeName:id". Exception message: {e}' ) graphene_type = info.schema.get_type(_type) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index ca87775a..06fc3567 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -101,10 +101,7 @@ class Mutation(ObjectType): "Read more:" " https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input" ) - if input_class: - arguments = props(input_class) - else: - arguments = {} + arguments = props(input_class) if input_class else {} if not resolver: mutate = getattr(cls, "mutate", None) assert mutate, "All mutations must define a mutate method in it" diff --git a/graphene/types/resolver.py b/graphene/types/resolver.py index 6a8ea02b..72d2edb8 100644 --- a/graphene/types/resolver.py +++ b/graphene/types/resolver.py @@ -7,9 +7,7 @@ def dict_resolver(attname, default_value, root, info, **args): def dict_or_attr_resolver(attname, default_value, root, info, **args): - resolver = attr_resolver - if isinstance(root, dict): - resolver = dict_resolver + resolver = dict_resolver if isinstance(root, dict) else attr_resolver return resolver(attname, default_value, root, info, **args) diff --git a/graphene/types/tests/test_subscribe_async.py b/graphene/types/tests/test_subscribe_async.py index 9b7a1f13..50e5ba68 100644 --- a/graphene/types/tests/test_subscribe_async.py +++ b/graphene/types/tests/test_subscribe_async.py @@ -14,9 +14,7 @@ class Subscription(ObjectType): count_to_ten = Field(Int) async def subscribe_count_to_ten(root, info): - count = 0 - while count < 10: - count += 1 + for count in range(1, 11): yield count diff --git a/graphene/utils/module_loading.py b/graphene/utils/module_loading.py index 25dc86ca..d9095d0a 100644 --- a/graphene/utils/module_loading.py +++ b/graphene/utils/module_loading.py @@ -27,19 +27,18 @@ def import_string(dotted_path, dotted_attributes=None): if not dotted_attributes: return result - else: - attributes = dotted_attributes.split(".") - traveled_attributes = [] - try: - for attribute in attributes: - traveled_attributes.append(attribute) - result = getattr(result, attribute) - return result - except AttributeError: - raise ImportError( - 'Module "%s" does not define a "%s" attribute inside attribute/class "%s"' - % (module_path, ".".join(traveled_attributes), class_name) - ) + attributes = dotted_attributes.split(".") + traveled_attributes = [] + try: + for attribute in attributes: + traveled_attributes.append(attribute) + result = getattr(result, attribute) + return result + except AttributeError: + raise ImportError( + 'Module "%s" does not define a "%s" attribute inside attribute/class "%s"' + % (module_path, ".".join(traveled_attributes), class_name) + ) def lazy_import(dotted_path, dotted_attributes=None): diff --git a/graphene/utils/tests/test_orderedtype.py b/graphene/utils/tests/test_orderedtype.py index ea6c7cc0..ad5bd77a 100644 --- a/graphene/utils/tests/test_orderedtype.py +++ b/graphene/utils/tests/test_orderedtype.py @@ -38,4 +38,4 @@ def test_orderedtype_non_orderabletypes(): assert one.__lt__(1) == NotImplemented assert one.__gt__(1) == NotImplemented - assert not one == 1 + assert one != 1 diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index 958a1afa..149ac628 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -18,14 +18,12 @@ schema = Schema(query=Query) def run_query(query: str): document = parse(query) - errors = validate( + return validate( schema=schema.graphql_schema, document_ast=document, rules=(DisableIntrospection,), ) - return errors - def test_disallows_introspection_queries(): errors = run_query("{ __schema { queryType { name } } }") From 27f19e5a905f95f3703a187aaabb67f1623d4a92 Mon Sep 17 00:00:00 2001 From: Mel van Londen Date: Sat, 13 Nov 2021 14:15:18 -0800 Subject: [PATCH 19/37] release v3 stable --- README.md | 8 +------- docs/quickstart.rst | 4 ++-- graphene/__init__.py | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 85849a3d..a7714e33 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,6 @@ **We are looking for contributors**! Please check the [ROADMAP](https://github.com/graphql-python/graphene/blob/master/ROADMAP.md) to see how you can help ❤️ ---- - -**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 an opinionated Python library for building GraphQL schemas/types fast and easily. @@ -37,7 +31,7 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit For instaling graphene, just run this command in your shell ```bash -pip install "graphene>=2.0" +pip install "graphene>=3.0" ``` ## Examples diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 62d11949..0b6c6993 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -60,14 +60,14 @@ Requirements ~~~~~~~~~~~~ - Python (2.7, 3.4, 3.5, 3.6, pypy) -- Graphene (2.0) +- Graphene (3.0) Project setup ~~~~~~~~~~~~~ .. code:: bash - pip install "graphene>=2.0" + pip install "graphene>=3.0" Creating a basic Schema ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/graphene/__init__.py b/graphene/__init__.py index c8ffc0c4..b0b4244d 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -41,7 +41,7 @@ from .types import ( from .utils.module_loading import lazy_import from .utils.resolve_only_args import resolve_only_args -VERSION = (3, 0, 0, "beta", 8) +VERSION = (3, 0, 0, "final", 0) __version__ = get_version(VERSION) From a61f0a214d4087acac097ab05f3969d77d0754b5 Mon Sep 17 00:00:00 2001 From: Mel van Londen Date: Sat, 13 Nov 2021 14:25:10 -0800 Subject: [PATCH 20/37] update README.rst using pandoc --- README.rst | 91 ++++++++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/README.rst b/README.rst index 243215e4..3fb51df2 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,18 @@ +|Graphene Logo| `Graphene `__ |Build Status| |PyPI version| |Coverage Status| +========================================================================================================= + +`💬 Join the community on +Slack `__ + **We are looking for contributors**! Please check the `ROADMAP `__ to see how you can help ❤️ --------------- - -|Graphene Logo| `Graphene `__ |Build Status| |PyPI version| |Coverage Status| -========================================================================================================= - - Introduction ------------ -`Graphene `__ is a Python library for -building GraphQL schemas/types fast and easily. +`Graphene `__ 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. @@ -27,17 +27,18 @@ Integrations Graphene has multiple integrations with different frameworks: -+---------------------+----------------------------------------------------------------------------------------------+ -| integration | Package | -+=====================+==============================================================================================+ -| Django | `graphene-django `__ | -+---------------------+----------------------------------------------------------------------------------------------+ -| SQLAlchemy | `graphene-sqlalchemy `__ | -+---------------------+----------------------------------------------------------------------------------------------+ -| Google App Engine | `graphene-gae `__ | -+---------------------+----------------------------------------------------------------------------------------------+ -| Peewee | *In progress* (`Tracking Issue `__) | -+---------------------+----------------------------------------------------------------------------------------------+ ++-------------------+-------------------------------------------------+ +| integration | Package | ++===================+=================================================+ +| Django | `graphene-django `__ | ++-------------------+-------------------------------------------------+ +| SQLAlchemy | `graphene-sqlalchemy `__ | ++-------------------+-------------------------------------------------+ +| Google App Engine | `graphene-gae `__ | ++-------------------+-------------------------------------------------+ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly with all GraphQL clients, such as @@ -52,13 +53,7 @@ For instaling graphene, just run this command in your shell .. code:: bash - pip install "graphene>=2.0" - -2.0 Upgrade Guide ------------------ - -Please read `UPGRADE-v2.0.md `__ to learn how to -upgrade. + pip install "graphene>=3.0" Examples -------- @@ -67,26 +62,26 @@ Here is one example for you to get started: .. code:: python - import graphene + import graphene - class Query(graphene.ObjectType): - hello = graphene.String(description='A typical hello world') + class Query(graphene.ObjectType): + hello = graphene.String(description='A typical hello world') - def resolve_hello(self, info): - return 'World' + def resolve_hello(self, info): + return 'World' - schema = graphene.Schema(query=Query) + schema = graphene.Schema(query=Query) Then Querying ``graphene.Schema`` is as simple as: .. code:: python - query = ''' - query SayHello { - hello - } - ''' - result = schema.execute(query) + query = ''' + query SayHello { + hello + } + ''' + result = schema.execute(query) If you want to learn even more, you can also check the following `examples `__: @@ -110,20 +105,20 @@ dependencies are installed by running: .. code:: sh - virtualenv venv - source venv/bin/activate - pip install -e ".[test]" + virtualenv venv + source venv/bin/activate + 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: .. code:: sh - py.test graphene/relay/tests/test_node.py # Single file - py.test graphene/relay # 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 +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. @@ -132,7 +127,7 @@ You can also run the benchmarks with: .. code:: sh - py.test graphene --benchmark-only + py.test graphene --benchmark-only Graphene supports several versions of Python. To make sure that changes do not break compatibility with any of those versions, we use ``tox`` to @@ -142,14 +137,14 @@ config file, just run: .. code:: sh - tox + tox If you wish to run against a specific version defined in the ``tox.ini`` file: .. code:: sh - tox -e py36 + 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 @@ -168,7 +163,7 @@ An HTML version of the documentation is produced by running: .. code:: sh - make docs + make docs .. |Graphene Logo| image:: http://graphene-python.org/favicon.png .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master From 7108bc857736dc85f9b4db3fec5d675fd094b61f Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Thu, 2 Dec 2021 12:04:07 +0100 Subject: [PATCH 21/37] Update node.py --- graphene/relay/node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 2a506aa2..c1316d22 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -88,7 +88,9 @@ class Node(AbstractNode): _type, _id = cls.from_global_id(global_id) except Exception as e: raise Exception( - f'Unable to parse global ID "{global_id}". Make sure it is a base64 encoded string in the format: "TypeName:id". Exception message: {e}' + f'Unable to parse global ID "{global_id}". ' + 'Make sure it is a base64 encoded string in the format: "TypeName:id". ' + f"Exception message: {e}" ) graphene_type = info.schema.get_type(_type) From e441fa72aa09ba20485e13eadf22830d28473443 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Tue, 11 Jan 2022 12:13:12 +0100 Subject: [PATCH 22/37] Add Python 3.9 and 3.10 to the test matrix Also update the test dependencies and adapt two tests (#1400). --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/tests.yml | 2 ++ .pre-commit-config.yaml | 18 +++++++++--------- graphene/types/tests/test_objecttype.py | 18 ++++++------------ setup.py | 20 +++++++++++--------- tox.ini | 16 ++++++++-------- 7 files changed, 40 insertions(+), 42 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2a6cdc6b..07c0766f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Build wheel and source tarball run: | pip install wheel diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 95251d9b..c9efc0cf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cdc4d01e..4a8bd62e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,6 +25,8 @@ jobs: fail-fast: false matrix: include: + - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} + - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd6a7340..2ba6d1f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,9 @@ default_language_version: - python: python3.8 + python: python3.9 repos: -- repo: git://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 hooks: - id: check-merge-conflict - id: check-json @@ -16,15 +16,15 @@ repos: - --autofix - id: trailing-whitespace exclude: README.md -- repo: git://github.com/asottile/pyupgrade - rev: v2.24.0 +- repo: https://github.com/asottile/pyupgrade + rev: v2.31.0 hooks: - id: pyupgrade -- repo: git://github.com/ambv/black - rev: 19.3b0 +- repo: https://github.com/ambv/black + rev: 21.12b0 hooks: - id: black -- repo: git://github.com/PyCQA/flake8 - rev: 3.8.4 +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index 1ff8fc8f..dece5e8b 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -191,21 +191,15 @@ def test_objecttype_as_container_all_kwargs(): def test_objecttype_as_container_extra_args(): - with raises(TypeError) as excinfo: - Container("1", "2", "3") - - assert "__init__() takes from 1 to 3 positional arguments but 4 were given" == str( - excinfo.value - ) + msg = r"__init__\(\) takes from 1 to 3 positional arguments but 4 were given" + with raises(TypeError, match=msg): + Container("1", "2", "3") # type: ignore def test_objecttype_as_container_invalid_kwargs(): - with raises(TypeError) as excinfo: - Container(unexisting_field="3") - - assert "__init__() got an unexpected keyword argument 'unexisting_field'" == str( - excinfo.value - ) + msg = r"__init__\(\) got an unexpected keyword argument 'unexisting_field'" + with raises(TypeError, match=msg): + Container(unexisting_field="3") # type: ignore def test_objecttype_container_benchmark(benchmark): diff --git a/setup.py b/setup.py index ae59a92a..517fd7b3 100644 --- a/setup.py +++ b/setup.py @@ -45,17 +45,17 @@ class PyTest(TestCommand): tests_require = [ - "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", + "pytest>=6,<7", + "pytest-benchmark>=3.4,<4", + "pytest-cov>=3,<4", + "pytest-mock>=3,<4", + "pytest-asyncio>=0.16,<2", + "snapshottest>=0.6,<1", + "coveralls>=3.3,<4", "promise>=2.3,<3", "mock>=4.0,<5", - "pytz==2021.1", - "iso8601>=0.1,<2", + "pytz==2021.3", + "iso8601>=1,<2", ] dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require @@ -78,6 +78,8 @@ setup( "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["examples*"]), diff --git a/tox.ini b/tox.ini index c4bf6ad0..96a40546 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py36,py37,py38,pre-commit,mypy +envlist = py3{6,7,8,9,10}, flake8, mypy, pre-commit skipsdist = true [testenv] @@ -8,28 +8,28 @@ deps = setenv = PYTHONPATH = .:{envdir} commands = - py{36,37,38}: pytest --cov=graphene graphene examples {posargs} + py{36,37,38,39,310}: pytest --cov=graphene graphene examples {posargs} [testenv:pre-commit] -basepython=python3.8 +basepython = python3.9 deps = - pre-commit>=2,<3 + pre-commit>=2.16,<3 setenv = LC_CTYPE=en_US.UTF-8 commands = pre-commit run --all-files --show-diff-on-failure [testenv:mypy] -basepython=python3.8 +basepython = python3.9 deps = - mypy>=0.761,<1 + mypy>=0.931,<1 commands = mypy graphene [testenv:flake8] -basepython=python3.8 +basepython = python3.9 deps = - flake8>=3.8,<4 + flake8>=4,<5 commands = pip install --pre -e . flake8 graphene From beb957382d0ab9025d095756655edaa3356bf207 Mon Sep 17 00:00:00 2001 From: Rahul Jha Date: Thu, 13 Jan 2022 15:33:09 +0530 Subject: [PATCH 23/37] Highlight .get in backticks When I first read through the documentation twice, it took me two tries and looking very hard to find out the difference b/w the two. The background highlight using backticks would be helpful in this case. --- docs/types/enums.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/types/enums.rst b/docs/types/enums.rst index a3215cad..b9ac5333 100644 --- a/docs/types/enums.rst +++ b/docs/types/enums.rst @@ -86,7 +86,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu assert Color(1) == Color.RED -However, in Graphene ``Enum`` you need to call get to have the same effect: +However, in Graphene ``Enum`` you need to call `.get` to have the same effect: .. code:: python From 763910e7b59be5b9811cd16228db781af88868ab Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Mon, 7 Feb 2022 15:51:08 +0900 Subject: [PATCH 24/37] fix UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 63f8f622..444bc12a 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -123,7 +123,7 @@ def resolve_my_field(root, info, my_arg): return ... ``` -**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as stantard `Connection`'s arguments (first, before, after, before).** +**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as stantard `Connection`'s arguments (first, last, after, before).** You may need something like this: ```python From 19ebf08339263f0446235ddffbbab0e6d3f11699 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sun, 20 Mar 2022 17:52:44 +0100 Subject: [PATCH 25/37] fix: default value for argument should be Undefined (Issue #1394) and update function from_global_id exception handling (https://github.com/graphql-python/graphql-relay-py/commit/b217aefa8c9162f0c165a0646b545f4da37bcd76) --- .../snap_test_objectidentification.py | 2 +- graphene/relay/node.py | 2 ++ graphene/tests/issues/test_1394.py | 36 +++++++++++++++++++ graphene/types/argument.py | 3 +- graphene/types/tests/test_query.py | 6 ++-- graphene/types/tests/test_type_map.py | 7 ++-- graphene/utils/tests/test_deduplicator.py | 5 +-- 7 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 graphene/tests/issues/test_1394.py diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index 7bce5ba3..d7694e90 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -49,7 +49,7 @@ type Faction implements Node { name: String """The ships used by the faction.""" - ships(before: String = null, after: String = null, first: Int = null, last: Int = null): ShipConnection + ships(before: String, after: String, first: Int, last: Int): ShipConnection } """An object with an ID""" diff --git a/graphene/relay/node.py b/graphene/relay/node.py index c1316d22..dabcff6c 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -86,6 +86,8 @@ 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) + if not _type: + raise ValueError("Invalid Global ID") except Exception as e: raise Exception( f'Unable to parse global ID "{global_id}". ' diff --git a/graphene/tests/issues/test_1394.py b/graphene/tests/issues/test_1394.py new file mode 100644 index 00000000..39374381 --- /dev/null +++ b/graphene/tests/issues/test_1394.py @@ -0,0 +1,36 @@ +from ...types import ObjectType, Schema, String, NonNull + + +class Query(ObjectType): + hello = String(input=NonNull(String)) + + def resolve_hello(self, info, input): + if input == "nothing": + return None + return f"Hello {input}!" + + +schema = Schema(query=Query) + + +def test_required_input_provided(): + """ + Test that a required argument works when provided. + """ + input_value = "Potato" + result = schema.execute('{ hello(input: "%s") }' % input_value) + assert not result.errors + assert result.data == {"hello": "Hello Potato!"} + + +def test_required_input_missing(): + """ + Test that a required argument raised an error if not provided. + """ + result = schema.execute("{ hello }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Field 'hello' argument 'input' of type 'String!' is required, but it was not provided." + ) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 71026d45..f9dc843b 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -1,4 +1,5 @@ from itertools import chain +from graphql import Undefined from .dynamic import Dynamic from .mountedtype import MountedType @@ -41,7 +42,7 @@ class Argument(MountedType): def __init__( self, type_, - default_value=None, + default_value=Undefined, description=None, name=None, required=False, diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 2d3e4c73..e117754f 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -229,11 +229,11 @@ def test_query_arguments(): result = test_schema.execute("{ test }", None) assert not result.errors - assert result.data == {"test": '[null,{"a_str":null,"a_int":null}]'} + assert result.data == {"test": "[null,{}]"} result = test_schema.execute('{ test(aStr: "String!") }', "Source!") assert not result.errors - assert result.data == {"test": '["Source!",{"a_str":"String!","a_int":null}]'} + assert result.data == {"test": '["Source!",{"a_str":"String!"}]'} result = test_schema.execute('{ test(aInt: -123, aStr: "String!") }', "Source!") assert not result.errors @@ -258,7 +258,7 @@ def test_query_input_field(): result = test_schema.execute("{ test }", None) assert not result.errors - assert result.data == {"test": '[null,{"a_input":null}]'} + assert result.data == {"test": "[null,{}]"} result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', "Source!") assert not result.errors diff --git a/graphene/types/tests/test_type_map.py b/graphene/types/tests/test_type_map.py index 12e7a1f4..f0c78e08 100644 --- a/graphene/types/tests/test_type_map.py +++ b/graphene/types/tests/test_type_map.py @@ -1,3 +1,4 @@ +from graphql import Undefined from graphql.type import ( GraphQLArgument, GraphQLEnumType, @@ -244,7 +245,9 @@ def test_objecttype_camelcase(): foo_field = fields["fooBar"] assert isinstance(foo_field, GraphQLField) assert foo_field.args == { - "barFoo": GraphQLArgument(GraphQLString, default_value=None, out_name="bar_foo") + "barFoo": GraphQLArgument( + GraphQLString, default_value=Undefined, out_name="bar_foo" + ) } @@ -267,7 +270,7 @@ def test_objecttype_camelcase_disabled(): assert isinstance(foo_field, GraphQLField) assert foo_field.args == { "bar_foo": GraphQLArgument( - GraphQLString, default_value=None, out_name="bar_foo" + GraphQLString, default_value=Undefined, out_name="bar_foo" ) } diff --git a/graphene/utils/tests/test_deduplicator.py b/graphene/utils/tests/test_deduplicator.py index b845caf1..95a70e74 100644 --- a/graphene/utils/tests/test_deduplicator.py +++ b/graphene/utils/tests/test_deduplicator.py @@ -94,6 +94,7 @@ TEST_DATA = { ], "movies": { "1198359": { + "id": "1198359", "name": "King Arthur: Legend of the Sword", "synopsis": ( "When the child Arthur's father is murdered, Vortigern, " @@ -159,7 +160,7 @@ def test_example_end_to_end(): "date": "2017-05-19", "movie": { "__typename": "Movie", - "id": "TW92aWU6Tm9uZQ==", + "id": "TW92aWU6MTE5ODM1OQ==", "name": "King Arthur: Legend of the Sword", "synopsis": ( "When the child Arthur's father is murdered, Vortigern, " @@ -172,7 +173,7 @@ def test_example_end_to_end(): "__typename": "Event", "id": "RXZlbnQ6MjM0", "date": "2017-05-20", - "movie": {"__typename": "Movie", "id": "TW92aWU6Tm9uZQ=="}, + "movie": {"__typename": "Movie", "id": "TW92aWU6MTE5ODM1OQ=="}, }, ] } From 181d9f76da858e1f7aff27f428e9d9499a0e0063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Birrer?= Date: Tue, 3 May 2022 13:51:14 +0200 Subject: [PATCH 26/37] fix: add default param _variables to parse_literal #1419 This is to match the `graphql-core` API. If it's not respected the `parse_literal` method will produce an error event though dealing with a valid value. --- graphene/tests/issues/test_1419.py | 53 ++++++++++++++++++++++++++++++ graphene/types/base64.py | 2 +- graphene/types/decimal.py | 2 +- graphene/types/generic.py | 2 +- graphene/types/json.py | 2 +- graphene/types/scalars.py | 12 +++---- graphene/types/uuid.py | 2 +- 7 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 graphene/tests/issues/test_1419.py diff --git a/graphene/tests/issues/test_1419.py b/graphene/tests/issues/test_1419.py new file mode 100644 index 00000000..243645fa --- /dev/null +++ b/graphene/tests/issues/test_1419.py @@ -0,0 +1,53 @@ +import pytest + +from ...types.base64 import Base64 +from ...types.datetime import Date, DateTime +from ...types.decimal import Decimal +from ...types.generic import GenericScalar +from ...types.json import JSONString +from ...types.objecttype import ObjectType +from ...types.scalars import ID, BigInt, Boolean, Float, Int, String +from ...types.schema import Schema +from ...types.uuid import UUID + + +@pytest.mark.parametrize( + "input_type,input_value", + [ + (Date, '"2022-02-02"'), + (GenericScalar, '"foo"'), + (Int, "1"), + (BigInt, "12345678901234567890"), + (Float, "1.1"), + (String, '"foo"'), + (Boolean, "true"), + (ID, "1"), + (DateTime, '"2022-02-02T11:11:11"'), + (UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'), + (Decimal, "1.1"), + (JSONString, '{key:"foo",value:"bar"}'), + (Base64, '"Q2hlbG8gd29ycmxkCg=="'), + ], +) +def test_parse_literal_with_variables(input_type, input_value): + # input_b needs to be evaluated as literal while the variable dict for + # input_a is passed along. + + class Query(ObjectType): + generic = GenericScalar(input_a=GenericScalar(), input_b=input_type()) + + def resolve_generic(self, info, input_a=None, input_b=None): + return input + + schema = Schema(query=Query) + + query = f""" + query Test($a: GenericScalar){{ + generic(inputA: $a, inputB: {input_value}) + }} + """ + result = schema.execute( + query, + variables={"a": "bar"}, + ) + assert not result.errors diff --git a/graphene/types/base64.py b/graphene/types/base64.py index baedabeb..69bb3380 100644 --- a/graphene/types/base64.py +++ b/graphene/types/base64.py @@ -22,7 +22,7 @@ class Base64(Scalar): return b64encode(value).decode("utf-8") @classmethod - def parse_literal(cls, node): + def parse_literal(cls, node, _variables=None): if not isinstance(node, StringValueNode): raise GraphQLError( f"Base64 cannot represent non-string value: {print_ast(node)}" diff --git a/graphene/types/decimal.py b/graphene/types/decimal.py index b2acbe7e..94968f49 100644 --- a/graphene/types/decimal.py +++ b/graphene/types/decimal.py @@ -22,7 +22,7 @@ class Decimal(Scalar): return str(dec) @classmethod - def parse_literal(cls, node): + def parse_literal(cls, node, _variables=None): if isinstance(node, (StringValueNode, IntValueNode)): return cls.parse_value(node.value) diff --git a/graphene/types/generic.py b/graphene/types/generic.py index 5d1a6c4b..2a3c8d52 100644 --- a/graphene/types/generic.py +++ b/graphene/types/generic.py @@ -29,7 +29,7 @@ class GenericScalar(Scalar): parse_value = identity @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, (StringValueNode, BooleanValueNode)): return ast.value elif isinstance(ast, IntValueNode): diff --git a/graphene/types/json.py b/graphene/types/json.py index 4bb5061c..7e60de7e 100644 --- a/graphene/types/json.py +++ b/graphene/types/json.py @@ -20,7 +20,7 @@ class JSONString(Scalar): return json.dumps(dt) @staticmethod - def parse_literal(node): + def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): return json.loads(node.value) diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 472f2d41..0bfcedfb 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -75,7 +75,7 @@ class Int(Scalar): parse_value = coerce_int @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, IntValueNode): num = int(ast.value) if MIN_INT <= num <= MAX_INT: @@ -104,7 +104,7 @@ class BigInt(Scalar): parse_value = coerce_int @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, IntValueNode): return int(ast.value) @@ -128,7 +128,7 @@ class Float(Scalar): parse_value = coerce_float @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, (FloatValueNode, IntValueNode)): return float(ast.value) @@ -150,7 +150,7 @@ class String(Scalar): parse_value = coerce_string @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, StringValueNode): return ast.value @@ -164,7 +164,7 @@ class Boolean(Scalar): parse_value = bool @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, BooleanValueNode): return ast.value @@ -182,6 +182,6 @@ class ID(Scalar): parse_value = str @staticmethod - def parse_literal(ast): + def parse_literal(ast, _variables=None): if isinstance(ast, (StringValueNode, IntValueNode)): return ast.value diff --git a/graphene/types/uuid.py b/graphene/types/uuid.py index c21eb165..4714a67f 100644 --- a/graphene/types/uuid.py +++ b/graphene/types/uuid.py @@ -21,7 +21,7 @@ class UUID(Scalar): return str(uuid) @staticmethod - def parse_literal(node): + def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): return _UUID(node.value) From e37ef00ca4606125272e67543d59b7f93c87c02e Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Fri, 6 May 2022 22:31:31 +0200 Subject: [PATCH 27/37] Update test and dev environment --- .pre-commit-config.yaml | 6 +++--- docs/conf.py | 16 +++++++-------- graphene/types/scalars.py | 2 +- graphene/types/tests/test_scalar.py | 20 +++++++++---------- .../types/tests/test_scalars_serialization.py | 2 +- setup.py | 6 +++--- tox.ini | 2 +- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ba6d1f5..87fa4872 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: check-merge-conflict - id: check-json @@ -17,11 +17,11 @@ repos: - id: trailing-whitespace exclude: README.md - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.32.1 hooks: - id: pyupgrade - repo: https://github.com/ambv/black - rev: 21.12b0 + rev: 22.3.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 diff --git a/docs/conf.py b/docs/conf.py index 26becbc2..0166d4c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,18 +64,18 @@ source_suffix = ".rst" master_doc = "index" # General information about the project. -project = u"Graphene" -copyright = u"Graphene 2016" -author = u"Syrus Akbary" +project = "Graphene" +copyright = "Graphene 2016" +author = "Syrus Akbary" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u"1.0" +version = "1.0" # The full version, including alpha/beta/rc tags. -release = u"1.0" +release = "1.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -278,7 +278,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual") + (master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -318,7 +318,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "graphene", u"Graphene Documentation", [author], 1)] +man_pages = [(master_doc, "graphene", "Graphene Documentation", [author], 1)] # If true, show URL addresses after external links. # @@ -334,7 +334,7 @@ texinfo_documents = [ ( master_doc, "Graphene", - u"Graphene Documentation", + "Graphene Documentation", author, "Graphene", "One line description of project.", diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 472f2d41..867b2242 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -143,7 +143,7 @@ class String(Scalar): @staticmethod def coerce_string(value): if isinstance(value, bool): - return u"true" if value else u"false" + return "true" if value else "false" return str(value) serialize = coerce_string diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py index 2ff67208..9dce6c38 100644 --- a/graphene/types/tests/test_scalar.py +++ b/graphene/types/tests/test_scalar.py @@ -11,19 +11,19 @@ def test_scalar(): def test_ints(): - assert Int.parse_value(2 ** 31 - 1) is not None + assert Int.parse_value(2**31 - 1) is not None assert Int.parse_value("2.0") is not None - assert Int.parse_value(2 ** 31) is None + assert Int.parse_value(2**31) is None - assert Int.parse_literal(IntValueNode(value=str(2 ** 31 - 1))) == 2 ** 31 - 1 - assert Int.parse_literal(IntValueNode(value=str(2 ** 31))) is None + assert Int.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 + assert Int.parse_literal(IntValueNode(value=str(2**31))) is None - assert Int.parse_value(-(2 ** 31)) is not None - assert Int.parse_value(-(2 ** 31) - 1) is None + assert Int.parse_value(-(2**31)) is not None + assert Int.parse_value(-(2**31) - 1) is None - assert BigInt.parse_value(2 ** 31) is not None + assert BigInt.parse_value(2**31) is not None assert BigInt.parse_value("2.0") is not None - assert BigInt.parse_value(-(2 ** 31) - 1) is not None + assert BigInt.parse_value(-(2**31) - 1) is not None - assert BigInt.parse_literal(IntValueNode(value=str(2 ** 31 - 1))) == 2 ** 31 - 1 - assert BigInt.parse_literal(IntValueNode(value=str(2 ** 31))) == 2 ** 31 + assert BigInt.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 + assert BigInt.parse_literal(IntValueNode(value=str(2**31))) == 2**31 diff --git a/graphene/types/tests/test_scalars_serialization.py b/graphene/types/tests/test_scalars_serialization.py index a95e8bd4..a91efe2c 100644 --- a/graphene/types/tests/test_scalars_serialization.py +++ b/graphene/types/tests/test_scalars_serialization.py @@ -38,7 +38,7 @@ def test_serializes_output_string(): assert String.serialize(-1.1) == "-1.1" assert String.serialize(True) == "true" assert String.serialize(False) == "false" - assert String.serialize(u"\U0001F601") == u"\U0001F601" + assert String.serialize("\U0001F601") == "\U0001F601" def test_serializes_output_boolean(): diff --git a/setup.py b/setup.py index 517fd7b3..b06702be 100644 --- a/setup.py +++ b/setup.py @@ -53,12 +53,12 @@ tests_require = [ "snapshottest>=0.6,<1", "coveralls>=3.3,<4", "promise>=2.3,<3", - "mock>=4.0,<5", - "pytz==2021.3", + "mock>=4,<5", + "pytz==2022.1", "iso8601>=1,<2", ] -dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require +dev_requires = ["black==22.3.0", "flake8>=4,<5"] + tests_require setup( name="graphene", diff --git a/tox.ini b/tox.ini index 96a40546..a5ad6026 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ commands = [testenv:mypy] basepython = python3.9 deps = - mypy>=0.931,<1 + mypy>=0.950,<1 commands = mypy graphene From 9e7e08d48ae4349cde7402bffd184673ed9ad767 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Sat, 7 May 2022 00:48:04 +0200 Subject: [PATCH 28/37] Make Graphene compatible with Core 3.2 --- .../snap_test_objectidentification.py | 3 +- .../tests/test_objectidentification.py | 2 +- graphene/relay/tests/test_node.py | 11 +++-- graphene/relay/tests/test_node_custom.py | 11 +++-- graphene/test/__init__.py | 3 +- graphene/tests/utils.py | 9 ---- graphene/types/definitions.py | 3 +- graphene/types/schema.py | 14 ++---- graphene/types/tests/test_enum.py | 43 ++++++++++--------- graphene/types/tests/test_schema.py | 12 ++++-- setup.py | 4 +- 11 files changed, 53 insertions(+), 62 deletions(-) delete mode 100644 graphene/tests/utils.py diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index d7694e90..b02a420c 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -115,5 +115,4 @@ input IntroduceShipInput { shipName: String! factionId: String! clientMutationId: String -} -''' +}''' diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index f280df04..c024f432 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -9,7 +9,7 @@ client = Client(schema) def test_str_schema(snapshot): - snapshot.assert_match(str(schema)) + snapshot.assert_match(str(schema).strip()) def test_correctly_fetches_id_name_rebels(snapshot): diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index d46838ac..6b310fde 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -1,7 +1,7 @@ import re -from graphql_relay import to_global_id +from textwrap import dedent -from graphene.tests.utils import dedent +from graphql_relay import to_global_id from ...types import ObjectType, Schema, String from ..node import Node, is_node @@ -171,8 +171,10 @@ def test_node_field_only_lazy_type_wrong(): def test_str_schema(): - assert str(schema) == dedent( - ''' + assert ( + str(schema).strip() + == dedent( + ''' schema { query: RootQuery } @@ -213,4 +215,5 @@ def test_str_schema(): ): MyNode } ''' + ).strip() ) diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index 76a2cad3..762e3424 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -1,6 +1,6 @@ -from graphql import graphql_sync +from textwrap import dedent -from graphene.tests.utils import dedent +from graphql import graphql_sync from ...types import Interface, ObjectType, Schema from ...types.scalars import Int, String @@ -54,8 +54,10 @@ graphql_schema = schema.graphql_schema def test_str_schema_correct(): - assert str(schema) == dedent( - ''' + assert ( + str(schema).strip() + == dedent( + ''' schema { query: RootQuery } @@ -93,6 +95,7 @@ def test_str_schema_correct(): ): Node } ''' + ).strip() ) diff --git a/graphene/test/__init__.py b/graphene/test/__init__.py index 8591dc06..13b05dd3 100644 --- a/graphene/test/__init__.py +++ b/graphene/test/__init__.py @@ -1,5 +1,4 @@ from promise import Promise, is_thenable -from graphql.error import format_error as format_graphql_error from graphql.error import GraphQLError from graphene.types.schema import Schema @@ -7,7 +6,7 @@ from graphene.types.schema import Schema def default_format_error(error): if isinstance(error, GraphQLError): - return format_graphql_error(error) + return error.formatted return {"message": str(error)} diff --git a/graphene/tests/utils.py b/graphene/tests/utils.py deleted file mode 100644 index b9804d9b..00000000 --- a/graphene/tests/utils.py +++ /dev/null @@ -1,9 +0,0 @@ -from textwrap import dedent as _dedent - - -def dedent(text: str) -> str: - """Fix indentation of given text by removing leading spaces and tabs. - Also removes leading newlines and trailing spaces and tabs, but keeps trailing - newlines. - """ - return _dedent(text.lstrip("\n").rstrip(" \t")) diff --git a/graphene/types/definitions.py b/graphene/types/definitions.py index 908cc7c8..e5505fd3 100644 --- a/graphene/types/definitions.py +++ b/graphene/types/definitions.py @@ -7,7 +7,6 @@ from graphql import ( GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, - Undefined, ) @@ -50,7 +49,7 @@ class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType): try: value = enum[value] except KeyError: - return Undefined + pass return super(GrapheneEnumType, self).serialize(value) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 0c6d4183..bf76b36d 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -376,19 +376,11 @@ class TypeMap(dict): 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, f"Can't find type {type_._meta.name} in schema" - assert ( - graphql_type.graphene_type == type_ - ), f"The type {type_} does not match with the associated graphene type {graphql_type.graphene_type}." - return graphql_type + return type_._meta.name - return type_ + return_type = self[type_name] + return default_type_resolver(root, info, return_type) class Schema: diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 6e204aa9..471727c0 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -251,19 +251,22 @@ def test_enum_types(): schema = Schema(query=Query) - assert str(schema) == dedent( - '''\ - type Query { - color: Color! - } + assert ( + str(schema).strip() + == dedent( + ''' + type Query { + color: Color! + } - """Primary colors""" - enum Color { - RED - YELLOW - BLUE - } - ''' + """Primary colors""" + enum Color { + RED + YELLOW + BLUE + } + ''' + ).strip() ) @@ -345,10 +348,7 @@ def test_enum_resolver_invalid(): results = schema.execute("query { color }") assert results.errors - assert ( - results.errors[0].message - == "Expected a value of type 'Color' but received: 'BLACK'" - ) + assert results.errors[0].message == "Enum 'Color' cannot represent value: 'BLACK'" def test_field_enum_argument(): @@ -460,12 +460,13 @@ def test_mutation_enum_input_type(): schema = Schema(query=Query, mutation=MyMutation) result = schema.execute( - """ mutation MyMutation { - createPaint(colorInput: { color: RED }) { - color + """ + mutation MyMutation { + createPaint(colorInput: { color: RED }) { + color + } } - } - """ + """ ) assert not result.errors assert result.data == {"createPaint": {"color": "RED"}} diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index fe4739c9..c03c81ba 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,7 +1,8 @@ -from graphql.type import GraphQLObjectType, GraphQLSchema +from textwrap import dedent + from pytest import raises -from graphene.tests.utils import dedent +from graphql.type import GraphQLObjectType, GraphQLSchema from ..field import Field from ..objecttype import ObjectType @@ -43,8 +44,10 @@ def test_schema_get_type_error(): def test_schema_str(): schema = Schema(Query) - assert str(schema) == dedent( - """ + assert ( + str(schema).strip() + == dedent( + """ type Query { inner: MyOtherType } @@ -53,6 +56,7 @@ def test_schema_str(): field: String } """ + ).strip() ) diff --git a/setup.py b/setup.py index b06702be..dce6aa6c 100644 --- a/setup.py +++ b/setup.py @@ -84,8 +84,8 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["examples*"]), install_requires=[ - "graphql-core~=3.1.2", - "graphql-relay>=3.0,<4", + "graphql-core>=3.1,<3.3", + "graphql-relay>=3.1,<3.3", "aniso8601>=8,<10", ], tests_require=tests_require, From 5d4e71f463eea841c0991fefb5078ede94412cb2 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Wed, 25 May 2022 17:45:28 +0200 Subject: [PATCH 29/37] Fix typo in union comments --- graphene/types/union.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/union.py b/graphene/types/union.py index 928656ae..f77e833a 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -21,7 +21,7 @@ class Union(UnmountedType, BaseType): to determine which type is actually used when the field is resolved. The schema in this example can take a search text and return any of the GraphQL object types - indicated: Human, Droid or Startship. + indicated: Human, Droid or Starship. Ambiguous return types can be resolved on each ObjectType through ``Meta.possible_types`` attribute or ``is_type_of`` method. Or by implementing ``resolve_type`` class method on the From 5475a7ad1ff982b973f4c8c2a4507020c8682e15 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 30 May 2022 13:57:16 +0100 Subject: [PATCH 30/37] v3.1.0 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index b0b4244d..bf9831b5 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -41,7 +41,7 @@ from .types import ( from .utils.module_loading import lazy_import from .utils.resolve_only_args import resolve_only_args -VERSION = (3, 0, 0, "final", 0) +VERSION = (3, 1, 0, "final", 0) __version__ = get_version(VERSION) From 3bdc67c6aed65cf9af27aa1066f3fb006d0a8505 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sun, 20 Mar 2022 21:04:29 +0100 Subject: [PATCH 31/37] fix: input with invalid types should raise an error --- docs/types/scalars.rst | 2 +- graphene/tests/issues/test_1419.py | 4 +- graphene/types/decimal.py | 6 +- graphene/types/json.py | 7 +- graphene/types/scalars.py | 14 +- graphene/types/tests/test_decimal.py | 17 + graphene/types/tests/test_json.py | 52 +++ graphene/types/tests/test_scalar.py | 299 +++++++++++++++++- .../types/tests/test_scalars_serialization.py | 13 +- graphene/types/tests/test_uuid.py | 37 +++ graphene/types/uuid.py | 2 + 11 files changed, 428 insertions(+), 25 deletions(-) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index db20a522..f47fffea 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -270,7 +270,7 @@ The following is an example for creating a DateTime scalar: return dt.isoformat() @staticmethod - def parse_literal(node): + def parse_literal(node, _variables=None): if isinstance(node, ast.StringValue): return datetime.datetime.strptime( node.value, "%Y-%m-%dT%H:%M:%S.%f") diff --git a/graphene/tests/issues/test_1419.py b/graphene/tests/issues/test_1419.py index 243645fa..a08374da 100644 --- a/graphene/tests/issues/test_1419.py +++ b/graphene/tests/issues/test_1419.py @@ -24,8 +24,8 @@ from ...types.uuid import UUID (ID, "1"), (DateTime, '"2022-02-02T11:11:11"'), (UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'), - (Decimal, "1.1"), - (JSONString, '{key:"foo",value:"bar"}'), + (Decimal, '"1.1"'), + (JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'), (Base64, '"Q2hlbG8gd29ycmxkCg=="'), ], ) diff --git a/graphene/types/decimal.py b/graphene/types/decimal.py index 94968f49..0c6ccc97 100644 --- a/graphene/types/decimal.py +++ b/graphene/types/decimal.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from decimal import Decimal as _Decimal +from graphql import Undefined from graphql.language.ast import StringValueNode, IntValueNode from .scalars import Scalar @@ -25,10 +26,11 @@ class Decimal(Scalar): def parse_literal(cls, node, _variables=None): if isinstance(node, (StringValueNode, IntValueNode)): return cls.parse_value(node.value) + return Undefined @staticmethod def parse_value(value): try: return _Decimal(value) - except ValueError: - return None + except Exception: + return Undefined diff --git a/graphene/types/json.py b/graphene/types/json.py index 7e60de7e..ca55836b 100644 --- a/graphene/types/json.py +++ b/graphene/types/json.py @@ -2,6 +2,7 @@ from __future__ import absolute_import import json +from graphql import Undefined from graphql.language.ast import StringValueNode from .scalars import Scalar @@ -22,7 +23,11 @@ class JSONString(Scalar): @staticmethod def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): - return json.loads(node.value) + try: + return json.loads(node.value) + except Exception as error: + raise ValueError(f"Badly formed JSONString: {str(error)}") + return Undefined @staticmethod def parse_value(value): diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 80cfcc8b..a468bb3e 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -1,5 +1,6 @@ from typing import Any +from graphql import Undefined from graphql.language.ast import ( BooleanValueNode, FloatValueNode, @@ -67,9 +68,10 @@ class Int(Scalar): try: num = int(float(value)) except ValueError: - return None + return Undefined if MIN_INT <= num <= MAX_INT: return num + return Undefined serialize = coerce_int parse_value = coerce_int @@ -80,6 +82,7 @@ class Int(Scalar): num = int(ast.value) if MIN_INT <= num <= MAX_INT: return num + return Undefined class BigInt(Scalar): @@ -97,7 +100,7 @@ class BigInt(Scalar): try: num = int(float(value)) except ValueError: - return None + return Undefined return num serialize = coerce_int @@ -107,6 +110,7 @@ class BigInt(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, IntValueNode): return int(ast.value) + return Undefined class Float(Scalar): @@ -122,7 +126,7 @@ class Float(Scalar): try: return float(value) except ValueError: - return None + return Undefined serialize = coerce_float parse_value = coerce_float @@ -131,6 +135,7 @@ class Float(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, (FloatValueNode, IntValueNode)): return float(ast.value) + return Undefined class String(Scalar): @@ -153,6 +158,7 @@ class String(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, StringValueNode): return ast.value + return Undefined class Boolean(Scalar): @@ -167,6 +173,7 @@ class Boolean(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, BooleanValueNode): return ast.value + return Undefined class ID(Scalar): @@ -185,3 +192,4 @@ class ID(Scalar): def parse_literal(ast, _variables=None): if isinstance(ast, (StringValueNode, IntValueNode)): return ast.value + return Undefined diff --git a/graphene/types/tests/test_decimal.py b/graphene/types/tests/test_decimal.py index 9757e82c..1ba48bd1 100644 --- a/graphene/types/tests/test_decimal.py +++ b/graphene/types/tests/test_decimal.py @@ -39,8 +39,25 @@ def test_bad_decimal_query(): not_a_decimal = "Nobody expects the Spanish Inquisition!" result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal) + assert result.errors assert len(result.errors) == 1 assert result.data is None + assert ( + result.errors[0].message + == "Expected value of type 'Decimal', found \"Nobody expects the Spanish Inquisition!\"." + ) + + result = schema.execute("{ decimal(input: true) }") + assert result.errors + assert len(result.errors) == 1 + assert result.data is None + assert result.errors[0].message == "Expected value of type 'Decimal', found true." + + result = schema.execute("{ decimal(input: 1.2) }") + assert result.errors + assert len(result.errors) == 1 + assert result.data is None + assert result.errors[0].message == "Expected value of type 'Decimal', found 1.2." def test_decimal_string_query_integer(): diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index b5537180..bb754b3a 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -21,6 +21,10 @@ def test_jsonstring_query(): assert not result.errors assert result.data == {"json": json_value} + result = schema.execute("""{ json(input: "{}") }""") + assert not result.errors + assert result.data == {"json": "{}"} + def test_jsonstring_query_variable(): json_value = '{"key": "value"}' @@ -31,3 +35,51 @@ def test_jsonstring_query_variable(): ) assert not result.errors assert result.data == {"json": json_value} + + +def test_jsonstring_optional_uuid_input(): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ json(input: null) }") + assert not result.errors + assert result.data == {"json": None} + + +def test_jsonstring_invalid_query(): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ json(input: 1) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'JSONString', found 1." + + result = schema.execute("{ json(input: {}) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'JSONString', found {}." + + result = schema.execute('{ json(input: "a") }') + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == ( + "Expected value of type 'JSONString', found \"a\"; " + "Badly formed JSONString: Expecting value: line 1 column 1 (char 0)" + ) + + result = schema.execute("""{ json(input: "{\\'key\\': 0}") }""") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Syntax Error: Invalid character escape sequence: '\\''." + ) + + result = schema.execute("""{ json(input: "{\\"key\\": 0,}") }""") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == ( + 'Expected value of type \'JSONString\', found "{\\"key\\": 0,}"; ' + "Badly formed JSONString: Expecting property name enclosed in double quotes: line 1 column 11 (char 10)" + ) diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py index 9dce6c38..cbdfd8c5 100644 --- a/graphene/types/tests/test_scalar.py +++ b/graphene/types/tests/test_scalar.py @@ -1,4 +1,7 @@ -from ..scalars import Scalar, Int, BigInt +from ..objecttype import ObjectType, Field +from ..scalars import Scalar, Int, BigInt, Float, String, Boolean +from ..schema import Schema +from graphql import Undefined from graphql.language.ast import IntValueNode @@ -11,19 +14,295 @@ def test_scalar(): def test_ints(): - assert Int.parse_value(2**31 - 1) is not None - assert Int.parse_value("2.0") is not None - assert Int.parse_value(2**31) is None + assert Int.parse_value(2**31 - 1) is not Undefined + assert Int.parse_value("2.0") == 2 + assert Int.parse_value(2**31) is Undefined assert Int.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 - assert Int.parse_literal(IntValueNode(value=str(2**31))) is None + assert Int.parse_literal(IntValueNode(value=str(2**31))) is Undefined - assert Int.parse_value(-(2**31)) is not None - assert Int.parse_value(-(2**31) - 1) is None + assert Int.parse_value(-(2**31)) is not Undefined + assert Int.parse_value(-(2**31) - 1) is Undefined - assert BigInt.parse_value(2**31) is not None - assert BigInt.parse_value("2.0") is not None - assert BigInt.parse_value(-(2**31) - 1) is not None + assert BigInt.parse_value(2**31) is not Undefined + assert BigInt.parse_value("2.0") == 2 + assert BigInt.parse_value(-(2**31) - 1) is not Undefined assert BigInt.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1 assert BigInt.parse_literal(IntValueNode(value=str(2**31))) == 2**31 + + +def return_input(_parent, _info, input): + return input + + +class Optional(ObjectType): + int = Int(input=Int(), resolver=return_input) + big_int = BigInt(input=BigInt(), resolver=return_input) + float = Float(input=Float(), resolver=return_input) + bool = Boolean(input=Boolean(), resolver=return_input) + string = String(input=String(), resolver=return_input) + + +class Query(ObjectType): + optional = Field(Optional) + + def resolve_optional(self, info): + return Optional() + + def resolve_required(self, info, input): + return input + + +schema = Schema(query=Query) + + +class TestInt: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { int(input: 20) } }") + assert not result.errors + assert result.data == {"optional": {"int": 20}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { int(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"int": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { int(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Int cannot represent non-integer value: "20"' + ) + + result = schema.execute('{ optional { int(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == 'Int cannot represent non-integer value: "a"' + + result = schema.execute("{ optional { int(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Int cannot represent non-integer value: true" + ) + + +class TestBigInt: + def test_query(self): + """ + Test that a normal query works. + """ + value = 2**31 + result = schema.execute("{ optional { bigInt(input: %s) } }" % value) + assert not result.errors + assert result.data == {"optional": {"bigInt": value}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { bigInt(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"bigInt": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { bigInt(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found \"20\"." + ) + + result = schema.execute('{ optional { bigInt(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found \"a\"." + ) + + result = schema.execute("{ optional { bigInt(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Expected value of type 'BigInt', found true." + ) + + +class TestFloat: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { float(input: 20) } }") + assert not result.errors + assert result.data == {"optional": {"float": 20.0}} + + result = schema.execute("{ optional { float(input: 20.2) } }") + assert not result.errors + assert result.data == {"optional": {"float": 20.2}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { float(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"float": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { float(input: "20") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Float cannot represent non numeric value: "20"' + ) + + result = schema.execute('{ optional { float(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == 'Float cannot represent non numeric value: "a"' + ) + + result = schema.execute("{ optional { float(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "Float cannot represent non numeric value: true" + ) + + +class TestBoolean: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute("{ optional { bool(input: true) } }") + assert not result.errors + assert result.data == {"optional": {"bool": True}} + + result = schema.execute("{ optional { bool(input: false) } }") + assert not result.errors + assert result.data == {"optional": {"bool": False}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { bool(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"bool": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute('{ optional { bool(input: "True") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "True"' + ) + + result = schema.execute('{ optional { bool(input: "true") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "true"' + ) + + result = schema.execute('{ optional { bool(input: "a") } }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == 'Boolean cannot represent a non boolean value: "a"' + ) + + result = schema.execute("{ optional { bool(input: 1) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Boolean cannot represent a non boolean value: 1" + ) + + result = schema.execute("{ optional { bool(input: 0) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Boolean cannot represent a non boolean value: 0" + ) + + +class TestString: + def test_query(self): + """ + Test that a normal query works. + """ + result = schema.execute('{ optional { string(input: "something something") } }') + assert not result.errors + assert result.data == {"optional": {"string": "something something"}} + + result = schema.execute('{ optional { string(input: "True") } }') + assert not result.errors + assert result.data == {"optional": {"string": "True"}} + + result = schema.execute('{ optional { string(input: "0") } }') + assert not result.errors + assert result.data == {"optional": {"string": "0"}} + + def test_optional_input(self): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ optional { string(input: null) } }") + assert not result.errors + assert result.data == {"optional": {"string": None}} + + def test_invalid_input(self): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ optional { string(input: 1) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message == "String cannot represent a non string value: 1" + ) + + result = schema.execute("{ optional { string(input: 3.2) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "String cannot represent a non string value: 3.2" + ) + + result = schema.execute("{ optional { string(input: true) } }") + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "String cannot represent a non string value: true" + ) diff --git a/graphene/types/tests/test_scalars_serialization.py b/graphene/types/tests/test_scalars_serialization.py index a91efe2c..a0028c85 100644 --- a/graphene/types/tests/test_scalars_serialization.py +++ b/graphene/types/tests/test_scalars_serialization.py @@ -1,3 +1,4 @@ +from graphql import Undefined from ..scalars import Boolean, Float, Int, String @@ -9,12 +10,12 @@ def test_serializes_output_int(): assert Int.serialize(1.1) == 1 assert Int.serialize(-1.1) == -1 assert Int.serialize(1e5) == 100000 - assert Int.serialize(9876504321) is None - assert Int.serialize(-9876504321) is None - assert Int.serialize(1e100) is None - assert Int.serialize(-1e100) is None + assert Int.serialize(9876504321) is Undefined + assert Int.serialize(-9876504321) is Undefined + assert Int.serialize(1e100) is Undefined + assert Int.serialize(-1e100) is Undefined assert Int.serialize("-1.1") == -1 - assert Int.serialize("one") is None + assert Int.serialize("one") is Undefined assert Int.serialize(False) == 0 assert Int.serialize(True) == 1 @@ -27,7 +28,7 @@ def test_serializes_output_float(): assert Float.serialize(1.1) == 1.1 assert Float.serialize(-1.1) == -1.1 assert Float.serialize("-1.1") == -1.1 - assert Float.serialize("one") is None + assert Float.serialize("one") is Undefined assert Float.serialize(False) == 0 assert Float.serialize(True) == 1 diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py index 2280b41f..d34f1664 100644 --- a/graphene/types/tests/test_uuid.py +++ b/graphene/types/tests/test_uuid.py @@ -1,14 +1,19 @@ from ..objecttype import ObjectType from ..schema import Schema from ..uuid import UUID +from ..structures import NonNull class Query(ObjectType): uuid = UUID(input=UUID()) + required_uuid = UUID(input=NonNull(UUID), required=True) def resolve_uuid(self, info, input): return input + def resolve_required_uuid(self, info, input): + return input + schema = Schema(query=Query) @@ -29,3 +34,35 @@ def test_uuidstring_query_variable(): ) assert not result.errors assert result.data == {"uuid": uuid_value} + + +def test_uuidstring_optional_uuid_input(): + """ + Test that we can provide a null value to an optional input + """ + result = schema.execute("{ uuid(input: null) }") + assert not result.errors + assert result.data == {"uuid": None} + + +def test_uuidstring_invalid_query(): + """ + Test that if an invalid type is provided we get an error + """ + result = schema.execute("{ uuid(input: 1) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'UUID', found 1." + + result = schema.execute('{ uuid(input: "a") }') + assert result.errors + assert len(result.errors) == 1 + assert ( + result.errors[0].message + == "Expected value of type 'UUID', found \"a\"; badly formed hexadecimal UUID string" + ) + + result = schema.execute("{ requiredUuid(input: null) }") + assert result.errors + assert len(result.errors) == 1 + assert result.errors[0].message == "Expected value of type 'UUID!', found null." diff --git a/graphene/types/uuid.py b/graphene/types/uuid.py index 4714a67f..f2ba1fcb 100644 --- a/graphene/types/uuid.py +++ b/graphene/types/uuid.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from uuid import UUID as _UUID from graphql.language.ast import StringValueNode +from graphql import Undefined from .scalars import Scalar @@ -24,6 +25,7 @@ class UUID(Scalar): def parse_literal(node, _variables=None): if isinstance(node, StringValueNode): return _UUID(node.value) + return Undefined @staticmethod def parse_value(value): From 8f6a8f9c4ace5fc2184040e6a56e877a6d8acae6 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Fri, 24 Jun 2022 18:00:55 +0200 Subject: [PATCH 32/37] feat: add ability to provide a type name to enum when using from_enum --- graphene/types/enum.py | 7 +++-- graphene/types/tests/test_enum.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 70e8ee8e..e5cc50ed 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -52,7 +52,10 @@ class EnumMeta(SubclassWithMeta_Meta): return super(EnumMeta, cls).__call__(*args, **kwargs) # return cls._meta.enum(*args, **kwargs) - def from_enum(cls, enum, description=None, deprecation_reason=None): # noqa: N805 + def from_enum( + cls, enum, name=None, description=None, deprecation_reason=None + ): # noqa: N805 + name = name or enum.__name__ description = description or enum.__doc__ meta_dict = { "enum": enum, @@ -60,7 +63,7 @@ class EnumMeta(SubclassWithMeta_Meta): "deprecation_reason": deprecation_reason, } meta_class = type("Meta", (object,), meta_dict) - return type(meta_class.enum.__name__, (Enum,), {"Meta": meta_class}) + return type(name, (Enum,), {"Meta": meta_class}) class Enum(UnmountedType, BaseType, metaclass=EnumMeta): diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 471727c0..679de16e 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -328,6 +328,52 @@ def test_enum_resolver_compat(): assert results.data["colorByName"] == Color.RED.name +def test_enum_with_name(): + from enum import Enum as PyEnum + + class Color(PyEnum): + RED = 1 + YELLOW = 2 + BLUE = 3 + + GColor = Enum.from_enum(Color, description="original colors") + UniqueGColor = Enum.from_enum( + Color, name="UniqueColor", description="unique colors" + ) + + class Query(ObjectType): + color = GColor(required=True) + unique_color = UniqueGColor(required=True) + + schema = Schema(query=Query) + + assert ( + str(schema).strip() + == dedent( + ''' + type Query { + color: Color! + uniqueColor: UniqueColor! + } + + """original colors""" + enum Color { + RED + YELLOW + BLUE + } + + """unique colors""" + enum UniqueColor { + RED + YELLOW + BLUE + } + ''' + ).strip() + ) + + def test_enum_resolver_invalid(): from enum import Enum as PyEnum From 2ee23b0b2c1440755da133bc59a06797cba78136 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Mon, 27 Jun 2022 16:30:01 +0200 Subject: [PATCH 33/37] Add codecov action --- .github/workflows/tests.yml | 22 ++++++++++++++++------ tox.ini | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a8bd62e..8a962ac6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,12 +27,12 @@ jobs: include: - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - - {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36} + - { name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38 } + - { name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37 } + - { name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36 } steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} @@ -47,10 +47,20 @@ jobs: run: echo "::set-output name=dir::$(pip cache dir)" - name: cache pip dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }} - run: pip install tox - run: tox -e ${{ matrix.tox }} + - name: Upload coverage.xml + if: ${{ matrix.python == '3.10' }} + uses: actions/upload-artifact@v3 + with: + name: graphene-sqlalchemy-coverage + path: coverage.xml + if-no-files-found: error + - name: Upload coverage.xml to codecov + if: ${{ matrix.python == '3.10' }} + uses: codecov/codecov-action@v3 diff --git a/tox.ini b/tox.ini index a5ad6026..07ddc767 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ deps = setenv = PYTHONPATH = .:{envdir} commands = - py{36,37,38,39,310}: pytest --cov=graphene graphene examples {posargs} + py{36,37,38,39,310}: pytest --cov=graphene graphene --cov-report=term --cov-report=xml examples {posargs} [testenv:pre-commit] basepython = python3.9 From 8589aaeb98f0b15ea4c312ef8b6d3382c65c70b4 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sat, 16 Jul 2022 14:40:00 +1000 Subject: [PATCH 34/37] docs: Fix a few typos There are small typos in: - UPGRADE-v1.0.md - UPGRADE-v2.0.md - docs/execution/fileuploading.rst Fixes: - Should read `standard` rather than `stantard`. - Should read `library` rather than `libary`. - Should read `explicitly` rather than `explicity`. Signed-off-by: Tim Gates --- UPGRADE-v1.0.md | 2 +- UPGRADE-v2.0.md | 2 +- docs/execution/fileuploading.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/UPGRADE-v1.0.md b/UPGRADE-v1.0.md index 8ace8756..ecfa9da7 100644 --- a/UPGRADE-v1.0.md +++ b/UPGRADE-v1.0.md @@ -153,7 +153,7 @@ class Query(ObjectType): ``` Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it -explicity. +explicitly. ## Django diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 444bc12a..04926e7a 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -123,7 +123,7 @@ def resolve_my_field(root, info, my_arg): return ... ``` -**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as stantard `Connection`'s arguments (first, last, after, before).** +**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as standard `Connection`'s arguments (first, last, after, before).** You may need something like this: ```python diff --git a/docs/execution/fileuploading.rst b/docs/execution/fileuploading.rst index d92174c0..66ce9bd3 100644 --- a/docs/execution/fileuploading.rst +++ b/docs/execution/fileuploading.rst @@ -4,5 +4,5 @@ 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 +If your server needs to support file uploading then you can use the library: `graphene-file-upload `_ which enhances Graphene to add file uploads and conforms to the unoffical GraphQL `multipart request spec `_. From 80e3498750e26babb5d7cb0fec4441d9842af39a Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Sat, 13 Aug 2022 14:51:58 +0200 Subject: [PATCH 35/37] Fix Test Failure due to #1304 assert bar_graphql_type.interfaces == [foo_graphql_type] failed only on tox, because .interfaces was a tuple instead of a list. Error didn't occur using just pytest. Fixed by explicitly converting both to list. --- graphene/types/tests/test_type_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/tests/test_type_map.py b/graphene/types/tests/test_type_map.py index adddd388..55b1706e 100644 --- a/graphene/types/tests/test_type_map.py +++ b/graphene/types/tests/test_type_map.py @@ -318,4 +318,4 @@ def test_interface_with_interfaces(): assert isinstance(fields["foo"], GraphQLField) assert isinstance(fields["bar"], GraphQLField) - assert bar_graphql_type.interfaces == [foo_graphql_type] + assert list(bar_graphql_type.interfaces) == list([foo_graphql_type]) From 13c661332e44335944bc6ca22742156cbe3c3db6 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Sat, 13 Aug 2022 15:08:34 +0200 Subject: [PATCH 36/37] Fix typo Co-authored-by: Justin Miller --- docs/execution/dataloader.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 6e9ca7d8..a19e9fd3 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -4,7 +4,7 @@ Dataloader DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching -and caching. It is provided by a seperate package `aiodataloader `. +and caching. It is provided by a separate package `aiodataloader `. Batching From 6339f489e9d780af2affadc6538673d71ac56c8d Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Sun, 14 Aug 2022 12:09:07 +0200 Subject: [PATCH 37/37] Delete coveralls.yml We are now using Codecov --- .github/workflows/coveralls.yml | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/workflows/coveralls.yml diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml deleted file mode 100644 index a8e2875c..00000000 --- a/.github/workflows/coveralls.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: 📊 Check Coverage -on: - push: - branches: - - master - - '*.x' - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' - pull_request: - branches: - - master - - '*.x' - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' -jobs: - coveralls_finish: - # check coverage increase/decrease - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop