diff --git a/.gitignore b/.gitignore index d98ebfc3..0e3fcd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ htmlcov/ .coverage .coverage.* .cache +.pytest_cache nosetests.xml coverage.xml *,cover diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 89a0435b..8ef09343 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -21,7 +21,7 @@ developer has to write to use them. > The type metaclasses are now deleted as they are no longer necessary. If your code was depending -> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/2.0/graphene/tests/issues/test_425.py). +> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/v2.0.0/graphene/tests/issues/test_425.py). ## Deprecations diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index c5e11aa7..55efe730 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -3,7 +3,7 @@ Middleware You can use ``middleware`` to affect the evaluation of fields in your schema. -A middleware is any object that responds to ``resolve(*args, next_middleware)``. +A middleware is any object or function that responds to ``resolve(next_middleware, *args)``. Inside that method, it should either: @@ -18,10 +18,8 @@ Middlewares ``resolve`` is invoked with several arguments: - ``next`` represents the execution chain. Call ``next`` to continue evalution. - ``root`` is the root value object passed throughout the query. -- ``args`` is the hash of arguments passed to the field. -- ``context`` is the context object passed throughout the query. - ``info`` is the resolver info. - +- ``args`` is the dict of arguments passed to the field. Example ------- @@ -42,3 +40,32 @@ And then execute it with: .. code:: python result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()]) + + +Functional example +------------------ + +Middleware can also be defined as a function. Here we define a middleware that +logs the time it takes to resolve each field + +.. code:: python + + from time import time as timer + + def timing_middleware(next, root, info, **args): + start = timer() + return_value = next(root, info, **args) + duration = timer() - start + logger.debug("{parent_type}.{field_name}: {duration} ms".format( + parent_type=root._meta.name if root and hasattr(root, '_meta') else '', + field_name=info.field_name, + duration=round(duration * 1000, 2) + )) + return return_value + + +And then execute it with: + +.. code:: python + + result = schema.execute('THE QUERY', middleware=[timing_middleware]) diff --git a/docs/relay/index.rst b/docs/relay/index.rst index e3a87d08..7eb418df 100644 --- a/docs/relay/index.rst +++ b/docs/relay/index.rst @@ -21,9 +21,9 @@ Useful links - `Relay Cursor Connection Specification`_ - `Relay input Object Mutation`_ -.. _Relay: https://facebook.github.io/relay/docs/graphql-relay-specification.html +.. _Relay: https://facebook.github.io/relay/docs/en/graphql-server-specification.html .. _Relay specification: https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-root-field -.. _Getting started with Relay: https://facebook.github.io/relay/docs/graphql-relay-specification.html +.. _Getting started with Relay: https://facebook.github.io/relay/docs/en/quick-start-guide.html .. _Relay Global Identification Specification: https://facebook.github.io/relay/graphql/objectidentification.htm .. _Relay Cursor Connection Specification: https://facebook.github.io/relay/graphql/connections.htm .. _Relay input Object Mutation: https://facebook.github.io/relay/graphql/mutations.htm diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index 74f42094..7af00ea1 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -55,12 +55,12 @@ Example of a custom node: return '{}:{}'.format(type, id) @staticmethod - def get_node_from_global_id(info global_id, only_type=None): + def get_node_from_global_id(info, global_id, only_type=None): type, id = global_id.split(':') - if only_node: + if only_type: # We assure that the node type that we want to retrieve # is the same that was indicated in the field type - assert type == only_node._meta.name, 'Received not compatible node.' + assert type == only_type._meta.name, 'Received not compatible node.' if type == 'User': return get_user(id) @@ -75,10 +75,10 @@ Accessing node types -------------------- If we want to retrieve node instances from a ``global_id`` (scalar that identifies an instance by it's type name and id), -we can simply do ``Node.get_node_from_global_id(global_id, context, info)``. +we can simply do ``Node.get_node_from_global_id(info, global_id)``. In the case we want to restrict the instance retrieval to a specific type, we can do: -``Node.get_node_from_global_id(global_id, context, info, only_type=Ship)``. This will raise an error +``Node.get_node_from_global_id(info, global_id, only_type=Ship)``. This will raise an error if the ``global_id`` doesn't correspond to a Ship type. diff --git a/docs/requirements.txt b/docs/requirements.txt index 31a23482..b6e0cd75 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ # Required library Sphinx==1.5.3 # Docs template -https://github.com/graphql-python/graphene-python.org/archive/docs.zip +http://graphene-python.org/sphinx_graphene_theme.zip diff --git a/docs/types/enums.rst b/docs/types/enums.rst index 96c52e92..6e730628 100644 --- a/docs/types/enums.rst +++ b/docs/types/enums.rst @@ -54,6 +54,13 @@ the ``Enum.from_enum`` function. graphene.Enum.from_enum(AlreadyExistingPyEnum) +``Enum.from_enum`` supports a ``description`` and ``deprecation_reason`` lambdas as input so +you can add description etc. to your enum without changing the original: + +.. code:: python + + graphene.Enum.from_enum(AlreadyExistingPyEnum, description=lambda value: return 'foo' if value == AlreadyExistingPyEnum.Foo else 'bar') + Notes ----- @@ -65,7 +72,7 @@ member getters. In the Python ``Enum`` implementation you can access a member by initing the Enum. .. code:: python - + from enum import Enum class Color(Enum): RED = 1 @@ -78,7 +85,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu However, in Graphene ``Enum`` you need to call get to have the same effect: .. code:: python - + from graphene import Enum class Color(Enum): RED = 1 diff --git a/docs/types/list-and-nonnull.rst b/docs/types/list-and-nonnull.rst index b48aa187..a127a9d2 100644 --- a/docs/types/list-and-nonnull.rst +++ b/docs/types/list-and-nonnull.rst @@ -48,3 +48,24 @@ Lists work in a similar way: We can use a type modifier to mark a type as a ``List``, which indicates that this field will return a list of that type. It works the same for arguments, where the validation step will expect a list for that value. + +NonNull Lists +------------- + +By default items in a list will be considered nullable. To define a list without +any nullable items the type needs to be marked as ``NonNull``. For example: + +.. code:: python + + import graphene + + class Character(graphene.ObjectType): + appears_in = graphene.List(graphene.NonNull(graphene.String)) + +The above results in the type definition: + +.. code:: + + type Character { + appearsIn: [String!] + } diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 0958722d..b6e8f654 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -1,19 +1,83 @@ Scalars ======= +All Scalar types accept the following arguments. All are optional: + +``name``: *string* + + Override the name of the Field. + +``description``: *string* + + A description of the type to show in the GraphiQL browser. + +``required``: *boolean* + + If ``True``, the server will enforce a value for this field. See `NonNull <./list-and-nonnull.html#nonnull>`_. Default is ``False``. + +``deprecation_reason``: *string* + + Provide a deprecation reason for the Field. + +``default_value``: *any* + + Provide a default value for the Field. + + + +Base scalars +------------ + Graphene defines the following base Scalar Types: -- ``graphene.String`` -- ``graphene.Int`` -- ``graphene.Float`` -- ``graphene.Boolean`` -- ``graphene.ID`` +``graphene.String`` + + Represents textual data, represented as UTF-8 + character sequences. The String type is most often used by GraphQL to + represent free-form human-readable text. + +``graphene.Int`` + + Represents non-fractional signed whole numeric + values. Int can represent values between `-(2^53 - 1)` and `2^53 - 1` since + represented in JSON as double-precision floating point numbers specified + by `IEEE 754 `_. + +``graphene.Float`` + + Represents signed double-precision fractional + values as specified by + `IEEE 754 `_. + +``graphene.Boolean`` + + Represents `true` or `false`. + +``graphene.ID`` + + Represents a unique identifier, often used to + refetch an object or as key for a cache. The ID type appears in a JSON + response as a String; however, it is not intended to be human-readable. + When expected as an input type, any string (such as `"4"`) or integer + (such as `4`) input value will be accepted as an ID. Graphene also provides custom scalars for Dates, Times, and JSON: -- ``graphene.types.datetime.DateTime`` -- ``graphene.types.datetime.Time`` -- ``graphene.types.json.JSONString`` +``graphene.types.datetime.Date`` + + Represents a Date value as specified by `iso8601 `_. + +``graphene.types.datetime.DateTime`` + + Represents a DateTime value as specified by `iso8601 `_. + +``graphene.types.datetime.Time`` + + Represents a Time value as specified by `iso8601 `_. + +``graphene.types.json.JSONString`` + + Represents a JSON string. Custom scalars diff --git a/docs/types/unions.rst b/docs/types/unions.rst index f3d66e02..2c5c5a75 100644 --- a/docs/types/unions.rst +++ b/docs/types/unions.rst @@ -12,8 +12,8 @@ The basics: Quick example ------------- -This example model defines a ``Character`` interface with a name. ``Human`` -and ``Droid`` are two implementations of that interface. +This example model defines several ObjectTypes with their own fields. +``SearchResult`` is the implementation of ``Union`` of this object types. .. code:: python diff --git a/graphene/__init__.py b/graphene/__init__.py index aedf23a9..0198043c 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -34,7 +34,7 @@ from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import -VERSION = (2, 0, 0, 'final', 0) +VERSION = (2, 0, 1, 'final', 0) __version__ = get_version(VERSION) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index afe6ffb3..bcd238d3 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -73,7 +73,7 @@ class Connection(ObjectType): edge = type(edge_name, edge_bases, {}) cls.Edge = edge - _meta.name = name + options['name'] = name _meta.node = node _meta.fields = OrderedDict([ ('page_info', Field(PageInfo, name='pageInfo', required=True)), @@ -99,16 +99,19 @@ class IterableConnectionField(Field): def type(self): type = super(IterableConnectionField, self).type connection_type = type - if is_node(type): + if isinstance(type, NonNull): + connection_type = type.of_type + + if is_node(connection_type): raise Exception( "ConnectionField's now need a explicit ConnectionType for Nodes.\n" - "Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#node-connections" + "Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#node-connections" ) assert issubclass(connection_type, Connection), ( '{} type have to be a subclass of Connection. Received "{}".' ).format(self.__class__.__name__, connection_type) - return connection_type + return type @classmethod def resolve_connection(cls, connection_type, args, resolved): @@ -133,6 +136,9 @@ class IterableConnectionField(Field): def connection_resolver(cls, resolver, connection_type, root, info, **args): resolved = resolver(root, info, **args) + if isinstance(connection_type, NonNull): + connection_type = connection_type.of_type + on_resolve = partial(cls.resolve_connection, connection_type, args) if is_thenable(resolved): return Promise.resolve(resolved).then(on_resolve) diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index b6a26df3..c206f714 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,6 +1,6 @@ import pytest -from ...types import Argument, Field, Int, List, NonNull, ObjectType, String +from ...types import Argument, Field, Int, List, NonNull, ObjectType, String, Schema from ..connection import Connection, ConnectionField, PageInfo from ..node import Node @@ -52,6 +52,21 @@ def test_connection_inherit_abstracttype(): assert list(fields.keys()) == ['page_info', 'edges', 'extra'] +def test_connection_name(): + custom_name = "MyObjectCustomNameConnection" + + class BaseConnection(object): + extra = String() + + class MyObjectConnection(BaseConnection, Connection): + + class Meta: + node = MyObject + name = custom_name + + assert MyObjectConnection._meta.name == custom_name + + def test_edge(): class MyObjectConnection(Connection): @@ -122,9 +137,10 @@ def test_connectionfield_node_deprecated(): field = ConnectionField(MyObject) with pytest.raises(Exception) as exc_info: field.type - + assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str(exc_info.value) + def test_connectionfield_custom_args(): class MyObjectConnection(Connection): @@ -139,3 +155,23 @@ def test_connectionfield_custom_args(): 'last': Argument(Int), 'extra': Argument(String), } + + +def test_connectionfield_required(): + class MyObjectConnection(Connection): + + class Meta: + node = MyObject + + class Query(ObjectType): + test_connection = ConnectionField(MyObjectConnection, required=True) + + def resolve_test_connection(root, info, **args): + return [] + + schema = Schema(query=Query) + executed = schema.execute( + '{ testConnection { edges { cursor } } }' + ) + assert not executed.errors + assert executed.data == {'testConnection': {'edges': []}} diff --git a/graphene/tests/issues/test_425.py b/graphene/tests/issues/test_425.py index 7f92a75a..d50edf84 100644 --- a/graphene/tests/issues/test_425.py +++ b/graphene/tests/issues/test_425.py @@ -2,8 +2,11 @@ # Adapted for Graphene 2.0 from graphene.types.objecttype import ObjectType, ObjectTypeOptions +from graphene.types.inputobjecttype import InputObjectType, InputObjectTypeOptions +from graphene.types.enum import Enum, EnumOptions +# ObjectType class SpecialOptions(ObjectTypeOptions): other_attr = None @@ -40,3 +43,77 @@ def test_special_objecttype_inherit_meta_options(): assert MyType._meta.name == 'MyType' assert MyType._meta.default_resolver is None assert MyType._meta.interfaces == () + + +# InputObjectType +class SpecialInputObjectTypeOptions(ObjectTypeOptions): + other_attr = None + + +class SpecialInputObjectType(InputObjectType): + + @classmethod + def __init_subclass_with_meta__(cls, other_attr='default', **options): + _meta = SpecialInputObjectTypeOptions(cls) + _meta.other_attr = other_attr + super(SpecialInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + +def test_special_inputobjecttype_could_be_subclassed(): + class MyInputObjectType(SpecialInputObjectType): + + class Meta: + other_attr = 'yeah!' + + assert MyInputObjectType._meta.other_attr == 'yeah!' + + +def test_special_inputobjecttype_could_be_subclassed_default(): + class MyInputObjectType(SpecialInputObjectType): + pass + + assert MyInputObjectType._meta.other_attr == 'default' + + +def test_special_inputobjecttype_inherit_meta_options(): + class MyInputObjectType(SpecialInputObjectType): + pass + + assert MyInputObjectType._meta.name == 'MyInputObjectType' + + +# Enum +class SpecialEnumOptions(EnumOptions): + other_attr = None + + +class SpecialEnum(Enum): + + @classmethod + def __init_subclass_with_meta__(cls, other_attr='default', **options): + _meta = SpecialEnumOptions(cls) + _meta.other_attr = other_attr + super(SpecialEnum, cls).__init_subclass_with_meta__(_meta=_meta, **options) + + +def test_special_enum_could_be_subclassed(): + class MyEnum(SpecialEnum): + + class Meta: + other_attr = 'yeah!' + + assert MyEnum._meta.other_attr == 'yeah!' + + +def test_special_enum_could_be_subclassed_default(): + class MyEnum(SpecialEnum): + pass + + assert MyEnum._meta.other_attr == 'default' + + +def test_special_enum_inherit_meta_options(): + class MyEnum(SpecialEnum): + pass + + assert MyEnum._meta.name == 'MyEnum' diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index 3e283493..aaa0ff37 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -7,6 +7,6 @@ class AbstractType(SubclassWithMeta): def __init_subclass__(cls, *args, **kwargs): warn_deprecation( "Abstract type is deprecated, please use normal object inheritance instead.\n" - "See more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#deprecations" + "See more: https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md#deprecations" ) super(AbstractType, cls).__init_subclass__(*args, **kwargs) diff --git a/graphene/types/base.py b/graphene/types/base.py index d9c3bdd4..50242674 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -21,7 +21,7 @@ class BaseOptions(object): raise Exception("Can't modify frozen Options {0}".format(self)) def __repr__(self): - return "<{} type={}>".format(self.__class__.__name__, self.class_type.__name__) + return "<{} name={}>".format(self.__class__.__name__, repr(self.name)) class BaseType(SubclassWithMeta): diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 5cc53258..b750ea01 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -31,7 +31,10 @@ class Date(Scalar): @staticmethod def parse_value(value): - return parse_date(value) + try: + return parse_date(value) + except ValueError: + return None class DateTime(Scalar): @@ -55,7 +58,10 @@ class DateTime(Scalar): @staticmethod def parse_value(value): - return parse_datetime(value) + try: + return parse_datetime(value) + except ValueError: + return None class Time(Scalar): @@ -79,4 +85,7 @@ class Time(Scalar): @classmethod def parse_value(cls, value): - return parse_time(value) + try: + return parse_time(value) + except ValueError: + return None diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 4e3f2ac2..67a3f6b2 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -27,7 +27,11 @@ class EnumOptions(BaseOptions): class EnumMeta(SubclassWithMeta_Meta): def __new__(cls, name, bases, classdict, **options): - enum = PyEnum(cls.__name__, OrderedDict(classdict, __eq__=eq_enum)) + enum_members = OrderedDict(classdict, __eq__=eq_enum) + # We remove the Meta attribute from the class to not collide + # with the enum values. + enum_members.pop('Meta', None) + enum = PyEnum(cls.__name__, enum_members) return SubclassWithMeta_Meta.__new__(cls, name, bases, OrderedDict(classdict, __enum__=enum), **options) def get(cls, value): @@ -60,8 +64,9 @@ class EnumMeta(SubclassWithMeta_Meta): class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)): @classmethod - def __init_subclass_with_meta__(cls, enum=None, **options): - _meta = EnumOptions(cls) + def __init_subclass_with_meta__(cls, enum=None, _meta=None, **options): + if not _meta: + _meta = EnumOptions(cls) _meta.enum = enum or cls.__enum__ _meta.deprecation_reason = options.pop('deprecation_reason', None) for key, value in _meta.enum.__members__.items(): diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 38173c79..dbfccc46 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -5,7 +5,6 @@ from .inputfield import InputField from .unmountedtype import UnmountedType from .utils import yank_fields_from_attrs - # For static type checking with Mypy MYPY = False if MYPY: @@ -14,7 +13,7 @@ if MYPY: class InputObjectTypeOptions(BaseOptions): fields = None # type: Dict[str, InputField] - create_container = None # type: Callable + container = None # type: InputObjectTypeContainer class InputObjectTypeContainer(dict, BaseType): @@ -23,8 +22,8 @@ class InputObjectTypeContainer(dict, BaseType): def __init__(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) - for key, value in self.items(): - setattr(self, key, value) + for key in self._meta.fields.keys(): + setattr(self, key, self.get(key, None)) def __init_subclass__(cls, *args, **kwargs): pass @@ -41,8 +40,9 @@ class InputObjectType(UnmountedType, BaseType): ''' @classmethod - def __init_subclass_with_meta__(cls, container=None, **options): - _meta = InputObjectTypeOptions(cls) + def __init_subclass_with_meta__(cls, container=None, _meta=None, **options): + if not _meta: + _meta = InputObjectTypeOptions(cls) fields = OrderedDict() for base in reversed(cls.__mro__): @@ -54,7 +54,8 @@ class InputObjectType(UnmountedType, BaseType): if container is None: container = type(cls.__name__, (InputObjectTypeContainer, cls), {}) _meta.container = container - super(InputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) + super(InputObjectType, cls).__init_subclass_with_meta__( + _meta=_meta, **options) @classmethod def get_type(cls): diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 25794d47..6b864e07 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -50,7 +50,8 @@ class Mutation(ObjectType): warn_deprecation(( "Please use {name}.Arguments instead of {name}.Input." "Input is now only used in ClientMutationID.\n" - "Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#mutation-input" + "Read more:" + " https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input" ).format(name=cls.__name__)) if input_class: @@ -72,10 +73,17 @@ class Mutation(ObjectType): _meta.resolver = resolver _meta.arguments = arguments - super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) + super(Mutation, cls).__init_subclass_with_meta__( + _meta=_meta, **options) @classmethod - def Field(cls, *args, **kwargs): + def Field(cls, name=None, description=None, deprecation_reason=None, required=False): return Field( - cls._meta.output, args=cls._meta.arguments, resolver=cls._meta.resolver + cls._meta.output, + args=cls._meta.arguments, + resolver=cls._meta.resolver, + name=name, + description=description, + deprecation_reason=deprecation_reason, + required=required, ) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 8066de3e..7fd513b2 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -1,6 +1,6 @@ import inspect -from graphql import GraphQLSchema, graphql, is_type +from graphql import GraphQLSchema, graphql, is_type, GraphQLObjectType from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective, GraphQLSkipDirective) from graphql.type.introspection import IntrospectionSchema @@ -12,6 +12,17 @@ from .objecttype import ObjectType from .typemap import TypeMap, is_graphene_type +def assert_valid_root_type(_type): + if _type is None: + return + is_graphene_objecttype = inspect.isclass( + _type) and issubclass(_type, ObjectType) + is_graphql_objecttype = isinstance(_type, GraphQLObjectType) + assert is_graphene_objecttype or is_graphql_objecttype, ( + "Type {} is not a valid ObjectType." + ).format(_type) + + class Schema(GraphQLSchema): ''' Schema Definition @@ -20,21 +31,23 @@ class Schema(GraphQLSchema): query and mutation (optional). ''' - def __init__(self, query=None, mutation=None, subscription=None, - directives=None, types=None, auto_camelcase=True): - assert inspect.isclass(query) and issubclass(query, ObjectType), ( - 'Schema query must be Object Type but got: {}.' - ).format(query) + def __init__(self, + query=None, + mutation=None, + subscription=None, + directives=None, + types=None, + auto_camelcase=True): + assert_valid_root_type(query) + assert_valid_root_type(mutation) + assert_valid_root_type(subscription) self._query = query self._mutation = mutation self._subscription = subscription self.types = types self.auto_camelcase = auto_camelcase if directives is None: - directives = [ - GraphQLIncludeDirective, - GraphQLSkipDirective - ] + directives = [GraphQLIncludeDirective, GraphQLSkipDirective] assert all(isinstance(d, GraphQLDirective) for d in directives), \ 'Schema directives must be List[GraphQLDirective] if provided but got: {}.'.format( @@ -61,7 +74,8 @@ class Schema(GraphQLSchema): ''' _type = super(Schema, self).get_type(type_name) if _type is None: - raise AttributeError('Type "{}" not found in the Schema'.format(type_name)) + raise AttributeError( + 'Type "{}" not found in the Schema'.format(type_name)) if isinstance(_type, GrapheneGraphQLType): return _type.graphene_type return _type @@ -73,7 +87,8 @@ class Schema(GraphQLSchema): return _type if is_graphene_type(_type): graphql_type = self.get_type(_type._meta.name) - assert graphql_type, "Type {} not found in this schema.".format(_type._meta.name) + assert graphql_type, "Type {} not found in this schema.".format( + _type._meta.name) assert graphql_type.graphene_type == _type return graphql_type raise Exception("{} is not a valid GraphQL type.".format(_type)) @@ -102,4 +117,8 @@ class Schema(GraphQLSchema): ] if self.types: initial_types += self.types - self._type_map = TypeMap(initial_types, auto_camelcase=self.auto_camelcase, schema=self) + self._type_map = TypeMap( + initial_types, + auto_camelcase=self.auto_camelcase, + schema=self + ) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index bfe491a3..b516e497 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -2,6 +2,8 @@ import datetime import pytz +from graphql import GraphQLError + from ..datetime import DateTime, Date, Time from ..objecttype import ObjectType from ..schema import Schema @@ -34,7 +36,7 @@ def test_datetime_query(): assert result.data == {'datetime': isoformat} -def test_datetime_query(): +def test_date_query(): now = datetime.datetime.now().replace(tzinfo=pytz.utc).date() isoformat = now.isoformat() @@ -53,6 +55,32 @@ def test_time_query(): assert not result.errors assert result.data == {'time': isoformat} +def test_bad_datetime_query(): + not_a_date = "Some string that's not a date" + + result = schema.execute('''{ datetime(in: "%s") }''' % not_a_date) + + assert len(result.errors) == 1 + assert isinstance(result.errors[0], GraphQLError) + assert result.data == None + +def test_bad_date_query(): + not_a_date = "Some string that's not a date" + + result = schema.execute('''{ date(in: "%s") }''' % not_a_date) + + assert len(result.errors) == 1 + assert isinstance(result.errors[0], GraphQLError) + assert result.data == None + +def test_bad_time_query(): + not_a_date = "Some string that's not a date" + + result = schema.execute('''{ time(at: "%s") }''' % not_a_date) + + assert len(result.errors) == 1 + assert isinstance(result.errors[0], GraphQLError) + assert result.data == None def test_datetime_query_variable(): now = datetime.datetime.now().replace(tzinfo=pytz.utc) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index c4cf3b85..fdd5f4e4 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -80,7 +80,8 @@ def test_enum_from_builtin_enum_accepts_lambda_description(): return 'meh' if value == Episode.NEWHOPE else None PyEpisode = PyEnum('PyEpisode', 'NEWHOPE,EMPIRE,JEDI') - Episode = Enum.from_enum(PyEpisode, description=custom_description, deprecation_reason=custom_deprecation_reason) + Episode = Enum.from_enum(PyEpisode, description=custom_description, + deprecation_reason=custom_deprecation_reason) class Query(ObjectType): foo = Episode() @@ -214,3 +215,19 @@ def test_enum_to_enum_comparison_should_differ(): assert RGB1.RED != RGB2.RED assert RGB1.GREEN != RGB2.GREEN assert RGB1.BLUE != RGB2.BLUE + + +def test_enum_skip_meta_from_members(): + class RGB1(Enum): + class Meta: + name = 'RGB' + + RED = 1 + GREEN = 2 + BLUE = 3 + + assert dict(RGB1._meta.enum.__members__) == { + 'RED': RGB1.RED, + 'GREEN': RGB1.GREEN, + 'BLUE': RGB1.BLUE, + } diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index 77b1eb0e..9f90055d 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -5,6 +5,8 @@ from ..inputfield import InputField from ..inputobjecttype import InputObjectType from ..objecttype import ObjectType from ..unmountedtype import UnmountedType +from ..scalars import String, Boolean +from ..schema import Schema class MyType(object): @@ -51,7 +53,8 @@ def test_ordered_fields_in_inputobjecttype(): field = MyScalar() asa = InputField(MyType) - assert list(MyInputObjectType._meta.fields.keys()) == ['b', 'a', 'field', 'asa'] + assert list(MyInputObjectType._meta.fields.keys()) == [ + 'b', 'a', 'field', 'asa'] def test_generate_inputobjecttype_unmountedtype(): @@ -86,7 +89,8 @@ def test_generate_inputobjecttype_inherit_abstracttype(): field2 = MyScalar(MyType) assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [InputField, InputField] + assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [ + InputField, InputField] def test_generate_inputobjecttype_inherit_abstracttype_reversed(): @@ -97,4 +101,34 @@ def test_generate_inputobjecttype_inherit_abstracttype_reversed(): field2 = MyScalar(MyType) assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [InputField, InputField] + assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [ + InputField, InputField] + + +def test_inputobjecttype_of_input(): + class Child(InputObjectType): + first_name = String() + last_name = String() + + @property + def full_name(self): + return "{} {}".format(self.first_name, self.last_name) + + class Parent(InputObjectType): + child = InputField(Child) + + class Query(ObjectType): + is_child = Boolean(parent=Parent()) + + def resolve_is_child(self, info, parent): + return isinstance(parent.child, Child) and parent.child.full_name == "Peter Griffin" + + schema = Schema(query=Query) + result = schema.execute('''query basequery { + isChild(parent: {child: {firstName: "Peter", lastName: "Griffin"}}) + } + ''') + assert not result.errors + assert result.data == { + 'isChild': True + } diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 91ab14d2..df17477d 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -6,6 +6,7 @@ from ..mutation import Mutation from ..objecttype import ObjectType from ..scalars import String from ..schema import Schema +from ..structures import NonNull def test_generate_mutation_no_args(): @@ -133,3 +134,27 @@ def test_mutation_no_fields_output(): 'name': None, } } + + +def test_mutation_allow_to_have_custom_args(): + class CreateUser(Mutation): + + class Arguments: + name = String() + + name = String() + + def mutate(self, info, name): + return CreateUser(name=name) + + class MyMutation(ObjectType): + create_user = CreateUser.Field( + description='Create a user', + deprecation_reason='Is deprecated', + required=True + ) + + field = MyMutation._meta.fields['create_user'] + assert field.description == 'Create a user' + assert field.deprecation_reason == 'Is deprecated' + assert field.type == NonNull(CreateUser) diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index e7c76d97..73d3823c 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -44,6 +44,8 @@ def test_generate_objecttype(): assert MyObjectType._meta.description == "Documentation" assert MyObjectType._meta.interfaces == tuple() assert MyObjectType._meta.fields == {} + assert repr( + MyObjectType) == ">" def test_generate_objecttype_with_meta(): @@ -65,7 +67,6 @@ def test_generate_lazy_objecttype(): class InnerObjectType(ObjectType): field = Field(MyType) - assert MyObjectType._meta.name == "MyObjectType" example_field = MyObjectType._meta.fields['example'] @@ -115,7 +116,8 @@ def test_generate_objecttype_inherit_abstracttype(): assert MyObjectType._meta.interfaces == () assert MyObjectType._meta.name == "MyObjectType" assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field] + assert list(map(type, MyObjectType._meta.fields.values())) == [ + Field, Field] def test_generate_objecttype_inherit_abstracttype_reversed(): @@ -129,7 +131,8 @@ def test_generate_objecttype_inherit_abstracttype_reversed(): assert MyObjectType._meta.interfaces == () assert MyObjectType._meta.name == "MyObjectType" assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2'] - assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field] + assert list(map(type, MyObjectType._meta.fields.values())) == [ + Field, Field] def test_generate_objecttype_unmountedtype(): @@ -145,7 +148,8 @@ def test_parent_container_get_fields(): def test_parent_container_interface_get_fields(): - assert list(ContainerWithInterface._meta.fields.keys()) == ['ifield', 'field1', 'field2'] + assert list(ContainerWithInterface._meta.fields.keys()) == [ + 'ifield', 'field1', 'field2'] def test_objecttype_as_container_only_args(): @@ -182,7 +186,8 @@ def test_objecttype_as_container_invalid_kwargs(): with pytest.raises(TypeError) as excinfo: Container(unexisting_field="3") - assert "'unexisting_field' is an invalid keyword argument for Container" == str(excinfo.value) + assert "'unexisting_field' is an invalid keyword argument for Container" == str( + excinfo.value) def test_objecttype_container_benchmark(benchmark): @@ -238,7 +243,6 @@ def test_objecttype_no_fields_output(): def resolve_user(self, info): return User() - schema = Schema(query=Query) result = schema.execute(''' query basequery { user { @@ -252,3 +256,12 @@ def test_objecttype_no_fields_output(): 'name': None, } } + + +def test_abstract_objecttype_can_str(): + class MyObjectType(ObjectType): + class Meta: + abstract = True + field = MyScalar() + + assert str(MyObjectType) == "MyObjectType" diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 082f25bd..c0626a1a 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -1,9 +1,11 @@ +import pytest from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, GraphQLField, GraphQLInputObjectField, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLObjectType, GraphQLString) +from ..structures import List, NonNull from ..dynamic import Dynamic from ..enum import Enum from ..field import Field @@ -11,8 +13,8 @@ from ..inputfield import InputField from ..inputobjecttype import InputObjectType from ..interface import Interface from ..objecttype import ObjectType -from ..scalars import String -from ..typemap import TypeMap +from ..scalars import String, Int +from ..typemap import TypeMap, resolve_type def test_enum(): @@ -38,7 +40,8 @@ def test_enum(): assert graphql_enum.description == 'Description' values = graphql_enum.values assert values == [ - GraphQLEnumValue(name='foo', value=1, description='Description foo=1', deprecation_reason='Is deprecated'), + GraphQLEnumValue(name='foo', value=1, description='Description foo=1', + deprecation_reason='Is deprecated'), GraphQLEnumValue(name='bar', value=2, description='Description bar=2'), ] @@ -46,7 +49,8 @@ def test_enum(): def test_objecttype(): class MyObjectType(ObjectType): '''Description''' - foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') + foo = String(bar=String(description='Argument description', + default_value='x'), description='Field description') bar = String(name='gizmo') def resolve_foo(self, bar): @@ -91,8 +95,10 @@ def test_dynamic_objecttype(): def test_interface(): class MyInterface(Interface): '''Description''' - foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') - bar = String(name='gizmo', first_arg=String(), other_arg=String(name='oth_arg')) + foo = String(bar=String(description='Argument description', + default_value='x'), description='Field description') + bar = String(name='gizmo', first_arg=String(), + other_arg=String(name='oth_arg')) own = Field(lambda: MyInterface) def resolve_foo(self, args, info): @@ -119,10 +125,18 @@ def test_interface(): def test_inputobject(): + class OtherObjectType(InputObjectType): + thingy = NonNull(Int) + + class MyInnerObjectType(InputObjectType): + some_field = String() + some_other_field = List(OtherObjectType) + class MyInputObjectType(InputObjectType): '''Description''' foo_bar = String(description='Field description') bar = String(name='gizmo') + baz = NonNull(MyInnerObjectType) own = InputField(lambda: MyInputObjectType) def resolve_foo_bar(self, args, info): @@ -135,15 +149,28 @@ def test_inputobject(): assert graphql_type.name == 'MyInputObjectType' assert graphql_type.description == 'Description' - # Container - container = graphql_type.create_container({'bar': 'oh!'}) + other_graphql_type = typemap['OtherObjectType'] + inner_graphql_type = typemap['MyInnerObjectType'] + container = graphql_type.create_container({ + 'bar': 'oh!', + 'baz': inner_graphql_type.create_container({ + 'some_other_field': [ + other_graphql_type.create_container({'thingy': 1}), + other_graphql_type.create_container({'thingy': 2}) + ] + }) + }) assert isinstance(container, MyInputObjectType) assert 'bar' in container assert container.bar == 'oh!' assert 'foo_bar' not in container + assert container.foo_bar is None + assert container.baz.some_field is None + assert container.baz.some_other_field[0].thingy == 1 + assert container.baz.some_other_field[1].thingy == 2 fields = graphql_type.fields - assert list(fields.keys()) == ['fooBar', 'gizmo', 'own'] + assert list(fields.keys()) == ['fooBar', 'gizmo', 'baz', 'own'] own_field = fields['own'] assert own_field.type == graphql_type foo_field = fields['fooBar'] @@ -206,3 +233,22 @@ def test_objecttype_with_possible_types(): assert graphql_type.is_type_of assert graphql_type.is_type_of({}, None) is True assert graphql_type.is_type_of(MyObjectType(), None) is False + + +def test_resolve_type_with_missing_type(): + class MyObjectType(ObjectType): + foo_bar = String() + + class MyOtherObjectType(ObjectType): + fizz_buzz = String() + + def resolve_type_func(root, info): + return MyOtherObjectType + + typemap = TypeMap([MyObjectType]) + with pytest.raises(AssertionError) as excinfo: + resolve_type( + resolve_type_func, typemap, 'MyOtherObjectType', {}, {} + ) + + assert 'MyOtherObjectTyp' in str(excinfo.value) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index d4a4d157..b2bc4a0e 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -46,7 +46,10 @@ def resolve_type(resolve_type_func, map, type_name, root, info): if inspect.isclass(_type) and issubclass(_type, ObjectType): graphql_type = map.get(_type._meta.name) - assert graphql_type and graphql_type.graphene_type == _type, ( + assert graphql_type, "Can't find type {} in schema".format( + _type._meta.name + ) + assert graphql_type.graphene_type == _type, ( 'The type {} does not match with the associated graphene type {}.' ).format(_type, graphql_type.graphene_type) return graphql_type diff --git a/graphene/utils/str_converters.py b/graphene/utils/str_converters.py index ae8ceffe..6fcdfb7b 100644 --- a/graphene/utils/str_converters.py +++ b/graphene/utils/str_converters.py @@ -1,13 +1,13 @@ import re -# From this response in Stackoverflow +# Adapted from this response in Stackoverflow # http://stackoverflow.com/a/19053800/1072990 def to_camel_case(snake_str): components = snake_str.split('_') # We capitalize the first letter of each component except the first one - # with the 'title' method and join them together. - return components[0] + "".join(x.title() if x else '_' for x in components[1:]) + # with the 'capitalize' method and join them together. + return components[0] + ''.join(x.capitalize() if x else '_' for x in components[1:]) # From this response in Stackoverflow diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index 9226e418..61205be0 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -6,9 +6,15 @@ from .props import props class SubclassWithMeta_Meta(InitSubclassMeta): + _meta = None + + def __str__(cls): + if cls._meta: + return cls._meta.name + return cls.__name__ def __repr__(cls): - return cls._meta.name + return "<{} meta={}>".format(cls.__name__, repr(cls._meta)) class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): @@ -24,7 +30,8 @@ class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): elif isclass(_Meta): _meta_props = props(_Meta) else: - raise Exception("Meta have to be either a class or a dict. Received {}".format(_Meta)) + raise Exception( + "Meta have to be either a class or a dict. Received {}".format(_Meta)) delattr(cls, "Meta") options = dict(meta_options, **_meta_props) diff --git a/graphene/utils/tests/test_str_converters.py b/graphene/utils/tests/test_str_converters.py index 2ee7d7a5..11f7e155 100644 --- a/graphene/utils/tests/test_str_converters.py +++ b/graphene/utils/tests/test_str_converters.py @@ -16,6 +16,7 @@ def test_camel_case(): assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane' assert to_camel_case('snakes_on_a__plane') == 'snakesOnA_Plane' assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria' + assert to_camel_case('field_i18n') == 'fieldI18n' def test_to_const(): diff --git a/setup.py b/setup.py index 8b54d6d7..102f46c4 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ setup( keywords='api graphql protocol rest relay graphene', - packages=find_packages(exclude=['tests', 'tests.*']), + packages=find_packages(exclude=['tests', 'tests.*', 'examples']), install_requires=[ 'six>=1.10.0,<2', diff --git a/tox.ini b/tox.ini index 48d4f5fc..f2eccefb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py33,py34,py35,pypy +envlist = flake8,py27,py33,py34,py35,py36,pypy skipsdist = true [testenv]