Skip to content

Commit 3ea1c35

Browse files
committed
feat
1 parent 8c63a7b commit 3ea1c35

3 files changed

Lines changed: 92 additions & 22 deletions

File tree

smart_storages/s3_backend.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,55 @@
44

55
class BaseSpecialS3Storage(S3Boto3Storage):
66
"""
7-
Generic reusable S3 storage backend.
8-
Uses the bucket defined by a Django setting key.
7+
Generic reusable S3 storage backend that provides a robust configuration
8+
hierarchy for bucket name, region, and options.
99
"""
1010

11-
storage_key = None # e.g. "import_export" or "COURSE_IMPORT_EXPORT_BUCKET"
11+
storage_key = None # e.g. "import_export" or "analytics"
1212

1313
def __init__(self, **kwargs):
14-
# Start with sane defaults
15-
options = {"querystring_auth": True}
16-
bucket_name = None
14+
# 1. Validation: Ensure the subclass has set a storage_key
15+
if self.storage_key is None:
16+
raise NotImplementedError(
17+
"Subclasses of BaseSpecialS3Storage must set a 'storage_key' class attribute."
18+
)
19+
20+
# 2. Initialize Options: Start with project-wide/legacy defaults
1721
options = {
1822
"querystring_auth": getattr(settings, "AWS_QUERYSTRING_AUTH", True),
1923
"default_acl": getattr(settings, "AWS_DEFAULT_ACL", None),
2024
"object_parameters": getattr(settings, "AWS_S3_OBJECT_PARAMETERS", {}),
25+
# General region default
26+
"region_name": getattr(settings, "AWS_S3_REGION_NAME", None),
2127
}
2228

23-
# 1. Try STORAGES dict
24-
if hasattr(settings, "STORAGES") and self.storage_key:
29+
bucket_name = None
30+
31+
# 3. Hierarchy for Options and Bucket Name (Prioritizing STORAGES)
32+
if hasattr(settings, "STORAGES"):
2533
storage_conf = settings.STORAGES.get(self.storage_key, {})
34+
35+
# Update base options with OPTIONS from STORAGES (includes region_name)
2636
options.update(storage_conf.get("OPTIONS", {}))
27-
bucket_name = options.get("bucket_name", None)
2837

29-
# 2. Fallback to storage_key setting if bucket_name is None
30-
if bucket_name is None and self.storage_key:
31-
bucket_name = getattr(settings, self.storage_key, None)
38+
# Check for bucket_name within the updated options
39+
bucket_name = options.get("bucket_name", None)
3240

33-
# 3. Final fallback to AWS_STORAGE_BUCKET_NAME if still None
41+
# 4. Fallback for Bucket Name (Directly to AWS_STORAGE_BUCKET_NAME)
42+
# This is the single, final bucket name fallback.
3443
if bucket_name is None:
3544
bucket_name = getattr(settings, "AWS_STORAGE_BUCKET_NAME", None)
3645

46+
# 5. Secondary Fallback for Region (Explicitly check <STORAGE_KEY>_REGION)
47+
# This remains useful for specific regional overrides not in STORAGES.
48+
if options["region_name"] is None:
49+
region_key = f"{self.storage_key.upper()}_REGION"
50+
options["region_name"] = getattr(settings, region_key, None)
51+
52+
# 6. Final Assembly and Override
3753
options["bucket_name"] = bucket_name
3854

39-
# 4. Override with any kwargs
55+
# Override any setting with explicit kwargs passed to the constructor
4056
options.update(kwargs)
4157

4258
super().__init__(**options)

tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
AWS_STORAGE_BUCKET_NAME = "default-bucket"
1010
AWS_QUERYSTRING_AUTH = False
1111
AWS_DEFAULT_ACL = "private"
12+
AWS_S3_REGION_NAME = "us-east-1" # Or any valid region name
1213
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=3600"}
1314

1415
INSTALLED_APPS = ["django.contrib.auth", "django.contrib.contenttypes", "storages", "smart_storages"]

tests/test_storages.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33
from django.test import TestCase
44
from django.conf import settings
55
from storages.backends.s3boto3 import S3Boto3Storage
6-
7-
from smart_storages.s3_backend import BaseSpecialS3Storage
6+
from smart_storages.s3_backend import BaseSpecialS3Storage # Assuming this is the module path
87

98
# Ensure Django settings are configured for standalone test runs
9+
# Assuming 'smart_storages.tests.settings' exists and configures AWS_STORAGE_BUCKET_NAME, STORAGES, etc.
1010
if not settings.configured:
1111
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "smart_storages.tests.settings")
1212

1313

14+
# --- Helper Storage Subclasses ---
15+
16+
class MissingKeyS3Storage(BaseSpecialS3Storage):
17+
# Intentional: storage_key = None
18+
pass
19+
20+
1421
class ImportExportS3Storage(BaseSpecialS3Storage):
1522
storage_key = "import_export"
1623

@@ -19,35 +26,81 @@ class AnalyticsS3Storage(BaseSpecialS3Storage):
1926
storage_key = "analytics"
2027

2128

22-
class DefaultsS3Storage(AnalyticsS3Storage):
23-
storage_key = "none"
24-
29+
# --- Test Suite ---
2530

2631
class TestSpecialStorages(TestCase):
32+
33+
# --- New Tests for Robustness ---
34+
35+
def test_missing_storage_key_raises_error(self):
36+
"""The base class must raise NotImplementedError if storage_key is not set."""
37+
with self.assertRaises(NotImplementedError):
38+
MissingKeyS3Storage()
39+
40+
def test_region_is_picked_up_from_storages_dict(self):
41+
"""The class should pick up the region name from the STORAGES dict."""
42+
storage = AnalyticsS3Storage()
43+
# Assuming settings.STORAGES['analytics']['OPTIONS']['region_name'] is set to 'eu-west-1'
44+
self.assertEqual(storage.region_name, settings.STORAGES["analytics"]["OPTIONS"]["region_name"])
45+
self.assertEqual(storage.bucket_name, settings.STORAGES["analytics"]["OPTIONS"]["bucket_name"])
46+
47+
def test_region_falls_back_to_global_setting(self):
48+
"""If not in STORAGES, region should fallback to AWS_S3_REGION_NAME."""
49+
# For this test, we temporarily set the key to something not in STORAGES
50+
# and assume AWS_S3_REGION_NAME is globally set (e.g., 'us-east-1').
51+
52+
# We'll use ImportExportS3Storage, but temporarily clear its region option for the test
53+
temp_options = settings.STORAGES["import_export"]["OPTIONS"].copy()
54+
temp_options.pop("region_name", None)
55+
settings.STORAGES["import_export"]["OPTIONS"] = temp_options
56+
57+
storage = ImportExportS3Storage()
58+
self.assertEqual(storage.region_name, settings.AWS_S3_REGION_NAME)
59+
60+
# Restore settings to avoid side effects
61+
settings.STORAGES["import_export"]["OPTIONS"]["region_name"] = "us-east-1-specific"
62+
63+
# --- Modified/Existing Tests ---
64+
2765
def test_base_storage_uses_default_bucket(self):
28-
"""Fallback to AWS_STORAGE_BUCKET_NAME if specific bucket setting is missing."""
29-
storage = DefaultsS3Storage()
66+
"""Fallback to AWS_STORAGE_BUCKET_NAME if specific bucket is not in STORAGES."""
67+
68+
# To test the fallback properly, we use a key that we assume exists in STORAGES,
69+
# but does *not* define a 'bucket_name' option.
70+
class TempNoBucketStorage(BaseSpecialS3Storage):
71+
storage_key = "default_fallback_test"
72+
73+
# Temporarily configure a non-existent storage key without a bucket_name
74+
settings.STORAGES["default_fallback_test"] = {"BACKEND": "..."}
75+
76+
storage = TempNoBucketStorage()
77+
3078
self.assertIsInstance(storage, S3Boto3Storage)
3179
self.assertEqual(storage.bucket_name, settings.AWS_STORAGE_BUCKET_NAME)
80+
# Verify defaults from settings are applied
3281
self.assertFalse(storage.querystring_auth)
3382
self.assertEqual(storage.default_acl, "private")
3483
self.assertEqual(storage.object_parameters, {"CacheControl": "max-age=3600"})
84+
# Clean up
85+
del settings.STORAGES["default_fallback_test"]
3586

3687
def test_custom_bucket_is_used(self):
3788
"""The class should pick up its bucket name from the STORAGES dict."""
3889
storage = ImportExportS3Storage()
39-
self.assertEqual(storage.bucket_name, "import_export")
90+
self.assertEqual(storage.bucket_name, settings.STORAGES["import_export"]["OPTIONS"]["bucket_name"])
4091

4192
def test_multiple_subclasses_can_have_different_buckets(self):
4293
"""Each subclass can have its own bucket."""
4394
export_storage = ImportExportS3Storage()
4495
analytics_storage = AnalyticsS3Storage()
96+
4597
self.assertNotEqual(export_storage.bucket_name, analytics_storage.bucket_name)
4698
self.assertEqual(export_storage.bucket_name, settings.STORAGES["import_export"]["OPTIONS"]["bucket_name"])
4799
self.assertEqual(analytics_storage.bucket_name, settings.STORAGES["analytics"]["OPTIONS"]["bucket_name"])
48100

49101
def test_init_allows_overriding_default_kwargs(self):
50102
"""User-provided kwargs override defaults."""
103+
# The default for import_export should be True (assuming settings)
51104
storage = ImportExportS3Storage(querystring_auth=False)
52105
self.assertFalse(storage.querystring_auth)
53106
self.assertEqual(storage.bucket_name, settings.STORAGES["import_export"]["OPTIONS"]["bucket_name"])

0 commit comments

Comments
 (0)