Repository: graphql-python/graphene-django

Issue:
Different types with the same name in the schema - Django fields with choices
Hi,

When you try to create a model with choices field and then create a second model which inherit from the previous one, you will receive error while schema generating.

Here you have a simple [project](https://github.com/KaRi94/graphene-enum-bug-example) with models and basic configuration. Try to generate schema and you will get following error:
```
AssertionError: Found different types with the same name in the schema: PageType, PageType.
```

It seems that it is unable to inherit models with choices fields.


Discussion:
Getting same 

AssertionError: Found different types with the same name in the schema: PartMarketState, PartMarketState.

This is using django-fsm (functional state machine) which is guess creates set of enums as well

I getting a same error.
File "E:\pythonvenv\django11\lib\site-packages\graphql\type\definition.py", line 180, in fields
    return define_field_map(self, self._fields)
  File "E:\pythonvenv\django11\lib\site-packages\graphql\type\definition.py", line 189, in define_field_map
    field_map = field_map()
  File "E:\pythonvenv\django11\lib\site-packages\graphene\types\typemap.py", line 235, in construct_fields_for_type
    map = self.reducer(map, field.type)
  File "E:\pythonvenv\django11\lib\site-packages\graphene\types\typemap.py", line 72, in reducer
    return self.graphene_reducer(map, type)
  File "E:\pythonvenv\django11\lib\site-packages\graphene\types\typemap.py", line 77, in graphene_reducer
    return self.reducer(map, type.of_type)
  File "E:\pythonvenv\django11\lib\site-packages\graphene\types\typemap.py", line 72, in reducer
    return self.graphene_reducer(map, type)
  File "E:\pythonvenv\django11\lib\site-packages\graphene\types\typemap.py", line 83, in graphene_reducer
    ).format(_type.graphene_type, type)
AssertionError: Found different types with the same name in the schema: UserUserType, UserUserType.

```

class User(BDAbstractUser):

    TYPE_CHOICES = [('agent', 'agent'), ('nomal', 'nomal.'), ('superuser',
     """                                                          'superuser')]
    user_type = models.CharField(max_length=10, blank=True)
    """
    user_type = models.CharField(
        _('user type'),
        max_length=10,
        choices=TYPE_CHOICES,
        default='nomal',
        help_text=_('need user type'))

```


It seems you're not allowed to include ``type`` in your field's name. I tried ``type = models.CharField()`` and ``foo_type = models.CharField()`` and both caused that error.

When I changed the field's name to ``foo`` the error was gone.
Still have this issue after renaming of models fields.
But this hack works for me https://github.com/graphql-python/graphene-django/issues/87#issuecomment-328829397
Is it documented anywhere that we can not use field name `type` in models when using this library?
@darkowic I was wondering why I was getting this error. Now that you mention, the problematic model has a `type` field in it.
This is quite a glaring bug to still have. 

What can be done about it? Thoughts?
When `graphene_django` converts a field with `choices` to `graphene.Enum`, it creates a new type for this field.

If it was `type` field of model `Foo`, new autogenerated type will be `FooType`.

And you probably already have your own

```
class FooType(DjangoObjectType):
     class Meta:
         model = Foo
```

So there is `AssertionError: Found different types with the same name in the schema: FooType, FooType.`

This conversion raised a lot of questions already: https://github.com/graphql-python/graphene-django/issues?q=is%3Aissue+choices

Maybe someone adds `GRAPHENE_CONVERT_CHOICES_TO_ENUMS = False` setting to `graphene_django` once.

Until then I am going to monkey-patch it somewhere before schema is imported, for example in settings file.

Patch: https://gist.github.com/denis-ryzhkov/fcb944f6ebfad4775efb74848703f0d7

@denis-ryzhkov thanks for the investigation! You're right, the issues seems to be that the auto naming of the Enum field that a Django choice field gets converted to is likely to clash with an existing type in your schema since it's just the object type name plus the field name: https://github.com/graphql-python/graphene-django/blob/a480a39713d3392dc3a7cee9565989a713d05856/graphene_django/converter.py#L50

I think the fix for that is make the generated name more explicit and less likely to clash with anything in the schema. Something like `DjangoModel{object_name}{field_name}Choices` (e.g. `DjangoModelFooTypeChoices`).

Separately, disabling the conversion of Django choice fields to Enums is a good idea since it's not always desired. Though I think it's probably something that should be done on a field level rather than globally.
I fixed it by just using the import statement. I imported the type from another class instead of rewriting it again. 

But that maybe does not solve the issues if you overwrite a class
I have also had this issue and @denis-ryzhkov explanation helped me resolve the issue. I have a model `Organization` with field `type` with choices in it. For schema I had the following:

```
class OrganizationType(DjangoObjectType):
    class Meta:
        model = Organization
```

As a result, I was getting:

> AssertionError: Found different types with the same name in the schema: OrganizationType, OrganizationType.

After reading through this, I just changed the class name to `OrganizationGQLType`. Now, everyhing works as expected. If this decision is not going to be changed, I think it would be beneficial to add this to the docs to clear out the confusion.
I also encountered the same error and renaming the field `type` into something fixed my problem 
> Is it documented anywhere that we can not use field name type in models when using this library?

It's generally not a good idea to use this as a variable name in Python, as you're overwriting the built-in `type` keyword. So this isn't documented in this package because it's probably more of a Python gotcha?
> as you're overwriting the built-in `type` keyword

There is a difference between [reserved keywords](https://docs.python.org/3/reference/lexical_analysis.html#keywords) that cannot be used as identifiers vs [built-in identifiers](https://docs.python.org/3/library/builtins.html) that are always available (either without import or via `builtins` module)

Both `type` and `id` are examples of built-in identifiers, and `id` is widely shadowed, e.g. in Django ORM

This shadowing usually happens on the class definition level, where built-in functions like `type()` and `id()` are of no use, but class fields with these names are very natural

Once such class fields are defined, `item.id` and `item.type` (including `self.id` and `self.type`) don't shadow built-ins any more

Imagine `graphene-django` expected you to never have `id` field in your classes and also felt no need to document this expectation and naming conflict it leads to, just because `id` is a built-in identifier - that would raise a ton of issues instantly, but even `type` raised multiple issues here

So please either document this poor design decision and its workaround or solve it somehow
> There is a difference between reserved keywords that cannot be used as identifiers vs built-in identifiers that are always available (either without import or via builtins module)

I mixed them up, woops!

> So please either document this poor design decision and its workaround or solve it somehow

I appreciate this hasn't been looked at yet. I'm a new collaborator to this project, I'll see what I can do to make sure this bug is added to a list of top priority things.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Looks like this is still an issue. Don't close
I think this can be solved by adding double underscore or something similar to the name (e.g “__OrganizationType” instead of “OrganizationType”). I just got curious about this to be honest. Why does a choice based field have a type? Is it possible to completely remove this auto-generated field type for choice field?
@GasimGasimzada there is an option in master (not yet released) that allows you to turn off the automatic choice to enum conversion: https://github.com/graphql-python/graphene-django/pull/674
I am trying to figure out why our unit tests don't catch this. We have a test model `Reporter` which has a field `reporter_type` which has options and it seems to work. See: https://github.com/graphql-python/graphene-django/blob/master/graphene_django/tests/models.py#L45
@zbyte64  Isn't it just if the field is named "type"? I have the same issue.

To fix it you have to use `ConvertedPageType = Enum.from_enum(PageType)` only once, and use `ConvertedPageType` everywhere
> To fix it you have to use `ConvertedPageType = Enum.from_enum(PageType)` only once, and use `ConvertedPageType` everywhere

This doesn't work if you are using `RelaySerializerMutation`, as it will create a type on its own
Related issue: `meta.object_name` is not unique enough, since it's possible to have multiple models with the same name in different Django apps which are running at the same time.

> I think the fix for that is make the generated name more explicit and less likely to clash with anything in the schema. Something like `DjangoModel{object_name}{field_name}Choices` (e.g. `DjangoModelFooTypeChoices`).

So this is not enough, it should be `DjangoModel{app_label}{object_name}{field_name}Choices`.
> Related issue: `meta.object_name` is not unique enough, since it's possible to have multiple models with the same name in different Django apps which are running at the same time.
> 
> > I think the fix for that is make the generated name more explicit and less likely to clash with anything in the schema. Something like `DjangoModel{object_name}{field_name}Choices` (e.g. `DjangoModelFooTypeChoices`).
> 
> So this is not enough, it should be `DjangoModel{app_label}{object_name}{field_name}Choices`.

I agree. But maybe check first for name collisions first? Otherwise new and very simple projects will have over-complicated names.
> > Related issue: `meta.object_name` is not unique enough, since it's possible to have multiple models with the same name in different Django apps which are running at the same time.
> > > I think the fix for that is make the generated name more explicit and less likely to clash with anything in the schema. Something like `DjangoModel{object_name}{field_name}Choices` (e.g. `DjangoModelFooTypeChoices`).
> > 
> > 
> > So this is not enough, it should be `DjangoModel{app_label}{object_name}{field_name}Choices`.
> 
> I agree. But maybe check first for name collisions first? Otherwise new and very simple projects will have over-complicated names.

Very good points and I especially like the suggestion of checking for name collisions first to progressively improve this. That way it’s not a breaking change either. I’ll try and create a PR for this as soon as possible. 
@jkimbo have you had a chance to work on that? Just ran into this, seems like your solution would solve my problems.
@MisterGlass I have had a look and unfortunately I don’t it would be possible to check for name collisions since the enum name is defined before it has access to the schema. I think the only way to solve this would be an opt in flag to change the naming generation. Currently not sure if this should be per object type or globally for the whole schema. Anyone have any suggestions? 
I think using the long names without checking for name collisions is fine. Certainly better than the collisions IMO.
@MisterGlass @arielnmz would it be very disruptive for you we added a global option to change all enums generated from field choices to the new long name format? It would be all or nothing. 

Resolving patch:
diff --git a/graphene_django/converter.py b/graphene_django/converter.py
--- 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
--- 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:


Test patch:
diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py
--- 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
--- 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