From b8e598d66d87e291591bc65be41bad2087d788d3 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 13 Mar 2020 10:04:25 +0000 Subject: [PATCH] =?UTF-8?q?Add=20options=20to=20override=20how=20Django=20?= =?UTF-8?q?Choice=20fields=20are=20converted=20t=E2=80=A6=20(#860)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Fix tests * Update docs * Import function through import_string function Co-authored-by: Syrus Akbary --- docs/settings.rst | 30 +++++++++ graphene_django/converter.py | 31 +++++++++- graphene_django/settings.py | 3 + graphene_django/tests/test_converter.py | 30 ++++++++- graphene_django/tests/test_types.py | 81 +++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 4 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 4776ce004..5a7e4c9da 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -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" + } diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 8b93d175c..bd8f79d03 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -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, @@ -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 @@ -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 ): @@ -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: diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 9a5e8a906..666ad8a14 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -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: diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 3790c4a7b..7f84de3ae 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -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 @@ -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 @@ -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 diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index c32f46cef..888521f78 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -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 @@ -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): @@ -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