Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to override how Django Choice fields are converted to Enums #860

Merged
merged 12 commits into from
Mar 13, 2020
30 changes: 30 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,33 @@ Default: ``False``
# '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"
}
31 changes: 28 additions & 3 deletions graphene_django/converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import OrderedDict
from django.db import models
from django.utils.encoding import force_str
from django.utils.module_loading import import_string

from graphene import (
ID,
Expand All @@ -22,6 +23,7 @@
from graphene.utils.str_converters import to_camel_case, to_const
from graphql import assert_valid_name

from .settings import graphene_settings
from .compat import ArrayField, HStoreField, JSONField, RangeField
from .fields import DjangoListField, DjangoConnectionField
from .utils import import_single_dispatch
Expand Down Expand Up @@ -68,6 +70,31 @@ def description(self):
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(
field, registry=None, convert_choices_to_enum=True
):
Expand All @@ -77,9 +104,7 @@ def convert_django_field_with_choices(
return converted
choices = getattr(field, "choices", None)
if choices and convert_choices_to_enum:
meta = field.model._meta
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
enum = convert_choice_field_to_enum(field)
required = not (field.blank or field.null)
converted = enum(description=field.help_text, required=required)
else:
Expand Down
3 changes: 3 additions & 0 deletions graphene_django/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
# Max items returned in ConnectionFields / FilterConnectionFields
"RELAY_CONNECTION_MAX_LIMIT": 100,
"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:
Expand Down
30 changes: 29 additions & 1 deletion graphene_django/tests/test_converter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from collections import namedtuple
from django.db import models
from django.utils.translation import ugettext_lazy as _
from graphene import NonNull
Expand All @@ -10,9 +11,14 @@
from graphene.types.json import JSONString

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 ..types import DjangoObjectType
from ..settings import graphene_settings
from .models import Article, Film, FilmDetails, Reporter


Expand Down Expand Up @@ -325,3 +331,25 @@ def test_should_postgres_range_convert_list():
assert isinstance(field.type, graphene.NonNull)
assert isinstance(field.type.of_type, graphene.List)
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
81 changes: 81 additions & 0 deletions graphene_django/tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from graphene.relay import Node

from .. import registry
from ..settings import graphene_settings
from ..types import DjangoObjectType, DjangoObjectTypeOptions
from ..converter import convert_choice_field_to_enum
from .models import Article as ArticleModel
from .models import Reporter as ReporterModel

Expand Down Expand Up @@ -386,6 +388,10 @@ class Meta:
assert len(record) == 0


def custom_enum_name(field):
return "CustomEnum{}".format(field.name.title())


class TestDjangoObjectType:
@pytest.fixture
def PetModel(self):
Expand Down Expand Up @@ -492,3 +498,78 @@ class Query(ObjectType):
}
"""
)

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