Compare commits

..

No commits in common. "main" and "v3.2.0" have entirely different histories.
main ... v3.2.0

28 changed files with 153 additions and 491 deletions

View File

@ -12,7 +12,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
django: ["3.2", "4.2", "5.0", "5.1"]
django: ["3.2", "4.2", "5.0"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
exclude:
- django: "3.2"
@ -23,10 +23,6 @@ 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 }}

View File

@ -1,18 +0,0 @@
# .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

View File

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

View File

@ -33,7 +33,7 @@ make tests
## Opening Pull Requests
Please fork the project and open a pull request against the `main` branch.
Please fork the project and open a pull request against the master branch.
This will trigger a series of test and lint checks.

View File

@ -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/main/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/master/CONTRIBUTING.md).
## License
Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/main/LICENSE).
Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/master/LICENSE).
## Resources

View File

@ -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/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following:

View File

@ -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/main/examples/cookbook>`__.
app <https://github.com/graphql-python/graphene-django/tree/master/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/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following:

View File

@ -1,5 +1,5 @@
graphene>=2.1,<3
graphene-django>=2.1,<3
graphql-core>=2.1,<3
django==3.2.25
django==3.1.14
django-filter>=2

View File

@ -28,5 +28,3 @@ TEMPLATES = [
GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"}
ROOT_URLCONF = "graphene_django.tests.urls"
USE_TZ = True

View File

@ -1,5 +1,5 @@
import graphene
from graphene import Schema, relay
from graphene import Schema, relay, resolve_only_args
from graphene_django import DjangoConnectionField, DjangoObjectType
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
@ -62,13 +62,16 @@ class Query(graphene.ObjectType):
node = relay.Node.Field()
ships = DjangoConnectionField(Ship, description="All the ships.")
def resolve_ships(self, info):
@resolve_only_args
def resolve_ships(self):
return get_ships()
def resolve_rebels(self, info):
@resolve_only_args
def resolve_rebels(self):
return get_rebels()
def resolve_empire(self, info):
@resolve_only_args
def resolve_empire(self):
return get_empire()

View File

@ -2,7 +2,7 @@ from .fields import DjangoConnectionField, DjangoListField
from .types import DjangoObjectType
from .utils import bypass_get_queryset
__version__ = "3.2.3"
__version__ = "3.2.0"
__all__ = [
"__version__",

View File

@ -1,11 +1,10 @@
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 Choices, JSONField
from django.db.models import JSONField
class MissingType:
@ -43,23 +42,3 @@ 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

View File

@ -1,4 +1,5 @@
import inspect
from collections import OrderedDict
from functools import partial, singledispatch, wraps
from django.db import models
@ -36,7 +37,7 @@ except ImportError:
from graphql import assert_valid_name as assert_name
from graphql.pyutils import register_description
from .compat import ArrayField, HStoreField, RangeField, normalize_choices
from .compat import ArrayField, HStoreField, RangeField
from .fields import DjangoConnectionField, DjangoListField
from .settings import graphene_settings
from .utils.str_converters import to_const
@ -60,24 +61,6 @@ 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:
@ -89,7 +72,8 @@ def convert_choice_name(name):
def get_choices(choices):
converted_names = []
choices = normalize_choices(choices)
if isinstance(choices, OrderedDict):
choices = choices.items()
for value, help_text in choices:
if isinstance(help_text, (tuple, list)):
yield from get_choices(help_text)
@ -166,7 +150,7 @@ def convert_django_field_with_choices(
converted = EnumCls(
description=get_django_field_description(field), required=required
).mount_as(EnumValueField)
).mount_as(BlankValueField)
else:
converted = convert_django_field(field, registry)
if registry is not None:
@ -199,13 +183,19 @@ def convert_field_to_string(field, registry=None):
)
@convert_django_field.register(models.AutoField)
@convert_django_field.register(models.BigAutoField)
@convert_django_field.register(models.SmallAutoField)
@convert_django_field.register(models.AutoField)
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(

View File

@ -20,20 +20,17 @@ 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 as underlying type"
return super().type
), "DjangoListField only accepts DjangoObjectType types"
@property
def _underlying_type(self):
@ -247,7 +244,7 @@ class DjangoConnectionField(ConnectionField):
def wrap_resolve(self, parent_resolver):
return partial(
self.connection_resolver,
self.resolver or parent_resolver,
parent_resolver,
self.connection_type,
self.get_manager(),
self.get_queryset_resolver(),

View File

@ -1,4 +1,4 @@
from django import VERSION as DJANGO_VERSION, forms
from django import forms
from pytest import raises
from graphene import (
@ -19,16 +19,12 @@ from graphene import (
from ..converter import convert_form_field
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
def assert_conversion(django_field, graphene_field, *args):
field = django_field(*args, help_text="Custom Help Text")
graphene_type = convert_form_field(field)
# Assert
assert isinstance(graphene_type, graphene_field)
field = graphene_type.Field()
assert field.description == help_text
assert field.description == "Custom Help Text"
return field
@ -63,12 +59,7 @@ def test_should_slug_convert_string():
def test_should_url_convert_string():
kwargs = {}
if DJANGO_VERSION >= (5, 0):
# silence RemovedInDjango60Warning
kwargs["assume_scheme"] = "https"
assert_conversion(forms.URLField, String, **kwargs)
assert_conversion(forms.URLField, String)
def test_should_choice_convert_string():
@ -84,7 +75,8 @@ def test_should_regex_convert_string():
def test_should_uuid_convert_string():
assert_conversion(forms.UUIDField, UUID)
if hasattr(forms, "UUIDField"):
assert_conversion(forms.UUIDField, UUID)
def test_should_integer_convert_int():

View File

@ -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 EnumValueField
from ..converter import BlankValueField
from ..types import ErrorType # noqa Import ErrorType for backwards compatibility
from .mutation import fields_for_form
@ -57,10 +57,11 @@ class DjangoFormInputObjectType(InputObjectType):
if (
object_type
and name in object_type._meta.fields
and isinstance(object_type._meta.fields[name], EnumValueField)
and isinstance(object_type._meta.fields[name], BlankValueField)
):
# Field type EnumValueField here means that field
# Field type BlankValueField 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

View File

@ -96,7 +96,8 @@ def test_should_regex_convert_string():
def test_should_uuid_convert_string():
assert_conversion(serializers.UUIDField, graphene.String)
if hasattr(serializers, "UUIDField"):
assert_conversion(serializers.UUIDField, graphene.String)
def test_should_model_convert_field():

View File

@ -1,43 +1,11 @@
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):
@ -80,21 +48,6 @@ 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)

View File

@ -25,7 +25,7 @@ from ..converter import (
)
from ..registry import Registry
from ..types import DjangoObjectType
from .models import Article, Film, FilmDetails, Reporter, TypedIntChoice, TypedStrChoice
from .models import Article, Film, FilmDetails, Reporter
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
@ -53,8 +53,9 @@ def assert_conversion(django_field, graphene_field, *args, **kwargs):
def test_should_unknown_django_field_raise_exception():
with raises(Exception, match="Don't know how to convert the Django field"):
with raises(Exception) as excinfo:
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():
@ -114,7 +115,8 @@ def test_should_big_auto_convert_id():
def test_should_small_auto_convert_id():
assert_conversion(models.SmallAutoField, graphene.ID, primary_key=True)
if hasattr(models, "SmallAutoField"):
assert_conversion(models.SmallAutoField, graphene.ID, primary_key=True)
def test_should_uuid_convert_id():
@ -164,34 +166,14 @@ def test_field_with_choices_convert_enum():
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
)
class ChoicesModel(models.Model):
class TranslatedModel(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 == "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.name == "TestTranslatedModelLanguageChoices"
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"
@ -441,102 +423,35 @@ def test_choice_enum_blank_value():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
fields = ("callable_choice",)
fields = (
"first_name",
"a_choice",
)
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
def resolve_reporter(root, info):
# return a model instance with blank choice field value
return Reporter(callable_choice="")
return Reporter.objects.first()
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 {
callableChoice
firstName
aChoice
}
}
"""
)
assert not result.errors
assert result.data == {
"reporter": {"callableChoice": None},
"reporter": {"firstName": "Bridget", "aChoice": 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

View File

@ -12,23 +12,17 @@ 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 Query(ObjectType):
something = DjangoListField(String)
class TestType(ObjectType):
foo = String()
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)
)
with pytest.raises(AssertionError):
DjangoListField(TestType)
def test_only_import_paths(self):
list_field = DjangoListField("graphene_django.tests.schema.Human")
@ -268,69 +262,6 @@ 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:

View File

@ -26,7 +26,6 @@ class TestShouldCallGetQuerySetOnForeignKey:
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
fields = "__all__"
@classmethod
def get_queryset(cls, queryset, info):
@ -37,7 +36,6 @@ class TestShouldCallGetQuerySetOnForeignKey:
class ArticleType(DjangoObjectType):
class Meta:
model = Article
fields = "__all__"
@classmethod
def get_queryset(cls, queryset, info):
@ -202,7 +200,6 @@ class TestShouldCallGetQuerySetOnForeignKeyNode:
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
fields = "__all__"
interfaces = (Node,)
@classmethod
@ -214,7 +211,6 @@ class TestShouldCallGetQuerySetOnForeignKeyNode:
class ArticleType(DjangoObjectType):
class Meta:
model = Article
fields = "__all__"
interfaces = (Node,)
@classmethod
@ -374,7 +370,6 @@ class TestShouldCallGetQuerySetOnOneToOne:
class FilmDetailsType(DjangoObjectType):
class Meta:
model = FilmDetails
fields = "__all__"
@classmethod
def get_queryset(cls, queryset, info):
@ -385,7 +380,6 @@ class TestShouldCallGetQuerySetOnOneToOne:
class FilmType(DjangoObjectType):
class Meta:
model = Film
fields = "__all__"
@classmethod
def get_queryset(cls, queryset, info):

View File

@ -1,6 +1,5 @@
import base64
import datetime
from unittest.mock import ANY, Mock
import pytest
from django.db import models
@ -2001,62 +2000,14 @@ 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))
@ -2071,8 +2022,10 @@ 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!) {
@ -2115,7 +2068,6 @@ 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):
@ -2124,7 +2076,6 @@ 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):

View File

@ -40,9 +40,6 @@ def test_should_map_fields_correctly():
"email",
"pets",
"a_choice",
"typed_choice",
"class_choice",
"callable_choice",
"fans",
"reporter_type",
]

View File

@ -1,4 +1,3 @@
import warnings
from collections import OrderedDict, defaultdict
from textwrap import dedent
from unittest.mock import patch
@ -77,9 +76,6 @@ def test_django_objecttype_map_correct_fields():
"email",
"pets",
"a_choice",
"typed_choice",
"class_choice",
"callable_choice",
"fans",
"reporter_type",
]
@ -189,9 +185,6 @@ 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!
}
@ -205,33 +198,6 @@ 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\"""
@ -433,7 +399,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:
@ -441,8 +407,7 @@ def test_django_objecttype_fields_exist_on_model():
fields = ["first_name", "some_method", "email"]
# Don't warn if selecting a custom field
with warnings.catch_warnings():
warnings.simplefilter("error")
with pytest.warns(None) as record:
class Reporter3(DjangoObjectType):
custom_field = String()
@ -451,6 +416,8 @@ 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():
@ -478,14 +445,15 @@ def test_django_objecttype_exclude_fields_exist_on_model():
exclude = ["custom_field"]
# Don't warn on exclude fields
with warnings.catch_warnings():
warnings.simplefilter("error")
with pytest.warns(None) as record:
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():
@ -499,22 +467,24 @@ def test_django_objecttype_neither_fields_nor_exclude():
class Meta:
model = ReporterModel
with warnings.catch_warnings():
warnings.simplefilter("error")
with pytest.warns(None) as record:
class Reporter2(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ["email"]
with warnings.catch_warnings():
warnings.simplefilter("error")
assert len(record) == 0
with pytest.warns(None) as record:
class Reporter3(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]
assert len(record) == 0
def custom_enum_name(field):
return f"CustomEnum{field.name.title()}"

View File

@ -1,5 +1,4 @@
import json
from http import HTTPStatus
from unittest.mock import patch
import pytest
@ -38,7 +37,7 @@ def jl(**kwargs):
def test_graphiql_is_enabled(client):
response = client.get(url_string(), HTTP_ACCEPT="text/html")
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response["Content-Type"].split(";")[0] == "text/html"
@ -47,7 +46,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 == HTTPStatus.OK
assert response.status_code == 200
assert response["Content-Type"].split(";")[0] == "text/html"
@ -56,7 +55,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 == HTTPStatus.OK
assert response.status_code == 200
assert response["Content-Type"].split(";")[0] == "application/json"
assert response_json(response) == {"data": {"test": "Hello World"}}
@ -64,7 +63,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 == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello World"}}
@ -76,7 +75,7 @@ def test_allows_get_with_variable_values(client):
)
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
@ -95,7 +94,7 @@ def test_allows_get_with_operation_name(client):
)
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {
"data": {"test": "Hello World", "shared": "Hello Everyone"}
}
@ -104,7 +103,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [
{
@ -129,7 +128,7 @@ def test_errors_when_missing_operation_name(client):
)
)
assert response.status_code == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [
{
@ -147,7 +146,7 @@ def test_errors_when_sending_a_mutation_via_get(client):
"""
)
)
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
assert response.status_code == 405
assert response_json(response) == {
"errors": [
{"message": "Can only perform a mutation operation from a POST request."}
@ -166,7 +165,7 @@ def test_errors_when_selecting_a_mutation_within_a_get(client):
)
)
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
assert response.status_code == 405
assert response_json(response) == {
"errors": [
{"message": "Can only perform a mutation operation from a POST request."}
@ -185,14 +184,14 @@ def test_allows_mutation_to_exist_within_a_get(client):
)
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
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 == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello World"}}
@ -201,7 +200,7 @@ def test_batch_allows_post_with_json_encoding(client):
batch_url_string(), jl(id=1, query="{test}"), "application/json"
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == [
{"id": 1, "data": {"test": "Hello World"}, "status": 200}
]
@ -210,7 +209,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [{"message": "Received an empty list in the batch request."}]
}
@ -223,7 +222,7 @@ def test_allows_sending_a_mutation_via_post(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}}
@ -234,7 +233,7 @@ def test_allows_post_with_url_encoding(client):
"application/x-www-form-urlencoded",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello World"}}
@ -248,7 +247,7 @@ def test_supports_post_json_query_with_string_variables(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
@ -263,7 +262,7 @@ def test_batch_supports_post_json_query_with_string_variables(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == [
{"id": 1, "data": {"test": "Hello Dolly"}, "status": 200}
]
@ -279,7 +278,7 @@ def test_supports_post_json_query_with_json_variables(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
@ -294,7 +293,7 @@ def test_batch_supports_post_json_query_with_json_variables(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == [
{"id": 1, "data": {"test": "Hello Dolly"}, "status": 200}
]
@ -312,7 +311,7 @@ def test_supports_post_url_encoded_query_with_string_variables(client):
"application/x-www-form-urlencoded",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
@ -323,7 +322,7 @@ def test_supports_post_json_quey_with_get_variable_values(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
@ -334,7 +333,7 @@ def test_post_url_encoded_query_with_get_variable_values(client):
"application/x-www-form-urlencoded",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
@ -345,7 +344,7 @@ def test_supports_post_raw_text_query_with_get_variable_values(client):
"application/graphql",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
@ -366,7 +365,7 @@ def test_allows_post_with_operation_name(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {
"data": {"test": "Hello World", "shared": "Hello Everyone"}
}
@ -390,7 +389,7 @@ def test_batch_allows_post_with_operation_name(client):
"application/json",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == [
{
"id": 1,
@ -414,7 +413,7 @@ def test_allows_post_with_get_operation_name(client):
"application/graphql",
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {
"data": {"test": "Hello World", "shared": "Hello Everyone"}
}
@ -431,7 +430,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 == HTTPStatus.OK
assert response.status_code == 200
@pytest.mark.urls("graphene_django.tests.urls_pretty")
@ -453,7 +452,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 == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {
"data": None,
"errors": [
@ -468,7 +467,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [
{
@ -482,7 +481,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [{"message": "Must provide query string."}]
}
@ -491,7 +490,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [{"message": "The received data is not a valid JSON query."}]
}
@ -500,7 +499,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [{"message": "POST body sent invalid JSON."}]
}
@ -515,14 +514,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [{"message": "POST body sent invalid JSON."}]
}
@ -534,7 +533,7 @@ def test_handles_plain_post_text(client):
"query helloWho($who: String){ test(who: $who) }",
"text/plain",
)
assert response.status_code == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [{"message": "Must provide query string."}]
}
@ -546,7 +545,7 @@ def test_handles_poorly_formed_variables(client):
query="query helloWho($who: String){ test(who: $who) }", variables="who:You"
)
)
assert response.status_code == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
assert response_json(response) == {
"errors": [{"message": "Variables are invalid JSON."}]
}
@ -554,7 +553,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 == HTTPStatus.METHOD_NOT_ALLOWED
assert response.status_code == 405
assert response["Allow"] == "GET, POST"
assert response_json(response) == {
"errors": [{"message": "GraphQL only supports GET and POST requests."}]
@ -564,7 +563,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 == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {"data": {"request": "testing"}}
@ -858,7 +857,7 @@ def test_allow_introspection(client):
response = client.post(
url_string("/graphql/", query="{__schema {queryType {name}}}")
)
assert response.status_code == HTTPStatus.OK
assert response.status_code == 200
assert response_json(response) == {
"data": {"__schema": {"queryType": {"name": "QueryRoot"}}}
@ -870,7 +869,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
json_response = response_json(response)
assert "data" not in json_response
@ -889,7 +888,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
json_response = response_json(response)
assert "data" not in json_response
@ -914,7 +913,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 == HTTPStatus.BAD_REQUEST
assert response.status_code == 400
json_response = response_json(response)
assert "data" not in json_response

View File

@ -111,7 +111,24 @@ def is_valid_django_model(model):
def import_single_dispatch():
from functools import singledispatch
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"
)
return singledispatch

View File

@ -10,7 +10,3 @@ 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

View File

@ -2,7 +2,8 @@
envlist =
py{38,39,310}-django32
py{38,39}-django42
py{310,311,312}-django{42,50,51,main}
py{310,311}-django{42,50,main}
py312-django{42,50,main}
pre-commit
[gh-actions]
@ -18,7 +19,6 @@ DJANGO =
3.2: django32
4.2: django42
5.0: django50
5.1: django51
main: djangomain
[testenv]
@ -33,7 +33,6 @@ 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}