Fix remaining graphene 3.0 and graphql 3.0 compatiblity

* names must be strings

* update to expected schemas to new format

* Call get() on django promises to get the actual value
This commit is contained in:
Ülgen Sarıkavak 2020-03-15 19:26:23 +03:00 committed by Jean-Louis Fuchs
parent 84ae00f803
commit eb46f0da0a
No known key found for this signature in database
GPG Key ID: 3440F981335FD30F
10 changed files with 338 additions and 214 deletions

View File

@ -3,11 +3,14 @@ from functools import singledispatch
from django.db import models from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.functional import Promise
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from graphene import ( from graphene import (
ID, ID,
UUID,
Boolean, Boolean,
Date,
DateTime,
Dynamic, Dynamic,
Enum, Enum,
Field, Field,
@ -16,18 +19,16 @@ from graphene import (
List, List,
NonNull, NonNull,
String, String,
UUID,
DateTime,
Date,
Time, Time,
) )
from graphene.types.json import JSONString from graphene.types.json import JSONString
from graphene.utils.str_converters import to_camel_case, to_const 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 .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): def convert_choice_name(name):
@ -66,7 +67,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
class EnumWithDescriptionsType(object): class EnumWithDescriptionsType(object):
@property @property
def description(self): def description(self):
return named_choices_descriptions[self.name] return str(named_choices_descriptions[self.name])
return Enum(name, list(named_choices), type=EnumWithDescriptionsType) 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)): if not isinstance(inner_type, (List, NonNull)):
inner_type = type(inner_type) inner_type = type(inner_type)
return List(inner_type, description=field.help_text, required=not field.null) 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)

View File

@ -17,7 +17,7 @@ class DjangoDebugContext(object):
if not self.debug_promise: if not self.debug_promise:
self.debug_promise = Promise.all(self.promises) self.debug_promise = Promise.all(self.promises)
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): def on_resolve_all_promises(self, values):
if self.promises: if self.promises:

View File

@ -5,7 +5,8 @@ from graphql_relay.connection.arrayconnection import connection_from_array_slice
from promise import Promise from promise import Promise
from graphene import NonNull 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 graphene.types import Field, List
from .settings import graphene_settings from .settings import graphene_settings
@ -126,11 +127,11 @@ class DjangoConnectionField(ConnectionField):
iterable, iterable,
args, args,
slice_start=0, slice_start=0,
connection_type=connection,
array_length=_len, array_length=_len,
array_slice_length=_len, array_slice_length=_len,
connection_type=partial(connection_adapter, connection),
edge_type=connection.Edge, edge_type=connection.Edge,
page_info_type=PageInfo, page_info_type=page_info_adapter,
) )
connection.iterable = iterable connection.iterable = iterable
connection.length = _len connection.length = _len

View File

@ -806,38 +806,56 @@ def test_integer_field_filter_type():
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: Query pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
} }
type PetTypeConnection { type PetTypeConnection {
\"""Pagination data for this connection.\"""
pageInfo: PageInfo! pageInfo: PageInfo!
\"""Contains the nodes in this connection.\"""
edges: [PetTypeEdge]! 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 { type PetTypeEdge {
\"""The item at the end of the edge\"""
node: PetType node: PetType
\"""A cursor for use in pagination\"""
cursor: String! cursor: String!
} }
type Query { type PetType implements Node {
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection \"""\"""
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( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: 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
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
} }
type PetTypeConnection { type PetTypeConnection {
\"""Pagination data for this connection.\"""
pageInfo: PageInfo! pageInfo: PageInfo!
\"""Contains the nodes in this connection.\"""
edges: [PetTypeEdge]! 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 { type PetTypeEdge {
\"""The item at the end of the edge\"""
node: PetType node: PetType
\"""A cursor for use in pagination\"""
cursor: String! cursor: String!
} }
type Query { type PetType implements Node {
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection \"""\"""
age: Int!
\"""The ID of the object\"""
id: ID!
} }
"""
\"""An object with an ID\"""
interface Node {
\"""The ID of the object\"""
id: ID!
}
"""
) )

View File

@ -56,7 +56,7 @@ class Command(CommandArguments):
def save_graphql_file(self, out, schema): def save_graphql_file(self, out, schema):
with open(out, "w") as outfile: 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): def get_schema(self, schema, out, indent):
schema_dict = {"data": schema.introspect()} schema_dict = {"data": schema.introspect()}

View File

@ -51,10 +51,6 @@ def test_generate_graphql_file_on_call_graphql_schema():
schema_output = handle.write.call_args[0][0] schema_output = handle.write.call_args[0][0]
assert schema_output == dedent( assert schema_output == dedent(
"""\ """\
schema {
query: Query
}
type Query { type Query {
hi: String hi: String
} }

View File

@ -713,7 +713,7 @@ def test_should_query_promise_connectionfields():
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
def resolve_all_reporters(self, info, **args): 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) schema = graphene.Schema(query=Query)
query = """ query = """
@ -842,7 +842,7 @@ def test_should_query_dataloader_fields():
articles = DjangoConnectionField(ArticleType) articles = DjangoConnectionField(ArticleType)
def resolve_articles(self, info, **args): def resolve_articles(self, info, **args):
return article_loader.load(self.id) return article_loader.load(self.id).get()
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
@ -1075,7 +1075,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
films = DjangoConnectionField(FilmType) films = DjangoConnectionField(FilmType)
def resolve_films(root, info, **args): def resolve_films(root, info, **kwargs):
qs = Film.objects.prefetch_related("reporters") qs = Film.objects.prefetch_related("reporters")
return qs return qs
@ -1105,6 +1105,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
} }
""" """
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
with django_assert_num_queries(3) as captured: with django_assert_num_queries(3) as captured:
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
@ -1127,7 +1128,7 @@ def test_should_preserve_annotations():
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
films = DjangoConnectionField(FilmType) films = DjangoConnectionField(FilmType)
def resolve_films(root, info): def resolve_films(root, info, **kwargs):
qs = Film.objects.prefetch_related("reporters") qs = Film.objects.prefetch_related("reporters")
return qs.annotate(reporters_count=models.Count("reporters")) return qs.annotate(reporters_count=models.Count("reporters"))

View File

@ -111,83 +111,165 @@ def test_django_objecttype_with_custom_meta():
def test_schema_representation(): def test_schema_representation():
expected = """ expected = dedent(
schema { """\
query: RootQuery schema {
} query: RootQuery
}
type Article implements Node { \"""Article description\"""
id: ID! type Article implements Node {
headline: String! \"""The ID of the object\"""
pubDate: Date! id: ID!
pubDateTime: DateTime!
reporter: Reporter!
editor: Reporter!
lang: ArticleLang!
importance: ArticleImportance
}
type ArticleConnection { \"""\"""
pageInfo: PageInfo! headline: String!
edges: [ArticleEdge]!
test: String
}
type ArticleEdge { \"""\"""
node: Article pubDate: Date!
cursor: String!
}
enum ArticleImportance { \"""\"""
A_1 pubDateTime: DateTime!
A_2
}
enum ArticleLang { \"""\"""
ES reporter: Reporter!
EN
}
scalar Date \"""\"""
editor: Reporter!
scalar DateTime \"""Language\"""
lang: ArticleLang!
interface Node { \"""\"""
id: ID! importance: ArticleImportance
} }
type PageInfo { \"""An object with an ID\"""
hasNextPage: Boolean! interface Node {
hasPreviousPage: Boolean! \"""The ID of the object\"""
startCursor: String id: ID!
endCursor: String }
}
type Reporter { \"""
id: ID! The `Date` scalar type represents a Date
firstName: String! value as specified by
lastName: String! [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
email: String! \"""
pets: [Reporter!]! scalar Date
aChoice: ReporterAChoice
reporterType: ReporterReporterType
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
}
enum ReporterAChoice { \"""
A_1 The `DateTime` scalar type represents a DateTime
A_2 value as specified by
} [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
\"""
scalar DateTime
enum ReporterReporterType { \"""An enumeration.\"""
A_1 enum ArticleLang {
A_2 \"""Spanish\"""
} ES
type RootQuery { \"""English\"""
node(id: ID!): Node EN
} }
""".lstrip()
\"""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 assert str(schema) == expected
@ -415,20 +497,21 @@ class TestDjangoObjectType:
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: Query pet: Pet
} }
type Pet { type Pet {
id: ID! \"""\"""
kind: String! id: ID!
cuteness: Int!
}
type Query { \"""\"""
pet: Pet kind: String!
}
""" \"""\"""
cuteness: Int!
}
"""
) )
def test_django_objecttype_convert_choices_enum_list(self, PetModel): def test_django_objecttype_convert_choices_enum_list(self, PetModel):
@ -444,25 +527,30 @@ class TestDjangoObjectType:
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: Query pet: Pet
} }
type Pet { type Pet {
id: ID! \"""\"""
kind: PetModelKind! id: ID!
cuteness: Int!
}
enum PetModelKind { \"""\"""
CAT kind: PetModelKind!
DOG
}
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): def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
@ -478,20 +566,21 @@ class TestDjangoObjectType:
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: Query pet: Pet
} }
type Pet { type Pet {
id: ID! \"""\"""
kind: String! id: ID!
cuteness: Int!
}
type Query { \"""\"""
pet: Pet kind: String!
}
""" \"""\"""
cuteness: Int!
}
"""
) )
def test_django_objecttype_convert_choices_enum_naming_collisions( def test_django_objecttype_convert_choices_enum_naming_collisions(
@ -511,24 +600,27 @@ class TestDjangoObjectType:
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: Query pet: PetModelKind
} }
type PetModelKind { type PetModelKind {
id: ID! \"""\"""
kind: TestsPetModelKindChoices! id: ID!
}
type Query { \"""\"""
pet: PetModelKind kind: TestsPetModelKindChoices!
} }
enum TestsPetModelKindChoices { \"""An enumeration.\"""
CAT enum TestsPetModelKindChoices {
DOG \"""Cat\"""
} CAT
"""
\"""Dog\"""
DOG
}
"""
) )
def test_django_objecttype_choices_custom_enum_name( def test_django_objecttype_choices_custom_enum_name(
@ -550,22 +642,25 @@ class TestDjangoObjectType:
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: Query pet: PetModelKind
} }
enum CustomEnumKind { type PetModelKind {
CAT \"""\"""
DOG id: ID!
}
type PetModelKind { \"""\"""
id: ID! kind: CustomEnumKind!
kind: CustomEnumKind! }
}
type Query { \"""An enumeration.\"""
pet: PetModelKind enum CustomEnumKind {
} \"""Cat\"""
""" CAT
\"""Dog\"""
DOG
}
"""
) )

View File

@ -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): def test_reports_validation_errors(client):
response = client.get(url_string(query="{ test, unknownOne, unknownTwo }")) 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) == { assert response_json(response) == {
"errors": [ "errors": [
{ {
"locations": None,
"message": "Must provide operation name if query contains multiple operations.", "message": "Must provide operation name if query contains multiple operations.",
"locations": None,
"path": 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): def test_handles_field_errors_caught_by_graphql(client):
response = client.get(url_string(query="{thrower}")) response = client.get(url_string(query="{thrower}"))
assert response.status_code == 200 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): def test_handles_syntax_errors_caught_by_graphql(client):
response = client.get(url_string(query="syntaxerror")) response = client.get(url_string(query="syntaxerror"))
assert response.status_code == 400 assert response.status_code == 400
@ -477,8 +468,8 @@ def test_handles_syntax_errors_caught_by_graphql(client):
"errors": [ "errors": [
{ {
"locations": [{"column": 1, "line": 1}], "locations": [{"column": 1, "line": 1}],
"message": "Syntax Error GraphQL (1:1) " "message": "Syntax Error: Unexpected Name 'syntaxerror'.",
'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n', "path": None,
} }
] ]
} }

View File

@ -8,11 +8,10 @@ from django.shortcuts import render
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View 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 GraphQLError
from graphql.error import format_error as format_graphql_error from graphql.error import format_error as format_graphql_error
from graphql.execution import ExecutionResult from graphql.execution import ExecutionResult
from graphql.type.schema import GraphQLSchema
from graphene import Schema from graphene import Schema
@ -259,9 +258,8 @@ class GraphQLView(View):
if validation_errors: if validation_errors:
return ExecutionResult(data=None, errors=validation_errors) return ExecutionResult(data=None, errors=validation_errors)
return execute( return self.schema.execute(
schema=self.schema.graphql_schema, source=query,
document=document,
root_value=self.get_root_value(request), root_value=self.get_root_value(request),
variable_values=variables, variable_values=variables,
operation_name=operation_name, operation_name=operation_name,