Skip to content

Commit ff99248

Browse files
committed
fix typed choices, make working with different Django 5x choices options
1 parent 2692250 commit ff99248

File tree

7 files changed

+208
-31
lines changed

7 files changed

+208
-31
lines changed

graphene_django/compat.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import sys
2+
from collections.abc import Callable
23
from pathlib import PurePath
34

45
# For backwards compatibility, we import JSONField to have it available for import via
56
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
67
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
7-
from django.db.models import JSONField
8+
import django
9+
from django.db.models import Choices, JSONField
810

911

1012
class MissingType:
@@ -42,3 +44,23 @@ def __init__(self, *args, **kwargs):
4244

4345
else:
4446
ArrayField = MissingType
47+
48+
49+
try:
50+
from django.utils.choices import normalize_choices
51+
except ImportError:
52+
53+
def normalize_choices(choices):
54+
if isinstance(choices, type) and issubclass(choices, Choices):
55+
choices = choices.choices
56+
57+
if isinstance(choices, Callable):
58+
choices = choices()
59+
60+
# In restframework==3.15.0, choices are not passed
61+
# as OrderedDict anymore, so it's safer to check
62+
# for a dict
63+
if isinstance(choices, dict):
64+
choices = choices.items()
65+
66+
return choices

graphene_django/converter.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import inspect
2-
from collections.abc import Callable
32
from functools import partial, singledispatch, wraps
43

54
from django.db import models
@@ -37,7 +36,7 @@
3736
from graphql import assert_valid_name as assert_name
3837
from graphql.pyutils import register_description
3938

40-
from .compat import ArrayField, HStoreField, RangeField
39+
from .compat import ArrayField, HStoreField, RangeField, normalize_choices
4140
from .fields import DjangoConnectionField, DjangoListField
4241
from .settings import graphene_settings
4342
from .utils.str_converters import to_const
@@ -61,6 +60,24 @@ def wrapped_resolver(*args, **kwargs):
6160
return blank_field_wrapper(resolver)
6261

6362

63+
class EnumValueField(BlankValueField):
64+
def wrap_resolve(self, parent_resolver):
65+
resolver = super().wrap_resolve(parent_resolver)
66+
67+
# create custom resolver
68+
def enum_field_wrapper(func):
69+
@wraps(func)
70+
def wrapped_resolver(*args, **kwargs):
71+
return_value = func(*args, **kwargs)
72+
if isinstance(return_value, models.Choices):
73+
return_value = return_value.value
74+
return return_value
75+
76+
return wrapped_resolver
77+
78+
return enum_field_wrapper(resolver)
79+
80+
6481
def convert_choice_name(name):
6582
name = to_const(force_str(name))
6683
try:
@@ -72,15 +89,7 @@ def convert_choice_name(name):
7289

7390
def get_choices(choices):
7491
converted_names = []
75-
if isinstance(choices, Callable):
76-
choices = choices()
77-
78-
# In restframework==3.15.0, choices are not passed
79-
# as OrderedDict anymore, so it's safer to check
80-
# for a dict
81-
if isinstance(choices, dict):
82-
choices = choices.items()
83-
92+
choices = normalize_choices(choices)
8493
for value, help_text in choices:
8594
if isinstance(help_text, (tuple, list)):
8695
yield from get_choices(help_text)
@@ -157,7 +166,7 @@ def convert_django_field_with_choices(
157166

158167
converted = EnumCls(
159168
description=get_django_field_description(field), required=required
160-
).mount_as(BlankValueField)
169+
).mount_as(EnumValueField)
161170
else:
162171
converted = convert_django_field(field, registry)
163172
if registry is not None:

graphene_django/forms/types.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from graphene.types.inputobjecttype import InputObjectType
44
from graphene.utils.str_converters import to_camel_case
55

6-
from ..converter import BlankValueField
6+
from ..converter import EnumValueField
77
from ..types import ErrorType # noqa Import ErrorType for backwards compatibility
88
from .mutation import fields_for_form
99

@@ -57,11 +57,10 @@ def mutate(_root, _info, data):
5757
if (
5858
object_type
5959
and name in object_type._meta.fields
60-
and isinstance(object_type._meta.fields[name], BlankValueField)
60+
and isinstance(object_type._meta.fields[name], EnumValueField)
6161
):
62-
# Field type BlankValueField here means that field
62+
# Field type EnumValueField here means that field
6363
# with choices have been converted to enum
64-
# (BlankValueField is using only for that task ?)
6564
setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type))
6665
elif (
6766
object_type

graphene_django/tests/models.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,38 @@
1+
import django
12
from django.db import models
23
from django.utils.translation import gettext_lazy as _
34

45
CHOICES = ((1, "this"), (2, _("that")))
56

67

8+
def get_choices_as_class(choices_class):
9+
if django.VERSION >= (5, 0):
10+
return choices_class
11+
else:
12+
return choices_class.choices
13+
14+
15+
def get_choices_as_callable(choices_class):
16+
if django.VERSION >= (5, 0):
17+
18+
def choices():
19+
return choices_class.choices
20+
21+
return choices
22+
else:
23+
return choices_class.choices
24+
25+
26+
class TypedIntChoice(models.IntegerChoices):
27+
CHOICE_THIS = 1
28+
CHOICE_THAT = 2
29+
30+
31+
class TypedStrChoice(models.TextChoices):
32+
CHOICE_THIS = "this"
33+
CHOICE_THAT = "that"
34+
35+
736
class Person(models.Model):
837
name = models.CharField(max_length=30)
938
parent = models.ForeignKey(
@@ -51,6 +80,21 @@ class Reporter(models.Model):
5180
email = models.EmailField()
5281
pets = models.ManyToManyField("self")
5382
a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True)
83+
typed_choice = models.IntegerField(
84+
choices=TypedIntChoice.choices,
85+
null=True,
86+
blank=True,
87+
)
88+
class_choice = models.IntegerField(
89+
choices=get_choices_as_class(TypedIntChoice),
90+
null=True,
91+
blank=True,
92+
)
93+
callable_choice = models.IntegerField(
94+
choices=get_choices_as_callable(TypedStrChoice),
95+
null=True,
96+
blank=True,
97+
)
5498
objects = models.Manager()
5599
doe_objects = DoeReporterManager()
56100
fans = models.ManyToManyField(Person)

graphene_django/tests/test_converter.py

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
)
2626
from ..registry import Registry
2727
from ..types import DjangoObjectType
28-
from .models import Article, Film, FilmDetails, Reporter
28+
from .models import Article, Film, FilmDetails, Reporter, TypedIntChoice, TypedStrChoice
2929

3030
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
3131

@@ -443,35 +443,102 @@ def test_choice_enum_blank_value():
443443
class ReporterType(DjangoObjectType):
444444
class Meta:
445445
model = Reporter
446-
fields = (
447-
"first_name",
448-
"a_choice",
449-
)
446+
fields = ("callable_choice",)
450447

451448
class Query(graphene.ObjectType):
452449
reporter = graphene.Field(ReporterType)
453450

454451
def resolve_reporter(root, info):
455-
return Reporter.objects.first()
452+
# return a model instance with blank choice field value
453+
return Reporter(callable_choice="")
456454

457455
schema = graphene.Schema(query=Query)
458456

459-
# Create model with empty choice option
460-
Reporter.objects.create(
461-
first_name="Bridget", last_name="Jones", email="bridget@example.com"
462-
)
463-
464457
result = schema.execute(
465458
"""
466459
query {
467460
reporter {
468-
firstName
469-
aChoice
461+
callableChoice
470462
}
471463
}
472464
"""
473465
)
474466
assert not result.errors
475467
assert result.data == {
476-
"reporter": {"firstName": "Bridget", "aChoice": None},
468+
"reporter": {"callableChoice": None},
469+
}
470+
471+
472+
def test_typed_choice_value():
473+
"""Test that typed choices fields are resolved correctly to the enum values"""
474+
475+
class ReporterType(DjangoObjectType):
476+
class Meta:
477+
model = Reporter
478+
fields = ("typed_choice", "class_choice", "callable_choice")
479+
480+
class Query(graphene.ObjectType):
481+
reporter = graphene.Field(ReporterType)
482+
483+
def resolve_reporter(root, info):
484+
# assign choice values to the fields instead of their str or int values
485+
return Reporter(
486+
typed_choice=TypedIntChoice.CHOICE_THIS,
487+
class_choice=TypedIntChoice.CHOICE_THAT,
488+
callable_choice=TypedStrChoice.CHOICE_THIS,
489+
)
490+
491+
class CreateReporter(graphene.Mutation):
492+
reporter = graphene.Field(ReporterType)
493+
494+
def mutate(root, info, **kwargs):
495+
return CreateReporter(
496+
reporter=Reporter(
497+
typed_choice=TypedIntChoice.CHOICE_THIS,
498+
class_choice=TypedIntChoice.CHOICE_THAT,
499+
callable_choice=TypedStrChoice.CHOICE_THIS,
500+
),
501+
)
502+
503+
class Mutation(graphene.ObjectType):
504+
create_reporter = CreateReporter.Field()
505+
506+
schema = graphene.Schema(query=Query, mutation=Mutation)
507+
508+
reporter_fragment = """
509+
fragment reporter on ReporterType {
510+
typedChoice
511+
classChoice
512+
callableChoice
513+
}
514+
"""
515+
516+
expected_reporter = {
517+
"typedChoice": "A_1",
518+
"classChoice": "A_2",
519+
"callableChoice": "THIS",
477520
}
521+
522+
result = schema.execute(
523+
reporter_fragment
524+
+ """
525+
query {
526+
reporter { ...reporter }
527+
}
528+
"""
529+
)
530+
assert not result.errors
531+
assert result.data["reporter"] == expected_reporter
532+
533+
result = schema.execute(
534+
reporter_fragment
535+
+ """
536+
mutation {
537+
createReporter {
538+
reporter { ...reporter }
539+
}
540+
}
541+
"""
542+
)
543+
assert not result.errors
544+
assert result.data["createReporter"]["reporter"] == expected_reporter

graphene_django/tests/test_schema.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class Meta:
4040
"email",
4141
"pets",
4242
"a_choice",
43+
"typed_choice",
44+
"class_choice",
45+
"callable_choice",
4346
"fans",
4447
"reporter_type",
4548
]

graphene_django/tests/test_types.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ def test_django_objecttype_map_correct_fields():
7777
"email",
7878
"pets",
7979
"a_choice",
80+
"typed_choice",
81+
"class_choice",
82+
"callable_choice",
8083
"fans",
8184
"reporter_type",
8285
]
@@ -186,6 +189,9 @@ def test_schema_representation():
186189
email: String!
187190
pets: [Reporter!]!
188191
aChoice: TestsReporterAChoiceChoices
192+
typedChoice: TestsReporterTypedChoiceChoices
193+
classChoice: TestsReporterClassChoiceChoices
194+
callableChoice: TestsReporterCallableChoiceChoices
189195
reporterType: TestsReporterReporterTypeChoices
190196
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
191197
}
@@ -199,6 +205,33 @@ def test_schema_representation():
199205
A_2
200206
}
201207
208+
\"""An enumeration.\"""
209+
enum TestsReporterTypedChoiceChoices {
210+
\"""Choice This\"""
211+
A_1
212+
213+
\"""Choice That\"""
214+
A_2
215+
}
216+
217+
\"""An enumeration.\"""
218+
enum TestsReporterClassChoiceChoices {
219+
\"""Choice This\"""
220+
A_1
221+
222+
\"""Choice That\"""
223+
A_2
224+
}
225+
226+
\"""An enumeration.\"""
227+
enum TestsReporterCallableChoiceChoices {
228+
\"""Choice This\"""
229+
THIS
230+
231+
\"""Choice That\"""
232+
THAT
233+
}
234+
202235
\"""An enumeration.\"""
203236
enum TestsReporterReporterTypeChoices {
204237
\"""Regular\"""

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy