diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index 4e3464af..639c4f7b 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -905,13 +905,20 @@ def deep_import_string(string: str) -> Any: def load_enum_name_overrides(): - return _load_enum_name_overrides(get_language()) + # id() of the current ENUM_NAME_OVERRIDES dict is used (in addition to language) as part + # of the cache key, so a patched settings object (e.g. via SpectacularAPIView's + # custom_settings, serving multiple schemas with different overrides) gets its own cache + # entry instead of reusing whatever was cached for a different settings object. The dict + # itself can't be used directly as a cache key/argument since it (or its values, which may + # be raw choice lists) is not hashable. + return _load_enum_name_overrides(get_language(), id(spectacular_settings.ENUM_NAME_OVERRIDES)) @functools.lru_cache() -def _load_enum_name_overrides(language: str): +def _load_enum_name_overrides(language: str, enum_name_overrides_id: int): + enum_name_overrides = spectacular_settings.ENUM_NAME_OVERRIDES overrides = {} - for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items(): + for name, choices in enum_name_overrides.items(): if isinstance(choices, str): choices = deep_import_string(choices) if not choices: @@ -944,7 +951,7 @@ def _load_enum_name_overrides(language: str): ] overrides[list_hash(hashable_values)] = name - if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides): + if len(enum_name_overrides) != len(overrides): error( 'ENUM_NAME_OVERRIDES has duplication issues. Encountered multiple names ' 'for the same choice set. Enum naming might be unexpected.' diff --git a/tests/test_postprocessing.py b/tests/test_postprocessing.py index eb84dc98..5fe5059d 100644 --- a/tests/test_postprocessing.py +++ b/tests/test_postprocessing.py @@ -157,6 +157,29 @@ class XView(generics.RetrieveAPIView): assert len(schema['components']['schemas']) == 2 +def test_global_enum_naming_override_not_stale_across_settings_objects(no_warnings, clear_caches): + """ + Regression test for #1244. load_enum_name_overrides() used to be cached only on + language, so loading overrides for one ENUM_NAME_OVERRIDES dict would poison the + cache for a *different* ENUM_NAME_OVERRIDES dict with the same language - exactly + what happens serving multiple schemas with per-schema custom_settings, since + SpectacularAPIView's custom_settings context manager doesn't clear this cache. + """ + with mock.patch( + 'drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', + {'LanguageEnum': 'tests.test_postprocessing.language_choices'}, + ): + overrides_a = load_enum_name_overrides() + assert overrides_a == {list_hash(list(language_choices)): 'LanguageEnum'} + + with mock.patch( + 'drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', + {'VoteEnum': 'tests.test_postprocessing.vote_choices'}, + ): + overrides_b = load_enum_name_overrides() + assert overrides_b == {list_hash(list(vote_choices)): 'VoteEnum'} + + @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', { 'LanguageEnum': 'tests.test_postprocessing.blank_null_language_choices' })