From c98d91ba1c74130d8ce3e4d52e78a8067d147633 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 20:33:03 -0700 Subject: [PATCH 01/82] Simplified Interface code --- UPGRADE-v2.0.md | 25 ++++++++++++ graphene/__init__.py | 19 +++++---- graphene/pyutils/init_subclass.py | 15 +++++++ graphene/types/__init__.py | 5 ++- graphene/types/abstracttype.py | 45 +++------------------ graphene/types/base.py | 37 +++++++++++++++++ graphene/types/interface.py | 56 +++++++++----------------- graphene/types/tests/test_interface.py | 6 +-- graphene/types/utils.py | 4 +- graphene/utils/subclass_with_meta.py | 24 +++++++++++ 10 files changed, 141 insertions(+), 95 deletions(-) create mode 100644 UPGRADE-v2.0.md create mode 100644 graphene/pyutils/init_subclass.py create mode 100644 graphene/types/base.py create mode 100644 graphene/utils/subclass_with_meta.py diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md new file mode 100644 index 00000000..28a2034c --- /dev/null +++ b/UPGRADE-v2.0.md @@ -0,0 +1,25 @@ +# v1.0 Upgrade Guide + +## Deprecations + + +* AbstractType is deprecated, please use normal inheritance instead. + + Before: + + ```python + class CommonFields(AbstractType): + name = String() + + class Pet(CommonFields, Interface): + pass + ``` + + With 2.0: + ```python + class CommonFields(object): + name = String() + + class Pet(CommonFields, Interface): + pass + ``` diff --git a/graphene/__init__.py b/graphene/__init__.py index b2e166db..556a8175 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -33,20 +33,19 @@ if not __SETUP__: Dynamic, Union, ) - from .relay import ( - Node, - is_node, - GlobalID, - ClientIDMutation, - Connection, - ConnectionField, - PageInfo - ) + # from .relay import ( + # Node, + # is_node, + # GlobalID, + # ClientIDMutation, + # Connection, + # ConnectionField, + # PageInfo + # ) from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import __all__ = [ - 'AbstractType', 'ObjectType', 'InputObjectType', 'Interface', diff --git a/graphene/pyutils/init_subclass.py b/graphene/pyutils/init_subclass.py new file mode 100644 index 00000000..62f9a6db --- /dev/null +++ b/graphene/pyutils/init_subclass.py @@ -0,0 +1,15 @@ + +class InitSubclassMeta(type): + """Metaclass that implements PEP 487 protocol""" + def __new__(cls, name, bases, ns): + __init_subclass__ = ns.pop('__init_subclass__', None) + if __init_subclass__: + __init_subclass__ = classmethod(__init_subclass__) + ns['__init_subclass__'] = __init_subclass__ + return type.__new__(cls, name, bases, ns) + + def __init__(cls, name, bases, ns): + super(InitSubclassMeta, cls).__init__(name, bases, ns) + super_class = super(cls, cls) + if hasattr(super_class, '__init_subclass__'): + super_class.__init_subclass__.__func__(cls) diff --git a/graphene/types/__init__.py b/graphene/types/__init__.py index fcb3fbfd..40d60df9 100644 --- a/graphene/types/__init__.py +++ b/graphene/types/__init__.py @@ -1,7 +1,6 @@ # flake8: noqa from .objecttype import ObjectType -from .abstracttype import AbstractType from .interface import Interface from .mutation import Mutation from .scalars import Scalar, String, ID, Int, Float, Boolean @@ -15,9 +14,11 @@ from .inputobjecttype import InputObjectType from .dynamic import Dynamic from .union import Union +# Deprecated +from .abstracttype import AbstractType + __all__ = [ - 'AbstractType', 'ObjectType', 'InputObjectType', 'Interface', diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index a5bbb73e..dcfd7a20 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -1,41 +1,8 @@ -import six - -from ..utils.is_base_type import is_base_type -from .options import Options -from .utils import get_base_fields, merge, yank_fields_from_attrs - - class AbstractTypeMeta(type): - ''' - AbstractType Definition - - When we want to share fields across multiple types, like a Interface, - a ObjectType and a Input ObjectType we can use AbstractTypes for defining - our fields that the other types will inherit from. - ''' - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # AbstractType - if not is_base_type(bases, AbstractTypeMeta): - return type.__new__(cls, name, bases, attrs) - - for base in bases: - if not issubclass(base, AbstractType) and issubclass(type(base), AbstractTypeMeta): - # raise Exception('You can only extend AbstractTypes after the base definition.') - return type.__new__(cls, name, bases, attrs) - - base_fields = get_base_fields(bases, _as=None) - - fields = yank_fields_from_attrs(attrs, _as=None) - - options = Options( - fields=merge(base_fields, fields) - ) - cls = type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - return cls - - -class AbstractType(six.with_metaclass(AbstractTypeMeta)): pass + + +class AbstractType(object): + def __init_subclass__(cls, *args, **kwargs): + print("Abstract type is deprecated") + super(AbstractType, cls).__init_subclass__(*args, **kwargs) diff --git a/graphene/types/base.py b/graphene/types/base.py new file mode 100644 index 00000000..325d36c2 --- /dev/null +++ b/graphene/types/base.py @@ -0,0 +1,37 @@ +from ..utils.subclass_with_meta import SubclassWithMeta +from ..utils.trim_docstring import trim_docstring + + +class BaseOptions(object): + name = None + description = None + + _frozen = False + + def __init__(self, class_type): + self.class_type = class_type + + def freeze(self): + self._frozen = True + + def __setattr__(self, name, value): + if not self._frozen: + super(BaseOptions, self).__setattr__(name, value) + else: + raise Exception("Can't modify frozen Options {0}".format(self)) + + def __repr__(self): + return "<{} type={}>".format(self.__class__.__name__, self.class_type.__name__) + + +class BaseType(SubclassWithMeta): + @classmethod + def __init_subclass_with_meta__(cls, name=None, description=None, _meta=None): + assert "_meta" not in cls.__dict__, "Can't assign directly meta" + if not _meta: + return + _meta.name = name or cls.__name__ + _meta.description = description or trim_docstring(cls.__doc__) + _meta.freeze() + cls._meta = _meta + super(BaseType, cls).__init_subclass_with_meta__() diff --git a/graphene/types/interface.py b/graphene/types/interface.py index f0980b6c..adbd7c5c 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -1,45 +1,15 @@ -import six - -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .abstracttype import AbstractTypeMeta from .field import Field -from .options import Options -from .utils import get_base_fields, merge, yank_fields_from_attrs +from .utils import yank_fields_from_attrs +from collections import OrderedDict + +from .base import BaseOptions, BaseType -class InterfaceMeta(AbstractTypeMeta): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # Interface - if not is_base_type(bases, InterfaceMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=trim_docstring(attrs.get('__doc__')), - local_fields=None, - ) - - options.base_fields = get_base_fields(bases, _as=Field) - - if not options.local_fields: - options.local_fields = yank_fields_from_attrs(attrs, _as=Field) - - options.fields = merge( - options.base_fields, - options.local_fields - ) - - return type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - def __str__(cls): # noqa: N802 - return cls._meta.name +class InterfaceOptions(BaseOptions): + fields = None # type: Dict[str, Field] -class Interface(six.with_metaclass(InterfaceMeta)): +class Interface(BaseType): ''' Interface Type Definition @@ -48,6 +18,18 @@ class Interface(six.with_metaclass(InterfaceMeta)): all types, as well as a function to determine which type is actually used when the field is resolved. ''' + @classmethod + def __init_subclass_with_meta__(cls, **options): + _meta = InterfaceOptions(cls) + + fields = OrderedDict() + for base in reversed(cls.__mro__): + fields.update( + yank_fields_from_attrs(base.__dict__, _as=Field) + ) + + _meta.fields = fields + super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod def resolve_type(cls, instance, context, info): diff --git a/graphene/types/tests/test_interface.py b/graphene/types/tests/test_interface.py index f9c12250..444ff4a6 100644 --- a/graphene/types/tests/test_interface.py +++ b/graphene/types/tests/test_interface.py @@ -1,5 +1,3 @@ - -from ..abstracttype import AbstractType from ..field import Field from ..interface import Interface from ..unmountedtype import UnmountedType @@ -61,7 +59,7 @@ def test_generate_interface_unmountedtype(): def test_generate_interface_inherit_abstracttype(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyInterface(Interface, MyAbstractType): @@ -84,7 +82,7 @@ def test_generate_interface_inherit_interface(): def test_generate_interface_inherit_abstracttype_reversed(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyInterface(MyAbstractType, Interface): diff --git a/graphene/types/utils.py b/graphene/types/utils.py index 19cc06e7..7c59bbaf 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -49,7 +49,7 @@ def get_field_as(value, _as=None): return _as.mounted(value) -def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True): +def yank_fields_from_attrs(attrs, _as=None, sort=True): ''' Extract all the fields in given attributes (dict) and return them ordered @@ -60,8 +60,6 @@ def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True): if not field: continue fields_with_names.append((attname, field)) - if delete: - del attrs[attname] if sort: fields_with_names = sorted(fields_with_names, key=lambda f: f[1]) diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py new file mode 100644 index 00000000..63fd0ec0 --- /dev/null +++ b/graphene/utils/subclass_with_meta.py @@ -0,0 +1,24 @@ +from .props import props +from ..pyutils.init_subclass import InitSubclassMeta + + +class SubclassWithMeta(object): + """This class improves __init_subclass__ to receive automatically the options from meta""" + # We will only have the metaclass in Python 2 + __metaclass__ = InitSubclassMeta + + def __init_subclass__(cls, **meta_options): + """This method just terminates the super() chain""" + _Meta = getattr(cls, "Meta", None) + _meta_props = {} + if _Meta: + _meta_props = props(_Meta) + delattr(cls, "Meta") + options = dict(meta_options, **_meta_props) + super_class = super(cls, cls) + if hasattr(super_class, '__init_subclass_with_meta__'): + super_class.__init_subclass_with_meta__(**options) + + @classmethod + def __init_subclass_with_meta__(cls, **meta_options): + """This method just terminates the super() chain""" From e487206818a17848b381fddd68e8e5e6e4f3dc61 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 20:53:49 -0700 Subject: [PATCH 02/82] Simplified ObjectType logic --- graphene/types/base.py | 8 +-- graphene/types/objecttype.py | 103 +++++++++++++---------------------- 2 files changed, 42 insertions(+), 69 deletions(-) diff --git a/graphene/types/base.py b/graphene/types/base.py index 325d36c2..b4a87e73 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -3,13 +3,13 @@ from ..utils.trim_docstring import trim_docstring class BaseOptions(object): - name = None - description = None + name = None # type: str + description = None # type: str - _frozen = False + _frozen = False # type: bool def __init__(self, class_type): - self.class_type = class_type + self.class_type = class_type # type: Type def freeze(self): self._frozen = True diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 1f0e5b8d..ac591e05 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -1,82 +1,55 @@ from collections import OrderedDict -import six - -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .abstracttype import AbstractTypeMeta from .field import Field from .interface import Interface -from .options import Options -from .utils import get_base_fields, merge, yank_fields_from_attrs +from .utils import yank_fields_from_attrs + +from .base import BaseOptions, BaseType -class ObjectTypeMeta(AbstractTypeMeta): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # ObjectType - if not is_base_type(bases, ObjectTypeMeta): - return type.__new__(cls, name, bases, attrs) - - _meta = attrs.pop('_meta', None) - defaults = dict( - name=name, - description=trim_docstring(attrs.get('__doc__')), - interfaces=(), - possible_types=(), - default_resolver=None, - local_fields=OrderedDict(), - ) - if not _meta: - options = Options( - attrs.pop('Meta', None), - **defaults - ) - else: - options = _meta.extend_with_defaults(defaults) - - options.base_fields = get_base_fields(bases, _as=Field) - - if not options.local_fields: - options.local_fields = yank_fields_from_attrs(attrs=attrs, _as=Field) - - options.interface_fields = OrderedDict() - for interface in options.interfaces: - assert issubclass(interface, Interface), ( - 'All interfaces of {} must be a subclass of Interface. Received "{}".' - ).format(name, interface) - options.interface_fields.update(interface._meta.fields) - - options.fields = merge( - options.interface_fields, - options.base_fields, - options.local_fields - ) - - cls = type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - assert not (options.possible_types and cls.is_type_of), ( - '{}.Meta.possible_types will cause type collision with {}.is_type_of. ' - 'Please use one or other.' - ).format(name, name) - - for interface in options.interfaces: - interface.implements(cls) - - return cls - - def __str__(cls): # noqa: N802 - return cls._meta.name +class ObjectTypeOptions(BaseOptions): + fields = None # type: Dict[str, Field] + interfaces = () # type: List[Type[Interface]] -class ObjectType(six.with_metaclass(ObjectTypeMeta)): +class ObjectTypeMeta(type): + pass + +class ObjectType(BaseType): ''' Object Type Definition Almost all of the GraphQL types you define will be object types. Object types have a name, but most importantly describe their fields. ''' + @classmethod + def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, **options): + _meta = ObjectTypeOptions(cls) + + fields = OrderedDict() + + for interface in interfaces: + assert issubclass(interface, Interface), ( + 'All interfaces of {} must be a subclass of Interface. Received "{}".' + ).format(name, interface) + fields.update(interface._meta.fields) + + for base in reversed(cls.__mro__): + fields.update( + yank_fields_from_attrs(base.__dict__, _as=Field) + ) + + assert not (possible_types and cls.is_type_of), ( + '{name}.Meta.possible_types will cause type collision with {name}.is_type_of. ' + 'Please use one or other.' + ).format(name=cls.__name__) + + _meta.fields = fields + _meta.interfaces = interfaces + _meta.possible_types = possible_types + _meta.default_resolver = default_resolver + + super(ObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) is_type_of = None From 5ee6e2b8e79b47ac00ce77f07ce97d2bb99fa222 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 20:59:02 -0700 Subject: [PATCH 03/82] Simplified Scalar --- graphene/types/scalars.py | 31 +++++++++-------------------- graphene/types/tests/test_scalar.py | 11 ++++++++++ 2 files changed, 20 insertions(+), 22 deletions(-) create mode 100644 graphene/types/tests/test_scalar.py diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index e1ff80d3..1ca17ff8 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -1,34 +1,17 @@ import six + from graphql.language.ast import (BooleanValue, FloatValue, IntValue, StringValue) -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .options import Options from .unmountedtype import UnmountedType +from .base import BaseOptions, BaseType -class ScalarTypeMeta(type): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of Model - # (excluding Model class itself). - if not is_base_type(bases, ScalarTypeMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=trim_docstring(attrs.get('__doc__')), - ) - - return type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - def __str__(cls): # noqa: N802 - return cls._meta.name +class ScalarOptions(BaseOptions): + pass -class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): +class Scalar(UnmountedType, BaseType): ''' Scalar Type Definition @@ -36,6 +19,10 @@ class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): Scalars (or Enums) and are defined with a name and a series of functions used to parse input from ast or variables and to ensure validity. ''' + @classmethod + def __init_subclass_with_meta__(cls, **options): + _meta = ScalarOptions(cls) + super(Scalar, cls).__init_subclass_with_meta__(_meta=_meta, **options) serialize = None parse_value = None diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py new file mode 100644 index 00000000..af62faa1 --- /dev/null +++ b/graphene/types/tests/test_scalar.py @@ -0,0 +1,11 @@ +import pytest + +from ..scalars import Scalar + + +def test_scalar(): + class JSONScalar(Scalar): + '''Documentation''' + + assert JSONScalar._meta.name == "JSONScalar" + assert JSONScalar._meta.description == "Documentation" From d8b42dd1ca6aefe77f5f46e19617ab77f4f9ee52 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 20:59:44 -0700 Subject: [PATCH 04/82] Simplified InputObjectType --- graphene/types/inputobjecttype.py | 55 +++++++++++-------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 1796988a..d3d82392 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -1,45 +1,17 @@ -import six +from collections import OrderedDict -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .abstracttype import AbstractTypeMeta from .inputfield import InputField -from .options import Options from .unmountedtype import UnmountedType -from .utils import get_base_fields, merge, yank_fields_from_attrs +from .utils import yank_fields_from_attrs + +from .base import BaseOptions, BaseType -class InputObjectTypeMeta(AbstractTypeMeta): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # InputObjectType - if not is_base_type(bases, InputObjectTypeMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=trim_docstring(attrs.get('__doc__')), - local_fields=None, - ) - - options.base_fields = get_base_fields(bases, _as=InputField) - - if not options.local_fields: - options.local_fields = yank_fields_from_attrs(attrs, _as=InputField) - - options.fields = merge( - options.base_fields, - options.local_fields - ) - return type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - def __str__(cls): # noqa: N802 - return cls._meta.name +class InputObjectTypeOptions(BaseOptions): + fields = None # type: Dict[str, Field] -class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)): +class InputObjectType(UnmountedType, BaseType): ''' Input Object Type Definition @@ -49,6 +21,19 @@ class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)): Using `NonNull` will ensure that a value must be provided by the query ''' + @classmethod + def __init_subclass_with_meta__(cls, **options): + _meta = InputObjectTypeOptions(cls) + + fields = OrderedDict() + for base in reversed(cls.__mro__): + fields.update( + yank_fields_from_attrs(base.__dict__, _as=InputField) + ) + + _meta.fields = fields + super(InputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) + @classmethod def get_type(cls): ''' From 2d557d6ed729063834dd3c6b39c6584e8a183ab2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 21:16:41 -0700 Subject: [PATCH 05/82] Simplified mutation --- graphene/types/mutation.py | 69 ++++++++++++++++++++++++------------ graphene/types/objecttype.py | 5 +-- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 2e8e120a..4b662f0d 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -1,33 +1,58 @@ -from functools import partial +from collections import OrderedDict -import six - -from ..utils.is_base_type import is_base_type from ..utils.get_unbound_function import get_unbound_function from ..utils.props import props from .field import Field -from .objecttype import ObjectType, ObjectTypeMeta +from .utils import yank_fields_from_attrs +from .objecttype import ObjectType, ObjectTypeOptions + +from .base import BaseOptions, BaseType -class MutationMeta(ObjectTypeMeta): - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # Mutation - if not is_base_type(bases, MutationMeta): - return type.__new__(cls, name, bases, attrs) +class MutationOptions(ObjectTypeOptions): + arguments = None # type: Dict[str, Argument] + output = None # type: Type[ObjectType] + resolver = None # type: Function - input_class = attrs.pop('Input', None) - cls = ObjectTypeMeta.__new__(cls, name, bases, attrs) - field_args = props(input_class) if input_class else {} - output_class = getattr(cls, 'Output', cls) - resolver = getattr(cls, 'mutate', None) +class Mutation(ObjectType): + ''' + Mutation Type Definition + ''' + @classmethod + def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None, **options): + _meta = MutationOptions(cls) + + output = output or getattr(cls, 'Output', None) + fields = {} + if not output: + # If output is defined, we don't need to get the fields + fields = OrderedDict() + for base in reversed(cls.__mro__): + fields.update( + yank_fields_from_attrs(base.__dict__, _as=Field) + ) + output = cls + + if not arguments: + input_class = getattr(cls, 'Input', None) + if input_class: + arguments = props(input_class) + else: + arguments = {} + + resolver = resolver or get_unbound_function(getattr(cls, 'mutate', None)) assert resolver, 'All mutations must define a mutate method in it' - resolver = get_unbound_function(resolver) - cls.Field = partial( - Field, output_class, args=field_args, resolver=resolver) - return cls + _meta.fields = fields + _meta.output = output + _meta.resolver = resolver + _meta.arguments = arguments -class Mutation(six.with_metaclass(MutationMeta, ObjectType)): - pass + super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + @classmethod + def Field(cls, *args, **kwargs): + return Field( + cls._meta.output, args=cls._meta.arguments, resolver=cls._meta.resolver + ) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index ac591e05..e641867a 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -23,8 +23,9 @@ class ObjectType(BaseType): have a name, but most importantly describe their fields. ''' @classmethod - def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, **options): - _meta = ObjectTypeOptions(cls) + def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, _meta=None, **options): + if not _meta: + _meta = ObjectTypeOptions(cls) fields = OrderedDict() From f0edcb224a595a8fa29567b3da3cc7a1b19c2c24 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 21:23:39 -0700 Subject: [PATCH 06/82] Removed AbstractType --- graphene/types/abstracttype.py | 6 +-- graphene/types/tests/test_abstracttype.py | 42 -------------------- graphene/types/tests/test_definition.py | 14 +------ graphene/types/tests/test_inputobjecttype.py | 5 +-- graphene/types/tests/test_objecttype.py | 5 +-- graphene/types/utils.py | 4 +- 6 files changed, 11 insertions(+), 65 deletions(-) delete mode 100644 graphene/types/tests/test_abstracttype.py diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index dcfd7a20..0fe75a44 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -1,8 +1,8 @@ -class AbstractTypeMeta(type): - pass - +from ..pyutils.init_subclass import InitSubclassMeta class AbstractType(object): + __metaclass__ = InitSubclassMeta + def __init_subclass__(cls, *args, **kwargs): print("Abstract type is deprecated") super(AbstractType, cls).__init_subclass__(*args, **kwargs) diff --git a/graphene/types/tests/test_abstracttype.py b/graphene/types/tests/test_abstracttype.py deleted file mode 100644 index dabf1e68..00000000 --- a/graphene/types/tests/test_abstracttype.py +++ /dev/null @@ -1,42 +0,0 @@ - -from ..abstracttype import AbstractType -from ..field import Field -from ..unmountedtype import UnmountedType - - -class MyType(object): - pass - - -class MyScalar(UnmountedType): - - def get_type(self): - return MyType - - -def test_generate_abstracttype_with_fields(): - class MyAbstractType(AbstractType): - field = Field(MyType) - - assert 'field' in MyAbstractType._meta.fields - assert isinstance(MyAbstractType._meta.fields['field'], Field) - - -def test_generate_abstracttype_with_unmountedfields(): - class MyAbstractType(AbstractType): - field = UnmountedType(MyType) - - assert 'field' in MyAbstractType._meta.fields - assert isinstance(MyAbstractType._meta.fields['field'], UnmountedType) - - -def test_generate_abstracttype_inheritance(): - class MyAbstractType1(AbstractType): - field1 = UnmountedType(MyType) - - class MyAbstractType2(MyAbstractType1): - field2 = UnmountedType(MyType) - - assert list(MyAbstractType2._meta.fields.keys()) == ['field1', 'field2'] - assert not hasattr(MyAbstractType1, 'field1') - assert not hasattr(MyAbstractType2, 'field2') diff --git a/graphene/types/tests/test_definition.py b/graphene/types/tests/test_definition.py index b040c42d..af9168c9 100644 --- a/graphene/types/tests/test_definition.py +++ b/graphene/types/tests/test_definition.py @@ -1,6 +1,5 @@ -from ..abstracttype import AbstractType from ..argument import Argument from ..enum import Enum from ..field import Field @@ -296,7 +295,7 @@ def test_stringifies_simple_types(): def test_does_not_mutate_passed_field_definitions(): - class CommonFields(AbstractType): + class CommonFields(object): field1 = String() field2 = String(id=String()) @@ -307,12 +306,8 @@ def test_does_not_mutate_passed_field_definitions(): pass assert TestObject1._meta.fields == TestObject2._meta.fields - assert CommonFields._meta.fields == { - 'field1': String(), - 'field2': String(id=String()), - } - class CommonFields(AbstractType): + class CommonFields(object): field1 = String() field2 = String() @@ -323,8 +318,3 @@ def test_does_not_mutate_passed_field_definitions(): pass assert TestInputObject1._meta.fields == TestInputObject2._meta.fields - - assert CommonFields._meta.fields == { - 'field1': String(), - 'field2': String(), - } diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index 7f8eaa7a..7cd3064c 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -1,5 +1,4 @@ -from ..abstracttype import AbstractType from ..field import Field from ..argument import Argument from ..inputfield import InputField @@ -80,7 +79,7 @@ def test_generate_inputobjecttype_as_argument(): def test_generate_inputobjecttype_inherit_abstracttype(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar(MyType) class MyInputObjectType(InputObjectType, MyAbstractType): @@ -91,7 +90,7 @@ def test_generate_inputobjecttype_inherit_abstracttype(): def test_generate_inputobjecttype_inherit_abstracttype_reversed(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar(MyType) class MyInputObjectType(MyAbstractType, InputObjectType): diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index 8a1cf898..2304d7b6 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -1,6 +1,5 @@ import pytest -from ..abstracttype import AbstractType from ..field import Field from ..interface import Interface from ..objecttype import ObjectType @@ -89,7 +88,7 @@ def test_ordered_fields_in_objecttype(): def test_generate_objecttype_inherit_abstracttype(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyObjectType(ObjectType, MyAbstractType): @@ -103,7 +102,7 @@ def test_generate_objecttype_inherit_abstracttype(): def test_generate_objecttype_inherit_abstracttype_reversed(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyObjectType(MyAbstractType, ObjectType): diff --git a/graphene/types/utils.py b/graphene/types/utils.py index 7c59bbaf..598b7f1f 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -23,9 +23,9 @@ def get_base_fields(bases, _as=None): Get all the fields in the given bases ''' fields = OrderedDict() - from ..types import AbstractType, Interface + from ..types import Interface # We allow inheritance in AbstractTypes and Interfaces but not ObjectTypes - inherited_bases = (AbstractType, Interface) + inherited_bases = (Interface) for base in bases: if base in inherited_bases or not issubclass(base, inherited_bases): continue From b78b8c4134dc0947aedefe0e75edc849f2c9014b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 21:27:20 -0700 Subject: [PATCH 07/82] Removed unused code --- graphene/types/abstracttype.py | 1 + graphene/types/objecttype.py | 3 --- graphene/types/options.py | 42 ---------------------------------- 3 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 graphene/types/options.py diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index 0fe75a44..8e132c66 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -1,5 +1,6 @@ from ..pyutils.init_subclass import InitSubclassMeta + class AbstractType(object): __metaclass__ = InitSubclassMeta diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index e641867a..5b6690cd 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -12,9 +12,6 @@ class ObjectTypeOptions(BaseOptions): interfaces = () # type: List[Type[Interface]] -class ObjectTypeMeta(type): - pass - class ObjectType(BaseType): ''' Object Type Definition diff --git a/graphene/types/options.py b/graphene/types/options.py deleted file mode 100644 index 7cefbea0..00000000 --- a/graphene/types/options.py +++ /dev/null @@ -1,42 +0,0 @@ -import inspect - -from ..utils.props import props - - -class Options(object): - ''' - This is the class wrapper around Meta. - It helps to validate and cointain the attributes inside - ''' - - def __init__(self, meta=None, **defaults): - if meta: - assert inspect.isclass(meta), ( - 'Meta have to be a class, received "{}".'.format(repr(meta)) - ) - - meta_attrs = props(meta) if meta else {} - for attr_name, value in defaults.items(): - if attr_name in meta_attrs: - value = meta_attrs.pop(attr_name) - setattr(self, attr_name, value) - - # If meta_attrs is not empty, it implicitly means - # it received invalid attributes - if meta_attrs: - raise TypeError( - "Invalid attributes: {}".format( - ', '.join(sorted(meta_attrs.keys())) - ) - ) - - def extend_with_defaults(self, defaults): - for attr_name, value in defaults.items(): - if not hasattr(self, attr_name): - setattr(self, attr_name, value) - return self - - def __repr__(self): - options_props = props(self) - props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()]) - return ''.format(props_as_attrs) From 02c203f7486e4ee493aff0a37ac26390c232a6da Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 21:31:38 -0700 Subject: [PATCH 08/82] Simplified Union implementation --- graphene/types/union.py | 46 ++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/graphene/types/union.py b/graphene/types/union.py index d4af88ed..3068a96c 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -1,38 +1,14 @@ -import six - -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .options import Options from .unmountedtype import UnmountedType +from .objecttype import ObjectType + +from .base import BaseOptions, BaseType -class UnionMeta(type): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # Union - if not is_base_type(bases, UnionMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=trim_docstring(attrs.get('__doc__')), - types=(), - ) - - assert ( - isinstance(options.types, (list, tuple)) and - len(options.types) > 0 - ), 'Must provide types for Union {}.'.format(options.name) - - return type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - def __str__(cls): # noqa: N805 - return cls._meta.name +class UnionOptions(BaseOptions): + types = () # type: List[Type[ObjectType]] -class Union(six.with_metaclass(UnionMeta, UnmountedType)): +class Union(UnmountedType, BaseType): ''' Union Type Definition @@ -40,6 +16,16 @@ class Union(six.with_metaclass(UnionMeta, UnmountedType)): is used to describe what types are possible as well as providing a function to determine which type is actually used when the field is resolved. ''' + @classmethod + def __init_subclass_with_meta__(cls, types=None, **options): + assert ( + isinstance(types, (list, tuple)) and + len(types) > 0 + ), 'Must provide types for Union {name}.'.format(name=cls.__name__) + + _meta = UnionOptions(cls) + _meta.types = types + super(Union, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod def get_type(cls): From 858343a8f6d20e4a62d80ba11ba97d21158ce8d6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 21:33:03 -0700 Subject: [PATCH 09/82] Removed unused functions --- graphene/types/utils.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/graphene/types/utils.py b/graphene/types/utils.py index 598b7f1f..affffe38 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -8,35 +8,6 @@ from .mountedtype import MountedType from .unmountedtype import UnmountedType -def merge(*dicts): - ''' - Merge the dicts into one - ''' - merged = OrderedDict() - for _dict in dicts: - merged.update(_dict) - return merged - - -def get_base_fields(bases, _as=None): - ''' - Get all the fields in the given bases - ''' - fields = OrderedDict() - from ..types import Interface - # We allow inheritance in AbstractTypes and Interfaces but not ObjectTypes - inherited_bases = (Interface) - for base in bases: - if base in inherited_bases or not issubclass(base, inherited_bases): - continue - for name, field in base._meta.fields.items(): - if name in fields: - continue - fields[name] = get_field_as(field, _as=_as) - - return fields - - def get_field_as(value, _as=None): ''' Get type mounted From 3604c8f17213fff520a0003d0322d0151c737c0d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 21:43:25 -0700 Subject: [PATCH 10/82] Improved mutation class --- graphene/types/mutation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 4b662f0d..9ef75df7 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -41,8 +41,10 @@ class Mutation(ObjectType): else: arguments = {} - resolver = resolver or get_unbound_function(getattr(cls, 'mutate', None)) - assert resolver, 'All mutations must define a mutate method in it' + if not resolver: + mutate = getattr(cls, 'mutate', None) + assert mutate, 'All mutations must define a mutate method in it' + resolver = get_unbound_function(mutate) _meta.fields = fields _meta.output = output From 9ce1288e12c6a4e9548dca5c556ba9d15a136ae3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 22:29:56 -0700 Subject: [PATCH 11/82] Improved Enum implementation --- graphene/pyutils/init_subclass.py | 30 +++++++++-------- graphene/types/enum.py | 53 +++++++++++-------------------- 2 files changed, 35 insertions(+), 48 deletions(-) diff --git a/graphene/pyutils/init_subclass.py b/graphene/pyutils/init_subclass.py index 62f9a6db..b12e51b6 100644 --- a/graphene/pyutils/init_subclass.py +++ b/graphene/pyutils/init_subclass.py @@ -1,15 +1,19 @@ +import six -class InitSubclassMeta(type): - """Metaclass that implements PEP 487 protocol""" - def __new__(cls, name, bases, ns): - __init_subclass__ = ns.pop('__init_subclass__', None) - if __init_subclass__: - __init_subclass__ = classmethod(__init_subclass__) - ns['__init_subclass__'] = __init_subclass__ - return type.__new__(cls, name, bases, ns) +if six.PY2: + class InitSubclassMeta(type): + """Metaclass that implements PEP 487 protocol""" + def __new__(cls, name, bases, ns): + __init_subclass__ = ns.pop('__init_subclass__', None) + if __init_subclass__: + __init_subclass__ = classmethod(__init_subclass__) + ns['__init_subclass__'] = __init_subclass__ + return super(InitSubclassMeta, cls).__new__(cls, name, bases, ns) - def __init__(cls, name, bases, ns): - super(InitSubclassMeta, cls).__init__(name, bases, ns) - super_class = super(cls, cls) - if hasattr(super_class, '__init_subclass__'): - super_class.__init_subclass__.__func__(cls) + def __init__(cls, name, bases, ns): + super(InitSubclassMeta, cls).__init__(name, bases, ns) + super_class = super(cls, cls) + if hasattr(super_class, '__init_subclass__'): + super_class.__init_subclass__.__func__(cls) +else: + InitSubclassMeta = type diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 029e6991..231fa61c 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -2,10 +2,9 @@ from collections import OrderedDict import six -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .options import Options +from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType +from graphene.pyutils.init_subclass import InitSubclassMeta try: from enum import Enum as PyEnum @@ -19,59 +18,43 @@ def eq_enum(self, other): return self.value is other -class EnumTypeMeta(type): +EnumType = type(PyEnum) - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of Model - # (excluding Model class itself). - if not is_base_type(bases, EnumTypeMeta): - return type.__new__(cls, name, bases, attrs) - options = Options( - attrs.pop('Meta', None), - name=name, - description=trim_docstring(attrs.get('__doc__')), - enum=None, - ) - if not options.enum: - attrs['__eq__'] = eq_enum - options.enum = PyEnum(cls.__name__, attrs) +class EnumOptions(BaseOptions): + enum = None # type: Enum - new_attrs = OrderedDict(attrs, _meta=options, **options.enum.__members__) - return type.__new__(cls, name, bases, new_attrs) - - def __prepare__(name, bases, **kwargs): # noqa: N805 - return OrderedDict() +class EnumMeta(InitSubclassMeta): def get(cls, value): return cls._meta.enum(value) def __getitem__(cls, value): return cls._meta.enum[value] + def __prepare__(name, bases, **kwargs): # noqa: N805 + return OrderedDict() + def __call__(cls, *args, **kwargs): # noqa: N805 if cls is Enum: description = kwargs.pop('description', None) return cls.from_enum(PyEnum(*args, **kwargs), description=description) - return super(EnumTypeMeta, cls).__call__(*args, **kwargs) + return super(EnumMeta, cls).__call__(*args, **kwargs) # return cls._meta.enum(*args, **kwargs) def from_enum(cls, enum, description=None): # noqa: N805 meta_class = type('Meta', (object,), {'enum': enum, 'description': description}) return type(meta_class.enum.__name__, (Enum,), {'Meta': meta_class}) - def __str__(cls): # noqa: N805 - return cls._meta.name - -class Enum(six.with_metaclass(EnumTypeMeta, UnmountedType)): - ''' - Enum Type Definition - - Some leaf values of requests and input values are Enums. GraphQL serializes - Enum values as strings, however internally Enums can be represented by any - kind of type, often integers. - ''' +class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)): + @classmethod + def __init_subclass_with_meta__(cls, enum=None, **options): + _meta = EnumOptions(cls) + _meta.enum = enum or PyEnum(cls.__name__, dict(cls.__dict__, __eq__=eq_enum)) + for key, value in _meta.enum.__members__.items(): + setattr(cls, key, value) + super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod def get_type(cls): From e86f73d30c20f8cf6aae2586970084d86e758a4d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 22:52:38 -0700 Subject: [PATCH 12/82] Added class arguments example for Python 3 --- UPGRADE-v2.0.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 28a2034c..2a46a005 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -16,6 +16,7 @@ ``` With 2.0: + ```python class CommonFields(object): name = String() @@ -23,3 +24,21 @@ class Pet(CommonFields, Interface): pass ``` + +* Meta options as class arguments (**ONLY PYTHON 3**). + + Before: + + ```python + class Dog(ObjectType): + class Meta: + interfaces = [Pet] + name = String() + ``` + + With 2.0: + + ```python + class Dog(ObjectType, interfaces=[Pet]): + name = String() + ``` From 28c987bdd112c42f069ef0ae3b9be6b27a69f8fb Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 23:07:20 -0700 Subject: [PATCH 13/82] Improved docs for extending Option attrs --- UPGRADE-v2.0.md | 5 ++ graphene/tests/issues/test_425.py | 75 +++++++++++---------- graphene/tests/issues/test_425_graphene2.py | 40 +++++++++++ 3 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 graphene/tests/issues/test_425_graphene2.py diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 2a46a005..a0f6c86b 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -1,5 +1,10 @@ # v1.0 Upgrade Guide +* `ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations + have been quite simplified, without the need of define a explicit Metaclass. + The metaclasses threfore are now deleted as are no longer necessary, if your code was depending + on this internal metaclass for creating custom attrs, please see an [example of how to do it now in 2.0](https://github.com/graphql-python/graphene/blob/master/graphene/tests/issues/test_425_graphene2.py). + ## Deprecations diff --git a/graphene/tests/issues/test_425.py b/graphene/tests/issues/test_425.py index 08bdde8c..42ee1178 100644 --- a/graphene/tests/issues/test_425.py +++ b/graphene/tests/issues/test_425.py @@ -1,53 +1,56 @@ -# https://github.com/graphql-python/graphene/issues/425 -import six +# THIS IS THE OLD IMPLEMENTATION, for Graphene 1.0 +# Keep here as reference for upgrade. -from graphene.utils.is_base_type import is_base_type +# # https://github.com/graphql-python/graphene/issues/425 +# import six -from graphene.types.objecttype import ObjectTypeMeta, ObjectType -from graphene.types.options import Options +# from graphene.utils.is_base_type import is_base_type -class SpecialObjectTypeMeta(ObjectTypeMeta): +# from graphene.types.objecttype import ObjectTypeMeta, ObjectType +# from graphene.types.options import Options - @staticmethod - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # DjangoObjectType - if not is_base_type(bases, SpecialObjectTypeMeta): - return type.__new__(cls, name, bases, attrs) +# class SpecialObjectTypeMeta(ObjectTypeMeta): - options = Options( - attrs.pop('Meta', None), - other_attr='default', - ) +# @staticmethod +# def __new__(cls, name, bases, attrs): +# # Also ensure initialization is only performed for subclasses of +# # DjangoObjectType +# if not is_base_type(bases, SpecialObjectTypeMeta): +# return type.__new__(cls, name, bases, attrs) - cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) - assert cls._meta is options - return cls +# options = Options( +# attrs.pop('Meta', None), +# other_attr='default', +# ) + +# cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) +# assert cls._meta is options +# return cls -class SpecialObjectType(six.with_metaclass(SpecialObjectTypeMeta, ObjectType)): - pass +# class SpecialObjectType(six.with_metaclass(SpecialObjectTypeMeta, ObjectType)): +# pass -def test_special_objecttype_could_be_subclassed(): - class MyType(SpecialObjectType): - class Meta: - other_attr = 'yeah!' +# def test_special_objecttype_could_be_subclassed(): +# class MyType(SpecialObjectType): +# class Meta: +# other_attr = 'yeah!' - assert MyType._meta.other_attr == 'yeah!' +# assert MyType._meta.other_attr == 'yeah!' -def test_special_objecttype_could_be_subclassed_default(): - class MyType(SpecialObjectType): - pass +# def test_special_objecttype_could_be_subclassed_default(): +# class MyType(SpecialObjectType): +# pass - assert MyType._meta.other_attr == 'default' +# assert MyType._meta.other_attr == 'default' -def test_special_objecttype_inherit_meta_options(): - class MyType(SpecialObjectType): - pass +# def test_special_objecttype_inherit_meta_options(): +# class MyType(SpecialObjectType): +# pass - assert MyType._meta.name == 'MyType' - assert MyType._meta.default_resolver == None - assert MyType._meta.interfaces == () +# assert MyType._meta.name == 'MyType' +# assert MyType._meta.default_resolver == None +# assert MyType._meta.interfaces == () diff --git a/graphene/tests/issues/test_425_graphene2.py b/graphene/tests/issues/test_425_graphene2.py new file mode 100644 index 00000000..38b12143 --- /dev/null +++ b/graphene/tests/issues/test_425_graphene2.py @@ -0,0 +1,40 @@ +# https://github.com/graphql-python/graphene/issues/425 +# Adapted for Graphene 2.0 +import six + +from graphene.types.objecttype import ObjectType, ObjectTypeOptions + +class SpecialOptions(ObjectTypeOptions): + other_attr = None + + +class SpecialObjectType(ObjectType): + @classmethod + def __init_subclass_with_meta__(cls, other_attr='default', **options): + _meta = SpecialOptions(cls) + _meta.other_attr = other_attr + super(SpecialObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + +def test_special_objecttype_could_be_subclassed(): + class MyType(SpecialObjectType): + class Meta: + other_attr = 'yeah!' + + assert MyType._meta.other_attr == 'yeah!' + + +def test_special_objecttype_could_be_subclassed_default(): + class MyType(SpecialObjectType): + pass + + assert MyType._meta.other_attr == 'default' + + +def test_special_objecttype_inherit_meta_options(): + class MyType(SpecialObjectType): + pass + + assert MyType._meta.name == 'MyType' + assert MyType._meta.default_resolver == None + assert MyType._meta.interfaces == () From 58b04dfcf5ae970dd0ef0d2893be6d59103da345 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 23:10:02 -0700 Subject: [PATCH 14/82] Updated Readme docs --- README.md | 8 ++++---- README.rst | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e2d79edf..5b27c4b5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graphene `1.0`. +Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade to Graphene `2.0`. --- @@ -32,12 +32,12 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit For instaling graphene, just run this command in your shell ```bash -pip install "graphene>=1.0" +pip install "graphene>=2.0" ``` -## 1.0 Upgrade Guide +## 2.0 Upgrade Guide -Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade. +Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade. ## Examples diff --git a/README.rst b/README.rst index c0fa1c28..fcde970f 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Please read `UPGRADE-v1.0.md `__ to learn how to -upgrade to Graphene ``1.0``. +Please read `UPGRADE-v2.0.md `__ to learn how to +upgrade to Graphene ``2.0``. -------------- @@ -11,7 +11,7 @@ building GraphQL schemas/types fast and easily. - **Easy to use:** Graphene helps you use GraphQL in Python without effort. -- **Relay:** Graphene has builtin support for both Relay. +- **Relay:** Graphene has builtin support for Relay. - **Data agnostic:** Graphene supports any kind of data source: SQL (Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe that by providing a complete API you could plug Graphene anywhere @@ -47,12 +47,12 @@ For instaling graphene, just run this command in your shell .. code:: bash - pip install "graphene>=1.0" + pip install "graphene>=2.0" -1.0 Upgrade Guide +2.0 Upgrade Guide ----------------- -Please read `UPGRADE-v1.0.md `__ to learn how to +Please read `UPGRADE-v2.0.md `__ to learn how to upgrade. Examples From e38007868ec04f696d67a0edaa94228859e3c3b9 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 11 Jul 2017 23:52:24 -0700 Subject: [PATCH 15/82] Fixed upgrade doc title --- UPGRADE-v2.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index a0f6c86b..8872aaca 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -1,4 +1,4 @@ -# v1.0 Upgrade Guide +# v2.0 Upgrade Guide * `ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations have been quite simplified, without the need of define a explicit Metaclass. From 563ca23d00251aaa1a0b9094ff7320d1a200182e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 00:04:37 -0700 Subject: [PATCH 16/82] Make init_subclass work in Python 3.5 --- graphene/pyutils/init_subclass.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene/pyutils/init_subclass.py b/graphene/pyutils/init_subclass.py index b12e51b6..c3a6143c 100644 --- a/graphene/pyutils/init_subclass.py +++ b/graphene/pyutils/init_subclass.py @@ -1,19 +1,19 @@ -import six +is_init_subclass_available = hasattr(object, '__init_subclass__') -if six.PY2: +if not is_init_subclass_available: class InitSubclassMeta(type): """Metaclass that implements PEP 487 protocol""" - def __new__(cls, name, bases, ns): + def __new__(cls, name, bases, ns, **kwargs): __init_subclass__ = ns.pop('__init_subclass__', None) if __init_subclass__: __init_subclass__ = classmethod(__init_subclass__) ns['__init_subclass__'] = __init_subclass__ - return super(InitSubclassMeta, cls).__new__(cls, name, bases, ns) + return super(InitSubclassMeta, cls).__new__(cls, name, bases, ns, **kwargs) - def __init__(cls, name, bases, ns): + def __init__(cls, name, bases, ns, **kwargs): super(InitSubclassMeta, cls).__init__(name, bases, ns) super_class = super(cls, cls) if hasattr(super_class, '__init_subclass__'): - super_class.__init_subclass__.__func__(cls) + super_class.__init_subclass__.__func__(cls, **kwargs) else: InitSubclassMeta = type From 4820a7fede27c8e9cc5bc9a47a4eedc5f87b5db5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 00:33:50 -0700 Subject: [PATCH 17/82] Improved Mutation --- graphene/relay/node.py | 39 +++++++--------------------- graphene/types/interface.py | 11 +++++--- graphene/types/mutation.py | 7 ++++- graphene/types/tests/test_options.py | 30 --------------------- 4 files changed, 23 insertions(+), 64 deletions(-) delete mode 100644 graphene/types/tests/test_options.py diff --git a/graphene/relay/node.py b/graphene/relay/node.py index aa6e2dc0..b129adbf 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -6,7 +6,7 @@ from graphql_relay import from_global_id, to_global_id from ..types import ID, Field, Interface, ObjectType from ..types.utils import get_type -from ..types.interface import InterfaceMeta +from ..types.interface import InterfaceOptions def is_node(objecttype): @@ -22,18 +22,6 @@ def is_node(objecttype): return False -def get_default_connection(cls): - from .connection import Connection - assert issubclass(cls, ObjectType), ( - 'Can only get connection type on implemented Nodes.' - ) - - class Meta: - node = cls - - return type('{}Connection'.format(cls.__name__), (Connection,), {'Meta': Meta}) - - class GlobalID(Field): def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs): @@ -51,14 +39,6 @@ class GlobalID(Field): return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name) -class NodeMeta(InterfaceMeta): - - def __new__(cls, name, bases, attrs): - cls = InterfaceMeta.__new__(cls, name, bases, attrs) - cls._meta.fields['id'] = GlobalID(cls, description='The ID of the object.') - return cls - - class NodeField(Field): def __init__(self, node, type=False, deprecation_reason=None, @@ -78,9 +58,16 @@ class NodeField(Field): return partial(self.node_type.node_resolver, only_type=get_type(self.field_type)) -class Node(six.with_metaclass(NodeMeta, Interface)): +class Node(Interface): '''An object with an ID''' + def __init_subclass_with_meta__(cls, **options): + _meta = InterfaceOptions(cls) + _meta.fields = { + 'id': GlobalID(cls, description='The ID of the object.') + } + super(Node, cls).__init_subclass_with_meta__(cls, _meta=_meta, **options) + @classmethod def Field(cls, *args, **kwargs): # noqa: N802 return NodeField(cls, *args, **kwargs) @@ -117,11 +104,3 @@ class Node(six.with_metaclass(NodeMeta, Interface)): @classmethod def to_global_id(cls, type, id): return to_global_id(type, id) - - @classmethod - def implements(cls, objecttype): - get_connection = getattr(objecttype, 'get_connection', None) - if not get_connection: - get_connection = partial(get_default_connection, objecttype) - - objecttype.Connection = get_connection() diff --git a/graphene/types/interface.py b/graphene/types/interface.py index adbd7c5c..dc7c34cc 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -19,16 +19,21 @@ class Interface(BaseType): when the field is resolved. ''' @classmethod - def __init_subclass_with_meta__(cls, **options): - _meta = InterfaceOptions(cls) + def __init_subclass_with_meta__(cls, _meta=None, **options): + if not _meta: + _meta = InterfaceOptions(cls) fields = OrderedDict() for base in reversed(cls.__mro__): fields.update( yank_fields_from_attrs(base.__dict__, _as=Field) ) + + if _meta.fields: + _meta.fields.update(fields) + else: + _meta.fields = fields - _meta.fields = fields super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 9ef75df7..e6ade33e 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -35,7 +35,12 @@ class Mutation(ObjectType): output = cls if not arguments: - input_class = getattr(cls, 'Input', None) + input_class = getattr(cls, 'Arguments', None) + if not input_class: + input_class = getattr(cls, 'Input', None) + if input_class: + print("WARNING: Please use Arguments for Mutation (Input is for ClientMutationID)") + if input_class: arguments = props(input_class) else: diff --git a/graphene/types/tests/test_options.py b/graphene/types/tests/test_options.py deleted file mode 100644 index fbcba2db..00000000 --- a/graphene/types/tests/test_options.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from ..options import Options - - -def test_options(): - class BaseOptions: - option_1 = False - name = True - meta = Options(BaseOptions, name=False, option_1=False) - assert meta.name == True - assert meta.option_1 == False - - -def test_options_extra_attrs(): - class BaseOptions: - name = True - type = True - - with pytest.raises(Exception) as exc_info: - meta = Options(BaseOptions) - - assert str(exc_info.value) == 'Invalid attributes: name, type' - - -def test_options_repr(): - class BaseOptions: - name = True - meta = Options(BaseOptions, name=False) - assert repr(meta) == '' From 7a6d741531bffedf1a06f8097dd966a025834531 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 01:36:44 -0700 Subject: [PATCH 18/82] Fixed relay ClientIDMutation --- graphene/relay/connection.py | 8 ++- graphene/relay/mutation.py | 75 +++++++++++++-------------- graphene/relay/tests/test_mutation.py | 20 ++++--- graphene/types/abstracttype.py | 2 +- graphene/types/mutation.py | 13 +++-- graphene/types/objecttype.py | 5 +- 6 files changed, 67 insertions(+), 56 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index be77700d..bc33e58b 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -10,9 +10,7 @@ from promise import Promise, is_thenable from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) from ..types.field import Field -from ..types.objecttype import ObjectType, ObjectTypeMeta -from ..types.options import Options -from ..utils.is_base_type import is_base_type +from ..types.objecttype import ObjectType from ..utils.props import props from .node import is_node @@ -41,7 +39,7 @@ class PageInfo(ObjectType): ) -class ConnectionMeta(ObjectTypeMeta): +class ConnectionMeta(type): def __new__(cls, name, bases, attrs): # Also ensure initialization is only performed for subclasses of Model @@ -89,7 +87,7 @@ class ConnectionMeta(ObjectTypeMeta): return ObjectTypeMeta.__new__(cls, name, bases, attrs) -class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): +class Connection(ObjectType): pass diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index ab1e9eb4..3b94e635 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -1,56 +1,53 @@ import re -from functools import partial - -import six +from collections import OrderedDict from promise import Promise -from ..types import Field, AbstractType, Argument, InputObjectType, String -from ..types.mutation import Mutation, MutationMeta -from ..types.objecttype import ObjectTypeMeta -from ..utils.is_base_type import is_base_type +from ..types import Field, AbstractType, Argument, InputObjectType, String, Field +from ..types.mutation import Mutation, MutationOptions from ..utils.props import props -class ClientIDMutationMeta(MutationMeta): - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # Mutation - if not is_base_type(bases, ClientIDMutationMeta): - return type.__new__(cls, name, bases, attrs) +class ClientIDMutation(Mutation): + class Meta: + abstract = True - input_class = attrs.pop('Input', None) + @classmethod + def __init_subclass_with_meta__(cls, output=None, arguments=None, name=None, **options): + input_class = getattr(cls, 'Input', None) + name = name or cls.__name__ base_name = re.sub('Payload$', '', name) - if 'client_mutation_id' not in attrs: - attrs['client_mutation_id'] = String(name='clientMutationId') - cls = ObjectTypeMeta.__new__(cls, '{}Payload'.format(base_name), bases, - attrs) + + assert not output, "Can't specify any output" + assert not arguments, "Can't specify any arguments" + + bases = (InputObjectType, ) + if input_class: + bases += (input_class, ) + + cls.Input = type('{}Input'.format(base_name), + bases, { + 'client_mutation_id': String(name='clientMutationId') + }) + + arguments = OrderedDict( + input=cls.Input(required=True) + # 'client_mutation_id': String(name='clientMutationId') + ) mutate_and_get_payload = getattr(cls, 'mutate_and_get_payload', None) if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__: assert mutate_and_get_payload, ( - "{}.mutate_and_get_payload method is required" - " in a ClientIDMutation.").format(name) - input_attrs = {} - bases = () - if not input_class: - input_attrs = {} - elif not issubclass(input_class, AbstractType): - input_attrs = props(input_class) - else: - bases += (input_class, ) - input_attrs['client_mutation_id'] = String(name='clientMutationId') - cls.Input = type('{}Input'.format(base_name), - bases + (InputObjectType, ), input_attrs) - output_class = getattr(cls, 'Output', cls) - cls.Field = partial( - Field, - output_class, - resolver=cls.mutate, - input=Argument(cls.Input, required=True)) - return cls + "{name}.mutate_and_get_payload method is required" + " in a ClientIDMutation.").format(name=name) + + if not name: + name = '{}Payload'.format(base_name) + super(ClientIDMutation, cls).__init_subclass_with_meta__(output=None, arguments=arguments, name=name, **options) + cls._meta.fields['client_mutation_id'] = ( + Field(String, name='clientMutationId') + ) -class ClientIDMutation(six.with_metaclass(ClientIDMutationMeta, Mutation)): @classmethod def mutate(cls, root, args, context, info): input = args.get('input') diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 8a76037a..6fb2658d 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -1,6 +1,6 @@ import pytest -from ...types import (AbstractType, Argument, Field, InputField, +from ...types import ( Argument, Field, InputField, ID, InputObjectType, NonNull, ObjectType, Schema) from ...types.scalars import String from ..mutation import ClientIDMutation @@ -8,14 +8,14 @@ from ..node import Node from promise import Promise -class SharedFields(AbstractType): +class SharedFields(object): shared = String() class MyNode(ObjectType): - class Meta: - interfaces = (Node, ) - + # class Meta: + # interfaces = (Node, ) + id = ID() name = String() @@ -43,18 +43,24 @@ class SaySomethingPromise(ClientIDMutation): return Promise.resolve(SaySomething(phrase=str(what))) +# MyEdge = MyNode.Connection.Edge +class MyEdge(ObjectType): + node = Field(MyNode) + cursor = String() + + class OtherMutation(ClientIDMutation): class Input(SharedFields): additional_field = String() name = String() - my_node_edge = Field(MyNode.Connection.Edge) + my_node_edge = Field(MyEdge) @classmethod def mutate_and_get_payload(cls, args, context, info): shared = args.get('shared', '') additionalField = args.get('additionalField', '') - edge_type = MyNode.Connection.Edge + edge_type = MyEdge return OtherMutation( name=shared + additionalField, my_node_edge=edge_type(cursor='1', node=MyNode(name='name'))) diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index 8e132c66..efc756a1 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -6,4 +6,4 @@ class AbstractType(object): def __init_subclass__(cls, *args, **kwargs): print("Abstract type is deprecated") - super(AbstractType, cls).__init_subclass__(*args, **kwargs) + # super(AbstractType, cls).__init_subclass__(*args, **kwargs) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index e6ade33e..8b2bb88b 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -20,8 +20,11 @@ class Mutation(ObjectType): Mutation Type Definition ''' @classmethod - def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None, **options): - _meta = MutationOptions(cls) + def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None, _meta=None, abstract=False, **options): + if abstract: + return + if not _meta: + _meta = MutationOptions(cls) output = output or getattr(cls, 'Output', None) fields = {} @@ -51,7 +54,11 @@ class Mutation(ObjectType): assert mutate, 'All mutations must define a mutate method in it' resolver = get_unbound_function(mutate) - _meta.fields = fields + if _meta.fields: + _meta.fields.update(fields) + else: + _meta.fields = fields + _meta.output = output _meta.resolver = resolver _meta.arguments = arguments diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 5b6690cd..c7bb34c5 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -42,7 +42,10 @@ class ObjectType(BaseType): 'Please use one or other.' ).format(name=cls.__name__) - _meta.fields = fields + if _meta.fields: + _meta.fields.update(fields) + else: + _meta.fields = fields _meta.interfaces = interfaces _meta.possible_types = possible_types _meta.default_resolver = default_resolver From a023aeba627f2d49a3237efc7302c1933efe4ff1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 01:57:15 -0700 Subject: [PATCH 19/82] Simplified Node type --- graphene/relay/node.py | 19 +++++++++++++------ graphene/relay/tests/test_node.py | 13 ++----------- graphene/types/interface.py | 4 +++- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index b129adbf..33a2628d 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from functools import partial import six @@ -58,15 +59,21 @@ class NodeField(Field): return partial(self.node_type.node_resolver, only_type=get_type(self.field_type)) -class Node(Interface): - '''An object with an ID''' +class AbstractNode(Interface): + class Meta: + abstract = True + @classmethod def __init_subclass_with_meta__(cls, **options): _meta = InterfaceOptions(cls) - _meta.fields = { - 'id': GlobalID(cls, description='The ID of the object.') - } - super(Node, cls).__init_subclass_with_meta__(cls, _meta=_meta, **options) + _meta.fields = OrderedDict( + id=GlobalID(cls, description='The ID of the object.') + ) + super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + +class Node(AbstractNode): + '''An object with an ID''' @classmethod def Field(cls, *args, **kwargs): # noqa: N802 diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index 6a9c2e04..9fdaa702 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -2,12 +2,12 @@ from collections import OrderedDict from graphql_relay import to_global_id -from ...types import AbstractType, ObjectType, Schema, String +from ...types import ObjectType, Schema, String from ..connection import Connection from ..node import Node -class SharedNodeFields(AbstractType): +class SharedNodeFields(object): shared = String() something_else = String() @@ -54,15 +54,6 @@ def test_node_good(): assert 'id' in MyNode._meta.fields -def test_node_get_connection(): - connection = MyNode.Connection - assert issubclass(connection, Connection) - - -def test_node_get_connection_dont_duplicate(): - assert MyNode.Connection == MyNode.Connection - - def test_node_query(): executed = schema.execute( '{ node(id:"%s") { ... on MyNode { name } } }' % Node.to_global_id("MyNode", 1) diff --git a/graphene/types/interface.py b/graphene/types/interface.py index dc7c34cc..37d38ac1 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -19,7 +19,9 @@ class Interface(BaseType): when the field is resolved. ''' @classmethod - def __init_subclass_with_meta__(cls, _meta=None, **options): + def __init_subclass_with_meta__(cls, abstract=False, _meta=None, **options): + if abstract: + return if not _meta: _meta = InterfaceOptions(cls) From 6321c52bd201829344d1c3c36db0f085d404e744 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 21:21:16 -0700 Subject: [PATCH 20/82] Fixed Connection tests --- examples/starwars_relay/schema.py | 7 +- .../snap_test_objectidentification.py | 60 +++++++++++++ .../tests/test_objectidentification.py | 62 +------------ graphene/__init__.py | 18 ++-- graphene/relay/connection.py | 89 +++++++++---------- graphene/relay/tests/test_connection.py | 16 +--- graphene/relay/tests/test_connection_query.py | 17 ++-- graphene/types/enum.py | 5 +- graphene/types/objecttype.py | 5 +- graphene/utils/subclass_with_meta.py | 11 ++- graphene/utils/tests/test_module_loading.py | 11 ++- 11 files changed, 151 insertions(+), 150 deletions(-) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 8ad1a643..f914dc2b 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -17,6 +17,11 @@ class Ship(graphene.ObjectType): return get_ship(id) +class ShipConnection(relay.Connection): + class Meta: + node = Ship + + class Faction(graphene.ObjectType): '''A faction in the Star Wars saga''' @@ -24,7 +29,7 @@ class Faction(graphene.ObjectType): interfaces = (relay.Node, ) name = graphene.String(description='The name of the faction.') - ships = relay.ConnectionField(Ship, description='The ships used by the faction.') + ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.') @resolve_only_args def resolve_ships(self, **args): diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index a6095bb0..bce35808 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -51,3 +51,63 @@ snapshots['test_correctly_refetches_xwing 1'] = { } } } + +snapshots['test_str_schema 1'] = '''schema { + query: Query + mutation: Mutation +} + +type Faction implements Node { + id: ID! + name: String + ships(before: String, after: String, first: Int, last: Int): ShipConnection +} + +type IntroduceShip { + ship: Ship + faction: Faction + clientMutationId: String +} + +input IntroduceShipInput { + shipName: String! + factionId: String! + clientMutationId: String +} + +type Mutation { + introduceShip(input: IntroduceShipInput!): IntroduceShip +} + +interface Node { + id: ID! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +type Query { + rebels: Faction + empire: Faction + node(id: ID!): Node +} + +type Ship implements Node { + id: ID! + name: String +} + +type ShipConnection { + pageInfo: PageInfo! + edges: [ShipEdge]! +} + +type ShipEdge { + node: Ship + cursor: String! +} +''' diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index d1b4c529..9ee1d495 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -7,66 +7,8 @@ setup() client = Client(schema) -def test_str_schema(): - assert str(schema) == '''schema { - query: Query - mutation: Mutation -} - -type Faction implements Node { - id: ID! - name: String - ships(before: String, after: String, first: Int, last: Int): ShipConnection -} - -input IntroduceShipInput { - shipName: String! - factionId: String! - clientMutationId: String -} - -type IntroduceShipPayload { - ship: Ship - faction: Faction - clientMutationId: String -} - -type Mutation { - introduceShip(input: IntroduceShipInput!): IntroduceShipPayload -} - -interface Node { - id: ID! -} - -type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String -} - -type Query { - rebels: Faction - empire: Faction - node(id: ID!): Node -} - -type Ship implements Node { - id: ID! - name: String -} - -type ShipConnection { - pageInfo: PageInfo! - edges: [ShipEdge]! -} - -type ShipEdge { - node: Ship - cursor: String! -} -''' +def test_str_schema(snapshot): + snapshot.assert_match(str(schema)) def test_correctly_fetches_id_name_rebels(snapshot): diff --git a/graphene/__init__.py b/graphene/__init__.py index 556a8175..c15f2cbe 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -33,15 +33,15 @@ if not __SETUP__: Dynamic, Union, ) - # from .relay import ( - # Node, - # is_node, - # GlobalID, - # ClientIDMutation, - # Connection, - # ConnectionField, - # PageInfo - # ) + from .relay import ( + Node, + is_node, + GlobalID, + ClientIDMutation, + Connection, + ConnectionField, + PageInfo + ) from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index bc33e58b..dea5feab 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -7,10 +7,10 @@ import six from graphql_relay import connection_from_list from promise import Promise, is_thenable -from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, +from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) from ..types.field import Field -from ..types.objecttype import ObjectType +from ..types.objecttype import ObjectType, ObjectTypeOptions from ..utils.props import props from .node import is_node @@ -39,56 +39,47 @@ class PageInfo(ObjectType): ) -class ConnectionMeta(type): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of Model - # (excluding Model class itself). - if not is_base_type(bases, ConnectionMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=None, - node=None, - ) - options.interfaces = () - options.local_fields = OrderedDict() - - assert options.node, 'You have to provide a node in {}.Meta'.format(cls.__name__) - assert issubclass(options.node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), ( - 'Received incompatible node "{}" for Connection {}.' - ).format(options.node, name) - - base_name = re.sub('Connection$', '', options.name) or options.node._meta.name - if not options.name: - options.name = '{}Connection'.format(base_name) - - edge_class = attrs.pop('Edge', None) - - class EdgeBase(AbstractType): - node = Field(options.node, description='The item at the end of the edge') - cursor = String(required=True, description='A cursor for use in pagination') - - edge_name = '{}Edge'.format(base_name) - if edge_class and issubclass(edge_class, AbstractType): - edge = type(edge_name, (EdgeBase, edge_class, ObjectType, ), {}) - else: - edge_attrs = props(edge_class) if edge_class else {} - edge = type(edge_name, (EdgeBase, ObjectType, ), edge_attrs) - - class ConnectionBase(AbstractType): - page_info = Field(PageInfo, name='pageInfo', required=True) - edges = NonNull(List(edge)) - - bases = (ConnectionBase, ) + bases - attrs = dict(attrs, _meta=options, Edge=edge) - return ObjectTypeMeta.__new__(cls, name, bases, attrs) +class ConnectionOptions(ObjectTypeOptions): + node = None class Connection(ObjectType): - pass + class Meta: + abstract = True + @classmethod + def __init_subclass_with_meta__(cls, node=None, name=None, **options): + _meta = ConnectionOptions(cls) + assert node, 'You have to provide a node in {}.Meta'.format(cls.__name__) + assert issubclass(node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), ( + 'Received incompatible node "{}" for Connection {}.' + ).format(node, cls.__name__) + + base_name = re.sub('Connection$', '', name or cls.__name__) or node._meta.name + if not name: + name = '{}Connection'.format(base_name) + + edge_class = getattr(cls, 'Edge', None) + _node = node + class EdgeBase(object): + node = Field(_node, description='The item at the end of the edge') + cursor = String(required=True, description='A cursor for use in pagination') + + edge_name = '{}Edge'.format(base_name) + if edge_class: + edge_bases = (edge_class, EdgeBase, ObjectType,) + else: + edge_bases = (EdgeBase, ObjectType,) + + edge = type(edge_name, edge_bases, {}) + cls.Edge = edge + + _meta.name = name + _meta.node = node + _meta.fields = OrderedDict([ + ('page_info', Field(PageInfo, name='pageInfo', required=True)), + ('edges', Field(NonNull(List(edge)))), + ]) + return super(Connection, cls).__init_subclass_with_meta__(_meta=_meta, **options) class IterableConnectionField(Field): diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 87f937ae..f44bc5ab 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,5 +1,5 @@ -from ...types import AbstractType, Field, List, NonNull, ObjectType, String, Argument, Int +from ...types import Field, List, NonNull, ObjectType, String, Argument, Int from ..connection import Connection, PageInfo, ConnectionField from ..node import Node @@ -38,7 +38,7 @@ def test_connection(): def test_connection_inherit_abstracttype(): - class BaseConnection(AbstractType): + class BaseConnection(object): extra = String() class MyObjectConnection(BaseConnection, Connection): @@ -73,7 +73,7 @@ def test_edge(): def test_edge_with_bases(): - class BaseEdge(AbstractType): + class BaseEdge(object): extra = String() class MyObjectConnection(Connection): @@ -96,16 +96,6 @@ def test_edge_with_bases(): assert edge_fields['other'].type == String -def test_edge_on_node(): - Edge = MyObject.Connection.Edge - assert Edge._meta.name == 'MyObjectEdge' - edge_fields = Edge._meta.fields - assert list(edge_fields.keys()) == ['node', 'cursor'] - - assert isinstance(edge_fields['node'], Field) - assert edge_fields['node'].type == MyObject - - def test_pageinfo(): assert PageInfo._meta.name == 'PageInfo' fields = PageInfo._meta.fields diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 068081d6..3b543552 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -4,7 +4,7 @@ from graphql_relay.utils import base64 from promise import Promise from ...types import ObjectType, Schema, String -from ..connection import ConnectionField, PageInfo +from ..connection import Connection, ConnectionField, PageInfo from ..node import Node letter_chars = ['A', 'B', 'C', 'D', 'E'] @@ -18,10 +18,15 @@ class Letter(ObjectType): letter = String() +class LetterConnection(Connection): + class Meta: + node = Letter + + class Query(ObjectType): - letters = ConnectionField(Letter) - connection_letters = ConnectionField(Letter) - promise_letters = ConnectionField(Letter) + letters = ConnectionField(LetterConnection) + connection_letters = ConnectionField(LetterConnection) + promise_letters = ConnectionField(LetterConnection) node = Node.Field() @@ -32,13 +37,13 @@ class Query(ObjectType): return Promise.resolve(list(letters.values())) def resolve_connection_letters(self, args, context, info): - return Letter.Connection( + return LetterConnection( page_info=PageInfo( has_next_page=True, has_previous_page=False ), edges=[ - Letter.Connection.Edge( + LetterConnection.Edge( node=Letter(id=0, letter='A'), cursor='a-cursor' ), diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 231fa61c..0103a3b2 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -5,6 +5,7 @@ import six from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType from graphene.pyutils.init_subclass import InitSubclassMeta +from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta try: from enum import Enum as PyEnum @@ -25,7 +26,7 @@ class EnumOptions(BaseOptions): enum = None # type: Enum -class EnumMeta(InitSubclassMeta): +class EnumMeta(SubclassWithMeta_Meta): def get(cls, value): return cls._meta.enum(value) @@ -51,7 +52,7 @@ class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)): @classmethod def __init_subclass_with_meta__(cls, enum=None, **options): _meta = EnumOptions(cls) - _meta.enum = enum or PyEnum(cls.__name__, dict(cls.__dict__, __eq__=eq_enum)) + _meta.enum = enum or PyEnum(cls.__name__, OrderedDict(cls.__dict__, __eq__=eq_enum)) for key, value in _meta.enum.__members__.items(): setattr(cls, key, value) super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index c7bb34c5..ac7214f6 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -20,7 +20,9 @@ class ObjectType(BaseType): have a name, but most importantly describe their fields. ''' @classmethod - def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, _meta=None, **options): + def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, _meta=None, abstract=False, **options): + if abstract: + return if not _meta: _meta = ObjectTypeOptions(cls) @@ -46,6 +48,7 @@ class ObjectType(BaseType): _meta.fields.update(fields) else: _meta.fields = fields + _meta.interfaces = interfaces _meta.possible_types = possible_types _meta.default_resolver = default_resolver diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index 63fd0ec0..00ddb09f 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -1,12 +1,17 @@ +import six + from .props import props from ..pyutils.init_subclass import InitSubclassMeta -class SubclassWithMeta(object): +class SubclassWithMeta_Meta(InitSubclassMeta): + def __repr__(cls): + return cls._meta.name + + +class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): """This class improves __init_subclass__ to receive automatically the options from meta""" # We will only have the metaclass in Python 2 - __metaclass__ = InitSubclassMeta - def __init_subclass__(cls, **meta_options): """This method just terminates the super() chain""" _Meta = getattr(cls, "Meta", None) diff --git a/graphene/utils/tests/test_module_loading.py b/graphene/utils/tests/test_module_loading.py index 769fde8b..7184ed2c 100644 --- a/graphene/utils/tests/test_module_loading.py +++ b/graphene/utils/tests/test_module_loading.py @@ -1,7 +1,6 @@ from pytest import raises -from graphene import String -from graphene.types.objecttype import ObjectTypeMeta +from graphene import String, ObjectType from ..module_loading import lazy_import, import_string @@ -9,8 +8,8 @@ def test_import_string(): MyString = import_string('graphene.String') assert MyString == String - MyObjectTypeMeta = import_string('graphene.ObjectType', '__class__') - assert MyObjectTypeMeta == ObjectTypeMeta + MyObjectTypeMeta = import_string('graphene.ObjectType', '__doc__') + assert MyObjectTypeMeta == ObjectType.__doc__ def test_import_string_module(): @@ -52,6 +51,6 @@ def test_lazy_import(): MyString = f() assert MyString == String - f = lazy_import('graphene.ObjectType', '__class__') + f = lazy_import('graphene.ObjectType', '__doc__') MyObjectTypeMeta = f() - assert MyObjectTypeMeta == ObjectTypeMeta + assert MyObjectTypeMeta == ObjectType.__doc__ From 3fbc3281a22cf24457b2f31a43a9b420f983270d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 21:41:57 -0700 Subject: [PATCH 21/82] Fixed Python 3.5 issues with Enum --- graphene/types/enum.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 0103a3b2..af305fde 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -27,6 +27,10 @@ class EnumOptions(BaseOptions): class EnumMeta(SubclassWithMeta_Meta): + def __new__(cls, name, bases, classdict, **options): + enum = PyEnum(cls.__name__, OrderedDict(classdict, __eq__=eq_enum)) + return SubclassWithMeta_Meta.__new__(cls, name, bases, OrderedDict(classdict, __enum__=enum), **options) + def get(cls, value): return cls._meta.enum(value) @@ -52,7 +56,7 @@ class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)): @classmethod def __init_subclass_with_meta__(cls, enum=None, **options): _meta = EnumOptions(cls) - _meta.enum = enum or PyEnum(cls.__name__, OrderedDict(cls.__dict__, __eq__=eq_enum)) + _meta.enum = enum or cls.__enum__ for key, value in _meta.enum.__members__.items(): setattr(cls, key, value) super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options) From f1624af08aec4344b365094951aefc9979bced07 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 21:45:06 -0700 Subject: [PATCH 22/82] Fixed Flake issues --- examples/context_example.py | 1 + examples/simple_example.py | 1 + examples/starwars/tests/test_query.py | 3 ++- examples/starwars_relay/schema.py | 1 + examples/starwars_relay/tests/test_connections.py | 1 + examples/starwars_relay/tests/test_mutation.py | 1 + .../tests/test_objectidentification.py | 1 + graphene/__init__.py | 3 +++ graphene/pyutils/tests/test_enum.py | 1 + graphene/relay/connection.py | 10 +++++----- graphene/relay/mutation.py | 14 +++++++------- graphene/relay/node.py | 5 ++--- graphene/relay/tests/test_connection.py | 6 ++++-- graphene/relay/tests/test_connection_query.py | 1 + graphene/relay/tests/test_global_id.py | 4 ++-- graphene/relay/tests/test_mutation.py | 11 +++++++---- graphene/relay/tests/test_node.py | 5 ++--- graphene/test/__init__.py | 1 + graphene/tests/issues/test_313.py | 7 ++++++- graphene/tests/issues/test_356.py | 6 ++++++ graphene/tests/issues/test_425_graphene2.py | 8 +++++--- graphene/tests/issues/test_490.py | 1 - graphene/types/__init__.py | 3 +++ graphene/types/argument.py | 2 +- graphene/types/base.py | 3 ++- graphene/types/enum.py | 6 ++++-- graphene/types/field.py | 1 - graphene/types/generic.py | 4 ++-- graphene/types/inputobjecttype.py | 3 +-- graphene/types/interface.py | 6 +++--- graphene/types/mutation.py | 11 +++++------ graphene/types/objecttype.py | 12 +++++++----- graphene/types/scalars.py | 2 +- graphene/types/tests/test_argument.py | 7 ++++--- graphene/types/tests/test_datetime.py | 1 + graphene/types/tests/test_dynamic.py | 4 ++-- graphene/types/tests/test_enum.py | 2 +- graphene/types/tests/test_field.py | 7 ++++--- graphene/types/tests/test_inputfield.py | 1 - graphene/types/tests/test_inputobjecttype.py | 6 +++--- graphene/types/tests/test_json.py | 3 +-- graphene/types/tests/test_mountedtype.py | 3 +-- graphene/types/tests/test_mutation.py | 9 ++++++--- graphene/types/tests/test_objecttype.py | 2 ++ graphene/types/tests/test_query.py | 10 +++++++--- graphene/types/tests/test_resolver.py | 4 ++-- graphene/types/tests/test_scalar.py | 1 - graphene/types/tests/test_schema.py | 4 ++-- graphene/types/tests/test_structures.py | 11 ++++++----- graphene/types/tests/test_union.py | 1 + graphene/types/typemap.py | 9 +++++---- graphene/types/union.py | 4 +--- graphene/types/utils.py | 3 ++- graphene/utils/subclass_with_meta.py | 3 ++- graphene/utils/tests/test_module_loading.py | 5 +++-- graphene/utils/tests/test_trim_docstring.py | 5 ++--- 56 files changed, 147 insertions(+), 103 deletions(-) diff --git a/examples/context_example.py b/examples/context_example.py index 058e578b..d1d273d6 100644 --- a/examples/context_example.py +++ b/examples/context_example.py @@ -12,6 +12,7 @@ class Query(graphene.ObjectType): def resolve_me(self, args, context, info): return context['user'] + schema = graphene.Schema(query=Query) query = ''' query something{ diff --git a/examples/simple_example.py b/examples/simple_example.py index 2bc9b44f..e8023266 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -14,6 +14,7 @@ class Query(graphene.ObjectType): def resolve_patron(self, args, context, info): return Patron(id=1, name='Syrus', age=27) + schema = graphene.Schema(query=Query) query = ''' query something{ diff --git a/examples/starwars/tests/test_query.py b/examples/starwars/tests/test_query.py index e6a70735..d88076a5 100644 --- a/examples/starwars/tests/test_query.py +++ b/examples/starwars/tests/test_query.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema @@ -6,6 +7,7 @@ setup() client = Client(schema) + def test_hero_name_query(snapshot): query = ''' query HeroNameQuery { @@ -15,7 +17,6 @@ def test_hero_name_query(snapshot): } ''' snapshot.assert_match(client.execute(query)) - def test_hero_name_and_friends_query(snapshot): diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index f914dc2b..103576c4 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -18,6 +18,7 @@ class Ship(graphene.ObjectType): class ShipConnection(relay.Connection): + class Meta: node = Ship diff --git a/examples/starwars_relay/tests/test_connections.py b/examples/starwars_relay/tests/test_connections.py index e3ecfa7b..bf26c0ec 100644 --- a/examples/starwars_relay/tests/test_connections.py +++ b/examples/starwars_relay/tests/test_connections.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema diff --git a/examples/starwars_relay/tests/test_mutation.py b/examples/starwars_relay/tests/test_mutation.py index 2c07f08c..fb4ab7ad 100644 --- a/examples/starwars_relay/tests/test_mutation.py +++ b/examples/starwars_relay/tests/test_mutation.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index 9ee1d495..28a5decb 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema diff --git a/graphene/__init__.py b/graphene/__init__.py index c15f2cbe..08351d1a 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -74,4 +74,7 @@ if not __SETUP__: 'ConnectionField', 'PageInfo', 'lazy_import', + + # Deprecated + 'AbstractType', ] diff --git a/graphene/pyutils/tests/test_enum.py b/graphene/pyutils/tests/test_enum.py index bf15a620..8854f4c2 100644 --- a/graphene/pyutils/tests/test_enum.py +++ b/graphene/pyutils/tests/test_enum.py @@ -21,6 +21,7 @@ def test__is_dunder(): for name in non_dunder_names: assert _is_dunder(name) is False + def test__is_sunder(): sunder_names = [ '_i_', diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index dea5feab..ed545891 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -2,16 +2,13 @@ import re from collections import Iterable, OrderedDict from functools import partial -import six - from graphql_relay import connection_from_list from promise import Promise, is_thenable -from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, - Union) +from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, + String, Union) from ..types.field import Field from ..types.objecttype import ObjectType, ObjectTypeOptions -from ..utils.props import props from .node import is_node @@ -44,8 +41,10 @@ class ConnectionOptions(ObjectTypeOptions): class Connection(ObjectType): + class Meta: abstract = True + @classmethod def __init_subclass_with_meta__(cls, node=None, name=None, **options): _meta = ConnectionOptions(cls) @@ -60,6 +59,7 @@ class Connection(ObjectType): edge_class = getattr(cls, 'Edge', None) _node = node + class EdgeBase(object): node = Field(_node, description='The item at the end of the edge') cursor = String(required=True, description='A cursor for use in pagination') diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 3b94e635..ea9522bf 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -3,12 +3,12 @@ from collections import OrderedDict from promise import Promise -from ..types import Field, AbstractType, Argument, InputObjectType, String, Field -from ..types.mutation import Mutation, MutationOptions -from ..utils.props import props +from ..types import Field, InputObjectType, String +from ..types.mutation import Mutation class ClientIDMutation(Mutation): + class Meta: abstract = True @@ -17,7 +17,7 @@ class ClientIDMutation(Mutation): input_class = getattr(cls, 'Input', None) name = name or cls.__name__ base_name = re.sub('Payload$', '', name) - + assert not output, "Can't specify any output" assert not arguments, "Can't specify any arguments" @@ -28,8 +28,8 @@ class ClientIDMutation(Mutation): cls.Input = type('{}Input'.format(base_name), bases, { 'client_mutation_id': String(name='clientMutationId') - }) - + }) + arguments = OrderedDict( input=cls.Input(required=True) # 'client_mutation_id': String(name='clientMutationId') @@ -39,7 +39,7 @@ class ClientIDMutation(Mutation): assert mutate_and_get_payload, ( "{name}.mutate_and_get_payload method is required" " in a ClientIDMutation.").format(name=name) - + if not name: name = '{}Payload'.format(base_name) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 33a2628d..ca31447d 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -1,13 +1,11 @@ from collections import OrderedDict from functools import partial -import six - from graphql_relay import from_global_id, to_global_id from ..types import ID, Field, Interface, ObjectType -from ..types.utils import get_type from ..types.interface import InterfaceOptions +from ..types.utils import get_type def is_node(objecttype): @@ -60,6 +58,7 @@ class NodeField(Field): class AbstractNode(Interface): + class Meta: abstract = True diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index f44bc5ab..c769eb89 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,6 +1,6 @@ -from ...types import Field, List, NonNull, ObjectType, String, Argument, Int -from ..connection import Connection, PageInfo, ConnectionField +from ...types import Argument, Field, Int, List, NonNull, ObjectType, String +from ..connection import Connection, ConnectionField, PageInfo from ..node import Node @@ -104,6 +104,7 @@ def test_pageinfo(): def test_connectionfield(): class MyObjectConnection(Connection): + class Meta: node = MyObject @@ -118,6 +119,7 @@ def test_connectionfield(): def test_connectionfield_custom_args(): class MyObjectConnection(Connection): + class Meta: node = MyObject diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 3b543552..11342a44 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -19,6 +19,7 @@ class Letter(ObjectType): class LetterConnection(Connection): + class Meta: node = Letter diff --git a/graphene/relay/tests/test_global_id.py b/graphene/relay/tests/test_global_id.py index b0a6c5cb..6e53bccc 100644 --- a/graphene/relay/tests/test_global_id.py +++ b/graphene/relay/tests/test_global_id.py @@ -1,8 +1,8 @@ from graphql_relay import to_global_id -from ..node import Node, GlobalID -from ...types import NonNull, ID, ObjectType, String +from ...types import ID, NonNull, ObjectType, String from ...types.definitions import GrapheneObjectType +from ..node import GlobalID, Node class CustomNode(Node): diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 6fb2658d..32ff07b8 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -1,11 +1,11 @@ import pytest -from ...types import ( Argument, Field, InputField, ID, - InputObjectType, NonNull, ObjectType, Schema) +from promise import Promise + +from ...types import (ID, Argument, Field, InputField, InputObjectType, + NonNull, ObjectType, Schema) from ...types.scalars import String from ..mutation import ClientIDMutation -from ..node import Node -from promise import Promise class SharedFields(object): @@ -20,6 +20,7 @@ class MyNode(ObjectType): class SaySomething(ClientIDMutation): + class Input: what = String() @@ -32,6 +33,7 @@ class SaySomething(ClientIDMutation): class SaySomethingPromise(ClientIDMutation): + class Input: what = String() @@ -50,6 +52,7 @@ class MyEdge(ObjectType): class OtherMutation(ClientIDMutation): + class Input(SharedFields): additional_field = String() diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index 9fdaa702..e0f65cdd 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -3,7 +3,6 @@ from collections import OrderedDict from graphql_relay import to_global_id from ...types import ObjectType, Schema, String -from ..connection import Connection from ..node import Node @@ -105,7 +104,7 @@ def test_node_field_only_type_wrong(): ) assert len(executed.errors) == 1 assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.' - assert executed.data == { 'onlyNode': None } + assert executed.data == {'onlyNode': None} def test_node_field_only_lazy_type(): @@ -122,7 +121,7 @@ def test_node_field_only_lazy_type_wrong(): ) assert len(executed.errors) == 1 assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.' - assert executed.data == { 'onlyNodeLazy': None } + assert executed.data == {'onlyNodeLazy': None} def test_str_schema(): diff --git a/graphene/test/__init__.py b/graphene/test/__init__.py index a613d905..f7823f48 100644 --- a/graphene/test/__init__.py +++ b/graphene/test/__init__.py @@ -29,6 +29,7 @@ def format_execution_result(execution_result, format_error): class Client(object): + def __init__(self, schema, format_error=None, **execute_options): assert isinstance(schema, Schema) self.schema = schema diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py index 1a67a8ec..ed89e45c 100644 --- a/graphene/tests/issues/test_313.py +++ b/graphene/tests/issues/test_313.py @@ -3,9 +3,11 @@ import graphene from graphene import resolve_only_args + class Query(graphene.ObjectType): rand = graphene.String() + class Success(graphene.ObjectType): yeah = graphene.String() @@ -15,11 +17,13 @@ class Error(graphene.ObjectType): class CreatePostResult(graphene.Union): + class Meta: types = [Success, Error] class CreatePost(graphene.Mutation): + class Input: text = graphene.String(required=True) @@ -37,6 +41,7 @@ class Mutations(graphene.ObjectType): # tests.py + def test_create_post(): query_string = ''' mutation { @@ -52,4 +57,4 @@ def test_create_post(): result = schema.execute(query_string) assert not result.errors - assert result.data['createPost']['result']['__typename'] == 'Success' \ No newline at end of file + assert result.data['createPost']['result']['__typename'] == 'Success' diff --git a/graphene/tests/issues/test_356.py b/graphene/tests/issues/test_356.py index 605594e1..8eeaed10 100644 --- a/graphene/tests/issues/test_356.py +++ b/graphene/tests/issues/test_356.py @@ -1,19 +1,25 @@ # https://github.com/graphql-python/graphene/issues/356 import pytest + import graphene from graphene import relay + class SomeTypeOne(graphene.ObjectType): pass + class SomeTypeTwo(graphene.ObjectType): pass + class MyUnion(graphene.Union): + class Meta: types = (SomeTypeOne, SomeTypeTwo) + def test_issue(): with pytest.raises(Exception) as exc_info: class Query(graphene.ObjectType): diff --git a/graphene/tests/issues/test_425_graphene2.py b/graphene/tests/issues/test_425_graphene2.py index 38b12143..7f92a75a 100644 --- a/graphene/tests/issues/test_425_graphene2.py +++ b/graphene/tests/issues/test_425_graphene2.py @@ -1,23 +1,25 @@ # https://github.com/graphql-python/graphene/issues/425 # Adapted for Graphene 2.0 -import six from graphene.types.objecttype import ObjectType, ObjectTypeOptions + class SpecialOptions(ObjectTypeOptions): other_attr = None class SpecialObjectType(ObjectType): + @classmethod def __init_subclass_with_meta__(cls, other_attr='default', **options): _meta = SpecialOptions(cls) _meta.other_attr = other_attr super(SpecialObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) - + def test_special_objecttype_could_be_subclassed(): class MyType(SpecialObjectType): + class Meta: other_attr = 'yeah!' @@ -36,5 +38,5 @@ def test_special_objecttype_inherit_meta_options(): pass assert MyType._meta.name == 'MyType' - assert MyType._meta.default_resolver == None + assert MyType._meta.default_resolver is None assert MyType._meta.interfaces == () diff --git a/graphene/tests/issues/test_490.py b/graphene/tests/issues/test_490.py index 0ecd8911..fc6d016e 100644 --- a/graphene/tests/issues/test_490.py +++ b/graphene/tests/issues/test_490.py @@ -1,7 +1,6 @@ # https://github.com/graphql-python/graphene/issues/313 import graphene -from graphene import resolve_only_args class Query(graphene.ObjectType): diff --git a/graphene/types/__init__.py b/graphene/types/__init__.py index 40d60df9..d1554be2 100644 --- a/graphene/types/__init__.py +++ b/graphene/types/__init__.py @@ -38,4 +38,7 @@ __all__ = [ 'Argument', 'Dynamic', 'Union', + + # Deprecated + 'AbstractType', ] diff --git a/graphene/types/argument.py b/graphene/types/argument.py index cb28ee5e..df032510 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -1,9 +1,9 @@ from collections import OrderedDict from itertools import chain +from .dynamic import Dynamic from .mountedtype import MountedType from .structures import NonNull -from .dynamic import Dynamic from .utils import get_type diff --git a/graphene/types/base.py b/graphene/types/base.py index b4a87e73..ad8869b9 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -13,7 +13,7 @@ class BaseOptions(object): def freeze(self): self._frozen = True - + def __setattr__(self, name, value): if not self._frozen: super(BaseOptions, self).__setattr__(name, value) @@ -25,6 +25,7 @@ class BaseOptions(object): class BaseType(SubclassWithMeta): + @classmethod def __init_subclass_with_meta__(cls, name=None, description=None, _meta=None): assert "_meta" not in cls.__dict__, "Can't assign directly meta" diff --git a/graphene/types/enum.py b/graphene/types/enum.py index af305fde..c98fb5f6 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -2,10 +2,10 @@ from collections import OrderedDict import six +from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta + from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType -from graphene.pyutils.init_subclass import InitSubclassMeta -from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta try: from enum import Enum as PyEnum @@ -27,6 +27,7 @@ class EnumOptions(BaseOptions): class EnumMeta(SubclassWithMeta_Meta): + def __new__(cls, name, bases, classdict, **options): enum = PyEnum(cls.__name__, OrderedDict(classdict, __eq__=eq_enum)) return SubclassWithMeta_Meta.__new__(cls, name, bases, OrderedDict(classdict, __enum__=enum), **options) @@ -53,6 +54,7 @@ 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) diff --git a/graphene/types/field.py b/graphene/types/field.py index 06632d35..9e699a12 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -8,7 +8,6 @@ from .structures import NonNull from .unmountedtype import UnmountedType from .utils import get_type - base_type = type diff --git a/graphene/types/generic.py b/graphene/types/generic.py index a1034bfd..3170e38d 100644 --- a/graphene/types/generic.py +++ b/graphene/types/generic.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals +from graphene.types.scalars import MAX_INT, MIN_INT from graphql.language.ast import (BooleanValue, FloatValue, IntValue, - StringValue, ListValue, ObjectValue) + ListValue, ObjectValue, StringValue) -from graphene.types.scalars import MIN_INT, MAX_INT from .scalars import Scalar diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index d3d82392..5615db3d 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -1,11 +1,10 @@ from collections import OrderedDict +from .base import BaseOptions, BaseType from .inputfield import InputField from .unmountedtype import UnmountedType from .utils import yank_fields_from_attrs -from .base import BaseOptions, BaseType - class InputObjectTypeOptions(BaseOptions): fields = None # type: Dict[str, Field] diff --git a/graphene/types/interface.py b/graphene/types/interface.py index 37d38ac1..389cb82c 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -1,8 +1,8 @@ -from .field import Field -from .utils import yank_fields_from_attrs from collections import OrderedDict from .base import BaseOptions, BaseType +from .field import Field +from .utils import yank_fields_from_attrs class InterfaceOptions(BaseOptions): @@ -30,7 +30,7 @@ class Interface(BaseType): fields.update( yank_fields_from_attrs(base.__dict__, _as=Field) ) - + if _meta.fields: _meta.fields.update(fields) else: diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 8b2bb88b..8756412f 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -3,10 +3,8 @@ from collections import OrderedDict from ..utils.get_unbound_function import get_unbound_function from ..utils.props import props from .field import Field -from .utils import yank_fields_from_attrs from .objecttype import ObjectType, ObjectTypeOptions - -from .base import BaseOptions, BaseType +from .utils import yank_fields_from_attrs class MutationOptions(ObjectTypeOptions): @@ -20,7 +18,8 @@ class Mutation(ObjectType): Mutation Type Definition ''' @classmethod - def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None, _meta=None, abstract=False, **options): + def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None, + _meta=None, abstract=False, **options): if abstract: return if not _meta: @@ -36,7 +35,7 @@ class Mutation(ObjectType): yank_fields_from_attrs(base.__dict__, _as=Field) ) output = cls - + if not arguments: input_class = getattr(cls, 'Arguments', None) if not input_class: @@ -64,7 +63,7 @@ class Mutation(ObjectType): _meta.arguments = arguments super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) - + @classmethod def Field(cls, *args, **kwargs): return Field( diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index ac7214f6..9987e4e4 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -1,15 +1,14 @@ from collections import OrderedDict +from .base import BaseOptions, BaseType from .field import Field from .interface import Interface from .utils import yank_fields_from_attrs -from .base import BaseOptions, BaseType - class ObjectTypeOptions(BaseOptions): fields = None # type: Dict[str, Field] - interfaces = () # type: List[Type[Interface]] + interfaces = () # type: List[Type[Interface]] class ObjectType(BaseType): @@ -20,7 +19,10 @@ class ObjectType(BaseType): have a name, but most importantly describe their fields. ''' @classmethod - def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, _meta=None, abstract=False, **options): + def __init_subclass_with_meta__( + cls, interfaces=(), + possible_types=(), + default_resolver=None, _meta=None, abstract=False, **options): if abstract: return if not _meta: @@ -31,7 +33,7 @@ class ObjectType(BaseType): for interface in interfaces: assert issubclass(interface, Interface), ( 'All interfaces of {} must be a subclass of Interface. Received "{}".' - ).format(name, interface) + ).format(cls.__name__, interface) fields.update(interface._meta.fields) for base in reversed(cls.__mro__): diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 1ca17ff8..ccfb089c 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -3,8 +3,8 @@ import six from graphql.language.ast import (BooleanValue, FloatValue, IntValue, StringValue) -from .unmountedtype import UnmountedType from .base import BaseOptions, BaseType +from .unmountedtype import UnmountedType class ScalarOptions(BaseOptions): diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index e485c01b..744c9e29 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -1,11 +1,12 @@ -import pytest from functools import partial +import pytest + from ..argument import Argument, to_arguments from ..field import Field from ..inputfield import InputField -from ..structures import NonNull from ..scalars import String +from ..structures import NonNull def test_argument(): @@ -73,4 +74,4 @@ def test_argument_with_lazy_type(): def test_argument_with_lazy_partial_type(): MyType = object() arg = Argument(partial(lambda: MyType)) - assert arg.type == MyType \ No newline at end of file + assert arg.type == MyType diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index 651850cb..e01bdc8a 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -1,4 +1,5 @@ import datetime + import pytz from ..datetime import DateTime, Time diff --git a/graphene/types/tests/test_dynamic.py b/graphene/types/tests/test_dynamic.py index 61dcbd81..4e72395f 100644 --- a/graphene/types/tests/test_dynamic.py +++ b/graphene/types/tests/test_dynamic.py @@ -1,6 +1,6 @@ -from ..structures import List, NonNull -from ..scalars import String from ..dynamic import Dynamic +from ..scalars import String +from ..structures import List, NonNull def test_dynamic(): diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 6cd22bd9..42f00605 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,7 +1,7 @@ +from ..argument import Argument from ..enum import Enum, PyEnum from ..field import Field from ..inputfield import InputField -from ..argument import Argument def test_enum_construction(): diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index 80a32154..d3782fe5 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -1,10 +1,11 @@ -import pytest from functools import partial +import pytest + from ..argument import Argument from ..field import Field -from ..structures import NonNull from ..scalars import String +from ..structures import NonNull from .utils import MyLazyType @@ -22,7 +23,7 @@ def test_field_basic(): resolver = lambda: None deprecation_reason = 'Deprecated now' description = 'My Field' - my_default='something' + my_default = 'something' field = Field( MyType, name='name', diff --git a/graphene/types/tests/test_inputfield.py b/graphene/types/tests/test_inputfield.py index a0888e44..bfedfb05 100644 --- a/graphene/types/tests/test_inputfield.py +++ b/graphene/types/tests/test_inputfield.py @@ -1,4 +1,3 @@ -import pytest from functools import partial from ..inputfield import InputField diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index 7cd3064c..77b1eb0e 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -1,9 +1,9 @@ -from ..field import Field from ..argument import Argument +from ..field import Field from ..inputfield import InputField -from ..objecttype import ObjectType from ..inputobjecttype import InputObjectType +from ..objecttype import ObjectType from ..unmountedtype import UnmountedType @@ -68,7 +68,7 @@ def test_generate_inputobjecttype_as_argument(): class MyObjectType(ObjectType): field = Field(MyType, input=MyInputObjectType()) - + assert 'field' in MyObjectType._meta.fields field = MyObjectType._meta.fields['field'] assert isinstance(field, Field) diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index ef6425a9..c912df56 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -1,4 +1,3 @@ -import json from ..json import JSONString from ..objecttype import ObjectType @@ -19,7 +18,7 @@ def test_jsonstring_query(): json_value = '{"key": "value"}' json_value_quoted = json_value.replace('"', '\\"') - result = schema.execute('''{ json(input: "%s") }'''%json_value_quoted) + result = schema.execute('''{ json(input: "%s") }''' % json_value_quoted) assert not result.errors assert result.data == { 'json': json_value diff --git a/graphene/types/tests/test_mountedtype.py b/graphene/types/tests/test_mountedtype.py index 0bc39a2f..9dcc11c7 100644 --- a/graphene/types/tests/test_mountedtype.py +++ b/graphene/types/tests/test_mountedtype.py @@ -1,11 +1,10 @@ -import pytest -from ..mountedtype import MountedType from ..field import Field from ..scalars import String class CustomField(Field): + def __init__(self, *args, **kwargs): self.metadata = kwargs.pop('metadata', None) super(CustomField, self).__init__(*args, **kwargs) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 0f6d8900..081e823c 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -1,11 +1,11 @@ import pytest +from ..argument import Argument +from ..dynamic import Dynamic from ..mutation import Mutation from ..objecttype import ObjectType -from ..schema import Schema -from ..argument import Argument from ..scalars import String -from ..dynamic import Dynamic +from ..schema import Schema def test_generate_mutation_no_args(): @@ -24,6 +24,7 @@ def test_generate_mutation_no_args(): def test_generate_mutation_with_meta(): class MyMutation(Mutation): + class Meta: name = 'MyOtherMutation' description = 'Documentation' @@ -52,6 +53,7 @@ def test_mutation_custom_output_type(): name = String() class CreateUser(Mutation): + class Input: name = String() @@ -70,6 +72,7 @@ def test_mutation_custom_output_type(): def test_mutation_execution(): class CreateUser(Mutation): + class Input: name = String() dynamic = Dynamic(lambda: String()) diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index 2304d7b6..5ff972de 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -187,6 +187,7 @@ def test_generate_objecttype_description(): def test_objecttype_with_possible_types(): class MyObjectType(ObjectType): + class Meta: possible_types = (dict, ) @@ -196,6 +197,7 @@ def test_objecttype_with_possible_types(): def test_objecttype_with_possible_types_and_is_type_of_should_raise(): with pytest.raises(AssertionError) as excinfo: class MyObjectType(ObjectType): + class Meta: possible_types = (dict, ) diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index daeb63e8..254570e1 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -1,18 +1,18 @@ import json from functools import partial -from graphql import Source, execute, parse, GraphQLError +from graphql import GraphQLError, Source, execute, parse +from ..dynamic import Dynamic from ..field import Field -from ..interface import Interface from ..inputfield import InputField from ..inputobjecttype import InputObjectType +from ..interface import Interface from ..objecttype import ObjectType from ..scalars import Int, String from ..schema import Schema from ..structures import List from ..union import Union -from ..dynamic import Dynamic def test_query(): @@ -48,6 +48,7 @@ def test_query_union(): return isinstance(root, two_object) class MyUnion(Union): + class Meta: types = (One, Two) @@ -81,6 +82,7 @@ def test_query_interface(): base = String() class One(ObjectType): + class Meta: interfaces = (MyInterface, ) @@ -91,6 +93,7 @@ def test_query_interface(): return isinstance(root, one_object) class Two(ObjectType): + class Meta: interfaces = (MyInterface, ) @@ -268,6 +271,7 @@ def test_query_middlewares(): def test_objecttype_on_instances(): class Ship: + def __init__(self, name): self.name = name diff --git a/graphene/types/tests/test_resolver.py b/graphene/types/tests/test_resolver.py index 25629979..64fdf94e 100644 --- a/graphene/types/tests/test_resolver.py +++ b/graphene/types/tests/test_resolver.py @@ -1,6 +1,6 @@ -import pytest -from ..resolver import attr_resolver, dict_resolver, get_default_resolver, set_default_resolver +from ..resolver import (attr_resolver, dict_resolver, get_default_resolver, + set_default_resolver) args = {} context = None diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py index af62faa1..3c6383fa 100644 --- a/graphene/types/tests/test_scalar.py +++ b/graphene/types/tests/test_scalar.py @@ -1,4 +1,3 @@ -import pytest from ..scalars import Scalar diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index af9bc14c..2ed5c099 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,9 +1,9 @@ import pytest -from ..schema import Schema +from ..field import Field from ..objecttype import ObjectType from ..scalars import String -from ..field import Field +from ..schema import Schema class MyOtherType(ObjectType): diff --git a/graphene/types/tests/test_structures.py b/graphene/types/tests/test_structures.py index 082bf097..6fb290fd 100644 --- a/graphene/types/tests/test_structures.py +++ b/graphene/types/tests/test_structures.py @@ -1,8 +1,9 @@ -import pytest from functools import partial -from ..structures import List, NonNull +import pytest + from ..scalars import String +from ..structures import List, NonNull from .utils import MyLazyType @@ -15,7 +16,7 @@ def test_list(): def test_list_with_unmounted_type(): with pytest.raises(Exception) as exc_info: List(String()) - + assert str(exc_info.value) == 'List could not have a mounted String() as inner type. Try with List(String).' @@ -80,14 +81,14 @@ def test_nonnull_inherited_works_list(): def test_nonnull_inherited_dont_work_nonnull(): with pytest.raises(Exception) as exc_info: NonNull(NonNull(String)) - + assert str(exc_info.value) == 'Can only create NonNull of a Nullable GraphQLType but got: String!.' def test_nonnull_with_unmounted_type(): with pytest.raises(Exception) as exc_info: NonNull(String()) - + assert str(exc_info.value) == 'NonNull could not have a mounted String() as inner type. Try with NonNull(String).' diff --git a/graphene/types/tests/test_union.py b/graphene/types/tests/test_union.py index d7ba2f31..ac2708ad 100644 --- a/graphene/types/tests/test_union.py +++ b/graphene/types/tests/test_union.py @@ -47,6 +47,7 @@ def test_generate_union_with_no_types(): def test_union_can_be_mounted(): class MyUnion(Union): + class Meta: types = (MyObjectType1, MyObjectType2) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 02069e41..acc0fbe3 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -11,10 +11,10 @@ from graphql.type.typemap import GraphQLTypeMap from ..utils.get_unbound_function import get_unbound_function from ..utils.str_converters import to_camel_case -from .definitions import (GrapheneEnumType, GrapheneInputObjectType, - GrapheneInterfaceType, GrapheneObjectType, - GrapheneScalarType, GrapheneUnionType, - GrapheneGraphQLType) +from .definitions import (GrapheneEnumType, GrapheneGraphQLType, + GrapheneInputObjectType, GrapheneInterfaceType, + GrapheneObjectType, GrapheneScalarType, + GrapheneUnionType) from .dynamic import Dynamic from .enum import Enum from .field import Field @@ -59,6 +59,7 @@ def is_type_of_from_possible_types(possible_types, root, context, info): class TypeMap(GraphQLTypeMap): + def __init__(self, types, auto_camelcase=True, schema=None): self.auto_camelcase = auto_camelcase self.schema = schema diff --git a/graphene/types/union.py b/graphene/types/union.py index 3068a96c..f5c12c04 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -1,7 +1,5 @@ -from .unmountedtype import UnmountedType -from .objecttype import ObjectType - from .base import BaseOptions, BaseType +from .unmountedtype import UnmountedType class UnionOptions(BaseOptions): diff --git a/graphene/types/utils.py b/graphene/types/utils.py index affffe38..c4434199 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -1,6 +1,7 @@ import inspect from collections import OrderedDict from functools import partial + from six import string_types from ..utils.module_loading import import_string @@ -40,6 +41,6 @@ def yank_fields_from_attrs(attrs, _as=None, sort=True): def get_type(_type): if isinstance(_type, string_types): return import_string(_type) - if inspect.isfunction(_type) or type(_type) is partial: + if inspect.isfunction(_type) or isinstance(_type, partial): return _type() return _type diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index 00ddb09f..d6d33cb6 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -1,10 +1,11 @@ import six -from .props import props from ..pyutils.init_subclass import InitSubclassMeta +from .props import props class SubclassWithMeta_Meta(InitSubclassMeta): + def __repr__(cls): return cls._meta.name diff --git a/graphene/utils/tests/test_module_loading.py b/graphene/utils/tests/test_module_loading.py index 7184ed2c..dd67ffe1 100644 --- a/graphene/utils/tests/test_module_loading.py +++ b/graphene/utils/tests/test_module_loading.py @@ -1,7 +1,8 @@ from pytest import raises -from graphene import String, ObjectType -from ..module_loading import lazy_import, import_string +from graphene import ObjectType, String + +from ..module_loading import import_string, lazy_import def test_import_string(): diff --git a/graphene/utils/tests/test_trim_docstring.py b/graphene/utils/tests/test_trim_docstring.py index 3aab5f11..9695fad4 100644 --- a/graphene/utils/tests/test_trim_docstring.py +++ b/graphene/utils/tests/test_trim_docstring.py @@ -9,11 +9,10 @@ def test_trim_docstring(): Multiple paragraphs too """ - pass assert (trim_docstring(WellDocumentedObject.__doc__) == - "This object is very well-documented. It has multiple lines in its\n" - "description.\n\nMultiple paragraphs too") + "This object is very well-documented. It has multiple lines in its\n" + "description.\n\nMultiple paragraphs too") class UndocumentedObject(object): pass From ec5697b185b81d7e427004ebb26767bd24d642d2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 21:53:35 -0700 Subject: [PATCH 23/82] Fixed Connection side effects and add into breaking changes. --- UPGRADE-v2.0.md | 33 +++++++++++++++++++++++++++++++++ graphene/relay/connection.py | 5 +---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 8872aaca..b03c9732 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -47,3 +47,36 @@ class Dog(ObjectType, interfaces=[Pet]): name = String() ``` + +## Breaking Changes + +* Node types no longer have a `Connection` by default. + In 2.0 and onwoards `Connection`s should be defined explicitly. + + Before: + + ```python + class User(ObjectType): + class Meta: + interfaces = [relay.Node] + name = String() + + class Query(ObjectType): + user_connection = relay.ConnectionField(User) + ``` + + With 2.0: + + ```python + class User(ObjectType): + class Meta: + interfaces = [relay.Node] + name = String() + + class UserConnection(relay.Connection): + class Meta: + node = User + + class Query(ObjectType): + user_connection = relay.ConnectionField(UserConnection) + ``` diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index ed545891..ad2ca119 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -98,10 +98,7 @@ class IterableConnectionField(Field): @property def type(self): type = super(IterableConnectionField, self).type - if is_node(type): - connection_type = type.Connection - else: - connection_type = type + connection_type = type assert issubclass(connection_type, Connection), ( '{} type have to be a subclass of Connection. Received "{}".' ).format(self.__class__.__name__, connection_type) From 8ff33802918d0e00f598ee0ea36d3438aa708219 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 21:53:53 -0700 Subject: [PATCH 24/82] Removed testing in Python 3.4 and start testing in 3.6 --- .travis.yml | 2 +- graphene/relay/connection.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 89b17c0c..d87939e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: python sudo: false python: - 2.7 -- 3.4 - 3.5 +- 3.6 - pypy before_install: - | diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index ad2ca119..20a5e648 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -9,7 +9,6 @@ from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) from ..types.field import Field from ..types.objecttype import ObjectType, ObjectTypeOptions -from .node import is_node class PageInfo(ObjectType): From 26686da30e6aadbbc7d1e2921d36c79d392ded15 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 15:17:10 -0700 Subject: [PATCH 25/82] Added inputobjecttype containers --- graphene/types/inputobjecttype.py | 29 ++++++++++++++++++++++++++-- graphene/types/tests/test_typemap.py | 10 +++++++++- graphene/types/typemap.py | 1 + 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 5615db3d..19bb7c74 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -8,9 +8,10 @@ from .utils import yank_fields_from_attrs class InputObjectTypeOptions(BaseOptions): fields = None # type: Dict[str, Field] + create_container = None # type: Callable -class InputObjectType(UnmountedType, BaseType): +class InputObjectType(dict, UnmountedType, BaseType): ''' Input Object Type Definition @@ -19,9 +20,30 @@ class InputObjectType(UnmountedType, BaseType): Using `NonNull` will ensure that a value must be provided by the query ''' + def __init__(self, *args, **kwargs): + as_container = kwargs.pop('_as_container', False) + if as_container: + # Is inited as container for the input args + self.__init_container__(*args, **kwargs) + else: + # Is inited as UnmountedType, e.g. + # + # class MyObjectType(graphene.ObjectType): + # my_input = MyInputType(required=True) + # + UnmountedType.__init__(self, *args, **kwargs) + + def __init_container__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + for key, value in self.items(): + setattr(self, key, value) + + @classmethod + def create_container(cls, data): + return cls(data, _as_container=True) @classmethod - def __init_subclass_with_meta__(cls, **options): + def __init_subclass_with_meta__(cls, create_container=None, **options): _meta = InputObjectTypeOptions(cls) fields = OrderedDict() @@ -31,6 +53,9 @@ class InputObjectType(UnmountedType, BaseType): ) _meta.fields = fields + if create_container is None: + create_container = cls.create_container + _meta.create_container = create_container super(InputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 475d0905..3a0f20dd 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -135,9 +135,17 @@ def test_inputobject(): assert graphql_type.name == 'MyInputObjectType' assert graphql_type.description == 'Description' + # Container + container = graphql_type.create_container({'bar': 'oh!'}) + assert isinstance(container, MyInputObjectType) + assert 'bar' in container + assert container.bar == 'oh!' + assert 'foo_bar' not in container + fields = graphql_type.fields assert list(fields.keys()) == ['fooBar', 'gizmo', 'own'] - assert fields['own'].type == graphql_type + own_field = fields['own'] + assert own_field.type == graphql_type foo_field = fields['fooBar'] assert isinstance(foo_field, GraphQLInputObjectField) assert foo_field.description == 'Field description' diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index acc0fbe3..7e0d9ca0 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -195,6 +195,7 @@ class TypeMap(GraphQLTypeMap): graphene_type=type, name=type._meta.name, description=type._meta.description, + container_type=type._meta.create_container, fields=partial( self.construct_fields_for_type, map, type, is_input_type=True), ) From f7fdc9aa3dcb9e15cb2d5f5c5ed00d01a436f2e6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 16:08:48 -0700 Subject: [PATCH 26/82] Initial version of function annotations --- graphene/pyutils/compat.py | 11 + graphene/pyutils/signature.py | 808 ++++++++++++++++++++++++++ graphene/types/enum.py | 5 +- graphene/utils/annotate.py | 35 ++ graphene/utils/tests/test_annotate.py | 33 ++ 5 files changed, 888 insertions(+), 4 deletions(-) create mode 100644 graphene/pyutils/compat.py create mode 100644 graphene/pyutils/signature.py create mode 100644 graphene/utils/annotate.py create mode 100644 graphene/utils/tests/test_annotate.py diff --git a/graphene/pyutils/compat.py b/graphene/pyutils/compat.py new file mode 100644 index 00000000..68734a99 --- /dev/null +++ b/graphene/pyutils/compat.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import + +try: + from enum import Enum +except ImportError: + from .enum import Enum + +try: + from inspect import signature +except ImportError: + from .signature import signature diff --git a/graphene/pyutils/signature.py b/graphene/pyutils/signature.py new file mode 100644 index 00000000..1308ded9 --- /dev/null +++ b/graphene/pyutils/signature.py @@ -0,0 +1,808 @@ +# Copyright 2001-2013 Python Software Foundation; All Rights Reserved +"""Function signature objects for callables +Back port of Python 3.3's function signature tools from the inspect module, +modified to be compatible with Python 2.7 and 3.2+. +""" +from __future__ import absolute_import, division, print_function +import itertools +import functools +import re +import types + +from collections import OrderedDict + +__version__ = "0.4" + +__all__ = ['BoundArguments', 'Parameter', 'Signature', 'signature'] + + +_WrapperDescriptor = type(type.__call__) +_MethodWrapper = type(all.__call__) + +_NonUserDefinedCallables = (_WrapperDescriptor, + _MethodWrapper, + types.BuiltinFunctionType) + + +def formatannotation(annotation, base_module=None): + if isinstance(annotation, type): + if annotation.__module__ in ('builtins', '__builtin__', base_module): + return annotation.__name__ + return annotation.__module__+'.'+annotation.__name__ + return repr(annotation) + + +def _get_user_defined_method(cls, method_name, *nested): + try: + if cls is type: + return + meth = getattr(cls, method_name) + for name in nested: + meth = getattr(meth, name, meth) + except AttributeError: + return + else: + if not isinstance(meth, _NonUserDefinedCallables): + # Once '__signature__' will be added to 'C'-level + # callables, this check won't be necessary + return meth + + +def signature(obj): + '''Get a signature object for the passed callable.''' + + if not callable(obj): + raise TypeError('{0!r} is not a callable object'.format(obj)) + + if isinstance(obj, types.MethodType): + sig = signature(obj.__func__) + if obj.__self__ is None: + # Unbound method: the first parameter becomes positional-only + if sig.parameters: + first = sig.parameters.values()[0].replace( + kind=_POSITIONAL_ONLY) + return sig.replace( + parameters=(first,) + tuple(sig.parameters.values())[1:]) + else: + return sig + else: + # In this case we skip the first parameter of the underlying + # function (usually `self` or `cls`). + return sig.replace(parameters=tuple(sig.parameters.values())[1:]) + + try: + sig = obj.__signature__ + except AttributeError: + pass + else: + if sig is not None: + return sig + + try: + # Was this function wrapped by a decorator? + wrapped = obj.__wrapped__ + except AttributeError: + pass + else: + return signature(wrapped) + + if isinstance(obj, types.FunctionType): + return Signature.from_function(obj) + + if isinstance(obj, functools.partial): + sig = signature(obj.func) + + new_params = OrderedDict(sig.parameters.items()) + + partial_args = obj.args or () + partial_keywords = obj.keywords or {} + try: + ba = sig.bind_partial(*partial_args, **partial_keywords) + except TypeError as ex: + msg = 'partial object {0!r} has incorrect arguments'.format(obj) + raise ValueError(msg) + + for arg_name, arg_value in ba.arguments.items(): + param = new_params[arg_name] + if arg_name in partial_keywords: + # We set a new default value, because the following code + # is correct: + # + # >>> def foo(a): print(a) + # >>> print(partial(partial(foo, a=10), a=20)()) + # 20 + # >>> print(partial(partial(foo, a=10), a=20)(a=30)) + # 30 + # + # So, with 'partial' objects, passing a keyword argument is + # like setting a new default value for the corresponding + # parameter + # + # We also mark this parameter with '_partial_kwarg' + # flag. Later, in '_bind', the 'default' value of this + # parameter will be added to 'kwargs', to simulate + # the 'functools.partial' real call. + new_params[arg_name] = param.replace(default=arg_value, + _partial_kwarg=True) + + elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and + not param._partial_kwarg): + new_params.pop(arg_name) + + return sig.replace(parameters=new_params.values()) + + sig = None + if isinstance(obj, type): + # obj is a class or a metaclass + + # First, let's see if it has an overloaded __call__ defined + # in its metaclass + call = _get_user_defined_method(type(obj), '__call__') + if call is not None: + sig = signature(call) + else: + # Now we check if the 'obj' class has a '__new__' method + new = _get_user_defined_method(obj, '__new__') + if new is not None: + sig = signature(new) + else: + # Finally, we should have at least __init__ implemented + init = _get_user_defined_method(obj, '__init__') + if init is not None: + sig = signature(init) + elif not isinstance(obj, _NonUserDefinedCallables): + # An object with __call__ + # We also check that the 'obj' is not an instance of + # _WrapperDescriptor or _MethodWrapper to avoid + # infinite recursion (and even potential segfault) + call = _get_user_defined_method(type(obj), '__call__', 'im_func') + if call is not None: + sig = signature(call) + + if sig is not None: + # For classes and objects we skip the first parameter of their + # __call__, __new__, or __init__ methods + return sig.replace(parameters=tuple(sig.parameters.values())[1:]) + + if isinstance(obj, types.BuiltinFunctionType): + # Raise a nicer error message for builtins + msg = 'no signature found for builtin function {0!r}'.format(obj) + raise ValueError(msg) + + raise ValueError('callable {0!r} is not supported by signature'.format(obj)) + + +class _void(object): + '''A private marker - used in Parameter & Signature''' + + +class _empty(object): + pass + + +class _ParameterKind(int): + def __new__(self, *args, **kwargs): + obj = int.__new__(self, *args) + obj._name = kwargs['name'] + return obj + + def __str__(self): + return self._name + + def __repr__(self): + return '<_ParameterKind: {0!r}>'.format(self._name) + + +_POSITIONAL_ONLY = _ParameterKind(0, name='POSITIONAL_ONLY') +_POSITIONAL_OR_KEYWORD = _ParameterKind(1, name='POSITIONAL_OR_KEYWORD') +_VAR_POSITIONAL = _ParameterKind(2, name='VAR_POSITIONAL') +_KEYWORD_ONLY = _ParameterKind(3, name='KEYWORD_ONLY') +_VAR_KEYWORD = _ParameterKind(4, name='VAR_KEYWORD') + + +class Parameter(object): + '''Represents a parameter in a function signature. + Has the following public attributes: + * name : str + The name of the parameter as a string. + * default : object + The default value for the parameter if specified. If the + parameter has no default value, this attribute is not set. + * annotation + The annotation for the parameter if specified. If the + parameter has no annotation, this attribute is not set. + * kind : str + Describes how argument values are bound to the parameter. + Possible values: `Parameter.POSITIONAL_ONLY`, + `Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`, + `Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`. + ''' + + __slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg') + + POSITIONAL_ONLY = _POSITIONAL_ONLY + POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD + VAR_POSITIONAL = _VAR_POSITIONAL + KEYWORD_ONLY = _KEYWORD_ONLY + VAR_KEYWORD = _VAR_KEYWORD + + empty = _empty + + def __init__(self, name, kind, default=_empty, annotation=_empty, + _partial_kwarg=False): + + if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD, + _VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD): + raise ValueError("invalid value for 'Parameter.kind' attribute") + self._kind = kind + + if default is not _empty: + if kind in (_VAR_POSITIONAL, _VAR_KEYWORD): + msg = '{0} parameters cannot have default values'.format(kind) + raise ValueError(msg) + self._default = default + self._annotation = annotation + + if name is None: + if kind != _POSITIONAL_ONLY: + raise ValueError("None is not a valid name for a " + "non-positional-only parameter") + self._name = name + else: + name = str(name) + if kind != _POSITIONAL_ONLY and not re.match(r'[a-z_]\w*$', name, re.I): + msg = '{0!r} is not a valid parameter name'.format(name) + raise ValueError(msg) + self._name = name + + self._partial_kwarg = _partial_kwarg + + @property + def name(self): + return self._name + + @property + def default(self): + return self._default + + @property + def annotation(self): + return self._annotation + + @property + def kind(self): + return self._kind + + def replace(self, name=_void, kind=_void, annotation=_void, + default=_void, _partial_kwarg=_void): + '''Creates a customized copy of the Parameter.''' + + if name is _void: + name = self._name + + if kind is _void: + kind = self._kind + + if annotation is _void: + annotation = self._annotation + + if default is _void: + default = self._default + + if _partial_kwarg is _void: + _partial_kwarg = self._partial_kwarg + + return type(self)(name, kind, default=default, annotation=annotation, + _partial_kwarg=_partial_kwarg) + + def __str__(self): + kind = self.kind + + formatted = self._name + if kind == _POSITIONAL_ONLY: + if formatted is None: + formatted = '' + formatted = '<{0}>'.format(formatted) + + # Add annotation and default value + if self._annotation is not _empty: + formatted = '{0}:{1}'.format(formatted, + formatannotation(self._annotation)) + + if self._default is not _empty: + formatted = '{0}={1}'.format(formatted, repr(self._default)) + + if kind == _VAR_POSITIONAL: + formatted = '*' + formatted + elif kind == _VAR_KEYWORD: + formatted = '**' + formatted + + return formatted + + def __repr__(self): + return '<{0} at {1:#x} {2!r}>'.format(self.__class__.__name__, + id(self), self.name) + + def __hash__(self): + msg = "unhashable type: '{0}'".format(self.__class__.__name__) + raise TypeError(msg) + + def __eq__(self, other): + return (issubclass(other.__class__, Parameter) and + self._name == other._name and + self._kind == other._kind and + self._default == other._default and + self._annotation == other._annotation) + + def __ne__(self, other): + return not self.__eq__(other) + + +class BoundArguments(object): + '''Result of `Signature.bind` call. Holds the mapping of arguments + to the function's parameters. + Has the following public attributes: + * arguments : OrderedDict + An ordered mutable mapping of parameters' names to arguments' values. + Does not contain arguments' default values. + * signature : Signature + The Signature object that created this instance. + * args : tuple + Tuple of positional arguments values. + * kwargs : dict + Dict of keyword arguments values. + ''' + + def __init__(self, signature, arguments): + self.arguments = arguments + self._signature = signature + + @property + def signature(self): + return self._signature + + @property + def args(self): + args = [] + for param_name, param in self._signature.parameters.items(): + if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or + param._partial_kwarg): + # Keyword arguments mapped by 'functools.partial' + # (Parameter._partial_kwarg is True) are mapped + # in 'BoundArguments.kwargs', along with VAR_KEYWORD & + # KEYWORD_ONLY + break + + try: + arg = self.arguments[param_name] + except KeyError: + # We're done here. Other arguments + # will be mapped in 'BoundArguments.kwargs' + break + else: + if param.kind == _VAR_POSITIONAL: + # *args + args.extend(arg) + else: + # plain argument + args.append(arg) + + return tuple(args) + + @property + def kwargs(self): + kwargs = {} + kwargs_started = False + for param_name, param in self._signature.parameters.items(): + if not kwargs_started: + if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or + param._partial_kwarg): + kwargs_started = True + else: + if param_name not in self.arguments: + kwargs_started = True + continue + + if not kwargs_started: + continue + + try: + arg = self.arguments[param_name] + except KeyError: + pass + else: + if param.kind == _VAR_KEYWORD: + # **kwargs + kwargs.update(arg) + else: + # plain keyword argument + kwargs[param_name] = arg + + return kwargs + + def __hash__(self): + msg = "unhashable type: '{0}'".format(self.__class__.__name__) + raise TypeError(msg) + + def __eq__(self, other): + return (issubclass(other.__class__, BoundArguments) and + self.signature == other.signature and + self.arguments == other.arguments) + + def __ne__(self, other): + return not self.__eq__(other) + + +class Signature(object): + '''A Signature object represents the overall signature of a function. + It stores a Parameter object for each parameter accepted by the + function, as well as information specific to the function itself. + A Signature object has the following public attributes and methods: + * parameters : OrderedDict + An ordered mapping of parameters' names to the corresponding + Parameter objects (keyword-only arguments are in the same order + as listed in `code.co_varnames`). + * return_annotation : object + The annotation for the return type of the function if specified. + If the function has no annotation for its return type, this + attribute is not set. + * bind(*args, **kwargs) -> BoundArguments + Creates a mapping from positional and keyword arguments to + parameters. + * bind_partial(*args, **kwargs) -> BoundArguments + Creates a partial mapping from positional and keyword arguments + to parameters (simulating 'functools.partial' behavior.) + ''' + + __slots__ = ('_return_annotation', '_parameters') + + _parameter_cls = Parameter + _bound_arguments_cls = BoundArguments + + empty = _empty + + def __init__(self, parameters=None, return_annotation=_empty, + __validate_parameters__=True): + '''Constructs Signature from the given list of Parameter + objects and 'return_annotation'. All arguments are optional. + ''' + + if parameters is None: + params = OrderedDict() + else: + if __validate_parameters__: + params = OrderedDict() + top_kind = _POSITIONAL_ONLY + + for idx, param in enumerate(parameters): + kind = param.kind + if kind < top_kind: + msg = 'wrong parameter order: {0} before {1}' + msg = msg.format(top_kind, param.kind) + raise ValueError(msg) + else: + top_kind = kind + + name = param.name + if name is None: + name = str(idx) + param = param.replace(name=name) + + if name in params: + msg = 'duplicate parameter name: {0!r}'.format(name) + raise ValueError(msg) + params[name] = param + else: + params = OrderedDict(((param.name, param) + for param in parameters)) + + self._parameters = params + self._return_annotation = return_annotation + + @classmethod + def from_function(cls, func): + '''Constructs Signature for the given python function''' + + if not isinstance(func, types.FunctionType): + raise TypeError('{0!r} is not a Python function'.format(func)) + + Parameter = cls._parameter_cls + + # Parameter information. + func_code = func.__code__ + pos_count = func_code.co_argcount + arg_names = func_code.co_varnames + positional = tuple(arg_names[:pos_count]) + keyword_only_count = getattr(func_code, 'co_kwonlyargcount', 0) + keyword_only = arg_names[pos_count:(pos_count + keyword_only_count)] + annotations = getattr(func, '__annotations__', {}) + defaults = func.__defaults__ + kwdefaults = getattr(func, '__kwdefaults__', None) + + if defaults: + pos_default_count = len(defaults) + else: + pos_default_count = 0 + + parameters = [] + + # Non-keyword-only parameters w/o defaults. + non_default_count = pos_count - pos_default_count + for name in positional[:non_default_count]: + annotation = annotations.get(name, _empty) + parameters.append(Parameter(name, annotation=annotation, + kind=_POSITIONAL_OR_KEYWORD)) + + # ... w/ defaults. + for offset, name in enumerate(positional[non_default_count:]): + annotation = annotations.get(name, _empty) + parameters.append(Parameter(name, annotation=annotation, + kind=_POSITIONAL_OR_KEYWORD, + default=defaults[offset])) + + # *args + if func_code.co_flags & 0x04: + name = arg_names[pos_count + keyword_only_count] + annotation = annotations.get(name, _empty) + parameters.append(Parameter(name, annotation=annotation, + kind=_VAR_POSITIONAL)) + + # Keyword-only parameters. + for name in keyword_only: + default = _empty + if kwdefaults is not None: + default = kwdefaults.get(name, _empty) + + annotation = annotations.get(name, _empty) + parameters.append(Parameter(name, annotation=annotation, + kind=_KEYWORD_ONLY, + default=default)) + # **kwargs + if func_code.co_flags & 0x08: + index = pos_count + keyword_only_count + if func_code.co_flags & 0x04: + index += 1 + + name = arg_names[index] + annotation = annotations.get(name, _empty) + parameters.append(Parameter(name, annotation=annotation, + kind=_VAR_KEYWORD)) + + return cls(parameters, + return_annotation=annotations.get('return', _empty), + __validate_parameters__=False) + + @property + def parameters(self): + try: + return types.MappingProxyType(self._parameters) + except AttributeError: + return OrderedDict(self._parameters.items()) + + @property + def return_annotation(self): + return self._return_annotation + + def replace(self, parameters=_void, return_annotation=_void): + '''Creates a customized copy of the Signature. + Pass 'parameters' and/or 'return_annotation' arguments + to override them in the new copy. + ''' + + if parameters is _void: + parameters = self.parameters.values() + + if return_annotation is _void: + return_annotation = self._return_annotation + + return type(self)(parameters, + return_annotation=return_annotation) + + def __hash__(self): + msg = "unhashable type: '{0}'".format(self.__class__.__name__) + raise TypeError(msg) + + def __eq__(self, other): + if (not issubclass(type(other), Signature) or + self.return_annotation != other.return_annotation or + len(self.parameters) != len(other.parameters)): + return False + + other_positions = dict((param, idx) + for idx, param in enumerate(other.parameters.keys())) + + for idx, (param_name, param) in enumerate(self.parameters.items()): + if param.kind == _KEYWORD_ONLY: + try: + other_param = other.parameters[param_name] + except KeyError: + return False + else: + if param != other_param: + return False + else: + try: + other_idx = other_positions[param_name] + except KeyError: + return False + else: + if (idx != other_idx or + param != other.parameters[param_name]): + return False + + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def _bind(self, args, kwargs, partial=False): + '''Private method. Don't use directly.''' + + arguments = OrderedDict() + + parameters = iter(self.parameters.values()) + parameters_ex = () + arg_vals = iter(args) + + if partial: + # Support for binding arguments to 'functools.partial' objects. + # See 'functools.partial' case in 'signature()' implementation + # for details. + for param_name, param in self.parameters.items(): + if (param._partial_kwarg and param_name not in kwargs): + # Simulating 'functools.partial' behavior + kwargs[param_name] = param.default + + while True: + # Let's iterate through the positional arguments and corresponding + # parameters + try: + arg_val = next(arg_vals) + except StopIteration: + # No more positional arguments + try: + param = next(parameters) + except StopIteration: + # No more parameters. That's it. Just need to check that + # we have no `kwargs` after this while loop + break + else: + if param.kind == _VAR_POSITIONAL: + # That's OK, just empty *args. Let's start parsing + # kwargs + break + elif param.name in kwargs: + if param.kind == _POSITIONAL_ONLY: + msg = '{arg!r} parameter is positional only, ' \ + 'but was passed as a keyword' + msg = msg.format(arg=param.name) + raise TypeError(msg) + parameters_ex = (param,) + break + elif (param.kind == _VAR_KEYWORD or + param.default is not _empty): + # That's fine too - we have a default value for this + # parameter. So, lets start parsing `kwargs`, starting + # with the current parameter + parameters_ex = (param,) + break + else: + if partial: + parameters_ex = (param,) + break + else: + msg = '{arg!r} parameter lacking default value' + msg = msg.format(arg=param.name) + raise TypeError(msg) + else: + # We have a positional argument to process + try: + param = next(parameters) + except StopIteration: + raise TypeError('too many positional arguments') + else: + if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY): + # Looks like we have no parameter for this positional + # argument + raise TypeError('too many positional arguments') + + if param.kind == _VAR_POSITIONAL: + # We have an '*args'-like argument, let's fill it with + # all positional arguments we have left and move on to + # the next phase + values = [arg_val] + values.extend(arg_vals) + arguments[param.name] = tuple(values) + break + + if param.name in kwargs: + raise TypeError('multiple values for argument ' + '{arg!r}'.format(arg=param.name)) + + arguments[param.name] = arg_val + + # Now, we iterate through the remaining parameters to process + # keyword arguments + kwargs_param = None + for param in itertools.chain(parameters_ex, parameters): + if param.kind == _POSITIONAL_ONLY: + # This should never happen in case of a properly built + # Signature object (but let's have this check here + # to ensure correct behaviour just in case) + raise TypeError('{arg!r} parameter is positional only, ' + 'but was passed as a keyword'. \ + format(arg=param.name)) + + if param.kind == _VAR_KEYWORD: + # Memorize that we have a '**kwargs'-like parameter + kwargs_param = param + continue + + param_name = param.name + try: + arg_val = kwargs.pop(param_name) + except KeyError: + # We have no value for this parameter. It's fine though, + # if it has a default value, or it is an '*args'-like + # parameter, left alone by the processing of positional + # arguments. + if (not partial and param.kind != _VAR_POSITIONAL and + param.default is _empty): + raise TypeError('{arg!r} parameter lacking default value'. \ + format(arg=param_name)) + + else: + arguments[param_name] = arg_val + + if kwargs: + if kwargs_param is not None: + # Process our '**kwargs'-like parameter + arguments[kwargs_param.name] = kwargs + else: + raise TypeError('too many keyword arguments') + + return self._bound_arguments_cls(self, arguments) + + def bind(self, *args, **kwargs): + '''Get a BoundArguments object, that maps the passed `args` + and `kwargs` to the function's signature. Raises `TypeError` + if the passed arguments can not be bound. + ''' + return self._bind(args, kwargs) + + def bind_partial(self, *args, **kwargs): + '''Get a BoundArguments object, that partially maps the + passed `args` and `kwargs` to the function's signature. + Raises `TypeError` if the passed arguments can not be bound. + ''' + return self._bind(args, kwargs, partial=True) + + def __str__(self): + result = [] + render_kw_only_separator = True + for idx, param in enumerate(self.parameters.values()): + formatted = str(param) + + kind = param.kind + if kind == _VAR_POSITIONAL: + # OK, we have an '*args'-like parameter, so we won't need + # a '*' to separate keyword-only arguments + render_kw_only_separator = False + elif kind == _KEYWORD_ONLY and render_kw_only_separator: + # We have a keyword-only parameter to render and we haven't + # rendered an '*args'-like parameter before, so add a '*' + # separator to the parameters list ("foo(arg1, *, arg2)" case) + result.append('*') + # This condition should be only triggered once, so + # reset the flag + render_kw_only_separator = False + + result.append(formatted) + + rendered = '({0})'.format(', '.join(result)) + + if self.return_annotation is not _empty: + anno = formatannotation(self.return_annotation) + rendered += ' -> {0}'.format(anno) + + return rendered diff --git a/graphene/types/enum.py b/graphene/types/enum.py index c98fb5f6..a328cc93 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -7,10 +7,7 @@ from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType -try: - from enum import Enum as PyEnum -except ImportError: - from ..pyutils.enum import Enum as PyEnum +from ..pyutils.compat import Enum as PyEnum def eq_enum(self, other): diff --git a/graphene/utils/annotate.py b/graphene/utils/annotate.py new file mode 100644 index 00000000..fee8b57b --- /dev/null +++ b/graphene/utils/annotate.py @@ -0,0 +1,35 @@ +import six +from functools import wraps +from ..pyutils.compat import signature + + +def annotate(_func=None, **annotations): + if not six.PY2: + print( + "annotate is intended for use in Python 2 only, as you can use type annotations Python 3.\n" + "Read more in https://docs.python.org/3/library/typing.html" + ) + + if not _func: + def _func(f): + return annotate(f, **annotations) + return _func + + func_signature = signature(_func) + + # We make sure the annotations are valid + for key, value in annotations.items(): + assert key in func_signature.parameters, ( + 'The key {key} is not a function parameter in the function "{func_name}".' + ).format( + key=key, + func_name=_func.func_name + ) + + func_annotations = getattr(_func, '__annotations__', None) + if func_annotations is None: + _func.__annotations__ = annotations + else: + _func.__annotations__.update(annotations) + + return _func diff --git a/graphene/utils/tests/test_annotate.py b/graphene/utils/tests/test_annotate.py new file mode 100644 index 00000000..f213919a --- /dev/null +++ b/graphene/utils/tests/test_annotate.py @@ -0,0 +1,33 @@ +import pytest +from ..annotate import annotate + +def func(a, b, *c, **d): + pass + +annotations = { + 'a': int, + 'b': str, + 'c': list, + 'd': dict +} + +def func_with_annotations(a, b, *c, **d): + pass +func_with_annotations.__annotations__ = annotations + + +def test_annotate_with_no_params(): + annotated_func = annotate(func) + assert annotated_func.__annotations__ == {} + + +def test_annotate_with_params(): + annotated_func = annotate(**annotations)(func) + assert annotated_func.__annotations__ == annotations + + +def test_annotate_with_wront_params(): + with pytest.raises(Exception) as exc_info: + annotated_func = annotate(p=int)(func) + + assert str(exc_info.value) == 'The key p is not a function parameter in the function "func".' From d2f1024d813c0cfad3df2f4f4217fcfb7f1cb042 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 17:19:45 -0700 Subject: [PATCH 27/82] Added annotated resolver and context --- graphene/__init__.py | 4 ++ graphene/types/__init__.py | 4 ++ graphene/types/context.py | 4 ++ graphene/utils/annotate.py | 4 +- graphene/utils/annotated_resolver.py | 36 +++++++++++++++++ .../utils/tests/test_annotated_resolver.py | 39 +++++++++++++++++++ 6 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 graphene/types/context.py create mode 100644 graphene/utils/annotated_resolver.py create mode 100644 graphene/utils/tests/test_annotated_resolver.py diff --git a/graphene/__init__.py b/graphene/__init__.py index 08351d1a..8a4714d8 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -32,6 +32,8 @@ if not __SETUP__: Argument, Dynamic, Union, + Context, + ResolveInfo ) from .relay import ( Node, @@ -74,6 +76,8 @@ if not __SETUP__: 'ConnectionField', 'PageInfo', 'lazy_import', + 'Context', + 'ResolveInfo', # Deprecated 'AbstractType', diff --git a/graphene/types/__init__.py b/graphene/types/__init__.py index d1554be2..e623506f 100644 --- a/graphene/types/__init__.py +++ b/graphene/types/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa +from graphql.execution.base import ResolveInfo from .objecttype import ObjectType from .interface import Interface @@ -13,6 +14,7 @@ from .argument import Argument from .inputobjecttype import InputObjectType from .dynamic import Dynamic from .union import Union +from .context import Context # Deprecated from .abstracttype import AbstractType @@ -38,6 +40,8 @@ __all__ = [ 'Argument', 'Dynamic', 'Union', + 'Context', + 'ResolveInfo', # Deprecated 'AbstractType', diff --git a/graphene/types/context.py b/graphene/types/context.py new file mode 100644 index 00000000..4877ce13 --- /dev/null +++ b/graphene/types/context.py @@ -0,0 +1,4 @@ +class Context(object): + def __init__(self, **params): + for key, value in params.items(): + setattr(self,key, value) diff --git a/graphene/utils/annotate.py b/graphene/utils/annotate.py index fee8b57b..a02677ca 100644 --- a/graphene/utils/annotate.py +++ b/graphene/utils/annotate.py @@ -3,8 +3,8 @@ from functools import wraps from ..pyutils.compat import signature -def annotate(_func=None, **annotations): - if not six.PY2: +def annotate(_func=None, _trigger_warning=True, **annotations): + if not six.PY2 and _trigger_warning: print( "annotate is intended for use in Python 2 only, as you can use type annotations Python 3.\n" "Read more in https://docs.python.org/3/library/typing.html" diff --git a/graphene/utils/annotated_resolver.py b/graphene/utils/annotated_resolver.py new file mode 100644 index 00000000..277ac94d --- /dev/null +++ b/graphene/utils/annotated_resolver.py @@ -0,0 +1,36 @@ +from ..pyutils.compat import signature +from functools import wraps + +from ..types import Context, ResolveInfo + + +def annotated_resolver(func): + func_signature = signature(func) + + _context_var = None + _info_var = None + for key, parameter in func_signature.parameters.items(): + param_type = parameter.annotation + if param_type is Context: + _context_var = key + elif param_type is ResolveInfo: + _info_var = key + continue + + # We generate different functions as it will be faster + # than calculating the args on the fly when executing + # the function resolver. + if _context_var and _info_var: + def inner(root, args, context, info): + return func(root, **dict(args, **{_info_var: info, _context_var: context})) + elif _context_var: + def inner(root, args, context, info): + return func(root, **dict(args, **{_context_var: context})) + elif _info_var: + def inner(root, args, context, info): + return func(root, **dict(args, **{_info_var: info})) + else: + def inner(root, args, context, info): + return func(root, **args) + + return wraps(func)(inner) diff --git a/graphene/utils/tests/test_annotated_resolver.py b/graphene/utils/tests/test_annotated_resolver.py new file mode 100644 index 00000000..7bb5f3ba --- /dev/null +++ b/graphene/utils/tests/test_annotated_resolver.py @@ -0,0 +1,39 @@ +import pytest +from ..annotate import annotate +from ..annotated_resolver import annotated_resolver + +from ...types import Context, ResolveInfo + +@annotate +def func(root, **args): + return root, args, None, None + +@annotate(context=Context) +def func_with_context(root, context, **args): + return root, args, context, None + +@annotate(info=ResolveInfo) +def func_with_info(root, info, **args): + return root, args, None, info + +@annotate(context=Context, info=ResolveInfo) +def func_with_context_and_info(root, context, info, **args): + return root, args, context, info + +root = 1 +args = { + 'arg': 0 +} +context = 2 +info = 3 + +@pytest.mark.parametrize("func,expected", [ + (func, (1, {'arg': 0}, None, None)), + (func_with_context, (1, {'arg': 0}, 2, None)), + (func_with_info, (1, {'arg': 0}, None, 3)), + (func_with_context_and_info, (1, {'arg': 0}, 2, 3)), +]) +def test_annotated_resolver(func, expected): + resolver_func = annotated_resolver(func) + resolved = resolver_func(root, args, context, info) + assert resolved == expected From b892eee0ae34ba318a812917f39c83aa82b32330 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 17:22:12 -0700 Subject: [PATCH 28/82] Removed unnecessary files --- graphene/tests/issues/test_425.py | 74 +++++++++------------ graphene/tests/issues/test_425_graphene2.py | 42 ------------ graphene/utils/is_base_type.py | 3 - 3 files changed, 30 insertions(+), 89 deletions(-) delete mode 100644 graphene/tests/issues/test_425_graphene2.py delete mode 100644 graphene/utils/is_base_type.py diff --git a/graphene/tests/issues/test_425.py b/graphene/tests/issues/test_425.py index 42ee1178..7f92a75a 100644 --- a/graphene/tests/issues/test_425.py +++ b/graphene/tests/issues/test_425.py @@ -1,56 +1,42 @@ -# THIS IS THE OLD IMPLEMENTATION, for Graphene 1.0 -# Keep here as reference for upgrade. +# https://github.com/graphql-python/graphene/issues/425 +# Adapted for Graphene 2.0 -# # https://github.com/graphql-python/graphene/issues/425 -# import six - -# from graphene.utils.is_base_type import is_base_type - -# from graphene.types.objecttype import ObjectTypeMeta, ObjectType -# from graphene.types.options import Options - -# class SpecialObjectTypeMeta(ObjectTypeMeta): - -# @staticmethod -# def __new__(cls, name, bases, attrs): -# # Also ensure initialization is only performed for subclasses of -# # DjangoObjectType -# if not is_base_type(bases, SpecialObjectTypeMeta): -# return type.__new__(cls, name, bases, attrs) - -# options = Options( -# attrs.pop('Meta', None), -# other_attr='default', -# ) - -# cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) -# assert cls._meta is options -# return cls +from graphene.types.objecttype import ObjectType, ObjectTypeOptions -# class SpecialObjectType(six.with_metaclass(SpecialObjectTypeMeta, ObjectType)): -# pass +class SpecialOptions(ObjectTypeOptions): + other_attr = None -# def test_special_objecttype_could_be_subclassed(): -# class MyType(SpecialObjectType): -# class Meta: -# other_attr = 'yeah!' +class SpecialObjectType(ObjectType): -# assert MyType._meta.other_attr == 'yeah!' + @classmethod + def __init_subclass_with_meta__(cls, other_attr='default', **options): + _meta = SpecialOptions(cls) + _meta.other_attr = other_attr + super(SpecialObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) -# def test_special_objecttype_could_be_subclassed_default(): -# class MyType(SpecialObjectType): -# pass +def test_special_objecttype_could_be_subclassed(): + class MyType(SpecialObjectType): -# assert MyType._meta.other_attr == 'default' + class Meta: + other_attr = 'yeah!' + + assert MyType._meta.other_attr == 'yeah!' -# def test_special_objecttype_inherit_meta_options(): -# class MyType(SpecialObjectType): -# pass +def test_special_objecttype_could_be_subclassed_default(): + class MyType(SpecialObjectType): + pass -# assert MyType._meta.name == 'MyType' -# assert MyType._meta.default_resolver == None -# assert MyType._meta.interfaces == () + assert MyType._meta.other_attr == 'default' + + +def test_special_objecttype_inherit_meta_options(): + class MyType(SpecialObjectType): + pass + + assert MyType._meta.name == 'MyType' + assert MyType._meta.default_resolver is None + assert MyType._meta.interfaces == () diff --git a/graphene/tests/issues/test_425_graphene2.py b/graphene/tests/issues/test_425_graphene2.py deleted file mode 100644 index 7f92a75a..00000000 --- a/graphene/tests/issues/test_425_graphene2.py +++ /dev/null @@ -1,42 +0,0 @@ -# https://github.com/graphql-python/graphene/issues/425 -# Adapted for Graphene 2.0 - -from graphene.types.objecttype import ObjectType, ObjectTypeOptions - - -class SpecialOptions(ObjectTypeOptions): - other_attr = None - - -class SpecialObjectType(ObjectType): - - @classmethod - def __init_subclass_with_meta__(cls, other_attr='default', **options): - _meta = SpecialOptions(cls) - _meta.other_attr = other_attr - super(SpecialObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) - - -def test_special_objecttype_could_be_subclassed(): - class MyType(SpecialObjectType): - - class Meta: - other_attr = 'yeah!' - - assert MyType._meta.other_attr == 'yeah!' - - -def test_special_objecttype_could_be_subclassed_default(): - class MyType(SpecialObjectType): - pass - - assert MyType._meta.other_attr == 'default' - - -def test_special_objecttype_inherit_meta_options(): - class MyType(SpecialObjectType): - pass - - assert MyType._meta.name == 'MyType' - assert MyType._meta.default_resolver is None - assert MyType._meta.interfaces == () diff --git a/graphene/utils/is_base_type.py b/graphene/utils/is_base_type.py deleted file mode 100644 index 8dfdfb70..00000000 --- a/graphene/utils/is_base_type.py +++ /dev/null @@ -1,3 +0,0 @@ - -def is_base_type(bases, _type): - return any(b for b in bases if isinstance(b, _type)) From b185f4cae7a258ec189fb9b8f096b332f89004c7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 17:36:20 -0700 Subject: [PATCH 29/82] Improved mutation with thenable check --- graphene/relay/mutation.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index ea9522bf..1f8f80e5 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -1,7 +1,7 @@ import re from collections import OrderedDict -from promise import Promise +from promise import Promise, is_thenable from ..types import Field, InputObjectType, String from ..types.mutation import Mutation @@ -60,6 +60,9 @@ class ClientIDMutation(Mutation): ('Cannot set client_mutation_id in the payload object {}' ).format(repr(payload))) return payload - - return Promise.resolve( - cls.mutate_and_get_payload(input, context, info)).then(on_resolve) + + result = cls.mutate_and_get_payload(input, context, info) + if is_thenable(result): + return Promise.resolve(result).then(on_resolve) + + return on_resolve(result) From fb4b4df5003363c82052c870fba5fd529bf1ae0c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 17:55:26 -0700 Subject: [PATCH 30/82] Added auto resolver function --- graphene/utils/annotate.py | 3 +- graphene/utils/auto_resolver.py | 12 +++++++ ...solver.py => resolver_from_annotations.py} | 2 +- graphene/utils/tests/test_auto_resolver.py | 36 +++++++++++++++++++ ...r.py => test_resolver_from_annotations.py} | 11 ++++-- 5 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 graphene/utils/auto_resolver.py rename graphene/utils/{annotated_resolver.py => resolver_from_annotations.py} (96%) create mode 100644 graphene/utils/tests/test_auto_resolver.py rename graphene/utils/tests/{test_annotated_resolver.py => test_resolver_from_annotations.py} (83%) diff --git a/graphene/utils/annotate.py b/graphene/utils/annotate.py index a02677ca..c58c7544 100644 --- a/graphene/utils/annotate.py +++ b/graphene/utils/annotate.py @@ -31,5 +31,6 @@ def annotate(_func=None, _trigger_warning=True, **annotations): _func.__annotations__ = annotations else: _func.__annotations__.update(annotations) - + + _func._is_annotated = True return _func diff --git a/graphene/utils/auto_resolver.py b/graphene/utils/auto_resolver.py new file mode 100644 index 00000000..6308271d --- /dev/null +++ b/graphene/utils/auto_resolver.py @@ -0,0 +1,12 @@ +from .resolver_from_annotations import resolver_from_annotations + + +def auto_resolver(func=None): + annotations = getattr(func, '__annotations__', {}) + is_annotated = getattr(func, '_is_annotated', False) + + if annotations or is_annotated: + # Is a Graphene 2.0 resolver function + return resolver_from_annotations(func) + else: + return func diff --git a/graphene/utils/annotated_resolver.py b/graphene/utils/resolver_from_annotations.py similarity index 96% rename from graphene/utils/annotated_resolver.py rename to graphene/utils/resolver_from_annotations.py index 277ac94d..d1c6fb94 100644 --- a/graphene/utils/annotated_resolver.py +++ b/graphene/utils/resolver_from_annotations.py @@ -4,7 +4,7 @@ from functools import wraps from ..types import Context, ResolveInfo -def annotated_resolver(func): +def resolver_from_annotations(func): func_signature = signature(func) _context_var = None diff --git a/graphene/utils/tests/test_auto_resolver.py b/graphene/utils/tests/test_auto_resolver.py new file mode 100644 index 00000000..93b7a991 --- /dev/null +++ b/graphene/utils/tests/test_auto_resolver.py @@ -0,0 +1,36 @@ +import pytest +from ..annotate import annotate +from ..auto_resolver import auto_resolver + +from ...types import Context, ResolveInfo + + +def resolver(root, args, context, info): + return root, args, context, info + + +@annotate +def resolver_annotated(root, **args): + return root, args, None, None + + +@annotate(context=Context, info=ResolveInfo) +def resolver_with_context_and_info(root, context, info, **args): + return root, args, context, info + + +def test_auto_resolver_non_annotated(): + decorated_resolver = auto_resolver(resolver) + # We make sure the function is not wrapped + assert decorated_resolver == resolver + assert decorated_resolver(1, {}, 2, 3) == (1, {}, 2, 3) + + +def test_auto_resolver_annotated(): + decorated_resolver = auto_resolver(resolver_annotated) + assert decorated_resolver(1, {}, 2, 3) == (1, {}, None, None) + + +def test_auto_resolver_annotated_with_context_and_info(): + decorated_resolver = auto_resolver(resolver_with_context_and_info) + assert decorated_resolver(1, {}, 2, 3) == (1, {}, 2, 3) diff --git a/graphene/utils/tests/test_annotated_resolver.py b/graphene/utils/tests/test_resolver_from_annotations.py similarity index 83% rename from graphene/utils/tests/test_annotated_resolver.py rename to graphene/utils/tests/test_resolver_from_annotations.py index 7bb5f3ba..226b387c 100644 --- a/graphene/utils/tests/test_annotated_resolver.py +++ b/graphene/utils/tests/test_resolver_from_annotations.py @@ -1,21 +1,25 @@ import pytest from ..annotate import annotate -from ..annotated_resolver import annotated_resolver +from ..resolver_from_annotations import resolver_from_annotations from ...types import Context, ResolveInfo + @annotate def func(root, **args): return root, args, None, None + @annotate(context=Context) def func_with_context(root, context, **args): return root, args, context, None + @annotate(info=ResolveInfo) def func_with_info(root, info, **args): return root, args, None, info + @annotate(context=Context, info=ResolveInfo) def func_with_context_and_info(root, context, info, **args): return root, args, context, info @@ -27,13 +31,14 @@ args = { context = 2 info = 3 + @pytest.mark.parametrize("func,expected", [ (func, (1, {'arg': 0}, None, None)), (func_with_context, (1, {'arg': 0}, 2, None)), (func_with_info, (1, {'arg': 0}, None, 3)), (func_with_context_and_info, (1, {'arg': 0}, 2, 3)), ]) -def test_annotated_resolver(func, expected): - resolver_func = annotated_resolver(func) +def test_resolver_from_annotations(func, expected): + resolver_func = resolver_from_annotations(func) resolved = resolver_func(root, args, context, info) assert resolved == expected From 6ae9717415a867cf06e4c8cc2acae3be21822455 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 18:57:17 -0700 Subject: [PATCH 31/82] Improved automatic resolver args from annotations --- UPGRADE-v2.0.md | 61 ++++++++++++++ examples/starwars/schema.py | 8 +- examples/starwars_relay/schema.py | 8 +- graphene/__init__.py | 2 + graphene/types/__init__.py | 2 +- graphene/types/field.py | 3 +- graphene/types/tests/test_query.py | 43 +++++++++- graphene/utils/annotate.py | 6 +- graphene/utils/auto_resolver.py | 4 +- graphene/utils/deprecated.py | 80 +++++++++++++++++++ graphene/utils/resolve_only_args.py | 19 ++++- graphene/utils/resolver_from_annotations.py | 16 +++- .../utils/tests/test_resolve_only_args.py | 7 +- setup.py | 1 + 14 files changed, 235 insertions(+), 25 deletions(-) create mode 100644 graphene/utils/deprecated.py diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index b03c9732..e966a5db 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -80,3 +80,64 @@ class Query(ObjectType): user_connection = relay.ConnectionField(UserConnection) ``` + +## New Features + +### InputObjectType + +`InputObjectType`s are now a first class citizen in Graphene. +That means, if you are using a custom InputObjectType, you can access +it's fields via `getattr` (`my_input.myattr`) when resolving, instead of +the classic way `my_input['myattr']`. + +And also use custom defined properties on your input class. + +Example. Before: + +```python +class User(ObjectType): + name = String() + +class UserInput(InputObjectType): + id = ID() + +def is_user_id(id): + return id.startswith('userid_') + +class Query(ObjectType): + user = graphene.Field(User, id=UserInput()) + + @resolve_only_args + def resolve_user(self, input): + user_id = input.get('id') + if is_user_id(user_id): + return get_user(user_id) +``` + +With 2.0: + +```python +class User(ObjectType): + id = ID() + +class UserInput(InputObjectType): + id = ID() + + @property + def is_user_id(self): + return id.startswith('userid_') + +class Query(ObjectType): + user = graphene.Field(User, id=UserInput()) + + @annotate(input=UserInput) + def resolve_user(self, input): + if input.is_user_id: + return get_user(input.id) + + # You can also do in Python 3: + def resolve_user(self, input: UserInput): + if input.is_user_id: + return get_user(input.id) + +``` diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py index 939f7253..b3c1a63b 100644 --- a/examples/starwars/schema.py +++ b/examples/starwars/schema.py @@ -1,5 +1,5 @@ import graphene -from graphene import resolve_only_args +from graphene import annotate from .data import get_character, get_droid, get_hero, get_human @@ -46,15 +46,15 @@ class Query(graphene.ObjectType): id=graphene.String() ) - @resolve_only_args + @annotate(episode=Episode) def resolve_hero(self, episode=None): return get_hero(episode) - @resolve_only_args + @annotate(id=str) def resolve_human(self, id): return get_human(id) - @resolve_only_args + @annotate(id=str) def resolve_droid(self, id): return get_droid(id) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 103576c4..69a8dba7 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -1,5 +1,5 @@ import graphene -from graphene import relay, resolve_only_args +from graphene import annotate, relay, annotate from .data import create_ship, get_empire, get_faction, get_rebels, get_ship @@ -32,7 +32,7 @@ class Faction(graphene.ObjectType): name = graphene.String(description='The name of the faction.') ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.') - @resolve_only_args + @annotate def resolve_ships(self, **args): # Transform the instance ship_ids into real instances return [get_ship(ship_id) for ship_id in self.ships] @@ -65,11 +65,11 @@ class Query(graphene.ObjectType): empire = graphene.Field(Faction) node = relay.Node.Field() - @resolve_only_args + @annotate def resolve_rebels(self): return get_rebels() - @resolve_only_args + @annotate def resolve_empire(self): return get_empire() diff --git a/graphene/__init__.py b/graphene/__init__.py index 8a4714d8..98dfe058 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -46,6 +46,7 @@ if not __SETUP__: ) from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import + from .utils.annotate import annotate __all__ = [ 'ObjectType', @@ -76,6 +77,7 @@ if not __SETUP__: 'ConnectionField', 'PageInfo', 'lazy_import', + 'annotate', 'Context', 'ResolveInfo', diff --git a/graphene/types/__init__.py b/graphene/types/__init__.py index e623506f..ee5d4981 100644 --- a/graphene/types/__init__.py +++ b/graphene/types/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -from graphql.execution.base import ResolveInfo +from graphql import ResolveInfo from .objecttype import ObjectType from .interface import Interface diff --git a/graphene/types/field.py b/graphene/types/field.py index 9e699a12..8de94bed 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -7,6 +7,7 @@ from .mountedtype import MountedType from .structures import NonNull from .unmountedtype import UnmountedType from .utils import get_type +from ..utils.auto_resolver import auto_resolver base_type = type @@ -63,4 +64,4 @@ class Field(MountedType): return get_type(self._type) def get_resolver(self, parent_resolver): - return self.resolver or parent_resolver + return auto_resolver(self.resolver or parent_resolver) diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 254570e1..ebc78c1d 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -1,7 +1,7 @@ import json from functools import partial -from graphql import GraphQLError, Source, execute, parse +from graphql import GraphQLError, Source, execute, parse, ResolveInfo from ..dynamic import Dynamic from ..field import Field @@ -13,6 +13,8 @@ from ..scalars import Int, String from ..schema import Schema from ..structures import List from ..union import Union +from ..context import Context +from ...utils.annotate import annotate def test_query(): @@ -406,3 +408,42 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark result = benchmark(big_list_query) assert not result.errors assert result.data == {'allContainers': [{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]} + + +def test_query_annotated_resolvers(): + import json + + context = Context(key="context") + + class Query(ObjectType): + annotated = String(id=String()) + context = String() + info = String() + + @annotate + def resolve_annotated(self, id): + return "{}-{}".format(self, id) + + @annotate(context=Context) + def resolve_context(self, context): + assert isinstance(context, Context) + return "{}-{}".format(self, context.key) + + @annotate(info=ResolveInfo) + def resolve_info(self, info): + assert isinstance(info, ResolveInfo) + return "{}-{}".format(self, info.field_name) + + test_schema = Schema(Query) + + result = test_schema.execute('{ annotated(id:"self") }', "base") + assert not result.errors + assert result.data == {'annotated': 'base-self'} + + result = test_schema.execute('{ context }', "base", context_value=context) + assert not result.errors + assert result.data == {'context': 'base-context'} + + result = test_schema.execute('{ info }', "base") + assert not result.errors + assert result.data == {'info': 'base-info'} diff --git a/graphene/utils/annotate.py b/graphene/utils/annotate.py index c58c7544..5298e40b 100644 --- a/graphene/utils/annotate.py +++ b/graphene/utils/annotate.py @@ -2,14 +2,16 @@ import six from functools import wraps from ..pyutils.compat import signature +from .deprecated import warn_deprecation + def annotate(_func=None, _trigger_warning=True, **annotations): if not six.PY2 and _trigger_warning: - print( + warn_deprecation( "annotate is intended for use in Python 2 only, as you can use type annotations Python 3.\n" "Read more in https://docs.python.org/3/library/typing.html" ) - + if not _func: def _func(f): return annotate(f, **annotations) diff --git a/graphene/utils/auto_resolver.py b/graphene/utils/auto_resolver.py index 6308271d..dd551731 100644 --- a/graphene/utils/auto_resolver.py +++ b/graphene/utils/auto_resolver.py @@ -1,11 +1,11 @@ -from .resolver_from_annotations import resolver_from_annotations +from .resolver_from_annotations import resolver_from_annotations, is_wrapped_from_annotations def auto_resolver(func=None): annotations = getattr(func, '__annotations__', {}) is_annotated = getattr(func, '_is_annotated', False) - if annotations or is_annotated: + if (annotations or is_annotated) and not is_wrapped_from_annotations(func): # Is a Graphene 2.0 resolver function return resolver_from_annotations(func) else: diff --git a/graphene/utils/deprecated.py b/graphene/utils/deprecated.py new file mode 100644 index 00000000..f70b2e7d --- /dev/null +++ b/graphene/utils/deprecated.py @@ -0,0 +1,80 @@ +import functools +import inspect +import warnings + +string_types = (type(b''), type(u'')) + + +def warn_deprecation(text): + warnings.simplefilter('always', DeprecationWarning) + warnings.warn( + text, + category=DeprecationWarning, + stacklevel=2 + ) + warnings.simplefilter('default', DeprecationWarning) + + +def deprecated(reason): + """ + This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + """ + + if isinstance(reason, string_types): + + # The @deprecated is used with a 'reason'. + # + # .. code-block:: python + # + # @deprecated("please, use another function") + # def old_function(x, y): + # pass + + def decorator(func1): + + if inspect.isclass(func1): + fmt1 = "Call to deprecated class {name} ({reason})." + else: + fmt1 = "Call to deprecated function {name} ({reason})." + + @functools.wraps(func1) + def new_func1(*args, **kwargs): + warn_deprecation( + fmt1.format(name=func1.__name__, reason=reason), + ) + return func1(*args, **kwargs) + + return new_func1 + + return decorator + + elif inspect.isclass(reason) or inspect.isfunction(reason): + + # The @deprecated is used without any 'reason'. + # + # .. code-block:: python + # + # @deprecated + # def old_function(x, y): + # pass + + func2 = reason + + if inspect.isclass(func2): + fmt2 = "Call to deprecated class {name}." + else: + fmt2 = "Call to deprecated function {name}." + + @functools.wraps(func2) + def new_func2(*args, **kwargs): + warn_deprecation( + fmt2.format(name=func2.__name__), + ) + return func2(*args, **kwargs) + + return new_func2 + + else: + raise TypeError(repr(type(reason))) diff --git a/graphene/utils/resolve_only_args.py b/graphene/utils/resolve_only_args.py index 93a9ab7e..93a5a0fb 100644 --- a/graphene/utils/resolve_only_args.py +++ b/graphene/utils/resolve_only_args.py @@ -1,8 +1,19 @@ +from six import PY2 from functools import wraps +from .annotate import annotate +from .deprecated import deprecated +if PY2: + deprecation_reason = ( + 'The decorator @resolve_only_args is deprecated.\n' + 'Please use @annotate instead.' + ) +else: + deprecation_reason = ( + 'The decorator @resolve_only_args is deprecated.\n' + 'Please use Python 3 type annotations instead. Read more: https://docs.python.org/3/library/typing.html' + ) +@deprecated(deprecation_reason) def resolve_only_args(func): - @wraps(func) - def inner(root, args, context, info): - return func(root, **args) - return inner + return annotate(func) diff --git a/graphene/utils/resolver_from_annotations.py b/graphene/utils/resolver_from_annotations.py index d1c6fb94..49aee652 100644 --- a/graphene/utils/resolver_from_annotations.py +++ b/graphene/utils/resolver_from_annotations.py @@ -1,10 +1,15 @@ from ..pyutils.compat import signature from functools import wraps -from ..types import Context, ResolveInfo - def resolver_from_annotations(func): + from ..types import Context, ResolveInfo + + _is_wrapped_from_annotations = is_wrapped_from_annotations(func) + assert not _is_wrapped_from_annotations, "The function {func_name} is already wrapped.".format( + func_name=func.func_name + ) + func_signature = signature(func) _context_var = None @@ -32,5 +37,10 @@ def resolver_from_annotations(func): else: def inner(root, args, context, info): return func(root, **args) - + + inner._is_wrapped_from_annotations = True return wraps(func)(inner) + + +def is_wrapped_from_annotations(func): + return getattr(func, '_is_wrapped_from_annotations', False) diff --git a/graphene/utils/tests/test_resolve_only_args.py b/graphene/utils/tests/test_resolve_only_args.py index 8c3ec248..d0d06e51 100644 --- a/graphene/utils/tests/test_resolve_only_args.py +++ b/graphene/utils/tests/test_resolve_only_args.py @@ -1,12 +1,13 @@ from ..resolve_only_args import resolve_only_args +from .. import deprecated -def test_resolve_only_args(): - +def test_resolve_only_args(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') def resolver(*args, **kwargs): return kwargs my_data = {'one': 1, 'two': 2} wrapped = resolve_only_args(resolver) - assert wrapped(None, my_data, None, None) == my_data + deprecated.warn_deprecation.assert_called_once() diff --git a/setup.py b/setup.py index 3405e0c5..f01f0d9b 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ tests_require = [ 'pytest>=2.7.2', 'pytest-benchmark', 'pytest-cov', + 'pytest-mock', 'snapshottest', 'coveralls', 'six', From 8bac3dc9e5196e313c05754ebd7f6e14219df159 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 19:02:41 -0700 Subject: [PATCH 32/82] Use latest graphql-core and promise lib --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f01f0d9b..1551cb59 100644 --- a/setup.py +++ b/setup.py @@ -83,9 +83,11 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.1', + # 'graphql-core>=1.1', + 'https://github.com/graphql-python/graphql-core/archive/master.zip', 'graphql-relay>=0.4.5', - 'promise>=2.0', + #'promise>=2.0', + 'https://github.com/syrusakbary/promise/archive/master.zip', ], tests_require=tests_require, extras_require={ From 85d31458624f809d38ee3c79c21dc7d55debd326 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 19:13:33 -0700 Subject: [PATCH 33/82] Improved abstract type --- graphene/types/abstracttype.py | 13 +++++--- graphene/types/tests/test_abstracttype.py | 40 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 graphene/types/tests/test_abstracttype.py diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index efc756a1..3e283493 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -1,9 +1,12 @@ -from ..pyutils.init_subclass import InitSubclassMeta +from ..utils.subclass_with_meta import SubclassWithMeta +from ..utils.deprecated import warn_deprecation -class AbstractType(object): - __metaclass__ = InitSubclassMeta +class AbstractType(SubclassWithMeta): def __init_subclass__(cls, *args, **kwargs): - print("Abstract type is deprecated") - # super(AbstractType, cls).__init_subclass__(*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" + ) + super(AbstractType, cls).__init_subclass__(*args, **kwargs) diff --git a/graphene/types/tests/test_abstracttype.py b/graphene/types/tests/test_abstracttype.py new file mode 100644 index 00000000..44fc570d --- /dev/null +++ b/graphene/types/tests/test_abstracttype.py @@ -0,0 +1,40 @@ +import pytest + +from ..objecttype import ObjectType +from ..unmountedtype import UnmountedType +from ..abstracttype import AbstractType +from ..field import Field +from ...utils import deprecated + + +class MyType(ObjectType): + pass + + +class MyScalar(UnmountedType): + + def get_type(self): + return MyType + + +def test_abstract_objecttype_warn_deprecation(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') + + class MyAbstractType(AbstractType): + field1 = MyScalar() + + deprecated.warn_deprecation.assert_called_once() + + +def test_generate_objecttype_inherit_abstracttype(): + class MyAbstractType(AbstractType): + field1 = MyScalar() + + class MyObjectType(ObjectType, MyAbstractType): + field2 = MyScalar() + + assert MyObjectType._meta.description is None + 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] From 287a7a814d68e56c7a70b7662cef8ca9ed8e75e8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 19:15:51 -0700 Subject: [PATCH 34/82] Added package test requirements into travis excluding setup --- .travis.yml | 4 ++++ setup.py | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d87939e0..cd8d9150 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,10 @@ before_install: install: - | if [ "$TEST_TYPE" = build ]; then + # For testing + pip install https://github.com/graphql-python/graphql-core/archive/master.zip + pip install https://github.com/syrusakbary/promise/archive/master.zip + pip install -e .[test] python setup.py develop elif [ "$TEST_TYPE" = lint ]; then diff --git a/setup.py b/setup.py index 1551cb59..f01f0d9b 100644 --- a/setup.py +++ b/setup.py @@ -83,11 +83,9 @@ setup( install_requires=[ 'six>=1.10.0', - # 'graphql-core>=1.1', - 'https://github.com/graphql-python/graphql-core/archive/master.zip', + 'graphql-core>=1.1', 'graphql-relay>=0.4.5', - #'promise>=2.0', - 'https://github.com/syrusakbary/promise/archive/master.zip', + 'promise>=2.0', ], tests_require=tests_require, extras_require={ From 8c3a933bd9c5f72c175cf7d4b701ed0fd9127bda Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 19:18:29 -0700 Subject: [PATCH 35/82] Fixed test 425 url --- UPGRADE-v2.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index e966a5db..b09dba45 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -3,7 +3,7 @@ * `ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations have been quite simplified, without the need of define a explicit Metaclass. The metaclasses threfore are now deleted as are no longer necessary, if your code was depending - on this internal metaclass for creating custom attrs, please see an [example of how to do it now in 2.0](https://github.com/graphql-python/graphene/blob/master/graphene/tests/issues/test_425_graphene2.py). + on this internal metaclass for creating custom attrs, please see an [example of how to do it now in 2.0](https://github.com/graphql-python/graphene/blob/2.0/graphene/tests/issues/test_425.py). ## Deprecations From f8561fa5c448c15927e04f811852bd8f59c2f182 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 19:20:19 -0700 Subject: [PATCH 36/82] Fixed travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index cd8d9150..88574e69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,8 +23,8 @@ install: - | if [ "$TEST_TYPE" = build ]; then # For testing - pip install https://github.com/graphql-python/graphql-core/archive/master.zip - pip install https://github.com/syrusakbary/promise/archive/master.zip + pip install https://github.com/graphql-python/graphql-core/archive/master.zip --upgrade + pip install https://github.com/syrusakbary/promise/archive/master.zip --upgrade pip install -e .[test] python setup.py develop From 6c4f4624e36926e235573f53334cad90eb9931d4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 19:30:13 -0700 Subject: [PATCH 37/82] Fixed Python 2/3 issues and flake syntax issues --- graphene/pyutils/compat.py | 8 ++++++++ graphene/relay/mutation.py | 4 ++-- graphene/types/context.py | 2 +- graphene/types/inputobjecttype.py | 2 +- graphene/types/tests/test_abstracttype.py | 6 +++--- graphene/utils/annotate.py | 7 +++---- graphene/utils/auto_resolver.py | 2 +- graphene/utils/resolve_only_args.py | 5 +++-- graphene/utils/resolver_from_annotations.py | 2 +- 9 files changed, 23 insertions(+), 15 deletions(-) diff --git a/graphene/pyutils/compat.py b/graphene/pyutils/compat.py index 68734a99..86452081 100644 --- a/graphene/pyutils/compat.py +++ b/graphene/pyutils/compat.py @@ -1,4 +1,5 @@ from __future__ import absolute_import +import six try: from enum import Enum @@ -9,3 +10,10 @@ try: from inspect import signature except ImportError: from .signature import signature + +if six.PY2: + def func_name(func): + return func.func_name +else: + def func_name(func): + return func.__name__ diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 1f8f80e5..fb57c275 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -60,9 +60,9 @@ class ClientIDMutation(Mutation): ('Cannot set client_mutation_id in the payload object {}' ).format(repr(payload))) return payload - + result = cls.mutate_and_get_payload(input, context, info) if is_thenable(result): return Promise.resolve(result).then(on_resolve) - + return on_resolve(result) diff --git a/graphene/types/context.py b/graphene/types/context.py index 4877ce13..bac2073c 100644 --- a/graphene/types/context.py +++ b/graphene/types/context.py @@ -1,4 +1,4 @@ class Context(object): def __init__(self, **params): for key, value in params.items(): - setattr(self,key, value) + setattr(self, key, value) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 19bb7c74..cf66065f 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -37,7 +37,7 @@ class InputObjectType(dict, UnmountedType, BaseType): dict.__init__(self, *args, **kwargs) for key, value in self.items(): setattr(self, key, value) - + @classmethod def create_container(cls, data): return cls(data, _as_container=True) diff --git a/graphene/types/tests/test_abstracttype.py b/graphene/types/tests/test_abstracttype.py index 44fc570d..b1bcaa3f 100644 --- a/graphene/types/tests/test_abstracttype.py +++ b/graphene/types/tests/test_abstracttype.py @@ -3,8 +3,8 @@ import pytest from ..objecttype import ObjectType from ..unmountedtype import UnmountedType from ..abstracttype import AbstractType +from .. import abstracttype from ..field import Field -from ...utils import deprecated class MyType(ObjectType): @@ -18,12 +18,12 @@ class MyScalar(UnmountedType): def test_abstract_objecttype_warn_deprecation(mocker): - mocker.patch.object(deprecated, 'warn_deprecation') + mocker.patch.object(abstracttype, 'warn_deprecation') class MyAbstractType(AbstractType): field1 = MyScalar() - deprecated.warn_deprecation.assert_called_once() + abstracttype.warn_deprecation.assert_called_once() def test_generate_objecttype_inherit_abstracttype(): diff --git a/graphene/utils/annotate.py b/graphene/utils/annotate.py index 5298e40b..b55c3d6c 100644 --- a/graphene/utils/annotate.py +++ b/graphene/utils/annotate.py @@ -1,6 +1,5 @@ import six -from functools import wraps -from ..pyutils.compat import signature +from ..pyutils.compat import signature, func_name from .deprecated import warn_deprecation @@ -25,7 +24,7 @@ def annotate(_func=None, _trigger_warning=True, **annotations): 'The key {key} is not a function parameter in the function "{func_name}".' ).format( key=key, - func_name=_func.func_name + func_name=func_name(_func) ) func_annotations = getattr(_func, '__annotations__', None) @@ -33,6 +32,6 @@ def annotate(_func=None, _trigger_warning=True, **annotations): _func.__annotations__ = annotations else: _func.__annotations__.update(annotations) - + _func._is_annotated = True return _func diff --git a/graphene/utils/auto_resolver.py b/graphene/utils/auto_resolver.py index dd551731..633c1a95 100644 --- a/graphene/utils/auto_resolver.py +++ b/graphene/utils/auto_resolver.py @@ -4,7 +4,7 @@ from .resolver_from_annotations import resolver_from_annotations, is_wrapped_fro def auto_resolver(func=None): annotations = getattr(func, '__annotations__', {}) is_annotated = getattr(func, '_is_annotated', False) - + if (annotations or is_annotated) and not is_wrapped_from_annotations(func): # Is a Graphene 2.0 resolver function return resolver_from_annotations(func) diff --git a/graphene/utils/resolve_only_args.py b/graphene/utils/resolve_only_args.py index 93a5a0fb..e30f1f03 100644 --- a/graphene/utils/resolve_only_args.py +++ b/graphene/utils/resolve_only_args.py @@ -1,5 +1,4 @@ from six import PY2 -from functools import wraps from .annotate import annotate from .deprecated import deprecated @@ -11,9 +10,11 @@ if PY2: else: deprecation_reason = ( 'The decorator @resolve_only_args is deprecated.\n' - 'Please use Python 3 type annotations instead. Read more: https://docs.python.org/3/library/typing.html' + 'Please use Python 3 type annotations instead. Read more: ' + 'https://docs.python.org/3/library/typing.html' ) + @deprecated(deprecation_reason) def resolve_only_args(func): return annotate(func) diff --git a/graphene/utils/resolver_from_annotations.py b/graphene/utils/resolver_from_annotations.py index 49aee652..79fb99de 100644 --- a/graphene/utils/resolver_from_annotations.py +++ b/graphene/utils/resolver_from_annotations.py @@ -37,7 +37,7 @@ def resolver_from_annotations(func): else: def inner(root, args, context, info): return func(root, **args) - + inner._is_wrapped_from_annotations = True return wraps(func)(inner) From 907a3d9915bb88a7999460e1608fc550c707e91a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 19:43:58 -0700 Subject: [PATCH 38/82] Fixed Python 3.5 issues --- graphene/types/tests/test_abstracttype.py | 2 +- graphene/types/tests/test_query.py | 6 +++--- graphene/utils/resolve_only_args.py | 2 -- graphene/utils/tests/test_annotate.py | 6 +++--- graphene/utils/tests/test_resolve_only_args.py | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/graphene/types/tests/test_abstracttype.py b/graphene/types/tests/test_abstracttype.py index b1bcaa3f..6484deb8 100644 --- a/graphene/types/tests/test_abstracttype.py +++ b/graphene/types/tests/test_abstracttype.py @@ -23,7 +23,7 @@ def test_abstract_objecttype_warn_deprecation(mocker): class MyAbstractType(AbstractType): field1 = MyScalar() - abstracttype.warn_deprecation.assert_called_once() + assert abstracttype.warn_deprecation.called def test_generate_objecttype_inherit_abstracttype(): diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index ebc78c1d..4adcedf9 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -420,16 +420,16 @@ def test_query_annotated_resolvers(): context = String() info = String() - @annotate + @annotate(_trigger_warning=False) def resolve_annotated(self, id): return "{}-{}".format(self, id) - @annotate(context=Context) + @annotate(context=Context, _trigger_warning=False) def resolve_context(self, context): assert isinstance(context, Context) return "{}-{}".format(self, context.key) - @annotate(info=ResolveInfo) + @annotate(info=ResolveInfo, _trigger_warning=False) def resolve_info(self, info): assert isinstance(info, ResolveInfo) return "{}-{}".format(self, info.field_name) diff --git a/graphene/utils/resolve_only_args.py b/graphene/utils/resolve_only_args.py index e30f1f03..0856ffcc 100644 --- a/graphene/utils/resolve_only_args.py +++ b/graphene/utils/resolve_only_args.py @@ -4,12 +4,10 @@ from .deprecated import deprecated if PY2: deprecation_reason = ( - 'The decorator @resolve_only_args is deprecated.\n' 'Please use @annotate instead.' ) else: deprecation_reason = ( - 'The decorator @resolve_only_args is deprecated.\n' 'Please use Python 3 type annotations instead. Read more: ' 'https://docs.python.org/3/library/typing.html' ) diff --git a/graphene/utils/tests/test_annotate.py b/graphene/utils/tests/test_annotate.py index f213919a..760a8bf5 100644 --- a/graphene/utils/tests/test_annotate.py +++ b/graphene/utils/tests/test_annotate.py @@ -17,17 +17,17 @@ func_with_annotations.__annotations__ = annotations def test_annotate_with_no_params(): - annotated_func = annotate(func) + annotated_func = annotate(func, _trigger_warning=False) assert annotated_func.__annotations__ == {} def test_annotate_with_params(): - annotated_func = annotate(**annotations)(func) + annotated_func = annotate(_trigger_warning=False, **annotations)(func) assert annotated_func.__annotations__ == annotations def test_annotate_with_wront_params(): with pytest.raises(Exception) as exc_info: - annotated_func = annotate(p=int)(func) + annotated_func = annotate(p=int, _trigger_warning=False)(func) assert str(exc_info.value) == 'The key p is not a function parameter in the function "func".' diff --git a/graphene/utils/tests/test_resolve_only_args.py b/graphene/utils/tests/test_resolve_only_args.py index d0d06e51..a3090c3a 100644 --- a/graphene/utils/tests/test_resolve_only_args.py +++ b/graphene/utils/tests/test_resolve_only_args.py @@ -10,4 +10,4 @@ def test_resolve_only_args(mocker): my_data = {'one': 1, 'two': 2} wrapped = resolve_only_args(resolver) - deprecated.warn_deprecation.assert_called_once() + assert deprecated.warn_deprecation.called From 66468e3ea5247d1915d6f9367fcfe0ab2efb94b1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 20:12:54 -0700 Subject: [PATCH 39/82] Improved Upgrade guide --- UPGRADE-v2.0.md | 189 +++++++++++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 76 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index b09dba45..e296f362 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -1,92 +1,119 @@ # v2.0 Upgrade Guide -* `ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations - have been quite simplified, without the need of define a explicit Metaclass. - The metaclasses threfore are now deleted as are no longer necessary, if your code was depending - on this internal metaclass for creating custom attrs, please see an [example of how to do it now in 2.0](https://github.com/graphql-python/graphene/blob/2.0/graphene/tests/issues/test_425.py). +`ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations +have been quite simplified, without the need to define a explicit Metaclass for each subtype. + +It also improves the function resolvers, [simplifying the code](#resolve_only_args) the +developer have to write to use them. + +Deprecations: +* [`AbstractType`](#abstracttype-deprecated) +* [`resolve_only_args`](#resolve_only_args) + +Breaking changes: +* [`Node Connections`](#node-connections) + +New Features! +* [`InputObjectType`](#inputobjecttype) +* [`Meta as Class arguments`](#meta-ass-class-arguments) (_only available for Python 3_) + + +> The type metaclases are now deleted as 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). ## Deprecations +### AbstractType deprecated -* AbstractType is deprecated, please use normal inheritance instead. +AbstractType is deprecated in graphene 2.0, you can now use normal inheritance instead. - Before: +Before: - ```python - class CommonFields(AbstractType): - name = String() - - class Pet(CommonFields, Interface): - pass - ``` +```python +class CommonFields(AbstractType): + name = String() - With 2.0: +class Pet(CommonFields, Interface): + pass +``` - ```python - class CommonFields(object): - name = String() - - class Pet(CommonFields, Interface): - pass - ``` +With 2.0: -* Meta options as class arguments (**ONLY PYTHON 3**). - - Before: +```python +class CommonFields(object): + name = String() - ```python - class Dog(ObjectType): - class Meta: - interfaces = [Pet] - name = String() - ``` +class Pet(CommonFields, Interface): + pass +``` - With 2.0: +### resolve\_only\_args + +`resolve_only_args` is now deprecated in favor of type annotations (using the polyfill `@graphene.annotate` in Python 2). + +Before: + +```python +class User(ObjectType): + name = String() + + @resolve_only_args + def resolve_name(self): + return self.name +``` + +With 2.0: + +```python +class User(ObjectType): + name = String() + + # Decorate the resolver with @annotate in Python 2 + def resolve_name(self) -> str: + return self.name +``` - ```python - class Dog(ObjectType, interfaces=[Pet]): - name = String() - ``` ## Breaking Changes -* Node types no longer have a `Connection` by default. - In 2.0 and onwoards `Connection`s should be defined explicitly. +### Node Connections + +Node types no longer have a `Connection` by default. +In 2.0 and onwards `Connection`s should be defined explicitly. + +Before: + +```python +class User(ObjectType): + class Meta: + interfaces = [relay.Node] + name = String() - Before: +class Query(ObjectType): + user_connection = relay.ConnectionField(User) +``` - ```python - class User(ObjectType): - class Meta: - interfaces = [relay.Node] - name = String() - - class Query(ObjectType): - user_connection = relay.ConnectionField(User) - ``` +With 2.0: - With 2.0: +```python +class User(ObjectType): + class Meta: + interfaces = [relay.Node] + name = String() - ```python - class User(ObjectType): - class Meta: - interfaces = [relay.Node] - name = String() - - class UserConnection(relay.Connection): - class Meta: - node = User +class UserConnection(relay.Connection): + class Meta: + node = User - class Query(ObjectType): - user_connection = relay.ConnectionField(UserConnection) - ``` +class Query(ObjectType): + user_connection = relay.ConnectionField(UserConnection) +``` ## New Features ### InputObjectType -`InputObjectType`s are now a first class citizen in Graphene. -That means, if you are using a custom InputObjectType, you can access +If you are using `InputObjectType`, you now can access it's fields via `getattr` (`my_input.myattr`) when resolving, instead of the classic way `my_input['myattr']`. @@ -95,9 +122,6 @@ And also use custom defined properties on your input class. Example. Before: ```python -class User(ObjectType): - name = String() - class UserInput(InputObjectType): id = ID() @@ -117,27 +141,40 @@ class Query(ObjectType): With 2.0: ```python -class User(ObjectType): - id = ID() - class UserInput(InputObjectType): id = ID() @property def is_user_id(self): - return id.startswith('userid_') + return self.id.startswith('userid_') class Query(ObjectType): user = graphene.Field(User, id=UserInput()) - @annotate(input=UserInput) - def resolve_user(self, input): - if input.is_user_id: - return get_user(input.id) - - # You can also do in Python 3: - def resolve_user(self, input: UserInput): + # Decorate the resolver with @annotate(input=UserInput) in Python 2 + def resolve_user(self, input: UserInput) -> User: if input.is_user_id: return get_user(input.id) ``` + + +### Meta as Class arguments + +Now you can use the meta options as class arguments (**ONLY PYTHON 3**). + +Before: + +```python +class Dog(ObjectType): + class Meta: + interfaces = [Pet] + name = String() +``` + +With 2.0: + +```python +class Dog(ObjectType, interfaces=[Pet]): + name = String() +``` From 40a15bdd2175f51c8f3b4c9fcee2cd1865b8d876 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 20:56:00 -0700 Subject: [PATCH 40/82] Improved test coverage --- graphene/relay/connection.py | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 20a5e648..5470e57e 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -9,6 +9,8 @@ from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) from ..types.field import Field from ..types.objecttype import ObjectType, ObjectTypeOptions +from ..utils.deprecated import warn_deprecation +from .node import is_node class PageInfo(ObjectType): @@ -98,6 +100,12 @@ class IterableConnectionField(Field): def type(self): type = super(IterableConnectionField, self).type connection_type = type + if is_node(type): + warn_deprecation( + "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" + ) + assert issubclass(connection_type, Connection), ( '{} type have to be a subclass of Connection. Received "{}".' ).format(self.__class__.__name__, connection_type) diff --git a/setup.cfg b/setup.cfg index d83176f0..efe77690 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ exclude = setup.py,docs/*,*/examples/*,graphene/pyutils/*,tests max-line-length = 120 [coverage:run] -omit = graphene/pyutils/*,*/tests/* +omit = graphene/pyutils/*,*/tests/*,graphene/types/scalars.py [isort] known_first_party=graphene From 800fbdf8203c49f1157dcf3e0f2df8143162db9c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 21:47:23 -0700 Subject: [PATCH 41/82] Use dev version of graphql-core and promise --- .travis.yml | 4 ---- setup.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 88574e69..d87939e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,10 +22,6 @@ before_install: install: - | if [ "$TEST_TYPE" = build ]; then - # For testing - pip install https://github.com/graphql-python/graphql-core/archive/master.zip --upgrade - pip install https://github.com/syrusakbary/promise/archive/master.zip --upgrade - pip install -e .[test] python setup.py develop elif [ "$TEST_TYPE" = lint ]; then diff --git a/setup.py b/setup.py index f01f0d9b..3bb6225f 100644 --- a/setup.py +++ b/setup.py @@ -83,9 +83,9 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.1', + 'graphql-core>=1.2.dev', 'graphql-relay>=0.4.5', - 'promise>=2.0', + 'promise>=2.1.dev', ], tests_require=tests_require, extras_require={ From 9769612a44c5f0e9a736870773608de756e50f6b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 23:10:15 -0700 Subject: [PATCH 42/82] Make resolvers simple again --- README.md | 4 +- README.rst | 2 +- UPGRADE-v2.0.md | 12 +++--- examples/context_example.py | 3 +- examples/simple_example.py | 2 +- examples/starwars/schema.py | 6 +-- examples/starwars_relay/schema.py | 3 -- graphene/relay/mutation.py | 8 ++-- graphene/relay/tests/test_connection_query.py | 6 +-- graphene/tests/issues/test_313.py | 1 - graphene/tests/issues/test_490.py | 4 +- graphene/types/field.py | 3 +- graphene/types/mutation.py | 3 +- graphene/types/tests/test_datetime.py | 7 ++-- graphene/types/tests/test_generic.py | 3 +- graphene/types/tests/test_json.py | 3 +- graphene/types/tests/test_mutation.py | 28 +++++++------- graphene/types/tests/test_query.py | 37 +++++++++---------- graphene/types/tests/test_typemap.py | 6 +-- graphene/types/typemap.py | 13 +++++-- graphene/utils/auto_resolver.py | 19 +++++++--- graphene/utils/resolver_from_annotations.py | 15 ++------ graphene/utils/tests/test_auto_resolver.py | 4 +- 23 files changed, 93 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 5b27c4b5..9635a190 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit For instaling graphene, just run this command in your shell ```bash -pip install "graphene>=2.0" +pip install "graphene>=2.0.dev" ``` ## 2.0 Upgrade Guide @@ -48,7 +48,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' schema = graphene.Schema(query=Query) diff --git a/README.rst b/README.rst index fcde970f..b31335f2 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' schema = graphene.Schema(query=Query) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index e296f362..ca0502ec 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -49,7 +49,7 @@ class Pet(CommonFields, Interface): ### resolve\_only\_args -`resolve_only_args` is now deprecated in favor of type annotations (using the polyfill `@graphene.annotate` in Python 2). +`resolve_only_args` is now deprecated in favor of type annotations (using the polyfill `@graphene.annotate` in Python 2 in case is necessary for accessing `context` or `info`). Before: @@ -68,8 +68,7 @@ With 2.0: class User(ObjectType): name = String() - # Decorate the resolver with @annotate in Python 2 - def resolve_name(self) -> str: + def resolve_name(self): return self.name ``` @@ -129,7 +128,7 @@ def is_user_id(id): return id.startswith('userid_') class Query(ObjectType): - user = graphene.Field(User, id=UserInput()) + user = graphene.Field(User, input=UserInput()) @resolve_only_args def resolve_user(self, input): @@ -149,10 +148,9 @@ class UserInput(InputObjectType): return self.id.startswith('userid_') class Query(ObjectType): - user = graphene.Field(User, id=UserInput()) + user = graphene.Field(User, input=UserInput()) - # Decorate the resolver with @annotate(input=UserInput) in Python 2 - def resolve_user(self, input: UserInput) -> User: + def resolve_user(self, input): if input.is_user_id: return get_user(input.id) diff --git a/examples/context_example.py b/examples/context_example.py index d1d273d6..2477a9bf 100644 --- a/examples/context_example.py +++ b/examples/context_example.py @@ -9,7 +9,8 @@ class User(graphene.ObjectType): class Query(graphene.ObjectType): me = graphene.Field(User) - def resolve_me(self, args, context, info): + @graphene.annotate(context=graphene.Context) + def resolve_me(self, context): return context['user'] diff --git a/examples/simple_example.py b/examples/simple_example.py index e8023266..58fd07d1 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -11,7 +11,7 @@ class Query(graphene.ObjectType): patron = graphene.Field(Patron) - def resolve_patron(self, args, context, info): + def resolve_patron(self): return Patron(id=1, name='Syrus', age=27) diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py index b3c1a63b..cc36c75f 100644 --- a/examples/starwars/schema.py +++ b/examples/starwars/schema.py @@ -1,5 +1,4 @@ import graphene -from graphene import annotate from .data import get_character, get_droid, get_hero, get_human @@ -16,7 +15,7 @@ class Character(graphene.Interface): friends = graphene.List(lambda: Character) appears_in = graphene.List(Episode) - def resolve_friends(self, args, *_): + def resolve_friends(self): # The character friends is a list of strings return [get_character(f) for f in self.friends] @@ -46,15 +45,12 @@ class Query(graphene.ObjectType): id=graphene.String() ) - @annotate(episode=Episode) def resolve_hero(self, episode=None): return get_hero(episode) - @annotate(id=str) def resolve_human(self, id): return get_human(id) - @annotate(id=str) def resolve_droid(self, id): return get_droid(id) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 69a8dba7..f2a6c6f3 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -32,7 +32,6 @@ class Faction(graphene.ObjectType): name = graphene.String(description='The name of the faction.') ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.') - @annotate def resolve_ships(self, **args): # Transform the instance ship_ids into real instances return [get_ship(ship_id) for ship_id in self.ships] @@ -65,11 +64,9 @@ class Query(graphene.ObjectType): empire = graphene.Field(Faction) node = relay.Node.Field() - @annotate def resolve_rebels(self): return get_rebels() - @annotate def resolve_empire(self): return get_empire() diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index fb57c275..e1dea353 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -3,8 +3,9 @@ from collections import OrderedDict from promise import Promise, is_thenable -from ..types import Field, InputObjectType, String +from ..types import Field, InputObjectType, String, Context, ResolveInfo from ..types.mutation import Mutation +from ..utils.annotate import annotate class ClientIDMutation(Mutation): @@ -49,9 +50,8 @@ class ClientIDMutation(Mutation): ) @classmethod - def mutate(cls, root, args, context, info): - input = args.get('input') - + @annotate(context=Context, info=ResolveInfo) + def mutate(cls, root, input, context, info): def on_resolve(payload): try: payload.client_mutation_id = input.get('clientMutationId') diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 11342a44..370db36e 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -31,13 +31,13 @@ class Query(ObjectType): node = Node.Field() - def resolve_letters(self, args, context, info): + def resolve_letters(self, **args): return list(letters.values()) - def resolve_promise_letters(self, args, context, info): + def resolve_promise_letters(self, **args): return Promise.resolve(list(letters.values())) - def resolve_connection_letters(self, args, context, info): + def resolve_connection_letters(self, **args): return LetterConnection( page_info=PageInfo( has_next_page=True, diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py index ed89e45c..3881590b 100644 --- a/graphene/tests/issues/test_313.py +++ b/graphene/tests/issues/test_313.py @@ -29,7 +29,6 @@ class CreatePost(graphene.Mutation): result = graphene.Field(CreatePostResult) - @resolve_only_args def mutate(self, text): result = Success(yeah='yeah') diff --git a/graphene/tests/issues/test_490.py b/graphene/tests/issues/test_490.py index fc6d016e..f402e483 100644 --- a/graphene/tests/issues/test_490.py +++ b/graphene/tests/issues/test_490.py @@ -6,8 +6,8 @@ import graphene class Query(graphene.ObjectType): some_field = graphene.String(from_=graphene.String(name="from")) - def resolve_some_field(_, args, context, infos): - return args.get("from_") + def resolve_some_field(self, from_=None): + return from_ def test_issue(): diff --git a/graphene/types/field.py b/graphene/types/field.py index 8de94bed..9e699a12 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -7,7 +7,6 @@ from .mountedtype import MountedType from .structures import NonNull from .unmountedtype import UnmountedType from .utils import get_type -from ..utils.auto_resolver import auto_resolver base_type = type @@ -64,4 +63,4 @@ class Field(MountedType): return get_type(self._type) def get_resolver(self, parent_resolver): - return auto_resolver(self.resolver or parent_resolver) + return self.resolver or parent_resolver diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 8756412f..431c1d87 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -5,6 +5,7 @@ from ..utils.props import props from .field import Field from .objecttype import ObjectType, ObjectTypeOptions from .utils import yank_fields_from_attrs +from ..utils.auto_resolver import auto_resolver class MutationOptions(ObjectTypeOptions): @@ -59,7 +60,7 @@ class Mutation(ObjectType): _meta.fields = fields _meta.output = output - _meta.resolver = resolver + _meta.resolver = auto_resolver(resolver) _meta.arguments = arguments super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index e01bdc8a..660ae091 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -11,12 +11,11 @@ class Query(ObjectType): datetime = DateTime(_in=DateTime(name='in')) time = Time(_at=Time(name='at')) - def resolve_datetime(self, args, context, info): - _in = args.get('_in') + def resolve_datetime(self, _in=None): return _in - def resolve_time(self, args, context, info): - return args.get('_at') + def resolve_time(self, _at=None): + return _at schema = Schema(query=Query) diff --git a/graphene/types/tests/test_generic.py b/graphene/types/tests/test_generic.py index aede1a8b..3a5eb88a 100644 --- a/graphene/types/tests/test_generic.py +++ b/graphene/types/tests/test_generic.py @@ -6,8 +6,7 @@ from ..schema import Schema class Query(ObjectType): generic = GenericScalar(input=GenericScalar()) - def resolve_generic(self, args, context, info): - input = args.get('input') + def resolve_generic(self, input=None): return input diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index c912df56..d671722d 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -7,8 +7,7 @@ from ..schema import Schema class Query(ObjectType): json = JSONString(input=JSONString()) - def resolve_json(self, args, context, info): - input = args.get('input') + def resolve_json(self, input): return input schema = Schema(query=Query) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 081e823c..79544af5 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -12,14 +12,14 @@ def test_generate_mutation_no_args(): class MyMutation(Mutation): '''Documentation''' - @classmethod - def mutate(cls, *args, **kwargs): - pass + def mutate(self, **args): + return args assert issubclass(MyMutation, ObjectType) assert MyMutation._meta.name == "MyMutation" assert MyMutation._meta.description == "Documentation" - assert MyMutation.Field().resolver == MyMutation.mutate + resolved = MyMutation.Field().resolver(None, {'name': 'Peter'}, None, None) + assert resolved == {'name': 'Peter'} def test_generate_mutation_with_meta(): @@ -29,13 +29,13 @@ def test_generate_mutation_with_meta(): name = 'MyOtherMutation' description = 'Documentation' - @classmethod - def mutate(cls, *args, **kwargs): - pass + def mutate(self, **args): + return args assert MyMutation._meta.name == "MyOtherMutation" assert MyMutation._meta.description == "Documentation" - assert MyMutation.Field().resolver == MyMutation.mutate + resolved = MyMutation.Field().resolver(None, {'name': 'Peter'}, None, None) + assert resolved == {'name': 'Peter'} def test_mutation_raises_exception_if_no_mutate(): @@ -59,15 +59,15 @@ def test_mutation_custom_output_type(): Output = User - @classmethod - def mutate(cls, args, context, info): - name = args.get('name') + def mutate(self, name): return User(name=name) field = CreateUser.Field() assert field.type == User assert field.args == {'name': Argument(String)} - assert field.resolver == CreateUser.mutate + resolved = field.resolver(None, {'name': 'Peter'}, None, None) + assert isinstance(resolved, User) + assert resolved.name == 'Peter' def test_mutation_execution(): @@ -81,9 +81,7 @@ def test_mutation_execution(): name = String() dynamic = Dynamic(lambda: String()) - def mutate(self, args, context, info): - name = args.get('name') - dynamic = args.get('dynamic') + def mutate(self, name, dynamic): return CreateUser(name=name, dynamic=dynamic) class Query(ObjectType): diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 4adcedf9..f41f40bc 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -57,7 +57,7 @@ def test_query_union(): class Query(ObjectType): unions = List(MyUnion) - def resolve_unions(self, args, context, info): + def resolve_unions(self): return [one_object(), two_object()] hello_schema = Schema(Query) @@ -108,7 +108,7 @@ def test_query_interface(): class Query(ObjectType): interfaces = List(MyInterface) - def resolve_interfaces(self, args, context, info): + def resolve_interfaces(self): return [one_object(), two_object()] hello_schema = Schema(Query, types=[One, Two]) @@ -188,7 +188,7 @@ def test_query_resolve_function(): class Query(ObjectType): hello = String() - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' hello_schema = Schema(Query) @@ -202,7 +202,7 @@ def test_query_arguments(): class Query(ObjectType): test = String(a_str=String(), a_int=Int()) - def resolve_test(self, args, context, info): + def resolve_test(self, **args): return json.dumps([self, args], separators=(',', ':')) test_schema = Schema(Query) @@ -231,7 +231,7 @@ def test_query_input_field(): class Query(ObjectType): test = String(a_input=Input()) - def resolve_test(self, args, context, info): + def resolve_test(self, **args): return json.dumps([self, args], separators=(',', ':')) test_schema = Schema(Query) @@ -254,10 +254,10 @@ def test_query_middlewares(): hello = String() other = String() - def resolve_hello(self, args, context, info): + def resolve_hello(self): return 'World' - def resolve_other(self, args, context, info): + def resolve_other(self): return 'other' def reversed_middleware(next, *args, **kwargs): @@ -280,14 +280,14 @@ def test_objecttype_on_instances(): class ShipType(ObjectType): name = String(description="Ship name", required=True) - def resolve_name(self, context, args, info): + def resolve_name(self): # Here self will be the Ship instance returned in resolve_ship return self.name class Query(ObjectType): ship = Field(ShipType) - def resolve_ship(self, context, args, info): + def resolve_ship(self): return Ship(name='xwing') schema = Schema(query=Query) @@ -302,7 +302,7 @@ def test_big_list_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self, args, context, info): + def resolve_all_ints(self): return big_list hello_schema = Schema(Query) @@ -319,7 +319,7 @@ def test_big_list_query_compiled_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self, args, context, info): + def resolve_all_ints(self): return big_list hello_schema = Schema(Query) @@ -341,7 +341,7 @@ def test_big_list_of_containers_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self): return big_container_list hello_schema = Schema(Query) @@ -364,7 +364,7 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self): return big_container_list hello_schema = Schema(Query) @@ -382,16 +382,16 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark z = Int() o = Int() - def resolve_x(self, args, context, info): + def resolve_x(self): return self.x - def resolve_y(self, args, context, info): + def resolve_y(self): return self.y - def resolve_z(self, args, context, info): + def resolve_z(self): return self.z - def resolve_o(self, args, context, info): + def resolve_o(self): return self.o big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(1000)] @@ -399,7 +399,7 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self): return big_container_list hello_schema = Schema(Query) @@ -420,7 +420,6 @@ def test_query_annotated_resolvers(): context = String() info = String() - @annotate(_trigger_warning=False) def resolve_annotated(self, id): return "{}-{}".format(self, id) diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 3a0f20dd..266173cf 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -49,8 +49,8 @@ def test_objecttype(): foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') bar = String(name='gizmo') - def resolve_foo(self, args, info): - return args.get('bar') + def resolve_foo(self, bar): + return bar typemap = TypeMap([MyObjectType]) assert 'MyObjectType' in typemap @@ -65,7 +65,7 @@ def test_objecttype(): assert isinstance(foo_field, GraphQLField) assert foo_field.description == 'Field description' f = MyObjectType.resolve_foo - assert foo_field.resolver == getattr(f, '__func__', f) + # assert foo_field.resolver == getattr(f, '__func__', f) assert foo_field.args == { 'bar': GraphQLArgument(GraphQLString, description='Argument description', default_value='x', out_name='bar') } diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 7e0d9ca0..17ff95b1 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -11,6 +11,7 @@ from graphql.type.typemap import GraphQLTypeMap from ..utils.get_unbound_function import get_unbound_function from ..utils.str_converters import to_camel_case +from ..utils.auto_resolver import auto_resolver, final_resolver from .definitions import (GrapheneEnumType, GrapheneGraphQLType, GrapheneInputObjectType, GrapheneInterfaceType, GrapheneObjectType, GrapheneScalarType, @@ -256,8 +257,14 @@ class TypeMap(GraphQLTypeMap): field_type, args=args, resolver=field.get_resolver( - self.get_resolver_for_type(type, name, - field.default_value)), + auto_resolver( + self.get_resolver_for_type( + type, + name, + field.default_value + ) + ) + ), deprecation_reason=field.deprecation_reason, description=field.description) field_name = field.name or self.get_name(name) @@ -287,7 +294,7 @@ class TypeMap(GraphQLTypeMap): default_resolver = type._meta.default_resolver or get_default_resolver( ) - return partial(default_resolver, name, default_value) + return final_resolver(partial(default_resolver, name, default_value)) def get_field_type(self, map, type): if isinstance(type, List): diff --git a/graphene/utils/auto_resolver.py b/graphene/utils/auto_resolver.py index 633c1a95..72f33515 100644 --- a/graphene/utils/auto_resolver.py +++ b/graphene/utils/auto_resolver.py @@ -1,12 +1,21 @@ -from .resolver_from_annotations import resolver_from_annotations, is_wrapped_from_annotations +from .resolver_from_annotations import resolver_from_annotations + + +def final_resolver(func): + func._is_final_resolver = True + return func def auto_resolver(func=None): - annotations = getattr(func, '__annotations__', {}) - is_annotated = getattr(func, '_is_annotated', False) + if not func: + return - if (annotations or is_annotated) and not is_wrapped_from_annotations(func): + if not is_final_resolver(func): # Is a Graphene 2.0 resolver function - return resolver_from_annotations(func) + return final_resolver(resolver_from_annotations(func)) else: return func + + +def is_final_resolver(func): + return getattr(func, '_is_final_resolver', False) diff --git a/graphene/utils/resolver_from_annotations.py b/graphene/utils/resolver_from_annotations.py index 79fb99de..2e006dd6 100644 --- a/graphene/utils/resolver_from_annotations.py +++ b/graphene/utils/resolver_from_annotations.py @@ -1,15 +1,10 @@ from ..pyutils.compat import signature -from functools import wraps +from functools import wraps, partial def resolver_from_annotations(func): from ..types import Context, ResolveInfo - _is_wrapped_from_annotations = is_wrapped_from_annotations(func) - assert not _is_wrapped_from_annotations, "The function {func_name} is already wrapped.".format( - func_name=func.func_name - ) - func_signature = signature(func) _context_var = None @@ -38,9 +33,7 @@ def resolver_from_annotations(func): def inner(root, args, context, info): return func(root, **args) - inner._is_wrapped_from_annotations = True + if isinstance(func, partial): + return inner + return wraps(func)(inner) - - -def is_wrapped_from_annotations(func): - return getattr(func, '_is_wrapped_from_annotations', False) diff --git a/graphene/utils/tests/test_auto_resolver.py b/graphene/utils/tests/test_auto_resolver.py index 93b7a991..9fa58c22 100644 --- a/graphene/utils/tests/test_auto_resolver.py +++ b/graphene/utils/tests/test_auto_resolver.py @@ -1,15 +1,15 @@ import pytest from ..annotate import annotate -from ..auto_resolver import auto_resolver +from ..auto_resolver import auto_resolver, final_resolver from ...types import Context, ResolveInfo +@final_resolver def resolver(root, args, context, info): return root, args, context, info -@annotate def resolver_annotated(root, **args): return root, args, None, None From 0e355ee296725def3a922970813fb00fbabe43df Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 23:16:51 -0700 Subject: [PATCH 43/82] Improved documentation examples --- UPGRADE-v2.0.md | 2 +- docs/execution/dataloader.rst | 4 ++-- docs/execution/execute.rst | 3 ++- docs/quickstart.rst | 4 ++-- docs/relay/connection.rst | 2 +- docs/types/mutations.rst | 24 +++++++++++------------- docs/types/objecttypes.rst | 8 +++----- 7 files changed, 22 insertions(+), 25 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index ca0502ec..d2187fd1 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -87,7 +87,7 @@ class User(ObjectType): class Meta: interfaces = [relay.Node] name = String() - + class Query(ObjectType): user_connection = relay.ConnectionField(User) ``` diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 3695fcf7..17374534 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -99,8 +99,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac best_friend = graphene.Field(lambda: User) friends = graphene.List(lambda: User) - def resolve_best_friend(self, args, context, info): + def resolve_best_friend(self): return user_loader.load(self.best_friend_id) - def resolve_friends(self, args, context, info): + def resolve_friends(self): return user_loader.load_many(self.friend_ids) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index 17bd9071..9fddce15 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -24,7 +24,8 @@ You can pass context to a query via ``context_value``. class Query(graphene.ObjectType): name = graphene.String() - def resolve_name(self, args, context, info): + @graphene.annotate(context=graphene.Context) + def resolve_name(self, context): return context.get('name') schema = graphene.Schema(Query) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 44a686cf..ab68ff0d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -39,8 +39,8 @@ one field: ``hello`` and an input name. And when we query it, it should return ` class Query(graphene.ObjectType): hello = graphene.String(name=graphene.Argument(graphene.String, default_value="stranger")) - def resolve_hello(self, args, context, info): - return 'Hello ' + args['name'] + def resolve_hello(self, name): + return 'Hello ' + name schema = graphene.Schema(query=Query) diff --git a/docs/relay/connection.rst b/docs/relay/connection.rst index f581e3f9..898d627d 100644 --- a/docs/relay/connection.rst +++ b/docs/relay/connection.rst @@ -41,5 +41,5 @@ that implements ``Node`` will have a default Connection. name = graphene.String() ships = relay.ConnectionField(ShipConnection) - def resolve_ships(self, args, context, info): + def resolve_ships(self): return [] diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index d7f590a4..58b182ef 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -19,9 +19,8 @@ This example defines a Mutation: ok = graphene.Boolean() person = graphene.Field(lambda: Person) - @staticmethod - def mutate(root, args, context, info): - person = Person(name=args.get('name')) + def mutate(self, name): + person = Person(name=name) ok = True return CreatePerson(person=person, ok=ok) @@ -90,30 +89,29 @@ InputFields are used in mutations to allow nested input data for mutations To use an InputField you define an InputObjectType that specifies the structure of your input data - - .. code:: python import graphene class PersonInput(graphene.InputObjectType): - name = graphene.String() - age = graphene.Int() + name = graphene.String(required=True) + age = graphene.Int(required=True) class CreatePerson(graphene.Mutation): class Input: - person_data = graphene.Argument(PersonInput) + person_data = PersonInput(required=True) - person = graphene.Field(lambda: Person) + person = graphene.Field(Person) @staticmethod - def mutate(root, args, context, info): - p_data = args.get('person_data') - + def mutate(root, person_data=None): name = p_data.get('name') age = p_data.get('age') - person = Person(name=name, age=age) + person = Person( + name=person_data.name, + age=person_data.age + ) return CreatePerson(person=person) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 2b3bce23..409db58c 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -25,7 +25,7 @@ This example model defines a Person, with a first and a last name: last_name = graphene.String() full_name = graphene.String() - def resolve_full_name(self, args, context, info): + def resolve_full_name(self): return '{} {}'.format(self.first_name, self.last_name) **first\_name** and **last\_name** are fields of the ObjectType. Each @@ -71,8 +71,7 @@ method in the class. class Query(graphene.ObjectType): reverse = graphene.String(word=graphene.String()) - def resolve_reverse(self, args, context, info): - word = args.get('word') + def resolve_reverse(self, word): return word[::-1] Resolvers outside the class @@ -84,8 +83,7 @@ A field can use a custom resolver from outside the class: import graphene - def reverse(root, args, context, info): - word = args.get('word') + def reverse(root, word): return word[::-1] class Query(graphene.ObjectType): From df58b9a48b23a7f17ab3b848c6c71dbef949e028 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 23:30:56 -0700 Subject: [PATCH 44/82] Remove _is_annotated flag from annotate decorator --- graphene/utils/annotate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/utils/annotate.py b/graphene/utils/annotate.py index b55c3d6c..51a87a78 100644 --- a/graphene/utils/annotate.py +++ b/graphene/utils/annotate.py @@ -33,5 +33,4 @@ def annotate(_func=None, _trigger_warning=True, **annotations): else: _func.__annotations__.update(annotations) - _func._is_annotated = True return _func From dba62565785676b8bf967dd60f74dc38886b1b57 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 23 Jul 2017 23:43:08 -0700 Subject: [PATCH 45/82] Changed version to 2.0.dev --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 98dfe058..cffb5074 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 4, 1, 'final', 0) +VERSION = (2, 0, 0, 'alpha', 0) __version__ = get_version(VERSION) From 33a30db5f1762d5ea16052b06afb6f32fce9ffe4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 24 Jul 2017 00:01:44 -0700 Subject: [PATCH 46/82] Improved quickstart docs --- docs/quickstart.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index ab68ff0d..4192fada 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -12,15 +12,15 @@ Let’s build a basic GraphQL schema from scratch. Requirements ------------ -- Python (2.7, 3.2, 3.3, 3.4, 3.5, pypy) -- Graphene (1.0) +- Python (2.7, 3.4, 3.5, 3.6, pypy) +- Graphene (2.0) Project setup ------------- .. code:: bash - pip install "graphene>=1.0" + pip install "graphene>=2.0" Creating a basic Schema ----------------------- @@ -37,7 +37,7 @@ one field: ``hello`` and an input name. And when we query it, it should return ` import graphene class Query(graphene.ObjectType): - hello = graphene.String(name=graphene.Argument(graphene.String, default_value="stranger")) + hello = graphene.String(name=graphene.String(default_value="stranger")) def resolve_hello(self, name): return 'Hello ' + name From 7ca5c2225f80571f394af97c5557767efbf6f30c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 24 Jul 2017 21:33:08 -0700 Subject: [PATCH 47/82] Added create_type. Support for Meta as dict --- graphene/types/base.py | 4 ++ graphene/types/tests/test_base.py | 63 ++++++++++++++++++++++++++++ graphene/utils/subclass_with_meta.py | 8 +++- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 graphene/types/tests/test_base.py diff --git a/graphene/types/base.py b/graphene/types/base.py index ad8869b9..d9c3bdd4 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -26,6 +26,10 @@ class BaseOptions(object): class BaseType(SubclassWithMeta): + @classmethod + def create_type(cls, class_name, **options): + return type(class_name, (cls, ), {'Meta': options}) + @classmethod def __init_subclass_with_meta__(cls, name=None, description=None, _meta=None): assert "_meta" not in cls.__dict__, "Can't assign directly meta" diff --git a/graphene/types/tests/test_base.py b/graphene/types/tests/test_base.py new file mode 100644 index 00000000..2ea3dcce --- /dev/null +++ b/graphene/types/tests/test_base.py @@ -0,0 +1,63 @@ +import pytest + +from ..base import BaseType, BaseOptions + + +class CustomOptions(BaseOptions): + pass + + +class CustomType(BaseType): + @classmethod + def __init_subclass_with_meta__(cls, **options): + _meta = CustomOptions(cls) + super(CustomType, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + +def test_basetype(): + class MyBaseType(CustomType): + pass + + assert isinstance(MyBaseType._meta, CustomOptions) + assert MyBaseType._meta.name == "MyBaseType" + assert MyBaseType._meta.description is None + + +def test_basetype_nones(): + class MyBaseType(CustomType): + '''Documentation''' + class Meta: + name = None + description = None + + assert isinstance(MyBaseType._meta, CustomOptions) + assert MyBaseType._meta.name == "MyBaseType" + assert MyBaseType._meta.description == "Documentation" + + +def test_basetype_custom(): + class MyBaseType(CustomType): + '''Documentation''' + class Meta: + name = 'Base' + description = 'Desc' + + assert isinstance(MyBaseType._meta, CustomOptions) + assert MyBaseType._meta.name == "Base" + assert MyBaseType._meta.description == "Desc" + + +def test_basetype_create(): + MyBaseType = CustomType.create_type('MyBaseType') + + assert isinstance(MyBaseType._meta, CustomOptions) + assert MyBaseType._meta.name == "MyBaseType" + assert MyBaseType._meta.description is None + + +def test_basetype_create_extra(): + MyBaseType = CustomType.create_type('MyBaseType', name='Base', description='Desc') + + assert isinstance(MyBaseType._meta, CustomOptions) + assert MyBaseType._meta.name == "Base" + assert MyBaseType._meta.description == "Desc" diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index d6d33cb6..f1ebfce9 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -1,4 +1,5 @@ import six +from inspect import isclass from ..pyutils.init_subclass import InitSubclassMeta from .props import props @@ -18,7 +19,12 @@ class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): _Meta = getattr(cls, "Meta", None) _meta_props = {} if _Meta: - _meta_props = props(_Meta) + if isinstance(_Meta, dict): + _meta_props = _Meta + elif isclass(_Meta): + _meta_props = props(_Meta) + else: + raise Exception("Meta have to be either a class or a dict. Received {}".format(_Meta)) delattr(cls, "Meta") options = dict(meta_options, **_meta_props) super_class = super(cls, cls) From ed4bcce0cfb0ec355888a939021c6cfefbf72869 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 24 Jul 2017 23:09:52 -0700 Subject: [PATCH 48/82] Improved Relay Mutation --- graphene/relay/mutation.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index e1dea353..d44836fa 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -14,7 +14,10 @@ class ClientIDMutation(Mutation): abstract = True @classmethod - def __init_subclass_with_meta__(cls, output=None, arguments=None, name=None, **options): + def __init_subclass_with_meta__(cls, output=None, input_fields=None, arguments=None, name=None, abstract=False, **options): + if abstract: + return + input_class = getattr(cls, 'Input', None) name = name or cls.__name__ base_name = re.sub('Payload$', '', name) @@ -25,11 +28,15 @@ class ClientIDMutation(Mutation): bases = (InputObjectType, ) if input_class: bases += (input_class, ) + + if not input_fields: + input_fields = {} - cls.Input = type('{}Input'.format(base_name), - bases, { - 'client_mutation_id': String(name='clientMutationId') - }) + cls.Input = type( + '{}Input'.format(base_name), + bases, + OrderedDict(input_fields, client_mutation_id=String(name='clientMutationId')) + ) arguments = OrderedDict( input=cls.Input(required=True) From eabb9b202c92674878c893f3bed87fcb215a5932 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 24 Jul 2017 23:09:59 -0700 Subject: [PATCH 49/82] Added UUID type --- graphene/__init__.py | 4 ++++ graphene/types/__init__.py | 4 ++++ graphene/types/tests/test_uuid.py | 34 +++++++++++++++++++++++++++++++ graphene/types/uuid.py | 27 ++++++++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 graphene/types/tests/test_uuid.py create mode 100644 graphene/types/uuid.py diff --git a/graphene/__init__.py b/graphene/__init__.py index cffb5074..5d6d41d7 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -27,6 +27,8 @@ if not __SETUP__: Schema, Scalar, String, ID, Int, Float, Boolean, + JSONString, + UUID, List, NonNull, Enum, Argument, @@ -63,6 +65,8 @@ if not __SETUP__: 'Float', 'Enum', 'Boolean', + 'JSONString', + 'UUID', 'List', 'NonNull', 'Argument', diff --git a/graphene/types/__init__.py b/graphene/types/__init__.py index ee5d4981..2699740a 100644 --- a/graphene/types/__init__.py +++ b/graphene/types/__init__.py @@ -5,6 +5,8 @@ from .objecttype import ObjectType from .interface import Interface from .mutation import Mutation from .scalars import Scalar, String, ID, Int, Float, Boolean +from .json import JSONString +from .uuid import UUID from .schema import Schema from .structures import List, NonNull from .enum import Enum @@ -34,6 +36,8 @@ __all__ = [ 'ID', 'Int', 'Float', + 'JSONString', + 'UUID', 'Boolean', 'List', 'NonNull', diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py new file mode 100644 index 00000000..23eac6b4 --- /dev/null +++ b/graphene/types/tests/test_uuid.py @@ -0,0 +1,34 @@ +from ..uuid import UUID +from ..objecttype import ObjectType +from ..schema import Schema + + +class Query(ObjectType): + uuid = UUID(input=UUID()) + + def resolve_uuid(self, input): + return input + +schema = Schema(query=Query) + + +def test_uuidstring_query(): + uuid_value = 'dfeb3bcf-70fd-11e7-a61a-6003088f8204' + result = schema.execute('''{ uuid(input: "%s") }''' % uuid_value) + assert not result.errors + assert result.data == { + 'uuid': uuid_value + } + + +def test_uuidstring_query_variable(): + uuid_value = 'dfeb3bcf-70fd-11e7-a61a-6003088f8204' + + result = schema.execute( + '''query Test($uuid: UUID){ uuid(input: $uuid) }''', + variable_values={'uuid': uuid_value} + ) + assert not result.errors + assert result.data == { + 'uuid': uuid_value + } diff --git a/graphene/types/uuid.py b/graphene/types/uuid.py new file mode 100644 index 00000000..7686f682 --- /dev/null +++ b/graphene/types/uuid.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import + +from uuid import UUID as _UUID + +from graphql.language import ast + +from .scalars import Scalar + + +class UUID(Scalar): + '''UUID''' + + @staticmethod + def serialize(uuid): + if isinstance(uuid, str): + uuid = _UUID(uuid) + assert isinstance(uuid, _UUID), "Expected UUID instance, received {}".format(uuid) + return str(uuid) + + @staticmethod + def parse_literal(node): + if isinstance(node, ast.StringValue): + return _UUID(node.value) + + @staticmethod + def parse_value(value): + return _UUID(value) From 5696c5af4fed8fb9c55c2fd96b794ae5ef238c11 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 24 Jul 2017 23:15:47 -0700 Subject: [PATCH 50/82] Added UUID docs --- UPGRADE-v2.0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index d2187fd1..1877b8b9 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -176,3 +176,7 @@ With 2.0: class Dog(ObjectType, interfaces=[Pet]): name = String() ``` + +### UUID Scalar + +In Graphene 2.0 there is a new dedicated scalar for UUIDs, `UUID`. From e71b59a8c3b63481bd3afe2381f266c3b7932292 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 24 Jul 2017 23:15:56 -0700 Subject: [PATCH 51/82] Fixed flake8 issues in mutation --- graphene/relay/mutation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index d44836fa..6414a533 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -14,7 +14,8 @@ class ClientIDMutation(Mutation): abstract = True @classmethod - def __init_subclass_with_meta__(cls, output=None, input_fields=None, arguments=None, name=None, abstract=False, **options): + def __init_subclass_with_meta__(cls, output=None, input_fields=None, + arguments=None, name=None, abstract=False, **options): if abstract: return @@ -28,7 +29,7 @@ class ClientIDMutation(Mutation): bases = (InputObjectType, ) if input_class: bases += (input_class, ) - + if not input_fields: input_fields = {} From c7c611266b155c1ee46261744be98d8c084b20f0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 19:26:54 -0700 Subject: [PATCH 52/82] Allow types to be abstract --- graphene/types/interface.py | 4 +--- graphene/types/mutation.py | 4 +--- graphene/types/objecttype.py | 4 +--- graphene/utils/subclass_with_meta.py | 14 +++++++++++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/graphene/types/interface.py b/graphene/types/interface.py index 389cb82c..054b7229 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -19,9 +19,7 @@ class Interface(BaseType): when the field is resolved. ''' @classmethod - def __init_subclass_with_meta__(cls, abstract=False, _meta=None, **options): - if abstract: - return + def __init_subclass_with_meta__(cls, _meta=None, **options): if not _meta: _meta = InterfaceOptions(cls) diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 431c1d87..32194e9e 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -20,9 +20,7 @@ class Mutation(ObjectType): ''' @classmethod def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None, - _meta=None, abstract=False, **options): - if abstract: - return + _meta=None, **options): if not _meta: _meta = MutationOptions(cls) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 9987e4e4..c579a88e 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -22,9 +22,7 @@ class ObjectType(BaseType): def __init_subclass_with_meta__( cls, interfaces=(), possible_types=(), - default_resolver=None, _meta=None, abstract=False, **options): - if abstract: - return + default_resolver=None, _meta=None, **options): if not _meta: _meta = ObjectTypeOptions(cls) diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index f1ebfce9..9226e418 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -27,9 +27,17 @@ class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_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) - super_class = super(cls, cls) - if hasattr(super_class, '__init_subclass_with_meta__'): - super_class.__init_subclass_with_meta__(**options) + + abstract = options.pop('abstract', False) + if abstract: + assert not options, ( + "Abstract types can only contain the abstract attribute. " + "Received: abstract, {option_keys}" + ).format(option_keys=', '.join(options.keys())) + else: + super_class = super(cls, cls) + if hasattr(super_class, '__init_subclass_with_meta__'): + super_class.__init_subclass_with_meta__(**options) @classmethod def __init_subclass_with_meta__(cls, **meta_options): From 6dde81ee65d1542ee6f8a41aebea6deabde36cc6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 19:44:17 -0700 Subject: [PATCH 53/82] Improved Mutation warning --- UPGRADE-v2.0.md | 40 ++++++++++++++++++++++++++++++++++++++ graphene/types/mutation.py | 7 ++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 1877b8b9..b4b671b7 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -9,6 +9,7 @@ developer have to write to use them. Deprecations: * [`AbstractType`](#abstracttype-deprecated) * [`resolve_only_args`](#resolve_only_args) +* [`Mutation.Input`](#mutation-input) Breaking changes: * [`Node Connections`](#node-connections) @@ -72,6 +73,26 @@ class User(ObjectType): return self.name ``` +### Mutation.Input + +`Mutation.Input` is now deprecated in favor using `Mutation.Arguments` (`ClientIDMutation` still uses `Input`). + +Before: + +```python +class User(Mutation): + class Input: + name = String() +``` + +With 2.0: + +```python +class User(Mutation): + class Arguments: + name = String() +``` + ## Breaking Changes @@ -177,6 +198,25 @@ class Dog(ObjectType, interfaces=[Pet]): name = String() ``` + +### Abstract types + +Now you can create abstact types super easily, without the need of subclassing the meta. + +```python +class Base(ObjectType): + class Meta: + abstract = True + + id = ID() + + def resolve_id(self): + return "{type}_{id}".format( + type=self.__class__.__name__, + id=self.id + ) +``` + ### UUID Scalar In Graphene 2.0 there is a new dedicated scalar for UUIDs, `UUID`. diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 32194e9e..e44f8c15 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -6,6 +6,7 @@ from .field import Field from .objecttype import ObjectType, ObjectTypeOptions from .utils import yank_fields_from_attrs from ..utils.auto_resolver import auto_resolver +from ..utils.deprecated import warn_deprecation class MutationOptions(ObjectTypeOptions): @@ -40,7 +41,11 @@ class Mutation(ObjectType): if not input_class: input_class = getattr(cls, 'Input', None) if input_class: - print("WARNING: Please use Arguments for Mutation (Input is for ClientMutationID)") + 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" + ).format(name=cls.__name__)) if input_class: arguments = props(input_class) From 711a096ec6cdfb5872e247af96810b14ee54f9b0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 20:08:11 -0700 Subject: [PATCH 54/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index b4b671b7..60a77692 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -3,7 +3,7 @@ `ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations have been quite simplified, without the need to define a explicit Metaclass for each subtype. -It also improves the function resolvers, [simplifying the code](#resolve_only_args) the +It also improves the field resolvers, [simplifying the code](#resolve_only_args) the developer have to write to use them. Deprecations: From 719acc677197703657ab8fcaeb0ab6ef0f85ef25 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 20:08:30 -0700 Subject: [PATCH 55/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 60a77692..e7f48be4 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -9,7 +9,7 @@ developer have to write to use them. Deprecations: * [`AbstractType`](#abstracttype-deprecated) * [`resolve_only_args`](#resolve_only_args) -* [`Mutation.Input`](#mutation-input) +* [`Mutation.Input`](#mutationinput) Breaking changes: * [`Node Connections`](#node-connections) From f3bdd7de6901521bec7bd22e32743bf3c87fdb91 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 20:12:15 -0700 Subject: [PATCH 56/82] Improved upgrade example. Fixed #509 --- UPGRADE-v2.0.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index e7f48be4..15e0b486 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -143,10 +143,10 @@ Example. Before: ```python class UserInput(InputObjectType): - id = ID() + id = ID(required=True) -def is_user_id(id): - return id.startswith('userid_') +def is_valid_input(input): + return input.get('id').startswith('userid_') class Query(ObjectType): user = graphene.Field(User, input=UserInput()) @@ -154,7 +154,7 @@ class Query(ObjectType): @resolve_only_args def resolve_user(self, input): user_id = input.get('id') - if is_user_id(user_id): + if is_valid_input(user_id): return get_user(user_id) ``` @@ -162,17 +162,17 @@ With 2.0: ```python class UserInput(InputObjectType): - id = ID() + id = ID(required=True) @property - def is_user_id(self): + def is_valid(self): return self.id.startswith('userid_') class Query(ObjectType): user = graphene.Field(User, input=UserInput()) def resolve_user(self, input): - if input.is_user_id: + if input.is_valid: return get_user(input.id) ``` From 5c58d9e686e887abaf5f37d339637b90f9c04e6e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 22:32:37 -0700 Subject: [PATCH 57/82] Fixed lazy type instant resolved with NonNull. Fixed #494 --- graphene/types/structures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/types/structures.py b/graphene/types/structures.py index 38fa5609..dcd9d5e3 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -68,9 +68,9 @@ class NonNull(Structure): def __init__(self, *args, **kwargs): super(NonNull, self).__init__(*args, **kwargs) - assert not isinstance(self.of_type, NonNull), ( + assert not isinstance(self._of_type, NonNull), ( 'Can only create NonNull of a Nullable GraphQLType but got: {}.' - ).format(self.of_type) + ).format(self._of_type) def __str__(self): return '{}!'.format(self.of_type) From fa512cfa507eb19984ab0eaf929faef02705cbf8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 22:33:59 -0700 Subject: [PATCH 58/82] Improved mutation docs --- docs/relay/mutations.rst | 4 ++-- docs/types/mutations.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/relay/mutations.rst b/docs/relay/mutations.rst index 507d4a09..25c2fd54 100644 --- a/docs/relay/mutations.rst +++ b/docs/relay/mutations.rst @@ -22,8 +22,8 @@ subclass of ``relay.ClientIDMutation``. @classmethod def mutate_and_get_payload(cls, input, context, info): - ship_name = input.get('ship_name') - faction_id = input.get('faction_id') + ship_name = input.ship_name + faction_id = input.faction_id ship = create_ship(ship_name, faction_id) faction = get_faction(faction_id) return IntroduceShip(ship=ship, faction=faction) diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index 58b182ef..b722d41e 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -13,7 +13,7 @@ This example defines a Mutation: import graphene class CreatePerson(graphene.Mutation): - class Input: + class Arguments: name = graphene.String() ok = graphene.Boolean() @@ -98,7 +98,7 @@ To use an InputField you define an InputObjectType that specifies the structure age = graphene.Int(required=True) class CreatePerson(graphene.Mutation): - class Input: + class Arguments: person_data = PersonInput(required=True) person = graphene.Field(Person) From a56d5d9f77e29775430ade492a0df985bd79e1d5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 22:43:22 -0700 Subject: [PATCH 59/82] Added union docs. Fixed #493 --- docs/types/unions.rst | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/types/unions.rst diff --git a/docs/types/unions.rst b/docs/types/unions.rst new file mode 100644 index 00000000..f3d66e02 --- /dev/null +++ b/docs/types/unions.rst @@ -0,0 +1,63 @@ +Unions +====== + +Union types are very similar to interfaces, but they don't get +to specify any common fields between the types. + +The basics: + +- Each Union is a Python class that inherits from ``graphene.Union``. +- Unions don't have any fields on it, just links to the possible objecttypes. + +Quick example +------------- + +This example model defines a ``Character`` interface with a name. ``Human`` +and ``Droid`` are two implementations of that interface. + +.. code:: python + + import graphene + + class Human(graphene.ObjectType): + name = graphene.String() + born_in = graphene.String() + + class Droid(graphene.ObjectType): + name = graphene.String() + primary_function = graphene.String() + + class Starship(graphene.ObjectType): + name = graphene.String() + length = graphene.Int() + + class SearchResult(graphene.Union): + class Meta: + types = (Human, Droid, Starship) + + +Wherever we return a SearchResult type in our schema, we might get a Human, a Droid, or a Starship. +Note that members of a union type need to be concrete object types; +you can't create a union type out of interfaces or other unions. + +The above types have the following representation in a schema: + +.. code:: + + type Droid { + name: String + primaryFunction: String + } + + type Human { + name: String + bornIn: String + } + + type Ship { + name: String + length: Int + } + + union SearchResult = Human | Droid | Starship + From 66390554d9d5fed1ff90a18b3b250fc1d6943e35 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 26 Jul 2017 23:14:32 -0700 Subject: [PATCH 60/82] Improved resolver consistency --- graphene/__init__.py | 3 +++ graphene/relay/connection.py | 3 ++- graphene/relay/mutation.py | 2 +- graphene/relay/node.py | 13 +++++++++---- graphene/types/mutation.py | 3 +-- graphene/types/tests/test_mutation.py | 6 +++--- graphene/types/typemap.py | 16 +++++++--------- graphene/utils/tests/test_auto_resolver.py | 2 +- 8 files changed, 27 insertions(+), 21 deletions(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 5d6d41d7..aae2a3ae 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -49,6 +49,7 @@ if not __SETUP__: from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import from .utils.annotate import annotate + from .utils.auto_resolver import final_resolver, is_final_resolver __all__ = [ 'ObjectType', @@ -84,6 +85,8 @@ if not __SETUP__: 'annotate', 'Context', 'ResolveInfo', + 'final_resolver', + 'is_final_resolver', # Deprecated 'AbstractType', diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 5470e57e..5906ec06 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -10,6 +10,7 @@ from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, from ..types.field import Field from ..types.objecttype import ObjectType, ObjectTypeOptions from ..utils.deprecated import warn_deprecation +from ..utils.auto_resolver import final_resolver from .node import is_node @@ -142,7 +143,7 @@ class IterableConnectionField(Field): def get_resolver(self, parent_resolver): resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) - return partial(self.connection_resolver, resolver, self.type) + return final_resolver(partial(self.connection_resolver, resolver, self.type)) ConnectionField = IterableConnectionField diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 6414a533..84e1e87b 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -58,7 +58,7 @@ class ClientIDMutation(Mutation): ) @classmethod - @annotate(context=Context, info=ResolveInfo) + @annotate(context=Context, info=ResolveInfo, _trigger_warning=False) def mutate(cls, root, input, context, info): def on_resolve(payload): try: diff --git a/graphene/relay/node.py b/graphene/relay/node.py index ca31447d..8b89f8f3 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -3,9 +3,11 @@ from functools import partial from graphql_relay import from_global_id, to_global_id -from ..types import ID, Field, Interface, ObjectType +from ..types import ID, Field, Interface, ObjectType, Context, ResolveInfo from ..types.interface import InterfaceOptions from ..types.utils import get_type +from ..utils.annotate import annotate +from ..utils.auto_resolver import final_resolver def is_node(objecttype): @@ -35,7 +37,9 @@ class GlobalID(Field): return node.to_global_id(parent_type_name, type_id) # root._meta.name def get_resolver(self, parent_resolver): - return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name) + return final_resolver(partial( + self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name + )) class NodeField(Field): @@ -79,8 +83,9 @@ class Node(AbstractNode): return NodeField(cls, *args, **kwargs) @classmethod - def node_resolver(cls, root, args, context, info, only_type=None): - return cls.get_node_from_global_id(args.get('id'), context, info, only_type) + @annotate(context=Context, info=ResolveInfo, _trigger_warning=False) + def node_resolver(cls, root, id, context, info, only_type=None): + return cls.get_node_from_global_id(id, context, info, only_type) @classmethod def get_node_from_global_id(cls, global_id, context, info, only_type=None): diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index e44f8c15..213c0f48 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -5,7 +5,6 @@ from ..utils.props import props from .field import Field from .objecttype import ObjectType, ObjectTypeOptions from .utils import yank_fields_from_attrs -from ..utils.auto_resolver import auto_resolver from ..utils.deprecated import warn_deprecation @@ -63,7 +62,7 @@ class Mutation(ObjectType): _meta.fields = fields _meta.output = output - _meta.resolver = auto_resolver(resolver) + _meta.resolver = resolver _meta.arguments = arguments super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 79544af5..d195b6ac 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -18,7 +18,7 @@ def test_generate_mutation_no_args(): assert issubclass(MyMutation, ObjectType) assert MyMutation._meta.name == "MyMutation" assert MyMutation._meta.description == "Documentation" - resolved = MyMutation.Field().resolver(None, {'name': 'Peter'}, None, None) + resolved = MyMutation.Field().resolver(None, name='Peter') assert resolved == {'name': 'Peter'} @@ -34,7 +34,7 @@ def test_generate_mutation_with_meta(): assert MyMutation._meta.name == "MyOtherMutation" assert MyMutation._meta.description == "Documentation" - resolved = MyMutation.Field().resolver(None, {'name': 'Peter'}, None, None) + resolved = MyMutation.Field().resolver(None, name='Peter') assert resolved == {'name': 'Peter'} @@ -65,7 +65,7 @@ def test_mutation_custom_output_type(): field = CreateUser.Field() assert field.type == User assert field.args == {'name': Argument(String)} - resolved = field.resolver(None, {'name': 'Peter'}, None, None) + resolved = field.resolver(None, name='Peter') assert isinstance(resolved, User) assert resolved.name == 'Peter' diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 17ff95b1..36d8db4b 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -256,15 +256,13 @@ class TypeMap(GraphQLTypeMap): _field = GraphQLField( field_type, args=args, - resolver=field.get_resolver( - auto_resolver( - self.get_resolver_for_type( - type, - name, - field.default_value - ) - ) - ), + resolver=auto_resolver(field.get_resolver( + auto_resolver(self.get_resolver_for_type( + type, + name, + field.default_value + )) + )), deprecation_reason=field.deprecation_reason, description=field.description) field_name = field.name or self.get_name(name) diff --git a/graphene/utils/tests/test_auto_resolver.py b/graphene/utils/tests/test_auto_resolver.py index 9fa58c22..3b135d83 100644 --- a/graphene/utils/tests/test_auto_resolver.py +++ b/graphene/utils/tests/test_auto_resolver.py @@ -14,7 +14,7 @@ def resolver_annotated(root, **args): return root, args, None, None -@annotate(context=Context, info=ResolveInfo) +@annotate(context=Context, info=ResolveInfo, _trigger_warning=False) def resolver_with_context_and_info(root, context, info, **args): return root, args, context, info From 6ae9e51320fdea7ea7b13b38e192e56dbd92cc2e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Jul 2017 00:10:22 -0700 Subject: [PATCH 61/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 15e0b486..c3238254 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -3,7 +3,7 @@ `ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations have been quite simplified, without the need to define a explicit Metaclass for each subtype. -It also improves the field resolvers, [simplifying the code](#resolve_only_args) the +It also improves the field resolvers, [simplifying the code](#simpler-resolvers) the developer have to write to use them. Deprecations: @@ -24,6 +24,51 @@ New Features! ## Deprecations +### Simpler resolvers + +All the resolvers in graphene have been simplified. If before resolvers must had received +four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments +and `context` and `info` will only be passed if the function is annotated with it. + +Before: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +def resolve_my_field(self, args, context, info): + my_arg = args.get('my_arg') + return ... +``` + +With 2.0: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +def resolve_my_field(self, arg1, arg2): + return ... +``` + +And, if the resolver want to receive the context: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +def resolve_my_field(self, context: graphene.Context, my_arg): + return ... +``` + +which is equivalent in Python 2 to: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +@annotate(context=graphene.Context) +def resolve_my_field(self, context, my_arg): + return ... +``` + + ### AbstractType deprecated AbstractType is deprecated in graphene 2.0, you can now use normal inheritance instead. From 586ea56693070200f86a58ae5491def2bb500f1c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Jul 2017 00:10:50 -0700 Subject: [PATCH 62/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index c3238254..0f7e8209 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -45,7 +45,7 @@ With 2.0: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, arg1, arg2): +def resolve_my_field(self, my_arg): return ... ``` From d85a4e48748fc91ad803e939c65db83ee2492256 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Jul 2017 00:13:12 -0700 Subject: [PATCH 63/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 96 ++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 0f7e8209..ab8b997b 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -6,15 +6,16 @@ have been quite simplified, without the need to define a explicit Metaclass for It also improves the field resolvers, [simplifying the code](#simpler-resolvers) the developer have to write to use them. -Deprecations: +**Deprecations:** * [`AbstractType`](#abstracttype-deprecated) * [`resolve_only_args`](#resolve_only_args) * [`Mutation.Input`](#mutationinput) -Breaking changes: +**Breaking changes:** +* [`Simpler Resolvers`](#simpler-resolvers) * [`Node Connections`](#node-connections) -New Features! +**New Features!** * [`InputObjectType`](#inputobjecttype) * [`Meta as Class arguments`](#meta-ass-class-arguments) (_only available for Python 3_) @@ -24,51 +25,6 @@ New Features! ## Deprecations -### Simpler resolvers - -All the resolvers in graphene have been simplified. If before resolvers must had received -four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments -and `context` and `info` will only be passed if the function is annotated with it. - -Before: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -def resolve_my_field(self, args, context, info): - my_arg = args.get('my_arg') - return ... -``` - -With 2.0: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -def resolve_my_field(self, my_arg): - return ... -``` - -And, if the resolver want to receive the context: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -def resolve_my_field(self, context: graphene.Context, my_arg): - return ... -``` - -which is equivalent in Python 2 to: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -@annotate(context=graphene.Context) -def resolve_my_field(self, context, my_arg): - return ... -``` - - ### AbstractType deprecated AbstractType is deprecated in graphene 2.0, you can now use normal inheritance instead. @@ -141,6 +97,50 @@ class User(Mutation): ## Breaking Changes +### Simpler resolvers + +All the resolvers in graphene have been simplified. If before resolvers must had received +four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments +and `context` and `info` will only be passed if the function is annotated with it. + +Before: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +def resolve_my_field(self, args, context, info): + my_arg = args.get('my_arg') + return ... +``` + +With 2.0: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +def resolve_my_field(self, my_arg): + return ... +``` + +And, if the resolver want to receive the context: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +def resolve_my_field(self, context: graphene.Context, my_arg): + return ... +``` + +which is equivalent in Python 2 to: + +```python +my_field = graphene.String(my_arg=graphene.String()) + +@annotate(context=graphene.Context) +def resolve_my_field(self, context, my_arg): + return ... +``` + ### Node Connections Node types no longer have a `Connection` by default. From 394a1beb073703dad2b0ce933f6cd1df435849f4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Jul 2017 02:51:25 -0700 Subject: [PATCH 64/82] Updated resolver api --- UPGRADE-v2.0.md | 11 +++++ examples/complex_example.py | 5 +-- examples/context_example.py | 5 +-- examples/simple_example.py | 2 +- examples/starwars/schema.py | 8 ++-- examples/starwars_relay/schema.py | 16 +++---- graphene/__init__.py | 5 --- graphene/relay/connection.py | 7 ++- graphene/relay/mutation.py | 10 ++--- graphene/relay/node.py | 21 ++++----- graphene/relay/tests/test_connection_query.py | 6 +-- graphene/relay/tests/test_global_id.py | 4 +- graphene/relay/tests/test_mutation.py | 14 +++--- graphene/relay/tests/test_node_custom.py | 2 +- graphene/tests/issues/test_313.py | 2 +- graphene/tests/issues/test_490.py | 2 +- graphene/types/inputobjecttype.py | 44 ++++++++----------- graphene/types/interface.py | 6 +-- graphene/types/resolver.py | 4 +- graphene/types/tests/test_datetime.py | 4 +- graphene/types/tests/test_generic.py | 2 +- graphene/types/tests/test_json.py | 2 +- graphene/types/tests/test_mutation.py | 14 +++--- graphene/types/tests/test_query.py | 43 +++++++++--------- graphene/types/tests/test_resolver.py | 8 ++-- graphene/types/tests/test_typemap.py | 4 +- graphene/types/tests/test_uuid.py | 2 +- graphene/types/typemap.py | 23 +++++----- graphene/types/union.py | 2 +- graphene/utils/auto_resolver.py | 21 --------- graphene/utils/resolve_only_args.py | 21 +++------ graphene/utils/tests/test_auto_resolver.py | 36 --------------- .../tests/test_resolver_from_annotations.py | 44 ------------------- 33 files changed, 134 insertions(+), 266 deletions(-) delete mode 100644 graphene/utils/auto_resolver.py delete mode 100644 graphene/utils/tests/test_auto_resolver.py diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 0f7e8209..475ed78f 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -174,6 +174,17 @@ class Query(ObjectType): user_connection = relay.ConnectionField(UserConnection) ``` + +## Mutation.mutate + +Now only receive (`root`, `info`, `**args`) + + +## ClientIDMutation.mutate_and_get_payload + +Now only receive (`root`, `info`, `**input`) + + ## New Features ### InputObjectType diff --git a/examples/complex_example.py b/examples/complex_example.py index f6f80f81..492e6a19 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -11,10 +11,9 @@ class Address(graphene.ObjectType): class Query(graphene.ObjectType): - address = graphene.Field(Address, geo=GeoInput()) + address = graphene.Field(Address, geo=GeoInput(required=True)) - def resolve_address(self, args, context, info): - geo = args.get('geo') + def resolve_address(self, info, geo): return Address(latlng="({},{})".format(geo.get('lat'), geo.get('lng'))) diff --git a/examples/context_example.py b/examples/context_example.py index 2477a9bf..dfcf3548 100644 --- a/examples/context_example.py +++ b/examples/context_example.py @@ -9,9 +9,8 @@ class User(graphene.ObjectType): class Query(graphene.ObjectType): me = graphene.Field(User) - @graphene.annotate(context=graphene.Context) - def resolve_me(self, context): - return context['user'] + def resolve_me(self, info): + return info.context['user'] schema = graphene.Schema(query=Query) diff --git a/examples/simple_example.py b/examples/simple_example.py index 58fd07d1..927e0962 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -11,7 +11,7 @@ class Query(graphene.ObjectType): patron = graphene.Field(Patron) - def resolve_patron(self): + def resolve_patron(self, info): return Patron(id=1, name='Syrus', age=27) diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py index cc36c75f..a19a7b31 100644 --- a/examples/starwars/schema.py +++ b/examples/starwars/schema.py @@ -15,7 +15,7 @@ class Character(graphene.Interface): friends = graphene.List(lambda: Character) appears_in = graphene.List(Episode) - def resolve_friends(self): + def resolve_friends(self, info): # The character friends is a list of strings return [get_character(f) for f in self.friends] @@ -45,13 +45,13 @@ class Query(graphene.ObjectType): id=graphene.String() ) - def resolve_hero(self, episode=None): + def resolve_hero(self, info, episode=None): return get_hero(episode) - def resolve_human(self, id): + def resolve_human(self, info, id): return get_human(id) - def resolve_droid(self, id): + def resolve_droid(self, info, id): return get_droid(id) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index f2a6c6f3..2d004e42 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -1,5 +1,5 @@ import graphene -from graphene import annotate, relay, annotate +from graphene import relay from .data import create_ship, get_empire, get_faction, get_rebels, get_ship @@ -13,7 +13,7 @@ class Ship(graphene.ObjectType): name = graphene.String(description='The name of the ship.') @classmethod - def get_node(cls, id, context, info): + def get_node(cls, id, info): return get_ship(id) @@ -32,12 +32,12 @@ class Faction(graphene.ObjectType): name = graphene.String(description='The name of the faction.') ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.') - def resolve_ships(self, **args): + def resolve_ships(self, info, **args): # Transform the instance ship_ids into real instances return [get_ship(ship_id) for ship_id in self.ships] @classmethod - def get_node(cls, id, context, info): + def get_node(cls, id, info): return get_faction(id) @@ -51,9 +51,7 @@ class IntroduceShip(relay.ClientIDMutation): faction = graphene.Field(Faction) @classmethod - def mutate_and_get_payload(cls, input, context, info): - ship_name = input.get('ship_name') - faction_id = input.get('faction_id') + def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None): ship = create_ship(ship_name, faction_id) faction = get_faction(faction_id) return IntroduceShip(ship=ship, faction=faction) @@ -64,10 +62,10 @@ class Query(graphene.ObjectType): empire = graphene.Field(Faction) node = relay.Node.Field() - def resolve_rebels(self): + def resolve_rebels(self, info): return get_rebels() - def resolve_empire(self): + def resolve_empire(self, info): return get_empire() diff --git a/graphene/__init__.py b/graphene/__init__.py index aae2a3ae..72eae8e5 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -48,8 +48,6 @@ if not __SETUP__: ) from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import - from .utils.annotate import annotate - from .utils.auto_resolver import final_resolver, is_final_resolver __all__ = [ 'ObjectType', @@ -82,11 +80,8 @@ if not __SETUP__: 'ConnectionField', 'PageInfo', 'lazy_import', - 'annotate', 'Context', 'ResolveInfo', - 'final_resolver', - 'is_final_resolver', # Deprecated 'AbstractType', diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 5906ec06..e480f036 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -10,7 +10,6 @@ from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, from ..types.field import Field from ..types.objecttype import ObjectType, ObjectTypeOptions from ..utils.deprecated import warn_deprecation -from ..utils.auto_resolver import final_resolver from .node import is_node @@ -132,8 +131,8 @@ class IterableConnectionField(Field): return connection @classmethod - def connection_resolver(cls, resolver, connection_type, root, args, context, info): - resolved = resolver(root, args, context, info) + def connection_resolver(cls, resolver, connection_type, root, info, **args): + resolved = resolver(root, info, **args) on_resolve = partial(cls.resolve_connection, connection_type, args) if is_thenable(resolved): @@ -143,7 +142,7 @@ class IterableConnectionField(Field): def get_resolver(self, parent_resolver): resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) - return final_resolver(partial(self.connection_resolver, resolver, self.type)) + return partial(self.connection_resolver, resolver, self.type) ConnectionField = IterableConnectionField diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 84e1e87b..7daccbf5 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -3,9 +3,8 @@ from collections import OrderedDict from promise import Promise, is_thenable -from ..types import Field, InputObjectType, String, Context, ResolveInfo +from ..types import Field, InputObjectType, String from ..types.mutation import Mutation -from ..utils.annotate import annotate class ClientIDMutation(Mutation): @@ -58,18 +57,17 @@ class ClientIDMutation(Mutation): ) @classmethod - @annotate(context=Context, info=ResolveInfo, _trigger_warning=False) - def mutate(cls, root, input, context, info): + def mutate(cls, root, info, input): def on_resolve(payload): try: - payload.client_mutation_id = input.get('clientMutationId') + payload.client_mutation_id = input.get('client_mutation_id') except: raise Exception( ('Cannot set client_mutation_id in the payload object {}' ).format(repr(payload))) return payload - result = cls.mutate_and_get_payload(input, context, info) + result = cls.mutate_and_get_payload(root, info, **input) if is_thenable(result): return Promise.resolve(result).then(on_resolve) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 8b89f8f3..bc6a0870 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -3,11 +3,9 @@ from functools import partial from graphql_relay import from_global_id, to_global_id -from ..types import ID, Field, Interface, ObjectType, Context, ResolveInfo +from ..types import ID, Field, Interface, ObjectType from ..types.interface import InterfaceOptions from ..types.utils import get_type -from ..utils.annotate import annotate -from ..utils.auto_resolver import final_resolver def is_node(objecttype): @@ -31,15 +29,15 @@ class GlobalID(Field): self.parent_type_name = parent_type._meta.name if parent_type else None @staticmethod - def id_resolver(parent_resolver, node, root, args, context, info, parent_type_name=None): - type_id = parent_resolver(root, args, context, info) + def id_resolver(parent_resolver, node, root, info, parent_type_name=None, **args): + type_id = parent_resolver(root, info, **args) parent_type_name = parent_type_name or info.parent_type.name return node.to_global_id(parent_type_name, type_id) # root._meta.name def get_resolver(self, parent_resolver): - return final_resolver(partial( + return partial( self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name - )) + ) class NodeField(Field): @@ -83,12 +81,11 @@ class Node(AbstractNode): return NodeField(cls, *args, **kwargs) @classmethod - @annotate(context=Context, info=ResolveInfo, _trigger_warning=False) - def node_resolver(cls, root, id, context, info, only_type=None): - return cls.get_node_from_global_id(id, context, info, only_type) + def node_resolver(cls, root, info, id, only_type=None): + return cls.get_node_from_global_id(id, info, only_type) @classmethod - def get_node_from_global_id(cls, global_id, context, info, only_type=None): + def get_node_from_global_id(cls, global_id, info, only_type=None): try: _type, _id = cls.from_global_id(global_id) graphene_type = info.schema.get_type(_type).graphene_type @@ -106,7 +103,7 @@ class Node(AbstractNode): get_node = getattr(graphene_type, 'get_node', None) if get_node: - return get_node(_id, context, info) + return get_node(_id, info) @classmethod def from_global_id(cls, global_id): diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 370db36e..b8150e64 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -31,13 +31,13 @@ class Query(ObjectType): node = Node.Field() - def resolve_letters(self, **args): + def resolve_letters(self, info, **args): return list(letters.values()) - def resolve_promise_letters(self, **args): + def resolve_promise_letters(self, info, **args): return Promise.resolve(list(letters.values())) - def resolve_connection_letters(self, **args): + def resolve_connection_letters(self, info, **args): return LetterConnection( page_info=PageInfo( has_next_page=True, diff --git a/graphene/relay/tests/test_global_id.py b/graphene/relay/tests/test_global_id.py index 6e53bccc..6301f954 100644 --- a/graphene/relay/tests/test_global_id.py +++ b/graphene/relay/tests/test_global_id.py @@ -48,7 +48,7 @@ def test_global_id_defaults_to_info_parent_type(): my_id = '1' gid = GlobalID() id_resolver = gid.get_resolver(lambda *_: my_id) - my_global_id = id_resolver(None, None, None, Info(User)) + my_global_id = id_resolver(None, Info(User)) assert my_global_id == to_global_id(User._meta.name, my_id) @@ -56,5 +56,5 @@ def test_global_id_allows_setting_customer_parent_type(): my_id = '1' gid = GlobalID(parent_type=User) id_resolver = gid.get_resolver(lambda *_: my_id) - my_global_id = id_resolver(None, None, None, None) + my_global_id = id_resolver(None, None) assert my_global_id == to_global_id(User._meta.name, my_id) diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 32ff07b8..d37f8047 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -27,8 +27,7 @@ class SaySomething(ClientIDMutation): phrase = String() @staticmethod - def mutate_and_get_payload(args, context, info): - what = args.get('what') + def mutate_and_get_payload(self, info, what, client_mutation_id=None): return SaySomething(phrase=str(what)) @@ -40,8 +39,7 @@ class SaySomethingPromise(ClientIDMutation): phrase = String() @staticmethod - def mutate_and_get_payload(args, context, info): - what = args.get('what') + def mutate_and_get_payload(self, info, what, client_mutation_id=None): return Promise.resolve(SaySomething(phrase=str(what))) @@ -59,13 +57,11 @@ class OtherMutation(ClientIDMutation): name = String() my_node_edge = Field(MyEdge) - @classmethod - def mutate_and_get_payload(cls, args, context, info): - shared = args.get('shared', '') - additionalField = args.get('additionalField', '') + @staticmethod + def mutate_and_get_payload(self, info, shared='', additional_field='', client_mutation_id=None): edge_type = MyEdge return OtherMutation( - name=shared + additionalField, + name=shared + additional_field, my_node_edge=edge_type(cursor='1', node=MyNode(name='name'))) diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index ba34c401..273f89fd 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -15,7 +15,7 @@ class CustomNode(Node): return id @staticmethod - def get_node_from_global_id(id, context, info, only_type=None): + def get_node_from_global_id(id, info, only_type=None): assert info.schema == schema if id in user_data: return user_data.get(id) diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py index 3881590b..9df6c17b 100644 --- a/graphene/tests/issues/test_313.py +++ b/graphene/tests/issues/test_313.py @@ -29,7 +29,7 @@ class CreatePost(graphene.Mutation): result = graphene.Field(CreatePostResult) - def mutate(self, text): + def mutate(self, info, text): result = Success(yeah='yeah') return CreatePost(result=result) diff --git a/graphene/tests/issues/test_490.py b/graphene/tests/issues/test_490.py index f402e483..9bd00590 100644 --- a/graphene/tests/issues/test_490.py +++ b/graphene/tests/issues/test_490.py @@ -6,7 +6,7 @@ import graphene class Query(graphene.ObjectType): some_field = graphene.String(from_=graphene.String(name="from")) - def resolve_some_field(self, from_=None): + def resolve_some_field(self, info, from_=None): return from_ diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index cf66065f..3beb3ebf 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -11,7 +11,20 @@ class InputObjectTypeOptions(BaseOptions): create_container = None # type: Callable -class InputObjectType(dict, UnmountedType, BaseType): +class InputObjectTypeContainer(dict, BaseType): + class Meta: + abstract = True + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + for key, value in self.items(): + setattr(self, key, value) + + def __init_subclass__(cls, *args, **kwargs): + pass + + +class InputObjectType(UnmountedType, BaseType): ''' Input Object Type Definition @@ -20,30 +33,9 @@ class InputObjectType(dict, UnmountedType, BaseType): Using `NonNull` will ensure that a value must be provided by the query ''' - def __init__(self, *args, **kwargs): - as_container = kwargs.pop('_as_container', False) - if as_container: - # Is inited as container for the input args - self.__init_container__(*args, **kwargs) - else: - # Is inited as UnmountedType, e.g. - # - # class MyObjectType(graphene.ObjectType): - # my_input = MyInputType(required=True) - # - UnmountedType.__init__(self, *args, **kwargs) - - def __init_container__(self, *args, **kwargs): - dict.__init__(self, *args, **kwargs) - for key, value in self.items(): - setattr(self, key, value) @classmethod - def create_container(cls, data): - return cls(data, _as_container=True) - - @classmethod - def __init_subclass_with_meta__(cls, create_container=None, **options): + def __init_subclass_with_meta__(cls, container=None, **options): _meta = InputObjectTypeOptions(cls) fields = OrderedDict() @@ -53,9 +45,9 @@ class InputObjectType(dict, UnmountedType, BaseType): ) _meta.fields = fields - if create_container is None: - create_container = cls.create_container - _meta.create_container = create_container + if container is None: + container = type(cls.__name__, (InputObjectTypeContainer, cls), {}) + _meta.container = container super(InputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod diff --git a/graphene/types/interface.py b/graphene/types/interface.py index 054b7229..c98f0f1f 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -37,14 +37,10 @@ class Interface(BaseType): super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod - def resolve_type(cls, instance, context, info): + def resolve_type(cls, instance, info): from .objecttype import ObjectType if isinstance(instance, ObjectType): return type(instance) def __init__(self, *args, **kwargs): raise Exception("An Interface cannot be intitialized") - - @classmethod - def implements(cls, objecttype): - pass diff --git a/graphene/types/resolver.py b/graphene/types/resolver.py index 1f395b50..e5652c2d 100644 --- a/graphene/types/resolver.py +++ b/graphene/types/resolver.py @@ -1,8 +1,8 @@ -def attr_resolver(attname, default_value, root, args, context, info): +def attr_resolver(attname, default_value, root, info, **args): return getattr(root, attname, default_value) -def dict_resolver(attname, default_value, root, args, context, info): +def dict_resolver(attname, default_value, root, info, **args): return root.get(attname, default_value) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index 660ae091..9d23fee5 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -11,10 +11,10 @@ class Query(ObjectType): datetime = DateTime(_in=DateTime(name='in')) time = Time(_at=Time(name='at')) - def resolve_datetime(self, _in=None): + def resolve_datetime(self, info, _in=None): return _in - def resolve_time(self, _at=None): + def resolve_time(self, info, _at=None): return _at diff --git a/graphene/types/tests/test_generic.py b/graphene/types/tests/test_generic.py index 3a5eb88a..be832763 100644 --- a/graphene/types/tests/test_generic.py +++ b/graphene/types/tests/test_generic.py @@ -6,7 +6,7 @@ from ..schema import Schema class Query(ObjectType): generic = GenericScalar(input=GenericScalar()) - def resolve_generic(self, input=None): + def resolve_generic(self, info, input=None): return input diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index d671722d..cadc729f 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -7,7 +7,7 @@ from ..schema import Schema class Query(ObjectType): json = JSONString(input=JSONString()) - def resolve_json(self, input): + def resolve_json(self, info, input): return input schema = Schema(query=Query) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index d195b6ac..b4d65dcc 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -12,13 +12,13 @@ def test_generate_mutation_no_args(): class MyMutation(Mutation): '''Documentation''' - def mutate(self, **args): + def mutate(self, info, **args): return args assert issubclass(MyMutation, ObjectType) assert MyMutation._meta.name == "MyMutation" assert MyMutation._meta.description == "Documentation" - resolved = MyMutation.Field().resolver(None, name='Peter') + resolved = MyMutation.Field().resolver(None, None, name='Peter') assert resolved == {'name': 'Peter'} @@ -29,12 +29,12 @@ def test_generate_mutation_with_meta(): name = 'MyOtherMutation' description = 'Documentation' - def mutate(self, **args): + def mutate(self, info, **args): return args assert MyMutation._meta.name == "MyOtherMutation" assert MyMutation._meta.description == "Documentation" - resolved = MyMutation.Field().resolver(None, name='Peter') + resolved = MyMutation.Field().resolver(None, None, name='Peter') assert resolved == {'name': 'Peter'} @@ -59,13 +59,13 @@ def test_mutation_custom_output_type(): Output = User - def mutate(self, name): + def mutate(self, info, name): return User(name=name) field = CreateUser.Field() assert field.type == User assert field.args == {'name': Argument(String)} - resolved = field.resolver(None, name='Peter') + resolved = field.resolver(None, None, name='Peter') assert isinstance(resolved, User) assert resolved.name == 'Peter' @@ -81,7 +81,7 @@ def test_mutation_execution(): name = String() dynamic = Dynamic(lambda: String()) - def mutate(self, name, dynamic): + def mutate(self, info, name, dynamic): return CreateUser(name=name, dynamic=dynamic) class Query(ObjectType): diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index f41f40bc..5cebcfc5 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -14,7 +14,6 @@ from ..schema import Schema from ..structures import List from ..union import Union from ..context import Context -from ...utils.annotate import annotate def test_query(): @@ -39,14 +38,14 @@ def test_query_union(): one = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, one_object) class Two(ObjectType): two = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, two_object) class MyUnion(Union): @@ -57,7 +56,7 @@ def test_query_union(): class Query(ObjectType): unions = List(MyUnion) - def resolve_unions(self): + def resolve_unions(self, info): return [one_object(), two_object()] hello_schema = Schema(Query) @@ -91,7 +90,7 @@ def test_query_interface(): one = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, one_object) class Two(ObjectType): @@ -102,13 +101,13 @@ def test_query_interface(): two = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, two_object) class Query(ObjectType): interfaces = List(MyInterface) - def resolve_interfaces(self): + def resolve_interfaces(self, info): return [one_object(), two_object()] hello_schema = Schema(Query, types=[One, Two]) @@ -156,7 +155,7 @@ def test_query_wrong_default_value(): field = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, MyType) class Query(ObjectType): @@ -188,7 +187,7 @@ def test_query_resolve_function(): class Query(ObjectType): hello = String() - def resolve_hello(self): + def resolve_hello(self, info): return 'World' hello_schema = Schema(Query) @@ -202,7 +201,7 @@ def test_query_arguments(): class Query(ObjectType): test = String(a_str=String(), a_int=Int()) - def resolve_test(self, **args): + def resolve_test(self, info, **args): return json.dumps([self, args], separators=(',', ':')) test_schema = Schema(Query) @@ -231,7 +230,7 @@ def test_query_input_field(): class Query(ObjectType): test = String(a_input=Input()) - def resolve_test(self, **args): + def resolve_test(self, info, **args): return json.dumps([self, args], separators=(',', ':')) test_schema = Schema(Query) @@ -254,10 +253,10 @@ def test_query_middlewares(): hello = String() other = String() - def resolve_hello(self): + def resolve_hello(self, info): return 'World' - def resolve_other(self): + def resolve_other(self, info): return 'other' def reversed_middleware(next, *args, **kwargs): @@ -280,14 +279,14 @@ def test_objecttype_on_instances(): class ShipType(ObjectType): name = String(description="Ship name", required=True) - def resolve_name(self): + def resolve_name(self, info): # Here self will be the Ship instance returned in resolve_ship return self.name class Query(ObjectType): ship = Field(ShipType) - def resolve_ship(self): + def resolve_ship(self, info): return Ship(name='xwing') schema = Schema(query=Query) @@ -302,7 +301,7 @@ def test_big_list_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self): + def resolve_all_ints(self, info): return big_list hello_schema = Schema(Query) @@ -319,7 +318,7 @@ def test_big_list_query_compiled_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self): + def resolve_all_ints(self, info): return big_list hello_schema = Schema(Query) @@ -420,15 +419,13 @@ def test_query_annotated_resolvers(): context = String() info = String() - def resolve_annotated(self, id): + def resolve_annotated(self, info, id): return "{}-{}".format(self, id) - @annotate(context=Context, _trigger_warning=False) - def resolve_context(self, context): - assert isinstance(context, Context) - return "{}-{}".format(self, context.key) + def resolve_context(self, info): + assert isinstance(info.context, Context) + return "{}-{}".format(self, info.context.key) - @annotate(info=ResolveInfo, _trigger_warning=False) def resolve_info(self, info): assert isinstance(info, ResolveInfo) return "{}-{}".format(self, info.field_name) diff --git a/graphene/types/tests/test_resolver.py b/graphene/types/tests/test_resolver.py index 64fdf94e..2beb607e 100644 --- a/graphene/types/tests/test_resolver.py +++ b/graphene/types/tests/test_resolver.py @@ -16,22 +16,22 @@ class demo_obj(object): def test_attr_resolver(): - resolved = attr_resolver('attr', None, demo_obj, args, context, info) + resolved = attr_resolver('attr', None, demo_obj, info, **args) assert resolved == 'value' def test_attr_resolver_default_value(): - resolved = attr_resolver('attr2', 'default', demo_obj, args, context, info) + resolved = attr_resolver('attr2', 'default', demo_obj, info, **args) assert resolved == 'default' def test_dict_resolver(): - resolved = dict_resolver('attr', None, demo_dict, args, context, info) + resolved = dict_resolver('attr', None, demo_dict, info, **args) assert resolved == 'value' def test_dict_resolver_default_value(): - resolved = dict_resolver('attr2', 'default', demo_dict, args, context, info) + resolved = dict_resolver('attr2', 'default', demo_dict, info, **args) assert resolved == 'default' diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 266173cf..082f25bd 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -204,5 +204,5 @@ def test_objecttype_with_possible_types(): typemap = TypeMap([MyObjectType]) graphql_type = typemap['MyObjectType'] assert graphql_type.is_type_of - assert graphql_type.is_type_of({}, None, None) is True - assert graphql_type.is_type_of(MyObjectType(), None, None) is False + assert graphql_type.is_type_of({}, None) is True + assert graphql_type.is_type_of(MyObjectType(), None) is False diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py index 23eac6b4..c1419750 100644 --- a/graphene/types/tests/test_uuid.py +++ b/graphene/types/tests/test_uuid.py @@ -6,7 +6,7 @@ from ..schema import Schema class Query(ObjectType): uuid = UUID(input=UUID()) - def resolve_uuid(self, input): + def resolve_uuid(self, info, input): return input schema = Schema(query=Query) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 36d8db4b..3b389cd6 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -11,7 +11,6 @@ from graphql.type.typemap import GraphQLTypeMap from ..utils.get_unbound_function import get_unbound_function from ..utils.str_converters import to_camel_case -from ..utils.auto_resolver import auto_resolver, final_resolver from .definitions import (GrapheneEnumType, GrapheneGraphQLType, GrapheneInputObjectType, GrapheneInterfaceType, GrapheneObjectType, GrapheneScalarType, @@ -38,12 +37,12 @@ def is_graphene_type(_type): return True -def resolve_type(resolve_type_func, map, type_name, root, context, info): - _type = resolve_type_func(root, context, info) +def resolve_type(resolve_type_func, map, type_name, root, info): + _type = resolve_type_func(root, info) if not _type: return_type = map[type_name] - return get_default_resolve_type_fn(root, context, info, return_type) + return get_default_resolve_type_fn(root, info, return_type) if inspect.isclass(_type) and issubclass(_type, ObjectType): graphql_type = map.get(_type._meta.name) @@ -55,7 +54,7 @@ def resolve_type(resolve_type_func, map, type_name, root, context, info): return _type -def is_type_of_from_possible_types(possible_types, root, context, info): +def is_type_of_from_possible_types(possible_types, root, info): return isinstance(root, possible_types) @@ -196,7 +195,7 @@ class TypeMap(GraphQLTypeMap): graphene_type=type, name=type._meta.name, description=type._meta.description, - container_type=type._meta.create_container, + container_type=type._meta.container, fields=partial( self.construct_fields_for_type, map, type, is_input_type=True), ) @@ -240,7 +239,7 @@ class TypeMap(GraphQLTypeMap): _field = GraphQLInputObjectField( field_type, default_value=field.default_value, - out_name=field.name or name, + out_name=name, description=field.description) else: args = OrderedDict() @@ -256,13 +255,13 @@ class TypeMap(GraphQLTypeMap): _field = GraphQLField( field_type, args=args, - resolver=auto_resolver(field.get_resolver( - auto_resolver(self.get_resolver_for_type( + resolver=field.get_resolver( + self.get_resolver_for_type( type, name, field.default_value - )) - )), + ) + ), deprecation_reason=field.deprecation_reason, description=field.description) field_name = field.name or self.get_name(name) @@ -292,7 +291,7 @@ class TypeMap(GraphQLTypeMap): default_resolver = type._meta.default_resolver or get_default_resolver( ) - return final_resolver(partial(default_resolver, name, default_value)) + return partial(default_resolver, name, default_value) def get_field_type(self, map, type): if isinstance(type, List): diff --git a/graphene/types/union.py b/graphene/types/union.py index f5c12c04..f9797fc0 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -34,7 +34,7 @@ class Union(UnmountedType, BaseType): return cls @classmethod - def resolve_type(cls, instance, context, info): + def resolve_type(cls, instance, info): from .objecttype import ObjectType if isinstance(instance, ObjectType): return type(instance) diff --git a/graphene/utils/auto_resolver.py b/graphene/utils/auto_resolver.py deleted file mode 100644 index 72f33515..00000000 --- a/graphene/utils/auto_resolver.py +++ /dev/null @@ -1,21 +0,0 @@ -from .resolver_from_annotations import resolver_from_annotations - - -def final_resolver(func): - func._is_final_resolver = True - return func - - -def auto_resolver(func=None): - if not func: - return - - if not is_final_resolver(func): - # Is a Graphene 2.0 resolver function - return final_resolver(resolver_from_annotations(func)) - else: - return func - - -def is_final_resolver(func): - return getattr(func, '_is_final_resolver', False) diff --git a/graphene/utils/resolve_only_args.py b/graphene/utils/resolve_only_args.py index 0856ffcc..897e6223 100644 --- a/graphene/utils/resolve_only_args.py +++ b/graphene/utils/resolve_only_args.py @@ -1,18 +1,11 @@ -from six import PY2 -from .annotate import annotate +from functools import wraps from .deprecated import deprecated -if PY2: - deprecation_reason = ( - 'Please use @annotate instead.' - ) -else: - deprecation_reason = ( - 'Please use Python 3 type annotations instead. Read more: ' - 'https://docs.python.org/3/library/typing.html' - ) - -@deprecated(deprecation_reason) +@deprecated('This function is deprecated') def resolve_only_args(func): - return annotate(func) + @wraps(func) + def wrapped_func(root, info, **args): + return func(root, **args) + + return wrapped_func diff --git a/graphene/utils/tests/test_auto_resolver.py b/graphene/utils/tests/test_auto_resolver.py deleted file mode 100644 index 3b135d83..00000000 --- a/graphene/utils/tests/test_auto_resolver.py +++ /dev/null @@ -1,36 +0,0 @@ -import pytest -from ..annotate import annotate -from ..auto_resolver import auto_resolver, final_resolver - -from ...types import Context, ResolveInfo - - -@final_resolver -def resolver(root, args, context, info): - return root, args, context, info - - -def resolver_annotated(root, **args): - return root, args, None, None - - -@annotate(context=Context, info=ResolveInfo, _trigger_warning=False) -def resolver_with_context_and_info(root, context, info, **args): - return root, args, context, info - - -def test_auto_resolver_non_annotated(): - decorated_resolver = auto_resolver(resolver) - # We make sure the function is not wrapped - assert decorated_resolver == resolver - assert decorated_resolver(1, {}, 2, 3) == (1, {}, 2, 3) - - -def test_auto_resolver_annotated(): - decorated_resolver = auto_resolver(resolver_annotated) - assert decorated_resolver(1, {}, 2, 3) == (1, {}, None, None) - - -def test_auto_resolver_annotated_with_context_and_info(): - decorated_resolver = auto_resolver(resolver_with_context_and_info) - assert decorated_resolver(1, {}, 2, 3) == (1, {}, 2, 3) diff --git a/graphene/utils/tests/test_resolver_from_annotations.py b/graphene/utils/tests/test_resolver_from_annotations.py index 226b387c..e69de29b 100644 --- a/graphene/utils/tests/test_resolver_from_annotations.py +++ b/graphene/utils/tests/test_resolver_from_annotations.py @@ -1,44 +0,0 @@ -import pytest -from ..annotate import annotate -from ..resolver_from_annotations import resolver_from_annotations - -from ...types import Context, ResolveInfo - - -@annotate -def func(root, **args): - return root, args, None, None - - -@annotate(context=Context) -def func_with_context(root, context, **args): - return root, args, context, None - - -@annotate(info=ResolveInfo) -def func_with_info(root, info, **args): - return root, args, None, info - - -@annotate(context=Context, info=ResolveInfo) -def func_with_context_and_info(root, context, info, **args): - return root, args, context, info - -root = 1 -args = { - 'arg': 0 -} -context = 2 -info = 3 - - -@pytest.mark.parametrize("func,expected", [ - (func, (1, {'arg': 0}, None, None)), - (func_with_context, (1, {'arg': 0}, 2, None)), - (func_with_info, (1, {'arg': 0}, None, 3)), - (func_with_context_and_info, (1, {'arg': 0}, 2, 3)), -]) -def test_resolver_from_annotations(func, expected): - resolver_func = resolver_from_annotations(func) - resolved = resolver_func(root, args, context, info) - assert resolved == expected From 0002d42e38833e45934970a7e5640d64b9dc6a4a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Jul 2017 03:00:21 -0700 Subject: [PATCH 65/82] Updated docs --- README.md | 2 +- README.rst | 2 +- UPGRADE-v2.0.md | 26 ++++++++------------------ docs/execution/dataloader.rst | 4 ++-- docs/execution/execute.rst | 9 ++++----- docs/execution/middleware.rst | 4 ++-- docs/quickstart.rst | 2 +- docs/relay/connection.rst | 2 +- docs/types/objecttypes.rst | 6 +++--- 9 files changed, 23 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 9635a190..80c3d9c1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self): + def resolve_hello(self, info): return 'World' schema = graphene.Schema(query=Query) diff --git a/README.rst b/README.rst index b31335f2..bea6c4d4 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self): + def resolve_hello(self, info): return 'World' schema = graphene.Schema(query=Query) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 475ed78f..a03a5e76 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -45,26 +45,17 @@ With 2.0: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, my_arg): +def resolve_my_field(self, info, my_arg): return ... ``` -And, if the resolver want to receive the context: +And, if the resolver want to get the context: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, context: graphene.Context, my_arg): - return ... -``` - -which is equivalent in Python 2 to: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -@annotate(context=graphene.Context) -def resolve_my_field(self, context, my_arg): +def resolve_my_field(self, info, my_arg): + context = info.context return ... ``` @@ -95,7 +86,7 @@ class Pet(CommonFields, Interface): ### resolve\_only\_args -`resolve_only_args` is now deprecated in favor of type annotations (using the polyfill `@graphene.annotate` in Python 2 in case is necessary for accessing `context` or `info`). +`resolve_only_args` is now deprecated as the resolver API has been simplified. Before: @@ -114,7 +105,7 @@ With 2.0: class User(ObjectType): name = String() - def resolve_name(self): + def resolve_name(self, info): return self.name ``` @@ -227,10 +218,9 @@ class UserInput(InputObjectType): class Query(ObjectType): user = graphene.Field(User, input=UserInput()) - def resolve_user(self, input): + def resolve_user(self, info, id): if input.is_valid: return get_user(input.id) - ``` @@ -266,7 +256,7 @@ class Base(ObjectType): id = ID() - def resolve_id(self): + def resolve_id(self, info): return "{type}_{id}".format( type=self.__class__.__name__, id=self.id diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 17374534..3322acfd 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -99,8 +99,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac best_friend = graphene.Field(lambda: User) friends = graphene.List(lambda: User) - def resolve_best_friend(self): + def resolve_best_friend(self, info): return user_loader.load(self.best_friend_id) - def resolve_friends(self): + def resolve_friends(self, info): return user_loader.load_many(self.friend_ids) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index 50e54e14..327ce230 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -24,9 +24,8 @@ You can pass context to a query via ``context_value``. class Query(graphene.ObjectType): name = graphene.String() - @graphene.annotate(context=graphene.Context) - def resolve_name(self, context): - return context.get('name') + def resolve_name(self, info): + return info.context.get('name') schema = graphene.Schema(Query) result = schema.execute('{ name }', context_value={'name': 'Syrus'}) @@ -44,8 +43,8 @@ You can pass variables to a query via ``variable_values``. class Query(graphene.ObjectType): user = graphene.Field(User) - def resolve_user(self, args, context, info): - return context.get('user') + def resolve_user(self, info): + return info.context.get('user') schema = graphene.Schema(Query) result = schema.execute( diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 3303ed41..c5e11aa7 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -31,10 +31,10 @@ This middleware only continues evaluation if the ``field_name`` is not ``'user'` .. code:: python class AuthorizationMiddleware(object): - def resolve(self, next, root, args, context, info): + def resolve(self, next, root, info, **args): if info.field_name == 'user': return None - return next(root, args, context, info) + return next(root, info, **args) And then execute it with: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4192fada..dde79c3b 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -39,7 +39,7 @@ one field: ``hello`` and an input name. And when we query it, it should return ` class Query(graphene.ObjectType): hello = graphene.String(name=graphene.String(default_value="stranger")) - def resolve_hello(self, name): + def resolve_hello(self, info, name): return 'Hello ' + name schema = graphene.Schema(query=Query) diff --git a/docs/relay/connection.rst b/docs/relay/connection.rst index 898d627d..c2379cbc 100644 --- a/docs/relay/connection.rst +++ b/docs/relay/connection.rst @@ -41,5 +41,5 @@ that implements ``Node`` will have a default Connection. name = graphene.String() ships = relay.ConnectionField(ShipConnection) - def resolve_ships(self): + def resolve_ships(self, info): return [] diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 409db58c..091617ce 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -25,7 +25,7 @@ This example model defines a Person, with a first and a last name: last_name = graphene.String() full_name = graphene.String() - def resolve_full_name(self): + def resolve_full_name(self, info): return '{} {}'.format(self.first_name, self.last_name) **first\_name** and **last\_name** are fields of the ObjectType. Each @@ -71,7 +71,7 @@ method in the class. class Query(graphene.ObjectType): reverse = graphene.String(word=graphene.String()) - def resolve_reverse(self, word): + def resolve_reverse(self, info, word): return word[::-1] Resolvers outside the class @@ -83,7 +83,7 @@ A field can use a custom resolver from outside the class: import graphene - def reverse(root, word): + def reverse(root, info, word): return word[::-1] class Query(graphene.ObjectType): From 6a85507325a078c354e76ce104d786d87038f0e0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Jul 2017 20:06:48 -0700 Subject: [PATCH 66/82] Improved get_node API --- UPGRADE-v2.0.md | 31 +++++++++++++++++++++--- docs/relay/mutations.rst | 4 +-- docs/relay/nodes.rst | 4 +-- examples/starwars_relay/schema.py | 4 +-- graphene/relay/node.py | 10 ++++---- graphene/relay/tests/test_node.py | 4 +-- graphene/relay/tests/test_node_custom.py | 2 +- 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index a03a5e76..0e961bc3 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -165,15 +165,40 @@ class Query(ObjectType): user_connection = relay.ConnectionField(UserConnection) ``` +## Node.get_node + +The method `get_node` in `ObjectTypes` that have `Node` as interface, changes it's api. +From `def get_node(cls, id, context, info)` to `def get_node(cls, info, id)`. + +```python +class MyObject(ObjectType): + class Meta: + interfaces = (Node, ) + + @classmethod + def get_node(cls, id, context, info): + return ... +``` + +To: +```python +class MyObject(ObjectType): + class Meta: + interfaces = (Node, ) + + @classmethod + def get_node(cls, info, id): + return ... +``` ## Mutation.mutate -Now only receive (`root`, `info`, `**args`) +Now only receives (`root`, `info`, `**args`) ## ClientIDMutation.mutate_and_get_payload -Now only receive (`root`, `info`, `**input`) +Now only receives (`root`, `info`, `**input`) ## New Features @@ -218,7 +243,7 @@ class UserInput(InputObjectType): class Query(ObjectType): user = graphene.Field(User, input=UserInput()) - def resolve_user(self, info, id): + def resolve_user(self, info, input): if input.is_valid: return get_user(input.id) ``` diff --git a/docs/relay/mutations.rst b/docs/relay/mutations.rst index 25c2fd54..89bf89b3 100644 --- a/docs/relay/mutations.rst +++ b/docs/relay/mutations.rst @@ -21,7 +21,7 @@ subclass of ``relay.ClientIDMutation``. faction = graphene.Field(Faction) @classmethod - def mutate_and_get_payload(cls, input, context, info): + def mutate_and_get_payload(cls, root, info, **input): ship_name = input.ship_name faction_id = input.faction_id ship = create_ship(ship_name, faction_id) @@ -46,7 +46,7 @@ Mutations can also accept files, that's how it will work with different integrat success = graphene.String() @classmethod - def mutate_and_get_payload(cls, input, context, info): + def mutate_and_get_payload(cls, root, info, **input): # When using it in Django, context will be the request files = context.FILES # Or, if used in Flask, context will be the flask global request diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index 5f470055..74f42094 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -22,7 +22,7 @@ Example usage (taken from the `Starwars Relay example`_): name = graphene.String(description='The name of the ship.') @classmethod - def get_node(cls, id, context, info): + def get_node(cls, info, id): return get_ship(id) The ``id`` returned by the ``Ship`` type when you query it will be a @@ -55,7 +55,7 @@ Example of a custom node: return '{}:{}'.format(type, id) @staticmethod - def get_node_from_global_id(global_id, context, info, only_type=None): + def get_node_from_global_id(info global_id, only_type=None): type, id = global_id.split(':') if only_node: # We assure that the node type that we want to retrieve diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 2d004e42..beb291c3 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -13,7 +13,7 @@ class Ship(graphene.ObjectType): name = graphene.String(description='The name of the ship.') @classmethod - def get_node(cls, id, info): + def get_node(cls, info, id): return get_ship(id) @@ -37,7 +37,7 @@ class Faction(graphene.ObjectType): return [get_ship(ship_id) for ship_id in self.ships] @classmethod - def get_node(cls, id, info): + def get_node(cls, info, id): return get_faction(id) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index bc6a0870..d762a110 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -56,7 +56,7 @@ class NodeField(Field): ) def get_resolver(self, parent_resolver): - return partial(self.node_type.node_resolver, only_type=get_type(self.field_type)) + return partial(self.node_type.node_resolver, get_type(self.field_type)) class AbstractNode(Interface): @@ -81,11 +81,11 @@ class Node(AbstractNode): return NodeField(cls, *args, **kwargs) @classmethod - def node_resolver(cls, root, info, id, only_type=None): - return cls.get_node_from_global_id(id, info, only_type) + def node_resolver(cls, only_type, root, info, id): + return cls.get_node_from_global_id(info, id, only_type=only_type) @classmethod - def get_node_from_global_id(cls, global_id, info, only_type=None): + def get_node_from_global_id(cls, info, global_id, only_type=None): try: _type, _id = cls.from_global_id(global_id) graphene_type = info.schema.get_type(_type).graphene_type @@ -103,7 +103,7 @@ class Node(AbstractNode): get_node = getattr(graphene_type, 'get_node', None) if get_node: - return get_node(_id, info) + return get_node(info, _id) @classmethod def from_global_id(cls, global_id): diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index e0f65cdd..315f2e39 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -22,7 +22,7 @@ class MyNode(ObjectType): name = String() @staticmethod - def get_node(id, *_): + def get_node(info, id): return MyNode(name=str(id)) @@ -36,7 +36,7 @@ class MyOtherNode(SharedNodeFields, ObjectType): return 'extra field info.' @staticmethod - def get_node(id, *_): + def get_node(info, id): return MyOtherNode(shared=str(id)) diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index 273f89fd..cc4e910c 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -15,7 +15,7 @@ class CustomNode(Node): return id @staticmethod - def get_node_from_global_id(id, info, only_type=None): + def get_node_from_global_id(info, id, only_type=None): assert info.schema == schema if id in user_data: return user_data.get(id) From 602d8866bbc45f5b8be49ef0eda218007a85a6fe Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 31 Jul 2017 22:08:05 -0700 Subject: [PATCH 67/82] Updated graphql-core version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3bb6225f..3723915d 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.2.dev', + 'graphql-core>=2.0.dev', 'graphql-relay>=0.4.5', 'promise>=2.1.dev', ], From 7f1c2a654bd6b183d5c5f45fa62d1cf4e068837d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 31 Jul 2017 22:15:26 -0700 Subject: [PATCH 68/82] Fixed benchmark tests --- graphene/types/tests/test_query.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 5cebcfc5..acaef0a1 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -340,7 +340,7 @@ def test_big_list_of_containers_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self): + def resolve_all_containers(self, info): return big_container_list hello_schema = Schema(Query) @@ -363,7 +363,7 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self): + def resolve_all_containers(self, info): return big_container_list hello_schema = Schema(Query) @@ -381,16 +381,16 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark z = Int() o = Int() - def resolve_x(self): + def resolve_x(self, info): return self.x - def resolve_y(self): + def resolve_y(self, info): return self.y - def resolve_z(self): + def resolve_z(self, info): return self.z - def resolve_o(self): + def resolve_o(self, info): return self.o big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(1000)] @@ -398,7 +398,7 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self): + def resolve_all_containers(self, info): return big_container_list hello_schema = Schema(Query) From 81018268aa337850ff8c1e0c0cbbd79a02cd00ee Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 31 Jul 2017 22:30:13 -0700 Subject: [PATCH 69/82] Added support for wheel distributions. Fixed #505 --- .travis.yml | 1 + graphene/__init__.py | 148 ++++++++++++++++++++----------------------- setup.cfg | 3 + setup.py | 33 +++++----- 4 files changed, 91 insertions(+), 94 deletions(-) diff --git a/.travis.yml b/.travis.yml index d87939e0..239df23d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,3 +58,4 @@ deploy: tags: true password: secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ= + distributions: "sdist bdist_wheel" diff --git a/graphene/__init__.py b/graphene/__init__.py index 72eae8e5..df0b96f1 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -1,88 +1,78 @@ from .pyutils.version import get_version - -try: - # This variable is injected in the __builtins__ by the build - # process. It used to enable importing subpackages when - # the required packages are not installed - __SETUP__ -except NameError: - __SETUP__ = False +from .types import ( + AbstractType, + ObjectType, + InputObjectType, + Interface, + Mutation, + Field, + InputField, + Schema, + Scalar, + String, ID, Int, Float, Boolean, + JSONString, + UUID, + List, NonNull, + Enum, + Argument, + Dynamic, + Union, + Context, + ResolveInfo +) +from .relay import ( + Node, + is_node, + GlobalID, + ClientIDMutation, + Connection, + ConnectionField, + PageInfo +) +from .utils.resolve_only_args import resolve_only_args +from .utils.module_loading import lazy_import VERSION = (2, 0, 0, 'alpha', 0) __version__ = get_version(VERSION) -if not __SETUP__: +__all__ = [ + '__version__', + 'ObjectType', + 'InputObjectType', + 'Interface', + 'Mutation', + 'Field', + 'InputField', + 'Schema', + 'Scalar', + 'String', + 'ID', + 'Int', + 'Float', + 'Enum', + 'Boolean', + 'JSONString', + 'UUID', + 'List', + 'NonNull', + 'Argument', + 'Dynamic', + 'Union', + 'resolve_only_args', + 'Node', + 'is_node', + 'GlobalID', + 'ClientIDMutation', + 'Connection', + 'ConnectionField', + 'PageInfo', + 'lazy_import', + 'Context', + 'ResolveInfo', - from .types import ( - AbstractType, - ObjectType, - InputObjectType, - Interface, - Mutation, - Field, - InputField, - Schema, - Scalar, - String, ID, Int, Float, Boolean, - JSONString, - UUID, - List, NonNull, - Enum, - Argument, - Dynamic, - Union, - Context, - ResolveInfo - ) - from .relay import ( - Node, - is_node, - GlobalID, - ClientIDMutation, - Connection, - ConnectionField, - PageInfo - ) - from .utils.resolve_only_args import resolve_only_args - from .utils.module_loading import lazy_import - - __all__ = [ - 'ObjectType', - 'InputObjectType', - 'Interface', - 'Mutation', - 'Field', - 'InputField', - 'Schema', - 'Scalar', - 'String', - 'ID', - 'Int', - 'Float', - 'Enum', - 'Boolean', - 'JSONString', - 'UUID', - 'List', - 'NonNull', - 'Argument', - 'Dynamic', - 'Union', - 'resolve_only_args', - 'Node', - 'is_node', - 'GlobalID', - 'ClientIDMutation', - 'Connection', - 'ConnectionField', - 'PageInfo', - 'lazy_import', - 'Context', - 'ResolveInfo', - - # Deprecated - 'AbstractType', - ] + # Deprecated + 'AbstractType', +] diff --git a/setup.cfg b/setup.cfg index efe77690..2037bc1b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,6 @@ omit = graphene/pyutils/*,*/tests/*,graphene/types/scalars.py [isort] known_first_party=graphene + +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index 3723915d..013e6b2e 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,25 @@ -import sys - from setuptools import find_packages, setup from setuptools.command.test import test as TestCommand +import sys +import ast +import re -if sys.version_info[0] < 3: - import __builtin__ as builtins -else: - import builtins +_version_re = re.compile(r'VERSION\s+=\s+(.*)') -# This is a bit (!) hackish: we are setting a global variable so that the main -# graphql __init__ can detect if it is being loaded by the setup routine, to -# avoid attempting to load components that aren't built yet: -# the numpy distutils extensions that are used by scikit-learn to recursively -# build the compiled extensions in sub-packages is based on the Python import -# machinery. -builtins.__SETUP__ = True +with open('graphene/__init__.py', 'rb') as f: + version = ast.literal_eval(_version_re.search( + f.read().decode('utf-8')).group(1)) -version = __import__('graphene').get_version() +path_copy = sys.path[:] + +sys.path.append('graphene') +try: + from pyutils.version import get_version + version = get_version(version) +except Exception: + version = ".".join([str(v) for v in version]) + +sys.path[:] = path_copy class PyTest(TestCommand): @@ -79,7 +82,7 @@ setup( keywords='api graphql protocol rest relay graphene', - packages=find_packages(exclude=['tests']), + packages=find_packages(exclude=['tests', 'tests.*']), install_requires=[ 'six>=1.10.0', From d8fac587018b8a99bd0d5c4f05e81f580d28344d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 1 Aug 2017 13:59:18 -0700 Subject: [PATCH 70/82] Fixed source resolver and added tests for it --- graphene/types/field.py | 2 +- graphene/types/tests/test_query.py | 62 ++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/graphene/types/field.py b/graphene/types/field.py index 9e699a12..4a79fcac 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -11,7 +11,7 @@ from .utils import get_type base_type = type -def source_resolver(source, root, args, context, info): +def source_resolver(source, root, info, **args): resolved = getattr(root, source, None) if inspect.isfunction(resolved) or inspect.ismethod(resolved): return resolved() diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index acaef0a1..95e626ef 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -27,6 +27,23 @@ def test_query(): assert executed.data == {'hello': 'World'} +def test_query_source(): + class Root(object): + _hello = "World" + + def hello(self): + return self._hello + + class Query(ObjectType): + hello = String(source="hello") + + hello_schema = Schema(Query) + + executed = hello_schema.execute('{ hello }', Root()) + assert not executed.errors + assert executed.data == {'hello': 'World'} + + def test_query_union(): class one_object(object): pass @@ -127,13 +144,15 @@ def test_query_dynamic(): class Query(ObjectType): hello = Dynamic(lambda: String(resolver=lambda *_: 'World')) hellos = Dynamic(lambda: List(String, resolver=lambda *_: ['Worlds'])) - hello_field = Dynamic(lambda: Field(String, resolver=lambda *_: 'Field World')) + hello_field = Dynamic(lambda: Field( + String, resolver=lambda *_: 'Field World')) hello_schema = Schema(Query) executed = hello_schema.execute('{ hello hellos helloField }') assert not executed.errors - assert executed.data == {'hello': 'World', 'hellos': ['Worlds'], 'helloField': 'Field World'} + assert executed.data == {'hello': 'World', 'hellos': [ + 'Worlds'], 'helloField': 'Field World'} def test_query_default_value(): @@ -165,7 +184,8 @@ def test_query_wrong_default_value(): executed = hello_schema.execute('{ hello { field } }') assert len(executed.errors) == 1 - assert executed.errors[0].message == GraphQLError('Expected value of type "MyType" but got: str.').message + assert executed.errors[0].message == GraphQLError( + 'Expected value of type "MyType" but got: str.').message assert executed.data == {'hello': None} @@ -174,7 +194,8 @@ def test_query_default_value_ignored_by_resolver(): field = String() class Query(ObjectType): - hello = Field(MyType, default_value='hello', resolver=lambda *_: MyType(field='no default.')) + hello = Field(MyType, default_value='hello', + resolver=lambda *_: MyType(field='no default.')) hello_schema = Schema(Query) @@ -214,7 +235,8 @@ def test_query_arguments(): assert not result.errors assert result.data == {'test': '["Source!",{"a_str":"String!"}]'} - result = test_schema.execute('{ test(aInt: -123, aStr: "String!") }', 'Source!') + result = test_schema.execute( + '{ test(aInt: -123, aStr: "String!") }', 'Source!') assert not result.errors assert result.data in [ {'test': '["Source!",{"a_str":"String!","a_int":-123}]'}, @@ -239,13 +261,17 @@ def test_query_input_field(): assert not result.errors assert result.data == {'test': '[null,{}]'} - result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', 'Source!') + result = test_schema.execute( + '{ test(aInput: {aField: "String!"} ) }', 'Source!') assert not result.errors - assert result.data == {'test': '["Source!",{"a_input":{"a_field":"String!"}}]'} + assert result.data == { + 'test': '["Source!",{"a_input":{"a_field":"String!"}}]'} - result = test_schema.execute('{ test(aInput: {recursiveField: {aField: "String!"}}) }', 'Source!') + result = test_schema.execute( + '{ test(aInput: {recursiveField: {aField: "String!"}}) }', 'Source!') assert not result.errors - assert result.data == {'test': '["Source!",{"a_input":{"recursive_field":{"a_field":"String!"}}}]'} + assert result.data == { + 'test': '["Source!",{"a_input":{"recursive_field":{"a_field":"String!"}}}]'} def test_query_middlewares(): @@ -265,7 +291,8 @@ def test_query_middlewares(): hello_schema = Schema(Query) - executed = hello_schema.execute('{ hello, other }', middleware=[reversed_middleware]) + executed = hello_schema.execute( + '{ hello, other }', middleware=[reversed_middleware]) assert not executed.errors assert executed.data == {'hello': 'dlroW', 'other': 'rehto'} @@ -348,7 +375,8 @@ def test_big_list_of_containers_query_benchmark(benchmark): big_list_query = partial(hello_schema.execute, '{ allContainers { x } }') result = benchmark(big_list_query) assert not result.errors - assert result.data == {'allContainers': [{'x': c.x} for c in big_container_list]} + assert result.data == {'allContainers': [ + {'x': c.x} for c in big_container_list]} def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): @@ -368,10 +396,12 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): hello_schema = Schema(Query) - big_list_query = partial(hello_schema.execute, '{ allContainers { x, y, z, o } }') + big_list_query = partial(hello_schema.execute, + '{ allContainers { x, y, z, o } }') result = benchmark(big_list_query) assert not result.errors - assert result.data == {'allContainers': [{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]} + assert result.data == {'allContainers': [ + {'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]} def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark(benchmark): @@ -403,10 +433,12 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark hello_schema = Schema(Query) - big_list_query = partial(hello_schema.execute, '{ allContainers { x, y, z, o } }') + big_list_query = partial(hello_schema.execute, + '{ allContainers { x, y, z, o } }') result = benchmark(big_list_query) assert not result.errors - assert result.data == {'allContainers': [{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]} + assert result.data == {'allContainers': [ + {'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]} def test_query_annotated_resolvers(): From 10a3e86cc5830223a4438b9805ef805d4304cb43 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 1 Aug 2017 15:24:22 -0700 Subject: [PATCH 71/82] Fixed mutation payload name and added tests for covering it. --- graphene/relay/mutation.py | 11 ++++++----- graphene/relay/tests/test_mutation.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 7daccbf5..97b72417 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -19,8 +19,7 @@ class ClientIDMutation(Mutation): return input_class = getattr(cls, 'Input', None) - name = name or cls.__name__ - base_name = re.sub('Payload$', '', name) + base_name = re.sub('Payload$', '', name or cls.__name__) assert not output, "Can't specify any output" assert not arguments, "Can't specify any arguments" @@ -35,7 +34,8 @@ class ClientIDMutation(Mutation): cls.Input = type( '{}Input'.format(base_name), bases, - OrderedDict(input_fields, client_mutation_id=String(name='clientMutationId')) + OrderedDict(input_fields, client_mutation_id=String( + name='clientMutationId')) ) arguments = OrderedDict( @@ -46,12 +46,13 @@ class ClientIDMutation(Mutation): if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__: assert mutate_and_get_payload, ( "{name}.mutate_and_get_payload method is required" - " in a ClientIDMutation.").format(name=name) + " in a ClientIDMutation.").format(name=name or cls.__name__) if not name: name = '{}Payload'.format(base_name) - super(ClientIDMutation, cls).__init_subclass_with_meta__(output=None, arguments=arguments, name=name, **options) + super(ClientIDMutation, cls).__init_subclass_with_meta__( + output=None, arguments=arguments, name=name, **options) cls._meta.fields['client_mutation_id'] = ( Field(String, name='clientMutationId') ) diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index d37f8047..97a58028 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -91,6 +91,7 @@ def test_no_mutate_and_get_payload(): def test_mutation(): fields = SaySomething._meta.fields assert list(fields.keys()) == ['phrase', 'client_mutation_id'] + assert SaySomething._meta.name == "SaySomethingPayload" assert isinstance(fields['phrase'], Field) field = SaySomething.Field() assert field.type == SaySomething From 7f33fbe63828184492991816fec68f8ffaf66b15 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 1 Aug 2017 15:24:31 -0700 Subject: [PATCH 72/82] Fixed field source tests --- graphene/types/tests/test_field.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index d3782fe5..d2bb8c36 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -20,7 +20,8 @@ class MyInstance(object): def test_field_basic(): MyType = object() args = {'my arg': Argument(True)} - resolver = lambda: None + + def resolver(): return None deprecation_reason = 'Deprecated now' description = 'My Field' my_default = 'something' @@ -60,7 +61,7 @@ def test_field_default_value_not_callable(): def test_field_source(): MyType = object() field = Field(MyType, source='value') - assert field.resolver(MyInstance, {}, None, None) == MyInstance.value + assert field.resolver(MyInstance(), None) == MyInstance.value def test_field_with_lazy_type(): @@ -84,19 +85,20 @@ def test_field_not_source_and_resolver(): MyType = object() with pytest.raises(Exception) as exc_info: Field(MyType, source='value', resolver=lambda: None) - assert str(exc_info.value) == 'A Field cannot have a source and a resolver in at the same time.' + assert str( + exc_info.value) == 'A Field cannot have a source and a resolver in at the same time.' def test_field_source_func(): MyType = object() field = Field(MyType, source='value_func') - assert field.resolver(MyInstance(), {}, None, None) == MyInstance.value_func() + assert field.resolver(MyInstance(), None) == MyInstance.value_func() def test_field_source_method(): MyType = object() field = Field(MyType, source='value_method') - assert field.resolver(MyInstance(), {}, None, None) == MyInstance().value_method() + assert field.resolver(MyInstance(), None) == MyInstance().value_method() def test_field_source_as_argument(): From eff882e5a5b883c67d7bcc64ac6e0e606ba23fc4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 1 Aug 2017 23:12:21 -0700 Subject: [PATCH 73/82] Fixed snapshot --- .../snapshots/snap_test_objectidentification.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index bce35808..a15d49d3 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -63,20 +63,20 @@ type Faction implements Node { ships(before: String, after: String, first: Int, last: Int): ShipConnection } -type IntroduceShip { - ship: Ship - faction: Faction - clientMutationId: String -} - input IntroduceShipInput { shipName: String! factionId: String! clientMutationId: String } +type IntroduceShipPayload { + ship: Ship + faction: Faction + clientMutationId: String +} + type Mutation { - introduceShip(input: IntroduceShipInput!): IntroduceShip + introduceShip(input: IntroduceShipInput!): IntroduceShipPayload } interface Node { From 7e901f83aeed1ae0b06c120f19c5dbf95dccc1ce Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 1 Aug 2017 23:39:27 -0700 Subject: [PATCH 74/82] Improved test coverage --- graphene/utils/resolver_from_annotations.py | 39 ----------- graphene/utils/tests/test_deprecated.py | 65 +++++++++++++++++++ .../utils/tests/test_resolve_only_args.py | 8 ++- 3 files changed, 70 insertions(+), 42 deletions(-) delete mode 100644 graphene/utils/resolver_from_annotations.py create mode 100644 graphene/utils/tests/test_deprecated.py diff --git a/graphene/utils/resolver_from_annotations.py b/graphene/utils/resolver_from_annotations.py deleted file mode 100644 index 2e006dd6..00000000 --- a/graphene/utils/resolver_from_annotations.py +++ /dev/null @@ -1,39 +0,0 @@ -from ..pyutils.compat import signature -from functools import wraps, partial - - -def resolver_from_annotations(func): - from ..types import Context, ResolveInfo - - func_signature = signature(func) - - _context_var = None - _info_var = None - for key, parameter in func_signature.parameters.items(): - param_type = parameter.annotation - if param_type is Context: - _context_var = key - elif param_type is ResolveInfo: - _info_var = key - continue - - # We generate different functions as it will be faster - # than calculating the args on the fly when executing - # the function resolver. - if _context_var and _info_var: - def inner(root, args, context, info): - return func(root, **dict(args, **{_info_var: info, _context_var: context})) - elif _context_var: - def inner(root, args, context, info): - return func(root, **dict(args, **{_context_var: context})) - elif _info_var: - def inner(root, args, context, info): - return func(root, **dict(args, **{_info_var: info})) - else: - def inner(root, args, context, info): - return func(root, **args) - - if isinstance(func, partial): - return inner - - return wraps(func)(inner) diff --git a/graphene/utils/tests/test_deprecated.py b/graphene/utils/tests/test_deprecated.py new file mode 100644 index 00000000..d196744f --- /dev/null +++ b/graphene/utils/tests/test_deprecated.py @@ -0,0 +1,65 @@ +import pytest +from .. import deprecated +from ..deprecated import deprecated as deprecated_decorator, warn_deprecation + + +def test_warn_deprecation(mocker): + mocker.patch.object(deprecated.warnings, 'warn') + + warn_deprecation("OH!") + deprecated.warnings.warn.assert_called_with('OH!', stacklevel=2, category=DeprecationWarning) + + +def test_deprecated_decorator(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') + + @deprecated_decorator + def my_func(): + return True + + result = my_func() + assert result + deprecated.warn_deprecation.assert_called_with("Call to deprecated function my_func.") + + +def test_deprecated_class(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') + + @deprecated_decorator + class X: + pass + + result = X() + assert result + deprecated.warn_deprecation.assert_called_with("Call to deprecated class X.") + + +def test_deprecated_decorator_text(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') + + @deprecated_decorator("Deprecation text") + def my_func(): + return True + + result = my_func() + assert result + deprecated.warn_deprecation.assert_called_with("Call to deprecated function my_func (Deprecation text).") + + +def test_deprecated_class_text(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') + + @deprecated_decorator("Deprecation text") + class X: + pass + + result = X() + assert result + deprecated.warn_deprecation.assert_called_with("Call to deprecated class X (Deprecation text).") + + +def test_deprecated_other_object(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') + + with pytest.raises(TypeError) as exc_info: + deprecated_decorator({}) diff --git a/graphene/utils/tests/test_resolve_only_args.py b/graphene/utils/tests/test_resolve_only_args.py index a3090c3a..f5b77de1 100644 --- a/graphene/utils/tests/test_resolve_only_args.py +++ b/graphene/utils/tests/test_resolve_only_args.py @@ -4,10 +4,12 @@ from .. import deprecated def test_resolve_only_args(mocker): mocker.patch.object(deprecated, 'warn_deprecation') - def resolver(*args, **kwargs): - return kwargs + def resolver(root, **args): + return root, args my_data = {'one': 1, 'two': 2} - wrapped = resolve_only_args(resolver) + wrapped_resolver = resolve_only_args(resolver) assert deprecated.warn_deprecation.called + result = wrapped_resolver(1, 2, a=3) + assert result == (1, {'a': 3}) From 0a97deaa4d262229d65298f6a28f5c4193a9908b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 1 Aug 2017 23:55:39 -0700 Subject: [PATCH 75/82] Improved relay coverage --- graphene/relay/connection.py | 3 +-- graphene/relay/mutation.py | 5 +---- graphene/relay/tests/test_connection.py | 8 +++++++ graphene/relay/tests/test_mutation.py | 29 ++++++++++++++++++++++++- graphene/relay/tests/test_node.py | 14 +++++++++++- 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index e480f036..afe6ffb3 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -9,7 +9,6 @@ from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) from ..types.field import Field from ..types.objecttype import ObjectType, ObjectTypeOptions -from ..utils.deprecated import warn_deprecation from .node import is_node @@ -101,7 +100,7 @@ class IterableConnectionField(Field): type = super(IterableConnectionField, self).type connection_type = type if is_node(type): - warn_deprecation( + 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" ) diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 97b72417..8c020ef0 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -14,10 +14,7 @@ class ClientIDMutation(Mutation): @classmethod def __init_subclass_with_meta__(cls, output=None, input_fields=None, - arguments=None, name=None, abstract=False, **options): - if abstract: - return - + arguments=None, name=None, **options): input_class = getattr(cls, 'Input', None) base_name = re.sub('Payload$', '', name or cls.__name__) diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index c769eb89..b6a26df3 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,3 +1,4 @@ +import pytest from ...types import Argument, Field, Int, List, NonNull, ObjectType, String from ..connection import Connection, ConnectionField, PageInfo @@ -117,6 +118,13 @@ def test_connectionfield(): } +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): diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 97a58028..aa5ce179 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -31,6 +31,25 @@ class SaySomething(ClientIDMutation): return SaySomething(phrase=str(what)) +class FixedSaySomething(object): + __slots__ = 'phrase', + + def __init__(self, phrase): + self.phrase = phrase + + +class SaySomethingFixed(ClientIDMutation): + + class Input: + what = String() + + phrase = String() + + @staticmethod + def mutate_and_get_payload(self, info, what, client_mutation_id=None): + return FixedSaySomething(phrase=str(what)) + + class SaySomethingPromise(ClientIDMutation): class Input: @@ -71,6 +90,7 @@ class RootQuery(ObjectType): class Mutation(ObjectType): say = SaySomething.Field() + say_fixed = SaySomethingFixed.Field() say_promise = SaySomethingPromise.Field() other = OtherMutation.Field() @@ -152,7 +172,14 @@ def test_node_query(): assert executed.data == {'say': {'phrase': 'hello'}} -def test_node_query(): +def test_node_query_fixed(): + executed = schema.execute( + 'mutation a { sayFixed(input: {what:"hello", clientMutationId:"1"}) { phrase } }' + ) + assert "Cannot set client_mutation_id in the payload object" in str(executed.errors[0]) + + +def test_node_query_promise(): executed = schema.execute( 'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }' ) diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index 315f2e39..10dc5d94 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -3,7 +3,7 @@ from collections import OrderedDict from graphql_relay import to_global_id from ...types import ObjectType, Schema, String -from ..node import Node +from ..node import Node, is_node class SharedNodeFields(object): @@ -46,11 +46,14 @@ class RootQuery(ObjectType): only_node = Node.Field(MyNode) only_node_lazy = Node.Field(lambda: MyNode) + schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode]) def test_node_good(): assert 'id' in MyNode._meta.fields + assert is_node(MyNode) + assert not is_node(object) def test_node_query(): @@ -70,6 +73,15 @@ def test_subclassed_node_query(): [('shared', '1'), ('extraField', 'extra field info.'), ('somethingElse', '----')])}) +def test_node_requesting_non_node(): + executed = schema.execute( + '{ node(id:"%s") { __typename } } ' % Node.to_global_id("RootQuery", 1) + ) + assert executed.data == { + 'node': None + } + + def test_node_query_incorrect_id(): executed = schema.execute( '{ node(id:"%s") { ... on MyNode { name } } }' % "something:2" From 8826d72ada22b911abaec1e95567d12b035b0879 Mon Sep 17 00:00:00 2001 From: picturedots Date: Thu, 3 Aug 2017 15:20:11 -0400 Subject: [PATCH 76/82] minor spelling and grammar changes UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 8c96c0b2..462bddc2 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -4,7 +4,7 @@ have been quite simplified, without the need to define a explicit Metaclass for each subtype. It also improves the field resolvers, [simplifying the code](#simpler-resolvers) the -developer have to write to use them. +developer has to write to use them. **Deprecations:** * [`AbstractType`](#abstracttype-deprecated) @@ -20,14 +20,14 @@ developer have to write to use them. * [`Meta as Class arguments`](#meta-ass-class-arguments) (_only available for Python 3_) -> The type metaclases are now deleted as are no longer necessary, if your code was depending +> 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). ## Deprecations ### Simpler resolvers -All the resolvers in graphene have been simplified. If before resolvers must had received +All the resolvers in graphene have been simplified. If before resolvers required four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments and `context` and `info` will only be passed if the function is annotated with it. @@ -112,7 +112,7 @@ class User(ObjectType): ### Mutation.Input -`Mutation.Input` is now deprecated in favor using `Mutation.Arguments` (`ClientIDMutation` still uses `Input`). +`Mutation.Input` is now deprecated in favor of using `Mutation.Arguments` (`ClientIDMutation` still uses `Input`). Before: @@ -135,7 +135,7 @@ class User(Mutation): ### Simpler resolvers -All the resolvers in graphene have been simplified. If before resolvers must had received +All the resolvers in graphene have been simplified. If before resolvers required four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments and `context` and `info` will only be passed if the function is annotated with it. @@ -212,7 +212,7 @@ class Query(ObjectType): ## Node.get_node -The method `get_node` in `ObjectTypes` that have `Node` as interface, changes it's api. +The method `get_node` in `ObjectTypes` that have `Node` as interface, changes its API. From `def get_node(cls, id, context, info)` to `def get_node(cls, info, id)`. ```python @@ -251,7 +251,7 @@ Now only receives (`root`, `info`, `**input`) ### InputObjectType If you are using `InputObjectType`, you now can access -it's fields via `getattr` (`my_input.myattr`) when resolving, instead of +its fields via `getattr` (`my_input.myattr`) when resolving, instead of the classic way `my_input['myattr']`. And also use custom defined properties on your input class. From 48754046b2608f4c56b65d9a1260288fd8dce167 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 7 Aug 2017 12:24:17 -0700 Subject: [PATCH 77/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 462bddc2..e5aa10b4 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -28,8 +28,7 @@ developer has to write to use them. ### Simpler resolvers All the resolvers in graphene have been simplified. If before resolvers required -four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments -and `context` and `info` will only be passed if the function is annotated with it. +four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments, `info` will be always passed after `root` or `self` and `context` is now inside `info` (`info.context`). Before: From 1e20e2bff3311943a63fc94477d53ebedcb69447 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 7 Aug 2017 12:27:23 -0700 Subject: [PATCH 78/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index e5aa10b4..173a2119 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -27,8 +27,9 @@ developer has to write to use them. ### Simpler resolvers -All the resolvers in graphene have been simplified. If before resolvers required -four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments, `info` will be always passed after `root` or `self` and `context` is now inside `info` (`info.context`). +All the resolvers in graphene have been simplified. +Prior to Graphene `2.0`, all resolvers required four arguments: `(root, args, context, info)`. +Now, resolver `args` are passed as keyword arguments to the function, and `context` argument dissapeared in favor of `info.context`. Before: From 19bf9b371348551c7405b8aeb2ed7f6a5b02cd24 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 7 Aug 2017 12:28:18 -0700 Subject: [PATCH 79/82] Update UPGRADE-v2.0.md --- UPGRADE-v2.0.md | 59 ++++++------------------------------------------- 1 file changed, 7 insertions(+), 52 deletions(-) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 173a2119..89a0435b 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -25,42 +25,6 @@ developer has to write to use them. ## Deprecations -### Simpler resolvers - -All the resolvers in graphene have been simplified. -Prior to Graphene `2.0`, all resolvers required four arguments: `(root, args, context, info)`. -Now, resolver `args` are passed as keyword arguments to the function, and `context` argument dissapeared in favor of `info.context`. - -Before: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -def resolve_my_field(self, args, context, info): - my_arg = args.get('my_arg') - return ... -``` - -With 2.0: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -def resolve_my_field(self, info, my_arg): - return ... -``` - -And, if you need the context in the resolver, you can use `info.context`: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -def resolve_my_field(self, info, my_arg): - context = info.context - return ... -``` - - ### AbstractType deprecated AbstractType is deprecated in graphene 2.0, you can now use normal inheritance instead. @@ -135,9 +99,9 @@ class User(Mutation): ### Simpler resolvers -All the resolvers in graphene have been simplified. If before resolvers required -four arguments `root`, `args`, `context` and `info`, now the `args` are passed as keyword arguments -and `context` and `info` will only be passed if the function is annotated with it. +All the resolvers in graphene have been simplified. +Prior to Graphene `2.0`, all resolvers required four arguments: `(root, args, context, info)`. +Now, resolver `args` are passed as keyword arguments to the function, and `context` argument dissapeared in favor of `info.context`. Before: @@ -154,26 +118,17 @@ With 2.0: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, my_arg): +def resolve_my_field(self, info, my_arg): return ... ``` -And, if the resolver want to receive the context: +And, if you need the context in the resolver, you can use `info.context`: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, context: graphene.Context, my_arg): - return ... -``` - -which is equivalent in Python 2 to: - -```python -my_field = graphene.String(my_arg=graphene.String()) - -@annotate(context=graphene.Context) -def resolve_my_field(self, context, my_arg): +def resolve_my_field(self, info, my_arg): + context = info.context return ... ``` From 7cfec55410d71adc6790634532f5142f4bc1107f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 7 Aug 2017 20:48:20 -0700 Subject: [PATCH 80/82] Added mypy static checking --- .gitignore | 1 + .travis.yml | 9 +++++++++ graphene/pyutils/init_subclass.py | 2 +- graphene/types/inputobjecttype.py | 8 +++++++- graphene/types/interface.py | 5 +++++ graphene/types/mutation.py | 9 ++++++++- graphene/types/objecttype.py | 7 ++++++- graphene/types/scalars.py | 1 + graphene/types/union.py | 9 ++++++++- mypy.ini | 17 +++++++++++++++++ 10 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 mypy.ini diff --git a/.gitignore b/.gitignore index 9f465556..d98ebfc3 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ target/ # Databases *.sqlite3 .vscode +.mypy_cache diff --git a/.travis.yml b/.travis.yml index 239df23d..e5d52a0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,12 +27,19 @@ install: elif [ "$TEST_TYPE" = lint ]; then pip install flake8 fi + elif [ "$TEST_TYPE" = mypy ]; then + pip install mypy + fi script: - | if [ "$TEST_TYPE" = lint ]; then echo "Checking Python code lint." flake8 graphene exit + elif [ "$TEST_TYPE" = mypy ]; then + echo "Checking Python types." + mypy graphene + exit elif [ "$TEST_TYPE" = build ]; then py.test --cov=graphene graphene examples fi @@ -51,6 +58,8 @@ matrix: include: - python: '2.7' env: TEST_TYPE=lint + - python: '3.6' + env: TEST_TYPE=mypy deploy: provider: pypi user: syrusakbary diff --git a/graphene/pyutils/init_subclass.py b/graphene/pyutils/init_subclass.py index c3a6143c..78e4ff19 100644 --- a/graphene/pyutils/init_subclass.py +++ b/graphene/pyutils/init_subclass.py @@ -16,4 +16,4 @@ if not is_init_subclass_available: if hasattr(super_class, '__init_subclass__'): super_class.__init_subclass__.__func__(cls, **kwargs) else: - InitSubclassMeta = type + InitSubclassMeta = type # type: ignore diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 3beb3ebf..28dc220b 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -6,8 +6,14 @@ from .unmountedtype import UnmountedType from .utils import yank_fields_from_attrs +# For static type checking with Mypy +MYPY = False +if MYPY: + from typing import Dict, Callable + + class InputObjectTypeOptions(BaseOptions): - fields = None # type: Dict[str, Field] + fields = None # type: Dict[str, InputField] create_container = None # type: Callable diff --git a/graphene/types/interface.py b/graphene/types/interface.py index c98f0f1f..b806de33 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -4,6 +4,11 @@ from .base import BaseOptions, BaseType from .field import Field from .utils import yank_fields_from_attrs +# For static type checking with Mypy +MYPY = False +if MYPY: + from typing import Dict + class InterfaceOptions(BaseOptions): fields = None # type: Dict[str, Field] diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 213c0f48..6a7a7bbb 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -8,10 +8,17 @@ from .utils import yank_fields_from_attrs from ..utils.deprecated import warn_deprecation +# For static type checking with Mypy +MYPY = False +if MYPY: + from .argument import Argument + from typing import Dict, Type, Callable + + class MutationOptions(ObjectTypeOptions): arguments = None # type: Dict[str, Argument] output = None # type: Type[ObjectType] - resolver = None # type: Function + resolver = None # type: Callable class Mutation(ObjectType): diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index c579a88e..53e00902 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -5,10 +5,15 @@ from .field import Field from .interface import Interface from .utils import yank_fields_from_attrs +# For static type checking with Mypy +MYPY = False +if MYPY: + from typing import Dict, Iterable, Type + class ObjectTypeOptions(BaseOptions): fields = None # type: Dict[str, Field] - interfaces = () # type: List[Type[Interface]] + interfaces = () # type: Iterable[Type[Interface]] class ObjectType(BaseType): diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index ccfb089c..3b78185d 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -86,6 +86,7 @@ class Float(Scalar): @staticmethod def coerce_float(value): + # type: (Any) -> float try: return float(value) except ValueError: diff --git a/graphene/types/union.py b/graphene/types/union.py index f9797fc0..ae6af8b4 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -2,8 +2,15 @@ from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType +# For static type checking with Mypy +MYPY = False +if MYPY: + from .objecttype import ObjectType + from typing import Iterable, Type + + class UnionOptions(BaseOptions): - types = () # type: List[Type[ObjectType]] + types = () # type: Iterable[Type[ObjectType]] class Union(UnmountedType, BaseType): diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..bbb37b77 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,17 @@ +[mypy] +ignore_missing_imports = True + +[mypy-graphene.pyutils.*] +ignore_errors = True + +[mypy-graphene.types.scalars] +ignore_errors = True + +[mypy-graphene.types.generic] +ignore_errors = True + +[mypy-graphene.types.tests.*] +ignore_errors = True + +[mypy-graphene.relay.tests.*] +ignore_errors = True From 45854694258898d6cc293ed6784fc08e7b89f81c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 7 Aug 2017 20:55:05 -0700 Subject: [PATCH 81/82] Fixed travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e5d52a0a..2ee20a4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,6 @@ install: python setup.py develop elif [ "$TEST_TYPE" = lint ]; then pip install flake8 - fi elif [ "$TEST_TYPE" = mypy ]; then pip install mypy fi From 7eb3ab574777a4205ef998f3b7fccddae69f9c38 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 7 Aug 2017 20:59:48 -0700 Subject: [PATCH 82/82] Fixed Flake8 issues --- graphene/types/inputobjecttype.py | 2 +- graphene/types/interface.py | 2 +- graphene/types/mutation.py | 4 ++-- graphene/types/objecttype.py | 2 +- graphene/types/union.py | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 28dc220b..38173c79 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -9,7 +9,7 @@ from .utils import yank_fields_from_attrs # For static type checking with Mypy MYPY = False if MYPY: - from typing import Dict, Callable + from typing import Dict, Callable # NOQA class InputObjectTypeOptions(BaseOptions): diff --git a/graphene/types/interface.py b/graphene/types/interface.py index b806de33..dbc3b476 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -7,7 +7,7 @@ from .utils import yank_fields_from_attrs # For static type checking with Mypy MYPY = False if MYPY: - from typing import Dict + from typing import Dict # NOQA class InterfaceOptions(BaseOptions): diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 6a7a7bbb..25794d47 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -11,8 +11,8 @@ from ..utils.deprecated import warn_deprecation # For static type checking with Mypy MYPY = False if MYPY: - from .argument import Argument - from typing import Dict, Type, Callable + from .argument import Argument # NOQA + from typing import Dict, Type, Callable # NOQA class MutationOptions(ObjectTypeOptions): diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 53e00902..fe234b09 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -8,7 +8,7 @@ from .utils import yank_fields_from_attrs # For static type checking with Mypy MYPY = False if MYPY: - from typing import Dict, Iterable, Type + from typing import Dict, Iterable, Type # NOQA class ObjectTypeOptions(BaseOptions): diff --git a/graphene/types/union.py b/graphene/types/union.py index ae6af8b4..c5925e88 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -5,8 +5,8 @@ from .unmountedtype import UnmountedType # For static type checking with Mypy MYPY = False if MYPY: - from .objecttype import ObjectType - from typing import Iterable, Type + from .objecttype import ObjectType # NOQA + from typing import Iterable, Type # NOQA class UnionOptions(BaseOptions): @@ -42,6 +42,6 @@ class Union(UnmountedType, BaseType): @classmethod def resolve_type(cls, instance, info): - from .objecttype import ObjectType + from .objecttype import ObjectType # NOQA if isinstance(instance, ObjectType): return type(instance)