From 47e13fb84a0685b8d3cd743b3e15db58ccd46a6e Mon Sep 17 00:00:00 2001 From: Josh Schneier Date: Fri, 5 Aug 2016 16:34:34 +0200 Subject: [PATCH 1/4] Drop Django 1.7 support and add Django 1.10 Remove unnecessary compatability wrappers. --- storages/backends/apache_libcloud.py | 6 ++-- storages/backends/azure_storage.py | 2 +- storages/backends/couchdb.py | 13 ++++---- storages/backends/database.py | 49 ++++++++++++++-------------- storages/backends/dropbox.py | 4 +-- storages/backends/ftp.py | 4 ++- storages/backends/gs.py | 2 +- storages/backends/hashpath.py | 6 ++-- storages/backends/image.py | 24 +++++++------- storages/backends/mogile.py | 35 ++++++++++---------- storages/backends/overwrite.py | 2 +- storages/backends/s3boto.py | 5 ++- storages/backends/s3boto3.py | 7 ++-- storages/backends/sftpstorage.py | 4 ++- storages/backends/symlinkorcopy.py | 2 +- storages/compat.py | 28 ---------------- tests/test_s3boto.py | 10 +----- tests/test_s3boto3.py | 13 ++------ tox.ini | 4 +-- 19 files changed, 94 insertions(+), 126 deletions(-) delete mode 100644 storages/compat.py diff --git a/storages/backends/apache_libcloud.py b/storages/backends/apache_libcloud.py index 4dc4b8b00..a2a5390de 100644 --- a/storages/backends/apache_libcloud.py +++ b/storages/backends/apache_libcloud.py @@ -5,12 +5,12 @@ from django.conf import settings from django.core.files.base import File +from django.core.files.storage import Storage from django.core.exceptions import ImproperlyConfigured -from django.utils.six import string_types +from django.utils.deconstruct import deconstructible +from django.utils.six import string_types, BytesIO from django.utils.six.moves.urllib.parse import urljoin -from storages.compat import BytesIO, deconstructible, Storage - try: from libcloud.storage.providers import get_driver from libcloud.storage.types import ObjectDoesNotExistError, Provider diff --git a/storages/backends/azure_storage.py b/storages/backends/azure_storage.py index e1e4b5651..52328b713 100644 --- a/storages/backends/azure_storage.py +++ b/storages/backends/azure_storage.py @@ -6,7 +6,7 @@ from django.core.files.base import ContentFile from django.core.exceptions import ImproperlyConfigured -from storages.compat import Storage +from django.core.files.storage import Storage try: import azure # noqa diff --git a/storages/backends/couchdb.py b/storages/backends/couchdb.py index 2bcecd8a2..1dcfadee9 100644 --- a/storages/backends/couchdb.py +++ b/storages/backends/couchdb.py @@ -7,9 +7,10 @@ from django.conf import settings from django.core.files import File +from django.core.files.storage import Storage from django.core.exceptions import ImproperlyConfigured - -from storages.compat import urlparse, BytesIO, Storage +from django.utils.six.moves.urllib import parse as urlparse +from django.utils.six import BytesIO try: import couchdb @@ -17,8 +18,8 @@ raise ImproperlyConfigured("Could not load couchdb dependency.\ \nSee http://code.google.com/p/couchdb-python/") -DEFAULT_SERVER= getattr(settings, 'COUCHDB_DEFAULT_SERVER', 'http://couchdb.local:5984') -STORAGE_OPTIONS= getattr(settings, 'COUCHDB_STORAGE_OPTIONS', {}) +DEFAULT_SERVER = getattr(settings, 'COUCHDB_DEFAULT_SERVER', 'http://couchdb.local:5984') +STORAGE_OPTIONS = getattr(settings, 'COUCHDB_STORAGE_OPTIONS', {}) class CouchDBStorage(Storage): @@ -26,9 +27,9 @@ class CouchDBStorage(Storage): CouchDBStorage - a Django Storage class for CouchDB. The CouchDBStorage can be configured in settings.py, e.g.:: - + COUCHDB_STORAGE_OPTIONS = { - 'server': "http://example.org", + 'server': "http://example.org", 'database': 'database_name' } diff --git a/storages/backends/database.py b/storages/backends/database.py index e0057ab16..114bdcf11 100644 --- a/storages/backends/database.py +++ b/storages/backends/database.py @@ -3,9 +3,10 @@ from django.conf import settings from django.core.files import File +from django.core.files.storage import Storage from django.core.exceptions import ImproperlyConfigured - -from storages.compat import urlparse, BytesIO, Storage +from django.utils.six import BytesIO +from django.utils.six.moves.urllib import parse as urlparse try: import pyodbc @@ -18,26 +19,26 @@ class DatabaseStorage(Storage): """ - Class DatabaseStorage provides storing files in the database. + Class DatabaseStorage provides storing files in the database. """ def __init__(self, option=settings.DB_FILES): - """Constructor. - + """Constructor. + Constructs object using dictionary either specified in contucotr or -in settings.DB_FILES. - +in settings.DB_FILES. + @param option dictionary with 'db_table', 'fname_column', -'blob_column', 'size_column', 'base_url' keys. - +'blob_column', 'size_column', 'base_url' keys. + option['db_table'] Table to work with. option['fname_column'] Column in the 'db_table' containing filenames (filenames can contain pathes). Values should be the same as where FileField keeps -filenames. +filenames. It is used to map filename to blob_column. In sql it's simply -used in where clause. +used in where clause. option['blob_column'] Blob column (for example 'image' type), created manually in the 'db_table', used to store image. @@ -46,7 +47,7 @@ def __init__(self, option=settings.DB_FILES): method (another way is to open file and get size) option['base_url'] Url prefix used with filenames. Should be mapped to the view, -that returns an image as result. +that returns an image as result. """ if not option or not all([field in option for field in REQUIRED_FIELDS]): @@ -64,18 +65,18 @@ def __init__(self, option=settings.DB_FILES): self.DATABASE_USER = settings.DATABASE_USER self.DATABASE_PASSWORD = settings.DATABASE_PASSWORD self.DATABASE_HOST = settings.DATABASE_HOST - + self.connection = pyodbc.connect('DRIVER=%s;SERVER=%s;DATABASE=%s;UID=%s;PWD=%s'%(self.DATABASE_ODBC_DRIVER,self.DATABASE_HOST,self.DATABASE_NAME, self.DATABASE_USER, self.DATABASE_PASSWORD) ) self.cursor = self.connection.cursor() def _open(self, name, mode='rb'): - """Open a file from database. - + """Open a file from database. + @param name filename or relative path to file based on base_url. path should contain only "/", but not "\". Apache sends pathes with "/". If there is no such file in the db, returs None """ - + assert mode == 'rb', "You've tried to open binary file without specifying binary mode! You specified: %s"%mode row = self.cursor.execute("SELECT %s from %s where %s = '%s'"%(self.blob_column,self.db_table,self.fname_column,name) ).fetchone() @@ -84,23 +85,23 @@ def _open(self, name, mode='rb'): inMemFile = BytesIO(row[0]) inMemFile.name = name inMemFile.mode = mode - + retFile = File(inMemFile) return retFile def _save(self, name, content): """Save 'content' as file named 'name'. - - @note '\' in path will be converted to '/'. + + @note '\' in path will be converted to '/'. """ - + name = name.replace('\\', '/') binary = pyodbc.Binary(content.read()) size = len(binary) - + #todo: check result and do something (exception?) if failed. if self.exists(name): - self.cursor.execute("UPDATE %s SET %s = ?, %s = ? WHERE %s = '%s'"%(self.db_table,self.blob_column,self.size_column,self.fname_column,name), + self.cursor.execute("UPDATE %s SET %s = ?, %s = ? WHERE %s = '%s'"%(self.db_table,self.blob_column,self.size_column,self.fname_column,name), (binary, size) ) else: self.cursor.execute("INSERT INTO %s VALUES(?, ?, ?)"%(self.db_table), (name, binary, size) ) @@ -110,7 +111,7 @@ def _save(self, name, content): def exists(self, name): row = self.cursor.execute("SELECT %s from %s where %s = '%s'"%(self.fname_column,self.db_table,self.fname_column,name)).fetchone() return row is not None - + def get_available_name(self, name, max_length=None): return name @@ -123,7 +124,7 @@ def url(self, name): if self.base_url is None: raise ValueError("This file is not accessible via a URL.") return urlparse.urljoin(self.base_url, name).replace('\\', '/') - + def size(self, name): row = self.cursor.execute("SELECT %s from %s where %s = '%s'"%(self.size_column,self.db_table,self.fname_column,name)).fetchone() if row is None: diff --git a/storages/backends/dropbox.py b/storages/backends/dropbox.py index fdeb9a70b..040858f89 100644 --- a/storages/backends/dropbox.py +++ b/storages/backends/dropbox.py @@ -14,11 +14,11 @@ from tempfile import SpooledTemporaryFile from shutil import copyfileobj -from django.core.files.base import File from django.core.exceptions import ImproperlyConfigured +from django.core.files.base import File +from django.core.files.storage import Storage from django.utils._os import safe_join -from storages.compat import Storage from storages.utils import setting from dropbox.client import DropboxClient diff --git a/storages/backends/ftp.py b/storages/backends/ftp.py index 296612f7c..c49f9c45e 100644 --- a/storages/backends/ftp.py +++ b/storages/backends/ftp.py @@ -20,9 +20,11 @@ from django.conf import settings from django.core.files.base import File +from django.core.files.storage import Storage from django.core.exceptions import ImproperlyConfigured +from django.utils.six.moves.urllib import parse as urlparse +from django.utils.six import BytesIO -from storages.compat import urlparse, BytesIO, Storage from storages.utils import setting diff --git a/storages/backends/gs.py b/storages/backends/gs.py index 2ee2ccf79..318ab0b84 100644 --- a/storages/backends/gs.py +++ b/storages/backends/gs.py @@ -1,8 +1,8 @@ from django.core.exceptions import ImproperlyConfigured +from django.utils.six import BytesIO from storages.backends.s3boto import S3BotoStorage, S3BotoStorageFile from storages.utils import setting -from storages.compat import BytesIO try: from boto.gs.connection import GSConnection, SubdomainCallingFormat diff --git a/storages/backends/hashpath.py b/storages/backends/hashpath.py index 44343c9ef..252f30fd9 100644 --- a/storages/backends/hashpath.py +++ b/storages/backends/hashpath.py @@ -1,7 +1,9 @@ -import os, hashlib, errno +import errno +import hashlib +import os +from django.core.files.storage import FileSystemStorage from django.utils.encoding import force_text, force_bytes -from storages.compat import FileSystemStorage class HashPathStorage(FileSystemStorage): diff --git a/storages/backends/image.py b/storages/backends/image.py index 637ae8b6b..0be152ad1 100644 --- a/storages/backends/image.py +++ b/storages/backends/image.py @@ -2,7 +2,7 @@ import os from django.core.exceptions import ImproperlyConfigured -from storages.compat import FileSystemStorage +from django.core.files.storage import FileSystemStorage try: from PIL import ImageFile as PILImageFile @@ -14,25 +14,25 @@ class ImageStorage(FileSystemStorage): """ A FileSystemStorage which normalizes extensions for images. - + Comes from http://www.djangosnippets.org/snippets/965/ """ - + def find_extension(self, format): """Normalizes PIL-returned format into a standard, lowercase extension.""" format = format.lower() - + if format == 'jpeg': format = 'jpg' - + return format - + def save(self, name, content, max_length=None): dirname = os.path.dirname(name) basename = os.path.basename(name) - + # Use PIL to determine filetype - + p = PILImageFile.Parser() while 1: data = content.read(1024) @@ -42,14 +42,14 @@ def save(self, name, content, max_length=None): if p.image: im = p.image break - + extension = self.find_extension(im.format) - + # Does the basename already have an extension? If so, replace it. # bare as in without extension bare_basename, _ = os.path.splitext(basename) basename = bare_basename + '.' + extension - + name = os.path.join(dirname, basename) return super(ImageStorage, self).save(name, content) - + diff --git a/storages/backends/mogile.py b/storages/backends/mogile.py index 5a31f663a..61d123c58 100644 --- a/storages/backends/mogile.py +++ b/storages/backends/mogile.py @@ -7,8 +7,7 @@ from django.utils.text import force_text from django.http import HttpResponse, HttpResponseNotFound from django.core.exceptions import ImproperlyConfigured - -from storages.compat import urlparse, BytesIO, Storage +from django.core.files.storage import Storage try: import mogilefs @@ -20,37 +19,37 @@ class MogileFSStorage(Storage): """MogileFS filesystem storage""" def __init__(self, base_url=settings.MEDIA_URL): - + # the MOGILEFS_MEDIA_URL overrides MEDIA_URL if hasattr(settings, 'MOGILEFS_MEDIA_URL'): self.base_url = settings.MOGILEFS_MEDIA_URL else: self.base_url = base_url - + for var in ('MOGILEFS_TRACKERS', 'MOGILEFS_DOMAIN',): if not hasattr(settings, var): raise ImproperlyConfigured("You must define %s to use the MogileFS backend." % var) - + self.trackers = settings.MOGILEFS_TRACKERS self.domain = settings.MOGILEFS_DOMAIN self.client = mogilefs.Client(self.domain, self.trackers) - + def get_mogile_paths(self, filename): - return self.client.get_paths(filename) - + return self.client.get_paths(filename) + # The following methods define the Backend API def filesize(self, filename): raise NotImplemented #return os.path.getsize(self._get_absolute_path(filename)) - + def path(self, filename): paths = self.get_mogile_paths(filename) if paths: return self.get_mogile_paths(filename)[0] else: return None - + def url(self, filename): return urlparse.urljoin(self.base_url, filename).replace('\\', '/') @@ -63,7 +62,7 @@ def exists(self, filename): def save(self, filename, raw_contents, max_length=None): filename = self.get_available_name(filename, max_length) - + if not hasattr(self, 'mogile_class'): self.mogile_class = None @@ -78,8 +77,8 @@ def save(self, filename, raw_contents, max_length=None): def delete(self, filename): self.client.delete(filename) - - + + def serve_mogilefs_file(request, key=None): """ Called when a user requests an image. @@ -90,21 +89,21 @@ def serve_mogilefs_file(request, key=None): client = mogilefs.Client(settings.MOGILEFS_DOMAIN, settings.MOGILEFS_TRACKERS) if hasattr(settings, "SERVE_WITH_PERLBAL") and settings.SERVE_WITH_PERLBAL: # we're reproxying with perlbal - + # check the path cache - + path = cache.get(key) if not path: path = client.get_paths(key) cache.set(key, path, 60) - + if path: response = HttpResponse(content_type=mimetype) response['X-REPROXY-URL'] = path[0] else: response = HttpResponseNotFound() - + else: # we don't have perlbal, let's just serve the image via django file_data = client[key] @@ -112,5 +111,5 @@ def serve_mogilefs_file(request, key=None): response = HttpResponse(file_data, mimetype=mimetype) else: response = HttpResponseNotFound() - + return response diff --git a/storages/backends/overwrite.py b/storages/backends/overwrite.py index 84969bddd..9d940bc7e 100644 --- a/storages/backends/overwrite.py +++ b/storages/backends/overwrite.py @@ -1,4 +1,4 @@ -from storages.compat import FileSystemStorage +from django.core.file.storage import FileSystemStorage class OverwriteStorage(FileSystemStorage): diff --git a/storages/backends/s3boto.py b/storages/backends/s3boto.py index e5dc1fbd2..47d3ffa6d 100644 --- a/storages/backends/s3boto.py +++ b/storages/backends/s3boto.py @@ -6,8 +6,12 @@ from tempfile import SpooledTemporaryFile from django.core.files.base import File +from django.core.files.storage import Storage from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation +from django.utils.deconstruct import deconstructible from django.utils.encoding import force_text, smart_str, filepath_to_uri, force_bytes +from django.utils.six import BytesIO +from django.utils.six.moves.urllib import parse as urlparse try: from boto import __version__ as boto_version @@ -20,7 +24,6 @@ "See https://github.com/boto/boto") from storages.utils import setting -from storages.compat import urlparse, BytesIO, deconstructible, Storage boto_version_info = tuple([int(i) for i in boto_version.split('-')[0].split('.')]) diff --git a/storages/backends/s3boto3.py b/storages/backends/s3boto3.py index e6bf08ceb..9f06d122f 100644 --- a/storages/backends/s3boto3.py +++ b/storages/backends/s3boto3.py @@ -4,9 +4,13 @@ from gzip import GzipFile from tempfile import SpooledTemporaryFile -from django.core.files.base import File from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation +from django.core.files.base import File +from django.core.files.storage import Storage +from django.utils.deconstruct import deconstructible from django.utils.encoding import force_text, smart_str, filepath_to_uri, force_bytes +from django.utils.six.moves.urllib import parse as urlparse +from django.utils.six import BytesIO from django.utils.timezone import localtime try: @@ -19,7 +23,6 @@ "See https://github.com/boto/boto3") from storages.utils import setting -from storages.compat import urlparse, BytesIO, deconstructible, Storage boto3_version_info = tuple([int(i) for i in boto3_version.split('-')[0].split('.')]) diff --git a/storages/backends/sftpstorage.py b/storages/backends/sftpstorage.py index f219b5950..c39425ded 100644 --- a/storages/backends/sftpstorage.py +++ b/storages/backends/sftpstorage.py @@ -14,8 +14,10 @@ from django.conf import settings from django.core.files.base import File +from django.core.files.storage import Storage +from django.utils.six import BytesIO +from django.utils.six.moves.urllib import parse as urlparse -from storages.compat import urlparse, BytesIO, Storage from storages.utils import setting diff --git a/storages/backends/symlinkorcopy.py b/storages/backends/symlinkorcopy.py index 881042358..9216a2db4 100644 --- a/storages/backends/symlinkorcopy.py +++ b/storages/backends/symlinkorcopy.py @@ -1,7 +1,7 @@ import os from django.conf import settings -from storages.compat import FileSystemStorage +from django.core.files.storage import FileSystemStorage __doc__ = """ I needed to efficiently create a mirror of a directory tree (so that diff --git a/storages/compat.py b/storages/compat.py deleted file mode 100644 index 1ac3e1d0a..000000000 --- a/storages/compat.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.utils.six.moves.urllib import parse as urlparse -from django.utils.six import BytesIO -import django - -try: - from django.utils.deconstruct import deconstructible -except ImportError: # Django 1.7+ migrations - deconstructible = lambda klass, *args, **kwargs: klass - -# Storage only accepts `max_length` in 1.8+ -if django.VERSION >= (1, 8): - from django.core.files.storage import Storage, FileSystemStorage -else: - from django.core.files.storage import Storage as DjangoStorage - from django.core.files.storage import FileSystemStorage as DjangoFileSystemStorage - - class StorageMixin(object): - def save(self, name, content, max_length=None): - return super(StorageMixin, self).save(name, content) - - def get_available_name(self, name, max_length=None): - return super(StorageMixin, self).get_available_name(name) - - class Storage(StorageMixin, DjangoStorage): - pass - - class FileSystemStorage(StorageMixin, DjangoFileSystemStorage): - pass diff --git a/tests/test_s3boto.py b/tests/test_s3boto.py index a56598f21..e19ed92b9 100644 --- a/tests/test_s3boto.py +++ b/tests/test_s3boto.py @@ -1,4 +1,3 @@ -import unittest try: from unittest import mock except ImportError: # Python 3.2 and below @@ -8,13 +7,12 @@ from django.test import TestCase from django.core.files.base import ContentFile -import django +from django.utils.six.moves.urllib import parse as urlparse from boto.exception import S3ResponseError from boto.s3.key import Key from boto.utils import parse_ts, ISO8601 -from storages.compat import urlparse from storages.backends import s3boto __all__ = ( @@ -329,9 +327,3 @@ def test_new_file_modified_time(self): self.storage.save(name, content) self.assertEqual(self.storage.modified_time(name), parse_ts(utcnow.strftime(ISO8601))) - - @unittest.skipIf(django.VERSION >= (1, 8), 'Only test backward compat of max_length for versions before 1.8') - def test_max_length_compat_okay(self): - self.storage.file_overwrite = False - self.storage.exists = lambda name: False - self.storage.get_available_name('gogogo', max_length=255) diff --git a/tests/test_s3boto3.py b/tests/test_s3boto3.py index 5e3370a2e..fbfdfdeae 100644 --- a/tests/test_s3boto3.py +++ b/tests/test_s3boto3.py @@ -1,19 +1,17 @@ +from datetime import datetime import gzip -import unittest try: from unittest import mock except ImportError: # Python 3.2 and below import mock -from datetime import datetime, timedelta, tzinfo from django.test import TestCase from django.core.files.base import ContentFile +from django.utils.six.moves.urllib import parse as urlparse from django.utils.timezone import is_aware, utc -import django from botocore.exceptions import ClientError -from storages.compat import urlparse from storages.backends import s3boto3 __all__ = ( @@ -315,13 +313,6 @@ def test_generated_url_is_encoded(self): "/whacky%20%26%20filename.mp4") self.assertFalse(self.storage.bucket.meta.client.generate_presigned_url.called) - @unittest.skipIf(django.VERSION >= (1, 8), - 'Only test backward compat of max_length for versions before 1.8') - def test_max_length_compat_okay(self): - self.storage.file_overwrite = False - self.storage.exists = lambda name: False - self.storage.get_available_name('gogogo', max_length=255) - def test_strip_signing_parameters(self): expected = 'http://bucket.s3-aws-region.amazonaws.com/foo/bar' self.assertEquals(self.storage._strip_signing_parameters( diff --git a/tox.ini b/tox.ini index 4eac06886..9c1a1e70f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = - {py27,py33,py34}-django17, {py27,py33,py34,py35}-django18, {py27,py34,py35}-django19 + {py27,py34,py35}-django110 [testenv] @@ -11,9 +11,9 @@ setenv = PYTHONDONTWRITEBYTECODE=1 DJANGO_SETTINGS_MODULE=tests.settings deps = - django17: Django>=1.7, <1.8 django18: Django>=1.8, <1.9 django19: Django>=1.9, <1.10 + django110: Django>=1.10, <1.11 py27: mock==1.0.1 boto>=2.32.0 pytest-cov==2.2.1 From e8009b207b0a139fe05d3a5f40b52e96f87d1ab0 Mon Sep 17 00:00:00 2001 From: Josh Schneier Date: Fri, 5 Aug 2016 16:07:20 +0100 Subject: [PATCH 2/4] Tell Travis to use the correct envs --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 081ae5ad0..205ed3a13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,6 @@ python: - 3.5 env: - - TOX_ENV=py27-django17 - - TOX_ENV=py33-django17 - - TOX_ENV=py34-django17 - TOX_ENV=py27-django18 - TOX_ENV=py33-django18 - TOX_ENV=py34-django18 @@ -15,6 +12,9 @@ env: - TOX_ENV=py27-django19 - TOX_ENV=py34-django19 - TOX_ENV=py35-django19 + - TOX_ENV=py27-django110 + - TOX_ENV=py34-django110 + - TOX_ENV=py35-django110 before_install: - pip install codecov From 20a2d2c5bed0bf13835fe5c8a00ce9346025f03d Mon Sep 17 00:00:00 2001 From: Josh Schneier Date: Fri, 5 Aug 2016 16:29:01 +0100 Subject: [PATCH 3/4] Update CHANGELOG --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4aad1c29d..b2a1b0b0c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,8 +4,10 @@ django-storages change log 1.5.1 (XXXX-XX-XX) ****************** +* **Drop support for Django 1.7** (`#185`_) * Update ``S3Boto3Storage`` for Django 1.10 (`#181`_) (``get_modified_time`` and ``get_accessed_time``) thanks @JshWright +.. _#185: https://github.com/jschneier/django-storages/pull/185 .. _#181: https://github.com/jschneier/django-storages/pull/181 1.5.0 (2016-08-02) From 90e9b481fed1ffd5aff9349d62399d80573bab46 Mon Sep 17 00:00:00 2001 From: Josh Schneier Date: Fri, 5 Aug 2016 16:30:58 +0100 Subject: [PATCH 4/4] Fix OverwriteStorage typo --- storages/backends/overwrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storages/backends/overwrite.py b/storages/backends/overwrite.py index 9d940bc7e..f4687dae5 100644 --- a/storages/backends/overwrite.py +++ b/storages/backends/overwrite.py @@ -1,4 +1,4 @@ -from django.core.file.storage import FileSystemStorage +from django.core.files.storage import FileSystemStorage class OverwriteStorage(FileSystemStorage):