From 7f59a8fa7e9199ad6cb83e52e85a84944bba7902 Mon Sep 17 00:00:00 2001 From: Eran Kampf Date: Mon, 30 Oct 2017 13:03:01 -0700 Subject: [PATCH 01/44] Added docs --- docs/types/enums.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/types/enums.rst b/docs/types/enums.rst index 96c52e92..6e730628 100644 --- a/docs/types/enums.rst +++ b/docs/types/enums.rst @@ -54,6 +54,13 @@ the ``Enum.from_enum`` function. graphene.Enum.from_enum(AlreadyExistingPyEnum) +``Enum.from_enum`` supports a ``description`` and ``deprecation_reason`` lambdas as input so +you can add description etc. to your enum without changing the original: + +.. code:: python + + graphene.Enum.from_enum(AlreadyExistingPyEnum, description=lambda value: return 'foo' if value == AlreadyExistingPyEnum.Foo else 'bar') + Notes ----- @@ -65,7 +72,7 @@ member getters. In the Python ``Enum`` implementation you can access a member by initing the Enum. .. code:: python - + from enum import Enum class Color(Enum): RED = 1 @@ -78,7 +85,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu However, in Graphene ``Enum`` you need to call get to have the same effect: .. code:: python - + from graphene import Enum class Color(Enum): RED = 1 From 1555c988e05049e49f119d7684fc0172e469c283 Mon Sep 17 00:00:00 2001 From: Eran Kampf Date: Mon, 30 Oct 2017 14:31:20 -0700 Subject: [PATCH 02/44] Fix resolve arguments section according to 2.0 resolvers --- docs/execution/middleware.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index c5e11aa7..a85b1f4b 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -18,10 +18,8 @@ Middlewares ``resolve`` is invoked with several arguments: - ``next`` represents the execution chain. Call ``next`` to continue evalution. - ``root`` is the root value object passed throughout the query. -- ``args`` is the hash of arguments passed to the field. -- ``context`` is the context object passed throughout the query. - ``info`` is the resolver info. - +- ``args`` is the hash of arguments passed to the field. Example ------- From b5abccb1dc733522df19d56e0906890531a700d3 Mon Sep 17 00:00:00 2001 From: Nathaniel Parrish Date: Fri, 3 Nov 2017 16:36:36 -0700 Subject: [PATCH 03/44] Set all fields on input object types --- graphene/types/inputobjecttype.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 38173c79..9b9b646b 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -23,8 +23,8 @@ class InputObjectTypeContainer(dict, BaseType): def __init__(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) - for key, value in self.items(): - setattr(self, key, value) + for key in self._meta.fields.keys(): + setattr(self, key, self.get(key, None)) def __init_subclass__(cls, *args, **kwargs): pass From 9c27db7ed55f90a4d348ca76943c8fa6eb0eb2f7 Mon Sep 17 00:00:00 2001 From: Nathaniel Parrish Date: Tue, 7 Nov 2017 09:06:36 -0800 Subject: [PATCH 04/44] Handle complex input types --- graphene/types/inputobjecttype.py | 25 +++++++++++++++++++++++-- graphene/types/tests/test_typemap.py | 24 +++++++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 9b9b646b..a162ca3c 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -1,11 +1,14 @@ from collections import OrderedDict from .base import BaseOptions, BaseType +from .field import Field from .inputfield import InputField +from .objecttype import ObjectType +from .scalars import Scalar +from .structures import List, NonNull from .unmountedtype import UnmountedType from .utils import yank_fields_from_attrs - # For static type checking with Mypy MYPY = False if MYPY: @@ -24,11 +27,29 @@ class InputObjectTypeContainer(dict, BaseType): def __init__(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) for key in self._meta.fields.keys(): - setattr(self, key, self.get(key, None)) + field = getattr(self, key, None) + if field is None or self.get(key, None) is None: + value = None + else: + value = InputObjectTypeContainer._get_typed_field_value(field, self[key]) + setattr(self, key, value) def __init_subclass__(cls, *args, **kwargs): pass + @staticmethod + def _get_typed_field_value(field_or_type, value): + if isinstance(field_or_type, NonNull): + return InputObjectTypeContainer._get_typed_field_value(field_or_type.of_type, value) + elif isinstance(field_or_type, List): + return [ + InputObjectTypeContainer._get_typed_field_value(field_or_type.of_type, v) + for v in value + ] + elif hasattr(field_or_type, '_meta') and hasattr(field_or_type._meta, 'container'): + return field_or_type._meta.container(value) + else: + return value class InputObjectType(UnmountedType, BaseType): ''' diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 082f25bd..fe8e99b6 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -4,6 +4,7 @@ from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLObjectType, GraphQLString) +from ..structures import List, NonNull from ..dynamic import Dynamic from ..enum import Enum from ..field import Field @@ -11,7 +12,7 @@ from ..inputfield import InputField from ..inputobjecttype import InputObjectType from ..interface import Interface from ..objecttype import ObjectType -from ..scalars import String +from ..scalars import String, Int from ..typemap import TypeMap @@ -119,10 +120,18 @@ def test_interface(): def test_inputobject(): + class OtherObjectType(InputObjectType): + thingy = NonNull(Int) + + class MyInnerObjectType(InputObjectType): + some_field = String() + some_other_field = List(OtherObjectType) + class MyInputObjectType(InputObjectType): '''Description''' foo_bar = String(description='Field description') bar = String(name='gizmo') + baz = NonNull(MyInnerObjectType) own = InputField(lambda: MyInputObjectType) def resolve_foo_bar(self, args, info): @@ -136,14 +145,23 @@ def test_inputobject(): assert graphql_type.description == 'Description' # Container - container = graphql_type.create_container({'bar': 'oh!'}) + container = graphql_type.create_container({ + 'bar': 'oh!', + 'baz': { + 'some_other_field': [{'thingy': 1}, {'thingy': 2}] + } + }) assert isinstance(container, MyInputObjectType) assert 'bar' in container assert container.bar == 'oh!' assert 'foo_bar' not in container + assert container.foo_bar is None + assert container.baz.some_field is None + assert container.baz.some_other_field[0].thingy == 1 + assert container.baz.some_other_field[1].thingy == 2 fields = graphql_type.fields - assert list(fields.keys()) == ['fooBar', 'gizmo', 'own'] + assert list(fields.keys()) == ['fooBar', 'gizmo', 'baz', 'own'] own_field = fields['own'] assert own_field.type == graphql_type foo_field = fields['fooBar'] From b2151d62da46ffda9d8f40b62362d9764a232044 Mon Sep 17 00:00:00 2001 From: Nathaniel Parrish Date: Tue, 7 Nov 2017 09:09:57 -0800 Subject: [PATCH 05/44] Code cleanup --- graphene/types/inputobjecttype.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index a162ca3c..cf76442e 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -1,10 +1,7 @@ from collections import OrderedDict from .base import BaseOptions, BaseType -from .field import Field from .inputfield import InputField -from .objecttype import ObjectType -from .scalars import Scalar from .structures import List, NonNull from .unmountedtype import UnmountedType from .utils import yank_fields_from_attrs @@ -51,6 +48,7 @@ class InputObjectTypeContainer(dict, BaseType): else: return value + class InputObjectType(UnmountedType, BaseType): ''' Input Object Type Definition From 3412dba31fdd5e57dd545eebeb6a116f40f1cf37 Mon Sep 17 00:00:00 2001 From: Stan Zhavoronkov Date: Tue, 14 Nov 2017 13:04:47 +0300 Subject: [PATCH 06/44] fix Unions.rst fix Unions.rst docs (copy-paste sentence from interfaces.rst). --- docs/types/unions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/types/unions.rst b/docs/types/unions.rst index f3d66e02..2c5c5a75 100644 --- a/docs/types/unions.rst +++ b/docs/types/unions.rst @@ -12,8 +12,8 @@ The basics: Quick example ------------- -This example model defines a ``Character`` interface with a name. ``Human`` -and ``Droid`` are two implementations of that interface. +This example model defines several ObjectTypes with their own fields. +``SearchResult`` is the implementation of ``Union`` of this object types. .. code:: python From f6697188ee9cbbe22df5e6971ef9686b589f4daf Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 14 Nov 2017 22:06:28 -0800 Subject: [PATCH 07/44] Added tests for nested inputobjecttypes --- graphene/types/tests/test_inputobjecttype.py | 40 ++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index 77b1eb0e..9f90055d 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -5,6 +5,8 @@ from ..inputfield import InputField from ..inputobjecttype import InputObjectType from ..objecttype import ObjectType from ..unmountedtype import UnmountedType +from ..scalars import String, Boolean +from ..schema import Schema class MyType(object): @@ -51,7 +53,8 @@ def test_ordered_fields_in_inputobjecttype(): field = MyScalar() asa = InputField(MyType) - assert list(MyInputObjectType._meta.fields.keys()) == ['b', 'a', 'field', 'asa'] + assert list(MyInputObjectType._meta.fields.keys()) == [ + 'b', 'a', 'field', 'asa'] def test_generate_inputobjecttype_unmountedtype(): @@ -86,7 +89,8 @@ def test_generate_inputobjecttype_inherit_abstracttype(): field2 = MyScalar(MyType) assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [InputField, InputField] + assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [ + InputField, InputField] def test_generate_inputobjecttype_inherit_abstracttype_reversed(): @@ -97,4 +101,34 @@ def test_generate_inputobjecttype_inherit_abstracttype_reversed(): field2 = MyScalar(MyType) assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [InputField, InputField] + assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [ + InputField, InputField] + + +def test_inputobjecttype_of_input(): + class Child(InputObjectType): + first_name = String() + last_name = String() + + @property + def full_name(self): + return "{} {}".format(self.first_name, self.last_name) + + class Parent(InputObjectType): + child = InputField(Child) + + class Query(ObjectType): + is_child = Boolean(parent=Parent()) + + def resolve_is_child(self, info, parent): + return isinstance(parent.child, Child) and parent.child.full_name == "Peter Griffin" + + schema = Schema(query=Query) + result = schema.execute('''query basequery { + isChild(parent: {child: {firstName: "Peter", lastName: "Griffin"}}) + } + ''') + assert not result.errors + assert result.data == { + 'isChild': True + } From eb5108f81e0a9504014e42ec7e8e980107817f56 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 14 Nov 2017 22:19:22 -0800 Subject: [PATCH 08/44] Simplified inputobjecttype implementation+tests --- graphene/types/inputobjecttype.py | 27 ++++----------------------- graphene/types/tests/test_typemap.py | 24 ++++++++++++++++-------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index cf76442e..76f5dd09 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -2,7 +2,6 @@ from collections import OrderedDict from .base import BaseOptions, BaseType from .inputfield import InputField -from .structures import List, NonNull from .unmountedtype import UnmountedType from .utils import yank_fields_from_attrs @@ -14,7 +13,7 @@ if MYPY: class InputObjectTypeOptions(BaseOptions): fields = None # type: Dict[str, InputField] - create_container = None # type: Callable + container = None # type: InputObjectTypeContainer class InputObjectTypeContainer(dict, BaseType): @@ -24,30 +23,11 @@ class InputObjectTypeContainer(dict, BaseType): def __init__(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) for key in self._meta.fields.keys(): - field = getattr(self, key, None) - if field is None or self.get(key, None) is None: - value = None - else: - value = InputObjectTypeContainer._get_typed_field_value(field, self[key]) - setattr(self, key, value) + setattr(self, key, self.get(key, None)) def __init_subclass__(cls, *args, **kwargs): pass - @staticmethod - def _get_typed_field_value(field_or_type, value): - if isinstance(field_or_type, NonNull): - return InputObjectTypeContainer._get_typed_field_value(field_or_type.of_type, value) - elif isinstance(field_or_type, List): - return [ - InputObjectTypeContainer._get_typed_field_value(field_or_type.of_type, v) - for v in value - ] - elif hasattr(field_or_type, '_meta') and hasattr(field_or_type._meta, 'container'): - return field_or_type._meta.container(value) - else: - return value - class InputObjectType(UnmountedType, BaseType): ''' @@ -73,7 +53,8 @@ class InputObjectType(UnmountedType, BaseType): if container is None: container = type(cls.__name__, (InputObjectTypeContainer, cls), {}) _meta.container = container - super(InputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) + super(InputObjectType, cls).__init_subclass_with_meta__( + _meta=_meta, **options) @classmethod def get_type(cls): diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index fe8e99b6..d4170db6 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -39,7 +39,8 @@ def test_enum(): assert graphql_enum.description == 'Description' values = graphql_enum.values assert values == [ - GraphQLEnumValue(name='foo', value=1, description='Description foo=1', deprecation_reason='Is deprecated'), + GraphQLEnumValue(name='foo', value=1, description='Description foo=1', + deprecation_reason='Is deprecated'), GraphQLEnumValue(name='bar', value=2, description='Description bar=2'), ] @@ -47,7 +48,8 @@ def test_enum(): def test_objecttype(): class MyObjectType(ObjectType): '''Description''' - foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') + foo = String(bar=String(description='Argument description', + default_value='x'), description='Field description') bar = String(name='gizmo') def resolve_foo(self, bar): @@ -92,8 +94,10 @@ def test_dynamic_objecttype(): def test_interface(): class MyInterface(Interface): '''Description''' - foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') - bar = String(name='gizmo', first_arg=String(), other_arg=String(name='oth_arg')) + foo = String(bar=String(description='Argument description', + default_value='x'), description='Field description') + bar = String(name='gizmo', first_arg=String(), + other_arg=String(name='oth_arg')) own = Field(lambda: MyInterface) def resolve_foo(self, args, info): @@ -144,12 +148,16 @@ def test_inputobject(): assert graphql_type.name == 'MyInputObjectType' assert graphql_type.description == 'Description' - # Container + other_graphql_type = typemap['OtherObjectType'] + inner_graphql_type = typemap['MyInnerObjectType'] container = graphql_type.create_container({ 'bar': 'oh!', - 'baz': { - 'some_other_field': [{'thingy': 1}, {'thingy': 2}] - } + 'baz': inner_graphql_type.create_container({ + 'some_other_field': [ + other_graphql_type.create_container({'thingy': 1}), + other_graphql_type.create_container({'thingy': 2}) + ] + }) }) assert isinstance(container, MyInputObjectType) assert 'bar' in container From e71a52140a89946d0e9abdfb95c340dbe34179be Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 14 Nov 2017 23:25:30 -0800 Subject: [PATCH 09/44] Update __init__.py --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index aedf23a9..0198043c 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -34,7 +34,7 @@ from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import -VERSION = (2, 0, 0, 'final', 0) +VERSION = (2, 0, 1, 'final', 0) __version__ = get_version(VERSION) From ec32c252e7fdb7354e42ac931a7a7ee412d4d56c Mon Sep 17 00:00:00 2001 From: Geoff Date: Mon, 20 Nov 2017 10:05:28 -0500 Subject: [PATCH 10/44] Relay documentation reflects api changes in 2.0 Specifically, get_node_from_global_id. --- docs/relay/nodes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index 74f42094..5d0ea35a 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -75,10 +75,10 @@ Accessing node types -------------------- If we want to retrieve node instances from a ``global_id`` (scalar that identifies an instance by it's type name and id), -we can simply do ``Node.get_node_from_global_id(global_id, context, info)``. +we can simply do ``Node.get_node_from_global_id(info, global_id)``. In the case we want to restrict the instance retrieval to a specific type, we can do: -``Node.get_node_from_global_id(global_id, context, info, only_type=Ship)``. This will raise an error +``Node.get_node_from_global_id(info, global_id, only_type=Ship)``. This will raise an error if the ``global_id`` doesn't correspond to a Ship type. From 834d52f9d3ca49e2a39fdf89da945f296b160c60 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Sat, 25 Nov 2017 12:51:53 +0000 Subject: [PATCH 11/44] Use more Pythonic terminology here "Hash" in the Python world implies a cryptographic hash, or possibly a checksum. Here, I believe "hash" is being used to mean "hash map", which would be more commonly known in Python as a dictionary, or dict for short. --- 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 a85b1f4b..7ef98150 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -19,7 +19,7 @@ Middlewares ``resolve`` is invoked with several arguments: - ``next`` represents the execution chain. Call ``next`` to continue evalution. - ``root`` is the root value object passed throughout the query. - ``info`` is the resolver info. -- ``args`` is the hash of arguments passed to the field. +- ``args`` is the dict of arguments passed to the field. Example ------- From 98366c6add10fa1532301145e9768bffcbb09dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Chauvet?= Date: Wed, 29 Nov 2017 14:32:17 +0100 Subject: [PATCH 12/44] Added Date scalar to documentation --- docs/types/scalars.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 0958722d..0d979b5d 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -11,6 +11,7 @@ Graphene defines the following base Scalar Types: Graphene also provides custom scalars for Dates, Times, and JSON: +- ``graphene.types.datetime.Date`` - ``graphene.types.datetime.DateTime`` - ``graphene.types.datetime.Time`` - ``graphene.types.json.JSONString`` From 27745078bcf416b2818b1a43ce06029765988e42 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 8 Dec 2017 14:58:03 +0000 Subject: [PATCH 13/44] Document functional middleware --- docs/execution/middleware.rst | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 7ef98150..01180c07 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -3,7 +3,7 @@ Middleware You can use ``middleware`` to affect the evaluation of fields in your schema. -A middleware is any object that responds to ``resolve(*args, next_middleware)``. +A middleware is any object or function that responds to ``resolve(next_middleware, *args)``. Inside that method, it should either: @@ -40,3 +40,32 @@ And then execute it with: .. code:: python result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()]) + + +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 + +.. code:: python + + from time import time as timer + + def timing_middleware(next, root, info, **args): + start = timer() + return_value = next(root, info, **args) + duration = timer() - start + logger.debug("{parent_type}.{field_name}: {duration}".format( + parent_type=root._meta.name if root else '', + field_name=info.field_name, + duration=duration + )) + return return_value + + +And then execute it with: + +.. code:: python + + result = schema.execute('THE QUERY', middleware=[timing_middleware]) From 502597c5a4db303890c9d1729a83dd928ae1a78c Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 8 Dec 2017 15:03:20 +0000 Subject: [PATCH 14/44] Format duration --- docs/execution/middleware.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 01180c07..203cd881 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -56,10 +56,10 @@ logs the time it takes to resolve each field start = timer() return_value = next(root, info, **args) duration = timer() - start - logger.debug("{parent_type}.{field_name}: {duration}".format( + logger.debug("{parent_type}.{field_name}: {duration} ms".format( parent_type=root._meta.name if root else '', field_name=info.field_name, - duration=duration + duration=round(duration * 1000, 2) )) return return_value From 76f8a359164cbca2d9f4ce6506699cc84b0ff18e Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 10 Dec 2017 15:53:28 +0100 Subject: [PATCH 15/44] Add documentation for scalar type arguments --- docs/types/scalars.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 0d979b5d..7ab1e73d 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -1,6 +1,33 @@ Scalars ======= +All Scalar types accept the following arguments. All are optional: + +``name``: *string* + + Override the name of the Field. + +``description``: *string* + + A description of the type to show in the GraphiQL browser. + +``required``: *boolean* + + If ``True``, the server will enforce a value for this field. See `NonNull <./list-and-nonnull.html#nonnull>`_. Default is ``False``. + +``deprecation_reason``: *string* + + Provide a deprecation reason for the Field. + +``default_value``: *any* + + Provide a default value for the Field. + + + +Base scalars +------------ + Graphene defines the following base Scalar Types: - ``graphene.String`` From 0a6921d2d048e17943e2d5ec3760e43d81b6145b Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 10 Dec 2017 16:07:48 +0100 Subject: [PATCH 16/44] Add extra documentation on base scalars --- docs/types/scalars.rst | 54 +++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 7ab1e73d..b6e8f654 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -30,18 +30,54 @@ Base scalars Graphene defines the following base Scalar Types: -- ``graphene.String`` -- ``graphene.Int`` -- ``graphene.Float`` -- ``graphene.Boolean`` -- ``graphene.ID`` +``graphene.String`` + + Represents textual data, represented as UTF-8 + character sequences. The String type is most often used by GraphQL to + represent free-form human-readable text. + +``graphene.Int`` + + Represents non-fractional signed whole numeric + values. Int can represent values between `-(2^53 - 1)` and `2^53 - 1` since + represented in JSON as double-precision floating point numbers specified + by `IEEE 754 `_. + +``graphene.Float`` + + Represents signed double-precision fractional + values as specified by + `IEEE 754 `_. + +``graphene.Boolean`` + + Represents `true` or `false`. + +``graphene.ID`` + + Represents a unique identifier, often used to + refetch an object or as key for a cache. The ID type appears in a JSON + response as a String; however, it is not intended to be human-readable. + When expected as an input type, any string (such as `"4"`) or integer + (such as `4`) input value will be accepted as an ID. Graphene also provides custom scalars for Dates, Times, and JSON: -- ``graphene.types.datetime.Date`` -- ``graphene.types.datetime.DateTime`` -- ``graphene.types.datetime.Time`` -- ``graphene.types.json.JSONString`` +``graphene.types.datetime.Date`` + + Represents a Date value as specified by `iso8601 `_. + +``graphene.types.datetime.DateTime`` + + Represents a DateTime value as specified by `iso8601 `_. + +``graphene.types.datetime.Time`` + + Represents a Time value as specified by `iso8601 `_. + +``graphene.types.json.JSONString`` + + Represents a JSON string. Custom scalars From 7afeaa052bf90f9fabb5ded21f63cc724f1d1180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=B6rpel?= Date: Sat, 16 Dec 2017 00:03:36 +0100 Subject: [PATCH 17/44] Docs: fix typos in example for relay nodes --- docs/relay/nodes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index 5d0ea35a..f8601bdf 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -55,7 +55,7 @@ Example of a custom node: return '{}:{}'.format(type, id) @staticmethod - def get_node_from_global_id(info global_id, only_type=None): + def get_node_from_global_id(info, global_id, only_node=None): type, id = global_id.split(':') if only_node: # We assure that the node type that we want to retrieve From a16c5ba00be1d963d438c5e96962b79d1b145cc3 Mon Sep 17 00:00:00 2001 From: Devin Fee Date: Sat, 16 Dec 2017 21:49:23 -0800 Subject: [PATCH 18/44] extended support for subclassing with meta to Enum and InputObjectType --- graphene/tests/issues/test_425.py | 77 +++++++++++++++++++++++++++++++ graphene/types/enum.py | 5 +- graphene/types/inputobjecttype.py | 5 +- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/graphene/tests/issues/test_425.py b/graphene/tests/issues/test_425.py index 7f92a75a..d50edf84 100644 --- a/graphene/tests/issues/test_425.py +++ b/graphene/tests/issues/test_425.py @@ -2,8 +2,11 @@ # Adapted for Graphene 2.0 from graphene.types.objecttype import ObjectType, ObjectTypeOptions +from graphene.types.inputobjecttype import InputObjectType, InputObjectTypeOptions +from graphene.types.enum import Enum, EnumOptions +# ObjectType class SpecialOptions(ObjectTypeOptions): other_attr = None @@ -40,3 +43,77 @@ def test_special_objecttype_inherit_meta_options(): assert MyType._meta.name == 'MyType' assert MyType._meta.default_resolver is None assert MyType._meta.interfaces == () + + +# InputObjectType +class SpecialInputObjectTypeOptions(ObjectTypeOptions): + other_attr = None + + +class SpecialInputObjectType(InputObjectType): + + @classmethod + def __init_subclass_with_meta__(cls, other_attr='default', **options): + _meta = SpecialInputObjectTypeOptions(cls) + _meta.other_attr = other_attr + super(SpecialInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + +def test_special_inputobjecttype_could_be_subclassed(): + class MyInputObjectType(SpecialInputObjectType): + + class Meta: + other_attr = 'yeah!' + + assert MyInputObjectType._meta.other_attr == 'yeah!' + + +def test_special_inputobjecttype_could_be_subclassed_default(): + class MyInputObjectType(SpecialInputObjectType): + pass + + assert MyInputObjectType._meta.other_attr == 'default' + + +def test_special_inputobjecttype_inherit_meta_options(): + class MyInputObjectType(SpecialInputObjectType): + pass + + assert MyInputObjectType._meta.name == 'MyInputObjectType' + + +# Enum +class SpecialEnumOptions(EnumOptions): + other_attr = None + + +class SpecialEnum(Enum): + + @classmethod + def __init_subclass_with_meta__(cls, other_attr='default', **options): + _meta = SpecialEnumOptions(cls) + _meta.other_attr = other_attr + super(SpecialEnum, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + +def test_special_enum_could_be_subclassed(): + class MyEnum(SpecialEnum): + + class Meta: + other_attr = 'yeah!' + + assert MyEnum._meta.other_attr == 'yeah!' + + +def test_special_enum_could_be_subclassed_default(): + class MyEnum(SpecialEnum): + pass + + assert MyEnum._meta.other_attr == 'default' + + +def test_special_enum_inherit_meta_options(): + class MyEnum(SpecialEnum): + pass + + assert MyEnum._meta.name == 'MyEnum' diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 4e3f2ac2..da0b6240 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -60,8 +60,9 @@ class EnumMeta(SubclassWithMeta_Meta): class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)): @classmethod - def __init_subclass_with_meta__(cls, enum=None, **options): - _meta = EnumOptions(cls) + def __init_subclass_with_meta__(cls, enum=None, _meta=None, **options): + if not _meta: + _meta = EnumOptions(cls) _meta.enum = enum or cls.__enum__ _meta.deprecation_reason = options.pop('deprecation_reason', None) for key, value in _meta.enum.__members__.items(): diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 76f5dd09..dbfccc46 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -40,8 +40,9 @@ class InputObjectType(UnmountedType, BaseType): ''' @classmethod - def __init_subclass_with_meta__(cls, container=None, **options): - _meta = InputObjectTypeOptions(cls) + def __init_subclass_with_meta__(cls, container=None, _meta=None, **options): + if not _meta: + _meta = InputObjectTypeOptions(cls) fields = OrderedDict() for base in reversed(cls.__mro__): From d6968c2e92486dda40725c9ef99f450557003f2b Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Wed, 20 Dec 2017 18:43:51 +0000 Subject: [PATCH 19/44] Raise better error if type is missing from schema --- graphene/types/tests/test_typemap.py | 22 +++++++++++++++++++++- graphene/types/typemap.py | 5 ++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index d4170db6..c0626a1a 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -1,3 +1,4 @@ +import pytest from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, GraphQLField, GraphQLInputObjectField, @@ -13,7 +14,7 @@ from ..inputobjecttype import InputObjectType from ..interface import Interface from ..objecttype import ObjectType from ..scalars import String, Int -from ..typemap import TypeMap +from ..typemap import TypeMap, resolve_type def test_enum(): @@ -232,3 +233,22 @@ def test_objecttype_with_possible_types(): assert graphql_type.is_type_of assert graphql_type.is_type_of({}, None) is True assert graphql_type.is_type_of(MyObjectType(), None) is False + + +def test_resolve_type_with_missing_type(): + class MyObjectType(ObjectType): + foo_bar = String() + + class MyOtherObjectType(ObjectType): + fizz_buzz = String() + + def resolve_type_func(root, info): + return MyOtherObjectType + + typemap = TypeMap([MyObjectType]) + with pytest.raises(AssertionError) as excinfo: + resolve_type( + resolve_type_func, typemap, 'MyOtherObjectType', {}, {} + ) + + assert 'MyOtherObjectTyp' in str(excinfo.value) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index d4a4d157..b2bc4a0e 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -46,7 +46,10 @@ def resolve_type(resolve_type_func, map, type_name, root, info): if inspect.isclass(_type) and issubclass(_type, ObjectType): graphql_type = map.get(_type._meta.name) - assert graphql_type and graphql_type.graphene_type == _type, ( + assert graphql_type, "Can't find type {} in schema".format( + _type._meta.name + ) + assert graphql_type.graphene_type == _type, ( 'The type {} does not match with the associated graphene type {}.' ).format(_type, graphql_type.graphene_type) return graphql_type From a2178dc22098a36e56d4cec021f984f4cae3649c Mon Sep 17 00:00:00 2001 From: Brian Chapman Date: Mon, 8 Jan 2018 09:16:02 -0800 Subject: [PATCH 20/44] Allow ObjectType to set Connection name --- graphene/relay/connection.py | 2 +- graphene/relay/tests/test_connection.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index afe6ffb3..3e2e9ad9 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -73,7 +73,7 @@ class Connection(ObjectType): edge = type(edge_name, edge_bases, {}) cls.Edge = edge - _meta.name = name + options['name'] = name _meta.node = node _meta.fields = OrderedDict([ ('page_info', Field(PageInfo, name='pageInfo', required=True)), diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index b6a26df3..36978882 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -52,6 +52,21 @@ def test_connection_inherit_abstracttype(): assert list(fields.keys()) == ['page_info', 'edges', 'extra'] +def test_connection_name(): + custom_name = "MyObjectCustomNameConnection" + + class BaseConnection(object): + extra = String() + + class MyObjectConnection(BaseConnection, Connection): + + class Meta: + node = MyObject + name = custom_name + + assert MyObjectConnection._meta.name == custom_name + + def test_edge(): class MyObjectConnection(Connection): @@ -122,9 +137,10 @@ def test_connectionfield_node_deprecated(): field = ConnectionField(MyObject) with pytest.raises(Exception) as exc_info: field.type - + assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str(exc_info.value) + def test_connectionfield_custom_args(): class MyObjectConnection(Connection): From 9dde259f5427c2438d3cbd25daea7736b408f644 Mon Sep 17 00:00:00 2001 From: Roman Fursov Date: Wed, 10 Jan 2018 17:23:25 +0300 Subject: [PATCH 21/44] Change .title method to .capitalize in to_camel_case in str_convertes.py --- graphene/utils/str_converters.py | 6 +++--- graphene/utils/tests/test_str_converters.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/graphene/utils/str_converters.py b/graphene/utils/str_converters.py index ae8ceffe..6fcdfb7b 100644 --- a/graphene/utils/str_converters.py +++ b/graphene/utils/str_converters.py @@ -1,13 +1,13 @@ import re -# From this response in Stackoverflow +# Adapted from this response in Stackoverflow # http://stackoverflow.com/a/19053800/1072990 def to_camel_case(snake_str): components = snake_str.split('_') # We capitalize the first letter of each component except the first one - # with the 'title' method and join them together. - return components[0] + "".join(x.title() if x else '_' for x in components[1:]) + # with the 'capitalize' method and join them together. + return components[0] + ''.join(x.capitalize() if x else '_' for x in components[1:]) # From this response in Stackoverflow diff --git a/graphene/utils/tests/test_str_converters.py b/graphene/utils/tests/test_str_converters.py index 2ee7d7a5..11f7e155 100644 --- a/graphene/utils/tests/test_str_converters.py +++ b/graphene/utils/tests/test_str_converters.py @@ -16,6 +16,7 @@ def test_camel_case(): assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane' assert to_camel_case('snakes_on_a__plane') == 'snakesOnA_Plane' assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria' + assert to_camel_case('field_i18n') == 'fieldI18n' def test_to_const(): From 8123c4ad8f945ec2154ce6368e3ec78e4c76700a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 20 Jan 2018 15:17:52 -0800 Subject: [PATCH 22/44] Fix relay links. --- docs/relay/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/relay/index.rst b/docs/relay/index.rst index e3a87d08..7eb418df 100644 --- a/docs/relay/index.rst +++ b/docs/relay/index.rst @@ -21,9 +21,9 @@ Useful links - `Relay Cursor Connection Specification`_ - `Relay input Object Mutation`_ -.. _Relay: https://facebook.github.io/relay/docs/graphql-relay-specification.html +.. _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/graphql-relay-specification.html +.. _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 input Object Mutation: https://facebook.github.io/relay/graphql/mutations.htm From 035ff7ef885e9e158093359e93e5723298ab177e Mon Sep 17 00:00:00 2001 From: Dean Kleissas Date: Wed, 24 Jan 2018 16:48:14 -0500 Subject: [PATCH 23/44] Update middleware example to support introspection In the `timing_middleware` example, introspection queries will fail due to `Schema` and others not having `_meta` attributes. API will work and tests pass but introspection will fail, which can be quite confusing to the developer. Simple update makes sure the `root` variable has a `_meta` attribute before accessing it. --- 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 203cd881..55efe730 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -57,7 +57,7 @@ logs the time it takes to resolve each field return_value = next(root, info, **args) duration = timer() - start logger.debug("{parent_type}.{field_name}: {duration} ms".format( - parent_type=root._meta.name if root else '', + parent_type=root._meta.name if root and hasattr(root, '_meta') else '', field_name=info.field_name, duration=round(duration * 1000, 2) )) From 4fa0df401af289c43ff7e3ec73492455446c275e Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Mon, 5 Feb 2018 16:49:16 +0200 Subject: [PATCH 24/44] Fix broken GitHub link tags: 2.0 -> v2.0.0 --- UPGRADE-v2.0.md | 2 +- graphene/relay/connection.py | 2 +- graphene/types/abstracttype.py | 2 +- graphene/types/mutation.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 89a0435b..8ef09343 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -21,7 +21,7 @@ developer has to write to use them. > The type metaclasses are now deleted as they are no longer necessary. If your code was depending -> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/2.0/graphene/tests/issues/test_425.py). +> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/v2.0.0/graphene/tests/issues/test_425.py). ## Deprecations diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 3e2e9ad9..aa2eeec3 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -102,7 +102,7 @@ class IterableConnectionField(Field): if is_node(type): raise Exception( "ConnectionField's now need a explicit ConnectionType for Nodes.\n" - "Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#node-connections" + "Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#node-connections" ) assert issubclass(connection_type, Connection), ( diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index 3e283493..1693cc8e 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -7,6 +7,6 @@ class AbstractType(SubclassWithMeta): def __init_subclass__(cls, *args, **kwargs): warn_deprecation( "Abstract type is deprecated, please use normal object inheritance instead.\n" - "See more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#deprecations" + "See more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#deprecations" ) super(AbstractType, cls).__init_subclass__(*args, **kwargs) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 25794d47..84a509aa 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -50,7 +50,7 @@ class Mutation(ObjectType): warn_deprecation(( "Please use {name}.Arguments instead of {name}.Input." "Input is now only used in ClientMutationID.\n" - "Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#mutation-input" + "Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input" ).format(name=cls.__name__)) if input_class: From e26c0a3717c4ff3f90ed8bd710436f8cb6ff4713 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Wed, 7 Feb 2018 12:06:29 -0800 Subject: [PATCH 25/44] Add documentation on NonNull lists --- docs/types/list-and-nonnull.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/types/list-and-nonnull.rst b/docs/types/list-and-nonnull.rst index b48aa187..a127a9d2 100644 --- a/docs/types/list-and-nonnull.rst +++ b/docs/types/list-and-nonnull.rst @@ -48,3 +48,24 @@ Lists work in a similar way: We can use a type modifier to mark a type as a ``List``, which indicates that this field will return a list of that type. It works the same for arguments, where the validation step will expect a list for that value. + +NonNull Lists +------------- + +By default items in a list will be considered nullable. To define a list without +any nullable items the type needs to be marked as ``NonNull``. For example: + +.. code:: python + + import graphene + + class Character(graphene.ObjectType): + appears_in = graphene.List(graphene.NonNull(graphene.String)) + +The above results in the type definition: + +.. code:: + + type Character { + appearsIn: [String!] + } From 368a2a02fe38be577c7274cdd3e0788a2b2155b5 Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Thu, 8 Feb 2018 01:26:27 +0200 Subject: [PATCH 26/44] (Fit line length < 120) --- graphene/types/mutation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 84a509aa..97a1bd5e 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -50,7 +50,8 @@ class Mutation(ObjectType): warn_deprecation(( "Please use {name}.Arguments instead of {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" + "Read more:" + " https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input" ).format(name=cls.__name__)) if input_class: From f4b21c7a75fc9ff36533b204d5193fb713e67292 Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Thu, 8 Feb 2018 01:31:12 +0200 Subject: [PATCH 27/44] Update .gitignore for pytest 3.4+ Pytest 3.4.0 changes the default cache directory from `.cache` to `.pytest_cache`. Changelog: https://docs.pytest.org/en/latest/changelog.html#pytest-3-4-0-2018-01-30 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d98ebfc3..0e3fcd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ htmlcov/ .coverage .coverage.* .cache +.pytest_cache nosetests.xml coverage.xml *,cover From 265719d11f0245823ea72e990b5de77458655fc6 Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Thu, 8 Feb 2018 01:32:10 +0200 Subject: [PATCH 28/44] Tox: Add py36 to default envlist --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 48d4f5fc..f2eccefb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py33,py34,py35,pypy +envlist = flake8,py27,py33,py34,py35,py36,pypy skipsdist = true [testenv] From a7a4ba62af4cb8b00ffa6a9f8cf79bd87dfb5e95 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 8 Feb 2018 23:56:13 -0800 Subject: [PATCH 29/44] Enabled possibility of setting name, description or deprecation_reason in mutation Fields Fixed #634, $660 #626 and #593 --- graphene/types/mutation.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 97a1bd5e..fe15f6a2 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -76,7 +76,12 @@ class Mutation(ObjectType): super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod - def Field(cls, *args, **kwargs): + def Field(cls, name=None, description=None, deprecation_reason=None): return Field( - cls._meta.output, args=cls._meta.arguments, resolver=cls._meta.resolver + cls._meta.output, + args=cls._meta.arguments, + resolver=cls._meta.resolver, + name=name, + description=description, + deprecation_reason=deprecation_reason, ) From 0971a05b33bf65cbc89038eebd85de5fec4e1dcf Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 9 Feb 2018 10:49:04 -0800 Subject: [PATCH 30/44] Improved support / assertion for graphql types in Schema --- graphene/types/schema.py | 45 ++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 8066de3e..7fd513b2 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -1,6 +1,6 @@ import inspect -from graphql import GraphQLSchema, graphql, is_type +from graphql import GraphQLSchema, graphql, is_type, GraphQLObjectType from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective, GraphQLSkipDirective) from graphql.type.introspection import IntrospectionSchema @@ -12,6 +12,17 @@ from .objecttype import ObjectType from .typemap import TypeMap, is_graphene_type +def assert_valid_root_type(_type): + if _type is None: + return + is_graphene_objecttype = inspect.isclass( + _type) and issubclass(_type, ObjectType) + is_graphql_objecttype = isinstance(_type, GraphQLObjectType) + assert is_graphene_objecttype or is_graphql_objecttype, ( + "Type {} is not a valid ObjectType." + ).format(_type) + + class Schema(GraphQLSchema): ''' Schema Definition @@ -20,21 +31,23 @@ class Schema(GraphQLSchema): query and mutation (optional). ''' - def __init__(self, query=None, mutation=None, subscription=None, - directives=None, types=None, auto_camelcase=True): - assert inspect.isclass(query) and issubclass(query, ObjectType), ( - 'Schema query must be Object Type but got: {}.' - ).format(query) + def __init__(self, + query=None, + mutation=None, + subscription=None, + directives=None, + types=None, + auto_camelcase=True): + assert_valid_root_type(query) + assert_valid_root_type(mutation) + assert_valid_root_type(subscription) self._query = query self._mutation = mutation self._subscription = subscription self.types = types self.auto_camelcase = auto_camelcase if directives is None: - directives = [ - GraphQLIncludeDirective, - GraphQLSkipDirective - ] + directives = [GraphQLIncludeDirective, GraphQLSkipDirective] assert all(isinstance(d, GraphQLDirective) for d in directives), \ 'Schema directives must be List[GraphQLDirective] if provided but got: {}.'.format( @@ -61,7 +74,8 @@ class Schema(GraphQLSchema): ''' _type = super(Schema, self).get_type(type_name) if _type is None: - raise AttributeError('Type "{}" not found in the Schema'.format(type_name)) + raise AttributeError( + 'Type "{}" not found in the Schema'.format(type_name)) if isinstance(_type, GrapheneGraphQLType): return _type.graphene_type return _type @@ -73,7 +87,8 @@ class Schema(GraphQLSchema): return _type if is_graphene_type(_type): graphql_type = self.get_type(_type._meta.name) - assert graphql_type, "Type {} not found in this schema.".format(_type._meta.name) + assert graphql_type, "Type {} not found in this schema.".format( + _type._meta.name) assert graphql_type.graphene_type == _type return graphql_type raise Exception("{} is not a valid GraphQL type.".format(_type)) @@ -102,4 +117,8 @@ class Schema(GraphQLSchema): ] if self.types: initial_types += self.types - self._type_map = TypeMap(initial_types, auto_camelcase=self.auto_camelcase, schema=self) + self._type_map = TypeMap( + initial_types, + auto_camelcase=self.auto_camelcase, + schema=self + ) From 42c96a453fa1a40452af18e3729e9b3c9cc05474 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 17 Feb 2018 20:18:49 +0000 Subject: [PATCH 31/44] Added failing test --- graphene/relay/tests/test_connection.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 36978882..c206f714 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,6 +1,6 @@ import pytest -from ...types import Argument, Field, Int, List, NonNull, ObjectType, String +from ...types import Argument, Field, Int, List, NonNull, ObjectType, String, Schema from ..connection import Connection, ConnectionField, PageInfo from ..node import Node @@ -155,3 +155,23 @@ def test_connectionfield_custom_args(): 'last': Argument(Int), 'extra': Argument(String), } + + +def test_connectionfield_required(): + class MyObjectConnection(Connection): + + class Meta: + node = MyObject + + class Query(ObjectType): + test_connection = ConnectionField(MyObjectConnection, required=True) + + def resolve_test_connection(root, info, **args): + return [] + + schema = Schema(query=Query) + executed = schema.execute( + '{ testConnection { edges { cursor } } }' + ) + assert not executed.errors + assert executed.data == {'testConnection': {'edges': []}} From 38baf7ab52f164db61df2c83d27102b0f3860cae Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 17 Feb 2018 20:18:55 +0000 Subject: [PATCH 32/44] Handle relay connection field being required --- graphene/relay/connection.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index aa2eeec3..1ea672fb 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -99,7 +99,10 @@ class IterableConnectionField(Field): def type(self): type = super(IterableConnectionField, self).type connection_type = type - if is_node(type): + if isinstance(type, NonNull): + connection_type = type.of_type + + if is_node(connection_type): raise Exception( "ConnectionField's now need a explicit ConnectionType for Nodes.\n" "Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#node-connections" @@ -108,10 +111,13 @@ class IterableConnectionField(Field): assert issubclass(connection_type, Connection), ( '{} type have to be a subclass of Connection. Received "{}".' ).format(self.__class__.__name__, connection_type) - return connection_type + return type @classmethod def resolve_connection(cls, connection_type, args, resolved): + if isinstance(connection_type, NonNull): + connection_type = connection_type.of_type + if isinstance(resolved, connection_type): return resolved From c25bcb334573504a7e42143bfbb4a175307c79ef Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 17 Feb 2018 21:50:40 +0000 Subject: [PATCH 33/44] Move NonNull check --- graphene/relay/connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 1ea672fb..bcd238d3 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -115,9 +115,6 @@ class IterableConnectionField(Field): @classmethod def resolve_connection(cls, connection_type, args, resolved): - if isinstance(connection_type, NonNull): - connection_type = connection_type.of_type - if isinstance(resolved, connection_type): return resolved @@ -139,6 +136,9 @@ class IterableConnectionField(Field): def connection_resolver(cls, resolver, connection_type, root, info, **args): resolved = resolver(root, info, **args) + if isinstance(connection_type, NonNull): + connection_type = connection_type.of_type + on_resolve = partial(cls.resolve_connection, connection_type, args) if is_thenable(resolved): return Promise.resolve(resolved).then(on_resolve) From d3b708533d05ef13a6b15e7a3f29eac23f812a09 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 17 Feb 2018 23:28:33 +0000 Subject: [PATCH 34/44] Fix kwarg name in example. Fixes #533 --- docs/relay/nodes.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index f8601bdf..7af00ea1 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -55,12 +55,12 @@ Example of a custom node: return '{}:{}'.format(type, id) @staticmethod - def get_node_from_global_id(info, global_id, only_node=None): + def get_node_from_global_id(info, global_id, only_type=None): type, id = global_id.split(':') - if only_node: + if only_type: # We assure that the node type that we want to retrieve # is the same that was indicated in the field type - assert type == only_node._meta.name, 'Received not compatible node.' + assert type == only_type._meta.name, 'Received not compatible node.' if type == 'User': return get_user(id) From 79f7cc08e3c173e78ab33abe0d49404242f8d26d Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sun, 18 Feb 2018 17:21:19 +0000 Subject: [PATCH 35/44] Exclude examples module in setup.py Fixes #608 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 52d7de0c..d1ec3139 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ setup( keywords='api graphql protocol rest relay graphene', - packages=find_packages(exclude=['tests', 'tests.*']), + packages=find_packages(exclude=['tests', 'tests.*', 'examples']), install_requires=[ 'six>=1.10.0,<2', From 84fbf5dc23351c26a852271aa6529a60bce13735 Mon Sep 17 00:00:00 2001 From: Anis Jonischkeit Date: Tue, 27 Feb 2018 10:00:20 +1000 Subject: [PATCH 36/44] Made DateTime types return GraphQLError on fail This change makes it so that when an incorrectly formatted date string gets passed to a Date / Time argument a GraphQLError is returned rather than a GraphQLLocatedError. Since Date / Time are types, their errors should not be in the same class as errors in your application. This is also inline with how other types work in graphene (graphene.Int, graphene.Float) --- graphene/types/datetime.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 3b0e3f13..b3e14682 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -38,8 +38,10 @@ class Date(Scalar): @staticmethod def parse_value(value): - return iso8601.parse_date(value).date() - + try: + return iso8601.parse_date(value).date() + except iso8601.ParseError: + return None class DateTime(Scalar): ''' @@ -62,8 +64,10 @@ class DateTime(Scalar): @staticmethod def parse_value(value): - return iso8601.parse_date(value) - + try: + return iso8601.parse_date(value) + except iso8601.ParseError: + return None class Time(Scalar): ''' @@ -87,5 +91,8 @@ class Time(Scalar): @classmethod def parse_value(cls, value): - dt = iso8601.parse_date('{}T{}'.format(cls.epoch_date, value)) - return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) + try: + dt = iso8601.parse_date('{}T{}'.format(cls.epoch_date, value)) + return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) + except iso8601.ParseError: + return None From 1a1efbd77d3958110747a3cbe9638cacab33d148 Mon Sep 17 00:00:00 2001 From: Anis Jonischkeit Date: Tue, 27 Feb 2018 10:21:41 +1000 Subject: [PATCH 37/44] linting: added two lines after end of class --- graphene/types/datetime.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index b3e14682..c3c78fb2 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -43,6 +43,7 @@ class Date(Scalar): except iso8601.ParseError: return None + class DateTime(Scalar): ''' The `DateTime` scalar type represents a DateTime @@ -69,6 +70,7 @@ class DateTime(Scalar): except iso8601.ParseError: return None + class Time(Scalar): ''' The `Time` scalar type represents a Time value as From 2a67ffeb35493968fa3aa1177a088a538208e420 Mon Sep 17 00:00:00 2001 From: Anis Jonischkeit Date: Wed, 14 Mar 2018 12:51:34 +1000 Subject: [PATCH 38/44] fixed function name for test to be what it is actually testing and prevent name colision --- graphene/types/tests/test_datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index bfe491a3..b5ffba7a 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -34,7 +34,7 @@ def test_datetime_query(): assert result.data == {'datetime': isoformat} -def test_datetime_query(): +def test_date_query(): now = datetime.datetime.now().replace(tzinfo=pytz.utc).date() isoformat = now.isoformat() From 9973fd314f6134a96a78617fee1caf21d592b6af Mon Sep 17 00:00:00 2001 From: Anis Jonischkeit Date: Wed, 14 Mar 2018 13:06:10 +1000 Subject: [PATCH 39/44] added tests for when bad input is passed into date/datetime/time fields --- graphene/types/tests/test_datetime.py | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index b5ffba7a..b516e497 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -2,6 +2,8 @@ import datetime import pytz +from graphql import GraphQLError + from ..datetime import DateTime, Date, Time from ..objecttype import ObjectType from ..schema import Schema @@ -53,6 +55,32 @@ def test_time_query(): assert not result.errors assert result.data == {'time': isoformat} +def test_bad_datetime_query(): + not_a_date = "Some string that's not a date" + + result = schema.execute('''{ datetime(in: "%s") }''' % not_a_date) + + assert len(result.errors) == 1 + assert isinstance(result.errors[0], GraphQLError) + assert result.data == None + +def test_bad_date_query(): + not_a_date = "Some string that's not a date" + + result = schema.execute('''{ date(in: "%s") }''' % not_a_date) + + assert len(result.errors) == 1 + assert isinstance(result.errors[0], GraphQLError) + assert result.data == None + +def test_bad_time_query(): + not_a_date = "Some string that's not a date" + + result = schema.execute('''{ time(at: "%s") }''' % not_a_date) + + assert len(result.errors) == 1 + assert isinstance(result.errors[0], GraphQLError) + assert result.data == None def test_datetime_query_variable(): now = datetime.datetime.now().replace(tzinfo=pytz.utc) From d6df14923dd47df69f7bec7909cb70809f31eea0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 13 Mar 2018 22:37:36 -0700 Subject: [PATCH 40/44] Updated docs template --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 31a23482..b6e0cd75 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ # Required library Sphinx==1.5.3 # Docs template -https://github.com/graphql-python/graphene-python.org/archive/docs.zip +http://graphene-python.org/sphinx_graphene_theme.zip From 2594cdb61409f8ecedf0958ee959f8f8ded3efbc Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 14 Mar 2018 22:15:24 -0700 Subject: [PATCH 41/44] Fixed Meta included in Enum members. Fixed #685 --- graphene/types/enum.py | 6 +++++- graphene/types/tests/test_enum.py | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index da0b6240..67a3f6b2 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -27,7 +27,11 @@ class EnumOptions(BaseOptions): class EnumMeta(SubclassWithMeta_Meta): def __new__(cls, name, bases, classdict, **options): - enum = PyEnum(cls.__name__, OrderedDict(classdict, __eq__=eq_enum)) + enum_members = OrderedDict(classdict, __eq__=eq_enum) + # We remove the Meta attribute from the class to not collide + # with the enum values. + enum_members.pop('Meta', None) + enum = PyEnum(cls.__name__, enum_members) return SubclassWithMeta_Meta.__new__(cls, name, bases, OrderedDict(classdict, __enum__=enum), **options) def get(cls, value): diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index c4cf3b85..fdd5f4e4 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -80,7 +80,8 @@ def test_enum_from_builtin_enum_accepts_lambda_description(): return 'meh' if value == Episode.NEWHOPE else None PyEpisode = PyEnum('PyEpisode', 'NEWHOPE,EMPIRE,JEDI') - Episode = Enum.from_enum(PyEpisode, description=custom_description, deprecation_reason=custom_deprecation_reason) + Episode = Enum.from_enum(PyEpisode, description=custom_description, + deprecation_reason=custom_deprecation_reason) class Query(ObjectType): foo = Episode() @@ -214,3 +215,19 @@ def test_enum_to_enum_comparison_should_differ(): assert RGB1.RED != RGB2.RED assert RGB1.GREEN != RGB2.GREEN assert RGB1.BLUE != RGB2.BLUE + + +def test_enum_skip_meta_from_members(): + class RGB1(Enum): + class Meta: + name = 'RGB' + + RED = 1 + GREEN = 2 + BLUE = 3 + + assert dict(RGB1._meta.enum.__members__) == { + 'RED': RGB1.RED, + 'GREEN': RGB1.GREEN, + 'BLUE': RGB1.BLUE, + } From a96ed550bd0c67b7a9ec0b9f636f71c530441e5f Mon Sep 17 00:00:00 2001 From: abawchen Date: Tue, 20 Mar 2018 08:14:59 +0800 Subject: [PATCH 42/44] Fix deprecations url in DeprecationWarning message. --- graphene/types/abstracttype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index 1693cc8e..aaa0ff37 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -7,6 +7,6 @@ class AbstractType(SubclassWithMeta): def __init_subclass__(cls, *args, **kwargs): warn_deprecation( "Abstract type is deprecated, please use normal object inheritance instead.\n" - "See more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#deprecations" + "See more: https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md#deprecations" ) super(AbstractType, cls).__init_subclass__(*args, **kwargs) From 562cafc14f66bbad7f76e7600fa0acd71d00194e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 21 Mar 2018 21:20:07 -0700 Subject: [PATCH 43/44] Fixed str on abstract types. Improved repr --- graphene/types/base.py | 2 +- graphene/types/tests/test_objecttype.py | 25 +++++++++++++++++++------ graphene/utils/subclass_with_meta.py | 11 +++++++++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/graphene/types/base.py b/graphene/types/base.py index d9c3bdd4..50242674 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -21,7 +21,7 @@ class BaseOptions(object): raise Exception("Can't modify frozen Options {0}".format(self)) def __repr__(self): - return "<{} type={}>".format(self.__class__.__name__, self.class_type.__name__) + return "<{} name={}>".format(self.__class__.__name__, repr(self.name)) class BaseType(SubclassWithMeta): diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index e7c76d97..73d3823c 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -44,6 +44,8 @@ def test_generate_objecttype(): assert MyObjectType._meta.description == "Documentation" assert MyObjectType._meta.interfaces == tuple() assert MyObjectType._meta.fields == {} + assert repr( + MyObjectType) == ">" def test_generate_objecttype_with_meta(): @@ -65,7 +67,6 @@ def test_generate_lazy_objecttype(): class InnerObjectType(ObjectType): field = Field(MyType) - assert MyObjectType._meta.name == "MyObjectType" example_field = MyObjectType._meta.fields['example'] @@ -115,7 +116,8 @@ def test_generate_objecttype_inherit_abstracttype(): assert MyObjectType._meta.interfaces == () assert MyObjectType._meta.name == "MyObjectType" assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field] + assert list(map(type, MyObjectType._meta.fields.values())) == [ + Field, Field] def test_generate_objecttype_inherit_abstracttype_reversed(): @@ -129,7 +131,8 @@ def test_generate_objecttype_inherit_abstracttype_reversed(): assert MyObjectType._meta.interfaces == () assert MyObjectType._meta.name == "MyObjectType" assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field] + assert list(map(type, MyObjectType._meta.fields.values())) == [ + Field, Field] def test_generate_objecttype_unmountedtype(): @@ -145,7 +148,8 @@ def test_parent_container_get_fields(): def test_parent_container_interface_get_fields(): - assert list(ContainerWithInterface._meta.fields.keys()) == ['ifield', 'field1', 'field2'] + assert list(ContainerWithInterface._meta.fields.keys()) == [ + 'ifield', 'field1', 'field2'] def test_objecttype_as_container_only_args(): @@ -182,7 +186,8 @@ def test_objecttype_as_container_invalid_kwargs(): with pytest.raises(TypeError) as excinfo: Container(unexisting_field="3") - assert "'unexisting_field' is an invalid keyword argument for Container" == str(excinfo.value) + assert "'unexisting_field' is an invalid keyword argument for Container" == str( + excinfo.value) def test_objecttype_container_benchmark(benchmark): @@ -238,7 +243,6 @@ def test_objecttype_no_fields_output(): def resolve_user(self, info): return User() - schema = Schema(query=Query) result = schema.execute(''' query basequery { user { @@ -252,3 +256,12 @@ def test_objecttype_no_fields_output(): 'name': None, } } + + +def test_abstract_objecttype_can_str(): + class MyObjectType(ObjectType): + class Meta: + abstract = True + field = MyScalar() + + assert str(MyObjectType) == "MyObjectType" diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index 9226e418..61205be0 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -6,9 +6,15 @@ from .props import props class SubclassWithMeta_Meta(InitSubclassMeta): + _meta = None + + def __str__(cls): + if cls._meta: + return cls._meta.name + return cls.__name__ def __repr__(cls): - return cls._meta.name + return "<{} meta={}>".format(cls.__name__, repr(cls._meta)) class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): @@ -24,7 +30,8 @@ class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): elif isclass(_Meta): _meta_props = props(_Meta) else: - raise Exception("Meta have to be either a class or a dict. Received {}".format(_Meta)) + raise Exception( + "Meta have to be either a class or a dict. Received {}".format(_Meta)) delattr(cls, "Meta") options = dict(meta_options, **_meta_props) From d46d8e8c33b1432ed664d94f432f357c44f3447e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 21 Mar 2018 21:31:56 -0700 Subject: [PATCH 44/44] Allow mutations to be required. Improved testing. Fixed #694 --- graphene/types/mutation.py | 6 ++++-- graphene/types/tests/test_mutation.py | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index fe15f6a2..6b864e07 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -73,10 +73,11 @@ class Mutation(ObjectType): _meta.resolver = resolver _meta.arguments = arguments - super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) + super(Mutation, cls).__init_subclass_with_meta__( + _meta=_meta, **options) @classmethod - def Field(cls, name=None, description=None, deprecation_reason=None): + def Field(cls, name=None, description=None, deprecation_reason=None, required=False): return Field( cls._meta.output, args=cls._meta.arguments, @@ -84,4 +85,5 @@ class Mutation(ObjectType): name=name, description=description, deprecation_reason=deprecation_reason, + required=required, ) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 91ab14d2..df17477d 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -6,6 +6,7 @@ from ..mutation import Mutation from ..objecttype import ObjectType from ..scalars import String from ..schema import Schema +from ..structures import NonNull def test_generate_mutation_no_args(): @@ -133,3 +134,27 @@ def test_mutation_no_fields_output(): 'name': None, } } + + +def test_mutation_allow_to_have_custom_args(): + class CreateUser(Mutation): + + class Arguments: + name = String() + + name = String() + + def mutate(self, info, name): + return CreateUser(name=name) + + class MyMutation(ObjectType): + create_user = CreateUser.Field( + description='Create a user', + deprecation_reason='Is deprecated', + required=True + ) + + field = MyMutation._meta.fields['create_user'] + assert field.description == 'Create a user' + assert field.deprecation_reason == 'Is deprecated' + assert field.type == NonNull(CreateUser)