graphql-python / graphene-django

Build powerful, efficient, and flexible GraphQL APIs with seamless Django integration.
http://docs.graphene-python.org/projects/django/en/latest/
MIT License
4.31k stars 770 forks source link

Enums are generating duplicated types #785

Open Tibiritabara opened 5 years ago

Tibiritabara commented 5 years ago

I think I read this issue in the past, graphene-django is currently throwing the next exception on my end:

"Found different types with the same name in the schema: currency, currency"

The currency field is just a field with enums

class Test:
    CURRENCY = (
        ("EUR", "EUR"),
        ("USD", "USD"),
    )

   currency = models.CharField(
        "Operation currency",
        max_length=5,
        choices=CURRENCY,
        default=CURRENCY[0]
    )

There is a serializer for this model:

class TestSerializer(serializers.ModelSerializer):
    class Meta:
        model = Test
        fields = [
            'id',
            'currency',
        ]

The schema is using the Test model

class TestNode(DjangoObjectType):
    class Meta:
        """Transaction GraphQL Type Metaclass
        Modifies the basic behavior of the type
        """
        model = Test
        filter_fields = {
            'currency': ['exact'],
        }
        interfaces = (node.Relay, )

and there are 2 mutations, one for creation based on the serializer and a custom one:

class CreateTestMutation(SerializerMutation):
    class Meta:
        serializer_class = TestSerializer
        model_operations = [
            'create',
        ]

class UpdateTestinput(InputObjectType):
    currency = graphene.String()

class UpdateTestMutation(graphene.Mutation):
    id = graphene.String()
    currency = graphene.String()

    class Arguments:
        """Arguments for the given mutation
        It defines which arguments are mandatory for the
        given mutation. The id is the one that will be used
        for searching and the input is the required object 
        for the update
        """
        id = graphene.String(required=True)
        input = UpdateTestInput(required=True)

    def mutate(self, info, id, input=None):
        test = Test.objects.get(pk=str(id))
        for key, value in input.items():
            setattr(transaction, key, value)
        test.save()
        # Notice we return an instance of this mutation
        return UpdateTestMutation(
            id=id,
            currency=transaction.currency,
        )

Now, this is currently throwing the next exception when using the latest release (2.6.0): Found different types with the same name in the schema: currency, currency.

On 2.5.0 this error does not happen.

jl-DaDar commented 5 years ago

i have exact same problem

stale[bot] commented 4 years ago

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.

begimai commented 4 years ago

it is all because both DjangoObjectType and SerializerMutation are converting choice field to graphene Enum. I am struggling with the same error. Currently I see the easiest solution to move all Django Enums to Graphene Enums and resolve everything manually

leewardbound commented 4 years ago

Also having the same problem -- it really seems like being able to create a DjangoObjectType and a SerializerMutation for the same class, without field conflicts arising, should be a core supported use-case.

I feel like this could be avoided if, somehow, maybe the SerializerMutation was "made aware of" the ObjectType, such that a new ObjectType isn't generated for the input/output? Looking through the source, I don't see any way to achieve this, and I'm also not confident in this hunch.

Code is here --

for model, fields, filterset, serializer in resources:
    class ObjectMeta:
        model = model
        name = model._meta.object_name
        fields = fields
        filter_fields = filterset.filter_fields
        interfaces = (graphene.relay.Node,)
        convert_choices_to_enum = False

    ObjectType = type('%sObjectType'%ObjectMeta.name, (DjangoObjectType,), {"Meta": ObjectMeta})

    class MutationMeta:
        serializer_class = serializer
        model_operations = ['create']
        convert_choices_to_enum = False

    Mutation = type('%sMutation'%ObjectMeta.name, (SerializerMutation,), {"Meta": MutationMeta})

EDIT -- I've isolated this bug in a test case -- I have verified that a simple test for adding a SerializerMutation with any simple ChoiceField causes the error to be thrown.

I think we can all agree that this test should not fail on the last line, but it throws the error E AssertionError: Found different types with the same name in the schema: status, status.

I have also submitted a PR #851 to allow us to disable this Enum creation, but I think we have positive confirmation here that these Enum types are causing schema definition collisions even in simple use cases.

stale[bot] commented 4 years ago

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.

alhajee commented 4 years ago

I had the same problem but this was how i solved it: You must not call your Enum class more than once in your schema

Models.py

class EnumAccountTypes(models.TextChoices):
    PERSONAL = 'PERSONAL', 'Personal'
    BUSINESS = 'BUSINESS', 'Business'
    COURIER = 'COURIER', 'Courier'
    DRIVER = 'DRIVER', 'Driver'

Schema.py

from .models import EnumAccountTypes as EnumAccountInputTypes

EnumAccountTypes = graphene.Enum.from_enum(EnumAccountInputTypes)

class CreateUser(graphene.Mutation):
  # Return object
  user = graphene.Field(UserType)
  # Arguments
  class Arguments:
''' other arguments '''
    account_type = graphene.Argument(
      EnumAccountTypes
    )

  def mutate(self, info, account_type):

    user = User(
      account_type=account_type
    )
    user.save()

    return CreateUser(user=user)
jasong2 commented 4 years ago

I had the same problem but this was how i solved it: You must not call your Enum class more than once in your schema

Models.py

class EnumAccountTypes(models.TextChoices):
  PERSONAL = 'PERSONAL', 'Personal'
  BUSINESS = 'BUSINESS', 'Business'
  COURIER = 'COURIER', 'Courier'
  DRIVER = 'DRIVER', 'Driver'

Schema.py

from .models import EnumAccountTypes as EnumAccountInputTypes

EnumAccountTypes = graphene.Enum.from_enum(EnumAccountInputTypes)

class CreateUser(graphene.Mutation):
  # Return object
  user = graphene.Field(UserType)
  # Arguments
  class Arguments:
''' other arguments '''
    account_type = graphene.Argument(
      EnumAccountTypes
    )

  def mutate(self, info, account_type):

    user = User(
      account_type=account_type
    )
    user.save()

    return CreateUser(user=user)

works. nice post

vijaymdu27 commented 4 years ago

I had the same problem but this was how i solved it: You must not call your Enum class more than once in your schema

Models.py

class EnumAccountTypes(models.TextChoices):
  PERSONAL = 'PERSONAL', 'Personal'
  BUSINESS = 'BUSINESS', 'Business'
  COURIER = 'COURIER', 'Courier'
  DRIVER = 'DRIVER', 'Driver'

Schema.py

from .models import EnumAccountTypes as EnumAccountInputTypes

EnumAccountTypes = graphene.Enum.from_enum(EnumAccountInputTypes)

class CreateUser(graphene.Mutation):
  # Return object
  user = graphene.Field(UserType)
  # Arguments
  class Arguments:
''' other arguments '''
    account_type = graphene.Argument(
      EnumAccountTypes
    )

  def mutate(self, info, account_type):

    user = User(
      account_type=account_type
    )
    user.save()

    return CreateUser(user=user)

successfully mutation created using Enum by this method, but how to query with same Enum values

moritz89 commented 3 years ago

Regarding the Enum conversion, I see it as a bug, that the string of the member variable instead of the first element in the tuple is used. i.e., given the following enum:

class EnumAccountTypes(models.TextChoices):
    PERSONAL = 'personal', 'Personal'
    BUSINESS = 'business 'Business'
    COURIER = 'courier', 'Courier'
    DRIVER = 'driver', 'Driver'

The following query:

query {
  user(id: "...") {
    id
    accountType
  }
}

Results in the following result:

{
  "data": {
    "controllerTask": {
      "id": "...",
      "accountType": "PERSONAL"
    }
  }
}

Instead of what I would expect

{
  "data": {
    "controllerTask": {
      "id": "...",
      "accountType": "personal"
    }
  }
}
Datoclement commented 2 years ago

This should fix the situation before the bug is patched.

import warnings

import graphene
from graphene.types.typemap import TypeMap
from graphql.type.introspection import IntrospectionSchema

from my_query_module import Query
from my_mutation_module import Mutation

class EnumConflictGracefulTypeMap(TypeMap):

    @staticmethod
    def dict_equal(this_dict: dict, that_dict: dict) -> bool:
        if set(this_dict.keys()) != set(that_dict.keys()): return False
        keys = set(this_dict.keys())
        if any(this_dict[key].name != that_dict[key].name for key in keys): return False
        if any(this_dict[key].value != that_dict[key].value for key in keys): return False
        return True

    @staticmethod
    def graphene_enum_to_dict(graphene_enum: graphene.Enum) -> dict:
        return graphene_enum._meta.enum.__members__

    def graphene_reducer(self, map, type):
        try:
            return super(EnumConflictGracefulTypeMap, self).graphene_reducer(map, type)
        except AssertionError as e:

            # re-raise other assertion errors
            if not str(e).startswith('Found different types with the same name in the schema:'): raise e

            # re-raise assertion errors not for enum types 
            if not issubclass(type, graphene.Enum): raise e

            # check if two enum types are truly equal, if not, raise more specific assertion error
            another_type = map[type._meta.name].graphene_type
            this = self.graphene_enum_to_dict(type)
            that = self.graphene_enum_to_dict(another_type)
            assert self.dict_equal(this, that), (
                f'Found enum types with the same name but with different definitions in the schema: \n'
                f'\tEnum: {type}\n'
                f'\tdefined as: {this} but also\n'
                f'\tdefined as: {that}\n'
            )

            warnings.warn(
                f'Found an enum type being mapped twice when generating the schema:\n'
                f'\t{type}: {this}\n'
            )

            return map

class EnumConflictGracefulSchema(graphene.Schema):

    def build_typemap(self):
        initial_types = [
            self._query,
            self._mutation,
            self._subscription,
            IntrospectionSchema,
        ]
        if self.types:
            initial_types += self.types

        # self._type_map = TypeMap(
        self._type_map = EnumConflictGracefulTypeMap(
            #            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
            #  the only place where the method is modified
            initial_types, auto_camelcase=self.auto_camelcase, schema=self
        )

# instead of using schema = graphene.Schema(query=Query, mutation=Mutation)
schema = EnumConflictGracefulSchema(query=Query, mutation=Mutation)