From 6f9cdb4888a230d39d99ffc540395cdc379d746c Mon Sep 17 00:00:00 2001 From: bartenra <77667589+bartenra@users.noreply.github.com> Date: Wed, 24 Mar 2021 20:32:35 +0100 Subject: [PATCH 01/87] Fix links to Relay docs (#1318) --- docs/relay/index.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/relay/index.rst b/docs/relay/index.rst index 8435ca2e..2efde25f 100644 --- a/docs/relay/index.rst +++ b/docs/relay/index.rst @@ -19,10 +19,8 @@ Useful links - `Getting started with Relay`_ - `Relay Global Identification Specification`_ - `Relay Cursor Connection Specification`_ -- `Relay input Object Mutation`_ -.. _Relay: https://facebook.github.io/relay/docs/en/graphql-server-specification.html -.. _Relay specification: https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-root-field -.. _Getting started with Relay: https://facebook.github.io/relay/docs/en/quick-start-guide.html -.. _Relay Global Identification Specification: https://facebook.github.io/relay/graphql/objectidentification.htm -.. _Relay Cursor Connection Specification: https://facebook.github.io/relay/graphql/connections.htm +.. _Relay: https://relay.dev/docs/guides/graphql-server-specification/ +.. _Getting started with Relay: https://relay.dev/docs/getting-started/step-by-step-guide/ +.. _Relay Global Identification Specification: https://relay.dev/graphql/objectidentification.htm +.. _Relay Cursor Connection Specification: https://relay.dev/graphql/connections.htm From f622f1f53c2da2ca5c6a0a105dc91dae66778c3d Mon Sep 17 00:00:00 2001 From: shukryzablah Date: Wed, 24 Mar 2021 15:32:51 -0400 Subject: [PATCH 02/87] Update index.rst (#1313) --- docs/testing/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/testing/index.rst b/docs/testing/index.rst index 0103779c..877879f6 100644 --- a/docs/testing/index.rst +++ b/docs/testing/index.rst @@ -77,13 +77,13 @@ Snapshot testing As our APIs evolve, we need to know when our changes introduce any breaking changes that might break some of the clients of our GraphQL app. -However, writing tests and replicate the same response we expect from our GraphQL application can be +However, writing tests and replicating the same response we expect from our GraphQL application can be a tedious and repetitive task, and sometimes it's easier to skip this process. Because of that, we recommend the usage of `SnapshotTest `_. -SnapshotTest let us write all this tests in a breeze, as creates automatically the ``snapshots`` for us -the first time the test is executed. +SnapshotTest lets us write all these tests in a breeze, as it automatically creates the ``snapshots`` for us +the first time the test are executed. Here is a simple example on how our tests will look if we use ``pytest``: From f5321d619c0e6c296883e02e13cab97672c03067 Mon Sep 17 00:00:00 2001 From: Justin Miller Date: Mon, 12 Apr 2021 22:33:14 -0700 Subject: [PATCH 03/87] 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 04/87] 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 05/87] 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 06/87] 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 07/87] 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 08/87] 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 09/87] 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 10/87] 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 11/87] 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 c08379ed85b2759de32777eb5dd3dca143a6d69f Mon Sep 17 00:00:00 2001 From: Minh Tu Le Date: Mon, 19 Apr 2021 10:03:11 -0700 Subject: [PATCH 12/87] Use argument's `default_value` regardless if the input field is required (#1326) * Use argument's default value regardless if the input field is required * Add a test * Format code --- graphene/types/schema.py | 5 +---- graphene/types/tests/test_type_map.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 4fd71769..99532354 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -26,7 +26,6 @@ from graphql import ( GraphQLObjectType, GraphQLSchema, GraphQLString, - Undefined, ) from graphql.execution import ExecutionContext from graphql.execution.values import get_argument_values @@ -313,9 +312,7 @@ class TypeMap(dict): arg_type, out_name=arg_name, description=arg.description, - default_value=Undefined - if isinstance(arg.type, NonNull) - else arg.default_value, + default_value=arg.default_value, ) subscribe = field.wrap_subscribe( self.get_function_for_type( diff --git a/graphene/types/tests/test_type_map.py b/graphene/types/tests/test_type_map.py index 334eb241..12e7a1f4 100644 --- a/graphene/types/tests/test_type_map.py +++ b/graphene/types/tests/test_type_map.py @@ -6,6 +6,7 @@ from graphql.type import ( GraphQLInputField, GraphQLInputObjectType, GraphQLInterfaceType, + GraphQLNonNull, GraphQLObjectType, GraphQLString, ) @@ -94,6 +95,21 @@ def test_objecttype(): } +def test_required_argument_with_default_value(): + class MyObjectType(ObjectType): + foo = String(bar=String(required=True, default_value="x")) + + type_map = create_type_map([MyObjectType]) + + graphql_type = type_map["MyObjectType"] + foo_field = graphql_type.fields["foo"] + + bar_argument = foo_field.args["bar"] + assert bar_argument.default_value == "x" + assert isinstance(bar_argument.type, GraphQLNonNull) + assert bar_argument.type.of_type == GraphQLString + + def test_dynamic_objecttype(): class MyObjectType(ObjectType): """Description""" From 485b1ed325287fd721b13aac8b4ec872d6295c6a Mon Sep 17 00:00:00 2001 From: kevinr-electric <77303321+kevinr-electric@users.noreply.github.com> Date: Thu, 22 Apr 2021 23:28:05 -0400 Subject: [PATCH 13/87] fix field name in execute.rst example (#1327) fix field name in execute.rst 'Operation Name' example --- docs/execution/execute.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index 23be0b42..1c0e2599 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -110,7 +110,7 @@ If there are multiple operations defined in a query string, ``operation_name`` s from graphene import ObjectType, Field, Schema class Query(ObjectType): - me = Field(User) + user = Field(User) def resolve_user(root, info): return get_user_by_id(12) From 69b628686105e4269c389f1125d79742ccab2bc2 Mon Sep 17 00:00:00 2001 From: Sergey Fedoseev Date: Fri, 16 Jul 2021 22:10:53 +0500 Subject: [PATCH 14/87] Fix typo in docstring of ObjectType (#1343) --- graphene/types/objecttype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index f4a0f5a0..c69be937 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -66,7 +66,7 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta): Methods starting with ``resolve_`` are bound as resolvers of the matching Field name. If no resolver is provided, the default resolver is used. - Ambiguous types with Interface and Union can be determined through``is_type_of`` method and + Ambiguous types with Interface and Union can be determined through ``is_type_of`` method and ``Meta.possible_types`` attribute. .. code:: python From 5290c9364c2479884eef2dbf06702381a4828fcb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Jul 2021 19:11:49 +0200 Subject: [PATCH 15/87] Allow later aniso8601 releases (#1331) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 48d7d285..03001111 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ setup( install_requires=[ "graphql-core>=3.1.2,<4", "graphql-relay>=3.0,<4", - "aniso8601>=8,<9", + "aniso8601>=8,<10", ], tests_require=tests_require, extras_require={"test": tests_require, "dev": dev_requires}, From fce45ef5520c92f59d7988f36eddbb889e48e1fd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Jul 2021 19:12:09 +0200 Subject: [PATCH 16/87] Update pytz to 2021.1 (#1330) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 03001111..1503c3c6 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ tests_require = [ "coveralls>=1.11,<2", "promise>=2.3,<3", "mock>=4.0,<5", - "pytz==2019.3", + "pytz==2021.1", "iso8601>=0.1,<2", ] From aa11681048a6be67023627a4907e013d65dd13d1 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:22:12 +0530 Subject: [PATCH 17/87] add depth limit validator --- docs/execution/index.rst | 1 + docs/execution/validators.rst | 35 +++ graphene/validators/__init__.py | 6 + graphene/validators/depth_limit_validator.py | 198 +++++++++++++ graphene/validators/tests/__init__.py | 0 .../tests/test_depth_limit_validator.py | 279 ++++++++++++++++++ 6 files changed, 519 insertions(+) create mode 100644 docs/execution/validators.rst create mode 100644 graphene/validators/__init__.py create mode 100644 graphene/validators/depth_limit_validator.py create mode 100644 graphene/validators/tests/__init__.py create mode 100644 graphene/validators/tests/test_depth_limit_validator.py diff --git a/docs/execution/index.rst b/docs/execution/index.rst index dbfbfa72..46652665 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -10,3 +10,4 @@ Execution dataloader fileuploading subscriptions + validators diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst new file mode 100644 index 00000000..94bb7c2f --- /dev/null +++ b/docs/execution/validators.rst @@ -0,0 +1,35 @@ +Middleware +========== + +Validation rules help validate a given GraphQL query, before executing it.To help with common use +cases, graphene provides a few validation rules out of the box. + + +Depth limit Validator +----------------- +The depth limit validator helps to prevent execution of malicious +queries. It takes in the following arguments. + +- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document. +- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean +- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. + +Example +------- + +Here is how you would implement depth-limiting on your schema. + +.. code:: python + from graphene.validators import depth_limit_validator + + # The following schema doesn't execute queries + # which have a depth more than 20. + + result = schema.execute( + 'THE QUERY', + validation_rules=[ + depth_limit_validator( + max_depth=20 + ) + ] + ) diff --git a/graphene/validators/__init__.py b/graphene/validators/__init__.py new file mode 100644 index 00000000..8bd8d884 --- /dev/null +++ b/graphene/validators/__init__.py @@ -0,0 +1,6 @@ +from .depth_limit_validator import depth_limit_validator + + +__all__ = [ + "depth_limit_validator" +] diff --git a/graphene/validators/depth_limit_validator.py b/graphene/validators/depth_limit_validator.py new file mode 100644 index 00000000..43615205 --- /dev/null +++ b/graphene/validators/depth_limit_validator.py @@ -0,0 +1,198 @@ +# This is a Python port of https://github.com/stems/graphql-depth-limit +# which is licensed under the terms of the MIT license, reproduced below. +# +# ----------- +# +# MIT License +# +# Copyright (c) 2017 Stem +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import re +from typing import Callable, Dict, List, Optional, Union + +from graphql import GraphQLError, is_introspection_type +from graphql.language import ( + DefinitionNode, + FieldNode, + FragmentDefinitionNode, + FragmentSpreadNode, + InlineFragmentNode, + Node, + OperationDefinitionNode, +) +from graphql.validation import ValidationContext, ValidationRule + + +IgnoreType = Union[Callable[[str], bool], re.Pattern, str] + + +def depth_limit_validator( + max_depth: int, + ignore: Optional[List[IgnoreType]] = None, + callback: Callable[[Dict[str, int]], None] = None, +): + class DepthLimitValidator(ValidationRule): + def __init__(self, validation_context: ValidationContext): + document = validation_context.document + definitions = document.definitions + + fragments = get_fragments(definitions) + queries = get_queries_and_mutations(definitions) + query_depths = {} + + for name in queries: + query_depths[name] = determine_depth( + node=queries[name], + fragments=fragments, + depth_so_far=0, + max_depth=max_depth, + context=validation_context, + operation_name=name, + ignore=ignore, + ) + + if callable(callback): + callback(query_depths) + super().__init__(validation_context) + + return DepthLimitValidator + + +def get_fragments( + definitions: List[DefinitionNode], +) -> Dict[str, FragmentDefinitionNode]: + fragments = {} + for definition in definitions: + if isinstance(definition, FragmentDefinitionNode): + fragments[definition.name.value] = definition + + return fragments + + +# This will actually get both queries and mutations. +# We can basically treat those the same +def get_queries_and_mutations( + definitions: List[DefinitionNode], +) -> Dict[str, OperationDefinitionNode]: + operations = {} + + for definition in definitions: + if isinstance(definition, OperationDefinitionNode): + operation = definition.name.value if definition.name else "anonymous" + operations[operation] = definition + + return operations + + +def determine_depth( + node: Node, + fragments: Dict[str, FragmentDefinitionNode], + depth_so_far: int, + max_depth: int, + context: ValidationContext, + operation_name: str, + ignore: Optional[List[IgnoreType]] = None, +) -> int: + if depth_so_far > max_depth: + context.report_error( + GraphQLError( + f"'{operation_name}' exceeds maximum operation depth of {max_depth}", + [node], + ) + ) + return depth_so_far + + if isinstance(node, FieldNode): + # from: https://spec.graphql.org/June2018/#sec-Schema + # > All types and directives defined within a schema must not have a name which + # > begins with "__" (two underscores), as this is used exclusively + # > by GraphQL’s introspection system. + should_ignore = str(node.name.value).startswith("__") or is_ignored( + node, ignore + ) + + if should_ignore or not node.selection_set: + return 0 + + return 1 + max( + map( + lambda selection: determine_depth( + node=selection, + fragments=fragments, + depth_so_far=depth_so_far + 1, + max_depth=max_depth, + context=context, + operation_name=operation_name, + ignore=ignore, + ), + node.selection_set.selections, + ) + ) + elif isinstance(node, FragmentSpreadNode): + return determine_depth( + node=fragments[node.name.value], + fragments=fragments, + depth_so_far=depth_so_far, + max_depth=max_depth, + context=context, + operation_name=operation_name, + ignore=ignore, + ) + elif isinstance( + node, (InlineFragmentNode, FragmentDefinitionNode, OperationDefinitionNode) + ): + return max( + map( + lambda selection: determine_depth( + node=selection, + fragments=fragments, + depth_so_far=depth_so_far, + max_depth=max_depth, + context=context, + operation_name=operation_name, + ignore=ignore, + ), + node.selection_set.selections, + ) + ) + else: + raise Exception(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover + + +def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool: + if ignore is None: + return False + + for rule in ignore: + field_name = node.name.value + if isinstance(rule, str): + if field_name == rule: + return True + elif isinstance(rule, re.Pattern): + if rule.match(field_name): + return True + elif callable(rule): + if rule(field_name): + return True + else: + raise ValueError(f"Invalid ignore option: {rule}") + + return False diff --git a/graphene/validators/tests/__init__.py b/graphene/validators/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/validators/tests/test_depth_limit_validator.py b/graphene/validators/tests/test_depth_limit_validator.py new file mode 100644 index 00000000..ea50c8d4 --- /dev/null +++ b/graphene/validators/tests/test_depth_limit_validator.py @@ -0,0 +1,279 @@ +import re + +from pytest import raises +from graphql import parse, get_introspection_query, validate + +from ...types import Schema, ObjectType, Interface +from ...types import String, Int, List, Field +from ..depth_limit_validator import depth_limit_validator + + +class PetType(Interface): + name = String(required=True) + + class meta: + name = "Pet" + + +class CatType(ObjectType): + class meta: + name = "Cat" + interfaces = (PetType,) + + +class DogType(ObjectType): + class meta: + name = "Dog" + interfaces = (PetType,) + + +class AddressType(ObjectType): + street = String(required=True) + number = Int(required=True) + city = String(required=True) + country = String(required=True) + + class Meta: + name = "Address" + + +class HumanType(ObjectType): + name = String(required=True) + email = String(required=True) + address = Field(AddressType, required=True) + pets = List(PetType, required=True) + + class Meta: + name = "Human" + + +class Query(ObjectType): + user = Field( + HumanType, + required=True, + name=String() + ) + version = String( + required=True + ) + user1 = Field( + HumanType, + required=True + ) + user2 = Field( + HumanType, + required=True + ) + user3 = Field( + HumanType, + required=True + ) + + @staticmethod + def resolve_user(root, info, name=None): + pass + + +schema = Schema(query=Query) + + +def run_query(query: str, max_depth: int, ignore=None): + document = parse(query) + + result = None + + def callback(query_depths): + nonlocal result + result = query_depths + + errors = validate( + schema.graphql_schema, + document, + rules=( + depth_limit_validator( + max_depth=max_depth, + ignore=ignore, + callback=callback + ), + ), + ) + + return errors, result + + +def test_should_count_depth_without_fragment(): + query = """ + query read0 { + version + } + query read1 { + version + user { + name + } + } + query read2 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + pets { + name + owner { + name + } + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_count_with_fragments(): + query = """ + query read0 { + ... on Query { + version + } + } + query read1 { + version + user { + ... on Human { + name + } + } + } + fragment humanInfo on Human { + email + } + fragment petInfo on Pet { + name + owner { + name + } + } + query read2 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ...humanInfo + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ... on Human { + email + } + address { + city + } + pets { + ...petInfo + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_ignore_the_introspection_query(): + errors, result = run_query(get_introspection_query(), 10) + assert not errors + assert result == {"IntrospectionQuery": 0} + + +def test_should_catch_very_deep_query(): + query = """{ + user { + pets { + owner { + pets { + owner { + pets { + name + } + } + } + } + } + } + } + """ + errors, result = run_query(query, 4) + + assert len(errors) == 1 + assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" + + +def test_should_ignore_field(): + query = """ + query read1 { + user { address { city } } + } + query read2 { + user1 { address { city } } + user2 { address { city } } + user3 { address { city } } + } + """ + + errors, result = run_query( + query, + 10, + ignore=[ + "user1", + re.compile("user2"), + lambda field_name: field_name == "user3", + ], + ) + + expected = {"read1": 2, "read2": 0} + assert not errors + assert result == expected + + +def test_should_raise_invalid_ignore(): + query = """ + query read1 { + user { address { city } } + } + """ + with raises(ValueError, match="Invalid ignore option:"): + run_query( + query, + 10, + ignore=[True], + ) From fc2967e276bb78a4b388feaa091c2f9bc1f31ca2 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:51:23 +0530 Subject: [PATCH 18/87] remove unused imports --- graphene/validators/depth_limit_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/validators/depth_limit_validator.py b/graphene/validators/depth_limit_validator.py index 43615205..d2589700 100644 --- a/graphene/validators/depth_limit_validator.py +++ b/graphene/validators/depth_limit_validator.py @@ -28,7 +28,7 @@ import re from typing import Callable, Dict, List, Optional, Union -from graphql import GraphQLError, is_introspection_type +from graphql import GraphQLError from graphql.language import ( DefinitionNode, FieldNode, From 4259502dc373c8d3a4d6463696441e4bd5a0cc68 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 20:02:20 +0530 Subject: [PATCH 19/87] update docs --- docs/execution/validators.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index 94bb7c2f..f1cfac88 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -1,4 +1,4 @@ -Middleware +Validators ========== Validation rules help validate a given GraphQL query, before executing it.To help with common use From 5977b1648ce75730a3a494aeeef9df43fc5f2330 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 20:04:42 +0530 Subject: [PATCH 20/87] fix typo --- docs/execution/validators.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index f1cfac88..a37c80ab 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -1,7 +1,7 @@ Validators ========== -Validation rules help validate a given GraphQL query, before executing it.To help with common use +Validation rules help validate a given GraphQL query, before executing it. To help with common use cases, graphene provides a few validation rules out of the box. From a784ef15e59851afa804162a200b9c80a11c200c Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 20:24:53 +0530 Subject: [PATCH 21/87] add disable introspection --- docs/execution/validators.rst | 2 +- graphene/utils/is_introspection_key.py | 6 + graphene/validation/__init__.py | 8 + .../depth_limit.py} | 10 +- graphene/validation/disable_introspection.py | 22 ++ .../tests/__init__.py | 0 .../tests/test_disable_introspection.py | 33 +++ graphene/validators/__init__.py | 6 - .../tests/test_depth_limit_validator.py | 279 ------------------ 9 files changed, 74 insertions(+), 292 deletions(-) create mode 100644 graphene/utils/is_introspection_key.py create mode 100644 graphene/validation/__init__.py rename graphene/{validators/depth_limit_validator.py => validation/depth_limit.py} (94%) create mode 100644 graphene/validation/disable_introspection.py rename graphene/{validators => validation}/tests/__init__.py (100%) create mode 100644 graphene/validation/tests/test_disable_introspection.py delete mode 100644 graphene/validators/__init__.py delete mode 100644 graphene/validators/tests/test_depth_limit_validator.py diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index a37c80ab..92b8ecd2 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -20,7 +20,7 @@ Example Here is how you would implement depth-limiting on your schema. .. code:: python - from graphene.validators import depth_limit_validator + from graphene.validation import depth_limit_validator # The following schema doesn't execute queries # which have a depth more than 20. diff --git a/graphene/utils/is_introspection_key.py b/graphene/utils/is_introspection_key.py new file mode 100644 index 00000000..68951940 --- /dev/null +++ b/graphene/utils/is_introspection_key.py @@ -0,0 +1,6 @@ +def is_introspection_key(key): + # from: https://spec.graphql.org/June2018/#sec-Schema + # > All types and directives defined within a schema must not have a name which + # > begins with "__" (two underscores), as this is used exclusively + # > by GraphQL’s introspection system. + return str(node.name.value).startswith("__") diff --git a/graphene/validation/__init__.py b/graphene/validation/__init__.py new file mode 100644 index 00000000..03e4605c --- /dev/null +++ b/graphene/validation/__init__.py @@ -0,0 +1,8 @@ +from .depth_limit import depth_limit_validator +from .disable_introspection import disable_introspection + + +__all__ = [ + "depth_limit_validator", + "disable_introspection" +] diff --git a/graphene/validators/depth_limit_validator.py b/graphene/validation/depth_limit.py similarity index 94% rename from graphene/validators/depth_limit_validator.py rename to graphene/validation/depth_limit.py index d2589700..4136555d 100644 --- a/graphene/validators/depth_limit_validator.py +++ b/graphene/validation/depth_limit.py @@ -29,6 +29,7 @@ import re from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError +from graphql.validation import ValidationContext, ValidationRule from graphql.language import ( DefinitionNode, FieldNode, @@ -38,7 +39,8 @@ from graphql.language import ( Node, OperationDefinitionNode, ) -from graphql.validation import ValidationContext, ValidationRule + +from ..utils.is_introspection_key import is_introspection_key IgnoreType = Union[Callable[[str], bool], re.Pattern, str] @@ -121,11 +123,7 @@ def determine_depth( return depth_so_far if isinstance(node, FieldNode): - # from: https://spec.graphql.org/June2018/#sec-Schema - # > All types and directives defined within a schema must not have a name which - # > begins with "__" (two underscores), as this is used exclusively - # > by GraphQL’s introspection system. - should_ignore = str(node.name.value).startswith("__") or is_ignored( + should_ignore = is_introspection_key(node.name.value) or is_ignored( node, ignore ) diff --git a/graphene/validation/disable_introspection.py b/graphene/validation/disable_introspection.py new file mode 100644 index 00000000..eb24be55 --- /dev/null +++ b/graphene/validation/disable_introspection.py @@ -0,0 +1,22 @@ +from graphql import GraphQLError +from graphql.language import FieldNode +from graphql.validation import ValidationRule + +from ..utils.is_introspection_key import is_introspection_key + + +def disable_introspection(): + class DisableIntrospection(ValidationRule): + def enter_field(self, node: FieldNode, *_args): + field_name = node.name.value + if not is_introspection_key(field_name): + return + + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': introspection is disabled.", + node, + ) + ) + + return DisableIntrospection diff --git a/graphene/validators/tests/__init__.py b/graphene/validation/tests/__init__.py similarity index 100% rename from graphene/validators/tests/__init__.py rename to graphene/validation/tests/__init__.py diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py new file mode 100644 index 00000000..4d1faa7d --- /dev/null +++ b/graphene/validation/tests/test_disable_introspection.py @@ -0,0 +1,33 @@ +from graphql import parse, validate + +from ...types import Schema, ObjectType, String +from ..disable_introspection import disable_introspection + + +class Query(ObjectType): + name = String( + required=True + ) + + +schema = Schema(query=Query) + + +def run_query(query: str): + document = parse(query) + + result = None + + def callback(query_depths): + nonlocal result + result = query_depths + + errors = validate( + schema.graphql_schema, + document, + rules=( + disable_introspection(), + ), + ) + + return errors, result diff --git a/graphene/validators/__init__.py b/graphene/validators/__init__.py deleted file mode 100644 index 8bd8d884..00000000 --- a/graphene/validators/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .depth_limit_validator import depth_limit_validator - - -__all__ = [ - "depth_limit_validator" -] diff --git a/graphene/validators/tests/test_depth_limit_validator.py b/graphene/validators/tests/test_depth_limit_validator.py deleted file mode 100644 index ea50c8d4..00000000 --- a/graphene/validators/tests/test_depth_limit_validator.py +++ /dev/null @@ -1,279 +0,0 @@ -import re - -from pytest import raises -from graphql import parse, get_introspection_query, validate - -from ...types import Schema, ObjectType, Interface -from ...types import String, Int, List, Field -from ..depth_limit_validator import depth_limit_validator - - -class PetType(Interface): - name = String(required=True) - - class meta: - name = "Pet" - - -class CatType(ObjectType): - class meta: - name = "Cat" - interfaces = (PetType,) - - -class DogType(ObjectType): - class meta: - name = "Dog" - interfaces = (PetType,) - - -class AddressType(ObjectType): - street = String(required=True) - number = Int(required=True) - city = String(required=True) - country = String(required=True) - - class Meta: - name = "Address" - - -class HumanType(ObjectType): - name = String(required=True) - email = String(required=True) - address = Field(AddressType, required=True) - pets = List(PetType, required=True) - - class Meta: - name = "Human" - - -class Query(ObjectType): - user = Field( - HumanType, - required=True, - name=String() - ) - version = String( - required=True - ) - user1 = Field( - HumanType, - required=True - ) - user2 = Field( - HumanType, - required=True - ) - user3 = Field( - HumanType, - required=True - ) - - @staticmethod - def resolve_user(root, info, name=None): - pass - - -schema = Schema(query=Query) - - -def run_query(query: str, max_depth: int, ignore=None): - document = parse(query) - - result = None - - def callback(query_depths): - nonlocal result - result = query_depths - - errors = validate( - schema.graphql_schema, - document, - rules=( - depth_limit_validator( - max_depth=max_depth, - ignore=ignore, - callback=callback - ), - ), - ) - - return errors, result - - -def test_should_count_depth_without_fragment(): - query = """ - query read0 { - version - } - query read1 { - version - user { - name - } - } - query read2 { - matt: user(name: "matt") { - email - } - andy: user(name: "andy") { - email - address { - city - } - } - } - query read3 { - matt: user(name: "matt") { - email - } - andy: user(name: "andy") { - email - address { - city - } - pets { - name - owner { - name - } - } - } - } - """ - - expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} - - errors, result = run_query(query, 10) - assert not errors - assert result == expected - - -def test_should_count_with_fragments(): - query = """ - query read0 { - ... on Query { - version - } - } - query read1 { - version - user { - ... on Human { - name - } - } - } - fragment humanInfo on Human { - email - } - fragment petInfo on Pet { - name - owner { - name - } - } - query read2 { - matt: user(name: "matt") { - ...humanInfo - } - andy: user(name: "andy") { - ...humanInfo - address { - city - } - } - } - query read3 { - matt: user(name: "matt") { - ...humanInfo - } - andy: user(name: "andy") { - ... on Human { - email - } - address { - city - } - pets { - ...petInfo - } - } - } - """ - - expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} - - errors, result = run_query(query, 10) - assert not errors - assert result == expected - - -def test_should_ignore_the_introspection_query(): - errors, result = run_query(get_introspection_query(), 10) - assert not errors - assert result == {"IntrospectionQuery": 0} - - -def test_should_catch_very_deep_query(): - query = """{ - user { - pets { - owner { - pets { - owner { - pets { - name - } - } - } - } - } - } - } - """ - errors, result = run_query(query, 4) - - assert len(errors) == 1 - assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" - - -def test_should_ignore_field(): - query = """ - query read1 { - user { address { city } } - } - query read2 { - user1 { address { city } } - user2 { address { city } } - user3 { address { city } } - } - """ - - errors, result = run_query( - query, - 10, - ignore=[ - "user1", - re.compile("user2"), - lambda field_name: field_name == "user3", - ], - ) - - expected = {"read1": 2, "read2": 0} - assert not errors - assert result == expected - - -def test_should_raise_invalid_ignore(): - query = """ - query read1 { - user { address { city } } - } - """ - with raises(ValueError, match="Invalid ignore option:"): - run_query( - query, - 10, - ignore=[True], - ) From d7b474751d59b9c94279283295057f224b7688a7 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 07:45:34 +0530 Subject: [PATCH 22/87] add depth limit validator tests --- docs/execution/validators.rst | 24 +- graphene/utils/is_introspection_key.py | 2 +- .../tests/test_depth_limit_validator.py | 279 ++++++++++++++++++ .../tests/test_disable_introspection.py | 4 - 4 files changed, 297 insertions(+), 12 deletions(-) create mode 100644 graphene/validation/tests/test_depth_limit_validator.py diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index 92b8ecd2..d7e1310b 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -20,16 +20,26 @@ Example Here is how you would implement depth-limiting on your schema. .. code:: python + from graphql import validate + from graphene import ObjectType, Schema, String from graphene.validation import depth_limit_validator - # The following schema doesn't execute queries - # which have a depth more than 20. - result = schema.execute( - 'THE QUERY', - validation_rules=[ + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + # Queries which have a depth more than 20 + # will not be executed. + + validation_errors = validate( + schema=schema, + document='THE QUERY', + rules=( depth_limit_validator( max_depth=20 - ) - ] + ), + ) ) diff --git a/graphene/utils/is_introspection_key.py b/graphene/utils/is_introspection_key.py index 68951940..59d72b24 100644 --- a/graphene/utils/is_introspection_key.py +++ b/graphene/utils/is_introspection_key.py @@ -3,4 +3,4 @@ def is_introspection_key(key): # > All types and directives defined within a schema must not have a name which # > begins with "__" (two underscores), as this is used exclusively # > by GraphQL’s introspection system. - return str(node.name.value).startswith("__") + return str(key).startswith("__") diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py new file mode 100644 index 00000000..3eea3a32 --- /dev/null +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -0,0 +1,279 @@ +import re + +from pytest import raises +from graphql import parse, get_introspection_query, validate + +from ...types import Schema, ObjectType, Interface +from ...types import String, Int, List, Field +from ..depth_limit import depth_limit_validator + + +class PetType(Interface): + name = String(required=True) + + class meta: + name = "Pet" + + +class CatType(ObjectType): + class meta: + name = "Cat" + interfaces = (PetType,) + + +class DogType(ObjectType): + class meta: + name = "Dog" + interfaces = (PetType,) + + +class AddressType(ObjectType): + street = String(required=True) + number = Int(required=True) + city = String(required=True) + country = String(required=True) + + class Meta: + name = "Address" + + +class HumanType(ObjectType): + name = String(required=True) + email = String(required=True) + address = Field(AddressType, required=True) + pets = List(PetType, required=True) + + class Meta: + name = "Human" + + +class Query(ObjectType): + user = Field( + HumanType, + required=True, + name=String() + ) + version = String( + required=True + ) + user1 = Field( + HumanType, + required=True + ) + user2 = Field( + HumanType, + required=True + ) + user3 = Field( + HumanType, + required=True + ) + + @staticmethod + def resolve_user(root, info, name=None): + pass + + +schema = Schema(query=Query) + + +def run_query(query: str, max_depth: int, ignore=None): + document = parse(query) + + result = None + + def callback(query_depths): + nonlocal result + result = query_depths + + errors = validate( + schema.graphql_schema, + document, + rules=( + depth_limit_validator( + max_depth=max_depth, + ignore=ignore, + callback=callback + ), + ), + ) + + return errors, result + + +def test_should_count_depth_without_fragment(): + query = """ + query read0 { + version + } + query read1 { + version + user { + name + } + } + query read2 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + pets { + name + owner { + name + } + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_count_with_fragments(): + query = """ + query read0 { + ... on Query { + version + } + } + query read1 { + version + user { + ... on Human { + name + } + } + } + fragment humanInfo on Human { + email + } + fragment petInfo on Pet { + name + owner { + name + } + } + query read2 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ...humanInfo + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ... on Human { + email + } + address { + city + } + pets { + ...petInfo + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_ignore_the_introspection_query(): + errors, result = run_query(get_introspection_query(), 10) + assert not errors + assert result == {"IntrospectionQuery": 0} + + +def test_should_catch_very_deep_query(): + query = """{ + user { + pets { + owner { + pets { + owner { + pets { + name + } + } + } + } + } + } + } + """ + errors, result = run_query(query, 4) + + assert len(errors) == 1 + assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" + + +def test_should_ignore_field(): + query = """ + query read1 { + user { address { city } } + } + query read2 { + user1 { address { city } } + user2 { address { city } } + user3 { address { city } } + } + """ + + errors, result = run_query( + query, + 10, + ignore=[ + "user1", + re.compile("user2"), + lambda field_name: field_name == "user3", + ], + ) + + expected = {"read1": 2, "read2": 0} + assert not errors + assert result == expected + + +def test_should_raise_invalid_ignore(): + query = """ + query read1 { + user { address { city } } + } + """ + with raises(ValueError, match="Invalid ignore option:"): + run_query( + query, + 10, + ignore=[True], + ) diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index 4d1faa7d..c13786ed 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -18,10 +18,6 @@ def run_query(query: str): result = None - def callback(query_depths): - nonlocal result - result = query_depths - errors = validate( schema.graphql_schema, document, From 7be4bd6bc6a916f2c4f2ecc7cf184064dc2c8f19 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 07:49:09 +0530 Subject: [PATCH 23/87] update docs --- docs/execution/index.rst | 2 +- docs/execution/{validators.rst => validation.rst} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/execution/{validators.rst => validation.rst} (99%) diff --git a/docs/execution/index.rst b/docs/execution/index.rst index 46652665..f775cc00 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -10,4 +10,4 @@ Execution dataloader fileuploading subscriptions - validators + validation diff --git a/docs/execution/validators.rst b/docs/execution/validation.rst similarity index 99% rename from docs/execution/validators.rst rename to docs/execution/validation.rst index d7e1310b..ac27ec43 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validation.rst @@ -1,4 +1,4 @@ -Validators +Validation ========== Validation rules help validate a given GraphQL query, before executing it. To help with common use From ac5dd90f5fc610a37af7fc27efd022d15ebe821f Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 07:54:58 +0530 Subject: [PATCH 24/87] fix typo in docs --- docs/execution/validation.rst | 4 ++-- graphene/validation/tests/test_depth_limit_validator.py | 4 ++-- graphene/validation/tests/test_disable_introspection.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/execution/validation.rst b/docs/execution/validation.rst index ac27ec43..7be4fd66 100644 --- a/docs/execution/validation.rst +++ b/docs/execution/validation.rst @@ -20,7 +20,7 @@ Example Here is how you would implement depth-limiting on your schema. .. code:: python - from graphql import validate + from graphql import validate, parse from graphene import ObjectType, Schema, String from graphene.validation import depth_limit_validator @@ -36,7 +36,7 @@ Here is how you would implement depth-limiting on your schema. validation_errors = validate( schema=schema, - document='THE QUERY', + document_ast=parse('THE QUERY'), rules=( depth_limit_validator( max_depth=20 diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py index 3eea3a32..ea62f999 100644 --- a/graphene/validation/tests/test_depth_limit_validator.py +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -87,8 +87,8 @@ def run_query(query: str, max_depth: int, ignore=None): result = query_depths errors = validate( - schema.graphql_schema, - document, + schema=schema.graphql_schema, + document_ast=document, rules=( depth_limit_validator( max_depth=max_depth, diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index c13786ed..b7f0b83f 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -19,8 +19,8 @@ def run_query(query: str): result = None errors = validate( - schema.graphql_schema, - document, + schema=schema.graphql_schema, + document_ast=document, rules=( disable_introspection(), ), From c68071952da8da3e6ca9d125587e626a8d02d8cd Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 08:20:46 +0530 Subject: [PATCH 25/87] mention how to implement custom validators --- docs/execution/index.rst | 2 +- docs/execution/queryvalidation.rst | 87 ++++++++++++++++++++++++++++++ docs/execution/validation.rst | 45 ---------------- 3 files changed, 88 insertions(+), 46 deletions(-) create mode 100644 docs/execution/queryvalidation.rst delete mode 100644 docs/execution/validation.rst diff --git a/docs/execution/index.rst b/docs/execution/index.rst index f775cc00..f26259d3 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -10,4 +10,4 @@ Execution dataloader fileuploading subscriptions - validation + queryvalidation diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst new file mode 100644 index 00000000..35f3577e --- /dev/null +++ b/docs/execution/queryvalidation.rst @@ -0,0 +1,87 @@ +Query Validation +========== +GraphQL uses query validators to check if Query AST is valid and can be executed. Every GraphQL server implements +standard query validators. For example, there is an validator that tests if queried field exists on queried type, that +makes query fail with "Cannot query field on type" error if it doesn't. + +To help with common use cases, graphene provides a few validation rules out of the box. + + +Depth limit Validator +----------------- +The depth limit validator helps to prevent execution of malicious +queries. It takes in the following arguments. + +- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document. +- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean +- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. + +Example +------- + +Here is how you would implement depth-limiting on your schema. + +.. code:: python + from graphql import validate, parse + from graphene import ObjectType, Schema, String + from graphene.validation import depth_limit_validator + + + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + # Queries which have a depth more than 20 + # will not be executed. + + validation_errors = validate( + schema=schema, + document_ast=parse('THE QUERY'), + rules=( + depth_limit_validator( + max_depth=20 + ), + ) + ) + + +Implementing custom validators +------------------------------ +All custom query validators should extend the `ValidationRule `_ +base class importable from the graphql.validation.rules module. Query validators are visitor classes. They are +instantiated at the time of query validation with one required argument (context: ASTValidationContext). In order to +perform validation, your validator class should define one or more of enter_* and leave_* methods. For possible +enter/leave items as well as details on function documentation, please see contents of the visitor module. To make +validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure +reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation +if any of those fields are introspection fields: + +.. code:: python + from graphql import GraphQLError + from graphql.language import FieldNode + from graphql.validation import ValidationRule + + + my_blacklist = ( + "disallowed_field", + ) + + + def is_blacklisted_field(field_name: str): + return key.lower() in my_blacklist + + + class BlackListRule(ValidationRule): + def enter_field(self, node: FieldNode, *_args): + field_name = node.name.value + if not is_blacklisted_field(field_name): + return + + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': field is blacklisted.", node, + ) + ) + diff --git a/docs/execution/validation.rst b/docs/execution/validation.rst deleted file mode 100644 index 7be4fd66..00000000 --- a/docs/execution/validation.rst +++ /dev/null @@ -1,45 +0,0 @@ -Validation -========== - -Validation rules help validate a given GraphQL query, before executing it. To help with common use -cases, graphene provides a few validation rules out of the box. - - -Depth limit Validator ------------------ -The depth limit validator helps to prevent execution of malicious -queries. It takes in the following arguments. - -- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document. -- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean -- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. - -Example -------- - -Here is how you would implement depth-limiting on your schema. - -.. code:: python - from graphql import validate, parse - from graphene import ObjectType, Schema, String - from graphene.validation import depth_limit_validator - - - class MyQuery(ObjectType): - name = String(required=True) - - - schema = Schema(query=MyQuery) - - # Queries which have a depth more than 20 - # will not be executed. - - validation_errors = validate( - schema=schema, - document_ast=parse('THE QUERY'), - rules=( - depth_limit_validator( - max_depth=20 - ), - ) - ) From ec982ac50b2c79dc956f4e59fde5cf40092af0d8 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 08:22:04 +0530 Subject: [PATCH 26/87] update docs typo --- docs/execution/queryvalidation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst index 35f3577e..2d58f7ab 100644 --- a/docs/execution/queryvalidation.rst +++ b/docs/execution/queryvalidation.rst @@ -56,7 +56,7 @@ perform validation, your validator class should define one or more of enter_* an enter/leave items as well as details on function documentation, please see contents of the visitor module. To make validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation -if any of those fields are introspection fields: +if any of those fields are blacklisted fields: .. code:: python from graphql import GraphQLError From 4e32dac25118e8b043601d61229a1cacfda9cbf6 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 08:41:24 +0530 Subject: [PATCH 27/87] add tests and docs for disable introspection rule --- docs/execution/queryvalidation.rst | 41 +++++++++++++++++-- graphene/validation/__init__.py | 6 +-- graphene/validation/depth_limit.py | 6 +-- graphene/validation/disable_introspection.py | 23 +++++------ .../tests/test_depth_limit_validator.py | 2 +- .../tests/test_disable_introspection.py | 24 ++++++++--- 6 files changed, 73 insertions(+), 29 deletions(-) diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst index 2d58f7ab..8402b9ea 100644 --- a/docs/execution/queryvalidation.rst +++ b/docs/execution/queryvalidation.rst @@ -16,7 +16,7 @@ queries. It takes in the following arguments. - ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean - ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. -Example +Usage ------- Here is how you would implement depth-limiting on your schema. @@ -33,7 +33,7 @@ Here is how you would implement depth-limiting on your schema. schema = Schema(query=MyQuery) - # Queries which have a depth more than 20 + # queries which have a depth more than 20 # will not be executed. validation_errors = validate( @@ -47,6 +47,39 @@ Here is how you would implement depth-limiting on your schema. ) +Disable Introspection +--------------------- +the disable introspection validation rule ensures that your schema cannot be introspected. +This is a useful security measure in production environments. + +Usage +------- + +Here is how you would disable introspection for your schema. + +.. code:: python + from graphql import validate, parse + from graphene import ObjectType, Schema, String + from graphene.validation import DisableIntrospection + + + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + # introspection queries will not be executed. + + validation_errors = validate( + schema=schema, + document_ast=parse('THE QUERY'), + rules=( + DisableIntrospection, + ) + ) + + Implementing custom validators ------------------------------ All custom query validators should extend the `ValidationRule `_ @@ -56,7 +89,7 @@ perform validation, your validator class should define one or more of enter_* an enter/leave items as well as details on function documentation, please see contents of the visitor module. To make validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation -if any of those fields are blacklisted fields: +if any of those fields are blacklisted: .. code:: python from graphql import GraphQLError @@ -70,7 +103,7 @@ if any of those fields are blacklisted fields: def is_blacklisted_field(field_name: str): - return key.lower() in my_blacklist + return field_name.lower() in my_blacklist class BlackListRule(ValidationRule): diff --git a/graphene/validation/__init__.py b/graphene/validation/__init__.py index 03e4605c..f338e2d0 100644 --- a/graphene/validation/__init__.py +++ b/graphene/validation/__init__.py @@ -1,8 +1,8 @@ from .depth_limit import depth_limit_validator -from .disable_introspection import disable_introspection +from .disable_introspection import DisableIntrospection __all__ = [ - "depth_limit_validator", - "disable_introspection" + "DisableIntrospection", + "depth_limit_validator" ] diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 4136555d..8363a6c9 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -116,7 +116,7 @@ def determine_depth( if depth_so_far > max_depth: context.report_error( GraphQLError( - f"'{operation_name}' exceeds maximum operation depth of {max_depth}", + f"'{operation_name}' exceeds maximum operation depth of {max_depth}.", [node], ) ) @@ -172,7 +172,7 @@ def determine_depth( ) ) else: - raise Exception(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover + raise Exception(f"Depth crawler cannot handle: {node.kind}.") # pragma: no cover def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool: @@ -191,6 +191,6 @@ def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bo if rule(field_name): return True else: - raise ValueError(f"Invalid ignore option: {rule}") + raise ValueError(f"Invalid ignore option: {rule}.") return False diff --git a/graphene/validation/disable_introspection.py b/graphene/validation/disable_introspection.py index eb24be55..4c83050e 100644 --- a/graphene/validation/disable_introspection.py +++ b/graphene/validation/disable_introspection.py @@ -5,18 +5,15 @@ from graphql.validation import ValidationRule from ..utils.is_introspection_key import is_introspection_key -def disable_introspection(): - class DisableIntrospection(ValidationRule): - def enter_field(self, node: FieldNode, *_args): - field_name = node.name.value - if not is_introspection_key(field_name): - return +class DisableIntrospection(ValidationRule): + def enter_field(self, node: FieldNode, *_args): + field_name = node.name.value + if not is_introspection_key(field_name): + return - self.report_error( - GraphQLError( - f"Cannot query '{field_name}': introspection is disabled.", - node, - ) + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': introspection is disabled.", + node, ) - - return DisableIntrospection + ) diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py index ea62f999..499adbcc 100644 --- a/graphene/validation/tests/test_depth_limit_validator.py +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -235,7 +235,7 @@ def test_should_catch_very_deep_query(): errors, result = run_query(query, 4) assert len(errors) == 1 - assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" + assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4." def test_should_ignore_field(): diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index b7f0b83f..06019900 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -1,7 +1,7 @@ from graphql import parse, validate from ...types import Schema, ObjectType, String -from ..disable_introspection import disable_introspection +from ..disable_introspection import DisableIntrospection class Query(ObjectType): @@ -9,6 +9,10 @@ class Query(ObjectType): required=True ) + @staticmethod + def resolve_name(root, info): + return "Hello world!" + schema = Schema(query=Query) @@ -16,14 +20,24 @@ schema = Schema(query=Query) def run_query(query: str): document = parse(query) - result = None - errors = validate( schema=schema.graphql_schema, document_ast=document, rules=( - disable_introspection(), + DisableIntrospection, ), ) - return errors, result + return errors + + +def test_disallows_introspection_queries(): + errors = run_query("{ __schema { queryType { name } } }") + + assert len(errors) == 1 + assert errors[0].message == "Cannot query '__schema': introspection is disabled." + + +def test_allows_non_introspection_queries(): + errors = run_query("{ name }") + assert len(errors) == 0 From b4be4a686bd2d5279433dc77346b279f13d3f1e3 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:59:58 +0530 Subject: [PATCH 28/87] add notice to failing tests --- graphene/types/tests/test_schema.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index 54c48b4f..f84d2e20 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -175,6 +175,11 @@ class TestUnforgivingExecutionContext: ], ) def test_unexpected_error(self, field, exception, schema): + # FIXME: tests are failing currently because no exception + # is being raised below. Instead, the errors are being propagated + # to the `errors` array of the response. If this is intended + # behaviour, we need to check if the error exists in the `errors` + # array rather than checking if an exception is raised. with raises(exception): # no result, but the exception should be propagated schema.execute( From 467b1f8e8d30d59a1422bbfe2805e20145f0a7fd Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 12:03:27 +0530 Subject: [PATCH 29/87] add workflow: tests --- .github/{ => workflows}/stale.yml | 0 .github/workflows/tests.yml | 85 +++++++++++++++++++ ...mmit-config.yaml => .pre-commit-config.yml | 0 .travis.yml | 42 --------- 4 files changed, 85 insertions(+), 42 deletions(-) rename .github/{ => workflows}/stale.yml (100%) create mode 100644 .github/workflows/tests.yml rename .pre-commit-config.yaml => .pre-commit-config.yml (100%) delete mode 100644 .travis.yml diff --git a/.github/stale.yml b/.github/workflows/stale.yml similarity index 100% rename from .github/stale.yml rename to .github/workflows/stale.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..cf8b8719 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,85 @@ +name: 📄 Tests +on: + push: + branches: + - master + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' + pull_request: + branches: + - master + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' +jobs: + tests: + # runs the test suite + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - {name: '3.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 + with: + python-version: ${{ matrix.python }} + + - name: update pip + run: | + pip install -U wheel + pip install -U setuptools + python -m pip install -U pip + + - name: get pip cache dir + id: pip-cache + run: echo "::set-output name=dir::$(pip cache dir)" + + - name: cache pip + uses: actions/cache@v2 + 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 }} + + coveralls: + # check coverage increase/decrease + needs: tests + runs-on: ${{ matrix.os }} + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true + + deploy: + # builds and publishes to PyPi + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yml similarity index 100% rename from .pre-commit-config.yaml rename to .pre-commit-config.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e1e55119..00000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -language: python -dist: xenial - -python: - - "3.6" - - "3.7" - - "3.8" - -install: - - pip install tox tox-travis -script: tox -after_success: - - pip install coveralls - - coveralls -cache: - directories: - - $HOME/.cache/pip - - $HOME/.cache/pre-commit - -stages: - - test - - name: deploy - if: tag IS present - -jobs: - fast_finish: true - include: - - env: TOXENV=pre-commit - python: 3.7 - - env: TOXENV=mypy - python: 3.7 - - stage: deploy - python: 3.7 - after_success: true - deploy: - provider: pypi - user: syrusakbary - on: - tags: true - password: - secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ= - distributions: "sdist bdist_wheel" From c0ddbbfaf4fd4a777834dec7662f8ce9d85ffb50 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 12:13:46 +0530 Subject: [PATCH 30/87] update workflow matrix --- .github/workflows/tests.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cf8b8719..b7cb3fdb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,6 +57,10 @@ jobs: # check coverage increase/decrease needs: tests runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - { os: ubuntu-latest } steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop @@ -66,12 +70,16 @@ jobs: deploy: # builds and publishes to PyPi runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - { python: '3.7', os: ubuntu-latest } steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip From 8ae436915575f2efefdef2e289cd04d667c9c6a8 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 12:16:13 +0530 Subject: [PATCH 31/87] remove build matrix wherever not needed --- .github/workflows/tests.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b7cb3fdb..39f15ef4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,11 +56,7 @@ jobs: coveralls: # check coverage increase/decrease needs: tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - { os: ubuntu-latest } + runs-on: "ubuntu-latest" steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop @@ -69,17 +65,13 @@ jobs: deploy: # builds and publishes to PyPi - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - { python: '3.7', os: ubuntu-latest } + runs-on: "ubuntu-latest" steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python }} + python-version: "3.7" - name: Install dependencies run: | python -m pip install --upgrade pip From 0e4c14b0767627c504cc0f0adee9a21824fd05a6 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 15:00:09 +0530 Subject: [PATCH 32/87] update workflow: tests --- .github/workflows/tests.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39f15ef4..6de43f37 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: id: pip-cache run: echo "::set-output name=dir::$(pip cache dir)" - - name: cache pip + - name: cache pip dependencies uses: actions/cache@v2 with: path: ${{ steps.pip-cache.outputs.dir }} @@ -53,25 +53,23 @@ jobs: - run: pip install tox - run: tox -e ${{ matrix.tox }} - coveralls: + coveralls_finish: # check coverage increase/decrease needs: tests - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop - with: - parallel-finished: true deploy: # builds and publishes to PyPi - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: "3.7" + python-version: '3.7' - name: Install dependencies run: | python -m pip install --upgrade pip From 7d890bf91521a3e7905e95f442a4e934a68603fb Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 19 Aug 2021 14:02:45 -0500 Subject: [PATCH 33/87] Update graphene/validation/disable_introspection.py --- graphene/validation/disable_introspection.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/graphene/validation/disable_introspection.py b/graphene/validation/disable_introspection.py index 4c83050e..be25a287 100644 --- a/graphene/validation/disable_introspection.py +++ b/graphene/validation/disable_introspection.py @@ -8,12 +8,10 @@ from ..utils.is_introspection_key import is_introspection_key class DisableIntrospection(ValidationRule): def enter_field(self, node: FieldNode, *_args): field_name = node.name.value - if not is_introspection_key(field_name): - return - - self.report_error( - GraphQLError( - f"Cannot query '{field_name}': introspection is disabled.", - node, + if is_introspection_key(field_name): + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': introspection is disabled.", + node, + ) ) - ) From 946c2a3807d8970deee4f51eed07144349f1dde3 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 15:58:43 +0530 Subject: [PATCH 34/87] Update schema.py --- graphene/types/schema.py | 236 --------------------------------------- 1 file changed, 236 deletions(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 99532354..9d3c8be5 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -391,239 +391,3 @@ class TypeMap(dict): return graphql_type return type_ - - -class UnforgivingExecutionContext(ExecutionContext): - """An execution context which doesn't swallow exceptions. - - The only difference between this execution context and the one it inherits from is - that ``except Exception`` is commented out within ``resolve_field_value_or_error``. - By removing that exception handling, only ``GraphQLError``'s are caught. - """ - - def resolve_field_value_or_error( - self, field_def, field_nodes, resolve_fn, source, info - ): - """Resolve field to a value or an error. - - Isolates the "ReturnOrAbrupt" behavior to not de-opt the resolve_field() - method. Returns the result of resolveFn or the abrupt-return Error object. - - For internal use only. - """ - try: - # Build a dictionary of arguments from the field.arguments AST, using the - # variables scope to fulfill any variable references. - args = get_argument_values(field_def, field_nodes[0], self.variable_values) - - # Note that contrary to the JavaScript implementation, we pass the context - # value as part of the resolve info. - result = resolve_fn(source, info, **args) - if self.is_awaitable(result): - # noinspection PyShadowingNames - async def await_result(): - try: - return await result - except GraphQLError as error: - return error - # except Exception as error: - # return GraphQLError(str(error), original_error=error) - - # Yes, this is commented out code. It's been intentionally - # _not_ removed to show what has changed from the original - # implementation. - - return await_result() - return result - except GraphQLError as error: - return error - # except Exception as error: - # return GraphQLError(str(error), original_error=error) - - # Yes, this is commented out code. It's been intentionally _not_ - # removed to show what has changed from the original implementation. - - def complete_value_catching_error( - self, return_type, field_nodes, info, path, result - ): - """Complete a value while catching an error. - - This is a small wrapper around completeValue which detects and logs errors in - the execution context. - """ - try: - if self.is_awaitable(result): - - async def await_result(): - value = self.complete_value( - return_type, field_nodes, info, path, await result - ) - if self.is_awaitable(value): - return await value - return value - - completed = await_result() - else: - completed = self.complete_value( - return_type, field_nodes, info, path, result - ) - if self.is_awaitable(completed): - # noinspection PyShadowingNames - async def await_completed(): - try: - return await completed - - # CHANGE WAS MADE HERE - # ``GraphQLError`` was swapped in for ``except Exception`` - except GraphQLError as error: - self.handle_field_error(error, field_nodes, path, return_type) - - return await_completed() - return completed - - # CHANGE WAS MADE HERE - # ``GraphQLError`` was swapped in for ``except Exception`` - except GraphQLError as error: - self.handle_field_error(error, field_nodes, path, return_type) - return None - - -class Schema: - """Schema Definition. - - A Graphene Schema can execute operations (query, mutation, subscription) against the defined - types. For advanced purposes, the schema can be used to lookup type definitions and answer - questions about the types through introspection. - - Args: - query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* - data in your Schema. - mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for - fields to *create, update or delete* data in your API. - subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point - for fields to receive continuous updates. - types (Optional[List[Type[ObjectType]]]): List of any types to include in schema that - may not be introspected through root types. - directives (List[GraphQLDirective], optional): List of custom directives to include in the - GraphQL schema. Defaults to only include directives defined by GraphQL spec (@include - and @skip) [GraphQLIncludeDirective, GraphQLSkipDirective]. - auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case - to camelCase (preferred by GraphQL standard). Default True. - """ - - def __init__( - self, - query=None, - mutation=None, - subscription=None, - types=None, - directives=None, - auto_camelcase=True, - ): - self.query = query - self.mutation = mutation - self.subscription = subscription - type_map = TypeMap( - query, mutation, subscription, types, auto_camelcase=auto_camelcase - ) - self.graphql_schema = GraphQLSchema( - type_map.query, - type_map.mutation, - type_map.subscription, - type_map.types, - directives, - ) - - def __str__(self): - return print_schema(self.graphql_schema) - - def __getattr__(self, type_name): - """ - This function let the developer select a type in a given schema - by accessing its attrs. - - Example: using schema.Query for accessing the "Query" type in the Schema - """ - _type = self.graphql_schema.get_type(type_name) - if _type is None: - raise AttributeError(f'Type "{type_name}" not found in the Schema') - if isinstance(_type, GrapheneGraphQLType): - return _type.graphene_type - return _type - - def lazy(self, _type): - return lambda: self.get_type(_type) - - def execute(self, *args, **kwargs): - """Execute a GraphQL query on the schema. - - Use the `graphql_sync` function from `graphql-core` to provide the result - for a query string. Most of the time this method will be called by one of the Graphene - :ref:`Integrations` via a web request. - - Args: - request_string (str or Document): GraphQL request (query, mutation or subscription) - as string or parsed AST form from `graphql-core`. - root_value (Any, optional): Value to use as the parent value object when resolving - root types. - context_value (Any, optional): Value to be made available to all resolvers via - `info.context`. Can be used to share authorization, dataloaders or other - information needed to resolve an operation. - variable_values (dict, optional): If variables are used in the request string, they can - be provided in dictionary form mapping the variable name to the variable value. - operation_name (str, optional): If multiple operations are provided in the - request_string, an operation name must be provided for the result to be provided. - middleware (List[SupportsGraphQLMiddleware]): Supply request level middleware as - defined in `graphql-core`. - execution_context_class (ExecutionContext, optional): The execution context class - to use when resolving queries and mutations. - - Returns: - :obj:`ExecutionResult` containing any data and errors for the operation. - """ - kwargs = normalize_execute_kwargs(kwargs) - return graphql_sync(self.graphql_schema, *args, **kwargs) - - async def execute_async(self, *args, **kwargs): - """Execute a GraphQL query on the schema asynchronously. - - Same as `execute`, but uses `graphql` instead of `graphql_sync`. - """ - kwargs = normalize_execute_kwargs(kwargs) - return await graphql(self.graphql_schema, *args, **kwargs) - - async def subscribe(self, query, *args, **kwargs): - """Execute a GraphQL subscription on the schema asynchronously.""" - # Do parsing - try: - document = parse(query) - except GraphQLError as error: - return ExecutionResult(data=None, errors=[error]) - - # Do validation - validation_errors = validate(self.graphql_schema, document) - if validation_errors: - return ExecutionResult(data=None, errors=validation_errors) - - # Execute the query - kwargs = normalize_execute_kwargs(kwargs) - return await subscribe(self.graphql_schema, document, *args, **kwargs) - - def introspect(self): - introspection = self.execute(introspection_query) - if introspection.errors: - raise introspection.errors[0] - return introspection.data - - -def normalize_execute_kwargs(kwargs): - """Replace alias names in keyword arguments for graphql()""" - if "root" in kwargs and "root_value" not in kwargs: - kwargs["root_value"] = kwargs.pop("root") - if "context" in kwargs and "context_value" not in kwargs: - kwargs["context_value"] = kwargs.pop("context") - if "variables" in kwargs and "variable_values" not in kwargs: - kwargs["variable_values"] = kwargs.pop("variables") - if "operation" in kwargs and "operation_name" not in kwargs: - kwargs["operation_name"] = kwargs.pop("operation") - return kwargs From 18cd3451f9715f3db900a64fd288b00b6706c003 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 15:59:38 +0530 Subject: [PATCH 35/87] Update test_schema.py --- graphene/types/tests/test_schema.py | 119 +--------------------------- 1 file changed, 1 insertion(+), 118 deletions(-) diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index f84d2e20..9cdbde3b 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -7,7 +7,7 @@ from graphene.tests.utils import dedent from ..field import Field from ..objecttype import ObjectType from ..scalars import String -from ..schema import Schema, UnforgivingExecutionContext +from ..schema import Schema class MyOtherType(ObjectType): @@ -69,120 +69,3 @@ def test_schema_requires_query_type(): assert len(result.errors) == 1 error = result.errors[0] assert error.message == "Query root type must be provided." - - -class TestUnforgivingExecutionContext: - @fixture - def schema(self): - class ErrorFieldsMixin: - sanity_field = String() - expected_error_field = String() - unexpected_value_error_field = String() - unexpected_type_error_field = String() - unexpected_attribute_error_field = String() - unexpected_key_error_field = String() - - @staticmethod - def resolve_sanity_field(obj, info): - return "not an error" - - @staticmethod - def resolve_expected_error_field(obj, info): - raise GraphQLError("expected error") - - @staticmethod - def resolve_unexpected_value_error_field(obj, info): - raise ValueError("unexpected error") - - @staticmethod - def resolve_unexpected_type_error_field(obj, info): - raise TypeError("unexpected error") - - @staticmethod - def resolve_unexpected_attribute_error_field(obj, info): - raise AttributeError("unexpected error") - - @staticmethod - def resolve_unexpected_key_error_field(obj, info): - return {}["fails"] - - class NestedObject(ErrorFieldsMixin, ObjectType): - pass - - class MyQuery(ErrorFieldsMixin, ObjectType): - nested_object = Field(NestedObject) - nested_object_error = Field(NestedObject) - - @staticmethod - def resolve_nested_object(obj, info): - return object() - - @staticmethod - def resolve_nested_object_error(obj, info): - raise TypeError() - - schema = Schema(query=MyQuery) - return schema - - def test_sanity_check(self, schema): - # this should pass with no errors (sanity check) - result = schema.execute( - "query { sanityField }", - execution_context_class=UnforgivingExecutionContext, - ) - assert not result.errors - assert result.data == {"sanityField": "not an error"} - - def test_nested_sanity_check(self, schema): - # this should pass with no errors (sanity check) - result = schema.execute( - r"query { nestedObject { sanityField } }", - execution_context_class=UnforgivingExecutionContext, - ) - assert not result.errors - assert result.data == {"nestedObject": {"sanityField": "not an error"}} - - def test_graphql_error(self, schema): - result = schema.execute( - "query { expectedErrorField }", - execution_context_class=UnforgivingExecutionContext, - ) - assert len(result.errors) == 1 - assert result.errors[0].message == "expected error" - assert result.data == {"expectedErrorField": None} - - def test_nested_graphql_error(self, schema): - result = schema.execute( - r"query { nestedObject { expectedErrorField } }", - execution_context_class=UnforgivingExecutionContext, - ) - assert len(result.errors) == 1 - assert result.errors[0].message == "expected error" - assert result.data == {"nestedObject": {"expectedErrorField": None}} - - @mark.parametrize( - "field,exception", - [ - ("unexpectedValueErrorField", ValueError), - ("unexpectedTypeErrorField", TypeError), - ("unexpectedAttributeErrorField", AttributeError), - ("unexpectedKeyErrorField", KeyError), - ("nestedObject { unexpectedValueErrorField }", ValueError), - ("nestedObject { unexpectedTypeErrorField }", TypeError), - ("nestedObject { unexpectedAttributeErrorField }", AttributeError), - ("nestedObject { unexpectedKeyErrorField }", KeyError), - ("nestedObjectError { __typename }", TypeError), - ], - ) - def test_unexpected_error(self, field, exception, schema): - # FIXME: tests are failing currently because no exception - # is being raised below. Instead, the errors are being propagated - # to the `errors` array of the response. If this is intended - # behaviour, we need to check if the error exists in the `errors` - # array rather than checking if an exception is raised. - with raises(exception): - # no result, but the exception should be propagated - schema.execute( - f"query {{ {field} }}", - execution_context_class=UnforgivingExecutionContext, - ) From ea4e6d65e9db41c69da9a23ecaaceaecb084054a Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:08:58 +0530 Subject: [PATCH 36/87] Update schema.py --- graphene/types/schema.py | 134 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 9d3c8be5..1ff0bff4 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -391,3 +391,137 @@ class TypeMap(dict): return graphql_type return type_ + + +class Schema: + """Schema Definition. + A Graphene Schema can execute operations (query, mutation, subscription) against the defined + types. For advanced purposes, the schema can be used to lookup type definitions and answer + questions about the types through introspection. + Args: + query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* + data in your Schema. + mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for + fields to *create, update or delete* data in your API. + subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point + for fields to receive continuous updates. + types (Optional[List[Type[ObjectType]]]): List of any types to include in schema that + may not be introspected through root types. + directives (List[GraphQLDirective], optional): List of custom directives to include in the + GraphQL schema. Defaults to only include directives defined by GraphQL spec (@include + and @skip) [GraphQLIncludeDirective, GraphQLSkipDirective]. + auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case + to camelCase (preferred by GraphQL standard). Default True. + """ + + def __init__( + self, + query=None, + mutation=None, + subscription=None, + types=None, + directives=None, + auto_camelcase=True, + ): + self.query = query + self.mutation = mutation + self.subscription = subscription + type_map = TypeMap( + query, mutation, subscription, types, auto_camelcase=auto_camelcase + ) + self.graphql_schema = GraphQLSchema( + type_map.query, + type_map.mutation, + type_map.subscription, + type_map.types, + directives, + ) + + def __str__(self): + return print_schema(self.graphql_schema) + + def __getattr__(self, type_name): + """ + This function let the developer select a type in a given schema + by accessing its attrs. + Example: using schema.Query for accessing the "Query" type in the Schema + """ + _type = self.graphql_schema.get_type(type_name) + if _type is None: + raise AttributeError(f'Type "{type_name}" not found in the Schema') + if isinstance(_type, GrapheneGraphQLType): + return _type.graphene_type + return _type + + def lazy(self, _type): + return lambda: self.get_type(_type) + + def execute(self, *args, **kwargs): + """Execute a GraphQL query on the schema. + Use the `graphql_sync` function from `graphql-core` to provide the result + for a query string. Most of the time this method will be called by one of the Graphene + :ref:`Integrations` via a web request. + Args: + request_string (str or Document): GraphQL request (query, mutation or subscription) + as string or parsed AST form from `graphql-core`. + root_value (Any, optional): Value to use as the parent value object when resolving + root types. + context_value (Any, optional): Value to be made available to all resolvers via + `info.context`. Can be used to share authorization, dataloaders or other + information needed to resolve an operation. + variable_values (dict, optional): If variables are used in the request string, they can + be provided in dictionary form mapping the variable name to the variable value. + operation_name (str, optional): If multiple operations are provided in the + request_string, an operation name must be provided for the result to be provided. + middleware (List[SupportsGraphQLMiddleware]): Supply request level middleware as + defined in `graphql-core`. + execution_context_class (ExecutionContext, optional): The execution context class + to use when resolving queries and mutations. + Returns: + :obj:`ExecutionResult` containing any data and errors for the operation. + """ + kwargs = normalize_execute_kwargs(kwargs) + return graphql_sync(self.graphql_schema, *args, **kwargs) + + async def execute_async(self, *args, **kwargs): + """Execute a GraphQL query on the schema asynchronously. + Same as `execute`, but uses `graphql` instead of `graphql_sync`. + """ + kwargs = normalize_execute_kwargs(kwargs) + return await graphql(self.graphql_schema, *args, **kwargs) + + async def subscribe(self, query, *args, **kwargs): + """Execute a GraphQL subscription on the schema asynchronously.""" + # Do parsing + try: + document = parse(query) + except GraphQLError as error: + return ExecutionResult(data=None, errors=[error]) + + # Do validation + validation_errors = validate(self.graphql_schema, document) + if validation_errors: + return ExecutionResult(data=None, errors=validation_errors) + + # Execute the query + kwargs = normalize_execute_kwargs(kwargs) + return await subscribe(self.graphql_schema, document, *args, **kwargs) + + def introspect(self): + introspection = self.execute(introspection_query) + if introspection.errors: + raise introspection.errors[0] + return introspection.data + + +def normalize_execute_kwargs(kwargs): + """Replace alias names in keyword arguments for graphql()""" + if "root" in kwargs and "root_value" not in kwargs: + kwargs["root_value"] = kwargs.pop("root") + if "context" in kwargs and "context_value" not in kwargs: + kwargs["context_value"] = kwargs.pop("context") + if "variables" in kwargs and "variable_values" not in kwargs: + kwargs["variable_values"] = kwargs.pop("variables") + if "operation" in kwargs and "operation_name" not in kwargs: + kwargs["operation_name"] = kwargs.pop("operation") + return kwargs From 57a4394bf3b149a16e36259a8b26cbe5aadc6970 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 20:56:19 +0530 Subject: [PATCH 37/87] Update depth_limit.py --- graphene/validation/depth_limit.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 8363a6c9..0a95aeae 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -25,7 +25,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import re +try: + from re import Pattern +except ImportError: + # backwards compatibility for v3.6 + from typing import Pattern + from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError @@ -43,7 +48,7 @@ from graphql.language import ( from ..utils.is_introspection_key import is_introspection_key -IgnoreType = Union[Callable[[str], bool], re.Pattern, str] +IgnoreType = Union[Callable[[str], bool], Pattern, str] def depth_limit_validator( From 98980b53f6032c186d94982ebf4d87b4a3bf5f80 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 21:04:22 +0530 Subject: [PATCH 38/87] Update depth_limit.py --- graphene/validation/depth_limit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 0a95aeae..47a04403 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -31,6 +31,7 @@ except ImportError: # backwards compatibility for v3.6 from typing import Pattern +import re from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError From 74a6565ea3f77f68758b099291fde9544d10d03f Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 21:07:57 +0530 Subject: [PATCH 39/87] Update depth_limit.py --- graphene/validation/depth_limit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 47a04403..c72b78d0 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -31,7 +31,6 @@ except ImportError: # backwards compatibility for v3.6 from typing import Pattern -import re from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError @@ -190,7 +189,7 @@ def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bo if isinstance(rule, str): if field_name == rule: return True - elif isinstance(rule, re.Pattern): + elif isinstance(rule, Pattern): if rule.match(field_name): return True elif callable(rule): From 3c50fa817af3b0d8205f1fb26186ae4dbf6dccf5 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:14:52 +0530 Subject: [PATCH 40/87] Delete stale.yml --- .github/workflows/stale.yml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 322a3eda..00000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: false -# Number of days of inactivity before a stale issue is closed -daysUntilClose: false -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - - 🐛 bug - - 📖 documentation - - 🙋 help wanted - - ✨ enhancement - - good first issue - - work in progress -# Label to use when marking an issue as stale -staleLabel: wontfix -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: false -# markComment: > - # This issue has been automatically marked as stale because it has not had - # recent activity. It will be closed if no further activity occurs. Thank you - # for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false From e1822c9ae97f2a76949b5cb1de98ae826ac8efcf Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:15:28 +0530 Subject: [PATCH 41/87] Create stale.yml --- .github/stale.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..322a3eda --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,24 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: false +# Number of days of inactivity before a stale issue is closed +daysUntilClose: false +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - 🐛 bug + - 📖 documentation + - 🙋 help wanted + - ✨ enhancement + - good first issue + - work in progress +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: false +# markComment: > + # This issue has been automatically marked as stale because it has not had + # recent activity. It will be closed if no further activity occurs. Thank you + # for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From 16551369b2adc6650014ae4a167fb9a7baa585e7 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:16:43 +0530 Subject: [PATCH 42/87] Update tests.yml --- .github/workflows/tests.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6de43f37..cdc4d01e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,32 +52,3 @@ jobs: - run: pip install tox - run: tox -e ${{ matrix.tox }} - - coveralls_finish: - # check coverage increase/decrease - needs: tests - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop - - deploy: - # builds and publishes to PyPi - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} From dc6b820635f38396e7b65226ca4c60ba67070e82 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:19:53 +0530 Subject: [PATCH 43/87] Create coveralls.yml --- .github/workflows/coveralls.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/coveralls.yml diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml new file mode 100644 index 00000000..c26a8975 --- /dev/null +++ b/.github/workflows/coveralls.yml @@ -0,0 +1,27 @@ + +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 + needs: tests + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop From 0aef168687f4621e670ca6a8a66b2410f7976aea Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:21:48 +0530 Subject: [PATCH 44/87] Create deploy.yml --- .github/workflows/deploy.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..2a6cdc6b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,26 @@ +name: 🚀 Deploy to PyPI + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Build wheel and source tarball + run: | + pip install wheel + python setup.py sdist bdist_wheel + - name: Publish a Python distribution to PyPI + uses: pypa/gh-action-pypi-publish@v1.1.0 + with: + user: __token__ + password: ${{ secrets.pypi_password }} From 772986ac8362500805be8b0ab6c58abc5e63af47 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:23:27 +0530 Subject: [PATCH 45/87] Create lint.yml --- lint.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 lint.yml diff --git a/lint.yml b/lint.yml new file mode 100644 index 00000000..95251d9b --- /dev/null +++ b/lint.yml @@ -0,0 +1,26 @@ +name: 💅 Lint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Run lint + run: tox + env: + TOXENV: pre-commit + - name: Run mypy + run: tox + env: + TOXENV: mypy From 1654d2fa29e42e9dc438279cfcebe4e32255ec90 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:24:11 +0530 Subject: [PATCH 46/87] Update coveralls.yml --- .github/workflows/coveralls.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index c26a8975..c2f9f3cc 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -1,4 +1,3 @@ - name: 📊 Check Coverage on: push: From e66d6148ab471fca40ae3a34ec2a480091a5fe6b Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:25:38 +0530 Subject: [PATCH 47/87] Create lint.yml --- .github/workflows/lint.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..95251d9b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: 💅 Lint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Run lint + run: tox + env: + TOXENV: pre-commit + - name: Run mypy + run: tox + env: + TOXENV: mypy From 9807d6102ce8a7757b67a472548f26a521f9a8b4 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:40:08 +0530 Subject: [PATCH 48/87] Update coveralls.yml --- .github/workflows/coveralls.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index c2f9f3cc..a8e2875c 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -19,7 +19,6 @@ on: jobs: coveralls_finish: # check coverage increase/decrease - needs: tests runs-on: ubuntu-latest steps: - name: Coveralls Finished From 7960b02124a2638cfd7144f77a2dd2606c042518 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:41:12 +0530 Subject: [PATCH 49/87] Delete lint.yml --- lint.yml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 lint.yml diff --git a/lint.yml b/lint.yml deleted file mode 100644 index 95251d9b..00000000 --- a/lint.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: 💅 Lint - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - name: Run lint - run: tox - env: - TOXENV: pre-commit - - name: Run mypy - run: tox - env: - TOXENV: mypy From 314554338655c68ade79a24a6f9a54fff5a6d562 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:46:14 +0530 Subject: [PATCH 50/87] Update tox.ini --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index dd922c46..6a3166f4 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ commands = py{36,37,38}: pytest --cov=graphene graphene examples {posargs} [testenv:pre-commit] -basepython=python3.7 +basepython=python3.8 deps = pre-commit>=2,<3 setenv = @@ -20,16 +20,16 @@ commands = pre-commit {posargs:run --all-files} [testenv:mypy] -basepython=python3.7 +basepython=python3.8 deps = mypy>=0.761,<1 commands = mypy graphene [testenv:flake8] -basepython=python3.7 +basepython=python3.8 deps = - flake8>=3.7,<4 + flake8>=3.8,<4 commands = pip install --pre -e . flake8 graphene From ce59f1ff15a32b0b4b90b96a4fb466970cf1c6aa Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:48:33 +0530 Subject: [PATCH 51/87] Rename .pre-commit-config.yml to .pre-commit-config.yaml --- .pre-commit-config.yml => .pre-commit-config.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .pre-commit-config.yml => .pre-commit-config.yaml (100%) diff --git a/.pre-commit-config.yml b/.pre-commit-config.yaml similarity index 100% rename from .pre-commit-config.yml rename to .pre-commit-config.yaml From 7827219ba2c7c20858351ecbdc74ba041fdd587f Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 18:29:56 +0530 Subject: [PATCH 52/87] Update schema.py --- graphene/types/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 1ff0bff4..79341d83 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -27,7 +27,6 @@ from graphql import ( GraphQLSchema, GraphQLString, ) -from graphql.execution import ExecutionContext from graphql.execution.values import get_argument_values from ..utils.str_converters import to_camel_case From 0ebff3313d985126cdf23e9b26d200d1a5902e1e Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 18:30:42 +0530 Subject: [PATCH 53/87] Update test_schema.py --- graphene/types/tests/test_schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index 9cdbde3b..fe4739c9 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,6 +1,5 @@ from graphql.type import GraphQLObjectType, GraphQLSchema -from graphql import GraphQLError -from pytest import mark, raises, fixture +from pytest import raises from graphene.tests.utils import dedent From 1886ec9dcbea27a944fd8864f5b530bacdab957d Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 18:34:39 +0530 Subject: [PATCH 54/87] Update schema.py --- graphene/types/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 79341d83..0c6d4183 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -27,7 +27,6 @@ from graphql import ( GraphQLSchema, GraphQLString, ) -from graphql.execution.values import get_argument_values from ..utils.str_converters import to_camel_case from ..utils.get_unbound_function import get_unbound_function From d54b81955225156db6a2d39a58769ba528b9e784 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 18:37:44 +0530 Subject: [PATCH 55/87] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9ffc21e..f2e50e5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,6 @@ repos: - id: black language_version: python3 - repo: https://github.com/PyCQA/flake8 - rev: 3.7.8 + rev: 3.8.4 hooks: - id: flake8 From 16d0b32a8fdcaf3b7960677b3ae11bb28cd8be4a Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 20:21:46 +0530 Subject: [PATCH 56/87] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2e50e5e..be667020 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: git://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v2.3.0 hooks: - id: check-merge-conflict - id: check-json @@ -18,7 +18,7 @@ repos: hooks: - id: pyupgrade - repo: https://github.com/ambv/black - rev: 19.10b0 + rev: 19.3b0 hooks: - id: black language_version: python3 From 3b77b5f92a049baee54d4a797163069693498bb4 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 20:36:41 +0530 Subject: [PATCH 57/87] Update tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6a3166f4..ff9973f7 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps = setenv = LC_CTYPE=en_US.UTF-8 commands = - pre-commit {posargs:run --all-files} + pre-commit run --all-files [testenv:mypy] basepython=python3.8 From 76701e0809d20ef93a7b266d4f701378ba163e57 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 20:53:58 +0530 Subject: [PATCH 58/87] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be667020..7f782135 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,8 +20,8 @@ repos: - repo: https://github.com/ambv/black rev: 19.3b0 hooks: - - id: black - language_version: python3 + - id: black + language_version: python3 - repo: https://github.com/PyCQA/flake8 rev: 3.8.4 hooks: From 5896ade2dd50058bc25ab3b73e93eff6b6a01dfa Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 20:58:18 +0530 Subject: [PATCH 59/87] Update test_connection_query.py --- graphene/relay/tests/test_connection_query.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index cac4b65b..8226febc 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -51,10 +51,10 @@ letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter def edges(selected_letters): return [ { - "node": {"id": base64("Letter:%s" % l.id), "letter": l.letter}, - "cursor": base64("arrayconnection:%s" % l.id), + "node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter}, + "cursor": base64("arrayconnection:%s" % letter.id), } - for l in [letters[i] for i in selected_letters] + for letter in [letters[i] for i in selected_letters] ] From 1c3054b7c8099b800671403856e90cdc455ab577 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 21:01:27 +0530 Subject: [PATCH 60/87] Update test_connection_async.py --- graphene/relay/tests/test_connection_async.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene/relay/tests/test_connection_async.py b/graphene/relay/tests/test_connection_async.py index b139f6a3..ae228cf9 100644 --- a/graphene/relay/tests/test_connection_async.py +++ b/graphene/relay/tests/test_connection_async.py @@ -51,10 +51,10 @@ letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter def edges(selected_letters): return [ { - "node": {"id": base64("Letter:%s" % l.id), "letter": l.letter}, - "cursor": base64("arrayconnection:%s" % l.id), + "node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter}, + "cursor": base64("arrayconnection:%s" % letter.id), } - for l in [letters[i] for i in selected_letters] + for letter in [letters[i] for i in selected_letters] ] From 7087710d025f940bd542f264e9f3493208a6a3d4 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 21:17:00 +0530 Subject: [PATCH 61/87] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f782135..f0e353b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: trailing-whitespace exclude: README.md - repo: https://github.com/asottile/pyupgrade - rev: v1.12.0 + rev: v2.24.0 hooks: - id: pyupgrade - repo: https://github.com/ambv/black From a3a2f999aadf385ec54c6ec8793536daf6588a1c Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 21:33:00 +0530 Subject: [PATCH 62/87] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0e353b4..58edab7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,16 +13,16 @@ repos: - --autofix - id: trailing-whitespace exclude: README.md -- repo: https://github.com/asottile/pyupgrade +- repo: git://github.com/asottile/pyupgrade rev: v2.24.0 hooks: - id: pyupgrade -- repo: https://github.com/ambv/black +- repo: git://github.com/ambv/black rev: 19.3b0 hooks: - id: black language_version: python3 -- repo: https://github.com/PyCQA/flake8 +- repo: git://github.com/PyCQA/flake8 rev: 3.8.4 hooks: - id: flake8 From 85f06fb2a6e9286860ea8f6a21cefce5108cb245 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 21:37:30 +0530 Subject: [PATCH 63/87] Update tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ff9973f7..c4bf6ad0 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps = setenv = LC_CTYPE=en_US.UTF-8 commands = - pre-commit run --all-files + pre-commit run --all-files --show-diff-on-failure [testenv:mypy] basepython=python3.8 From d5d7a0e5e079e0a67d2236dd5c0b0b4e6419232c Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sat, 21 Aug 2021 21:41:47 +0530 Subject: [PATCH 64/87] Update tox.ini --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index c4bf6ad0..693f5e69 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ deps = setenv = LC_CTYPE=en_US.UTF-8 commands = + pre-commit install pre-commit run --all-files --show-diff-on-failure [testenv:mypy] From 908d5aeaeb9a565307b0cea7bd2842737a748334 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sun, 22 Aug 2021 08:48:15 +0530 Subject: [PATCH 65/87] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58edab7b..bd6a7340 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +default_language_version: + python: python3.8 + repos: - repo: git://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 @@ -21,7 +24,6 @@ repos: rev: 19.3b0 hooks: - id: black - language_version: python3 - repo: git://github.com/PyCQA/flake8 rev: 3.8.4 hooks: From 2c66e496f7e0f3b85f244b04223aa7d87ba96ad9 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Sun, 22 Aug 2021 08:48:38 +0530 Subject: [PATCH 66/87] Update tox.ini --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 693f5e69..c4bf6ad0 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,6 @@ deps = setenv = LC_CTYPE=en_US.UTF-8 commands = - pre-commit install pre-commit run --all-files --show-diff-on-failure [testenv:mypy] From 2e5944eb2023179714d213bcf38d44a945b180b1 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan Date: Sun, 22 Aug 2021 11:03:22 +0530 Subject: [PATCH 67/87] format code --- graphene/pyutils/dataclasses.py | 40 +++---------------- graphene/relay/tests/test_connection_query.py | 13 +++--- graphene/types/mutation.py | 9 +---- graphene/types/objecttype.py | 11 +++-- graphene/types/tests/test_base64.py | 3 +- graphene/validation/__init__.py | 5 +-- graphene/validation/depth_limit.py | 12 ++---- .../tests/test_depth_limit_validator.py | 29 +++----------- .../tests/test_disable_introspection.py | 8 +--- 9 files changed, 33 insertions(+), 97 deletions(-) diff --git a/graphene/pyutils/dataclasses.py b/graphene/pyutils/dataclasses.py index 19530eff..f847b211 100644 --- a/graphene/pyutils/dataclasses.py +++ b/graphene/pyutils/dataclasses.py @@ -442,13 +442,11 @@ def _field_init(f, frozen, globals, self_name): # This field does not need initialization. Signify that # to the caller by returning None. return None - # Only test this now, so that we can create variables for the # default. However, return None to signify that we're not going # to actually do the assignment statement for InitVars. if f._field_type == _FIELD_INITVAR: return None - # Now, actually generate the field assignment. return _field_assign(frozen, f.name, value, self_name) @@ -490,7 +488,6 @@ def _init_fn(fields, frozen, has_post_init, self_name): raise TypeError( f"non-default argument {f.name!r} " "follows default argument" ) - globals = {"MISSING": MISSING, "_HAS_DEFAULT_FACTORY": _HAS_DEFAULT_FACTORY} body_lines = [] @@ -500,16 +497,13 @@ def _init_fn(fields, frozen, has_post_init, self_name): # initialization (it's a pseudo-field). Just skip it. if line: body_lines.append(line) - # Does this class have a post-init function? if has_post_init: params_str = ",".join(f.name for f in fields if f._field_type is _FIELD_INITVAR) body_lines.append(f"{self_name}.{_POST_INIT_NAME}({params_str})") - # If no body lines, use 'pass'. if not body_lines: body_lines = ["pass"] - locals = {f"_type_{f.name}": f.type for f in fields} return _create_fn( "__init__", @@ -674,7 +668,6 @@ def _get_field(cls, a_name, a_type): # This is a field in __slots__, so it has no default value. default = MISSING f = field(default=default) - # Only at this point do we know the name and the type. Set them. f.name = a_name f.type = a_type @@ -705,7 +698,6 @@ def _get_field(cls, a_name, a_type): and _is_type(f.type, cls, typing, typing.ClassVar, _is_classvar) ): f._field_type = _FIELD_CLASSVAR - # If the type is InitVar, or if it's a matching string annotation, # then it's an InitVar. if f._field_type is _FIELD: @@ -717,7 +709,6 @@ def _get_field(cls, a_name, a_type): and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, _is_initvar) ): f._field_type = _FIELD_INITVAR - # Validations for individual fields. This is delayed until now, # instead of in the Field() constructor, since only here do we # know the field name, which allows for better error reporting. @@ -731,14 +722,12 @@ def _get_field(cls, a_name, a_type): # example, how about init=False (or really, # init=)? It makes no sense for # ClassVar and InitVar to specify init=. - # For real fields, disallow mutable defaults for known types. if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)): raise ValueError( f"mutable default {type(f.default)} for field " f"{f.name} is not allowed: use default_factory" ) - return f @@ -827,7 +816,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): fields[f.name] = f if getattr(b, _PARAMS).frozen: any_frozen_base = True - # Annotations that are defined in this class (not in base # classes). If __annotations__ isn't present, then this class # adds no new annotations. We use this to compute fields that are @@ -866,22 +854,18 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): delattr(cls, f.name) else: setattr(cls, f.name, f.default) - # Do we have any Field members that don't also have annotations? for name, value in cls.__dict__.items(): if isinstance(value, Field) and not name in cls_annotations: raise TypeError(f"{name!r} is a field but has no type annotation") - # Check rules that apply if we are derived from any dataclasses. if has_dataclass_bases: # Raise an exception if any of our bases are frozen, but we're not. if any_frozen_base and not frozen: raise TypeError("cannot inherit non-frozen dataclass from a " "frozen one") - # Raise an exception if we're frozen, but none of our bases are. if not any_frozen_base and frozen: raise TypeError("cannot inherit frozen dataclass from a " "non-frozen one") - # Remember all of the fields on our class (including bases). This # also marks this class as being a dataclass. setattr(cls, _FIELDS, fields) @@ -900,7 +884,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # eq methods. if order and not eq: raise ValueError("eq must be true if order is true") - if init: # Does this class have a post-init function? has_post_init = hasattr(cls, _POST_INIT_NAME) @@ -920,7 +903,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): "__dataclass_self__" if "self" in fields else "self", ), ) - # Get the fields as a list, and include only real fields. This is # used in all of the following methods. field_list = [f for f in fields.values() if f._field_type is _FIELD] @@ -928,7 +910,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): if repr: flds = [f for f in field_list if f.repr] _set_new_attribute(cls, "__repr__", _repr_fn(flds)) - if eq: # Create _eq__ method. There's no need for a __ne__ method, # since python will call __eq__ and negate it. @@ -938,7 +919,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): _set_new_attribute( cls, "__eq__", _cmp_fn("__eq__", "==", self_tuple, other_tuple) ) - if order: # Create and set the ordering methods. flds = [f for f in field_list if f.compare] @@ -958,7 +938,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): f"in class {cls.__name__}. Consider using " "functools.total_ordering" ) - if frozen: for fn in _frozen_get_del_attr(cls, field_list): if _set_new_attribute(cls, fn.__name__, fn): @@ -966,7 +945,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): f"Cannot overwrite attribute {fn.__name__} " f"in class {cls.__name__}" ) - # Decide if/how we're going to create a hash function. hash_action = _hash_action[ bool(unsafe_hash), bool(eq), bool(frozen), has_explicit_hash @@ -975,11 +953,9 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # No need to call _set_new_attribute here, since by the time # we're here the overwriting is unconditional. cls.__hash__ = hash_action(cls, field_list) - if not getattr(cls, "__doc__"): # Create a class doc-string. cls.__doc__ = cls.__name__ + str(inspect.signature(cls)).replace(" -> None", "") - return cls @@ -1015,7 +991,6 @@ def dataclass( if _cls is None: # We're called with parens. return wrap - # We're called as @dataclass without parens. return wrap(_cls) @@ -1032,7 +1007,6 @@ def fields(class_or_instance): fields = getattr(class_or_instance, _FIELDS) except AttributeError: raise TypeError("must be called with a dataclass type or instance") - # Exclude pseudo-fields. Note that fields is sorted by insertion # order, so the order of the tuple is as the fields were defined. return tuple(f for f in fields.values() if f._field_type is _FIELD) @@ -1174,7 +1148,6 @@ def make_dataclass( else: # Copy namespace since we're going to mutate it. namespace = namespace.copy() - # While we're looking through the field names, validate that they # are identifiers, are not keywords, and not duplicates. seen = set() @@ -1184,23 +1157,23 @@ def make_dataclass( name = item tp = "typing.Any" elif len(item) == 2: - name, tp, = item + ( + name, + tp, + ) = item elif len(item) == 3: name, tp, spec = item namespace[name] = spec else: raise TypeError(f"Invalid field: {item!r}") - if not isinstance(name, str) or not name.isidentifier(): raise TypeError(f"Field names must be valid identifers: {name!r}") if keyword.iskeyword(name): raise TypeError(f"Field names must not be keywords: {name!r}") if name in seen: raise TypeError(f"Field name duplicated: {name!r}") - seen.add(name) anns[name] = tp - namespace["__annotations__"] = anns # We use `types.new_class()` instead of simply `type()` to allow dynamic creation # of generic dataclassses. @@ -1229,14 +1202,13 @@ def replace(obj, **changes): c = C(1, 2) c1 = replace(c, x=3) assert c1.x == 3 and c1.y == 2 - """ + """ # We're going to mutate 'changes', but that's okay because it's a # new dict, even if called with 'replace(obj, **my_changes)'. if not _is_dataclass_instance(obj): raise TypeError("replace() should be called on dataclass instances") - # It's an error to have init=False fields in 'changes'. # If a field is not in 'changes', read its value from the provided obj. @@ -1250,10 +1222,8 @@ def replace(obj, **changes): "replace()" ) continue - if f.name not in changes: changes[f.name] = getattr(obj, f.name) - # Create the new object, which calls __init__() and # __post_init__() (if defined), using all of the init fields we've # added and/or left in 'changes'. If there are values supplied in diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 8226febc..42345e54 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -66,7 +66,6 @@ def cursor_for(ltr): async def execute(args=""): if args: args = "(" + args + ")" - return await schema.execute_async( """ { @@ -164,14 +163,16 @@ async def test_respects_first_and_after_and_before_too_few(): @mark.asyncio async def test_respects_first_and_after_and_before_too_many(): await check( - f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD", + f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', + "BCD", ) @mark.asyncio async def test_respects_first_and_after_and_before_exactly_right(): await check( - f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD", + f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', + "BCD", ) @@ -187,14 +188,16 @@ async def test_respects_last_and_after_and_before_too_few(): @mark.asyncio async def test_respects_last_and_after_and_before_too_many(): await check( - f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD", + f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', + "BCD", ) @mark.asyncio async def test_respects_last_and_after_and_before_exactly_right(): await check( - f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD", + f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', + "BCD", ) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 6e041bbf..7f98e312 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -76,7 +76,6 @@ class Mutation(ObjectType): ): if not _meta: _meta = MutationOptions(cls) - output = output or getattr(cls, "Output", None) fields = {} @@ -85,14 +84,12 @@ class Mutation(ObjectType): interface, Interface ), f'All interfaces of {cls.__name__} must be a subclass of Interface. Received "{interface}".' fields.update(interface._meta.fields) - if not output: # If output is defined, we don't need to get the fields fields = {} for base in reversed(cls.__mro__): fields.update(yank_fields_from_attrs(base.__dict__, _as=Field)) output = cls - if not arguments: input_class = getattr(cls, "Arguments", None) if not input_class: @@ -106,22 +103,18 @@ class Mutation(ObjectType): " 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 = {} - if not resolver: mutate = getattr(cls, "mutate", None) assert mutate, "All mutations must define a mutate method in it" resolver = get_unbound_function(mutate) - if _meta.fields: _meta.fields.update(fields) else: _meta.fields = fields - _meta.interfaces = interfaces _meta.output = output _meta.resolver = resolver @@ -133,7 +126,7 @@ class Mutation(ObjectType): def Field( cls, name=None, description=None, deprecation_reason=None, required=False ): - """ Mount instance of mutation Field. """ + """Mount instance of mutation Field.""" return Field( cls._meta.output, args=cls._meta.arguments, diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index c69be937..2b1902ea 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -7,7 +7,6 @@ try: from dataclasses import make_dataclass, field except ImportError: from ..pyutils.dataclasses import make_dataclass, field # type: ignore - # For static type checking with Mypy MYPY = False if MYPY: @@ -28,7 +27,11 @@ class ObjectTypeMeta(BaseTypeMeta): pass base_cls = super().__new__( - cls, name_, (InterObjectType,) + bases, namespace, **options, + cls, + name_, + (InterObjectType,) + bases, + namespace, + **options, ) if base_cls._meta: fields = [ @@ -133,7 +136,6 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta): ): if not _meta: _meta = ObjectTypeOptions(cls) - fields = {} for interface in interfaces: @@ -141,10 +143,8 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta): interface, Interface ), f'All interfaces of {cls.__name__} must be a subclass of Interface. Received "{interface}".' fields.update(interface._meta.fields) - for base in reversed(cls.__mro__): fields.update(yank_fields_from_attrs(base.__dict__, _as=Field)) - assert not (possible_types and cls.is_type_of), ( f"{cls.__name__}.Meta.possible_types will cause type collision with {cls.__name__}.is_type_of. " "Please use one or other." @@ -154,7 +154,6 @@ class ObjectType(BaseType, metaclass=ObjectTypeMeta): _meta.fields.update(fields) else: _meta.fields = fields - if not _meta.interfaces: _meta.interfaces = interfaces _meta.possible_types = possible_types diff --git a/graphene/types/tests/test_base64.py b/graphene/types/tests/test_base64.py index b096dcbc..d1b76cb4 100644 --- a/graphene/types/tests/test_base64.py +++ b/graphene/types/tests/test_base64.py @@ -72,7 +72,8 @@ def test_base64_query_invalid(): for input_ in bad_inputs: result = schema.execute( - """{ base64(input: $input) }""", variables={"input": input_}, + """{ base64(input: $input) }""", + variables={"input": input_}, ) assert isinstance(result.errors, list) assert len(result.errors) == 1 diff --git a/graphene/validation/__init__.py b/graphene/validation/__init__.py index f338e2d0..5b592a2c 100644 --- a/graphene/validation/__init__.py +++ b/graphene/validation/__init__.py @@ -2,7 +2,4 @@ from .depth_limit import depth_limit_validator from .disable_introspection import DisableIntrospection -__all__ = [ - "DisableIntrospection", - "depth_limit_validator" -] +__all__ = ["DisableIntrospection", "depth_limit_validator"] diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index c72b78d0..5be852c7 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -30,7 +30,6 @@ try: except ImportError: # backwards compatibility for v3.6 from typing import Pattern - from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError @@ -75,7 +74,6 @@ def depth_limit_validator( operation_name=name, ignore=ignore, ) - if callable(callback): callback(query_depths) super().__init__(validation_context) @@ -90,7 +88,6 @@ def get_fragments( for definition in definitions: if isinstance(definition, FragmentDefinitionNode): fragments[definition.name.value] = definition - return fragments @@ -105,7 +102,6 @@ def get_queries_and_mutations( if isinstance(definition, OperationDefinitionNode): operation = definition.name.value if definition.name else "anonymous" operations[operation] = definition - return operations @@ -126,7 +122,6 @@ def determine_depth( ) ) return depth_so_far - if isinstance(node, FieldNode): should_ignore = is_introspection_key(node.name.value) or is_ignored( node, ignore @@ -134,7 +129,6 @@ def determine_depth( if should_ignore or not node.selection_set: return 0 - return 1 + max( map( lambda selection: determine_depth( @@ -177,13 +171,14 @@ def determine_depth( ) ) else: - raise Exception(f"Depth crawler cannot handle: {node.kind}.") # pragma: no cover + raise Exception( + f"Depth crawler cannot handle: {node.kind}." + ) # pragma: no cover def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool: if ignore is None: return False - for rule in ignore: field_name = node.name.value if isinstance(rule, str): @@ -197,5 +192,4 @@ def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bo return True else: raise ValueError(f"Invalid ignore option: {rule}.") - return False diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py index 499adbcc..0c22089a 100644 --- a/graphene/validation/tests/test_depth_limit_validator.py +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -48,26 +48,11 @@ class HumanType(ObjectType): class Query(ObjectType): - user = Field( - HumanType, - required=True, - name=String() - ) - version = String( - required=True - ) - user1 = Field( - HumanType, - required=True - ) - user2 = Field( - HumanType, - required=True - ) - user3 = Field( - HumanType, - required=True - ) + user = Field(HumanType, required=True, name=String()) + version = String(required=True) + user1 = Field(HumanType, required=True) + user2 = Field(HumanType, required=True) + user3 = Field(HumanType, required=True) @staticmethod def resolve_user(root, info, name=None): @@ -91,9 +76,7 @@ def run_query(query: str, max_depth: int, ignore=None): document_ast=document, rules=( depth_limit_validator( - max_depth=max_depth, - ignore=ignore, - callback=callback + max_depth=max_depth, ignore=ignore, callback=callback ), ), ) diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index 06019900..958a1afa 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -5,9 +5,7 @@ from ..disable_introspection import DisableIntrospection class Query(ObjectType): - name = String( - required=True - ) + name = String(required=True) @staticmethod def resolve_name(root, info): @@ -23,9 +21,7 @@ def run_query(query: str): errors = validate( schema=schema.graphql_schema, document_ast=document, - rules=( - DisableIntrospection, - ), + rules=(DisableIntrospection,), ) return errors From 47696559c700c1314db3efcb52ed1f4986bc2b27 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Tue, 24 Aug 2021 08:30:54 +0530 Subject: [PATCH 68/87] run linters locally --- graphene/pyutils/dataclasses.py | 14 ++------------ graphene/relay/node.py | 8 +++----- graphene/relay/tests/test_connection_query.py | 12 ++++-------- graphene/types/mutation.py | 10 ++++------ graphene/types/objecttype.py | 6 +----- graphene/types/tests/test_base64.py | 9 ++------- graphene/types/tests/test_enum.py | 8 ++++---- graphene/utils/orderedtype.py | 2 +- graphene/validation/disable_introspection.py | 3 +-- .../validation/tests/test_depth_limit_validator.py | 12 ++---------- 10 files changed, 24 insertions(+), 60 deletions(-) diff --git a/graphene/pyutils/dataclasses.py b/graphene/pyutils/dataclasses.py index f847b211..1a474526 100644 --- a/graphene/pyutils/dataclasses.py +++ b/graphene/pyutils/dataclasses.py @@ -291,14 +291,7 @@ class Field: class _DataclassParams: - __slots__ = ( - "init", - "repr", - "eq", - "order", - "unsafe_hash", - "frozen", - ) + __slots__ = ("init", "repr", "eq", "order", "unsafe_hash", "frozen") def __init__(self, init, repr, eq, order, unsafe_hash, frozen): self.init = init @@ -1157,10 +1150,7 @@ def make_dataclass( name = item tp = "typing.Any" elif len(item) == 2: - ( - name, - tp, - ) = item + (name, tp) = item elif len(item) == 3: name, tp, spec = item namespace[name] = spec diff --git a/graphene/relay/node.py b/graphene/relay/node.py index b189bc97..8defefff 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -92,11 +92,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". ' - 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". ' + f"Exception message: {str(e)}" ) graphene_type = info.schema.get_type(_type) diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 42345e54..b697c462 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -163,16 +163,14 @@ async def test_respects_first_and_after_and_before_too_few(): @mark.asyncio async def test_respects_first_and_after_and_before_too_many(): await check( - f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', - "BCD", + f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD" ) @mark.asyncio async def test_respects_first_and_after_and_before_exactly_right(): await check( - f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', - "BCD", + f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD" ) @@ -188,16 +186,14 @@ async def test_respects_last_and_after_and_before_too_few(): @mark.asyncio async def test_respects_last_and_after_and_before_too_many(): await check( - f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', - "BCD", + f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD" ) @mark.asyncio async def test_respects_last_and_after_and_before_exactly_right(): await check( - f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', - "BCD", + f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD" ) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 7f98e312..ca87775a 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -96,12 +96,10 @@ class Mutation(ObjectType): input_class = getattr(cls, "Input", None) if input_class: warn_deprecation( - ( - f"Please use {cls.__name__}.Arguments instead of {cls.__name__}.Input." - " Input is now only used in ClientMutationID.\n" - "Read more:" - " https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input" - ) + f"Please use {cls.__name__}.Arguments instead of {cls.__name__}.Input." + " Input is now only used in ClientMutationID.\n" + "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) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 2b1902ea..1ff29a2e 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -27,11 +27,7 @@ class ObjectTypeMeta(BaseTypeMeta): pass base_cls = super().__new__( - cls, - name_, - (InterObjectType,) + bases, - namespace, - **options, + cls, name_, (InterObjectType,) + bases, namespace, **options ) if base_cls._meta: fields = [ diff --git a/graphene/types/tests/test_base64.py b/graphene/types/tests/test_base64.py index d1b76cb4..433f63c3 100644 --- a/graphene/types/tests/test_base64.py +++ b/graphene/types/tests/test_base64.py @@ -64,16 +64,11 @@ def test_base64_query_none(): def test_base64_query_invalid(): - bad_inputs = [ - dict(), - 123, - "This is not valid base64", - ] + bad_inputs = [dict(), 123, "This is not valid base64"] for input_ in bad_inputs: result = schema.execute( - """{ base64(input: $input) }""", - variables={"input": input_}, + """{ base64(input: $input) }""", variables={"input": input_} ) assert isinstance(result.errors, list) assert len(result.errors) == 1 diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 8d5e87af..6e204aa9 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -26,8 +26,8 @@ def test_enum_construction(): assert RGB._meta.description == "Description" values = RGB._meta.enum.__members__.values() - assert sorted([v.name for v in values]) == ["BLUE", "GREEN", "RED"] - assert sorted([v.description for v in values]) == [ + assert sorted(v.name for v in values) == ["BLUE", "GREEN", "RED"] + assert sorted(v.description for v in values) == [ "Description BLUE", "Description GREEN", "Description RED", @@ -52,7 +52,7 @@ def test_enum_instance_construction(): RGB = Enum("RGB", "RED,GREEN,BLUE") values = RGB._meta.enum.__members__.values() - assert sorted([v.name for v in values]) == ["BLUE", "GREEN", "RED"] + assert sorted(v.name for v in values) == ["BLUE", "GREEN", "RED"] def test_enum_from_builtin_enum(): @@ -465,7 +465,7 @@ def test_mutation_enum_input_type(): color } } - """, + """ ) assert not result.errors assert result.data == {"createPaint": {"color": "RED"}} diff --git a/graphene/utils/orderedtype.py b/graphene/utils/orderedtype.py index fb8783d2..294ad54e 100644 --- a/graphene/utils/orderedtype.py +++ b/graphene/utils/orderedtype.py @@ -36,4 +36,4 @@ class OrderedType: return NotImplemented def __hash__(self): - return hash((self.creation_counter)) + return hash(self.creation_counter) diff --git a/graphene/validation/disable_introspection.py b/graphene/validation/disable_introspection.py index be25a287..49a7d607 100644 --- a/graphene/validation/disable_introspection.py +++ b/graphene/validation/disable_introspection.py @@ -11,7 +11,6 @@ class DisableIntrospection(ValidationRule): if is_introspection_key(field_name): self.report_error( GraphQLError( - f"Cannot query '{field_name}': introspection is disabled.", - node, + f"Cannot query '{field_name}': introspection is disabled.", node ) ) diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py index 0c22089a..29c1508c 100644 --- a/graphene/validation/tests/test_depth_limit_validator.py +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -236,11 +236,7 @@ def test_should_ignore_field(): errors, result = run_query( query, 10, - ignore=[ - "user1", - re.compile("user2"), - lambda field_name: field_name == "user3", - ], + ignore=["user1", re.compile("user2"), lambda field_name: field_name == "user3"], ) expected = {"read1": 2, "read2": 0} @@ -255,8 +251,4 @@ def test_should_raise_invalid_ignore(): } """ with raises(ValueError, match="Invalid ignore option:"): - run_query( - query, - 10, - ignore=[True], - ) + run_query(query, 10, ignore=[True]) From c1bd25555ce8e06a3a02513b627c7aacc6b6bb55 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Tue, 28 Sep 2021 06:41:54 +0530 Subject: [PATCH 69/87] Update queryvalidation.rst --- docs/execution/queryvalidation.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst index 8402b9ea..e4ff7641 100644 --- a/docs/execution/queryvalidation.rst +++ b/docs/execution/queryvalidation.rst @@ -22,6 +22,7 @@ Usage Here is how you would implement depth-limiting on your schema. .. code:: python + from graphql import validate, parse from graphene import ObjectType, Schema, String from graphene.validation import depth_limit_validator @@ -58,6 +59,7 @@ Usage Here is how you would disable introspection for your schema. .. code:: python + from graphql import validate, parse from graphene import ObjectType, Schema, String from graphene.validation import DisableIntrospection @@ -92,6 +94,7 @@ reason. Here is an example query validator that visits field definitions in Grap if any of those fields are blacklisted: .. code:: python + from graphql import GraphQLError from graphql.language import FieldNode from graphql.validation import ValidationRule From 1d6f9e984b7d96cc90e21667a0127d665241c92d Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Wed, 29 Sep 2021 18:13:08 +0530 Subject: [PATCH 70/87] Mame sure to pass correct graphql schema instance --- docs/execution/queryvalidation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst index e4ff7641..9c24a2e3 100644 --- a/docs/execution/queryvalidation.rst +++ b/docs/execution/queryvalidation.rst @@ -38,7 +38,7 @@ Here is how you would implement depth-limiting on your schema. # will not be executed. validation_errors = validate( - schema=schema, + schema=schema.graphql_schema, document_ast=parse('THE QUERY'), rules=( depth_limit_validator( @@ -74,7 +74,7 @@ Here is how you would disable introspection for your schema. # introspection queries will not be executed. validation_errors = validate( - schema=schema, + schema=schema.graphql_schema, document_ast=parse('THE QUERY'), rules=( DisableIntrospection, From b6c8931b22d7016a0022fc8de3af12d73b7697ec Mon Sep 17 00:00:00 2001 From: Eran Kampf <205185+ekampf@users.noreply.github.com> Date: Wed, 29 Sep 2021 17:11:16 -0700 Subject: [PATCH 71/87] Fix GraphQL-core dependency GraphQL-core released `3.2.0rc1` with some breaking changes and 1. We should be getting RC releases in our dependencies 2. It has breaking changes, so we shouldn't get 3.2.0 unless someone fixes it explicitly --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1503c3c6..ae59a92a 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ setup( keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["examples*"]), install_requires=[ - "graphql-core>=3.1.2,<4", + "graphql-core~=3.1.2", "graphql-relay>=3.0,<4", "aniso8601>=8,<10", ], From 0a54094f59e1b1bca83e4574dbb35587536bbce6 Mon Sep 17 00:00:00 2001 From: Eran Kampf <205185+ekampf@users.noreply.github.com> Date: Wed, 29 Sep 2021 23:42:36 -0700 Subject: [PATCH 72/87] v3.0.0b8 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 34729de0..c8ffc0c4 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", 7) +VERSION = (3, 0, 0, "beta", 8) __version__ = get_version(VERSION) From 9e17044ddca17bc5d93f8c670d85030613008fcc Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Fri, 5 Nov 2021 02:21:14 +0100 Subject: [PATCH 73/87] 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 74/87] 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 75/87] 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 76/87] 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 77/87] 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 763910e7b59be5b9811cd16228db781af88868ab Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Mon, 7 Feb 2022 15:51:08 +0900 Subject: [PATCH 78/87] 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 79/87] 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 80/87] 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 81/87] 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 82/87] 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 83/87] 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 84/87] 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 8f6a8f9c4ace5fc2184040e6a56e877a6d8acae6 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Fri, 24 Jun 2022 18:00:55 +0200 Subject: [PATCH 85/87] 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 86/87] 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 87/87] 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 `_.