Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-graphene for openSUSE:Factory
checked in at 2023-01-06 17:06:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-graphene (Old)
and /work/SRC/openSUSE:Factory/.python-graphene.new.1563 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-graphene"
Fri Jan 6 17:06:05 2023 rev:8 rq:1056360 version:3.2.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-graphene/python-graphene.changes
2022-10-12 18:27:33.842069267 +0200
+++
/work/SRC/openSUSE:Factory/.python-graphene.new.1563/python-graphene.changes
2023-01-06 17:06:50.456614442 +0100
@@ -1,0 +2,8 @@
+Thu Jan 5 19:48:47 UTC 2023 - Yogalakshmi Arunachalam <[email protected]>
+
+- Update to version 3.2.1
+ * What's Changed
+ Non-required InputFields and Arguments can now be marked as deprecated by
passing the deprecation_reason keyword argument to the constructor.
+ Complete deprecated fields and arguments support by @vhutov in #1472
+
+-------------------------------------------------------------------
Old:
----
graphene-3.1.1.tar.gz
New:
----
graphene-3.2.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-graphene.spec ++++++
--- /var/tmp/diff_new_pack.YKEYE2/_old 2023-01-06 17:06:50.856616689 +0100
+++ /var/tmp/diff_new_pack.YKEYE2/_new 2023-01-06 17:06:50.860616711 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-graphene
#
-# Copyright (c) 2022 SUSE LLC
+# Copyright (c) 2023 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-graphene
-Version: 3.1.1
+Version: 3.2.1
Release: 0
Summary: GraphQL Framework for Python
License: MIT
++++++ graphene-3.1.1.tar.gz -> graphene-3.2.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/.github/workflows/tests.yml
new/graphene-3.2.1/.github/workflows/tests.yml
--- old/graphene-3.1.1/.github/workflows/tests.yml 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/.github/workflows/tests.yml 2022-12-11
21:05:25.000000000 +0100
@@ -30,7 +30,7 @@
- {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.6', python: '3.6', os: ubuntu-20.04, tox: py36}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -58,7 +58,7 @@
if: ${{ matrix.python == '3.10' }}
uses: actions/upload-artifact@v3
with:
- name: graphene-sqlalchemy-coverage
+ name: graphene-coverage
path: coverage.xml
if-no-files-found: error
- name: Upload coverage.xml to codecov
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/Makefile new/graphene-3.2.1/Makefile
--- old/graphene-3.1.1/Makefile 2022-09-08 10:55:05.000000000 +0200
+++ new/graphene-3.2.1/Makefile 2022-12-11 21:05:25.000000000 +0100
@@ -7,6 +7,7 @@
install-dev:
pip install -e ".[dev]"
+.PHONY: test ## Run tests
test:
py.test graphene examples
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/docs/execution/middleware.rst
new/graphene-3.2.1/docs/execution/middleware.rst
--- old/graphene-3.1.1/docs/execution/middleware.rst 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/docs/execution/middleware.rst 2022-12-11
21:05:25.000000000 +0100
@@ -41,6 +41,8 @@
result = schema.execute('THE QUERY',
middleware=[AuthorizationMiddleware()])
+If the ``middleware`` argument includes multiple middlewares,
+these middlewares will be executed bottom-up, i.e. from last to first.
Functional example
------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/docs/quickstart.rst
new/graphene-3.2.1/docs/quickstart.rst
--- old/graphene-3.1.1/docs/quickstart.rst 2022-09-08 10:55:05.000000000
+0200
+++ new/graphene-3.2.1/docs/quickstart.rst 2022-12-11 21:05:25.000000000
+0100
@@ -37,12 +37,12 @@
Letâs build a basic GraphQL schema to say "hello" and "goodbye" in Graphene.
-When we send a **Query** requesting only one **Field**, ``hello``, and specify
a value for the ``name`` **Argument**...
+When we send a **Query** requesting only one **Field**, ``hello``, and specify
a value for the ``firstName`` **Argument**...
.. code::
{
- hello(name: "friend")
+ hello(firstName: "friend")
}
...we would expect the following Response containing only the data requested
(the ``goodbye`` field is not resolved).
@@ -79,14 +79,15 @@
from graphene import ObjectType, String, Schema
class Query(ObjectType):
- # this defines a Field `hello` in our Schema with a single Argument
`name`
- hello = String(name=String(default_value="stranger"))
+ # this defines a Field `hello` in our Schema with a single Argument
`first_name`
+ # By default, the argument name will automatically be camel-based into
firstName in the generated schema
+ hello = String(first_name=String(default_value="stranger"))
goodbye = String()
# our Resolver method takes the GraphQL context (root, info) as well as
- # Argument (name) for the Field and returns data for the query Response
- def resolve_hello(root, info, name):
- return f'Hello {name}!'
+ # Argument (first_name) for the Field and returns data for the query
Response
+ def resolve_hello(root, info, first_name):
+ return f'Hello {first_name}!'
def resolve_goodbye(root, info):
return 'See ya!'
@@ -110,7 +111,7 @@
.. code::
type Query {
- hello(name: String = "stranger"): String
+ hello(firstName: String = "stranger"): String
goodbye: String
}
@@ -130,7 +131,7 @@
# "Hello stranger!"
# or passing the argument in the query
- query_with_argument = '{ hello(name: "GraphQL") }'
+ query_with_argument = '{ hello(firstName: "GraphQL") }'
result = schema.execute(query_with_argument)
print(result.data['hello'])
# "Hello GraphQL!"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/__init__.py
new/graphene-3.2.1/graphene/__init__.py
--- old/graphene-3.1.1/graphene/__init__.py 2022-09-08 10:55:05.000000000
+0200
+++ new/graphene-3.2.1/graphene/__init__.py 2022-12-11 21:05:25.000000000
+0100
@@ -1,11 +1,15 @@
from .pyutils.version import get_version
from .relay import (
+ BaseGlobalIDType,
ClientIDMutation,
Connection,
ConnectionField,
+ DefaultGlobalIDType,
GlobalID,
Node,
PageInfo,
+ SimpleGlobalIDType,
+ UUIDGlobalIDType,
is_node,
)
from .types import (
@@ -42,7 +46,7 @@
from .utils.module_loading import lazy_import
from .utils.resolve_only_args import resolve_only_args
-VERSION = (3, 1, 1, "final", 0)
+VERSION = (3, 2, 1, "final", 0)
__version__ = get_version(VERSION)
@@ -52,6 +56,7 @@
"Argument",
"Base64",
"BigInt",
+ "BaseGlobalIDType",
"Boolean",
"ClientIDMutation",
"Connection",
@@ -60,6 +65,7 @@
"Date",
"DateTime",
"Decimal",
+ "DefaultGlobalIDType",
"Dynamic",
"Enum",
"Field",
@@ -80,10 +86,12 @@
"ResolveInfo",
"Scalar",
"Schema",
+ "SimpleGlobalIDType",
"String",
"Time",
- "UUID",
"Union",
+ "UUID",
+ "UUIDGlobalIDType",
"is_node",
"lazy_import",
"resolve_only_args",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/relay/__init__.py
new/graphene-3.2.1/graphene/relay/__init__.py
--- old/graphene-3.1.1/graphene/relay/__init__.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/relay/__init__.py 2022-12-11
21:05:25.000000000 +0100
@@ -1,13 +1,23 @@
from .node import Node, is_node, GlobalID
from .mutation import ClientIDMutation
from .connection import Connection, ConnectionField, PageInfo
+from .id_type import (
+ BaseGlobalIDType,
+ DefaultGlobalIDType,
+ SimpleGlobalIDType,
+ UUIDGlobalIDType,
+)
__all__ = [
- "Node",
- "is_node",
- "GlobalID",
+ "BaseGlobalIDType",
"ClientIDMutation",
"Connection",
"ConnectionField",
+ "DefaultGlobalIDType",
+ "GlobalID",
+ "Node",
"PageInfo",
+ "SimpleGlobalIDType",
+ "UUIDGlobalIDType",
+ "is_node",
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/relay/id_type.py
new/graphene-3.2.1/graphene/relay/id_type.py
--- old/graphene-3.1.1/graphene/relay/id_type.py 1970-01-01
01:00:00.000000000 +0100
+++ new/graphene-3.2.1/graphene/relay/id_type.py 2022-12-11
21:05:25.000000000 +0100
@@ -0,0 +1,87 @@
+from graphql_relay import from_global_id, to_global_id
+
+from ..types import ID, UUID
+from ..types.base import BaseType
+
+from typing import Type
+
+
+class BaseGlobalIDType:
+ """
+ Base class that define the required attributes/method for a type.
+ """
+
+ graphene_type = ID # type: Type[BaseType]
+
+ @classmethod
+ def resolve_global_id(cls, info, global_id):
+ # return _type, _id
+ raise NotImplementedError
+
+ @classmethod
+ def to_global_id(cls, _type, _id):
+ # return _id
+ raise NotImplementedError
+
+
+class DefaultGlobalIDType(BaseGlobalIDType):
+ """
+ Default global ID type: base64 encoded version of "<node type name>: <node
id>".
+ """
+
+ graphene_type = ID
+
+ @classmethod
+ def resolve_global_id(cls, info, global_id):
+ try:
+ _type, _id = from_global_id(global_id)
+ if not _type:
+ raise ValueError("Invalid Global ID")
+ return _type, _id
+ except Exception as e:
+ raise Exception(
+ f'Unable to parse global ID "{global_id}". '
+ 'Make sure it is a base64 encoded string in the format:
"TypeName:id". '
+ f"Exception message: {e}"
+ )
+
+ @classmethod
+ def to_global_id(cls, _type, _id):
+ return to_global_id(_type, _id)
+
+
+class SimpleGlobalIDType(BaseGlobalIDType):
+ """
+ Simple global ID type: simply the id of the object.
+ To be used carefully as the user is responsible for ensuring that the IDs
are indeed global
+ (otherwise it could cause request caching issues).
+ """
+
+ graphene_type = ID
+
+ @classmethod
+ def resolve_global_id(cls, info, global_id):
+ _type = info.return_type.graphene_type._meta.name
+ return _type, global_id
+
+ @classmethod
+ def to_global_id(cls, _type, _id):
+ return _id
+
+
+class UUIDGlobalIDType(BaseGlobalIDType):
+ """
+ UUID global ID type.
+ By definition UUID are global so they are used as they are.
+ """
+
+ graphene_type = UUID
+
+ @classmethod
+ def resolve_global_id(cls, info, global_id):
+ _type = info.return_type.graphene_type._meta.name
+ return _type, global_id
+
+ @classmethod
+ def to_global_id(cls, _type, _id):
+ return _id
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/relay/node.py
new/graphene-3.2.1/graphene/relay/node.py
--- old/graphene-3.1.1/graphene/relay/node.py 2022-09-08 10:55:05.000000000
+0200
+++ new/graphene-3.2.1/graphene/relay/node.py 2022-12-11 21:05:25.000000000
+0100
@@ -1,11 +1,10 @@
from functools import partial
from inspect import isclass
-from graphql_relay import from_global_id, to_global_id
-
-from ..types import ID, Field, Interface, ObjectType
+from ..types import Field, Interface, ObjectType
from ..types.interface import InterfaceOptions
from ..types.utils import get_type
+from .id_type import BaseGlobalIDType, DefaultGlobalIDType
def is_node(objecttype):
@@ -22,8 +21,18 @@
class GlobalID(Field):
- def __init__(self, node=None, parent_type=None, required=True, *args,
**kwargs):
- super(GlobalID, self).__init__(ID, required=required, *args, **kwargs)
+ def __init__(
+ self,
+ node=None,
+ parent_type=None,
+ required=True,
+ global_id_type=DefaultGlobalIDType,
+ *args,
+ **kwargs,
+ ):
+ super(GlobalID, self).__init__(
+ global_id_type.graphene_type, required=required, *args, **kwargs
+ )
self.node = node or Node
self.parent_type_name = parent_type._meta.name if parent_type else None
@@ -47,12 +56,14 @@
assert issubclass(node, Node), "NodeField can only operate in Nodes"
self.node_type = node
self.field_type = type_
+ global_id_type = node._meta.global_id_type
super(NodeField, self).__init__(
- # If we don's specify a type, the field type will be the node
- # interface
+ # If we don't specify a type, the field type will be the node
interface
type_ or node,
- id=ID(required=True, description="The ID of the object"),
+ id=global_id_type.graphene_type(
+ required=True, description="The ID of the object"
+ ),
**kwargs,
)
@@ -65,11 +76,23 @@
abstract = True
@classmethod
- def __init_subclass_with_meta__(cls, **options):
+ def __init_subclass_with_meta__(cls, global_id_type=DefaultGlobalIDType,
**options):
+ assert issubclass(
+ global_id_type, BaseGlobalIDType
+ ), "Custom ID type need to be implemented as a subclass of
BaseGlobalIDType."
_meta = InterfaceOptions(cls)
- _meta.fields = {"id": GlobalID(cls, description="The ID of the
object")}
+ _meta.global_id_type = global_id_type
+ _meta.fields = {
+ "id": GlobalID(
+ cls, global_id_type=global_id_type, description="The ID of the
object"
+ )
+ }
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta,
**options)
+ @classmethod
+ def resolve_global_id(cls, info, global_id):
+ return cls._meta.global_id_type.resolve_global_id(info, global_id)
+
class Node(AbstractNode):
"""An object with an ID"""
@@ -84,16 +107,7 @@
@classmethod
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}". '
- 'Make sure it is a base64 encoded string in the format:
"TypeName:id". '
- f"Exception message: {e}"
- )
+ _type, _id = cls.resolve_global_id(info, global_id)
graphene_type = info.schema.get_type(_type)
if graphene_type is None:
@@ -117,9 +131,5 @@
return get_node(info, _id)
@classmethod
- def from_global_id(cls, global_id):
- return from_global_id(global_id)
-
- @classmethod
def to_global_id(cls, type_, id):
- return to_global_id(type_, id)
+ return cls._meta.global_id_type.to_global_id(type_, id)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/graphene-3.1.1/graphene/relay/tests/test_custom_global_id.py
new/graphene-3.2.1/graphene/relay/tests/test_custom_global_id.py
--- old/graphene-3.1.1/graphene/relay/tests/test_custom_global_id.py
1970-01-01 01:00:00.000000000 +0100
+++ new/graphene-3.2.1/graphene/relay/tests/test_custom_global_id.py
2022-12-11 21:05:25.000000000 +0100
@@ -0,0 +1,325 @@
+import re
+from uuid import uuid4
+
+from graphql import graphql_sync
+
+from ..id_type import BaseGlobalIDType, SimpleGlobalIDType, UUIDGlobalIDType
+from ..node import Node
+from ...types import Int, ObjectType, Schema, String
+
+
+class TestUUIDGlobalID:
+ def setup(self):
+ self.user_list = [
+ {"id": uuid4(), "name": "First"},
+ {"id": uuid4(), "name": "Second"},
+ {"id": uuid4(), "name": "Third"},
+ {"id": uuid4(), "name": "Fourth"},
+ ]
+ self.users = {user["id"]: user for user in self.user_list}
+
+ class CustomNode(Node):
+ class Meta:
+ global_id_type = UUIDGlobalIDType
+
+ class User(ObjectType):
+ class Meta:
+ interfaces = [CustomNode]
+
+ name = String()
+
+ @classmethod
+ def get_node(cls, _type, _id):
+ return self.users[_id]
+
+ class RootQuery(ObjectType):
+ user = CustomNode.Field(User)
+
+ self.schema = Schema(query=RootQuery, types=[User])
+ self.graphql_schema = self.schema.graphql_schema
+
+ def test_str_schema_correct(self):
+ """
+ Check that the schema has the expected and custom node interface and
user type and that they both use UUIDs
+ """
+ parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
+ types = [t for t, f in parsed]
+ fields = [f for t, f in parsed]
+ custom_node_interface = "interface CustomNode"
+ assert custom_node_interface in types
+ assert (
+ '"""The ID of the object"""\n id: UUID!'
+ == fields[types.index(custom_node_interface)]
+ )
+ user_type = "type User implements CustomNode"
+ assert user_type in types
+ assert (
+ '"""The ID of the object"""\n id: UUID!\n name: String'
+ == fields[types.index(user_type)]
+ )
+
+ def test_get_by_id(self):
+ query = """query userById($id: UUID!) {
+ user(id: $id) {
+ id
+ name
+ }
+ }"""
+ # UUID need to be converted to string for serialization
+ result = graphql_sync(
+ self.graphql_schema,
+ query,
+ variable_values={"id": str(self.user_list[0]["id"])},
+ )
+ assert not result.errors
+ assert result.data["user"]["id"] == str(self.user_list[0]["id"])
+ assert result.data["user"]["name"] == self.user_list[0]["name"]
+
+
+class TestSimpleGlobalID:
+ def setup(self):
+ self.user_list = [
+ {"id": "my global primary key in clear 1", "name": "First"},
+ {"id": "my global primary key in clear 2", "name": "Second"},
+ {"id": "my global primary key in clear 3", "name": "Third"},
+ {"id": "my global primary key in clear 4", "name": "Fourth"},
+ ]
+ self.users = {user["id"]: user for user in self.user_list}
+
+ class CustomNode(Node):
+ class Meta:
+ global_id_type = SimpleGlobalIDType
+
+ class User(ObjectType):
+ class Meta:
+ interfaces = [CustomNode]
+
+ name = String()
+
+ @classmethod
+ def get_node(cls, _type, _id):
+ return self.users[_id]
+
+ class RootQuery(ObjectType):
+ user = CustomNode.Field(User)
+
+ self.schema = Schema(query=RootQuery, types=[User])
+ self.graphql_schema = self.schema.graphql_schema
+
+ def test_str_schema_correct(self):
+ """
+ Check that the schema has the expected and custom node interface and
user type and that they both use UUIDs
+ """
+ parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
+ types = [t for t, f in parsed]
+ fields = [f for t, f in parsed]
+ custom_node_interface = "interface CustomNode"
+ assert custom_node_interface in types
+ assert (
+ '"""The ID of the object"""\n id: ID!'
+ == fields[types.index(custom_node_interface)]
+ )
+ user_type = "type User implements CustomNode"
+ assert user_type in types
+ assert (
+ '"""The ID of the object"""\n id: ID!\n name: String'
+ == fields[types.index(user_type)]
+ )
+
+ def test_get_by_id(self):
+ query = """query {
+ user(id: "my global primary key in clear 3") {
+ id
+ name
+ }
+ }"""
+ result = graphql_sync(self.graphql_schema, query)
+ assert not result.errors
+ assert result.data["user"]["id"] == self.user_list[2]["id"]
+ assert result.data["user"]["name"] == self.user_list[2]["name"]
+
+
+class TestCustomGlobalID:
+ def setup(self):
+ self.user_list = [
+ {"id": 1, "name": "First"},
+ {"id": 2, "name": "Second"},
+ {"id": 3, "name": "Third"},
+ {"id": 4, "name": "Fourth"},
+ ]
+ self.users = {user["id"]: user for user in self.user_list}
+
+ class CustomGlobalIDType(BaseGlobalIDType):
+ """
+ Global id that is simply and integer in clear.
+ """
+
+ graphene_type = Int
+
+ @classmethod
+ def resolve_global_id(cls, info, global_id):
+ _type = info.return_type.graphene_type._meta.name
+ return _type, global_id
+
+ @classmethod
+ def to_global_id(cls, _type, _id):
+ return _id
+
+ class CustomNode(Node):
+ class Meta:
+ global_id_type = CustomGlobalIDType
+
+ class User(ObjectType):
+ class Meta:
+ interfaces = [CustomNode]
+
+ name = String()
+
+ @classmethod
+ def get_node(cls, _type, _id):
+ return self.users[_id]
+
+ class RootQuery(ObjectType):
+ user = CustomNode.Field(User)
+
+ self.schema = Schema(query=RootQuery, types=[User])
+ self.graphql_schema = self.schema.graphql_schema
+
+ def test_str_schema_correct(self):
+ """
+ Check that the schema has the expected and custom node interface and
user type and that they both use UUIDs
+ """
+ parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
+ types = [t for t, f in parsed]
+ fields = [f for t, f in parsed]
+ custom_node_interface = "interface CustomNode"
+ assert custom_node_interface in types
+ assert (
+ '"""The ID of the object"""\n id: Int!'
+ == fields[types.index(custom_node_interface)]
+ )
+ user_type = "type User implements CustomNode"
+ assert user_type in types
+ assert (
+ '"""The ID of the object"""\n id: Int!\n name: String'
+ == fields[types.index(user_type)]
+ )
+
+ def test_get_by_id(self):
+ query = """query {
+ user(id: 2) {
+ id
+ name
+ }
+ }"""
+ result = graphql_sync(self.graphql_schema, query)
+ assert not result.errors
+ assert result.data["user"]["id"] == self.user_list[1]["id"]
+ assert result.data["user"]["name"] == self.user_list[1]["name"]
+
+
+class TestIncompleteCustomGlobalID:
+ def setup(self):
+ self.user_list = [
+ {"id": 1, "name": "First"},
+ {"id": 2, "name": "Second"},
+ {"id": 3, "name": "Third"},
+ {"id": 4, "name": "Fourth"},
+ ]
+ self.users = {user["id"]: user for user in self.user_list}
+
+ def test_must_define_to_global_id(self):
+ """
+ Test that if the `to_global_id` method is not defined, we can query
the object, but we can't request its ID.
+ """
+
+ class CustomGlobalIDType(BaseGlobalIDType):
+ graphene_type = Int
+
+ @classmethod
+ def resolve_global_id(cls, info, global_id):
+ _type = info.return_type.graphene_type._meta.name
+ return _type, global_id
+
+ class CustomNode(Node):
+ class Meta:
+ global_id_type = CustomGlobalIDType
+
+ class User(ObjectType):
+ class Meta:
+ interfaces = [CustomNode]
+
+ name = String()
+
+ @classmethod
+ def get_node(cls, _type, _id):
+ return self.users[_id]
+
+ class RootQuery(ObjectType):
+ user = CustomNode.Field(User)
+
+ self.schema = Schema(query=RootQuery, types=[User])
+ self.graphql_schema = self.schema.graphql_schema
+
+ query = """query {
+ user(id: 2) {
+ name
+ }
+ }"""
+ result = graphql_sync(self.graphql_schema, query)
+ assert not result.errors
+ assert result.data["user"]["name"] == self.user_list[1]["name"]
+
+ query = """query {
+ user(id: 2) {
+ id
+ name
+ }
+ }"""
+ result = graphql_sync(self.graphql_schema, query)
+ assert result.errors is not None
+ assert len(result.errors) == 1
+ assert result.errors[0].path == ["user", "id"]
+
+ def test_must_define_resolve_global_id(self):
+ """
+ Test that if the `resolve_global_id` method is not defined, we can't
query the object by ID.
+ """
+
+ class CustomGlobalIDType(BaseGlobalIDType):
+ graphene_type = Int
+
+ @classmethod
+ def to_global_id(cls, _type, _id):
+ return _id
+
+ class CustomNode(Node):
+ class Meta:
+ global_id_type = CustomGlobalIDType
+
+ class User(ObjectType):
+ class Meta:
+ interfaces = [CustomNode]
+
+ name = String()
+
+ @classmethod
+ def get_node(cls, _type, _id):
+ return self.users[_id]
+
+ class RootQuery(ObjectType):
+ user = CustomNode.Field(User)
+
+ self.schema = Schema(query=RootQuery, types=[User])
+ self.graphql_schema = self.schema.graphql_schema
+
+ query = """query {
+ user(id: 2) {
+ id
+ name
+ }
+ }"""
+ result = graphql_sync(self.graphql_schema, query)
+ assert result.errors is not None
+ assert len(result.errors) == 1
+ assert result.errors[0].path == ["user"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/graphene-3.1.1/graphene/relay/tests/test_mutation_async.py
new/graphene-3.2.1/graphene/relay/tests/test_mutation_async.py
--- old/graphene-3.1.1/graphene/relay/tests/test_mutation_async.py
2022-09-08 10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/relay/tests/test_mutation_async.py
2022-12-11 21:05:25.000000000 +0100
@@ -3,6 +3,7 @@
from graphene.types import ID, Field, ObjectType, Schema
from graphene.types.scalars import String
from graphene.relay.mutation import ClientIDMutation
+from graphene.test import Client
class SharedFields(object):
@@ -61,24 +62,27 @@
schema = Schema(query=RootQuery, mutation=Mutation)
+client = Client(schema)
@mark.asyncio
async def test_node_query_promise():
- executed = await schema.execute_async(
+ executed = await client.execute_async(
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"})
{ phrase } }'
)
- assert not executed.errors
- assert executed.data == {"sayPromise": {"phrase": "hello"}}
+ assert isinstance(executed, dict)
+ assert "errors" not in executed
+ assert executed["data"] == {"sayPromise": {"phrase": "hello"}}
@mark.asyncio
async def test_edge_query():
- executed = await schema.execute_async(
+ executed = await client.execute_async(
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId,
myNodeEdge { cursor node { name }} } }'
)
- assert not executed.errors
- assert dict(executed.data) == {
+ assert isinstance(executed, dict)
+ assert "errors" not in executed
+ assert executed["data"] == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/relay/tests/test_node.py
new/graphene-3.2.1/graphene/relay/tests/test_node.py
--- old/graphene-3.1.1/graphene/relay/tests/test_node.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/relay/tests/test_node.py 2022-12-11
21:05:25.000000000 +0100
@@ -55,6 +55,7 @@
assert "id" in MyNode._meta.fields
assert is_node(MyNode)
assert not is_node(object)
+ assert not is_node("node")
def test_node_query():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/test/__init__.py
new/graphene-3.2.1/graphene/test/__init__.py
--- old/graphene-3.1.1/graphene/test/__init__.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/test/__init__.py 2022-12-11
21:05:25.000000000 +0100
@@ -1,4 +1,3 @@
-from promise import Promise, is_thenable
from graphql.error import GraphQLError
from graphene.types.schema import Schema
@@ -31,7 +30,10 @@
def execute(self, *args, **kwargs):
executed = self.schema.execute(*args, **dict(self.execute_options,
**kwargs))
- if is_thenable(executed):
- return Promise.resolve(executed).then(self.format_result)
+ return self.format_result(executed)
+ async def execute_async(self, *args, **kwargs):
+ executed = await self.schema.execute_async(
+ *args, **dict(self.execute_options, **kwargs)
+ )
return self.format_result(executed)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/argument.py
new/graphene-3.2.1/graphene/types/argument.py
--- old/graphene-3.1.1/graphene/types/argument.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/argument.py 2022-12-11
21:05:25.000000000 +0100
@@ -31,18 +31,22 @@
type (class for a graphene.UnmountedType): must be a class (not an
instance) of an
unmounted graphene type (ex. scalar or object) which is used for
the type of this
argument in the GraphQL schema.
- required (bool): indicates this argument as not null in the graphql
schema. Same behavior
+ required (optional, bool): indicates this argument as not null in the
graphql schema. Same behavior
as graphene.NonNull. Default False.
- name (str): the name of the GraphQL argument. Defaults to parameter
name.
- description (str): the description of the GraphQL argument in the
schema.
- default_value (Any): The value to be provided if the user does not set
this argument in
+ name (optional, str): the name of the GraphQL argument. Defaults to
parameter name.
+ description (optional, str): the description of the GraphQL argument
in the schema.
+ default_value (optional, Any): The value to be provided if the user
does not set this argument in
the operation.
+ deprecation_reason (optional, str): Setting this value indicates that
the argument is
+ depreciated and may provide instruction or reason on how for
clients to proceed. Cannot be
+ set if the argument is required (see spec).
"""
def __init__(
self,
type_,
default_value=Undefined,
+ deprecation_reason=None,
description=None,
name=None,
required=False,
@@ -51,12 +55,16 @@
super(Argument, self).__init__(_creation_counter=_creation_counter)
if required:
+ assert (
+ deprecation_reason is None
+ ), f"Argument {name} is required, cannot deprecate it."
type_ = NonNull(type_)
self.name = name
self._type = type_
self.default_value = default_value
self.description = description
+ self.deprecation_reason = deprecation_reason
@property
def type(self):
@@ -68,6 +76,7 @@
and self.type == other.type
and self.default_value == other.default_value
and self.description == other.description
+ and self.deprecation_reason == other.deprecation_reason
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/definitions.py
new/graphene-3.2.1/graphene/types/definitions.py
--- old/graphene-3.1.1/graphene/types/definitions.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/definitions.py 2022-12-11
21:05:25.000000000 +0100
@@ -20,6 +20,11 @@
self.graphene_type = kwargs.pop("graphene_type")
super(GrapheneGraphQLType, self).__init__(*args, **kwargs)
+ def __copy__(self):
+ result = GrapheneGraphQLType(graphene_type=self.graphene_type)
+ result.__dict__.update(self.__dict__)
+ return result
+
class GrapheneInterfaceType(GrapheneGraphQLType, GraphQLInterfaceType):
pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/enum.py
new/graphene-3.2.1/graphene/types/enum.py
--- old/graphene-3.1.1/graphene/types/enum.py 2022-09-08 10:55:05.000000000
+0200
+++ new/graphene-3.2.1/graphene/types/enum.py 2022-12-11 21:05:25.000000000
+0100
@@ -12,6 +12,10 @@
return self.value is other
+def hash_enum(self):
+ return hash(self.name)
+
+
EnumType = type(PyEnum)
@@ -22,7 +26,7 @@
class EnumMeta(SubclassWithMeta_Meta):
def __new__(cls, name_, bases, classdict, **options):
- enum_members = dict(classdict, __eq__=eq_enum)
+ enum_members = dict(classdict, __eq__=eq_enum, __hash__=hash_enum)
# We remove the Meta attribute from the class to not collide
# with the enum values.
enum_members.pop("Meta", None)
@@ -52,6 +56,9 @@
return super(EnumMeta, cls).__call__(*args, **kwargs)
# return cls._meta.enum(*args, **kwargs)
+ def __iter__(cls):
+ return cls._meta.enum.__iter__()
+
def from_enum(
cls, enum, name=None, description=None, deprecation_reason=None
): # noqa: N805
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/inputfield.py
new/graphene-3.2.1/graphene/types/inputfield.py
--- old/graphene-3.1.1/graphene/types/inputfield.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/inputfield.py 2022-12-11
21:05:25.000000000 +0100
@@ -55,11 +55,14 @@
description=None,
required=False,
_creation_counter=None,
- **extra_args
+ **extra_args,
):
super(InputField, self).__init__(_creation_counter=_creation_counter)
self.name = name
if required:
+ assert (
+ deprecation_reason is None
+ ), f"InputField {name} is required, cannot deprecate it."
type_ = NonNull(type_)
self._type = type_
self.deprecation_reason = deprecation_reason
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/inputobjecttype.py
new/graphene-3.2.1/graphene/types/inputobjecttype.py
--- old/graphene-3.1.1/graphene/types/inputobjecttype.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/inputobjecttype.py 2022-12-11
21:05:25.000000000 +0100
@@ -14,7 +14,7 @@
container = None # type: InputObjectTypeContainer
-class InputObjectTypeContainer(dict, BaseType):
+class InputObjectTypeContainer(dict, BaseType): # type: ignore
class Meta:
abstract = True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/schema.py
new/graphene-3.2.1/graphene/types/schema.py
--- old/graphene-3.1.1/graphene/types/schema.py 2022-09-08 10:55:05.000000000
+0200
+++ new/graphene-3.2.1/graphene/types/schema.py 2022-12-11 21:05:25.000000000
+0100
@@ -1,3 +1,4 @@
+from enum import Enum as PyEnum
import inspect
from functools import partial
@@ -169,10 +170,16 @@
values = {}
for name, value in graphene_type._meta.enum.__members__.items():
description = getattr(value, "description", None)
- deprecation_reason = getattr(value, "deprecation_reason", None)
+ # if the "description" attribute is an Enum, it is likely an enum
member
+ # called description, not a description property
+ if isinstance(description, PyEnum):
+ description = None
if not description and callable(graphene_type._meta.description):
description = graphene_type._meta.description(value)
+ deprecation_reason = getattr(value, "deprecation_reason", None)
+ if isinstance(deprecation_reason, PyEnum):
+ deprecation_reason = None
if not deprecation_reason and callable(
graphene_type._meta.deprecation_reason
):
@@ -309,6 +316,7 @@
default_value=field.default_value,
out_name=name,
description=field.description,
+ deprecation_reason=field.deprecation_reason,
)
else:
args = {}
@@ -320,6 +328,7 @@
out_name=arg_name,
description=arg.description,
default_value=arg.default_value,
+ deprecation_reason=arg.deprecation_reason,
)
subscribe = field.wrap_subscribe(
self.get_function_for_type(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/tests/test_argument.py
new/graphene-3.2.1/graphene/types/tests/test_argument.py
--- old/graphene-3.1.1/graphene/types/tests/test_argument.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/tests/test_argument.py 2022-12-11
21:05:25.000000000 +0100
@@ -18,8 +18,20 @@
def test_argument_comparasion():
- arg1 = Argument(String, name="Hey", description="Desc",
default_value="default")
- arg2 = Argument(String, name="Hey", description="Desc",
default_value="default")
+ arg1 = Argument(
+ String,
+ name="Hey",
+ description="Desc",
+ default_value="default",
+ deprecation_reason="deprecated",
+ )
+ arg2 = Argument(
+ String,
+ name="Hey",
+ description="Desc",
+ default_value="default",
+ deprecation_reason="deprecated",
+ )
assert arg1 == arg2
assert arg1 != String()
@@ -40,6 +52,30 @@
}
+def test_to_arguments_deprecated():
+ args = {"unmounted_arg": String(required=False,
deprecation_reason="deprecated")}
+
+ my_args = to_arguments(args)
+ assert my_args == {
+ "unmounted_arg": Argument(
+ String, required=False, deprecation_reason="deprecated"
+ ),
+ }
+
+
+def test_to_arguments_required_deprecated():
+ args = {
+ "unmounted_arg": String(
+ required=True, name="arg", deprecation_reason="deprecated"
+ )
+ }
+
+ with raises(AssertionError) as exc_info:
+ to_arguments(args)
+
+ assert str(exc_info.value) == "Argument arg is required, cannot deprecate
it."
+
+
def test_to_arguments_raises_if_field():
args = {"arg_string": Field(String)}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/graphene-3.1.1/graphene/types/tests/test_definition.py
new/graphene-3.2.1/graphene/types/tests/test_definition.py
--- old/graphene-3.1.1/graphene/types/tests/test_definition.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/tests/test_definition.py 2022-12-11
21:05:25.000000000 +0100
@@ -1,4 +1,7 @@
+import copy
+
from ..argument import Argument
+from ..definitions import GrapheneGraphQLType
from ..enum import Enum
from ..field import Field
from ..inputfield import InputField
@@ -312,3 +315,16 @@
pass
assert TestInputObject1._meta.fields == TestInputObject2._meta.fields
+
+
+def test_graphene_graphql_type_can_be_copied():
+ class Query(ObjectType):
+ field = String()
+
+ def resolve_field(self, info):
+ return ""
+
+ schema = Schema(query=Query)
+ query_type_copy = copy.copy(schema.graphql_schema.query_type)
+ assert query_type_copy.__dict__ ==
schema.graphql_schema.query_type.__dict__
+ assert isinstance(schema.graphql_schema.query_type, GrapheneGraphQLType)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/tests/test_enum.py
new/graphene-3.2.1/graphene/types/tests/test_enum.py
--- old/graphene-3.1.1/graphene/types/tests/test_enum.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/tests/test_enum.py 2022-12-11
21:05:25.000000000 +0100
@@ -518,3 +518,83 @@
assert result.data == {"createPaint": {"color": "RED"}}
assert color_input_value == RGB.RED
+
+
+def test_hashable_enum():
+ class RGB(Enum):
+ """Available colors"""
+
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ color_map = {RGB.RED: "a", RGB.BLUE: "b", 1: "c"}
+
+ assert color_map[RGB.RED] == "a"
+ assert color_map[RGB.BLUE] == "b"
+ assert color_map[1] == "c"
+
+
+def test_hashable_instance_creation_enum():
+ Episode = Enum("Episode", [("NEWHOPE", 4), ("EMPIRE", 5), ("JEDI", 6)])
+
+ trilogy_map = {Episode.NEWHOPE: "better", Episode.EMPIRE: "best", 5: "foo"}
+
+ assert trilogy_map[Episode.NEWHOPE] == "better"
+ assert trilogy_map[Episode.EMPIRE] == "best"
+ assert trilogy_map[5] == "foo"
+
+
+def test_enum_iteration():
+ class TestEnum(Enum):
+ FIRST = 1
+ SECOND = 2
+
+ result = []
+ expected_values = ["FIRST", "SECOND"]
+ for c in TestEnum:
+ result.append(c.name)
+ assert result == expected_values
+
+
+def test_iterable_instance_creation_enum():
+ TestEnum = Enum("TestEnum", [("FIRST", 1), ("SECOND", 2)])
+
+ result = []
+ expected_values = ["FIRST", "SECOND"]
+ for c in TestEnum:
+ result.append(c.name)
+ assert result == expected_values
+
+
+# https://github.com/graphql-python/graphene/issues/1321
+def test_enum_description_member_not_interpreted_as_property():
+ class RGB(Enum):
+ """Description"""
+
+ red = "red"
+ green = "green"
+ blue = "blue"
+ description = "description"
+ deprecation_reason = "deprecation_reason"
+
+ class Query(ObjectType):
+ color = RGB()
+
+ def resolve_color(_, info):
+ return RGB.description
+
+ values = RGB._meta.enum.__members__.values()
+ assert sorted(v.name for v in values) == [
+ "blue",
+ "deprecation_reason",
+ "description",
+ "green",
+ "red",
+ ]
+
+ schema = Schema(query=Query)
+
+ results = schema.execute("query { color }")
+ assert not results.errors
+ assert results.data["color"] == RGB.description.name
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/types/tests/test_field.py
new/graphene-3.2.1/graphene/types/tests/test_field.py
--- old/graphene-3.1.1/graphene/types/tests/test_field.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/tests/test_field.py 2022-12-11
21:05:25.000000000 +0100
@@ -128,13 +128,20 @@
def test_field_source_argument_as_kw():
MyType = object()
- field = Field(MyType, b=NonNull(True), c=Argument(None), a=NonNull(False))
+ deprecation_reason = "deprecated"
+ field = Field(
+ MyType,
+ b=NonNull(True),
+ c=Argument(None, deprecation_reason=deprecation_reason),
+ a=NonNull(False),
+ )
assert list(field.args) == ["b", "c", "a"]
assert isinstance(field.args["b"], Argument)
assert isinstance(field.args["b"].type, NonNull)
assert field.args["b"].type.of_type is True
assert isinstance(field.args["c"], Argument)
assert field.args["c"].type is None
+ assert field.args["c"].deprecation_reason == deprecation_reason
assert isinstance(field.args["a"], Argument)
assert isinstance(field.args["a"].type, NonNull)
assert field.args["a"].type.of_type is False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/graphene-3.1.1/graphene/types/tests/test_inputfield.py
new/graphene-3.2.1/graphene/types/tests/test_inputfield.py
--- old/graphene-3.1.1/graphene/types/tests/test_inputfield.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/types/tests/test_inputfield.py 2022-12-11
21:05:25.000000000 +0100
@@ -1,5 +1,7 @@
from functools import partial
+from pytest import raises
+
from ..inputfield import InputField
from ..structures import NonNull
from .utils import MyLazyType
@@ -12,6 +14,22 @@
assert field.type.of_type == MyType
+def test_inputfield_deprecated():
+ MyType = object()
+ deprecation_reason = "deprecated"
+ field = InputField(MyType, required=False,
deprecation_reason=deprecation_reason)
+ assert isinstance(field.type, type(MyType))
+ assert field.deprecation_reason == deprecation_reason
+
+
+def test_inputfield_required_deprecated():
+ MyType = object()
+ with raises(AssertionError) as exc_info:
+ InputField(MyType, name="input", required=True,
deprecation_reason="deprecated")
+
+ assert str(exc_info.value) == "InputField input is required, cannot
deprecate it."
+
+
def test_inputfield_with_lazy_type():
MyType = object()
field = InputField(lambda: MyType)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/graphene/validation/depth_limit.py
new/graphene-3.2.1/graphene/validation/depth_limit.py
--- old/graphene-3.1.1/graphene/validation/depth_limit.py 2022-09-08
10:55:05.000000000 +0200
+++ new/graphene-3.2.1/graphene/validation/depth_limit.py 2022-12-11
21:05:25.000000000 +0100
@@ -53,7 +53,7 @@
def depth_limit_validator(
max_depth: int,
ignore: Optional[List[IgnoreType]] = None,
- callback: Callable[[Dict[str, int]], None] = None,
+ callback: Optional[Callable[[Dict[str, int]], None]] = None,
):
class DepthLimitValidator(ValidationRule):
def __init__(self, validation_context: ValidationContext):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/graphene-3.1.1/setup.py new/graphene-3.2.1/setup.py
--- old/graphene-3.1.1/setup.py 2022-09-08 10:55:05.000000000 +0200
+++ new/graphene-3.2.1/setup.py 2022-12-11 21:05:25.000000000 +0100
@@ -52,7 +52,6 @@
"pytest-asyncio>=0.16,<2",
"snapshottest>=0.6,<1",
"coveralls>=3.3,<4",
- "promise>=2.3,<3",
"mock>=4,<5",
"pytz==2022.1",
"iso8601>=1,<2",