From 010f3c217cad2c4f8a42bf317a5be32492873684 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 13 Jun 2019 17:19:08 +0100 Subject: [PATCH 1/8] Add convert_choices_to_enum meta option --- graphene_django/converter.py | 4 ++-- graphene_django/types.py | 25 ++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 1bb16f48f..11a427e12 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -52,13 +52,13 @@ def get_choices(choices): yield name, value, description -def convert_django_field_with_choices(field, registry=None): +def convert_django_field_with_choices(field, registry=None, convert_choices_to_enum=True): if registry is not None: converted = registry.get_converted_field(field) if converted: return converted choices = getattr(field, "choices", None) - if choices: + if choices and convert_choices_to_enum: meta = field.model._meta name = to_camel_case("{}_{}".format(meta.object_name, field.name)) choices = list(get_choices(choices)) diff --git a/graphene_django/types.py b/graphene_django/types.py index a1e17b32b..9c38bd308 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -18,7 +18,8 @@ from typing import Type -def construct_fields(model, registry, only_fields, exclude_fields): +def construct_fields(model, registry, only_fields, exclude_fields, + convert_choices_to_enum): _model_fields = get_model_fields(model) fields = OrderedDict() @@ -33,7 +34,17 @@ def construct_fields(model, registry, only_fields, exclude_fields): # in there. Or when we exclude this field in exclude_fields. # Or when there is no back reference. continue - converted = convert_django_field_with_choices(field, registry) + + _convert_choices_to_enum = convert_choices_to_enum + if not isinstance(_convert_choices_to_enum, bool): + # then `convert_choices_to_enum` is a list of field names to convert + if name in _convert_choices_to_enum: + _convert_choices_to_enum = True + else: + _convert_choices_to_enum = False + + converted = convert_django_field_with_choices( + field, registry, convert_choices_to_enum=_convert_choices_to_enum) fields[name] = converted return fields @@ -63,6 +74,7 @@ def __init_subclass_with_meta__( connection_class=None, use_connection=None, interfaces=(), + convert_choices_to_enum=True, _meta=None, **options ): @@ -90,7 +102,14 @@ def __init_subclass_with_meta__( ) django_fields = yank_fields_from_attrs( - construct_fields(model, registry, only_fields, exclude_fields), _as=Field + construct_fields( + model, + registry, + only_fields, + exclude_fields, + convert_choices_to_enum + ), + _as=Field ) if use_connection is None and interfaces: From 0afd512bb22c8801d1a52bd47ddcd4e68b575cdc Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 13 Jun 2019 17:19:22 +0100 Subject: [PATCH 2/8] Add tests --- graphene_django/tests/test_converter.py | 18 ++++++ graphene_django/tests/test_types.py | 75 ++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index bb176b343..07ac082cf 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -196,6 +196,24 @@ class Meta: convert_django_field_with_choices(field) +def test_field_with_choices_convert_enum_false(): + field = models.CharField( + help_text="Language", choices=(("es", "Spanish"), ("en", "English")) + ) + + class TranslatedModel(models.Model): + language = field + + class Meta: + app_label = "test" + + graphene_type = convert_django_field_with_choices( + field, + convert_choices_to_enum=False + ) + assert isinstance(graphene_type, graphene.String) + + def test_should_float_convert_float(): assert_conversion(models.FloatField, graphene.Float) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8a8643b9b..5beedb941 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,6 +1,10 @@ +from textwrap import dedent + +import pytest +from django.db import models from mock import patch -from graphene import Interface, ObjectType, Schema, Connection, String +from graphene import Connection, Field, Interface, ObjectType, Schema, String from graphene.relay import Node from .. import registry @@ -224,3 +228,72 @@ class Meta: fields = list(Reporter._meta.fields.keys()) assert "email" not in fields + + +class TestDjangoObjectType(): + @pytest.fixture + def PetModel(self): + class PetModel(models.Model): + kind = models.CharField(choices=(('cat', 'Cat'), ('dog', 'Dog'))) + cuteness = models.IntegerField(choices=( + (1, 'Kind of cute'), (2, 'Pretty cute'), (3, 'OMG SO CUTE!!!'))) + return PetModel + + def test_django_objecttype_convert_choices_enum_false(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = False + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent("""\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: String! + cuteness: Int! + } + + type Query { + pet: Pet + } + """) + + def test_django_objecttype_convert_choices_enum_list(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = ['kind'] + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent("""\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: PetModelKind! + cuteness: Int! + } + + enum PetModelKind { + CAT + DOG + } + + type Query { + pet: Pet + } + """) From e2ca1ed5e0bb9b3a4f0b65b2b26b74824edc4f65 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 13 Jun 2019 17:33:49 +0100 Subject: [PATCH 3/8] Run black --- graphene_django/converter.py | 4 +++- graphene_django/tests/test_converter.py | 3 +-- graphene_django/tests/test_types.py | 24 +++++++++++++++--------- graphene_django/types.py | 16 +++++++--------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 11a427e12..4d0b45f95 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -52,7 +52,9 @@ def get_choices(choices): yield name, value, description -def convert_django_field_with_choices(field, registry=None, convert_choices_to_enum=True): +def convert_django_field_with_choices( + field, registry=None, convert_choices_to_enum=True +): if registry is not None: converted = registry.get_converted_field(field) if converted: diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 07ac082cf..5542c90a7 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -208,8 +208,7 @@ class Meta: app_label = "test" graphene_type = convert_django_field_with_choices( - field, - convert_choices_to_enum=False + field, convert_choices_to_enum=False ) assert isinstance(graphene_type, graphene.String) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 5beedb941..f7c26a96d 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -230,13 +230,15 @@ class Meta: assert "email" not in fields -class TestDjangoObjectType(): +class TestDjangoObjectType: @pytest.fixture def PetModel(self): class PetModel(models.Model): - kind = models.CharField(choices=(('cat', 'Cat'), ('dog', 'Dog'))) - cuteness = models.IntegerField(choices=( - (1, 'Kind of cute'), (2, 'Pretty cute'), (3, 'OMG SO CUTE!!!'))) + kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog"))) + cuteness = models.IntegerField( + choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!")) + ) + return PetModel def test_django_objecttype_convert_choices_enum_false(self, PetModel): @@ -250,7 +252,8 @@ class Query(ObjectType): schema = Schema(query=Query) - assert str(schema) == dedent("""\ + assert str(schema) == dedent( + """\ schema { query: Query } @@ -264,20 +267,22 @@ class Query(ObjectType): type Query { pet: Pet } - """) + """ + ) def test_django_objecttype_convert_choices_enum_list(self, PetModel): class Pet(DjangoObjectType): class Meta: model = PetModel - convert_choices_to_enum = ['kind'] + convert_choices_to_enum = ["kind"] class Query(ObjectType): pet = Field(Pet) schema = Schema(query=Query) - assert str(schema) == dedent("""\ + assert str(schema) == dedent( + """\ schema { query: Query } @@ -296,4 +301,5 @@ class Query(ObjectType): type Query { pet: Pet } - """) + """ + ) diff --git a/graphene_django/types.py b/graphene_django/types.py index 9c38bd308..005300dc3 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -18,8 +18,9 @@ from typing import Type -def construct_fields(model, registry, only_fields, exclude_fields, - convert_choices_to_enum): +def construct_fields( + model, registry, only_fields, exclude_fields, convert_choices_to_enum +): _model_fields = get_model_fields(model) fields = OrderedDict() @@ -44,7 +45,8 @@ def construct_fields(model, registry, only_fields, exclude_fields, _convert_choices_to_enum = False converted = convert_django_field_with_choices( - field, registry, convert_choices_to_enum=_convert_choices_to_enum) + field, registry, convert_choices_to_enum=_convert_choices_to_enum + ) fields[name] = converted return fields @@ -103,13 +105,9 @@ def __init_subclass_with_meta__( django_fields = yank_fields_from_attrs( construct_fields( - model, - registry, - only_fields, - exclude_fields, - convert_choices_to_enum + model, registry, only_fields, exclude_fields, convert_choices_to_enum ), - _as=Field + _as=Field, ) if use_connection is None and interfaces: From 2d71a107d51cf2027502479750ba06ef69759854 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 14 Jun 2019 11:13:26 +0100 Subject: [PATCH 4/8] Update documentation --- docs/queries.rst | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/queries.rst b/docs/queries.rst index 0edd1dd67..6dce245a1 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -92,6 +92,66 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType return 'hello!' +Choices to Enum conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default Graphene-Django will convert any Django fields that have ``choices`` +into a GraphQL enum type. + +For example the following ``Model`` and ``DjangoObjectType``: + +.. code:: python + + class PetModel(models.Model): + kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog'))) + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + +Results in the following GraphQL schema definition: + +.. code:: + + type Pet { + id: ID! + kind: PetModelKind! + } + + enum PetModelKind { + CAT + DOG + } + +You can disable this automatic conversion by setting +``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType`` +``Meta`` class. + +.. code:: python + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = False + +.. code:: + + type Pet { + id: ID! + kind: String! + } + +You can also set ``convert_choices_to_enum`` to a list of fields that should be +automatically converted into enums: + +.. code:: python + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = ['kind'] + + Related models -------------- From 380ad94b3b6d72a165bfc7e1cc2ddac127986bd6 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 14 Jun 2019 11:16:07 +0100 Subject: [PATCH 5/8] Add link to Django choices documentation --- docs/queries.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/queries.rst b/docs/queries.rst index 6dce245a1..75bee3dec 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -95,8 +95,10 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType Choices to Enum conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~ -By default Graphene-Django will convert any Django fields that have ``choices`` -into a GraphQL enum type. +By default Graphene-Django will convert any Django fields that have `choices`_ +defined into a GraphQL enum type. + +.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices For example the following ``Model`` and ``DjangoObjectType``: From 448802b2b6a317880456ab95b5fd19227d958cbb Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 14 Jun 2019 15:07:32 +0100 Subject: [PATCH 6/8] Add test and documentation note That setting to an empty list is the same as setting the value as False --- docs/queries.rst | 3 +++ graphene_django/tests/test_types.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/docs/queries.rst b/docs/queries.rst index 75bee3dec..fe67a9d05 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -153,6 +153,9 @@ automatically converted into enums: model = PetModel convert_choices_to_enum = ['kind'] +**Note:** Setting ``convert_choices_to_enum`` to an empty list is the same as +setting it to `False`. + Related models -------------- diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index f7c26a96d..f7e41bff2 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -303,3 +303,32 @@ class Query(ObjectType): } """ ) + + def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = [] + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: String! + cuteness: Int! + } + + type Query { + pet: Pet + } + """ + ) From 488ff0119b4bbe708c6431f8c7baefdc6d77b254 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 14 Jun 2019 15:08:04 +0100 Subject: [PATCH 7/8] Fix Django warning in tests --- graphene_django/tests/test_types.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index f7e41bff2..c1ac6c2bc 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,3 +1,4 @@ +from collections import OrderedDict, defaultdict from textwrap import dedent import pytest @@ -239,7 +240,11 @@ class PetModel(models.Model): choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!")) ) - return PetModel + yield PetModel + + # Clear Django model cache so we don't get warnings when creating the + # model multiple times + PetModel._meta.apps.all_models = defaultdict(OrderedDict) def test_django_objecttype_convert_choices_enum_false(self, PetModel): class Pet(DjangoObjectType): From 829e3afd259c2af0e106bda1aa2dcb7ab31dd589 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 17 Jun 2019 17:19:08 +0100 Subject: [PATCH 8/8] rst is not markdown --- docs/queries.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/queries.rst b/docs/queries.rst index fe67a9d05..7aff57216 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -153,8 +153,8 @@ automatically converted into enums: model = PetModel convert_choices_to_enum = ['kind'] -**Note:** Setting ``convert_choices_to_enum`` to an empty list is the same as -setting it to `False`. +**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to +``False``. Related models