graphql 3.0 and graphene 3.0 final rebase (#951)

This commit is contained in:
Jean-Louis Fuchs 2020-05-09 13:13:47 +02:00 committed by GitHub
parent 77b9832606
commit 10d22de98e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 406 additions and 300 deletions

View File

@ -8,7 +8,7 @@ jobs:
strategy: strategy:
max-parallel: 4 max-parallel: 4
matrix: matrix:
django: ["1.11", "2.2", "3.0"] django: ["2.2", "3.0"]
python-version: ["3.6", "3.7", "3.8"] python-version: ["3.6", "3.7", "3.8"]
steps: steps:

View File

@ -28,7 +28,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra
For installing graphene, just run this command in your shell For installing graphene, just run this command in your shell
```bash ```bash
pip install "graphene-django>=2.0" pip install "graphene-django>=3"
``` ```
### Settings ### Settings

View File

@ -23,7 +23,7 @@ For installing graphene, just run this command in your shell
.. code:: bash .. code:: bash
pip install "graphene-django>=2.0" pip install "graphene-django>=3"
Settings Settings
~~~~~~~~ ~~~~~~~~

View File

@ -166,16 +166,7 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``: After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
For Django 1.11: For Django 2.2 and above:
.. code:: python
urlpatterns = [
# some other urls
url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
]
For Django 2.0 and above:
.. code:: python .. code:: python

View File

@ -2,8 +2,8 @@ Filtering
========= =========
Graphene integrates with Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results. See the `usage `django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results.
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__ See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``. This filtering is automatically available when implementing a ``relay.Node``.

View File

@ -8,7 +8,7 @@ Requirements
Graphene-Django currently supports the following versions of Django: Graphene-Django currently supports the following versions of Django:
* >= Django 1.11 * >= Django 2.2
Installation Installation
------------ ------------
@ -32,19 +32,7 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of
We need to add a ``graphql`` URL to the ``urls.py`` of your Django project: We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
For Django 1.11: For Django 2.2 and above:
.. code:: python
from django.conf.urls import url
from graphene_django.views import GraphQLView
urlpatterns = [
# ...
url(r"graphql", GraphQLView.as_view(graphiql=True)),
]
For Django 2.0 and above:
.. code:: python .. code:: python

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,25 +19,23 @@ 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 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):
name = to_const(force_str(name)) name = to_const(force_str(name))
try: try:
assert_valid_name(name) assert_valid_name(name)
except AssertionError: except GraphQLError:
name = "A_%s" % name name = "A_%s" % name
return name return name
@ -52,7 +53,9 @@ def get_choices(choices):
while name in converted_names: while name in converted_names:
name += "_" + str(len(converted_names)) name += "_" + str(len(converted_names))
converted_names.append(name) converted_names.append(name)
description = help_text description = str(
help_text
) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58
yield name, value, description yield name, value, description
@ -64,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)
@ -276,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

@ -68,7 +68,7 @@ def test_should_query_nested_field():
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType) reporter = graphene.Field(ReporterType)
debug = graphene.Field(DjangoDebug, name="__debug") debug = graphene.Field(DjangoDebug, name="_debug")
def resolve_reporter(self, info, **args): def resolve_reporter(self, info, **args):
return Reporter.objects.first() return Reporter.objects.first()
@ -82,7 +82,7 @@ def test_should_query_nested_field():
pets { edges { node { lastName } } } pets { edges { node { lastName } } }
} } } } } }
} }
__debug { _debug {
sql { sql {
rawSql rawSql
} }
@ -110,12 +110,12 @@ def test_should_query_nested_field():
) )
assert not result.errors assert not result.errors
query = str(Reporter.objects.order_by("pk")[:1].query) query = str(Reporter.objects.order_by("pk")[:1].query)
assert result.data["__debug"]["sql"][0]["rawSql"] == query assert result.data["_debug"]["sql"][0]["rawSql"] == query
assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"] assert "COUNT" in result.data["_debug"]["sql"][1]["rawSql"]
assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] assert "tests_reporter_pets" in result.data["_debug"]["sql"][2]["rawSql"]
assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"] assert "COUNT" in result.data["_debug"]["sql"][3]["rawSql"]
assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"] assert "tests_reporter_pets" in result.data["_debug"]["sql"][4]["rawSql"]
assert len(result.data["__debug"]["sql"]) == 5 assert len(result.data["_debug"]["sql"]) == 5
assert result.data["reporter"] == expected["reporter"] assert result.data["reporter"] == expected["reporter"]

View File

@ -1,11 +1,12 @@
from functools import partial from functools import partial
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from graphql_relay.connection.arrayconnection import connection_from_list_slice 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
@ -122,15 +123,15 @@ class DjangoConnectionField(ConnectionField):
_len = iterable.count() _len = iterable.count()
else: else:
_len = len(iterable) _len = len(iterable)
connection = connection_from_list_slice( connection = connection_from_array_slice(
iterable, iterable,
args, args,
slice_start=0, slice_start=0,
list_length=_len, array_length=_len,
list_slice_length=_len, array_slice_length=_len,
connection_type=connection, connection_type=partial(connection_adapter, connection),
edge_type=connection.Edge, edge_type=connection.Edge,
pageinfo_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

@ -17,12 +17,14 @@ def mock_info():
None, None,
None, None,
None, None,
path=None,
schema=None, schema=None,
fragments=None, fragments=None,
root_value=None, root_value=None,
operation=None, operation=None,
variable_values=None, variable_values=None,
context=None, context=None,
is_awaitable=None,
) )

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

@ -612,9 +612,9 @@ def test_should_enforce_first_or_last(graphene_settings):
result = schema.execute(query) result = schema.execute(query)
assert len(result.errors) == 1 assert len(result.errors) == 1
assert str(result.errors[0]) == ( assert str(result.errors[0]).startswith(
"You must provide a `first` or `last` value to properly " "You must provide a `first` or `last` value to properly "
"paginate the `allReporters` connection." "paginate the `allReporters` connection.\n"
) )
assert result.data == expected assert result.data == expected
@ -653,9 +653,9 @@ def test_should_error_if_first_is_greater_than_max(graphene_settings):
result = schema.execute(query) result = schema.execute(query)
assert len(result.errors) == 1 assert len(result.errors) == 1
assert str(result.errors[0]) == ( assert str(result.errors[0]).startswith(
"Requesting 101 records on the `allReporters` connection " "Requesting 101 records on the `allReporters` connection "
"exceeds the `first` limit of 100 records." "exceeds the `first` limit of 100 records.\n"
) )
assert result.data == expected assert result.data == expected
@ -694,9 +694,9 @@ def test_should_error_if_last_is_greater_than_max(graphene_settings):
result = schema.execute(query) result = schema.execute(query)
assert len(result.errors) == 1 assert len(result.errors) == 1
assert str(result.errors[0]) == ( assert str(result.errors[0]).startswith(
"Requesting 101 records on the `allReporters` connection " "Requesting 101 records on the `allReporters` connection "
"exceeds the `last` limit of 100 records." "exceeds the `last` limit of 100 records.\n"
) )
assert result.data == expected assert result.data == expected
@ -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): def resolve_films(root, info, **kwargs):
qs = Film.objects.prefetch_related("reporters") qs = Film.objects.prefetch_related("reporters")
return qs return qs
@ -1105,9 +1105,10 @@ 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
def test_should_preserve_annotations(): def test_should_preserve_annotations():
@ -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"))
@ -1160,3 +1161,4 @@ def test_should_preserve_annotations():
} }
} }
assert result.data == expected, str(result.data) assert result.data == expected, str(result.data)
assert not result.errors

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

@ -99,12 +99,14 @@ def test_reports_validation_errors(client):
assert response_json(response) == { assert response_json(response) == {
"errors": [ "errors": [
{ {
"message": 'Cannot query field "unknownOne" on type "QueryRoot".', "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 9}], "locations": [{"line": 1, "column": 9}],
"path": None,
}, },
{ {
"message": 'Cannot query field "unknownTwo" on type "QueryRoot".', "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 21}], "locations": [{"line": 1, "column": 21}],
"path": None,
}, },
] ]
} }
@ -124,7 +126,9 @@ def test_errors_when_missing_operation_name(client):
assert response_json(response) == { assert response_json(response) == {
"errors": [ "errors": [
{ {
"message": "Must provide operation name if query contains multiple operations." "message": "Must provide operation name if query contains multiple operations.",
"locations": None,
"path": None,
} }
] ]
} }
@ -464,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

@ -6,14 +6,14 @@ from django.http import HttpResponse, HttpResponseNotAllowed
from django.http.response import HttpResponseBadRequest from django.http.response import HttpResponseBadRequest
from django.shortcuts import render from django.shortcuts import render
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import View
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 graphql import get_default_backend from graphql import OperationType, get_operation_ast, parse, validate
from graphql.error import format_error as format_graphql_error
from graphql.error import GraphQLError from graphql.error import GraphQLError
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 .settings import graphene_settings from .settings import graphene_settings
@ -56,8 +56,6 @@ class GraphQLView(View):
schema = None schema = None
graphiql = False graphiql = False
executor = None
backend = None
middleware = None middleware = None
root_value = None root_value = None
pretty = False pretty = False
@ -66,35 +64,28 @@ class GraphQLView(View):
def __init__( def __init__(
self, self,
schema=None, schema=None,
executor=None,
middleware=None, middleware=None,
root_value=None, root_value=None,
graphiql=False, graphiql=False,
pretty=False, pretty=False,
batch=False, batch=False,
backend=None,
): ):
if not schema: if not schema:
schema = graphene_settings.SCHEMA schema = graphene_settings.SCHEMA
if backend is None:
backend = get_default_backend()
if middleware is None: if middleware is None:
middleware = graphene_settings.MIDDLEWARE middleware = graphene_settings.MIDDLEWARE
self.schema = self.schema or schema self.schema = self.schema or schema
if middleware is not None: if middleware is not None:
self.middleware = list(instantiate_middleware(middleware)) self.middleware = list(instantiate_middleware(middleware))
self.executor = executor
self.root_value = root_value self.root_value = root_value
self.pretty = self.pretty or pretty self.pretty = self.pretty or pretty
self.graphiql = self.graphiql or graphiql self.graphiql = self.graphiql or graphiql
self.batch = self.batch or batch self.batch = self.batch or batch
self.backend = backend
assert isinstance( assert isinstance(
self.schema, GraphQLSchema self.schema, Schema
), "A Schema is required to be provided to GraphQLView." ), "A Schema is required to be provided to GraphQLView."
assert not all((graphiql, batch)), "Use either graphiql or batch processing" assert not all((graphiql, batch)), "Use either graphiql or batch processing"
@ -108,9 +99,6 @@ class GraphQLView(View):
def get_context(self, request): def get_context(self, request):
return request return request
def get_backend(self, request):
return self.backend
@method_decorator(ensure_csrf_cookie) @method_decorator(ensure_csrf_cookie)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
@ -172,7 +160,9 @@ class GraphQLView(View):
self.format_error(e) for e in execution_result.errors self.format_error(e) for e in execution_result.errors
] ]
if execution_result.invalid: if execution_result.errors and any(
not e.path for e in execution_result.errors
):
status_code = 400 status_code = 400
else: else:
response["data"] = execution_result.data response["data"] = execution_result.data
@ -245,14 +235,13 @@ class GraphQLView(View):
raise HttpError(HttpResponseBadRequest("Must provide query string.")) raise HttpError(HttpResponseBadRequest("Must provide query string."))
try: try:
backend = self.get_backend(request) document = parse(query)
document = backend.document_from_string(self.schema, query)
except Exception as e: except Exception as e:
return ExecutionResult(errors=[e], invalid=True) return ExecutionResult(errors=[e])
if request.method.lower() == "get": if request.method.lower() == "get":
operation_type = document.get_operation_type(operation_name) operation_ast = get_operation_ast(document, operation_name)
if operation_type and operation_type != "query": if operation_ast and operation_ast.operation != OperationType.QUERY:
if show_graphiql: if show_graphiql:
return None return None
@ -260,28 +249,23 @@ class GraphQLView(View):
HttpResponseNotAllowed( HttpResponseNotAllowed(
["POST"], ["POST"],
"Can only perform a {} operation from a POST request.".format( "Can only perform a {} operation from a POST request.".format(
operation_type operation_ast.operation.value
), ),
) )
) )
try: validation_errors = validate(self.schema.graphql_schema, document)
extra_options = {} if validation_errors:
if self.executor: return ExecutionResult(data=None, errors=validation_errors)
# We only include it optionally since
# executor is not a valid argument in all backends
extra_options["executor"] = self.executor
return document.execute( return self.schema.execute(
root_value=self.get_root_value(request), source=query,
variable_values=variables, root_value=self.get_root_value(request),
operation_name=operation_name, variable_values=variables,
context_value=self.get_context(request), operation_name=operation_name,
middleware=self.get_middleware(request), context_value=self.get_context(request),
**extra_options middleware=self.get_middleware(request),
) )
except Exception as e:
return ExecutionResult(errors=[e], invalid=True)
@classmethod @classmethod
def can_display_graphiql(cls, request, data): def can_display_graphiql(cls, request, data):

View File

@ -1,7 +1,8 @@
from setuptools import find_packages, setup
import ast import ast
import re import re
from setuptools import find_packages, setup
_version_re = re.compile(r"__version__\s+=\s+(.*)") _version_re = re.compile(r"__version__\s+=\s+(.*)")
with open("graphene_django/__init__.py", "rb") as f: with open("graphene_django/__init__.py", "rb") as f:
@ -53,9 +54,9 @@ setup(
keywords="api graphql protocol rest relay graphene", keywords="api graphql protocol rest relay graphene",
packages=find_packages(exclude=["tests"]), packages=find_packages(exclude=["tests"]),
install_requires=[ install_requires=[
"graphene>=2.1.7,<3", "graphene>=3.0.0b1,<4",
"graphql-core>=2.1.0,<3", "graphql-core>=3.1.0,<4",
"Django>=1.11,!=2.0.*,!=2.1.*", "Django>=2.2",
"promise>=2.1", "promise>=2.1",
], ],
setup_requires=["pytest-runner"], setup_requires=["pytest-runner"],

View File

@ -1,6 +1,6 @@
[tox] [tox]
envlist = envlist =
py{36,37,38}-django{111,22,30,master}, py{36,37,38}-django{22,30,master},
black,flake8 black,flake8
[gh-actions] [gh-actions]
@ -11,7 +11,6 @@ python =
[gh-actions:env] [gh-actions:env]
DJANGO = DJANGO =
1.11: django111
2.2: django22 2.2: django22
3.0: django30 3.0: django30
master: djangomaster master: djangomaster
@ -24,7 +23,6 @@ setenv =
deps = deps =
-e.[test] -e.[test]
psycopg2-binary psycopg2-binary
django111: Django>=1.11,<2.0
django22: Django>=2.2,<3.0 django22: Django>=2.2,<3.0
django30: Django>=3.0a1,<3.1 django30: Django>=3.0a1,<3.1
djangomaster: https://github.com/django/django/archive/master.zip djangomaster: https://github.com/django/django/archive/master.zip