From 316569b01903746f85276ce05d386ff2a12ff55f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 21 Sep 2016 01:16:35 -0700 Subject: [PATCH 01/24] Improved docs. Added schema page --- docs/types/index.rst | 1 + docs/types/interfaces.rst | 1 + docs/types/scalars.rst | 2 + docs/types/schema.rst | 81 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 docs/types/schema.rst 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..c92cd28f 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. 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..a35909bf --- /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:: graphql + + { + 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, + ) From d9b8f5941d3d54b580acf0cdb8ad4bb31d175d75 Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Wed, 21 Sep 2016 09:34:29 +0100 Subject: [PATCH 02/24] Added default value for default resolver. --- .gitignore | 1 + graphene/types/field.py | 13 ++++++--- graphene/types/tests/test_field.py | 14 ++++++++- graphene/types/tests/test_query.py | 46 +++++++++++++++++++++++++++++- graphene/types/typemap.py | 10 +++---- 5 files changed, 73 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index b7554723..9f465556 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ target/ # PyCharm .idea +*.iml # Databases *.sqlite3 diff --git a/graphene/types/field.py b/graphene/types/field.py index 531c2f5c..88f1e131 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -17,9 +17,10 @@ def source_resolver(source, root, args, context, info): class Field(OrderedType): - def __init__(self, type, args=None, resolver=None, source=None, + def __init__(self, gql_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,9 +28,12 @@ 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(type(default_value)) if required: - type = NonNull(type) + gql_type = NonNull(gql_type) # Check if name is actually an argument of the field if isinstance(name, (Argument, UnmountedType)): @@ -42,13 +46,14 @@ class Field(OrderedType): source = None self.name = name - self._type = type + self._type = gql_type self.args = to_arguments(args or OrderedDict(), extra_args) if source: resolver = partial(source_resolver, source) 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/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_query.py b/graphene/types/tests/test_query.py index 3059776a..1f09b925 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,49 @@ 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() + + 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() diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index af193a9b..ed036d6c 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -190,8 +190,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 +224,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 +232,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) @@ -253,7 +253,7 @@ class TypeMap(GraphQLTypeMap): return resolver.__func__ return 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): From b72684192f0016478ac91d97f834bae88c738a57 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 21 Sep 2016 08:31:09 -0700 Subject: [PATCH 03/24] Fixed tests. --- graphene/types/field.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphene/types/field.py b/graphene/types/field.py index 6331b0ef..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): @@ -30,7 +33,7 @@ class Field(OrderedType): ) assert not callable(default_value), ( 'The default value can not be a function but received "{}".' - ).format(type(default_value)) + ).format(base_type(default_value)) if required: type = NonNull(type) From 8128292b028fe1254f0f13c9ba99e675da687348 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 21 Sep 2016 09:45:57 -0700 Subject: [PATCH 04/24] Improved quickstart --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4fad567b..5e037ca2 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 --upgrade Creating a basic Schema ----------------------- From 6bd03d59d7adb1c0aaecdef2b31f6b6c42c83603 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 21 Sep 2016 19:02:00 -0700 Subject: [PATCH 05/24] Added clientMutationId field to relay.ClientIDMutation. Fixed #300 --- .../starwars_relay/tests/test_objectidentification.py | 1 + graphene/relay/mutation.py | 2 ++ graphene/relay/tests/test_mutation.py | 11 +++++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index cd63dd54..87394421 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -25,6 +25,7 @@ input IntroduceShipInput { type IntroduceShipPayload { ship: Ship faction: Faction + clientMutationId: String } type Mutation { diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 50851781..8db462d6 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -21,6 +21,8 @@ class ClientIDMutationMeta(ObjectTypeMeta): input_class = attrs.pop('Input', None) 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) mutate_and_get_payload = getattr(cls, 'mutate_and_get_payload', None) if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__: diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 8f414c33..34fbb936 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -71,7 +71,7 @@ def test_no_mutate_and_get_payload(): def test_mutation(): fields = SaySomething._meta.fields - assert list(fields.keys()) == ['phrase'] + assert list(fields.keys()) == ['phrase', 'client_mutation_id'] assert isinstance(fields['phrase'], Field) field = SaySomething.Field() assert field.type == SaySomething @@ -79,6 +79,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(): @@ -94,7 +97,7 @@ def test_mutation_input(): def test_subclassed_mutation(): fields = OtherMutation._meta.fields - assert list(fields.keys()) == ['name', 'my_node_edge'] + assert list(fields.keys()) == ['name', 'my_node_edge', 'client_mutation_id'] assert isinstance(fields['name'], Field) field = OtherMutation.Field() assert field.type == OtherMutation @@ -126,7 +129,7 @@ def test_subclassed_mutation_input(): 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'}}}} From c9a30f7139f32295192dbde24405ea2d9f8ca3c6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 25 Sep 2016 05:01:12 -0700 Subject: [PATCH 06/24] Improved docs --- docs/quickstart.rst | 2 +- docs/types/abstracttypes.rst | 2 +- docs/types/interfaces.rst | 2 +- docs/types/mutations.rst | 2 +- docs/types/objecttypes.rst | 4 ++-- docs/types/schema.rst | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 5e037ca2..5dfae32f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -14,7 +14,7 @@ Project setup .. code:: bash - pip install graphene --upgrade + pip install "graphene>=1.0.dev" Creating a basic Schema ----------------------- 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/interfaces.rst b/docs/types/interfaces.rst index c92cd28f..ee0410a7 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -44,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..ab1ea489 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 diff --git a/docs/types/schema.rst b/docs/types/schema.rst index a35909bf..7c7ad73d 100644 --- a/docs/types/schema.rst +++ b/docs/types/schema.rst @@ -62,7 +62,7 @@ would be converted to ``_other_Name`` (without any other transformation). So, you would need to query with: -.. code:: graphql +.. code:: { lastName From 46cd0258358e56a463075564dcd5da98b962f1c5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 26 Sep 2016 00:45:23 -0700 Subject: [PATCH 07/24] Updated docs theme --- docs/conf.py | 11 ++++++++--- docs/requirements.txt | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 docs/requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 8f79896f..5db1ec43 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 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 From c920537380a8646795dbd129a925934efd0b896f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 26 Sep 2016 09:16:27 -0700 Subject: [PATCH 08/24] =?UTF-8?q?Updated=20graphene=20to=201.0=20?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++--- README.rst | 4 ++-- docs/conf.py | 4 ++-- docs/quickstart.rst | 2 +- graphene/__init__.py | 2 +- setup.py | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7b0ab346..f4093243 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. +This are the docs for Graphene `1.0`. Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade. --- @@ -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..7b20ad02 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -You are in the ``next`` unreleased version of Graphene (``1.0.dev``). +This are the docs for Graphene ``1.0``. Please read Please read `UPGRADE-v1.0.md`_ to learn how to upgrade. -------------- @@ -41,7 +41,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 5db1ec43..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. @@ -154,7 +154,7 @@ html_theme_path = [sphinx_graphene_theme.get_html_theme_path()] # 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 5dfae32f..5a93ff30 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -14,7 +14,7 @@ Project setup .. code:: bash - pip install "graphene>=1.0.dev" + pip install "graphene>=1.0" Creating a basic Schema ----------------------- diff --git a/graphene/__init__.py b/graphene/__init__.py index 5afc4f97..1ce095dd 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, 0, 'final', 0) __version__ = get_version(VERSION) diff --git a/setup.py b/setup.py index dfd82742..37f5fa50 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', ], From 69ad24961935611094b75dd159b65cf307de9c43 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 26 Sep 2016 09:46:49 -0700 Subject: [PATCH 09/24] Updated docs --- README.md | 2 +- README.rst | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f4093243..c6f924e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -This are the docs for Graphene `1.0`. 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`. --- diff --git a/README.rst b/README.rst index 7b20ad02..72a6a020 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,4 @@ -This are the docs for Graphene ``1.0``. Please read -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``. -------------- From c65d5a532a01553ba348b4c9ddaf0e1f565b3319 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 28 Sep 2016 13:57:41 -0700 Subject: [PATCH 10/24] Updated docs reflecting static resolvers (And a working example of is_type_of) --- docs/types/objecttypes.rst | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index ab1ea489..001b0e04 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -55,6 +55,12 @@ 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``. In the case this happens, please +overwrite the ``is_type_of`` method. + + Quick example ~~~~~~~~~~~~~ @@ -90,6 +96,53 @@ A field could also specify a custom resolver outside the class: reverse = graphene.String(word=graphene.String(), resolver=reverse) +Is Type Of +---------- + +An ``ObjectType`` could be resolved within a object that is not an instance of this +``ObjectType``. That means that the resolver of a ``Field`` could return any object. + +Let's see an example: + +.. code:: python + import graphene + + class Ship: + def __init__(self, name): + self.name = name + + class ShipType(graphene.ObjectType): + name = graphene.String(description="Ship name", required=True) + + @resolve_only_args + def resolve_name(self): + # Here self will be the Ship instance returned in resolve_ship + return self.name + + class Query(graphene.ObjectType): + ship = graphene.Field(ShipNode) + + def resolve_ship(self, context, args, info): + return Ship(name='xwing') + + schema = graphene.Schema(query=Query) + + +In this example, we are returning a ``Ship`` which is not an instance of ``ShipType``. +If we execute a query on the ship, we would see this error: +`"Expected value of type \"ShipType\" but got: instance."` + +That's happening because GraphQL have no idea what type ``Ship`` is. For solving this, +we only have to add a ``is_type_of`` method in ``ShipType`` + +.. code:: python + + class ShipType(graphene.ObjectType): + @classmethod + def is_type_of(cls, root, context, info): + return isinstance(root, (Ship, ShipType)) + + Instances as data containers ---------------------------- From c9aa461c654931f612e7708fdb600858cb843ba3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 28 Sep 2016 13:58:05 -0700 Subject: [PATCH 11/24] Fixed code --- docs/types/objecttypes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 001b0e04..73526018 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -105,6 +105,7 @@ An ``ObjectType`` could be resolved within a object that is not an instance of t Let's see an example: .. code:: python + import graphene class Ship: From cdd4afb79af057424177dc6efdb0f66e344fd72b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 28 Sep 2016 13:58:39 -0700 Subject: [PATCH 12/24] Update objecttypes.rst --- docs/types/objecttypes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 73526018..887e86c9 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -121,7 +121,7 @@ Let's see an example: return self.name class Query(graphene.ObjectType): - ship = graphene.Field(ShipNode) + ship = graphene.Field(ShipType) def resolve_ship(self, context, args, info): return Ship(name='xwing') From 8030fea44318b9dfcda854c96dfa5091b266f95e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 29 Sep 2016 01:38:56 -0700 Subject: [PATCH 13/24] Fixed flexible resolving in return type --- graphene/types/interface.py | 6 +++++- graphene/types/objecttype.py | 5 +---- graphene/types/tests/test_query.py | 28 ++++++++++++++++++++++++++++ graphene/types/typemap.py | 8 ++++++-- 4 files changed, 40 insertions(+), 7 deletions(-) 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/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/tests/test_query.py b/graphene/types/tests/test_query.py index 1f09b925..4f9d8810 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -41,6 +41,10 @@ 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') @@ -153,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/typemap.py b/graphene/types/typemap.py index ed036d6c..ecaedf75 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -6,6 +6,7 @@ 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 @@ -26,11 +27,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 From 71556031bf41b3f276d1483b64b67cda80381f12 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 29 Sep 2016 01:42:32 -0700 Subject: [PATCH 14/24] Removing is_type_of from docs --- docs/types/objecttypes.rst | 51 +------------------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 887e86c9..4006c61a 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -57,8 +57,7 @@ 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``. In the case this happens, please -overwrite the ``is_type_of`` method. +need to be an actual instance of the ``ObjectType``. Quick example @@ -96,54 +95,6 @@ A field could also specify a custom resolver outside the class: reverse = graphene.String(word=graphene.String(), resolver=reverse) -Is Type Of ----------- - -An ``ObjectType`` could be resolved within a object that is not an instance of this -``ObjectType``. That means that the resolver of a ``Field`` could return any object. - -Let's see an example: - -.. code:: python - - import graphene - - class Ship: - def __init__(self, name): - self.name = name - - class ShipType(graphene.ObjectType): - name = graphene.String(description="Ship name", required=True) - - @resolve_only_args - def resolve_name(self): - # Here self will be the Ship instance returned in resolve_ship - return self.name - - class Query(graphene.ObjectType): - ship = graphene.Field(ShipType) - - def resolve_ship(self, context, args, info): - return Ship(name='xwing') - - schema = graphene.Schema(query=Query) - - -In this example, we are returning a ``Ship`` which is not an instance of ``ShipType``. -If we execute a query on the ship, we would see this error: -`"Expected value of type \"ShipType\" but got: instance."` - -That's happening because GraphQL have no idea what type ``Ship`` is. For solving this, -we only have to add a ``is_type_of`` method in ``ShipType`` - -.. code:: python - - class ShipType(graphene.ObjectType): - @classmethod - def is_type_of(cls, root, context, info): - return isinstance(root, (Ship, ShipType)) - - Instances as data containers ---------------------------- From ad953f01a7b12492a67b7415f918f0a040aa935f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 29 Sep 2016 01:58:14 -0700 Subject: [PATCH 15/24] Updated version to 1.0.1 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 1ce095dd..1948affb 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 0, 0, 'final', 0) +VERSION = (1, 0, 1, 'final', 0) __version__ = get_version(VERSION) From c7929234294a181993158b4439c24006e9ed1ebd Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 1 Oct 2016 10:42:06 -0700 Subject: [PATCH 16/24] Added ability to return a Connection instance in the connection resolver --- graphene/relay/connection.py | 20 ++++--- graphene/relay/tests/test_connection_query.py | 56 ++++++++++++++++++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index a1039e43..67ccea50 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -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_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, + } + } + } From bd207b5f06a64dbbd610ea901f0d76c5b4918734 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 1 Oct 2016 11:49:30 -0700 Subject: [PATCH 17/24] Improved arguments and structures comparison --- graphene/types/argument.py | 8 +++++ graphene/types/structures.py | 14 ++++++++ graphene/types/tests/test_argument.py | 26 +++++++++++++++ graphene/types/tests/test_structures.py | 44 +++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 graphene/types/tests/test_argument.py create mode 100644 graphene/types/tests/test_structures.py 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/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_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 From c961f0b3c60c3a7a6223d5218c912d69585aef5c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 1 Oct 2016 11:52:31 -0700 Subject: [PATCH 18/24] Fixed ConnectionField arguments overwritten. Fixed #252 --- graphene/relay/connection.py | 8 +++--- graphene/relay/tests/test_connection.py | 33 +++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 67ccea50..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 ) 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), + } From 9231e0d28dafcfa699dcebf052a5512af56364c5 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Mon, 3 Oct 2016 19:07:16 -0700 Subject: [PATCH 19/24] Update datetime.py Restore ios8601 handling per https://github.com/graphql-python/graphene/pull/152/files --- graphene/types/datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 02a6c1c60300e599f1922716dd6ca63023c44d8d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Oct 2016 19:59:01 -0700 Subject: [PATCH 20/24] Isolated unbound function logic in utils --- graphene/types/typemap.py | 6 +++--- graphene/utils/get_unbound_function.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 graphene/utils/get_unbound_function.py diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index ecaedf75..c2b47279 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -10,6 +10,7 @@ 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 @@ -251,11 +252,10 @@ 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, default_value) 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 From 999bca84c98d452634dd530e374ea6b649167c64 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Oct 2016 19:59:26 -0700 Subject: [PATCH 21/24] Fixed mutation with unbound mutate method. Fixed #311 --- graphene/types/mutation.py | 2 + graphene/types/tests/test_mutation.py | 54 +++++++++++++++++---------- 2 files changed, 36 insertions(+), 20 deletions(-) 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/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" + } + } From fa231fb472b7e72c22ec9fdf1169d6b75d70469a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Oct 2016 20:12:11 -0700 Subject: [PATCH 22/24] Updated version to 1.0.2 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 1948affb..7a01ed16 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 0, 1, 'final', 0) +VERSION = (1, 0, 2, 'final', 0) __version__ = get_version(VERSION) From 5dd92b7d6bb641abbe37e503d5adbe59d2e5287f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Oct 2016 20:51:37 -0700 Subject: [PATCH 23/24] Added datetime tests --- graphene/types/tests/test_datetime.py | 41 +++++++++++++++++++++++++++ setup.py | 2 ++ 2 files changed, 43 insertions(+) create mode 100644 graphene/types/tests/test_datetime.py 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/setup.py b/setup.py index 37f5fa50..b9f8d1f8 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,8 @@ setup( 'pytest>=2.7.2', 'pytest-benchmark', 'mock', + 'pytz', + 'iso8601', ], extras_require={ 'django': [ From 88ccaec8fac06adb42963c506a27fd0b5b1d2dd7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Oct 2016 20:58:57 -0700 Subject: [PATCH 24/24] Added jsonstring tests --- .travis.yml | 2 +- graphene/types/tests/test_json.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 graphene/types/tests/test_json.py 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/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 + }