Repository: tfranzel/drf-spectacular

Issue:
Missing description for `SlugRelatedField` and `PrimaryKeyRelatedField` with `many=True`
**Describe the bug**
See https://github.com/tfranzel/drf-spectacular/pull/893#issuecomment-1344043990
Both fields are missing the description if used with `many=True`,

**To Reproduce**
Have a model with a `SlugRelatedField` and `many=True`. The according field in the Model should have a `help_text` attached.

**Expected behavior**
This `help_text` is shown, possibly with a variation like "[$help_text]" or "List of $help_text"

**Further notes**
[This commit](https://github.com/tfranzel/drf-spectacular/commit/f7ae2fbc0392d43593a53c18c78513bf4770b3cb) is removing the description for the `manyRelatedField`. It seems that the reason was a [duplicate description inserted into the nested field](https://github.com/tfranzel/drf-spectacular/issues/61). 

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 06e7ed3..995860a 100644
--- a/README.rst
+++ b/README.rst
@@ -156,7 +156,7 @@ Generate your schema with the CLI:
 
 .. code:: bash
 
-    $ ./manage.py spectacular --file schema.yml
+    $ ./manage.py spectacular --color --file schema.yml
     $ docker run -p 80:8080 -e SWAGGER_JSON=/schema.yml -v ${PWD}/schema.yml:/schema.yml swaggerapi/swagger-ui
 
 If you also want to validate your schema add the ``--validate`` flag. Or serve your schema directly
diff --git a/drf_spectacular/contrib/django_filters.py b/drf_spectacular/contrib/django_filters.py
index cddcd33..d2d80b9 100644
--- a/drf_spectacular/contrib/django_filters.py
+++ b/drf_spectacular/contrib/django_filters.py
@@ -57,7 +57,7 @@ class DjangoFilterExtension(OpenApiFilterExtension):
             return []
 
         result = []
-        with add_trace_message(filterset_class.__name__):
+        with add_trace_message(filterset_class):
             for field_name, filter_field in filterset_class.base_filters.items():
                 result += self.resolve_filter_field(
                     auto_schema, model, filterset_class, field_name, filter_field
diff --git a/drf_spectacular/drainage.py b/drf_spectacular/drainage.py
index 2c520df..8116d38 100644
--- a/drf_spectacular/drainage.py
+++ b/drf_spectacular/drainage.py
@@ -1,5 +1,6 @@
 import contextlib
 import functools
+import inspect
 import sys
 from collections import defaultdict
 from typing import DefaultDict
@@ -8,9 +9,13 @@ from typing import DefaultDict
 class GeneratorStats:
     _warn_cache: DefaultDict[str, int] = defaultdict(int)
     _error_cache: DefaultDict[str, int] = defaultdict(int)
+    _blue = ''
+    _red = ''
+    _yellow = ''
+    _clear = ''
 
     def __getattr__(self, name):
-        if not self.__dict__:
+        if 'silent' not in self.__dict__:
             from drf_spectacular.settings import spectacular_settings
             self.silent = spectacular_settings.DISABLE_ERRORS_AND_WARNINGS
         try:
@@ -33,12 +38,25 @@ class GeneratorStats:
         self._warn_cache.clear()
         self._error_cache.clear()
 
+    def enable_color(self):
+        self._blue = '\033[0;34m'
+        self._red = '\033[0;31m'
+        self._yellow = '\033[0;33m'
+        self._clear = '\033[0m'
+
     def emit(self, msg, severity):
         assert severity in ['warning', 'error']
-        msg = _get_current_trace() + str(msg)
         cache = self._warn_cache if severity == 'warning' else self._error_cache
+
+        source_location, breadcrumbs = _get_current_trace()
+        prefix = f'{self._blue}{source_location}: ' if source_location else ''
+        prefix += self._yellow if severity == 'warning' else self._red
+        prefix += f'{severity.capitalize()}'
+        prefix += f' [{breadcrumbs}]: ' if breadcrumbs else ': '
+
+        msg = prefix + self._clear + str(msg)
         if not self.silent and msg not in cache:
-            print(f'{severity.capitalize()} #{len(cache)}: {msg}', file=sys.stderr)
+            print(msg, file=sys.stderr)
         cache[msg] += 1
 
     def emit_summary(self):
@@ -80,17 +98,34 @@ _TRACES = []
 
 
 @contextlib.contextmanager
-def add_trace_message(trace_message):
+def add_trace_message(obj):
     """
     Adds a message to be used as a prefix when emitting warnings and errors.
     """
-    _TRACES.append(trace_message)
+    sourcefile, lineno = _get_source_location(obj)
+    _TRACES.append((sourcefile, lineno, obj.__name__))
     yield
     _TRACES.pop()
 
 
+@functools.lru_cache(maxsize=1000)
+def _get_source_location(obj):
+    try:
+        sourcefile = inspect.getsourcefile(obj)
+        lineno = inspect.getsourcelines(obj)[1]
+        return sourcefile, lineno
+    except:  # noqa: E722
+        return None, None
+
+
 def _get_current_trace():
-    return ''.join(f"{trace}: " for trace in _TRACES if trace)
+    source_locations = [t for t in _TRACES if t[0]]
+    if source_locations:
+        source_location = f"{source_locations[-1][0]}:{source_locations[-1][1]}"
+    else:
+        source_location = ''
+    breadcrumbs = ' > '.join(t[2] for t in _TRACES)
+    return source_location, breadcrumbs
 
 
 def has_override(obj, prop):
diff --git a/drf_spectacular/generators.py b/drf_spectacular/generators.py
index 83812dc..1414885 100644
--- a/drf_spectacular/generators.py
+++ b/drf_spectacular/generators.py
@@ -235,7 +235,7 @@ class SchemaGenerator(BaseSchemaGenerator):
                 f'DEFAULT_SCHEMA_CLASS pointing to "drf_spectacular.openapi.AutoSchema" '
                 f'or any other drf-spectacular compatible AutoSchema?'
             )
-            with add_trace_message(getattr(view, '__class__', view).__name__):
+            with add_trace_message(getattr(view, '__class__', view)):
                 operation = view.schema.get_operation(
                     path, path_regex, path_prefix, method, self.registry
                 )
diff --git a/drf_spectacular/management/commands/spectacular.py b/drf_spectacular/management/commands/spectacular.py
index d5a99d5..8f6bceb 100644
--- a/drf_spectacular/management/commands/spectacular.py
+++ b/drf_spectacular/management/commands/spectacular.py
@@ -6,7 +6,7 @@ from django.utils.module_loading import import_string
 
 from drf_spectacular.drainage import GENERATOR_STATS
 from drf_spectacular.renderers import OpenApiJsonRenderer, OpenApiYamlRenderer
-from drf_spectacular.settings import spectacular_settings
+from drf_spectacular.settings import patched_settings, spectacular_settings
 from drf_spectacular.validation import validate_schema
 
 
@@ -40,6 +40,8 @@ class Command(BaseCommand):
         parser.add_argument('--validate', dest="validate", default=False, action='store_true')
         parser.add_argument('--api-version', dest="api_version", default=None, type=str)
         parser.add_argument('--lang', dest="lang", default=None, type=str)
+        parser.add_argument('--color', dest="color", default=False, action='store_true')
+        parser.add_argument('--custom-settings', dest="custom_settings", default=None, type=str)
 
     def handle(self, *args, **options):
         if options['generator_class']:
@@ -47,15 +49,25 @@ class Command(BaseCommand):
         else:
             generator_class = spectacular_settings.DEFAULT_GENERATOR_CLASS
 
+        if options['color']:
+            GENERATOR_STATS.enable_color()
+
         generator = generator_class(
             urlconf=options['urlconf'],
             api_version=options['api_version'],
         )
-        if options['lang']:
-            with translation.override(options['lang']):
-                schema = generator.get_schema(request=None, public=True)
+
+        if options['custom_settings']:
+            custom_settings = import_string(options['custom_settings'])
         else:
-            schema = generator.get_schema(request=None, public=True)
+            custom_settings = None
+
+        with patched_settings(custom_settings):
+            if options['lang']:
+                with translation.override(options['lang']):
+                    schema = generator.get_schema(request=None, public=True)
+            else:
+                schema = generator.get_schema(request=None, public=True)
 
         GENERATOR_STATS.emit_summary()
 
diff --git a/drf_spectacular/openapi.py b/drf_spectacular/openapi.py
index 23e6575..03c0095 100644
--- a/drf_spectacular/openapi.py
+++ b/drf_spectacular/openapi.py
@@ -655,8 +655,9 @@ class AutoSchema(ViewInspector):
         if isinstance(field, serializers.ManyRelatedField):
             schema = self._map_serializer_field(field.child_relation, direction)
             # remove hand-over initkwargs applying only to outer scope
-            schema.pop('description', None)
             schema.pop('readOnly', None)
+            if meta.get('description') == schema.get('description'):
+                schema.pop('description', None)
             return append_meta(build_array_type(schema), meta)
 
         if isinstance(field, (serializers.PrimaryKeyRelatedField, serializers.SlugRelatedField)):
@@ -1497,7 +1498,7 @@ class AutoSchema(ViewInspector):
         assert_basic_serializer(serializer)
         serializer = force_instance(serializer)
 
-        with add_trace_message(serializer.__class__.__name__):
+        with add_trace_message(serializer.__class__):
             component = ResolvedComponent(
                 name=self._get_serializer_name(serializer, direction, bypass_extensions),
                 type=ResolvedComponent.SCHEMA,
diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py
index c1b3609..4fa4ce3 100644
--- a/drf_spectacular/plumbing.py
+++ b/drf_spectacular/plumbing.py
@@ -796,13 +796,18 @@ def load_enum_name_overrides():
             choices = [c.value for c in choices]
         normalized_choices = []
         for choice in choices:
-            if isinstance(choice, str):
+            # Allow None values in the simple values list case
+            if isinstance(choice, str) or choice is None:
                 normalized_choices.append((choice, choice))  # simple choice list
             elif isinstance(choice[1], (list, tuple)):
                 normalized_choices.extend(choice[1])  # categorized nested choices
             else:
                 normalized_choices.append(choice)  # normal 2-tuple form
-        overrides[list_hash(list(dict(normalized_choices).keys()))] = name
+
+        # Get all of choice values that should be used in the hash, blank and None values get excluded
+        # in the post-processing hook for enum overrides, so we do the same here to ensure the hashes match
+        hashable_values = [value for value, _ in normalized_choices if value not in ['', None]]
+        overrides[list_hash(hashable_values)] = name
 
     if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides):
         error(
diff --git a/drf_spectacular/templates/drf_spectacular/redoc.html b/drf_spectacular/templates/drf_spectacular/redoc.html
index 402106b..73fadfa 100644
--- a/drf_spectacular/templates/drf_spectacular/redoc.html
+++ b/drf_spectacular/templates/drf_spectacular/redoc.html
@@ -15,14 +15,14 @@
   <body>
     {% if settings %}
     <div id="redoc-container"></div>
-    <script src="{{ dist }}/bundles/redoc.standalone.js"></script>
+    <script src="{{ redoc_standalone }}"></script>
     <script>
       const redocSettings = {{ settings|safe }};
       Redoc.init("{{ schema_url }}", redocSettings, document.getElementById('redoc-container'))
     </script>
     {% else %}
     <redoc spec-url="{{ schema_url }}"></redoc>
-    <script src="{{ dist }}/bundles/redoc.standalone.js"></script>
+    <script src="{{ redoc_standalone }}"></script>
     {% endif %}
   </body>
 </html>
diff --git a/drf_spectacular/templates/drf_spectacular/swagger_ui.html b/drf_spectacular/templates/drf_spectacular/swagger_ui.html
index f991fb9..f3240c4 100644
--- a/drf_spectacular/templates/drf_spectacular/swagger_ui.html
+++ b/drf_spectacular/templates/drf_spectacular/swagger_ui.html
@@ -5,7 +5,7 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     {% if favicon_href %}<link rel="icon" href="{{ favicon_href }}">{% endif %}
-    <link rel="stylesheet" href="{{ dist }}/swagger-ui.css">
+    <link rel="stylesheet" href="{{ swagger_ui_css }}">
     <style>
       html { box-sizing: border-box; overflow-y: scroll; }
       *, *:after, *:before { box-sizing: inherit; }
@@ -14,8 +14,8 @@
   </head>
   <body>
     <div id="swagger-ui"></div>
-    <script src="{{ dist }}/swagger-ui-bundle.js"></script>
-    <script src="{{ dist }}/swagger-ui-standalone-preset.js"></script>
+    <script src="{{ swagger_ui_bundle }}"></script>
+    <script src="{{ swagger_ui_standalone }}"></script>
     {% if script_url %}
     <script src="{{ script_url }}"></script>
     {% else %}
diff --git a/drf_spectacular/views.py b/drf_spectacular/views.py
index d9480ce..95f0f97 100644
--- a/drf_spectacular/views.py
+++ b/drf_spectacular/views.py
@@ -114,8 +114,8 @@ class SpectacularJSONAPIView(SpectacularAPIView):
     renderer_classes = [OpenApiJsonRenderer, OpenApiJsonRenderer2]
 
 
-def _get_sidecar_url(package):
-    return static(f'drf_spectacular_sidecar/{package}')
+def _get_sidecar_url(filepath):
+    return static(f'drf_spectacular_sidecar/{filepath}')
 
 
 class SpectacularSwaggerView(APIView):
@@ -133,7 +133,9 @@ class SpectacularSwaggerView(APIView):
         return Response(
             data={
                 'title': self.title,
-                'dist': self._swagger_ui_dist(),
+                'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'),
+                'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'),
+                'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'),
                 'favicon_href': self._swagger_ui_favicon(),
                 'schema_url': self._get_schema_url(request),
                 'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS),
@@ -172,14 +174,16 @@ class SpectacularSwaggerView(APIView):
         ]
         return [auth.name for auth in auth_extensions if auth]
 
-    def _swagger_ui_dist(self):
+    @staticmethod
+    def _swagger_ui_resource(filename):
         if spectacular_settings.SWAGGER_UI_DIST == 'SIDECAR':
-            return _get_sidecar_url('swagger-ui-dist')
-        return spectacular_settings.SWAGGER_UI_DIST
+            return _get_sidecar_url(f'swagger-ui-dist/{filename}')
+        return f'{spectacular_settings.SWAGGER_UI_DIST}/{filename}'
 
-    def _swagger_ui_favicon(self):
+    @staticmethod
+    def _swagger_ui_favicon():
         if spectacular_settings.SWAGGER_UI_FAVICON_HREF == 'SIDECAR':
-            return _get_sidecar_url('swagger-ui-dist') + '/favicon-32x32.png'
+            return _get_sidecar_url('swagger-ui-dist/favicon-32x32.png')
         return spectacular_settings.SWAGGER_UI_FAVICON_HREF
 
 
@@ -209,7 +213,9 @@ class SpectacularSwaggerSplitView(SpectacularSwaggerView):
             return Response(
                 data={
                     'title': self.title,
-                    'dist': self._swagger_ui_dist(),
+                    'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'),
+                    'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'),
+                    'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'),
                     'favicon_href': self._swagger_ui_favicon(),
                     'script_url': set_query_parameters(
                         url=script_url,
@@ -235,7 +241,7 @@ class SpectacularRedocView(APIView):
         return Response(
             data={
                 'title': self.title,
-                'dist': self._redoc_dist(),
+                'redoc_standalone': self._redoc_standalone(),
                 'schema_url': self._get_schema_url(request),
                 'settings': self._dump(spectacular_settings.REDOC_UI_SETTINGS),
             },
@@ -250,10 +256,11 @@ class SpectacularRedocView(APIView):
         else:
             return json.dumps(data, indent=2)
 
-    def _redoc_dist(self):
+    @staticmethod
+    def _redoc_standalone():
         if spectacular_settings.REDOC_DIST == 'SIDECAR':
-            return _get_sidecar_url('redoc')
-        return spectacular_settings.REDOC_DIST
+            return _get_sidecar_url('redoc/bundles/redoc.standalone.js')
+        return f'{spectacular_settings.REDOC_DIST}/bundles/redoc.standalone.js'
 
     def _get_schema_url(self, request):
         schema_url = self.url or get_relative_url(reverse(self.url_name, request=request))
@@ -273,4 +280,4 @@ class SpectacularSwaggerOauthRedirectView(RedirectView):
     ``SPECTACULAR_SETTINGS.SWAGGER_UI_SETTINGS.oauth2RedirectUrl`` django settings.
     """
     def get_redirect_url(self, *args, **kwargs):
-        return _get_sidecar_url("swagger-ui-dist") + "/oauth2-redirect.html?" + self.request.GET.urlencode()
+        return _get_sidecar_url("swagger-ui-dist/oauth2-redirect.html") + "?" + self.request.GET.urlencode()


Test patch:
diff --git a/tests/test_command.py b/tests/test_command.py
index fa786be..8b2bf99 100644
--- a/tests/test_command.py
+++ b/tests/test_command.py
@@ -19,7 +19,7 @@ def test_command_plain(capsys):
     assert 'paths' in schema
 
 
-def test_command_parameterized(capsys):
+def test_command_parameterized():
     with tempfile.NamedTemporaryFile() as fh:
         management.call_command(
             'spectacular',
@@ -44,10 +44,33 @@ def test_command_fail(capsys):
             '--urlconf=tests.test_command',
         )
     stderr = capsys.readouterr().err
-    assert 'Error #0: func: unable to guess serializer' in stderr
+    assert 'Error [func]: unable to guess serializer' in stderr
     assert 'Schema generation summary:' in stderr
 
 
+def test_command_color(capsys):
+    management.call_command(
+        'spectacular',
+        '--color',
+        '--urlconf=tests.test_command',
+    )
+    stderr = capsys.readouterr().err
+    assert '\033[0;31mError [func]:' in stderr
+
+    # undo global state change
+    from drf_spectacular.drainage import GENERATOR_STATS
+    GENERATOR_STATS._red = GENERATOR_STATS._blue = ''
+    GENERATOR_STATS._yellow = GENERATOR_STATS._clear = ''
+
+
+CUSTOM = {'DESCRIPTION': 'custom setting'}
+
+
+def test_command_custom_settings(capsys):
+    management.call_command('spectacular', '--custom-settings=tests.test_command.CUSTOM')
+    assert 'description: custom setting' in capsys.readouterr().out
+
+
 def test_command_check(capsys):
     management.call_command('check', '--deploy')
     stderr = capsys.readouterr().err
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 3fb7315..e6a025c 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -35,7 +35,7 @@ fs = FileSystemStorage(location=tempfile.gettempdir())
 class Aux(models.Model):
     id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
     field_foreign = models.ForeignKey('Aux', null=True, on_delete=models.CASCADE)
-    url = models.URLField(unique=True)
+    url = models.URLField(unique=True, help_text="URL identifier for Aux")
 
 
 class AuxSerializer(serializers.ModelSerializer):
diff --git a/tests/test_fields.yml b/tests/test_fields.yml
index a2f2166..4e5ec9d 100644
--- a/tests/test_fields.yml
+++ b/tests/test_fields.yml
@@ -127,12 +127,14 @@ components:
         field_related_slug:
           type: string
           format: uri
+          description: URL identifier for Aux
           readOnly: true
         field_related_slug_many:
           type: array
           items:
             type: string
             format: uri
+            description: URL identifier for Aux
           readOnly: true
         field_related_string:
           type: string
@@ -368,6 +370,7 @@ components:
         url:
           type: string
           format: uri
+          description: URL identifier for Aux
           maxLength: 200
         field_foreign:
           type: string
diff --git a/tests/test_postprocessing.py b/tests/test_postprocessing.py
index a3c1990..3ef0c3f 100644
--- a/tests/test_postprocessing.py
+++ b/tests/test_postprocessing.py
@@ -24,6 +24,15 @@ language_choices = (
     ('cn', 'cn'),
 )
 
+blank_null_language_choices = (
+    ('en', 'en'),
+    ('es', 'es'),
+    ('ru', 'ru'),
+    ('cn', 'cn'),
+    ('', 'not provided'),
+    (None, 'unknown'),
+)
+
 vote_choices = (
     (1, 'Positive'),
     (0, 'Neutral'),
@@ -45,6 +54,29 @@ class LanguageChoices(TextChoices):
     EN = 'en'
 
 
+blank_null_language_list = ['en', '', None]
+
+
+class BlankNullLanguageEnum(Enum):
+    EN = 'en'
+    BLANK = ''
+    NULL = None
+
+
+class BlankNullLanguageStrEnum(str, Enum):
+    EN = 'en'
+    BLANK = ''
+    # These will still be included since the values get cast to strings so 'None' != None
+    NULL = None
+
+
+class BlankNullLanguageChoices(TextChoices):
+    EN = 'en'
+    BLANK = ''
+    # These will still be included since the values get cast to strings so 'None' != None
+    NULL = None
+
+
 class ASerializer(serializers.Serializer):
     language = serializers.ChoiceField(choices=language_choices)
     vote = serializers.ChoiceField(choices=vote_choices)
@@ -96,6 +128,40 @@ def test_global_enum_naming_override(no_warnings, clear_caches):
     assert len(schema['components']['schemas']) == 2
 
 
+@mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {
+    'LanguageEnum': 'tests.test_postprocessing.blank_null_language_choices'
+})
+def test_global_enum_naming_override_with_blank_and_none(no_warnings, clear_caches):
+    """Test that choices with blank values can still have their name overridden."""
+    class XSerializer(serializers.Serializer):
+        foo = serializers.ChoiceField(choices=blank_null_language_choices)
+        bar = serializers.ChoiceField(choices=blank_null_language_choices)
+
+    class XView(generics.RetrieveAPIView):
+        serializer_class = XSerializer
+
+    schema = generate_schema('/x', view=XView)
+    foo_data = schema['components']['schemas']['X']['properties']['foo']
+    bar_data = schema['components']['schemas']['X']['properties']['bar']
+
+    assert len(foo_data['oneOf']) == 3
+    assert len(bar_data['oneOf']) == 3
+
+    foo_ref_values = [ref_object['$ref'] for ref_object in foo_data['oneOf']]
+    bar_ref_values = [ref_object['$ref'] for ref_object in bar_data['oneOf']]
+
+    assert foo_ref_values == [
+        '#/components/schemas/LanguageEnum',
+        '#/components/schemas/BlankEnum',
+        '#/components/schemas/NullEnum'
+    ]
+    assert bar_ref_values == [
+        '#/components/schemas/LanguageEnum',
+        '#/components/schemas/BlankEnum',
+        '#/components/schemas/NullEnum'
+    ]
+
+
 def test_enum_name_reuse_warning(capsys):
     class XSerializer(serializers.Serializer):
         foo = serializers.ChoiceField(choices=language_choices)
@@ -191,6 +257,31 @@ def test_enum_override_variations(no_warnings):
             assert list_hash(['en']) in load_enum_name_overrides()
 
 
+def test_enum_override_variations_with_blank_and_null(no_warnings):
+    enum_override_variations = [
+        'blank_null_language_list',
+        'BlankNullLanguageEnum',
+        ('BlankNullLanguageStrEnum', ['en', 'None'])
+    ]
+    if DJANGO_VERSION > '3':
+        enum_override_variations += [
+            ('BlankNullLanguageChoices', ['en', 'None']),
+            ('BlankNullLanguageChoices.choices', ['en', 'None'])
+        ]
+
+    for variation in enum_override_variations:
+        expected_hashed_keys = ['en']
+        if isinstance(variation, (list, tuple, )):
+            variation, expected_hashed_keys = variation
+        with mock.patch(
+            'drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES',
+            {'LanguageEnum': f'tests.test_postprocessing.{variation}'}
+        ):
+            load_enum_name_overrides.cache_clear()
+            # Should match after None and blank strings are removed
+            assert list_hash(expected_hashed_keys) in load_enum_name_overrides()
+
+
 @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {
     'LanguageEnum': 'tests.test_postprocessing.NOTEXISTING'
 })
diff --git a/tests/test_warnings.py b/tests/test_warnings.py
index de520c4..4975e70 100644
--- a/tests/test_warnings.py
+++ b/tests/test_warnings.py
@@ -122,7 +122,9 @@ def test_serializer_not_found(capsys):
         pass  # pragma: no cover
 
     generate_schema('x', XViewset)
-    assert 'XViewset: exception raised while getting serializer.' in capsys.readouterr().err
+    assert (
+        'Error [XViewset]: exception raised while getting serializer.'
+    ) in capsys.readouterr().err
 
 
 def test_extend_schema_unknown_class(capsys):
@@ -157,7 +159,7 @@ def test_no_serializer_class_on_apiview(capsys):
             pass  # pragma: no cover
 
     generate_schema('x', view=XView)
-    assert 'XView: unable to guess serializer.' in capsys.readouterr().err
+    assert 'Error [XView]: unable to guess serializer.' in capsys.readouterr().err
 
 
 def test_unable_to_follow_field_source_through_intermediate_property_warning(capsys):
@@ -180,7 +182,7 @@ def test_unable_to_follow_field_source_through_intermediate_property_warning(cap
 
     generate_schema('x', view=XAPIView)
     assert (
-        'XAPIView: XSerializer: could not follow field source through intermediate property'
+        '[XAPIView > XSerializer]: could not follow field source through intermediate property'
     ) in capsys.readouterr().err
 
 
@@ -208,8 +210,8 @@ def test_unable_to_derive_function_type_warning(capsys):
 
     generate_schema('x', view=XAPIView)
     stderr = capsys.readouterr().err
-    assert 'XAPIView: XSerializer: unable to resolve type hint for function "x"' in stderr
-    assert 'XAPIView: XSerializer: unable to resolve type hint for function "get_y"' in stderr
+    assert '[XAPIView > XSerializer]: unable to resolve type hint for function "x"' in stderr
+    assert '[XAPIView > XSerializer]: unable to resolve type hint for function "get_y"' in stderr
 
 
 def test_unable_to_traverse_union_type_hint(capsys):