diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..5dd418e
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,25 @@
+name: 🚀 Deploy to PyPI
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.8
+ - name: Build wheel and source tarball
+ run: |
+ python setup.py sdist bdist_wheel
+ - name: Publish a Python distribution to PyPI
+ uses: pypa/gh-action-pypi-publish@v1.1.0
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_password }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..20cf7fb
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,22 @@
+name: Lint
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.8
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install tox
+ - name: Run lint 💅
+ run: tox
+ env:
+ TOXENV: flake8
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..270b24e
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,28 @@
+name: Tests
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ max-parallel: 4
+ matrix:
+ django: ["2.2", "3.0"]
+ python-version: ["3.6", "3.7", "3.8"]
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v1
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install tox tox-gh-actions
+ - name: Test with tox
+ run: tox
+ env:
+ DJANGO: ${{ matrix.django }}
+ TOXENV: ${{ matrix.toxenv }}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f1ca201..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-language: python
-cache: pip
-dist: xenial
-
-install:
- - pip install tox tox-travis
-
-script:
- - tox
-
-after_success:
- - pip install coveralls
- - coveralls
-
-stages:
- - test
- - name: deploy
- if: tag IS present
-
-jobs:
- fast_finish: true
-
- allow_failures:
- - env: DJANGO=master
-
- include:
- - python: 3.6
- env: DJANGO=1.11
- - python: 3.6
- env: DJANGO=2.2
- - python: 3.6
- env: DJANGO=3.0
- - python: 3.6
- env: DJANGO=master
-
- - python: 3.7
- env: DJANGO=1.11
- - python: 3.7
- env: DJANGO=2.2
- - python: 3.7
- env: DJANGO=3.0
- - python: 3.7
- env: DJANGO=master
-
- - python: 3.8
- env: DJANGO=1.11
- - python: 3.8
- env: DJANGO=2.2
- - python: 3.8
- env: DJANGO=3.0
- - python: 3.8
- env: DJANGO=master
-
- - python: 3.8
- env: TOXENV=black,flake8
-
- - stage: deploy
- script: skip
- python: 3.8
- after_success: true
- deploy:
- provider: pypi
- user: syrusakbary
- on:
- tags: true
- password:
- secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=
- distributions: "sdist bdist_wheel"
diff --git a/README.md b/README.md
index 8605065..2490209 100644
--- a/README.md
+++ b/README.md
@@ -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
```bash
-pip install "graphene-django>=2.0"
+pip install "graphene-django>=3"
```
### Settings
diff --git a/README.rst b/README.rst
index 44feaee..4ac7dda 100644
--- a/README.rst
+++ b/README.rst
@@ -23,7 +23,7 @@ For installing graphene, just run this command in your shell
.. code:: bash
- pip install "graphene-django>=2.0"
+ pip install "graphene-django>=3"
Settings
~~~~~~~~
diff --git a/docs/authorization.rst b/docs/authorization.rst
index 7e09c37..8ef05b4 100644
--- a/docs/authorization.rst
+++ b/docs/authorization.rst
@@ -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``:
-For Django 1.11:
-
-.. code:: python
-
- urlpatterns = [
- # some other urls
- url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
- ]
-
-For Django 2.0 and above:
+For Django 2.2 and above:
.. code:: python
diff --git a/docs/filtering.rst b/docs/filtering.rst
index cab61ec..a511c64 100644
--- a/docs/filtering.rst
+++ b/docs/filtering.rst
@@ -2,8 +2,8 @@ Filtering
=========
Graphene integrates with
-`django-filter `__ to provide filtering of results. See the `usage
-documentation `__
+`django-filter `__ to provide filtering of results.
+See the `usage documentation `__
for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``.
@@ -123,6 +123,15 @@ create your own ``FilterSet``. You can pass it directly as follows:
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_expr=['iexact'])
+ # Allow multiple genera to be selected at once
+ genera = django_filters.MultipleChoiceFilter(
+ field_name='genus',
+ choices=(
+ ('Canis', 'Canis'),
+ ('Panthera', 'Panthera'),
+ ('Seahorse', 'Seahorse')
+ )
+ )
class Meta:
model = Animal
@@ -135,6 +144,22 @@ create your own ``FilterSet``. You can pass it directly as follows:
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
+
+If you were interested in selecting all dogs and cats, you might query as follows:
+
+.. code::
+
+ query {
+ allAnimals(genera: ["Canis", "Panthera"]) {
+ edges {
+ node {
+ id,
+ name
+ }
+ }
+ }
+ }
+
You can also specify the ``FilterSet`` class using the ``filterset_class``
parameter when defining your ``DjangoObjectType``, however, this can't be used
in unison with the ``filter_fields`` parameter:
@@ -162,6 +187,7 @@ in unison with the ``filter_fields`` parameter:
animal = relay.Node.Field(AnimalNode)
all_animals = DjangoFilterConnectionField(AnimalNode)
+
The context argument is passed on as the `request argument `__
in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
diff --git a/docs/installation.rst b/docs/installation.rst
index 048a994..573032e 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -8,7 +8,7 @@ Requirements
Graphene-Django currently supports the following versions of Django:
-* >= Django 1.11
+* >= Django 2.2
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:
-For Django 1.11:
-
-.. 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:
+For Django 2.2 and above:
.. code:: python
diff --git a/graphene_django/conftest.py b/graphene_django/conftest.py
new file mode 100644
index 0000000..509a84c
--- /dev/null
+++ b/graphene_django/conftest.py
@@ -0,0 +1,18 @@
+import pytest
+
+from graphene_django.settings import graphene_settings as gsettings
+
+from .registry import reset_global_registry
+
+
+@pytest.fixture(autouse=True)
+def reset_registry_fixture(db):
+ yield None
+ reset_global_registry()
+
+
+@pytest.fixture()
+def graphene_settings():
+ settings = dict(gsettings.__dict__)
+ yield gsettings
+ gsettings.__dict__ = settings
diff --git a/graphene_django/converter.py b/graphene_django/converter.py
index 36116ed..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,25 +19,23 @@ 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
+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):
name = to_const(force_str(name))
try:
assert_valid_name(name)
- except AssertionError:
+ except GraphQLError:
name = "A_%s" % name
return name
@@ -52,7 +53,9 @@ def get_choices(choices):
while name in converted_names:
name += "_" + str(len(converted_names))
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
@@ -64,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)
@@ -276,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/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py
index db8f275..7255ec6 100644
--- a/graphene_django/debug/tests/test_query.py
+++ b/graphene_django/debug/tests/test_query.py
@@ -1,5 +1,3 @@
-import pytest
-
import graphene
from graphene.relay import Node
from graphene_django import DjangoConnectionField, DjangoObjectType
@@ -13,11 +11,6 @@ class context(object):
pass
-# from examples.starwars_django.models import Character
-
-pytestmark = pytest.mark.django_db
-
-
def test_should_query_field():
r1 = Reporter(last_name="ABA")
r1.save()
@@ -75,7 +68,7 @@ def test_should_query_nested_field():
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
- debug = graphene.Field(DjangoDebug, name="__debug")
+ debug = graphene.Field(DjangoDebug, name="_debug")
def resolve_reporter(self, info, **args):
return Reporter.objects.first()
@@ -89,7 +82,7 @@ def test_should_query_nested_field():
pets { edges { node { lastName } } }
} } }
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -117,12 +110,12 @@ def test_should_query_nested_field():
)
assert not result.errors
query = str(Reporter.objects.order_by("pk")[:1].query)
- assert result.data["__debug"]["sql"][0]["rawSql"] == query
- assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"]
- assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"]
- assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"]
- assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"]
- assert len(result.data["__debug"]["sql"]) == 5
+ assert result.data["_debug"]["sql"][0]["rawSql"] == query
+ assert "COUNT" in result.data["_debug"]["sql"][1]["rawSql"]
+ assert "tests_reporter_pets" in result.data["_debug"]["sql"][2]["rawSql"]
+ assert "COUNT" in result.data["_debug"]["sql"][3]["rawSql"]
+ assert "tests_reporter_pets" in result.data["_debug"]["sql"][4]["rawSql"]
+ assert len(result.data["_debug"]["sql"]) == 5
assert result.data["reporter"] == expected["reporter"]
diff --git a/graphene_django/fields.py b/graphene_django/fields.py
index f0a3828..418a14b 100644
--- a/graphene_django/fields.py
+++ b/graphene_django/fields.py
@@ -1,11 +1,12 @@
from functools import partial
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 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
@@ -122,15 +123,15 @@ class DjangoConnectionField(ConnectionField):
_len = iterable.count()
else:
_len = len(iterable)
- connection = connection_from_list_slice(
+ connection = connection_from_array_slice(
iterable,
args,
slice_start=0,
- list_length=_len,
- list_slice_length=_len,
- connection_type=connection,
+ array_length=_len,
+ array_slice_length=_len,
+ connection_type=partial(connection_adapter, connection),
edge_type=connection.Edge,
- pageinfo_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 a0f7d96..59cc30b 100644
--- a/graphene_django/filter/tests/test_fields.py
+++ b/graphene_django/filter/tests/test_fields.py
@@ -35,9 +35,6 @@ else:
)
)
-pytestmark.append(pytest.mark.django_db)
-
-
if DJANGO_FILTER_INSTALLED:
class ArticleNode(DjangoObjectType):
@@ -809,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!
}
"""
)
@@ -861,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/forms/converter.py b/graphene_django/forms/converter.py
index 7b154b4..077e984 100644
--- a/graphene_django/forms/converter.py
+++ b/graphene_django/forms/converter.py
@@ -55,9 +55,14 @@ def convert_form_field_to_float(field):
return Float(description=field.help_text, required=field.required)
+@convert_form_field.register(forms.MultipleChoiceField)
+def convert_form_field_to_string_list(field):
+ return List(String, description=field.help_text, required=field.required)
+
+
@convert_form_field.register(forms.ModelMultipleChoiceField)
@convert_form_field.register(GlobalIDMultipleChoiceField)
-def convert_form_field_to_list(field):
+def convert_form_field_to_id_list(field):
return List(ID, required=field.required)
diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py
index 955b952..29a4419 100644
--- a/graphene_django/forms/tests/test_converter.py
+++ b/graphene_django/forms/tests/test_converter.py
@@ -66,6 +66,10 @@ def test_should_choice_convert_string():
assert_conversion(forms.ChoiceField, String)
+def test_should_multiple_choice_convert_list():
+ assert_conversion(forms.MultipleChoiceField, List)
+
+
def test_should_base_field_convert_string():
assert_conversion(forms.Field, String)
diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py
index 093f398..a455a0a 100644
--- a/graphene_django/forms/tests/test_mutation.py
+++ b/graphene_django/forms/tests/test_mutation.py
@@ -1,16 +1,25 @@
+import pytest
from django import forms
-from django.test import TestCase
from django.core.exceptions import ValidationError
from py.test import raises
-from graphene import ObjectType, Schema, String, Field
+from graphene import Field, ObjectType, Schema, String
from graphene_django import DjangoObjectType
-from graphene_django.tests.models import Film, Pet
+from graphene_django.tests.models import Pet
-from ...settings import graphene_settings
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
+@pytest.fixture()
+def pet_type():
+ class PetType(DjangoObjectType):
+ class Meta:
+ model = Pet
+ fields = "__all__"
+
+ return PetType
+
+
class MyForm(forms.Form):
text = forms.CharField()
@@ -36,18 +45,6 @@ class PetForm(forms.ModelForm):
return age
-class PetType(DjangoObjectType):
- class Meta:
- model = Pet
- fields = "__all__"
-
-
-class FilmType(DjangoObjectType):
- class Meta:
- model = Film
- fields = "__all__"
-
-
def test_needs_form_class():
with raises(Exception) as exc:
@@ -73,7 +70,7 @@ def test_has_input_fields():
assert "text" in MyMutation.Input._meta.fields
-def test_mutation_error_camelcased():
+def test_mutation_error_camelcased(pet_type, graphene_settings):
class ExtraPetForm(PetForm):
test_field = forms.CharField(required=True)
@@ -86,234 +83,237 @@ def test_mutation_error_camelcased():
graphene_settings.CAMELCASE_ERRORS = True
result = PetMutation.mutate_and_get_payload(None, None)
assert {f.field for f in result.errors} == {"name", "age", "testField"}
- graphene_settings.CAMELCASE_ERRORS = False
class MockQuery(ObjectType):
a = String()
-class FormMutationTests(TestCase):
- def test_form_invalid_form(self):
- class MyMutation(DjangoFormMutation):
- class Meta:
- form_class = MyForm
+def test_form_invalid_form():
+ class MyMutation(DjangoFormMutation):
+ class Meta:
+ form_class = MyForm
- class Mutation(ObjectType):
- my_mutation = MyMutation.Field()
+ class Mutation(ObjectType):
+ my_mutation = MyMutation.Field()
- schema = Schema(query=MockQuery, mutation=Mutation)
+ schema = Schema(query=MockQuery, mutation=Mutation)
- result = schema.execute(
- """ mutation MyMutation {
- myMutation(input: { text: "INVALID_INPUT" }) {
- errors {
- field
- messages
- }
- text
+ result = schema.execute(
+ """ mutation MyMutation {
+ myMutation(input: { text: "INVALID_INPUT" }) {
+ errors {
+ field
+ messages
+ }
+ text
+ }
+ }
+ """
+ )
+
+ assert result.errors is None
+ assert result.data["myMutation"]["errors"] == [
+ {"field": "text", "messages": ["Invalid input"]}
+ ]
+
+
+def test_form_valid_input():
+ class MyMutation(DjangoFormMutation):
+ class Meta:
+ form_class = MyForm
+
+ class Mutation(ObjectType):
+ my_mutation = MyMutation.Field()
+
+ schema = Schema(query=MockQuery, mutation=Mutation)
+
+ result = schema.execute(
+ """ mutation MyMutation {
+ myMutation(input: { text: "VALID_INPUT" }) {
+ errors {
+ field
+ messages
+ }
+ text
+ }
+ }
+ """
+ )
+
+ assert result.errors is None
+ assert result.data["myMutation"]["errors"] == []
+ assert result.data["myMutation"]["text"] == "VALID_INPUT"
+
+
+def test_default_meta_fields(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ class Meta:
+ form_class = PetForm
+
+ assert PetMutation._meta.model is Pet
+ assert PetMutation._meta.return_field_name == "pet"
+ assert "pet" in PetMutation._meta.fields
+
+
+def test_default_input_meta_fields(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ class Meta:
+ form_class = PetForm
+
+ assert PetMutation._meta.model is Pet
+ assert PetMutation._meta.return_field_name == "pet"
+ assert "name" in PetMutation.Input._meta.fields
+ assert "client_mutation_id" in PetMutation.Input._meta.fields
+ assert "id" in PetMutation.Input._meta.fields
+
+
+def test_exclude_fields_input_meta_fields(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ class Meta:
+ form_class = PetForm
+ exclude_fields = ["id"]
+
+ assert PetMutation._meta.model is Pet
+ assert PetMutation._meta.return_field_name == "pet"
+ assert "name" in PetMutation.Input._meta.fields
+ assert "age" in PetMutation.Input._meta.fields
+ assert "client_mutation_id" in PetMutation.Input._meta.fields
+ assert "id" not in PetMutation.Input._meta.fields
+
+
+def test_custom_return_field_name(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ class Meta:
+ form_class = PetForm
+ model = Pet
+ return_field_name = "animal"
+
+ assert PetMutation._meta.model is Pet
+ assert PetMutation._meta.return_field_name == "animal"
+ assert "animal" in PetMutation._meta.fields
+
+
+def test_model_form_mutation_mutate_existing(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ pet = Field(pet_type)
+
+ class Meta:
+ form_class = PetForm
+
+ class Mutation(ObjectType):
+ pet_mutation = PetMutation.Field()
+
+ schema = Schema(query=MockQuery, mutation=Mutation)
+
+ pet = Pet.objects.create(name="Axel", age=10)
+
+ result = schema.execute(
+ """ mutation PetMutation($pk: ID!) {
+ petMutation(input: { id: $pk, name: "Mia", age: 10 }) {
+ pet {
+ name
+ age
}
}
- """
- )
+ }
+ """,
+ variable_values={"pk": pet.pk},
+ )
- self.assertIs(result.errors, None)
- self.assertEqual(
- result.data["myMutation"]["errors"],
- [{"field": "text", "messages": ["Invalid input"]}],
- )
+ assert result.errors is None
+ assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10}
- def test_form_valid_input(self):
- class MyMutation(DjangoFormMutation):
- class Meta:
- form_class = MyForm
+ assert Pet.objects.count() == 1
+ pet.refresh_from_db()
+ assert pet.name == "Mia"
- class Mutation(ObjectType):
- my_mutation = MyMutation.Field()
- schema = Schema(query=MockQuery, mutation=Mutation)
+def test_model_form_mutation_creates_new(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ pet = Field(pet_type)
- result = schema.execute(
- """ mutation MyMutation {
- myMutation(input: { text: "VALID_INPUT" }) {
- errors {
- field
- messages
- }
- text
+ class Meta:
+ form_class = PetForm
+
+ class Mutation(ObjectType):
+ pet_mutation = PetMutation.Field()
+
+ schema = Schema(query=MockQuery, mutation=Mutation)
+
+ result = schema.execute(
+ """ mutation PetMutation {
+ petMutation(input: { name: "Mia", age: 10 }) {
+ pet {
+ name
+ age
+ }
+ errors {
+ field
+ messages
}
}
- """
- )
+ }
+ """
+ )
+ assert result.errors is None
+ assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10}
- self.assertIs(result.errors, None)
- self.assertEqual(result.data["myMutation"]["errors"], [])
- self.assertEqual(result.data["myMutation"]["text"], "VALID_INPUT")
+ assert Pet.objects.count() == 1
+ pet = Pet.objects.get()
+ assert pet.name == "Mia"
+ assert pet.age == 10
-class ModelFormMutationTests(TestCase):
- def test_default_meta_fields(self):
- class PetMutation(DjangoModelFormMutation):
- class Meta:
- form_class = PetForm
+def test_model_form_mutation_invalid_input(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ pet = Field(pet_type)
- self.assertEqual(PetMutation._meta.model, Pet)
- self.assertEqual(PetMutation._meta.return_field_name, "pet")
- self.assertIn("pet", PetMutation._meta.fields)
+ class Meta:
+ form_class = PetForm
- def test_default_input_meta_fields(self):
- class PetMutation(DjangoModelFormMutation):
- class Meta:
- form_class = PetForm
+ class Mutation(ObjectType):
+ pet_mutation = PetMutation.Field()
- self.assertEqual(PetMutation._meta.model, Pet)
- self.assertEqual(PetMutation._meta.return_field_name, "pet")
- self.assertIn("name", PetMutation.Input._meta.fields)
- self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
- self.assertIn("id", PetMutation.Input._meta.fields)
+ schema = Schema(query=MockQuery, mutation=Mutation)
- def test_exclude_fields_input_meta_fields(self):
- class PetMutation(DjangoModelFormMutation):
- class Meta:
- form_class = PetForm
- exclude_fields = ["id"]
-
- self.assertEqual(PetMutation._meta.model, Pet)
- self.assertEqual(PetMutation._meta.return_field_name, "pet")
- self.assertIn("name", PetMutation.Input._meta.fields)
- self.assertIn("age", PetMutation.Input._meta.fields)
- self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
- self.assertNotIn("id", PetMutation.Input._meta.fields)
-
- def test_custom_return_field_name(self):
- class PetMutation(DjangoModelFormMutation):
- class Meta:
- form_class = PetForm
- model = Pet
- return_field_name = "animal"
-
- self.assertEqual(PetMutation._meta.model, Pet)
- self.assertEqual(PetMutation._meta.return_field_name, "animal")
- self.assertIn("animal", PetMutation._meta.fields)
-
- def test_model_form_mutation_mutate_existing(self):
- class PetMutation(DjangoModelFormMutation):
- pet = Field(PetType)
-
- class Meta:
- form_class = PetForm
-
- class Mutation(ObjectType):
- pet_mutation = PetMutation.Field()
-
- schema = Schema(query=MockQuery, mutation=Mutation)
-
- pet = Pet.objects.create(name="Axel", age=10)
-
- result = schema.execute(
- """ mutation PetMutation($pk: ID!) {
- petMutation(input: { id: $pk, name: "Mia", age: 10 }) {
- pet {
- name
- age
- }
+ result = schema.execute(
+ """ mutation PetMutation {
+ petMutation(input: { name: "Mia", age: 99 }) {
+ pet {
+ name
+ age
+ }
+ errors {
+ field
+ messages
}
}
- """,
- variable_values={"pk": pet.pk},
- )
+ }
+ """
+ )
+ assert result.errors is None
+ assert result.data["petMutation"]["pet"] is None
+ assert result.data["petMutation"]["errors"] == [
+ {"field": "age", "messages": ["Too old"]}
+ ]
- self.assertIs(result.errors, None)
- self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
+ assert Pet.objects.count() == 0
- self.assertEqual(Pet.objects.count(), 1)
- pet.refresh_from_db()
- self.assertEqual(pet.name, "Mia")
- def test_model_form_mutation_creates_new(self):
- class PetMutation(DjangoModelFormMutation):
- pet = Field(PetType)
+def test_model_form_mutation_mutate_invalid_form(pet_type):
+ class PetMutation(DjangoModelFormMutation):
+ class Meta:
+ form_class = PetForm
- class Meta:
- form_class = PetForm
+ result = PetMutation.mutate_and_get_payload(None, None)
- class Mutation(ObjectType):
- pet_mutation = PetMutation.Field()
+ # A pet was not created
+ Pet.objects.count() == 0
- schema = Schema(query=MockQuery, mutation=Mutation)
-
- result = schema.execute(
- """ mutation PetMutation {
- petMutation(input: { name: "Mia", age: 10 }) {
- pet {
- name
- age
- }
- errors {
- field
- messages
- }
- }
- }
- """
- )
- self.assertIs(result.errors, None)
- self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
-
- self.assertEqual(Pet.objects.count(), 1)
- pet = Pet.objects.get()
- self.assertEqual(pet.name, "Mia")
- self.assertEqual(pet.age, 10)
-
- def test_model_form_mutation_invalid_input(self):
- class PetMutation(DjangoModelFormMutation):
- pet = Field(PetType)
-
- class Meta:
- form_class = PetForm
-
- class Mutation(ObjectType):
- pet_mutation = PetMutation.Field()
-
- schema = Schema(query=MockQuery, mutation=Mutation)
-
- result = schema.execute(
- """ mutation PetMutation {
- petMutation(input: { name: "Mia", age: 99 }) {
- pet {
- name
- age
- }
- errors {
- field
- messages
- }
- }
- }
- """
- )
- self.assertIs(result.errors, None)
- self.assertEqual(result.data["petMutation"]["pet"], None)
- self.assertEqual(
- result.data["petMutation"]["errors"],
- [{"field": "age", "messages": ["Too old"],}],
- )
-
- self.assertEqual(Pet.objects.count(), 0)
-
- def test_model_form_mutation_mutate_invalid_form(self):
- class PetMutation(DjangoModelFormMutation):
- class Meta:
- form_class = PetForm
-
- result = PetMutation.mutate_and_get_payload(None, None)
-
- # A pet was not created
- self.assertEqual(Pet.objects.count(), 0)
-
- fields_w_error = [e.field for e in result.errors]
- self.assertEqual(len(result.errors), 2)
- self.assertIn("name", fields_w_error)
- self.assertEqual(result.errors[0].messages, ["This field is required."])
- self.assertIn("age", fields_w_error)
- self.assertEqual(result.errors[1].messages, ["This field is required."])
+ fields_w_error = [e.field for e in result.errors]
+ assert len(result.errors) == 2
+ assert result.errors[0].messages == ["This field is required."]
+ assert result.errors[1].messages == ["This field is required."]
+ assert "age" in fields_w_error
+ assert "name" in fields_w_error
diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py
index 751a385..9cf55ca 100644
--- a/graphene_django/management/commands/graphql_schema.py
+++ b/graphene_django/management/commands/graphql_schema.py
@@ -48,6 +48,7 @@ class CommandArguments(BaseCommand):
class Command(CommandArguments):
help = "Dump Graphene schema as a JSON or GraphQL file"
can_import_settings = True
+ requires_system_checks = False
def save_json_file(self, out, schema_dict, indent):
with open(out, "w") as outfile:
@@ -55,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/rest_framework/tests/test_multiple_model_serializers.py b/graphene_django/rest_framework/tests/test_multiple_model_serializers.py
index c1f4626..1676b62 100644
--- a/graphene_django/rest_framework/tests/test_multiple_model_serializers.py
+++ b/graphene_django/rest_framework/tests/test_multiple_model_serializers.py
@@ -1,14 +1,11 @@
-import graphene
-import pytest
from django.db import models
-from graphene import Schema
from rest_framework import serializers
+import graphene
+from graphene import Schema
from graphene_django import DjangoObjectType
from graphene_django.rest_framework.mutation import SerializerMutation
-pytestmark = pytest.mark.django_db
-
class MyFakeChildModel(models.Model):
name = models.CharField(max_length=50)
diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py
index 5bf3bc1..1b31e36 100644
--- a/graphene_django/rest_framework/tests/test_mutation.py
+++ b/graphene_django/rest_framework/tests/test_mutation.py
@@ -1,14 +1,13 @@
import datetime
-from py.test import mark, raises
+from py.test import raises
from rest_framework import serializers
from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType
-from ...settings import graphene_settings
from ...types import DjangoObjectType
-from ..models import MyFakeModel, MyFakeModelWithPassword, MyFakeModelWithDate
+from ..models import MyFakeModel, MyFakeModelWithDate, MyFakeModelWithPassword
from ..mutation import SerializerMutation
@@ -18,12 +17,14 @@ def mock_info():
None,
None,
None,
+ path=None,
schema=None,
fragments=None,
root_value=None,
operation=None,
variable_values=None,
context=None,
+ is_awaitable=None,
)
@@ -99,7 +100,6 @@ def test_exclude_fields():
assert "created" not in MyMutation.Input._meta.fields
-@mark.django_db
def test_write_only_field():
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
@@ -122,7 +122,6 @@ def test_write_only_field():
), "'password' is write_only field and shouldn't be visible"
-@mark.django_db
def test_write_only_field_using_extra_kwargs():
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
class Meta:
@@ -144,7 +143,6 @@ def test_write_only_field_using_extra_kwargs():
), "'password' is write_only field and shouldn't be visible"
-@mark.django_db
def test_read_only_fields():
class ReadOnlyFieldModelSerializer(serializers.ModelSerializer):
cool_name = serializers.CharField(read_only=True)
@@ -194,7 +192,6 @@ def test_mutate_and_get_payload_success():
assert result.errors is None
-@mark.django_db
def test_model_add_mutate_and_get_payload_success():
result = MyModelMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "Narf"}
@@ -204,7 +201,6 @@ def test_model_add_mutate_and_get_payload_success():
assert isinstance(result.created, datetime.datetime)
-@mark.django_db
def test_model_update_mutate_and_get_payload_success():
instance = MyFakeModel.objects.create(cool_name="Narf")
result = MyModelMutation.mutate_and_get_payload(
@@ -214,7 +210,6 @@ def test_model_update_mutate_and_get_payload_success():
assert result.cool_name == "New Narf"
-@mark.django_db
def test_model_partial_update_mutate_and_get_payload_success():
instance = MyFakeModel.objects.create(cool_name="Narf")
result = MyModelMutation.mutate_and_get_payload(
@@ -224,7 +219,6 @@ def test_model_partial_update_mutate_and_get_payload_success():
assert result.cool_name == "Narf"
-@mark.django_db
def test_model_invalid_update_mutate_and_get_payload_success():
class InvalidModelMutation(SerializerMutation):
class Meta:
@@ -239,7 +233,6 @@ def test_model_invalid_update_mutate_and_get_payload_success():
assert '"id" required' in str(exc.value)
-@mark.django_db
def test_perform_mutate_success():
class MyMethodMutation(SerializerMutation):
class Meta:
@@ -272,11 +265,10 @@ def test_model_mutate_and_get_payload_error():
assert len(result.errors) > 0
-def test_mutation_error_camelcased():
+def test_mutation_error_camelcased(graphene_settings):
graphene_settings.CAMELCASE_ERRORS = True
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
assert result.errors[0].field == "coolName"
- graphene_settings.CAMELCASE_ERRORS = False
def test_invalid_serializer_operations():
diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py
index 6dfe330..70116b8 100644
--- a/graphene_django/tests/test_command.py
+++ b/graphene_django/tests/test_command.py
@@ -8,7 +8,7 @@ from graphene import ObjectType, Schema, String
@patch("graphene_django.management.commands.graphql_schema.Command.save_json_file")
-def test_generate_json_file_on_call_graphql_schema(savefile_mock, settings):
+def test_generate_json_file_on_call_graphql_schema(savefile_mock):
out = StringIO()
management.call_command("graphql_schema", schema="", stdout=out)
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
@@ -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_converter.py b/graphene_django/tests/test_converter.py
index f1d1b9b..0225e66 100644
--- a/graphene_django/tests/test_converter.py
+++ b/graphene_django/tests/test_converter.py
@@ -1,16 +1,17 @@
-import pytest
from collections import namedtuple
+
+import pytest
from django.db import models
from django.utils.translation import gettext_lazy as _
-from graphene import NonNull
from py.test import raises
import graphene
+from graphene import NonNull
from graphene.relay import ConnectionField, Node
-from graphene.types.datetime import DateTime, Date, Time
+from graphene.types.datetime import Date, DateTime, Time
from graphene.types.json import JSONString
-from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
+from ..compat import ArrayField, HStoreField, JSONField, MissingType, RangeField
from ..converter import (
convert_django_field,
convert_django_field_with_choices,
@@ -18,10 +19,8 @@ from ..converter import (
)
from ..registry import Registry
from ..types import DjangoObjectType
-from ..settings import graphene_settings
from .models import Article, Film, FilmDetails, Reporter
-
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
@@ -333,7 +332,7 @@ def test_should_postgres_range_convert_list():
assert field.type.of_type.of_type == graphene.Int
-def test_generate_enum_name():
+def test_generate_enum_name(graphene_settings):
MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"])
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
@@ -351,5 +350,3 @@ def test_generate_enum_name():
generate_enum_name(model_meta, field)
== "SomeLongAppNameSomeObjectFizzBuzzChoices"
)
-
- graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py
index 8ea1901..67b3a35 100644
--- a/graphene_django/tests/test_fields.py
+++ b/graphene_django/tests/test_fields.py
@@ -10,7 +10,6 @@ from .models import Article as ArticleModel
from .models import Reporter as ReporterModel
-@pytest.mark.django_db
class TestDjangoListField:
def test_only_django_object_types(self):
class TestType(ObjectType):
diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py
index 95db2d1..75053db 100644
--- a/graphene_django/tests/test_query.py
+++ b/graphene_django/tests/test_query.py
@@ -1,25 +1,20 @@
-import base64
import datetime
import pytest
from django.db import models
+from django.db.models import Q
from django.utils.functional import SimpleLazyObject
+from graphql_relay import to_global_id
from py.test import raises
-from django.db.models import Q
-
-from graphql_relay import to_global_id
import graphene
from graphene.relay import Node
-from ..utils import DJANGO_FILTER_INSTALLED
-from ..compat import MissingType, JSONField
+from ..compat import JSONField, MissingType
from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
-from ..settings import graphene_settings
-from .models import Article, CNNReporter, Reporter, Film, FilmDetails
-
-pytestmark = pytest.mark.django_db
+from ..utils import DJANGO_FILTER_INSTALLED
+from .models import Article, CNNReporter, Film, FilmDetails, Reporter
def test_should_query_only_fields():
@@ -147,9 +142,6 @@ def test_should_query_postgres_fields():
def test_should_node():
- # reset_global_registry()
- # Node._meta.registry = get_global_registry()
-
class ReporterNode(DjangoObjectType):
class Meta:
model = Reporter
@@ -588,7 +580,7 @@ def test_should_query_node_multiple_filtering():
assert result.data == expected
-def test_should_enforce_first_or_last():
+def test_should_enforce_first_or_last(graphene_settings):
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True
class ReporterType(DjangoObjectType):
@@ -620,14 +612,14 @@ def test_should_enforce_first_or_last():
result = schema.execute(query)
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 "
- "paginate the `allReporters` connection."
+ "paginate the `allReporters` connection.\n"
)
assert result.data == expected
-def test_should_error_if_first_is_greater_than_max():
+def test_should_error_if_first_is_greater_than_max(graphene_settings):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
class ReporterType(DjangoObjectType):
@@ -661,16 +653,14 @@ def test_should_error_if_first_is_greater_than_max():
result = schema.execute(query)
assert len(result.errors) == 1
- assert str(result.errors[0]) == (
+ assert str(result.errors[0]).startswith(
"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
- graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
-
-def test_should_error_if_last_is_greater_than_max():
+def test_should_error_if_last_is_greater_than_max(graphene_settings):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
class ReporterType(DjangoObjectType):
@@ -704,14 +694,12 @@ def test_should_error_if_last_is_greater_than_max():
result = schema.execute(query)
assert len(result.errors) == 1
- assert str(result.errors[0]) == (
+ assert str(result.errors[0]).startswith(
"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
- graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
-
def test_should_query_promise_connectionfields():
from promise import Promise
@@ -725,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 = """
@@ -854,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)
@@ -1087,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):
+ def resolve_films(root, info, **kwargs):
qs = Film.objects.prefetch_related("reporters")
return qs
@@ -1117,9 +1105,10 @@ 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
+ assert not result.errors
def test_should_preserve_annotations():
@@ -1139,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"))
@@ -1172,3 +1161,4 @@ def test_should_preserve_annotations():
}
}
assert result.data == expected, str(result.data)
+ assert not result.errors
diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py
index 888521f..2a6d357 100644
--- a/graphene_django/tests/test_types.py
+++ b/graphene_django/tests/test_types.py
@@ -9,14 +9,10 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String
from graphene.relay import Node
from .. import registry
-from ..settings import graphene_settings
from ..types import DjangoObjectType, DjangoObjectTypeOptions
-from ..converter import convert_choice_field_to_enum
from .models import Article as ArticleModel
from .models import Reporter as ReporterModel
-registry.reset_global_registry()
-
class Reporter(DjangoObjectType):
"""Reporter description"""
@@ -115,90 +111,171 @@ 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
def with_local_registry(func):
def inner(*args, **kwargs):
old = registry.get_global_registry()
- registry.reset_global_registry()
try:
retval = func(*args, **kwargs)
except Exception as e:
@@ -420,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):
@@ -449,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):
@@ -483,23 +566,26 @@ 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(self, PetModel):
+ def test_django_objecttype_convert_choices_enum_naming_collisions(
+ self, PetModel, graphene_settings
+ ):
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
class PetModelKind(DjangoObjectType):
@@ -514,28 +600,32 @@ 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
+ }
+ """
)
- graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
- def test_django_objecttype_choices_custom_enum_name(self, PetModel):
+ def test_django_objecttype_choices_custom_enum_name(
+ self, PetModel, graphene_settings
+ ):
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = (
"graphene_django.tests.test_types.custom_enum_name"
)
@@ -552,24 +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
+ }
+ """
)
-
- graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = None
diff --git a/graphene_django/tests/test_utils.py b/graphene_django/tests/test_utils.py
index 55cfd4f..c0d376b 100644
--- a/graphene_django/tests/test_utils.py
+++ b/graphene_django/tests/test_utils.py
@@ -1,6 +1,10 @@
-from django.utils.translation import gettext_lazy
+import json
-from ..utils import camelize, get_model_fields
+import pytest
+from django.utils.translation import gettext_lazy
+from mock import patch
+
+from ..utils import camelize, get_model_fields, GraphQLTestCase
from .models import Film, Reporter
@@ -30,3 +34,27 @@ def test_camelize():
"valueA": "value_b"
}
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}
+
+
+@pytest.mark.django_db
+@patch("graphene_django.utils.testing.Client.post")
+def test_graphql_test_case_op_name(post_mock):
+ """
+ Test that `GraphQLTestCase.query()`'s `op_name` argument produces an `operationName` field.
+ """
+
+ class TestClass(GraphQLTestCase):
+ GRAPHQL_SCHEMA = True
+
+ def runTest(self):
+ pass
+
+ tc = TestClass()
+ tc.setUpClass()
+ tc.query("query { }", op_name="QueryName")
+ body = json.loads(post_mock.call_args.args[1])
+ # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
+ assert (
+ "operationName",
+ "QueryName",
+ ) in body.items(), "Field 'operationName' is not present in the final request."
diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py
index db6cc4e..1c027d9 100644
--- a/graphene_django/tests/test_views.py
+++ b/graphene_django/tests/test_views.py
@@ -99,12 +99,14 @@ def test_reports_validation_errors(client):
assert response_json(response) == {
"errors": [
{
- "message": 'Cannot query field "unknownOne" on type "QueryRoot".',
+ "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
"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}],
+ "path": None,
},
]
}
@@ -124,7 +126,9 @@ def test_errors_when_missing_operation_name(client):
assert response_json(response) == {
"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": [
{
"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/utils/testing.py b/graphene_django/utils/testing.py
index 6365d81..0eba4fd 100644
--- a/graphene_django/utils/testing.py
+++ b/graphene_django/utils/testing.py
@@ -34,20 +34,20 @@ class GraphQLTestCase(TestCase):
supply the op_name. For annon queries ("{ ... }"),
should be None (default).
input_data (dict) - If provided, the $input variable in GraphQL will be set
- to this value. If both ``input_data`` and ``variables``,
+ to this value. If both ``input_data`` and ``variables``,
are provided, the ``input`` field in the ``variables``
dict will be overwritten with this value.
variables (dict) - If provided, the "variables" field in GraphQL will be
set to this value.
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
- will be set to this value.
+ will be set to this value.
Returns:
Response object from client
"""
body = {"query": query}
- if operation_name:
- body["operationName"] = operation_name
+ if op_name:
+ body["operationName"] = op_name
if variables:
body["variables"] = variables
if input_data:
diff --git a/graphene_django/views.py b/graphene_django/views.py
index 8d57d50..1a373c7 100644
--- a/graphene_django/views.py
+++ b/graphene_django/views.py
@@ -6,14 +6,14 @@ from django.http import HttpResponse, HttpResponseNotAllowed
from django.http.response import HttpResponseBadRequest
from django.shortcuts import render
from django.utils.decorators import method_decorator
-from django.views.generic import View
from django.views.decorators.csrf import ensure_csrf_cookie
-
-from graphql import get_default_backend
-from graphql.error import format_error as format_graphql_error
+from django.views.generic import View
+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
from .settings import graphene_settings
@@ -56,8 +56,6 @@ class GraphQLView(View):
schema = None
graphiql = False
- executor = None
- backend = None
middleware = None
root_value = None
pretty = False
@@ -66,35 +64,28 @@ class GraphQLView(View):
def __init__(
self,
schema=None,
- executor=None,
middleware=None,
root_value=None,
graphiql=False,
pretty=False,
batch=False,
- backend=None,
):
if not schema:
schema = graphene_settings.SCHEMA
- if backend is None:
- backend = get_default_backend()
-
if middleware is None:
middleware = graphene_settings.MIDDLEWARE
self.schema = self.schema or schema
if middleware is not None:
self.middleware = list(instantiate_middleware(middleware))
- self.executor = executor
self.root_value = root_value
self.pretty = self.pretty or pretty
self.graphiql = self.graphiql or graphiql
self.batch = self.batch or batch
- self.backend = backend
assert isinstance(
- self.schema, GraphQLSchema
+ self.schema, Schema
), "A Schema is required to be provided to GraphQLView."
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
@@ -108,9 +99,6 @@ class GraphQLView(View):
def get_context(self, request):
return request
- def get_backend(self, request):
- return self.backend
-
@method_decorator(ensure_csrf_cookie)
def dispatch(self, request, *args, **kwargs):
try:
@@ -172,7 +160,9 @@ class GraphQLView(View):
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
else:
response["data"] = execution_result.data
@@ -245,14 +235,13 @@ class GraphQLView(View):
raise HttpError(HttpResponseBadRequest("Must provide query string."))
try:
- backend = self.get_backend(request)
- document = backend.document_from_string(self.schema, query)
+ document = parse(query)
except Exception as e:
- return ExecutionResult(errors=[e], invalid=True)
+ return ExecutionResult(errors=[e])
if request.method.lower() == "get":
- operation_type = document.get_operation_type(operation_name)
- if operation_type and operation_type != "query":
+ operation_ast = get_operation_ast(document, operation_name)
+ if operation_ast and operation_ast.operation != OperationType.QUERY:
if show_graphiql:
return None
@@ -260,28 +249,23 @@ class GraphQLView(View):
HttpResponseNotAllowed(
["POST"],
"Can only perform a {} operation from a POST request.".format(
- operation_type
+ operation_ast.operation.value
),
)
)
- try:
- extra_options = {}
- if self.executor:
- # We only include it optionally since
- # executor is not a valid argument in all backends
- extra_options["executor"] = self.executor
+ validation_errors = validate(self.schema.graphql_schema, document)
+ if validation_errors:
+ return ExecutionResult(data=None, errors=validation_errors)
- return document.execute(
- root_value=self.get_root_value(request),
- variable_values=variables,
- operation_name=operation_name,
- context_value=self.get_context(request),
- middleware=self.get_middleware(request),
- **extra_options
- )
- except Exception as e:
- return ExecutionResult(errors=[e], invalid=True)
+ return self.schema.execute(
+ source=query,
+ root_value=self.get_root_value(request),
+ variable_values=variables,
+ operation_name=operation_name,
+ context_value=self.get_context(request),
+ middleware=self.get_middleware(request),
+ )
@classmethod
def can_display_graphiql(cls, request, data):
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 4e47ff4..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[pytest]
-DJANGO_SETTINGS_MODULE = django_test_settings
diff --git a/setup.cfg b/setup.cfg
index def0b67..d588786 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -43,3 +43,7 @@ include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
+
+[tool:pytest]
+DJANGO_SETTINGS_MODULE = django_test_settings
+addopts = --random-order
diff --git a/setup.py b/setup.py
index 3639fb1..980871a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,8 @@
-from setuptools import find_packages, setup
import ast
import re
+from setuptools import find_packages, setup
+
_version_re = re.compile(r"__version__\s+=\s+(.*)")
with open("graphene_django/__init__.py", "rb") as f:
@@ -15,6 +16,7 @@ rest_framework_require = ["djangorestframework>=3.6.3"]
tests_require = [
"pytest>=3.6.3",
"pytest-cov",
+ "pytest-random-order",
"coveralls",
"mock",
"pytz",
@@ -52,9 +54,9 @@ setup(
keywords="api graphql protocol rest relay graphene",
packages=find_packages(exclude=["tests"]),
install_requires=[
- "graphene>=2.1.7,<3",
- "graphql-core>=2.1.0,<3",
- "Django>=1.11,!=2.0.*,!=2.1.*",
+ "graphene>=3.0.0b1,<4",
+ "graphql-core>=3.1.0,<4",
+ "Django>=2.2",
"promise>=2.1",
],
setup_requires=["pytest-runner"],
diff --git a/tox.ini b/tox.ini
index 18bbd30..7e01ac9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,16 @@
[tox]
envlist =
- py{36,37,38}-django{111,22,30,master},
+ py{36,37,38}-django{22,30,master},
black,flake8
-[travis:env]
+[gh-actions]
+python =
+ 3.6: py36
+ 3.7: py37
+ 3.8: py38
+
+[gh-actions:env]
DJANGO =
- 1.11: django111
2.2: django22
3.0: django30
master: djangomaster
@@ -18,7 +23,6 @@ setenv =
deps =
-e.[test]
psycopg2-binary
- django111: Django>=1.11,<2.0
django22: Django>=2.2,<3.0
django30: Django>=3.0a1,<3.1
djangomaster: https://github.com/django/django/archive/master.zip
@@ -34,4 +38,4 @@ commands =
basepython = python3.8
deps = -e.[dev]
commands =
- flake8 graphene_django examples
+ flake8 graphene_django examples setup.py