From bfab6683478b06af6f65094c1f3c3599f34718e1 Mon Sep 17 00:00:00 2001 From: varun Date: Sat, 15 Aug 2026 23:20:46 +0530 Subject: [PATCH] fix: key enum_name_overrides cache on settings object identity, not just language load_enum_name_overrides() was cached only on language via functools.lru_cache, but it internally reads the mutable global spectacular_settings.ENUM_NAME_OVERRIDES. SpectacularAPIView's custom_settings context manager patches this global temporarily to serve multiple schemas with different enum overrides from the same process. Since the cache didn't account for that, loading overrides for one ENUM_NAME_OVERRIDES dict would return the stale cached result for a different one with the same language, once both had been requested. Add id(spectacular_settings.ENUM_NAME_OVERRIDES) to the cache key. The dict itself (or its values, which can be raw choice lists) isn't hashable, so it can't be used as a cache key/argument directly - but each distinct settings profile is a genuinely distinct dict object for the lifetime of the process, so identity is a safe and simple proxy. Fixes #1244 --- drf_spectacular/plumbing.py | 15 +++++++++++---- tests/test_postprocessing.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) 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' })