mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-04-15 14:42:06 +03:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c52cf2b045 | ||
|
e69e4a0399 | ||
|
97deb761e9 | ||
|
8d4a64a40d | ||
|
269225085d | ||
|
28c71c58f7 | ||
|
6f21dc7a94 | ||
|
ea45de02ad | ||
|
eac113e136 | ||
|
d69c90550f | ||
|
3f813d4679 | ||
|
45c2aa09b5 | ||
|
ac09cd2967 | ||
|
54372b41d5 | ||
|
96c09ac439 | ||
|
b85177cebf |
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
django: ["3.2", "4.2", "5.0"]
|
||||
django: ["3.2", "4.2", "5.0", "5.1"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
exclude:
|
||||
- django: "3.2"
|
||||
|
@ -23,6 +23,10 @@ jobs:
|
|||
python-version: "3.8"
|
||||
- django: "5.0"
|
||||
python-version: "3.9"
|
||||
- django: "5.1"
|
||||
python-version: "3.8"
|
||||
- django: "5.1"
|
||||
python-version: "3.9"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
|
18
.readthedocs.yaml
Normal file
18
.readthedocs.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
|
||||
# Build documentation in the "docs/" directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
|
@ -25,7 +25,6 @@ target-version = "py38"
|
|||
[per-file-ignores]
|
||||
# Ignore unused imports (F401) in these files
|
||||
"__init__.py" = ["F401"]
|
||||
"graphene_django/compat.py" = ["F401"]
|
||||
|
||||
[isort]
|
||||
known-first-party = ["graphene", "graphene-django"]
|
||||
|
|
|
@ -33,7 +33,7 @@ make tests
|
|||
|
||||
## Opening Pull Requests
|
||||
|
||||
Please fork the project and open a pull request against the master branch.
|
||||
Please fork the project and open a pull request against the `main` branch.
|
||||
|
||||
This will trigger a series of test and lint checks.
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ Graphene-Django is an open-source library that provides seamless integration bet
|
|||
|
||||
To install Graphene-Django, run the following command:
|
||||
|
||||
```
|
||||
```sh
|
||||
pip install graphene-django
|
||||
```
|
||||
|
||||
|
@ -114,11 +114,11 @@ class MyModelAPITestCase(GraphQLTestCase):
|
|||
|
||||
## Contributing
|
||||
|
||||
Contributions to Graphene-Django are always welcome! To get started, check the repository's [issue tracker](https://github.com/graphql-python/graphene-django/issues) and [contribution guidelines](https://github.com/graphql-python/graphene-django/blob/master/CONTRIBUTING.md).
|
||||
Contributions to Graphene-Django are always welcome! To get started, check the repository's [issue tracker](https://github.com/graphql-python/graphene-django/issues) and [contribution guidelines](https://github.com/graphql-python/graphene-django/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/master/LICENSE).
|
||||
Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/main/LICENSE).
|
||||
|
||||
## Resources
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ Load some test data
|
|||
|
||||
Now is a good time to load up some test data. The easiest option will be
|
||||
to `download the
|
||||
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
|
||||
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
|
||||
fixture and place it in
|
||||
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
|
||||
following:
|
||||
|
|
|
@ -7,7 +7,7 @@ Graphene has a number of additional features that are designed to make
|
|||
working with Django *really simple*.
|
||||
|
||||
Note: The code in this quickstart is pulled from the `cookbook example
|
||||
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
|
||||
app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__.
|
||||
|
||||
A good idea is to check the following things first:
|
||||
|
||||
|
@ -87,7 +87,7 @@ Load some test data
|
|||
|
||||
Now is a good time to load up some test data. The easiest option will be
|
||||
to `download the
|
||||
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
|
||||
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
|
||||
fixture and place it in
|
||||
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
|
||||
following:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
graphene>=2.1,<3
|
||||
graphene-django>=2.1,<3
|
||||
graphql-core>=2.1,<3
|
||||
django==3.1.14
|
||||
django==3.2.25
|
||||
django-filter>=2
|
||||
|
|
|
@ -28,3 +28,5 @@ TEMPLATES = [
|
|||
GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"}
|
||||
|
||||
ROOT_URLCONF = "graphene_django.tests.urls"
|
||||
|
||||
USE_TZ = True
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import graphene
|
||||
from graphene import Schema, relay, resolve_only_args
|
||||
from graphene import Schema, relay
|
||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||
|
||||
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
|
||||
|
@ -62,16 +62,13 @@ class Query(graphene.ObjectType):
|
|||
node = relay.Node.Field()
|
||||
ships = DjangoConnectionField(Ship, description="All the ships.")
|
||||
|
||||
@resolve_only_args
|
||||
def resolve_ships(self):
|
||||
def resolve_ships(self, info):
|
||||
return get_ships()
|
||||
|
||||
@resolve_only_args
|
||||
def resolve_rebels(self):
|
||||
def resolve_rebels(self, info):
|
||||
return get_rebels()
|
||||
|
||||
@resolve_only_args
|
||||
def resolve_empire(self):
|
||||
def resolve_empire(self, info):
|
||||
return get_empire()
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from .fields import DjangoConnectionField, DjangoListField
|
|||
from .types import DjangoObjectType
|
||||
from .utils import bypass_get_queryset
|
||||
|
||||
__version__ = "3.2.0"
|
||||
__version__ = "3.2.3"
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import sys
|
||||
from collections.abc import Callable
|
||||
from pathlib import PurePath
|
||||
|
||||
# For backwards compatibility, we import JSONField to have it available for import via
|
||||
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
|
||||
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
|
||||
from django.db.models import JSONField
|
||||
from django.db.models import Choices, JSONField
|
||||
|
||||
|
||||
class MissingType:
|
||||
|
@ -42,3 +43,23 @@ except ImportError:
|
|||
|
||||
else:
|
||||
ArrayField = MissingType
|
||||
|
||||
|
||||
try:
|
||||
from django.utils.choices import normalize_choices
|
||||
except ImportError:
|
||||
|
||||
def normalize_choices(choices):
|
||||
if isinstance(choices, type) and issubclass(choices, Choices):
|
||||
choices = choices.choices
|
||||
|
||||
if isinstance(choices, Callable):
|
||||
choices = choices()
|
||||
|
||||
# In restframework==3.15.0, choices are not passed
|
||||
# as OrderedDict anymore, so it's safer to check
|
||||
# for a dict
|
||||
if isinstance(choices, dict):
|
||||
choices = choices.items()
|
||||
|
||||
return choices
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import inspect
|
||||
from collections import OrderedDict
|
||||
from functools import partial, singledispatch, wraps
|
||||
|
||||
from django.db import models
|
||||
|
@ -37,7 +36,7 @@ except ImportError:
|
|||
from graphql import assert_valid_name as assert_name
|
||||
from graphql.pyutils import register_description
|
||||
|
||||
from .compat import ArrayField, HStoreField, RangeField
|
||||
from .compat import ArrayField, HStoreField, RangeField, normalize_choices
|
||||
from .fields import DjangoConnectionField, DjangoListField
|
||||
from .settings import graphene_settings
|
||||
from .utils.str_converters import to_const
|
||||
|
@ -61,6 +60,24 @@ class BlankValueField(Field):
|
|||
return blank_field_wrapper(resolver)
|
||||
|
||||
|
||||
class EnumValueField(BlankValueField):
|
||||
def wrap_resolve(self, parent_resolver):
|
||||
resolver = super().wrap_resolve(parent_resolver)
|
||||
|
||||
# create custom resolver
|
||||
def enum_field_wrapper(func):
|
||||
@wraps(func)
|
||||
def wrapped_resolver(*args, **kwargs):
|
||||
return_value = func(*args, **kwargs)
|
||||
if isinstance(return_value, models.Choices):
|
||||
return_value = return_value.value
|
||||
return return_value
|
||||
|
||||
return wrapped_resolver
|
||||
|
||||
return enum_field_wrapper(resolver)
|
||||
|
||||
|
||||
def convert_choice_name(name):
|
||||
name = to_const(force_str(name))
|
||||
try:
|
||||
|
@ -72,8 +89,7 @@ def convert_choice_name(name):
|
|||
|
||||
def get_choices(choices):
|
||||
converted_names = []
|
||||
if isinstance(choices, OrderedDict):
|
||||
choices = choices.items()
|
||||
choices = normalize_choices(choices)
|
||||
for value, help_text in choices:
|
||||
if isinstance(help_text, (tuple, list)):
|
||||
yield from get_choices(help_text)
|
||||
|
@ -150,7 +166,7 @@ def convert_django_field_with_choices(
|
|||
|
||||
converted = EnumCls(
|
||||
description=get_django_field_description(field), required=required
|
||||
).mount_as(BlankValueField)
|
||||
).mount_as(EnumValueField)
|
||||
else:
|
||||
converted = convert_django_field(field, registry)
|
||||
if registry is not None:
|
||||
|
@ -183,19 +199,13 @@ def convert_field_to_string(field, registry=None):
|
|||
)
|
||||
|
||||
|
||||
@convert_django_field.register(models.BigAutoField)
|
||||
@convert_django_field.register(models.AutoField)
|
||||
@convert_django_field.register(models.BigAutoField)
|
||||
@convert_django_field.register(models.SmallAutoField)
|
||||
def convert_field_to_id(field, registry=None):
|
||||
return ID(description=get_django_field_description(field), required=not field.null)
|
||||
|
||||
|
||||
if hasattr(models, "SmallAutoField"):
|
||||
|
||||
@convert_django_field.register(models.SmallAutoField)
|
||||
def convert_field_small_to_id(field, registry=None):
|
||||
return convert_field_to_id(field, registry)
|
||||
|
||||
|
||||
@convert_django_field.register(models.UUIDField)
|
||||
def convert_field_to_uuid(field, registry=None):
|
||||
return UUID(
|
||||
|
|
|
@ -20,17 +20,20 @@ from .utils import maybe_queryset
|
|||
|
||||
class DjangoListField(Field):
|
||||
def __init__(self, _type, *args, **kwargs):
|
||||
from .types import DjangoObjectType
|
||||
|
||||
if isinstance(_type, NonNull):
|
||||
_type = _type.of_type
|
||||
|
||||
# Django would never return a Set of None vvvvvvv
|
||||
super().__init__(List(NonNull(_type)), *args, **kwargs)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
from .types import DjangoObjectType
|
||||
|
||||
assert issubclass(
|
||||
self._underlying_type, DjangoObjectType
|
||||
), "DjangoListField only accepts DjangoObjectType types"
|
||||
), "DjangoListField only accepts DjangoObjectType types as underlying type"
|
||||
return super().type
|
||||
|
||||
@property
|
||||
def _underlying_type(self):
|
||||
|
@ -244,7 +247,7 @@ class DjangoConnectionField(ConnectionField):
|
|||
def wrap_resolve(self, parent_resolver):
|
||||
return partial(
|
||||
self.connection_resolver,
|
||||
parent_resolver,
|
||||
self.resolver or parent_resolver,
|
||||
self.connection_type,
|
||||
self.get_manager(),
|
||||
self.get_queryset_resolver(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django import forms
|
||||
from django import VERSION as DJANGO_VERSION, forms
|
||||
from pytest import raises
|
||||
|
||||
from graphene import (
|
||||
|
@ -19,12 +19,16 @@ from graphene import (
|
|||
from ..converter import convert_form_field
|
||||
|
||||
|
||||
def assert_conversion(django_field, graphene_field, *args):
|
||||
field = django_field(*args, help_text="Custom Help Text")
|
||||
def assert_conversion(django_field, graphene_field, *args, **kwargs):
|
||||
# Arrange
|
||||
help_text = kwargs.setdefault("help_text", "Custom Help Text")
|
||||
field = django_field(*args, **kwargs)
|
||||
# Act
|
||||
graphene_type = convert_form_field(field)
|
||||
# Assert
|
||||
assert isinstance(graphene_type, graphene_field)
|
||||
field = graphene_type.Field()
|
||||
assert field.description == "Custom Help Text"
|
||||
assert field.description == help_text
|
||||
return field
|
||||
|
||||
|
||||
|
@ -59,7 +63,12 @@ def test_should_slug_convert_string():
|
|||
|
||||
|
||||
def test_should_url_convert_string():
|
||||
assert_conversion(forms.URLField, String)
|
||||
kwargs = {}
|
||||
if DJANGO_VERSION >= (5, 0):
|
||||
# silence RemovedInDjango60Warning
|
||||
kwargs["assume_scheme"] = "https"
|
||||
|
||||
assert_conversion(forms.URLField, String, **kwargs)
|
||||
|
||||
|
||||
def test_should_choice_convert_string():
|
||||
|
@ -75,8 +84,7 @@ def test_should_regex_convert_string():
|
|||
|
||||
|
||||
def test_should_uuid_convert_string():
|
||||
if hasattr(forms, "UUIDField"):
|
||||
assert_conversion(forms.UUIDField, UUID)
|
||||
assert_conversion(forms.UUIDField, UUID)
|
||||
|
||||
|
||||
def test_should_integer_convert_int():
|
||||
|
|
|
@ -3,7 +3,7 @@ from graphene import ID
|
|||
from graphene.types.inputobjecttype import InputObjectType
|
||||
from graphene.utils.str_converters import to_camel_case
|
||||
|
||||
from ..converter import BlankValueField
|
||||
from ..converter import EnumValueField
|
||||
from ..types import ErrorType # noqa Import ErrorType for backwards compatibility
|
||||
from .mutation import fields_for_form
|
||||
|
||||
|
@ -57,11 +57,10 @@ class DjangoFormInputObjectType(InputObjectType):
|
|||
if (
|
||||
object_type
|
||||
and name in object_type._meta.fields
|
||||
and isinstance(object_type._meta.fields[name], BlankValueField)
|
||||
and isinstance(object_type._meta.fields[name], EnumValueField)
|
||||
):
|
||||
# Field type BlankValueField here means that field
|
||||
# Field type EnumValueField here means that field
|
||||
# with choices have been converted to enum
|
||||
# (BlankValueField is using only for that task ?)
|
||||
setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type))
|
||||
elif (
|
||||
object_type
|
||||
|
|
|
@ -96,8 +96,7 @@ def test_should_regex_convert_string():
|
|||
|
||||
|
||||
def test_should_uuid_convert_string():
|
||||
if hasattr(serializers, "UUIDField"):
|
||||
assert_conversion(serializers.UUIDField, graphene.String)
|
||||
assert_conversion(serializers.UUIDField, graphene.String)
|
||||
|
||||
|
||||
def test_should_model_convert_field():
|
||||
|
|
|
@ -1,11 +1,43 @@
|
|||
import django
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
CHOICES = ((1, "this"), (2, _("that")))
|
||||
|
||||
|
||||
def get_choices_as_class(choices_class):
|
||||
if django.VERSION >= (5, 0):
|
||||
return choices_class
|
||||
else:
|
||||
return choices_class.choices
|
||||
|
||||
|
||||
def get_choices_as_callable(choices_class):
|
||||
if django.VERSION >= (5, 0):
|
||||
|
||||
def choices():
|
||||
return choices_class.choices
|
||||
|
||||
return choices
|
||||
else:
|
||||
return choices_class.choices
|
||||
|
||||
|
||||
class TypedIntChoice(models.IntegerChoices):
|
||||
CHOICE_THIS = 1
|
||||
CHOICE_THAT = 2
|
||||
|
||||
|
||||
class TypedStrChoice(models.TextChoices):
|
||||
CHOICE_THIS = "this"
|
||||
CHOICE_THAT = "that"
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
name = models.CharField(max_length=30)
|
||||
parent = models.ForeignKey(
|
||||
"self", on_delete=models.CASCADE, null=True, blank=True, related_name="children"
|
||||
)
|
||||
|
||||
|
||||
class Pet(models.Model):
|
||||
|
@ -48,6 +80,21 @@ class Reporter(models.Model):
|
|||
email = models.EmailField()
|
||||
pets = models.ManyToManyField("self")
|
||||
a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True)
|
||||
typed_choice = models.IntegerField(
|
||||
choices=TypedIntChoice.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
class_choice = models.IntegerField(
|
||||
choices=get_choices_as_class(TypedIntChoice),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
callable_choice = models.IntegerField(
|
||||
choices=get_choices_as_callable(TypedStrChoice),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
objects = models.Manager()
|
||||
doe_objects = DoeReporterManager()
|
||||
fans = models.ManyToManyField(Person)
|
||||
|
|
|
@ -25,7 +25,7 @@ from ..converter import (
|
|||
)
|
||||
from ..registry import Registry
|
||||
from ..types import DjangoObjectType
|
||||
from .models import Article, Film, FilmDetails, Reporter
|
||||
from .models import Article, Film, FilmDetails, Reporter, TypedIntChoice, TypedStrChoice
|
||||
|
||||
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
|
||||
|
||||
|
@ -53,9 +53,8 @@ def assert_conversion(django_field, graphene_field, *args, **kwargs):
|
|||
|
||||
|
||||
def test_should_unknown_django_field_raise_exception():
|
||||
with raises(Exception) as excinfo:
|
||||
with raises(Exception, match="Don't know how to convert the Django field"):
|
||||
convert_django_field(None)
|
||||
assert "Don't know how to convert the Django field" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_should_date_time_convert_string():
|
||||
|
@ -115,8 +114,7 @@ def test_should_big_auto_convert_id():
|
|||
|
||||
|
||||
def test_should_small_auto_convert_id():
|
||||
if hasattr(models, "SmallAutoField"):
|
||||
assert_conversion(models.SmallAutoField, graphene.ID, primary_key=True)
|
||||
assert_conversion(models.SmallAutoField, graphene.ID, primary_key=True)
|
||||
|
||||
|
||||
def test_should_uuid_convert_id():
|
||||
|
@ -166,14 +164,34 @@ def test_field_with_choices_convert_enum():
|
|||
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
|
||||
)
|
||||
|
||||
class TranslatedModel(models.Model):
|
||||
class ChoicesModel(models.Model):
|
||||
language = field
|
||||
|
||||
class Meta:
|
||||
app_label = "test"
|
||||
|
||||
graphene_type = convert_django_field_with_choices(field).type.of_type
|
||||
assert graphene_type._meta.name == "TestTranslatedModelLanguageChoices"
|
||||
assert graphene_type._meta.name == "TestChoicesModelLanguageChoices"
|
||||
assert graphene_type._meta.enum.__members__["ES"].value == "es"
|
||||
assert graphene_type._meta.enum.__members__["ES"].description == "Spanish"
|
||||
assert graphene_type._meta.enum.__members__["EN"].value == "en"
|
||||
assert graphene_type._meta.enum.__members__["EN"].description == "English"
|
||||
|
||||
|
||||
def test_field_with_callable_choices_convert_enum():
|
||||
def get_choices():
|
||||
return ("es", "Spanish"), ("en", "English")
|
||||
|
||||
field = models.CharField(help_text="Language", choices=get_choices)
|
||||
|
||||
class CallableChoicesModel(models.Model):
|
||||
language = field
|
||||
|
||||
class Meta:
|
||||
app_label = "test"
|
||||
|
||||
graphene_type = convert_django_field_with_choices(field).type.of_type
|
||||
assert graphene_type._meta.name == "TestCallableChoicesModelLanguageChoices"
|
||||
assert graphene_type._meta.enum.__members__["ES"].value == "es"
|
||||
assert graphene_type._meta.enum.__members__["ES"].description == "Spanish"
|
||||
assert graphene_type._meta.enum.__members__["EN"].value == "en"
|
||||
|
@ -423,35 +441,102 @@ def test_choice_enum_blank_value():
|
|||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = (
|
||||
"first_name",
|
||||
"a_choice",
|
||||
)
|
||||
fields = ("callable_choice",)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
reporter = graphene.Field(ReporterType)
|
||||
|
||||
def resolve_reporter(root, info):
|
||||
return Reporter.objects.first()
|
||||
# return a model instance with blank choice field value
|
||||
return Reporter(callable_choice="")
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
||||
# Create model with empty choice option
|
||||
Reporter.objects.create(
|
||||
first_name="Bridget", last_name="Jones", email="bridget@example.com"
|
||||
)
|
||||
|
||||
result = schema.execute(
|
||||
"""
|
||||
query {
|
||||
reporter {
|
||||
firstName
|
||||
aChoice
|
||||
callableChoice
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
assert not result.errors
|
||||
assert result.data == {
|
||||
"reporter": {"firstName": "Bridget", "aChoice": None},
|
||||
"reporter": {"callableChoice": None},
|
||||
}
|
||||
|
||||
|
||||
def test_typed_choice_value():
|
||||
"""Test that typed choices fields are resolved correctly to the enum values"""
|
||||
|
||||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = ("typed_choice", "class_choice", "callable_choice")
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
reporter = graphene.Field(ReporterType)
|
||||
|
||||
def resolve_reporter(root, info):
|
||||
# assign choice values to the fields instead of their str or int values
|
||||
return Reporter(
|
||||
typed_choice=TypedIntChoice.CHOICE_THIS,
|
||||
class_choice=TypedIntChoice.CHOICE_THAT,
|
||||
callable_choice=TypedStrChoice.CHOICE_THIS,
|
||||
)
|
||||
|
||||
class CreateReporter(graphene.Mutation):
|
||||
reporter = graphene.Field(ReporterType)
|
||||
|
||||
def mutate(root, info, **kwargs):
|
||||
return CreateReporter(
|
||||
reporter=Reporter(
|
||||
typed_choice=TypedIntChoice.CHOICE_THIS,
|
||||
class_choice=TypedIntChoice.CHOICE_THAT,
|
||||
callable_choice=TypedStrChoice.CHOICE_THIS,
|
||||
),
|
||||
)
|
||||
|
||||
class Mutation(graphene.ObjectType):
|
||||
create_reporter = CreateReporter.Field()
|
||||
|
||||
schema = graphene.Schema(query=Query, mutation=Mutation)
|
||||
|
||||
reporter_fragment = """
|
||||
fragment reporter on ReporterType {
|
||||
typedChoice
|
||||
classChoice
|
||||
callableChoice
|
||||
}
|
||||
"""
|
||||
|
||||
expected_reporter = {
|
||||
"typedChoice": "A_1",
|
||||
"classChoice": "A_2",
|
||||
"callableChoice": "THIS",
|
||||
}
|
||||
|
||||
result = schema.execute(
|
||||
reporter_fragment
|
||||
+ """
|
||||
query {
|
||||
reporter { ...reporter }
|
||||
}
|
||||
"""
|
||||
)
|
||||
assert not result.errors
|
||||
assert result.data["reporter"] == expected_reporter
|
||||
|
||||
result = schema.execute(
|
||||
reporter_fragment
|
||||
+ """
|
||||
mutation {
|
||||
createReporter {
|
||||
reporter { ...reporter }
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
assert not result.errors
|
||||
assert result.data["createReporter"]["reporter"] == expected_reporter
|
||||
|
|
|
@ -12,17 +12,23 @@ from .models import (
|
|||
Article as ArticleModel,
|
||||
Film as FilmModel,
|
||||
FilmDetails as FilmDetailsModel,
|
||||
Person as PersonModel,
|
||||
Reporter as ReporterModel,
|
||||
)
|
||||
|
||||
|
||||
class TestDjangoListField:
|
||||
def test_only_django_object_types(self):
|
||||
class TestType(ObjectType):
|
||||
foo = String()
|
||||
class Query(ObjectType):
|
||||
something = DjangoListField(String)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
DjangoListField(TestType)
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
Schema(query=Query)
|
||||
|
||||
assert (
|
||||
"Query fields cannot be resolved. DjangoListField only accepts DjangoObjectType types as underlying type"
|
||||
in str(excinfo.value)
|
||||
)
|
||||
|
||||
def test_only_import_paths(self):
|
||||
list_field = DjangoListField("graphene_django.tests.schema.Human")
|
||||
|
@ -262,6 +268,69 @@ class TestDjangoListField:
|
|||
]
|
||||
}
|
||||
|
||||
def test_same_type_nested_list_field(self):
|
||||
class Person(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PersonModel
|
||||
fields = ("name", "parent")
|
||||
|
||||
children = DjangoListField(lambda: Person)
|
||||
|
||||
class Query(ObjectType):
|
||||
persons = DjangoListField(Person)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
query = """
|
||||
query {
|
||||
persons {
|
||||
name
|
||||
children {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
p1 = PersonModel.objects.create(name="Tara")
|
||||
PersonModel.objects.create(name="Debra")
|
||||
|
||||
PersonModel.objects.create(
|
||||
name="Toto",
|
||||
parent=p1,
|
||||
)
|
||||
PersonModel.objects.create(
|
||||
name="Tata",
|
||||
parent=p1,
|
||||
)
|
||||
|
||||
result = schema.execute(query)
|
||||
|
||||
assert not result.errors
|
||||
assert result.data == {
|
||||
"persons": [
|
||||
{
|
||||
"name": "Tara",
|
||||
"children": [
|
||||
{"name": "Toto"},
|
||||
{"name": "Tata"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Debra",
|
||||
"children": [],
|
||||
},
|
||||
{
|
||||
"name": "Toto",
|
||||
"children": [],
|
||||
},
|
||||
{
|
||||
"name": "Tata",
|
||||
"children": [],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
def test_get_queryset_filter(self):
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
|
|
|
@ -26,6 +26,7 @@ class TestShouldCallGetQuerySetOnForeignKey:
|
|||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = "__all__"
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
|
@ -36,6 +37,7 @@ class TestShouldCallGetQuerySetOnForeignKey:
|
|||
class ArticleType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = "__all__"
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
|
@ -200,6 +202,7 @@ class TestShouldCallGetQuerySetOnForeignKeyNode:
|
|||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = "__all__"
|
||||
interfaces = (Node,)
|
||||
|
||||
@classmethod
|
||||
|
@ -211,6 +214,7 @@ class TestShouldCallGetQuerySetOnForeignKeyNode:
|
|||
class ArticleType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = "__all__"
|
||||
interfaces = (Node,)
|
||||
|
||||
@classmethod
|
||||
|
@ -370,6 +374,7 @@ class TestShouldCallGetQuerySetOnOneToOne:
|
|||
class FilmDetailsType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = FilmDetails
|
||||
fields = "__all__"
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
|
@ -380,6 +385,7 @@ class TestShouldCallGetQuerySetOnOneToOne:
|
|||
class FilmType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Film
|
||||
fields = "__all__"
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import base64
|
||||
import datetime
|
||||
from unittest.mock import ANY, Mock
|
||||
|
||||
import pytest
|
||||
from django.db import models
|
||||
|
@ -2000,14 +2001,62 @@ def test_connection_should_succeed_if_last_higher_than_number_of_objects():
|
|||
assert result.data == expected
|
||||
|
||||
|
||||
def test_connection_should_call_resolver_function():
|
||||
resolver_mock = Mock(
|
||||
name="resolver",
|
||||
return_value=[
|
||||
Reporter(first_name="Some", last_name="One"),
|
||||
Reporter(first_name="John", last_name="Doe"),
|
||||
],
|
||||
)
|
||||
|
||||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = "__all__"
|
||||
interfaces = [Node]
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
reporters = DjangoConnectionField(ReporterType, resolver=resolver_mock)
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
result = schema.execute(
|
||||
"""
|
||||
query {
|
||||
reporters {
|
||||
edges {
|
||||
node {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
resolver_mock.assert_called_once_with(None, ANY)
|
||||
assert not result.errors
|
||||
assert result.data == {
|
||||
"reporters": {
|
||||
"edges": [
|
||||
{"node": {"firstName": "Some", "lastName": "One"}},
|
||||
{"node": {"firstName": "John", "lastName": "Doe"}},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_should_query_nullable_foreign_key():
|
||||
class PetType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Pet
|
||||
fields = "__all__"
|
||||
|
||||
class PersonType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = "__all__"
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
pet = graphene.Field(PetType, name=graphene.String(required=True))
|
||||
|
@ -2022,10 +2071,8 @@ def test_should_query_nullable_foreign_key():
|
|||
schema = graphene.Schema(query=Query)
|
||||
|
||||
person = Person.objects.create(name="Jane")
|
||||
[
|
||||
Pet.objects.create(name="Stray dog", age=1),
|
||||
Pet.objects.create(name="Jane's dog", owner=person, age=1),
|
||||
]
|
||||
Pet.objects.create(name="Stray dog", age=1)
|
||||
Pet.objects.create(name="Jane's dog", owner=person, age=1)
|
||||
|
||||
query_pet = """
|
||||
query getPet($name: String!) {
|
||||
|
@ -2068,6 +2115,7 @@ def test_should_query_nullable_one_to_one_relation_with_custom_resolver():
|
|||
class FilmType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Film
|
||||
fields = "__all__"
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
|
@ -2076,6 +2124,7 @@ def test_should_query_nullable_one_to_one_relation_with_custom_resolver():
|
|||
class FilmDetailsType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = FilmDetails
|
||||
fields = "__all__"
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
|
|
|
@ -40,6 +40,9 @@ def test_should_map_fields_correctly():
|
|||
"email",
|
||||
"pets",
|
||||
"a_choice",
|
||||
"typed_choice",
|
||||
"class_choice",
|
||||
"callable_choice",
|
||||
"fans",
|
||||
"reporter_type",
|
||||
]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import warnings
|
||||
from collections import OrderedDict, defaultdict
|
||||
from textwrap import dedent
|
||||
from unittest.mock import patch
|
||||
|
@ -76,6 +77,9 @@ def test_django_objecttype_map_correct_fields():
|
|||
"email",
|
||||
"pets",
|
||||
"a_choice",
|
||||
"typed_choice",
|
||||
"class_choice",
|
||||
"callable_choice",
|
||||
"fans",
|
||||
"reporter_type",
|
||||
]
|
||||
|
@ -185,6 +189,9 @@ def test_schema_representation():
|
|||
email: String!
|
||||
pets: [Reporter!]!
|
||||
aChoice: TestsReporterAChoiceChoices
|
||||
typedChoice: TestsReporterTypedChoiceChoices
|
||||
classChoice: TestsReporterClassChoiceChoices
|
||||
callableChoice: TestsReporterCallableChoiceChoices
|
||||
reporterType: TestsReporterReporterTypeChoices
|
||||
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
|
||||
}
|
||||
|
@ -198,6 +205,33 @@ def test_schema_representation():
|
|||
A_2
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum TestsReporterTypedChoiceChoices {
|
||||
\"""Choice This\"""
|
||||
A_1
|
||||
|
||||
\"""Choice That\"""
|
||||
A_2
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum TestsReporterClassChoiceChoices {
|
||||
\"""Choice This\"""
|
||||
A_1
|
||||
|
||||
\"""Choice That\"""
|
||||
A_2
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum TestsReporterCallableChoiceChoices {
|
||||
\"""Choice This\"""
|
||||
THIS
|
||||
|
||||
\"""Choice That\"""
|
||||
THAT
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum TestsReporterReporterTypeChoices {
|
||||
\"""Regular\"""
|
||||
|
@ -399,7 +433,7 @@ def test_django_objecttype_fields_exist_on_model():
|
|||
with pytest.warns(
|
||||
UserWarning,
|
||||
match=r"Field name .* matches an attribute on Django model .* but it's not a model field",
|
||||
) as record:
|
||||
):
|
||||
|
||||
class Reporter2(DjangoObjectType):
|
||||
class Meta:
|
||||
|
@ -407,7 +441,8 @@ def test_django_objecttype_fields_exist_on_model():
|
|||
fields = ["first_name", "some_method", "email"]
|
||||
|
||||
# Don't warn if selecting a custom field
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
|
||||
class Reporter3(DjangoObjectType):
|
||||
custom_field = String()
|
||||
|
@ -416,8 +451,6 @@ def test_django_objecttype_fields_exist_on_model():
|
|||
model = ReporterModel
|
||||
fields = ["first_name", "custom_field", "email"]
|
||||
|
||||
assert len(record) == 0
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_fields_exist_on_model():
|
||||
|
@ -445,15 +478,14 @@ def test_django_objecttype_exclude_fields_exist_on_model():
|
|||
exclude = ["custom_field"]
|
||||
|
||||
# Don't warn on exclude fields
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
|
||||
class Reporter4(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email", "first_name"]
|
||||
|
||||
assert len(record) == 0
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_neither_fields_nor_exclude():
|
||||
|
@ -467,24 +499,22 @@ def test_django_objecttype_neither_fields_nor_exclude():
|
|||
class Meta:
|
||||
model = ReporterModel
|
||||
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
|
||||
class Reporter2(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = ["email"]
|
||||
|
||||
assert len(record) == 0
|
||||
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
|
||||
class Reporter3(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email"]
|
||||
|
||||
assert len(record) == 0
|
||||
|
||||
|
||||
def custom_enum_name(field):
|
||||
return f"CustomEnum{field.name.title()}"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
@ -37,7 +38,7 @@ def jl(**kwargs):
|
|||
|
||||
def test_graphiql_is_enabled(client):
|
||||
response = client.get(url_string(), HTTP_ACCEPT="text/html")
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response["Content-Type"].split(";")[0] == "text/html"
|
||||
|
||||
|
||||
|
@ -46,7 +47,7 @@ def test_qfactor_graphiql(client):
|
|||
url_string(query="{test}"),
|
||||
HTTP_ACCEPT="application/json;q=0.8, text/html;q=0.9",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response["Content-Type"].split(";")[0] == "text/html"
|
||||
|
||||
|
||||
|
@ -55,7 +56,7 @@ def test_qfactor_json(client):
|
|||
url_string(query="{test}"),
|
||||
HTTP_ACCEPT="text/html;q=0.8, application/json;q=0.9",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response["Content-Type"].split(";")[0] == "application/json"
|
||||
assert response_json(response) == {"data": {"test": "Hello World"}}
|
||||
|
||||
|
@ -63,7 +64,7 @@ def test_qfactor_json(client):
|
|||
def test_allows_get_with_query_param(client):
|
||||
response = client.get(url_string(query="{test}"))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello World"}}
|
||||
|
||||
|
||||
|
@ -75,7 +76,7 @@ def test_allows_get_with_variable_values(client):
|
|||
)
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
|
||||
|
||||
|
||||
|
@ -94,7 +95,7 @@ def test_allows_get_with_operation_name(client):
|
|||
)
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {
|
||||
"data": {"test": "Hello World", "shared": "Hello Everyone"}
|
||||
}
|
||||
|
@ -103,7 +104,7 @@ def test_allows_get_with_operation_name(client):
|
|||
def test_reports_validation_errors(client):
|
||||
response = client.get(url_string(query="{ test, unknownOne, unknownTwo }"))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [
|
||||
{
|
||||
|
@ -128,7 +129,7 @@ def test_errors_when_missing_operation_name(client):
|
|||
)
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [
|
||||
{
|
||||
|
@ -146,7 +147,7 @@ def test_errors_when_sending_a_mutation_via_get(client):
|
|||
"""
|
||||
)
|
||||
)
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
|
||||
assert response_json(response) == {
|
||||
"errors": [
|
||||
{"message": "Can only perform a mutation operation from a POST request."}
|
||||
|
@ -165,7 +166,7 @@ def test_errors_when_selecting_a_mutation_within_a_get(client):
|
|||
)
|
||||
)
|
||||
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
|
||||
assert response_json(response) == {
|
||||
"errors": [
|
||||
{"message": "Can only perform a mutation operation from a POST request."}
|
||||
|
@ -184,14 +185,14 @@ def test_allows_mutation_to_exist_within_a_get(client):
|
|||
)
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello World"}}
|
||||
|
||||
|
||||
def test_allows_post_with_json_encoding(client):
|
||||
response = client.post(url_string(), j(query="{test}"), "application/json")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello World"}}
|
||||
|
||||
|
||||
|
@ -200,7 +201,7 @@ def test_batch_allows_post_with_json_encoding(client):
|
|||
batch_url_string(), jl(id=1, query="{test}"), "application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == [
|
||||
{"id": 1, "data": {"test": "Hello World"}, "status": 200}
|
||||
]
|
||||
|
@ -209,7 +210,7 @@ def test_batch_allows_post_with_json_encoding(client):
|
|||
def test_batch_fails_if_is_empty(client):
|
||||
response = client.post(batch_url_string(), "[]", "application/json")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "Received an empty list in the batch request."}]
|
||||
}
|
||||
|
@ -222,7 +223,7 @@ def test_allows_sending_a_mutation_via_post(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}}
|
||||
|
||||
|
||||
|
@ -233,7 +234,7 @@ def test_allows_post_with_url_encoding(client):
|
|||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello World"}}
|
||||
|
||||
|
||||
|
@ -247,7 +248,7 @@ def test_supports_post_json_query_with_string_variables(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
|
||||
|
||||
|
||||
|
@ -262,7 +263,7 @@ def test_batch_supports_post_json_query_with_string_variables(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == [
|
||||
{"id": 1, "data": {"test": "Hello Dolly"}, "status": 200}
|
||||
]
|
||||
|
@ -278,7 +279,7 @@ def test_supports_post_json_query_with_json_variables(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
|
||||
|
||||
|
||||
|
@ -293,7 +294,7 @@ def test_batch_supports_post_json_query_with_json_variables(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == [
|
||||
{"id": 1, "data": {"test": "Hello Dolly"}, "status": 200}
|
||||
]
|
||||
|
@ -311,7 +312,7 @@ def test_supports_post_url_encoded_query_with_string_variables(client):
|
|||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
|
||||
|
||||
|
||||
|
@ -322,7 +323,7 @@ def test_supports_post_json_quey_with_get_variable_values(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
|
||||
|
||||
|
||||
|
@ -333,7 +334,7 @@ def test_post_url_encoded_query_with_get_variable_values(client):
|
|||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
|
||||
|
||||
|
||||
|
@ -344,7 +345,7 @@ def test_supports_post_raw_text_query_with_get_variable_values(client):
|
|||
"application/graphql",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
|
||||
|
||||
|
||||
|
@ -365,7 +366,7 @@ def test_allows_post_with_operation_name(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {
|
||||
"data": {"test": "Hello World", "shared": "Hello Everyone"}
|
||||
}
|
||||
|
@ -389,7 +390,7 @@ def test_batch_allows_post_with_operation_name(client):
|
|||
"application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == [
|
||||
{
|
||||
"id": 1,
|
||||
|
@ -413,7 +414,7 @@ def test_allows_post_with_get_operation_name(client):
|
|||
"application/graphql",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {
|
||||
"data": {"test": "Hello World", "shared": "Hello Everyone"}
|
||||
}
|
||||
|
@ -430,7 +431,7 @@ def test_inherited_class_with_attributes_works(client):
|
|||
|
||||
# Check graphiql works
|
||||
response = client.get(url_string(inherited_url), HTTP_ACCEPT="text/html")
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
|
||||
@pytest.mark.urls("graphene_django.tests.urls_pretty")
|
||||
|
@ -452,7 +453,7 @@ def test_supports_pretty_printing_by_request(client):
|
|||
|
||||
def test_handles_field_errors_caught_by_graphql(client):
|
||||
response = client.get(url_string(query="{thrower}"))
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {
|
||||
"data": None,
|
||||
"errors": [
|
||||
|
@ -467,7 +468,7 @@ def test_handles_field_errors_caught_by_graphql(client):
|
|||
|
||||
def test_handles_syntax_errors_caught_by_graphql(client):
|
||||
response = client.get(url_string(query="syntaxerror"))
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [
|
||||
{
|
||||
|
@ -481,7 +482,7 @@ def test_handles_syntax_errors_caught_by_graphql(client):
|
|||
def test_handles_errors_caused_by_a_lack_of_query(client):
|
||||
response = client.get(url_string())
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "Must provide query string."}]
|
||||
}
|
||||
|
@ -490,7 +491,7 @@ def test_handles_errors_caused_by_a_lack_of_query(client):
|
|||
def test_handles_not_expected_json_bodies(client):
|
||||
response = client.post(url_string(), "[]", "application/json")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "The received data is not a valid JSON query."}]
|
||||
}
|
||||
|
@ -499,7 +500,7 @@ def test_handles_not_expected_json_bodies(client):
|
|||
def test_handles_invalid_json_bodies(client):
|
||||
response = client.post(url_string(), "[oh}", "application/json")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "POST body sent invalid JSON."}]
|
||||
}
|
||||
|
@ -514,14 +515,14 @@ def test_handles_django_request_error(client, monkeypatch):
|
|||
valid_json = json.dumps({"foo": "bar"})
|
||||
response = client.post(url_string(), valid_json, "application/json")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {"errors": [{"message": "foo-bar"}]}
|
||||
|
||||
|
||||
def test_handles_incomplete_json_bodies(client):
|
||||
response = client.post(url_string(), '{"query":', "application/json")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "POST body sent invalid JSON."}]
|
||||
}
|
||||
|
@ -533,7 +534,7 @@ def test_handles_plain_post_text(client):
|
|||
"query helloWho($who: String){ test(who: $who) }",
|
||||
"text/plain",
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "Must provide query string."}]
|
||||
}
|
||||
|
@ -545,7 +546,7 @@ def test_handles_poorly_formed_variables(client):
|
|||
query="query helloWho($who: String){ test(who: $who) }", variables="who:You"
|
||||
)
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "Variables are invalid JSON."}]
|
||||
}
|
||||
|
@ -553,7 +554,7 @@ def test_handles_poorly_formed_variables(client):
|
|||
|
||||
def test_handles_unsupported_http_methods(client):
|
||||
response = client.put(url_string(query="{test}"))
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
|
||||
assert response["Allow"] == "GET, POST"
|
||||
assert response_json(response) == {
|
||||
"errors": [{"message": "GraphQL only supports GET and POST requests."}]
|
||||
|
@ -563,7 +564,7 @@ def test_handles_unsupported_http_methods(client):
|
|||
def test_passes_request_into_context_request(client):
|
||||
response = client.get(url_string(query="{request}", q="testing"))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response_json(response) == {"data": {"request": "testing"}}
|
||||
|
||||
|
||||
|
@ -857,7 +858,7 @@ def test_allow_introspection(client):
|
|||
response = client.post(
|
||||
url_string("/graphql/", query="{__schema {queryType {name}}}")
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
assert response_json(response) == {
|
||||
"data": {"__schema": {"queryType": {"name": "QueryRoot"}}}
|
||||
|
@ -869,7 +870,7 @@ def test_allow_introspection(client):
|
|||
def test_validation_disallow_introspection(client, url):
|
||||
response = client.post(url_string(url, query="{__schema {queryType {name}}}"))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
json_response = response_json(response)
|
||||
assert "data" not in json_response
|
||||
|
@ -888,7 +889,7 @@ def test_validation_disallow_introspection(client, url):
|
|||
def test_within_max_validation_errors(client, url):
|
||||
response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
json_response = response_json(response)
|
||||
assert "data" not in json_response
|
||||
|
@ -913,7 +914,7 @@ def test_within_max_validation_errors(client, url):
|
|||
def test_exceeds_max_validation_errors(client, url):
|
||||
response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
json_response = response_json(response)
|
||||
assert "data" not in json_response
|
||||
|
|
|
@ -111,24 +111,7 @@ def is_valid_django_model(model):
|
|||
|
||||
|
||||
def import_single_dispatch():
|
||||
try:
|
||||
from functools import singledispatch
|
||||
except ImportError:
|
||||
singledispatch = None
|
||||
|
||||
if not singledispatch:
|
||||
try:
|
||||
from singledispatch import singledispatch
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not singledispatch:
|
||||
raise Exception(
|
||||
"It seems your python version does not include "
|
||||
"functools.singledispatch. Please install the 'singledispatch' "
|
||||
"package. More information here: "
|
||||
"https://pypi.python.org/pypi/singledispatch"
|
||||
)
|
||||
from functools import singledispatch
|
||||
|
||||
return singledispatch
|
||||
|
||||
|
|
|
@ -10,3 +10,7 @@ omit = */tests/*
|
|||
[tool:pytest]
|
||||
DJANGO_SETTINGS_MODULE = examples.django_test_settings
|
||||
addopts = --random-order
|
||||
filterwarnings =
|
||||
error
|
||||
# we can't do anything about the DeprecationWarning about typing.ByteString in graphql
|
||||
default:'typing\.ByteString' is deprecated:DeprecationWarning:graphql\.pyutils\.is_iterable
|
||||
|
|
5
tox.ini
5
tox.ini
|
@ -2,8 +2,7 @@
|
|||
envlist =
|
||||
py{38,39,310}-django32
|
||||
py{38,39}-django42
|
||||
py{310,311}-django{42,50,main}
|
||||
py312-django{42,50,main}
|
||||
py{310,311,312}-django{42,50,51,main}
|
||||
pre-commit
|
||||
|
||||
[gh-actions]
|
||||
|
@ -19,6 +18,7 @@ DJANGO =
|
|||
3.2: django32
|
||||
4.2: django42
|
||||
5.0: django50
|
||||
5.1: django51
|
||||
main: djangomain
|
||||
|
||||
[testenv]
|
||||
|
@ -33,6 +33,7 @@ deps =
|
|||
django32: Django>=3.2,<4.0
|
||||
django42: Django>=4.2,<4.3
|
||||
django50: Django>=5.0,<5.1
|
||||
django51: Django>=5.1,<5.2
|
||||
djangomain: https://github.com/django/django/archive/main.zip
|
||||
commands = {posargs:pytest --cov=graphene_django graphene_django examples}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user