Add options to override how Django Choice fields are converted t… (#860)

* Add new setting to create unique enum names

* Add specific tests for name generation

* Add schema test

* Rename settings field

* Rename setting

* Add custom function setting

* Add documentation

* Use format instead of f strings

* Update graphene_django/converter.py

Co-Authored-By: Syrus Akbary <me@syrusakbary.com>

* Fix tests

* Update docs

* Import function through import_string function

Co-authored-by: Syrus Akbary <me@syrusakbary.com>
This commit is contained in:
Jonathan Kim 2020-03-13 10:04:25 +00:00 committed by GitHub
parent 13352216a4
commit b8e598d66d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 4 deletions

View File

@ -140,3 +140,33 @@ Default: ``False``
# 'messages': ['This field is required.'], # 'messages': ['This field is required.'],
# } # }
# ] # ]
``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING``
--------------------------------------
Set to ``True`` to use the new naming format for the auto generated Enum types from Django choice fields. The new format looks like this: ``{app_label}{object_name}{field_name}Choices``
Default: ``False``
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
--------------------------------------
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING`` setting is ignored.
Default: ``None``
.. code:: python
# myapp.utils
def enum_naming(field):
if isinstance(field.model, User):
return f"CustomUserEnum{field.name.title()}"
return f"CustomEnum{field.name.title()}"
GRAPHENE = {
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
}

View File

@ -1,6 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from django.db import models from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.module_loading import import_string
from graphene import ( from graphene import (
ID, ID,
@ -22,6 +23,7 @@ from graphene.types.json import JSONString
from graphene.utils.str_converters import to_camel_case, to_const from graphene.utils.str_converters import to_camel_case, to_const
from graphql import assert_valid_name from graphql import assert_valid_name
from .settings import graphene_settings
from .compat import ArrayField, HStoreField, JSONField, RangeField from .compat import ArrayField, HStoreField, JSONField, RangeField
from .fields import DjangoListField, DjangoConnectionField from .fields import DjangoListField, DjangoConnectionField
from .utils import import_single_dispatch from .utils import import_single_dispatch
@ -68,6 +70,31 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
return Enum(name, list(named_choices), type=EnumWithDescriptionsType) return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
def generate_enum_name(django_model_meta, field):
if graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME:
# Try and import custom function
custom_func = import_string(
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME
)
name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING is True:
name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()),
object_name=django_model_meta.object_name,
field_name=to_camel_case(field.name.title()),
)
else:
name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
return name
def convert_choice_field_to_enum(field, name=None):
if name is None:
name = generate_enum_name(field.model._meta, field)
choices = field.choices
return convert_choices_to_named_enum_with_descriptions(name, choices)
def convert_django_field_with_choices( def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=True field, registry=None, convert_choices_to_enum=True
): ):
@ -77,9 +104,7 @@ def convert_django_field_with_choices(
return converted return converted
choices = getattr(field, "choices", None) choices = getattr(field, "choices", None)
if choices and convert_choices_to_enum: if choices and convert_choices_to_enum:
meta = field.model._meta enum = convert_choice_field_to_enum(field)
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
required = not (field.blank or field.null) required = not (field.blank or field.null)
converted = enum(description=field.help_text, required=required) converted = enum(description=field.help_text, required=required)
else: else:

View File

@ -36,6 +36,9 @@ DEFAULTS = {
# Max items returned in ConnectionFields / FilterConnectionFields # Max items returned in ConnectionFields / FilterConnectionFields
"RELAY_CONNECTION_MAX_LIMIT": 100, "RELAY_CONNECTION_MAX_LIMIT": 100,
"CAMELCASE_ERRORS": False, "CAMELCASE_ERRORS": False,
# Set to True to enable v3 naming convention for choice field Enum's
"DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
} }
if settings.DEBUG: if settings.DEBUG:

View File

@ -1,4 +1,5 @@
import pytest import pytest
from collections import namedtuple
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from graphene import NonNull from graphene import NonNull
@ -10,9 +11,14 @@ from graphene.types.datetime import DateTime, Date, Time
from graphene.types.json import JSONString from graphene.types.json import JSONString
from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
from ..converter import convert_django_field, convert_django_field_with_choices from ..converter import (
convert_django_field,
convert_django_field_with_choices,
generate_enum_name,
)
from ..registry import Registry from ..registry import Registry
from ..types import DjangoObjectType from ..types import DjangoObjectType
from ..settings import graphene_settings
from .models import Article, Film, FilmDetails, Reporter from .models import Article, Film, FilmDetails, Reporter
@ -325,3 +331,25 @@ def test_should_postgres_range_convert_list():
assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type, graphene.NonNull)
assert isinstance(field.type.of_type, graphene.List) assert isinstance(field.type.of_type, graphene.List)
assert field.type.of_type.of_type == graphene.Int assert field.type.of_type.of_type == graphene.Int
def test_generate_enum_name():
MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"])
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
# Simple case
field = graphene.Field(graphene.String, name="type")
model_meta = MockDjangoModelMeta(app_label="users", object_name="User")
assert generate_enum_name(model_meta, field) == "UsersUserTypeChoices"
# More complicated multiple work case
field = graphene.Field(graphene.String, name="fizz_buzz")
model_meta = MockDjangoModelMeta(
app_label="some_long_app_name", object_name="SomeObject"
)
assert (
generate_enum_name(model_meta, field)
== "SomeLongAppNameSomeObjectFizzBuzzChoices"
)
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False

View File

@ -9,7 +9,9 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String
from graphene.relay import Node from graphene.relay import Node
from .. import registry from .. import registry
from ..settings import graphene_settings
from ..types import DjangoObjectType, DjangoObjectTypeOptions from ..types import DjangoObjectType, DjangoObjectTypeOptions
from ..converter import convert_choice_field_to_enum
from .models import Article as ArticleModel from .models import Article as ArticleModel
from .models import Reporter as ReporterModel from .models import Reporter as ReporterModel
@ -386,6 +388,10 @@ def test_django_objecttype_exclude_fields_exist_on_model():
assert len(record) == 0 assert len(record) == 0
def custom_enum_name(field):
return "CustomEnum{}".format(field.name.title())
class TestDjangoObjectType: class TestDjangoObjectType:
@pytest.fixture @pytest.fixture
def PetModel(self): def PetModel(self):
@ -492,3 +498,78 @@ class TestDjangoObjectType:
} }
""" """
) )
def test_django_objecttype_convert_choices_enum_naming_collisions(self, PetModel):
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
class PetModelKind(DjangoObjectType):
class Meta:
model = PetModel
fields = ["id", "kind"]
class Query(ObjectType):
pet = Field(PetModelKind)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type PetModelKind {
id: ID!
kind: TestsPetModelKindChoices!
}
type Query {
pet: PetModelKind
}
enum TestsPetModelKindChoices {
CAT
DOG
}
"""
)
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
def test_django_objecttype_choices_custom_enum_name(self, PetModel):
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = (
"graphene_django.tests.test_types.custom_enum_name"
)
class PetModelKind(DjangoObjectType):
class Meta:
model = PetModel
fields = ["id", "kind"]
class Query(ObjectType):
pet = Field(PetModelKind)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
enum CustomEnumKind {
CAT
DOG
}
type PetModelKind {
id: ID!
kind: CustomEnumKind!
}
type Query {
pet: PetModelKind
}
"""
)
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = None