From 5cdcbb203e19c65fa309732230baf060fbf82fb6 Mon Sep 17 00:00:00 2001 From: Alexandre Spaeth Date: Tue, 9 May 2023 14:32:10 -0700 Subject: [PATCH] Refactor how we get the default storage class --- docs/configuration.rst | 13 +++++++++- imagekit/cachefiles/__init__.py | 17 +++++------- imagekit/conf.py | 8 +++--- imagekit/utils.py | 16 ++++++++++++ tests/test_settings.py | 46 +++++++++++++++++++++++++++++++-- tests/test_utils.py | 42 ++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 tests/test_utils.py diff --git a/docs/configuration.rst b/docs/configuration.rst index 236191e3..84636cee 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -21,12 +21,23 @@ Settings :default: ``None`` + Starting with Django 4.2, if you defined ``settings.STORAGES``: + the Django storage backend alias to retrieve the storage instance defined + in your settings, as described in the `Django file storage documentation`_. + If no value is provided for ``IMAGEKIT_DEFAULT_FILE_STORAGE``, + and none is specified by the spec definition, the ``default`` file storage + will be used. + + Before Django 4.2, or if ``settings.STORAGES`` is not defined: The qualified class name of a Django storage backend to use to save the cached images. If no value is provided for ``IMAGEKIT_DEFAULT_FILE_STORAGE``, and none is specified by the spec definition, `your default file storage`__ will be used. +.. _`Django file storage documentation`: https://docs.djangoproject.com/en/dev/ref/files/storage/ + + .. attribute:: IMAGEKIT_DEFAULT_CACHEFILE_BACKEND :default: ``'imagekit.cachefiles.backends.Simple'`` @@ -52,7 +63,7 @@ Settings The cache is then used to store information like the state of cached images (i.e. validated or not). -.. _`Django cache section`: https://docs.djangoproject.com/en/1.8/topics/cache/#accessing-the-cache +.. _`Django cache section`: https://docs.djangoproject.com/en/dev/topics/cache/#accessing-the-cache .. attribute:: IMAGEKIT_CACHE_TIMEOUT diff --git a/imagekit/cachefiles/__init__.py b/imagekit/cachefiles/__init__.py index 7326ebd9..3d036fd1 100644 --- a/imagekit/cachefiles/__init__.py +++ b/imagekit/cachefiles/__init__.py @@ -10,7 +10,9 @@ from ..files import BaseIKFile from ..registry import generator_registry from ..signals import content_required, existence_required -from ..utils import generate, get_by_qname, get_logger, get_singleton +from ..utils import ( + generate, get_by_qname, get_logger, get_singleton, get_storage +) class ImageCacheFile(BaseIKFile, ImageFile): @@ -44,8 +46,7 @@ def __init__(self, generator, name=None, storage=None, cachefile_backend=None, c self.name = name storage = (callable(storage) and storage()) or storage or \ - getattr(generator, 'cachefile_storage', None) or get_singleton( - settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') + getattr(generator, 'cachefile_storage', None) or get_storage() self.cachefile_backend = ( cachefile_backend or getattr(generator, 'cachefile_backend', None) @@ -156,20 +157,14 @@ def __getstate__(self): # remove storage from state as some non-FileSystemStorage can't be # pickled - settings_storage = get_singleton( - settings.IMAGEKIT_DEFAULT_FILE_STORAGE, - 'file storage backend' - ) + settings_storage = get_storage() if state['storage'] == settings_storage: state.pop('storage') return state def __setstate__(self, state): if 'storage' not in state: - state['storage'] = get_singleton( - settings.IMAGEKIT_DEFAULT_FILE_STORAGE, - 'file storage backend' - ) + state['storage'] = get_storage() self.__dict__.update(state) def __repr__(self): diff --git a/imagekit/conf.py b/imagekit/conf.py index beadde49..786aeb34 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -37,7 +37,9 @@ def configure_cache_timeout(self, value): def configure_default_file_storage(self, value): if value is None: try: - value = settings.STORAGES["default"]["BACKEND"] - except AttributeError: - value = settings.DEFAULT_FILE_STORAGE + from django.conf import DEFAULT_STORAGE_ALIAS + except ImportError: # Django < 4.2 + return settings.DEFAULT_FILE_STORAGE + else: + return DEFAULT_STORAGE_ALIAS return value diff --git a/imagekit/utils.py b/imagekit/utils.py index 3f72c297..08f4aac2 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -125,6 +125,22 @@ def get_cache(): return caches[settings.IMAGEKIT_CACHE_BACKEND] +def get_storage(): + try: + from django.core.files.storage import storages, InvalidStorageError + except ImportError: # Django < 4.2 + return get_singleton( + settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend' + ) + else: + try: + return storages[settings.IMAGEKIT_DEFAULT_FILE_STORAGE] + except InvalidStorageError: + return get_singleton( + settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend' + ) + + def sanitize_cache_key(key): if settings.IMAGEKIT_USE_MEMCACHED_SAFE_CACHE_KEY: # Memcached keys can't contain whitespace or control characters. diff --git a/tests/test_settings.py b/tests/test_settings.py index d6aa2674..2cf49301 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -2,6 +2,7 @@ from django.test import override_settings import pytest from imagekit.conf import ImageKitConf, settings +from imagekit.utils import get_storage @pytest.mark.skipif( @@ -17,7 +18,7 @@ def test_custom_storages(): }, ): conf = ImageKitConf() - assert conf.configure_default_file_storage(None) == "tests.utils.CustomStorage" + assert conf.configure_default_file_storage(None) == "default" @pytest.mark.skipif( @@ -29,4 +30,45 @@ def test_custom_default_file_storage(): # If we don’t remove this, Django 4.2 will keep the old value. del settings.STORAGES conf = ImageKitConf() - assert conf.configure_default_file_storage(None) == "tests.utils.CustomStorage" + + if django.VERSION >= (4, 2): + assert conf.configure_default_file_storage(None) == "default" + else: + assert ( + conf.configure_default_file_storage(None) == "tests.utils.CustomStorage" + ) + + +def test_get_storage_default(): + from django.core.files.storage import FileSystemStorage + + assert isinstance(get_storage(), FileSystemStorage) + + +@pytest.mark.skipif( + django.VERSION >= (5, 1), + reason="DEFAULT_FILE_STORAGE is removed in Django 5.1.", +) +def test_get_storage_custom_path(): + from tests.utils import CustomStorage + + with override_settings(IMAGEKIT_DEFAULT_FILE_STORAGE="tests.utils.CustomStorage"): + assert isinstance(get_storage(), CustomStorage) + + +@pytest.mark.skipif( + django.VERSION < (4, 2), + reason="STORAGES was introduced in Django 4.2", +) +def test_get_storage_custom_key(): + from tests.utils import CustomStorage + + with override_settings( + STORAGES={ + "custom": { + "BACKEND": "tests.utils.CustomStorage", + } + }, + IMAGEKIT_DEFAULT_FILE_STORAGE="custom", + ): + assert isinstance(get_storage(), CustomStorage) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..0bac5dc8 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,42 @@ +import django +from django.test import override_settings +import pytest +from imagekit.utils import get_storage + + +def test_get_storage_default(): + from django.core.files.storage import default_storage + + if django.VERSION >= (4, 2): + assert get_storage() == default_storage + else: + assert isinstance(get_storage(), type(default_storage._wrapped)) + + +@pytest.mark.skipif( + django.VERSION >= (5, 1), + reason="DEFAULT_FILE_STORAGE is removed in Django 5.1.", +) +def test_get_storage_custom_import_path(): + from tests.utils import CustomStorage + + with override_settings(IMAGEKIT_DEFAULT_FILE_STORAGE="tests.utils.CustomStorage"): + assert isinstance(get_storage(), CustomStorage) + + +@pytest.mark.skipif( + django.VERSION < (4, 2), + reason="STORAGES was introduced in Django 4.2", +) +def test_get_storage_custom_key(): + from tests.utils import CustomStorage + + with override_settings( + STORAGES={ + "custom": { + "BACKEND": "tests.utils.CustomStorage", + } + }, + IMAGEKIT_DEFAULT_FILE_STORAGE="custom", + ): + assert isinstance(get_storage(), CustomStorage)