diff --git a/graphene_django/converter.py b/graphene_django/converter.py index ffd3d94..187874a 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -3,11 +3,14 @@ from functools import singledispatch from django.db import models from django.utils.encoding import force_str +from django.utils.functional import Promise from django.utils.module_loading import import_string - from graphene import ( ID, + UUID, Boolean, + Date, + DateTime, Dynamic, Enum, Field, @@ -16,18 +19,16 @@ from graphene import ( List, NonNull, String, - UUID, - DateTime, - Date, Time, ) from graphene.types.json import JSONString from graphene.utils.str_converters import to_camel_case, to_const -from graphql import assert_valid_name, GraphQLError +from graphql import GraphQLError, assert_valid_name +from graphql.pyutils import register_description -from .settings import graphene_settings from .compat import ArrayField, HStoreField, JSONField, RangeField -from .fields import DjangoListField, DjangoConnectionField +from .fields import DjangoConnectionField, DjangoListField +from .settings import graphene_settings def convert_choice_name(name): @@ -66,7 +67,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices): class EnumWithDescriptionsType(object): @property def description(self): - return named_choices_descriptions[self.name] + return str(named_choices_descriptions[self.name]) return Enum(name, list(named_choices), type=EnumWithDescriptionsType) @@ -278,3 +279,8 @@ def convert_postgres_range_to_string(field, registry=None): if not isinstance(inner_type, (List, NonNull)): inner_type = type(inner_type) return List(inner_type, description=field.help_text, required=not field.null) + + +# Register Django lazy()-wrapped values as GraphQL description/help_text. +# This is needed for using lazy translations, see https://github.com/graphql-python/graphql-core-next/issues/58. +register_description(Promise) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 0fe3fe3..8621b55 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -17,7 +17,7 @@ class DjangoDebugContext(object): if not self.debug_promise: self.debug_promise = Promise.all(self.promises) self.promises = [] - return self.debug_promise.then(self.on_resolve_all_promises) + return self.debug_promise.then(self.on_resolve_all_promises).get() def on_resolve_all_promises(self, values): if self.promises: diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 66b6011..418a14b 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -5,7 +5,8 @@ from graphql_relay.connection.arrayconnection import connection_from_array_slice from promise import Promise from graphene import NonNull -from graphene.relay import ConnectionField, PageInfo +from graphene.relay import ConnectionField +from graphene.relay.connection import connection_adapter, page_info_adapter from graphene.types import Field, List from .settings import graphene_settings @@ -126,11 +127,11 @@ class DjangoConnectionField(ConnectionField): iterable, args, slice_start=0, - connection_type=connection, array_length=_len, array_slice_length=_len, + connection_type=partial(connection_adapter, connection), edge_type=connection.Edge, - page_info_type=PageInfo, + page_info_type=page_info_adapter, ) connection.iterable = iterable connection.length = _len diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 166d806..59cc30b 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -806,38 +806,56 @@ def test_integer_field_filter_type(): assert str(schema) == dedent( """\ - schema { - query: Query - } - - interface Node { - id: ID! - } - - type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String - } - - type PetType implements Node { - age: Int! - id: ID! + type Query { + pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection } type PetTypeConnection { + \"""Pagination data for this connection.\""" pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" edges: [PetTypeEdge]! } + \""" + The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. + \""" + type PageInfo { + \"""When paginating forwards, are there more items?\""" + hasNextPage: Boolean! + + \"""When paginating backwards, are there more items?\""" + hasPreviousPage: Boolean! + + \"""When paginating backwards, the cursor to continue.\""" + startCursor: String + + \"""When paginating forwards, the cursor to continue.\""" + endCursor: String + } + + \"""A Relay edge containing a `PetType` and its cursor.\""" type PetTypeEdge { + \"""The item at the end of the edge\""" node: PetType + + \"""A cursor for use in pagination\""" cursor: String! } - - type Query { - pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection + + type PetType implements Node { + \"""\""" + age: Int! + + \"""The ID of the object\""" + id: ID! + } + + \"""An object with an ID\""" + interface Node { + \"""The ID of the object\""" + id: ID! } """ ) @@ -858,40 +876,58 @@ def test_other_filter_types(): assert str(schema) == dedent( """\ - schema { - query: Query - } - - interface Node { - id: ID! - } - - type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String - } - - type PetType implements Node { - age: Int! - id: ID! + type Query { + pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection } type PetTypeConnection { + \"""Pagination data for this connection.\""" pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" edges: [PetTypeEdge]! } + \""" + The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. + \""" + type PageInfo { + \"""When paginating forwards, are there more items?\""" + hasNextPage: Boolean! + + \"""When paginating backwards, are there more items?\""" + hasPreviousPage: Boolean! + + \"""When paginating backwards, the cursor to continue.\""" + startCursor: String + + \"""When paginating forwards, the cursor to continue.\""" + endCursor: String + } + + \"""A Relay edge containing a `PetType` and its cursor.\""" type PetTypeEdge { + \"""The item at the end of the edge\""" node: PetType + + \"""A cursor for use in pagination\""" cursor: String! } - type Query { - pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection + type PetType implements Node { + \"""\""" + age: Int! + + \"""The ID of the object\""" + id: ID! } - """ + + \"""An object with an ID\""" + interface Node { + \"""The ID of the object\""" + id: ID! + } + """ ) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index dcef73c..9cf55ca 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -56,7 +56,7 @@ class Command(CommandArguments): def save_graphql_file(self, out, schema): with open(out, "w") as outfile: - outfile.write(print_schema(schema)) + outfile.write(print_schema(schema.graphql_schema)) def get_schema(self, schema, out, indent): schema_dict = {"data": schema.introspect()} diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index f979b5c..70116b8 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -51,10 +51,6 @@ def test_generate_graphql_file_on_call_graphql_schema(): schema_output = handle.write.call_args[0][0] assert schema_output == dedent( """\ - schema { - query: Query - } - type Query { hi: String } diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index a5238bf..75053db 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -713,7 +713,7 @@ def test_should_query_promise_connectionfields(): all_reporters = DjangoConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): - return Promise.resolve([Reporter(id=1)]) + return Promise.resolve([Reporter(id=1)]).get() schema = graphene.Schema(query=Query) query = """ @@ -842,7 +842,7 @@ def test_should_query_dataloader_fields(): articles = DjangoConnectionField(ArticleType) def resolve_articles(self, info, **args): - return article_loader.load(self.id) + return article_loader.load(self.id).get() class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) @@ -1075,7 +1075,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries): class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) - def resolve_films(root, info, **args): + def resolve_films(root, info, **kwargs): qs = Film.objects.prefetch_related("reporters") return qs @@ -1105,6 +1105,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries): } """ schema = graphene.Schema(query=Query) + with django_assert_num_queries(3) as captured: result = schema.execute(query) assert not result.errors @@ -1127,7 +1128,7 @@ def test_should_preserve_annotations(): class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) - def resolve_films(root, info): + def resolve_films(root, info, **kwargs): qs = Film.objects.prefetch_related("reporters") return qs.annotate(reporters_count=models.Count("reporters")) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 9b3ceb6..2a6d357 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -111,83 +111,165 @@ def test_django_objecttype_with_custom_meta(): def test_schema_representation(): - expected = """ -schema { - query: RootQuery -} + expected = dedent( + """\ + schema { + query: RootQuery + } -type Article implements Node { - id: ID! - headline: String! - pubDate: Date! - pubDateTime: DateTime! - reporter: Reporter! - editor: Reporter! - lang: ArticleLang! - importance: ArticleImportance -} + \"""Article description\""" + type Article implements Node { + \"""The ID of the object\""" + id: ID! -type ArticleConnection { - pageInfo: PageInfo! - edges: [ArticleEdge]! - test: String -} + \"""\""" + headline: String! -type ArticleEdge { - node: Article - cursor: String! -} + \"""\""" + pubDate: Date! -enum ArticleImportance { - A_1 - A_2 -} + \"""\""" + pubDateTime: DateTime! -enum ArticleLang { - ES - EN -} + \"""\""" + reporter: Reporter! -scalar Date + \"""\""" + editor: Reporter! -scalar DateTime + \"""Language\""" + lang: ArticleLang! -interface Node { - id: ID! -} + \"""\""" + importance: ArticleImportance + } -type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String -} + \"""An object with an ID\""" + interface Node { + \"""The ID of the object\""" + id: ID! + } -type Reporter { - id: ID! - firstName: String! - lastName: String! - email: String! - pets: [Reporter!]! - aChoice: ReporterAChoice - reporterType: ReporterReporterType - articles(before: String, after: String, first: Int, last: Int): ArticleConnection! -} + \""" + The `Date` scalar type represents a Date + value as specified by + [iso8601](https://en.wikipedia.org/wiki/ISO_8601). + \""" + scalar Date -enum ReporterAChoice { - A_1 - A_2 -} + \""" + The `DateTime` scalar type represents a DateTime + value as specified by + [iso8601](https://en.wikipedia.org/wiki/ISO_8601). + \""" + scalar DateTime -enum ReporterReporterType { - A_1 - A_2 -} + \"""An enumeration.\""" + enum ArticleLang { + \"""Spanish\""" + ES -type RootQuery { - node(id: ID!): Node -} -""".lstrip() + \"""English\""" + EN + } + + \"""An enumeration.\""" + enum ArticleImportance { + \"""Very important\""" + A_1 + + \"""Not as important\""" + A_2 + } + + \"""Reporter description\""" + type Reporter { + \"""\""" + id: ID! + + \"""\""" + firstName: String! + + \"""\""" + lastName: String! + + \"""\""" + email: String! + + \"""\""" + pets: [Reporter!]! + + \"""\""" + aChoice: ReporterAChoice + + \"""\""" + reporterType: ReporterReporterType + + \"""\""" + articles(before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection! + } + + \"""An enumeration.\""" + enum ReporterAChoice { + \"""this\""" + A_1 + + \"""that\""" + A_2 + } + + \"""An enumeration.\""" + enum ReporterReporterType { + \"""Regular\""" + A_1 + + \"""CNN Reporter\""" + A_2 + } + + type ArticleConnection { + \"""Pagination data for this connection.\""" + pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" + edges: [ArticleEdge]! + test: String + } + + \""" + The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. + \""" + type PageInfo { + \"""When paginating forwards, are there more items?\""" + hasNextPage: Boolean! + + \"""When paginating backwards, are there more items?\""" + hasPreviousPage: Boolean! + + \"""When paginating backwards, the cursor to continue.\""" + startCursor: String + + \"""When paginating forwards, the cursor to continue.\""" + endCursor: String + } + + \"""A Relay edge containing a `Article` and its cursor.\""" + type ArticleEdge { + \"""The item at the end of the edge\""" + node: Article + + \"""A cursor for use in pagination\""" + cursor: String! + } + + type RootQuery { + node( + \"""The ID of the object\""" + id: ID! + ): Node + } + """ + ) assert str(schema) == expected @@ -415,20 +497,21 @@ class TestDjangoObjectType: assert str(schema) == dedent( """\ - schema { - query: Query - } + type Query { + pet: Pet + } - type Pet { - id: ID! - kind: String! - cuteness: Int! - } + type Pet { + \"""\""" + id: ID! - type Query { - pet: Pet - } - """ + \"""\""" + kind: String! + + \"""\""" + cuteness: Int! + } + """ ) def test_django_objecttype_convert_choices_enum_list(self, PetModel): @@ -444,25 +527,30 @@ class TestDjangoObjectType: assert str(schema) == dedent( """\ - schema { - query: Query - } + type Query { + pet: Pet + } - type Pet { - id: ID! - kind: PetModelKind! - cuteness: Int! - } + type Pet { + \"""\""" + id: ID! - enum PetModelKind { - CAT - DOG - } + \"""\""" + kind: PetModelKind! - type Query { - pet: Pet - } - """ + \"""\""" + cuteness: Int! + } + + \"""An enumeration.\""" + enum PetModelKind { + \"""Cat\""" + CAT + + \"""Dog\""" + DOG + } + """ ) def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): @@ -478,20 +566,21 @@ class TestDjangoObjectType: assert str(schema) == dedent( """\ - schema { - query: Query - } + type Query { + pet: Pet + } - type Pet { - id: ID! - kind: String! - cuteness: Int! - } + type Pet { + \"""\""" + id: ID! - type Query { - pet: Pet - } - """ + \"""\""" + kind: String! + + \"""\""" + cuteness: Int! + } + """ ) def test_django_objecttype_convert_choices_enum_naming_collisions( @@ -511,24 +600,27 @@ class TestDjangoObjectType: assert str(schema) == dedent( """\ - schema { - query: Query - } + type Query { + pet: PetModelKind + } - type PetModelKind { - id: ID! - kind: TestsPetModelKindChoices! - } + type PetModelKind { + \"""\""" + id: ID! - type Query { - pet: PetModelKind - } + \"""\""" + kind: TestsPetModelKindChoices! + } - enum TestsPetModelKindChoices { - CAT - DOG - } - """ + \"""An enumeration.\""" + enum TestsPetModelKindChoices { + \"""Cat\""" + CAT + + \"""Dog\""" + DOG + } + """ ) def test_django_objecttype_choices_custom_enum_name( @@ -550,22 +642,25 @@ class TestDjangoObjectType: assert str(schema) == dedent( """\ - schema { - query: Query - } + type Query { + pet: PetModelKind + } - enum CustomEnumKind { - CAT - DOG - } + type PetModelKind { + \"""\""" + id: ID! - type PetModelKind { - id: ID! - kind: CustomEnumKind! - } + \"""\""" + kind: CustomEnumKind! + } - type Query { - pet: PetModelKind - } - """ + \"""An enumeration.\""" + enum CustomEnumKind { + \"""Cat\""" + CAT + + \"""Dog\""" + DOG + } + """ ) diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index 5be94f1..1c027d9 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -92,9 +92,6 @@ def test_allows_get_with_operation_name(client): } -@pytest.mark.xfail( - reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" -) def test_reports_validation_errors(client): response = client.get(url_string(query="{ test, unknownOne, unknownTwo }")) @@ -129,8 +126,8 @@ def test_errors_when_missing_operation_name(client): assert response_json(response) == { "errors": [ { - "locations": None, "message": "Must provide operation name if query contains multiple operations.", + "locations": None, "path": None, } ] @@ -449,9 +446,6 @@ def test_supports_pretty_printing_by_request(client): ) -@pytest.mark.xfail( - reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" -) def test_handles_field_errors_caught_by_graphql(client): response = client.get(url_string(query="{thrower}")) assert response.status_code == 200 @@ -467,9 +461,6 @@ def test_handles_field_errors_caught_by_graphql(client): } -@pytest.mark.xfail( - reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" -) def test_handles_syntax_errors_caught_by_graphql(client): response = client.get(url_string(query="syntaxerror")) assert response.status_code == 400 @@ -477,8 +468,8 @@ def test_handles_syntax_errors_caught_by_graphql(client): "errors": [ { "locations": [{"column": 1, "line": 1}], - "message": "Syntax Error GraphQL (1:1) " - 'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n', + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None, } ] } diff --git a/graphene_django/views.py b/graphene_django/views.py index 112447e..1a373c7 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -8,11 +8,10 @@ from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic import View -from graphql import OperationType, execute, get_operation_ast, parse, validate +from graphql import OperationType, get_operation_ast, parse, validate from graphql.error import GraphQLError from graphql.error import format_error as format_graphql_error from graphql.execution import ExecutionResult -from graphql.type.schema import GraphQLSchema from graphene import Schema @@ -259,9 +258,8 @@ class GraphQLView(View): if validation_errors: return ExecutionResult(data=None, errors=validation_errors) - return execute( - schema=self.schema.graphql_schema, - document=document, + return self.schema.execute( + source=query, root_value=self.get_root_value(request), variable_values=variables, operation_name=operation_name,