diff --git a/.travis.yml b/.travis.yml index ca118d8c..b313bff4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ before_install: install: - | if [ "$TEST_TYPE" = build ]; then - pip install pytest pytest-cov pytest-benchmark coveralls six + pip install pytest pytest-cov pytest-benchmark coveralls six pytz iso8601 pip install -e . python setup.py develop elif [ "$TEST_TYPE" = lint ]; then diff --git a/README.md b/README.md index 7b0ab346..c6f924e3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -You are in the `next` unreleased version of Graphene (`1.0.dev`). -Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade. +Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graphene `1.0`. --- @@ -32,7 +31,7 @@ Graphene has multiple integrations with different frameworks: For instaling graphene, just run this command in your shell ```bash -pip install "graphene>=1.0.dev" +pip install "graphene>=1.0" ``` ## 1.0 Upgrade Guide diff --git a/README.rst b/README.rst index 1d5f34f4..72a6a020 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,4 @@ -You are in the ``next`` unreleased version of Graphene (``1.0.dev``). -Please read `UPGRADE-v1.0.md`_ to learn how to upgrade. +Please read `UPGRADE-v1.0.md`_ to learn how to upgrade to Graphene ``1.0``. -------------- @@ -41,7 +40,7 @@ For instaling graphene, just run this command in your shell .. code:: bash - pip install "graphene>=1.0.dev" + pip install "graphene>=1.0" 1.0 Upgrade Guide ----------------- diff --git a/docs/conf.py b/docs/conf.py index 8f79896f..9d902f9a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,7 +73,7 @@ author = u'Syrus Akbary' # The short X.Y version. version = u'1.0' # The full version, including alpha/beta/rc tags. -release = u'1.0.dev' +release = u'1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -133,9 +133,14 @@ todo_include_todos = True # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' -if on_rtd: - html_theme = 'sphinx_rtd_theme' +# html_theme = 'alabaster' +# if on_rtd: +# html_theme = 'sphinx_rtd_theme' +import sphinx_graphene_theme + +html_theme = "sphinx_graphene_theme" + +html_theme_path = [sphinx_graphene_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -149,7 +154,7 @@ if on_rtd: # The name for this set of Sphinx documents. # " v documentation" by default. # -# html_title = u'Graphene v1.0.dev' +# html_title = u'Graphene v1.0' # A shorter title for the navigation bar. Default is the same as html_title. # diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4fad567b..5a93ff30 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -14,7 +14,7 @@ Project setup .. code:: bash - pip install graphene>=1.0 + pip install "graphene>=1.0" Creating a basic Schema ----------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..5de8cc6b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +# Docs template +https://github.com/graphql-python/graphene-python.org/archive/docs.zip diff --git a/docs/types/abstracttypes.rst b/docs/types/abstracttypes.rst index cd6b1ece..5e85a804 100644 --- a/docs/types/abstracttypes.rst +++ b/docs/types/abstracttypes.rst @@ -32,7 +32,7 @@ plus the ones defined in ``UserFields``. pass -.. code:: graphql +.. code:: type User { name: String diff --git a/docs/types/index.rst b/docs/types/index.rst index aaf87760..41b34f27 100644 --- a/docs/types/index.rst +++ b/docs/types/index.rst @@ -10,4 +10,5 @@ Types Reference interfaces abstracttypes objecttypes + schema mutations diff --git a/docs/types/interfaces.rst b/docs/types/interfaces.rst index 541f808a..ee0410a7 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -5,6 +5,7 @@ An Interface contains the essential fields that will be implemented among multiple ObjectTypes. The basics: + - Each Interface is a Python class that inherits from ``graphene.Interface``. - Each attribute of the Interface represents a GraphQL field. @@ -43,7 +44,7 @@ time. The above types would have the following representation in a schema: -.. code:: graphql +.. code:: interface Character { name: String diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index cb2d780b..3f39658a 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -53,7 +53,7 @@ Executing the Mutation Then, if we query (``schema.execute(query_str)``) the following: -.. code:: graphql +.. code:: mutation myFirstMutation { createPerson(name:"Peter") { diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 9d2c44cc..4006c61a 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -8,7 +8,7 @@ querying. The basics: - Each ObjectType is a Python class that inherits - ``graphene.ObjectType`` or inherits an implemented `Interface`_. + ``graphene.ObjectType``. - Each attribute of the ObjectType represents a ``Field``. Quick example @@ -36,7 +36,7 @@ Field. The above ``Person`` ObjectType would have the following representation in a schema: -.. code:: graphql +.. code:: type Person { firstName: String @@ -55,6 +55,11 @@ otherwise, the ``resolve_{field_name}`` within the ``ObjectType``. By default a resolver will take the ``args``, ``context`` and ``info`` arguments. +NOTE: The class resolvers in a ``ObjectType`` are treated as ``staticmethod``s +always, so the first argument in the resolver: ``self`` (or ``root``) doesn't +need to be an actual instance of the ``ObjectType``. + + Quick example ~~~~~~~~~~~~~ diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 9c7e037b..d8e22b54 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -2,6 +2,7 @@ Scalars ======= Graphene define the following base Scalar Types: + - ``graphene.String`` - ``graphene.Int`` - ``graphene.Float`` @@ -9,6 +10,7 @@ Graphene define the following base Scalar Types: - ``graphene.ID`` Graphene also provides custom scalars for Dates and JSON: + - ``graphene.types.datetime.DateTime`` - ``graphene.types.json.JSONString`` diff --git a/docs/types/schema.rst b/docs/types/schema.rst new file mode 100644 index 00000000..7c7ad73d --- /dev/null +++ b/docs/types/schema.rst @@ -0,0 +1,81 @@ +Schema +====== + +A Schema is created by supplying the root types of each type of operation, query and mutation (optional). +A schema definition is then supplied to the validator and executor. + +.. code:: python + my_schema = Schema( + query=MyRootQuery, + mutation=MyRootMutation, + ) + +Types +----- + +There are some cases where the schema could not access all the types that we plan to have. +For example, when a field returns an ``Interface``, the schema doesn't know any of the +implementations. + +In this case, we would need to use the ``types`` argument when creating the Schema. + + +.. code:: python + + my_schema = Schema( + query=MyRootQuery, + types=[SomeExtraObjectType, ] + ) + + +Querying +-------- + +If you need to query a schema, you can directly call the ``execute`` method on it. + + +.. code:: python + + my_schema.execute('{ lastName }') + + +Auto CamelCase field names +-------------------------- + +By default all field and argument names (that are not +explicitly set with the ``name`` arg) will be converted from +`snake_case` to `camelCase` (`as the API is usually being consumed by a js/mobile client`) + +So, for example if we have the following ObjectType + +.. code:: python + + class Person(graphene.ObjectType): + last_name = graphene.String() + other_name = graphene.String(name='_other_Name') + +Then the ``last_name`` field name is converted to ``lastName``. + +In the case we don't want to apply any transformation, we can specify +the field name with the ``name`` argument. So ``other_name`` field name +would be converted to ``_other_Name`` (without any other transformation). + +So, you would need to query with: + +.. code:: + + { + lastName + _other_Name + } + + +If you want to disable this behavior, you set use the ``auto_camelcase`` argument +to ``False`` when you create the Schema. + +.. code:: python + + my_schema = Schema( + query=MyRootQuery, + auto_camelcase=False, + ) diff --git a/graphene/__init__.py b/graphene/__init__.py index 5afc4f97..7a01ed16 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 0, 0, 'alpha', 0) +VERSION = (1, 0, 2, 'final', 0) __version__ = get_version(VERSION) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index a1039e43..e63478e5 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -95,13 +95,13 @@ class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): class IterableConnectionField(Field): def __init__(self, type, *args, **kwargs): + kwargs.setdefault('before', String()) + kwargs.setdefault('after', String()) + kwargs.setdefault('first', Int()) + kwargs.setdefault('last', Int()) super(IterableConnectionField, self).__init__( type, *args, - before=String(), - after=String(), - first=Int(), - last=Int(), **kwargs ) @@ -117,21 +117,25 @@ class IterableConnectionField(Field): ).format(str(self), connection_type) return connection_type - @staticmethod - def connection_resolver(resolver, connection, root, args, context, info): - iterable = resolver(root, args, context, info) - assert isinstance(iterable, Iterable), ( - 'Resolved value from the connection field have to be iterable. ' + @classmethod + def connection_resolver(cls, resolver, connection, root, args, context, info): + resolved = resolver(root, args, context, info) + + if isinstance(resolved, connection): + return resolved + + assert isinstance(resolved, Iterable), ( + 'Resolved value from the connection field have to be iterable or instance of {}. ' 'Received "{}"' - ).format(iterable) + ).format(connection, resolved) connection = connection_from_list( - iterable, + resolved, args, connection_type=connection, edge_type=connection.Edge, pageinfo_type=PageInfo ) - connection.iterable = iterable + connection.iterable = resolved return connection def get_resolver(self, parent_resolver): diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index d2279254..18d890c1 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,6 +1,6 @@ -from ...types import AbstractType, Field, List, NonNull, ObjectType, String -from ..connection import Connection, PageInfo +from ...types import AbstractType, Field, List, NonNull, ObjectType, String, Argument, Int +from ..connection import Connection, PageInfo, ConnectionField from ..node import Node @@ -109,3 +109,32 @@ def test_pageinfo(): assert PageInfo._meta.name == 'PageInfo' fields = PageInfo._meta.fields assert list(fields.keys()) == ['has_next_page', 'has_previous_page', 'start_cursor', 'end_cursor'] + + +def test_connectionfield(): + class MyObjectConnection(Connection): + class Meta: + node = MyObject + + field = ConnectionField(MyObjectConnection) + assert field.args == { + 'before': Argument(String), + 'after': Argument(String), + 'first': Argument(Int), + 'last': Argument(Int), + } + + +def test_connectionfield_custom_args(): + class MyObjectConnection(Connection): + class Meta: + node = MyObject + + field = ConnectionField(MyObjectConnection, before=String(required=True), extra=String()) + assert field.args == { + 'before': Argument(NonNull(String)), + 'after': Argument(String), + 'first': Argument(Int), + 'last': Argument(Int), + 'extra': Argument(String), + } diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 7a197f27..cc1f12ce 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -3,7 +3,7 @@ from collections import OrderedDict from graphql_relay.utils import base64 from ...types import ObjectType, Schema, String -from ..connection import ConnectionField +from ..connection import ConnectionField, PageInfo from ..node import Node letter_chars = ['A', 'B', 'C', 'D', 'E'] @@ -19,11 +19,26 @@ class Letter(ObjectType): class Query(ObjectType): letters = ConnectionField(Letter) + connection_letters = ConnectionField(Letter) + + node = Node.Field() def resolve_letters(self, args, context, info): return list(letters.values()) - node = Node.Field() + def resolve_connection_letters(self, args, context, info): + return Letter.Connection( + page_info=PageInfo( + has_next_page=True, + has_previous_page=False + ), + edges=[ + Letter.Connection.Edge( + node=Letter(id=0, letter='A'), + cursor='a-cursor' + ), + ] + ) schema = Schema(Query) @@ -176,3 +191,40 @@ def test_returns_all_elements_if_cursors_are_on_the_outside(): def test_returns_no_elements_if_cursors_cross(): check('before: "{}" after: "{}"'.format(base64('arrayconnection:%s' % 2), base64('arrayconnection:%s' % 4)), '') + + +def test_connection_type_nodes(): + result = schema.execute(''' + { + connectionLetters { + edges { + node { + id + letter + } + cursor + } + pageInfo { + hasPreviousPage + hasNextPage + } + } + } + ''') + + assert not result.errors + assert result.data == { + 'connectionLetters': { + 'edges': [{ + 'node': { + 'id': 'TGV0dGVyOjA=', + 'letter': 'A', + }, + 'cursor': 'a-cursor', + }], + 'pageInfo': { + 'hasPreviousPage': False, + 'hasNextPage': True, + } + } + } diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 5a2be210..4c14c684 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -84,6 +84,9 @@ def test_mutation(): assert isinstance(field.args['input'], Argument) assert isinstance(field.args['input'].type, NonNull) assert field.args['input'].type.of_type == SaySomething.Input + assert isinstance(fields['client_mutation_id'], Field) + assert fields['client_mutation_id'].name == 'clientMutationId' + assert fields['client_mutation_id'].type == String def test_mutation_input(): @@ -132,7 +135,7 @@ def test_node_query(): def test_edge_query(): executed = schema.execute( - 'mutation a { other(input: {clientMutationId:"1"}) { myNodeEdge { cursor node { name }} } }' + 'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }' ) assert not executed.errors - assert dict(executed.data) == {'other': {'myNodeEdge': {'cursor': '1', 'node': {'name': 'name'}}}} + assert dict(executed.data) == {'other': {'clientMutationId': '1', 'myNodeEdge': {'cursor': '1', 'node': {'name': 'name'}}}} diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 07dc1d1d..49784b10 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -18,6 +18,14 @@ class Argument(OrderedType): self.default_value = default_value self.description = description + def __eq__(self, other): + return isinstance(other, Argument) and ( + self.name == other.name, + self.type == other.type, + self.default_value == other.default_value, + self.description == other.description + ) + def to_arguments(args, extra_args): from .unmountedtype import UnmountedType diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 9baa731e..3dfbbb97 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -36,4 +36,4 @@ class DateTime(Scalar): @staticmethod def parse_value(value): - return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") + return iso8601.parse_date(value) diff --git a/graphene/types/field.py b/graphene/types/field.py index 531c2f5c..3b9347c0 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -8,6 +8,9 @@ from .structures import NonNull from .unmountedtype import UnmountedType +base_type = type + + def source_resolver(source, root, args, context, info): resolved = getattr(root, source, None) if inspect.isfunction(resolved): @@ -19,7 +22,8 @@ class Field(OrderedType): def __init__(self, type, args=None, resolver=None, source=None, deprecation_reason=None, name=None, description=None, - required=False, _creation_counter=None, **extra_args): + required=False, _creation_counter=None, default_value=None, + **extra_args): super(Field, self).__init__(_creation_counter=_creation_counter) assert not args or isinstance(args, Mapping), ( 'Arguments in a field have to be a mapping, received "{}".' @@ -27,6 +31,9 @@ class Field(OrderedType): assert not (source and resolver), ( 'A Field cannot have a source and a resolver in at the same time.' ) + assert not callable(default_value), ( + 'The default value can not be a function but received "{}".' + ).format(base_type(default_value)) if required: type = NonNull(type) @@ -49,6 +56,7 @@ class Field(OrderedType): self.resolver = resolver self.deprecation_reason = deprecation_reason self.description = description + self.default_value = default_value @property def type(self): diff --git a/graphene/types/interface.py b/graphene/types/interface.py index f5a2d61a..cc8361e6 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -48,7 +48,11 @@ class Interface(six.with_metaclass(InterfaceMeta)): when the field is resolved. ''' - resolve_type = None + @classmethod + def resolve_type(cls, instance, context, info): + from .objecttype import ObjectType + if isinstance(instance, ObjectType): + return type(instance) def __init__(self, *args, **kwargs): raise Exception("An Interface cannot be intitialized") diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index d54740ec..f6f5b19b 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -3,6 +3,7 @@ from functools import partial 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 @@ -22,6 +23,7 @@ class MutationMeta(ObjectTypeMeta): field_args = props(input_class) if input_class else {} resolver = getattr(cls, 'mutate', None) assert resolver, 'All mutations must define a mutate method in it' + resolver = get_unbound_function(resolver) cls.Field = partial(Field, cls, args=field_args, resolver=resolver) return cls diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index ca3f6b0c..f06dbf5e 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -63,10 +63,7 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta)): have a name, but most importantly describe their fields. ''' - @classmethod - def is_type_of(cls, root, context, info): - if isinstance(root, cls): - return True + is_type_of = None def __init__(self, *args, **kwargs): # ObjectType acting as container diff --git a/graphene/types/structures.py b/graphene/types/structures.py index 6cf0e4aa..6c9c0e7e 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -27,6 +27,13 @@ class List(Structure): def __str__(self): return '[{}]'.format(self.of_type) + def __eq__(self, other): + return isinstance(other, List) and ( + self.of_type == other.of_type and + self.args == other.args and + self.kwargs == other.kwargs + ) + class NonNull(Structure): ''' @@ -49,3 +56,10 @@ class NonNull(Structure): def __str__(self): return '{}!'.format(self.of_type) + + def __eq__(self, other): + return isinstance(other, NonNull) and ( + self.of_type == other.of_type and + self.args == other.args and + self.kwargs == other.kwargs + ) diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py new file mode 100644 index 00000000..34ed3144 --- /dev/null +++ b/graphene/types/tests/test_argument.py @@ -0,0 +1,26 @@ +import pytest + +from ..argument import Argument +from ..structures import NonNull +from ..scalars import String + + +def test_argument(): + arg = Argument(String, default_value='a', description='desc', name='b') + assert arg.type == String + assert arg.default_value == 'a' + assert arg.description == 'desc' + assert arg.name == 'b' + + +def test_argument_comparasion(): + arg1 = Argument(String, name='Hey', description='Desc', default_value='default') + arg2 = Argument(String, name='Hey', description='Desc', default_value='default') + + assert arg1 == arg2 + assert arg1 != String() + + +def test_argument_required(): + arg = Argument(String, required=True) + assert arg.type == NonNull(String) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py new file mode 100644 index 00000000..f55cd8c6 --- /dev/null +++ b/graphene/types/tests/test_datetime.py @@ -0,0 +1,41 @@ +import datetime +import pytz + +from ..datetime import DateTime +from ..objecttype import ObjectType +from ..schema import Schema + + +class Query(ObjectType): + datetime = DateTime(_in=DateTime(name='in')) + + def resolve_datetime(self, args, context, info): + _in = args.get('in') + return _in + +schema = Schema(query=Query) + + +def test_datetime_query(): + now = datetime.datetime.now().replace(tzinfo=pytz.utc) + isoformat = now.isoformat() + + result = schema.execute('''{ datetime(in: "%s") }'''%isoformat) + assert not result.errors + assert result.data == { + 'datetime': isoformat + } + + +def test_datetime_query_variable(): + now = datetime.datetime.now().replace(tzinfo=pytz.utc) + isoformat = now.isoformat() + + result = schema.execute( + '''query Test($date: DateTime){ datetime(in: $date) }''', + variable_values={'date': isoformat} + ) + assert not result.errors + assert result.data == { + 'datetime': isoformat + } diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index 5883e588..7ca557ba 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -16,19 +16,22 @@ def test_field_basic(): resolver = lambda: None deprecation_reason = 'Deprecated now' description = 'My Field' + my_default='something' field = Field( MyType, name='name', args=args, resolver=resolver, description=description, - deprecation_reason=deprecation_reason + deprecation_reason=deprecation_reason, + default_value=my_default, ) assert field.name == 'name' assert field.args == args assert field.resolver == resolver assert field.deprecation_reason == deprecation_reason assert field.description == description + assert field.default_value == my_default def test_field_required(): @@ -38,6 +41,15 @@ def test_field_required(): assert field.type.of_type == MyType +def test_field_default_value_not_callable(): + MyType = object() + try: + Field(MyType, default_value=lambda: True) + except AssertionError as e: + # substring comparison for py 2/3 compatibility + assert 'The default value can not be a function but received' in str(e) + + def test_field_source(): MyType = object() field = Field(MyType, source='value') diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py new file mode 100644 index 00000000..ef6425a9 --- /dev/null +++ b/graphene/types/tests/test_json.py @@ -0,0 +1,39 @@ +import json + +from ..json import JSONString +from ..objecttype import ObjectType +from ..schema import Schema + + +class Query(ObjectType): + json = JSONString(input=JSONString()) + + def resolve_json(self, args, context, info): + input = args.get('input') + return input + +schema = Schema(query=Query) + + +def test_jsonstring_query(): + json_value = '{"key": "value"}' + + json_value_quoted = json_value.replace('"', '\\"') + result = schema.execute('''{ json(input: "%s") }'''%json_value_quoted) + assert not result.errors + assert result.data == { + 'json': json_value + } + + +def test_jsonstring_query_variable(): + json_value = '{"key": "value"}' + + result = schema.execute( + '''query Test($json: JSONString){ json(input: $json) }''', + variable_values={'json': json_value} + ) + assert not result.errors + assert result.data == { + 'json': json_value + } diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index ceffc2ba..2af6f4fd 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -2,6 +2,8 @@ import pytest from ..mutation import Mutation from ..objecttype import ObjectType +from ..schema import Schema +from ..scalars import String def test_generate_mutation_no_args(): @@ -17,26 +19,6 @@ def test_generate_mutation_no_args(): assert MyMutation.Field().resolver == MyMutation.mutate -# def test_generate_mutation_with_args(): -# class MyMutation(Mutation): -# '''Documentation''' -# class Input: -# s = String() - -# @classmethod -# def mutate(cls, *args, **kwargs): -# pass - -# graphql_type = MyMutation._meta.graphql_type -# field = MyMutation.Field() -# assert graphql_type.name == "MyMutation" -# assert graphql_type.description == "Documentation" -# assert isinstance(field, Field) -# assert field.type == MyMutation._meta.graphql_type -# assert 's' in field.args -# assert field.args['s'].type == String - - def test_generate_mutation_with_meta(): class MyMutation(Mutation): @@ -59,3 +41,35 @@ def test_mutation_raises_exception_if_no_mutate(): pass assert "All mutations must define a mutate method in it" == str(excinfo.value) + + +def test_mutation_execution(): + class CreateUser(Mutation): + class Input: + name = String() + + name = String() + + def mutate(self, args, context, info): + name = args.get('name') + return CreateUser(name=name) + + class Query(ObjectType): + a = String() + + class MyMutation(ObjectType): + create_user = CreateUser.Field() + + schema = Schema(query=Query, mutation=MyMutation) + result = schema.execute(''' mutation mymutation { + createUser(name:"Peter") { + name + } + } + ''') + assert not result.errors + assert result.data == { + 'createUser': { + 'name': "Peter" + } + } diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 3059776a..4f9d8810 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -1,8 +1,9 @@ import json from functools import partial -from graphql import Source, execute, parse +from graphql import Source, execute, parse, GraphQLError +from ..field import Field from ..inputfield import InputField from ..inputobjecttype import InputObjectType from ..objecttype import ObjectType @@ -22,6 +23,53 @@ def test_query(): assert executed.data == {'hello': 'World'} +def test_query_default_value(): + class MyType(ObjectType): + field = String() + + class Query(ObjectType): + hello = Field(MyType, default_value=MyType(field='something else!')) + + hello_schema = Schema(Query) + + executed = hello_schema.execute('{ hello { field } }') + assert not executed.errors + assert executed.data == {'hello': {'field': 'something else!'}} + + +def test_query_wrong_default_value(): + class MyType(ObjectType): + field = String() + + @classmethod + def is_type_of(cls, root, context, info): + return isinstance(root, MyType) + + class Query(ObjectType): + hello = Field(MyType, default_value='hello') + + hello_schema = Schema(Query) + + 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.data == {'hello': None} + + +def test_query_default_value_ignored_by_resolver(): + class MyType(ObjectType): + field = String() + + class Query(ObjectType): + hello = Field(MyType, default_value='hello', resolver=lambda *_: MyType(field='no default.')) + + hello_schema = Schema(Query) + + executed = hello_schema.execute('{ hello { field } }') + assert not executed.errors + assert executed.data == {'hello': {'field': 'no default.'}} + + def test_query_resolve_function(): class Query(ObjectType): hello = String() @@ -109,6 +157,30 @@ def test_query_middlewares(): assert executed.data == {'hello': 'dlroW', 'other': 'rehto'} +def test_objecttype_on_instances(): + class Ship: + def __init__(self, name): + self.name = name + + class ShipType(ObjectType): + name = String(description="Ship name", required=True) + + def resolve_name(self, context, args, 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, context, args, info): + return Ship(name='xwing') + + schema = Schema(query=Query) + executed = schema.execute('{ ship { name } }') + assert not executed.errors + assert executed.data == {'ship': {'name': 'xwing'}} + + def test_big_list_query_benchmark(benchmark): big_list = range(10000) diff --git a/graphene/types/tests/test_structures.py b/graphene/types/tests/test_structures.py new file mode 100644 index 00000000..9027895e --- /dev/null +++ b/graphene/types/tests/test_structures.py @@ -0,0 +1,44 @@ +import pytest + +from ..structures import List, NonNull +from ..scalars import String + + +def test_list(): + _list = List(String) + assert _list.of_type == String + assert str(_list) == '[String]' + + +def test_nonnull(): + nonnull = NonNull(String) + assert nonnull.of_type == String + assert str(nonnull) == 'String!' + + +def test_list_comparasion(): + list1 = List(String) + list2 = List(String) + list3 = List(None) + + list1_argskwargs = List(String, None, b=True) + list2_argskwargs = List(String, None, b=True) + + assert list1 == list2 + assert list1 != list3 + assert list1_argskwargs == list2_argskwargs + assert list1 != list1_argskwargs + + +def test_nonnull_comparasion(): + nonnull1 = NonNull(String) + nonnull2 = NonNull(String) + nonnull3 = NonNull(None) + + nonnull1_argskwargs = NonNull(String, None, b=True) + nonnull2_argskwargs = NonNull(String, None, b=True) + + assert nonnull1 == nonnull2 + assert nonnull1 != nonnull3 + assert nonnull1_argskwargs == nonnull2_argskwargs + assert nonnull1 != nonnull1_argskwargs diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index af193a9b..c2b47279 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -6,9 +6,11 @@ from graphql import (GraphQLArgument, GraphQLBoolean, GraphQLField, GraphQLFloat, GraphQLID, GraphQLInputObjectField, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString) from graphql.type import GraphQLEnumValue +from graphql.execution.executor import get_default_resolve_type_fn from graphql.type.typemap import GraphQLTypeMap from ..utils.str_converters import to_camel_case +from ..utils.get_unbound_function import get_unbound_function from .dynamic import Dynamic from .enum import Enum from .inputobjecttype import InputObjectType @@ -26,11 +28,14 @@ def is_graphene_type(_type): return True -def resolve_type(resolve_type_func, map, root, args, info): - _type = resolve_type_func(root, args, info) +def resolve_type(resolve_type_func, map, root, context, info): + _type = resolve_type_func(root, context, info) # assert inspect.isclass(_type) and issubclass(_type, ObjectType), ( # 'Received incompatible type "{}".'.format(_type) # ) + if not _type: + return get_default_resolve_type_fn(root, context, info, info.return_type) + if inspect.isclass(_type) and issubclass(_type, ObjectType): graphql_type = map.get(_type._meta.name) assert graphql_type and graphql_type.graphene_type == _type @@ -190,8 +195,8 @@ class TypeMap(GraphQLTypeMap): return to_camel_case(name) return name - def default_resolver(self, attname, root, *_): - return getattr(root, attname, None) + def default_resolver(self, attname, default_value, root, *_): + return getattr(root, attname, default_value) def construct_fields_for_type(self, map, type, is_input_type=False): fields = OrderedDict() @@ -224,7 +229,7 @@ class TypeMap(GraphQLTypeMap): _field = GraphQLField( field_type, args=args, - resolver=field.get_resolver(self.get_resolver_for_type(type, name)), + resolver=field.get_resolver(self.get_resolver_for_type(type, name, field.default_value)), deprecation_reason=field.deprecation_reason, description=field.description ) @@ -232,7 +237,7 @@ class TypeMap(GraphQLTypeMap): fields[field_name] = _field return fields - def get_resolver_for_type(self, type, name): + def get_resolver_for_type(self, type, name, default_value): if not issubclass(type, ObjectType): return resolver = getattr(type, 'resolve_{}'.format(name), None) @@ -247,13 +252,12 @@ class TypeMap(GraphQLTypeMap): if interface_resolver: break resolver = interface_resolver + # Only if is not decorated with classmethod if resolver: - if not getattr(resolver, '__self__', True): - return resolver.__func__ - return resolver + return get_unbound_function(resolver) - return partial(self.default_resolver, name) + return partial(self.default_resolver, name, default_value) def get_field_type(self, map, type): if isinstance(type, List): diff --git a/graphene/utils/get_unbound_function.py b/graphene/utils/get_unbound_function.py new file mode 100644 index 00000000..64add00a --- /dev/null +++ b/graphene/utils/get_unbound_function.py @@ -0,0 +1,4 @@ +def get_unbound_function(func): + if not getattr(func, '__self__', True): + return func.__func__ + return func diff --git a/setup.py b/setup.py index dfd82742..b9f8d1f8 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.0.dev', + 'graphql-core>=1.0', 'graphql-relay>=0.4.4', 'promise', ], @@ -78,6 +78,8 @@ setup( 'pytest>=2.7.2', 'pytest-benchmark', 'mock', + 'pytz', + 'iso8601', ], extras_require={ 'django': [