Merge branch 'v3' into test-utils-rename-op-name

This commit is contained in:
Jonathan Kim 2020-05-09 12:18:28 +01:00 committed by GitHub
commit 270108d472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 823 additions and 662 deletions

25
.github/workflows/deploy.yml vendored Normal file
View File

@ -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 }}

22
.github/workflows/lint.yml vendored Normal file
View File

@ -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

28
.github/workflows/tests.yml vendored Normal file
View File

@ -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 }}

View File

@ -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"

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
```bash
pip install "graphene-django>=2.0"
pip install "graphene-django>=3"
```
### Settings

View File

@ -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
~~~~~~~~

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``:
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

View File

@ -2,8 +2,8 @@ Filtering
=========
Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results. See the `usage
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
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 <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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"]

View File

@ -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

View File

@ -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!
}
"""
)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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()}

View File

@ -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)

View File

@ -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():

View File

@ -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
}

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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,
}
]
}

View File

@ -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:

View File

@ -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):

View File

@ -1,2 +0,0 @@
[pytest]
DJANGO_SETTINGS_MODULE = django_test_settings

View File

@ -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

View File

@ -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"],

14
tox.ini
View File

@ -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