Repository: hzdg/django-enumfields

Issue:
2.1.0 Incompatibility with drf-yasg
After updated to `django-enumfields==2.1.0` I am getting the following error in a unit test that tests generation of OpenAPI schema using `drf-yasg2`.

The `enum` being used is:

```py
from django.contrib.postgres.fields import ArrayField

@unique
class Tag(Enum):
    Upper = "upper"
    Lower = "lower"

class MyModel(models.Model):
    tags = ArrayField(EnumField(Tag), blank=True, null=True)
    ...
```

Relevant verisions:

```
Django==3.1.6
drf-yasg==1.20.0
django-enumfields==2.1.0
```

<details>

<summary>Traceback</summary>

```
Traceback (most recent call last):
  File "/venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/venv/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/venv/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/venv/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/venv/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/venv/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "/venv/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "/venv/lib/python3.9/site-packages/drf_yasg/views.py", line 94, in get
    schema = generator.get_schema(request, self.public)
  File "/venv/lib/python3.9/site-packages/drf_yasg/generators.py", line 246, in get_schema
    paths, prefix = self.get_paths(endpoints, components, request, public)
  File "/venv/lib/python3.9/site-packages/drf_yasg/generators.py", line 404, in get_paths
    operation = self.get_operation(view, path, prefix, method, components, request)
  File "/venv/lib/python3.9/site-packages/drf_yasg/generators.py", line 446, in get_operation
    operation = view_inspector.get_operation(operation_keys)
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py", line 45, in get_operation
    responses = self.get_responses()
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py", line 180, in get_responses
    response_serializers = self.get_response_serializers()
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py", line 235, in get_response_serializers
    responses = self.get_default_responses()
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py", line 211, in get_default_responses
    default_schema = self.serializer_to_schema(default_schema) or ''
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 437, in serializer_to_schema
    return self.probe_inspectors(
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 110, in probe_inspectors
    result = method(obj, **kwargs)
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py", line 33, in get_schema
    return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions)
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 228, in probe_field_inspectors
    return self.probe_inspectors(
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 110, in probe_inspectors
    result = method(obj, **kwargs)
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py", line 124, in field_to_swagger_object
    actual_schema = definitions.setdefault(ref_name, make_schema_definition)
  File "/venv/lib/python3.9/site-packages/drf_yasg/openapi.py", line 688, in setdefault
    ret = maker()
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py", line 100, in make_schema_definition
    child_schema = self.probe_field_inspectors(
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 228, in probe_field_inspectors
    return self.probe_inspectors(
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 110, in probe_inspectors
    result = method(obj, **kwargs)
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py", line 77, in field_to_swagger_object
    child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 228, in probe_field_inspectors
    return self.probe_inspectors(
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py", line 110, in probe_inspectors
    result = method(obj, **kwargs)
  File "/venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py", line 640, in field_to_swagger_object
    choice = field_value_to_representation(field, choice)
  File "/venv/lib/python3.9/site-packages/drf_yasg/utils.py", line 461, in field_value_to_representation
    value = field.to_representation(value)
  File "/venv/lib/python3.9/site-packages/enumfields/drf/fields.py", line 22, in to_representation
    assert isinstance(instance, self.enum), instance
AssertionError: upper

rest_api/tests/test_views/test_api/test_swagger.py:8 (test_swagger)
client = <rest_framework.test.APIClient object at 0x11b307580>
django_user_model = <class 'django.contrib.auth.models.User'>

    def test_swagger(client: APIClient, django_user_model: Model) -> None:
        """Ensure the OpenAPI specification is generated without error."""
        create_and_login(client, django_user_model)
>       resp = client.get(f"{swagger_json_url}")

test_swagger.py:12: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../../venv/lib/python3.9/site-packages/rest_framework/test.py:288: in get
    response = super().get(path, data=data, **extra)
../../../../venv/lib/python3.9/site-packages/rest_framework/test.py:205: in get
    return self.generic('GET', path, **r)
../../../../venv/lib/python3.9/site-packages/rest_framework/test.py:233: in generic
    return super().generic(
../../../../venv/lib/python3.9/site-packages/django/test/client.py:470: in generic
    return self.request(**r)
../../../../venv/lib/python3.9/site-packages/rest_framework/test.py:285: in request
    return super().request(**kwargs)
../../../../venv/lib/python3.9/site-packages/rest_framework/test.py:237: in request
    request = super().request(**kwargs)
../../../../venv/lib/python3.9/site-packages/django/test/client.py:716: in request
    self.check_exception(response)
../../../../venv/lib/python3.9/site-packages/django/test/client.py:577: in check_exception
    raise exc_value
../../../../venv/lib/python3.9/site-packages/django/core/handlers/exception.py:47: in inner
    response = get_response(request)
../../../../venv/lib/python3.9/site-packages/django/core/handlers/base.py:181: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
../../../../venv/lib/python3.9/site-packages/django/views/decorators/csrf.py:54: in wrapped_view
    return view_func(*args, **kwargs)
../../../../venv/lib/python3.9/site-packages/django/views/generic/base.py:70: in view
    return self.dispatch(request, *args, **kwargs)
../../../../venv/lib/python3.9/site-packages/rest_framework/views.py:509: in dispatch
    response = self.handle_exception(exc)
../../../../venv/lib/python3.9/site-packages/rest_framework/views.py:469: in handle_exception
    self.raise_uncaught_exception(exc)
../../../../venv/lib/python3.9/site-packages/rest_framework/views.py:480: in raise_uncaught_exception
    raise exc
../../../../venv/lib/python3.9/site-packages/rest_framework/views.py:506: in dispatch
    response = handler(request, *args, **kwargs)
../../../../venv/lib/python3.9/site-packages/drf_yasg/views.py:94: in get
    schema = generator.get_schema(request, self.public)
../../../../venv/lib/python3.9/site-packages/drf_yasg/generators.py:246: in get_schema
    paths, prefix = self.get_paths(endpoints, components, request, public)
../../../../venv/lib/python3.9/site-packages/drf_yasg/generators.py:404: in get_paths
    operation = self.get_operation(view, path, prefix, method, components, request)
../../../../venv/lib/python3.9/site-packages/drf_yasg/generators.py:446: in get_operation
    operation = view_inspector.get_operation(operation_keys)
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py:45: in get_operation
    responses = self.get_responses()
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py:180: in get_responses
    response_serializers = self.get_response_serializers()
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py:235: in get_response_serializers
    responses = self.get_default_responses()
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/view.py:211: in get_default_responses
    default_schema = self.serializer_to_schema(default_schema) or ''
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:437: in serializer_to_schema
    return self.probe_inspectors(
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:110: in probe_inspectors
    result = method(obj, **kwargs)
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py:33: in get_schema
    return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions)
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:228: in probe_field_inspectors
    return self.probe_inspectors(
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:110: in probe_inspectors
    result = method(obj, **kwargs)
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py:124: in field_to_swagger_object
    actual_schema = definitions.setdefault(ref_name, make_schema_definition)
../../../../venv/lib/python3.9/site-packages/drf_yasg/openapi.py:688: in setdefault
    ret = maker()
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py:100: in make_schema_definition
    child_schema = self.probe_field_inspectors(
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:228: in probe_field_inspectors
    return self.probe_inspectors(
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:110: in probe_inspectors
    result = method(obj, **kwargs)
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py:77: in field_to_swagger_object
    child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:228: in probe_field_inspectors
    return self.probe_inspectors(
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/base.py:110: in probe_inspectors
    result = method(obj, **kwargs)
../../../../venv/lib/python3.9/site-packages/drf_yasg/inspectors/field.py:640: in field_to_swagger_object
    choice = field_value_to_representation(field, choice)
../../../../venv/lib/python3.9/site-packages/drf_yasg/utils.py:461: in field_value_to_representation
    value = field.to_representation(value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = EnumField(choices=[(<Tag.Upper: 'upper'>, 'Upper'), (<Tag.Lower: 'lower'>, 'Lower')], enum=<enum 'Tag'>, label='Tags')
instance = 'upper'

    def to_representation(self, instance):
>       assert isinstance(instance, self.enum), instance
E       AssertionError: upper

../../../../venv/lib/python3.9/site-packages/enumfields/drf/fields.py:22: AssertionError
```

</details>

Stepping through in the debugger, it looks like `to_representation()` is being passed a `str` `instance` which is tripping this assertion.

The unit test is roughly:

```py
def test_swagger(client: APIClient) -> None:
    """Ensure the OpenAPI specification is generated without error."""
    resp = client.get("/rest/api/swagger.json")
    assert resp.status_code == HTTP_200_OK
```

Discussion:

Patch:
diff --git a/enumfields/drf/fields.py b/enumfields/drf/fields.py
index 6a7e864..bfac3c8 100644
--- a/enumfields/drf/fields.py
+++ b/enumfields/drf/fields.py
@@ -19,11 +19,17 @@ class EnumField(ChoiceField):
         super().__init__(**kwargs)
 
     def to_representation(self, instance):
-        assert isinstance(instance, self.enum), instance
-        if self.ints_as_names and isinstance(instance.value, int):
-            # If the enum value is an int, assume the name is more representative
-            return instance.name.lower()
-        return instance.value
+        if instance in ('', None):
+            return instance
+        try:
+            if not isinstance(instance, self.enum):
+                instance = self.enum(instance)  # Try to cast it
+            if self.ints_as_names and isinstance(instance.value, int):
+                # If the enum value is an int, assume the name is more representative
+                return instance.name.lower()
+            return instance.value
+        except ValueError:
+            raise ValueError('Invalid value [{!r}] of enum {}'.format(instance, self.enum.__name__))
 
     def to_internal_value(self, data):
         if isinstance(data, self.enum):


Test patch:
diff --git a/tests/enums.py b/tests/enums.py
index 5b47276..40c8f69 100644
--- a/tests/enums.py
+++ b/tests/enums.py
@@ -1,4 +1,4 @@
-from django.utils.translation import ugettext_lazy
+from django.utils.translation import gettext_lazy
 
 from enumfields import Enum, IntEnum
 
@@ -12,7 +12,7 @@ class Color(Enum):
 
     class Labels:
         RED = 'Reddish'
-        BLUE = ugettext_lazy('bluë')
+        BLUE = gettext_lazy('bluë')
 
 
 class Taste(Enum):
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
index 59236f8..cb132a4 100644
--- a/tests/test_serializers.py
+++ b/tests/test_serializers.py
@@ -1,6 +1,7 @@
 import uuid
 
 import pytest
+from enumfields.drf import EnumField
 from enumfields.drf.serializers import EnumSupportSerializerMixin
 from rest_framework import serializers
 
@@ -42,6 +43,22 @@ def test_serialize(int_names):
         assert data['int_enum'] == data['int_enum_not_editable'] == IntegerEnum.B.value
 
 
+@pytest.mark.parametrize('instance, representation', [
+    ('', ''),
+    (None, None),
+    ('r', 'r'),
+    ('g', 'g'),
+    ('b', 'b'),
+])
+def test_enumfield_to_representation(instance, representation):
+    assert EnumField(Color).to_representation(instance) == representation
+
+
+def test_invalid_enumfield_to_representation():
+    with pytest.raises(ValueError, match=r"Invalid value.*"):
+        assert EnumField(Color).to_representation('INVALID_ENUM_STRING')
+
+
 @pytest.mark.django_db
 @pytest.mark.parametrize('lenient_serializer', (False, True))
 @pytest.mark.parametrize('lenient_data', (False, True))