Repository: boto/boto

Issue:
S3 storage class defaults to STANDARD
If I have an object that has transitioned to glacier, I can get two different values for the storage class depending on how I create the Key object:

``` python
>>> s3 = connect_s3()
>>> bucket = s3.get_bucket('mybucket')
>>> list(bucket)[0].storage_class
u'GLACIER'
>>> bucket.get_key('mykey').storage_class
'STANDARD'
```

This is because the `Key` explicitly sets the storage_class to 'STANDARD' in its `__init__`:

``` python
    def __init__(self, bucket=None, name=None):
        self.bucket = bucket
        self.name = name
        self.metadata = {}
        ...
        self.storage_class = 'STANDARD'
```

I'm wondering if there's a way to make this consistent and still maintain backwards compatibility (is it as straightforward as initially setting `self.storage_class = None`?



Discussion:

Resolving patch:
diff --git a/boto/s3/key.py b/boto/s3/key.py
--- a/boto/s3/key.py
+++ b/boto/s3/key.py
@@ -116,7 +116,7 @@ def __init__(self, bucket=None, name=None):
         self.is_latest = False
         self.last_modified = None
         self.owner = None
-        self.storage_class = 'STANDARD'
+        self._storage_class = None
         self.path = None
         self.resp = None
         self.mode = None
@@ -187,6 +187,23 @@ def _set_base64md5(self, value):
 
     base64md5 = property(_get_base64md5, _set_base64md5);
 
+    def _get_storage_class(self):
+        if self._storage_class is None and self.bucket:
+            # Attempt to fetch storage class
+            list_items = list(self.bucket.list(self.name.encode('utf-8')))
+            if len(list_items):
+                self._storage_class = list_items[0].storage_class
+            else:
+                # Key is not yet saved? Just use default...
+                self._storage_class = 'STANDARD'
+
+        return self._storage_class
+
+    def _set_storage_class(self, value):
+        self._storage_class = value
+
+    storage_class = property(_get_storage_class, _set_storage_class)
+
     def get_md5_from_hexdigest(self, md5_hexdigest):
         """
         A utility function to create the 2-tuple (md5hexdigest, base64md5)
@@ -877,7 +894,9 @@ def sender(http_conn, method, path, data, headers):
         for header in find_matching_headers('User-Agent', headers):
             del headers[header]
         headers['User-Agent'] = UserAgent
-        if self.storage_class != 'STANDARD':
+        # If storage_class is None, then a user has not explicitly requested
+        # a storage class, so we can assume STANDARD here
+        if self._storage_class not in [None, 'STANDARD']:
             headers[provider.storage_class_header] = self.storage_class
         if find_matching_headers('Content-Encoding', headers):
             self.content_encoding = merge_headers_by_name(


Test patch:
diff --git a/tests/unit/s3/test_key.py b/tests/unit/s3/test_key.py
--- a/tests/unit/s3/test_key.py
+++ b/tests/unit/s3/test_key.py
@@ -75,6 +75,31 @@ def test_delete_key_return_key(self):
         key = b.delete_key('fookey')
         self.assertIsNotNone(key)
 
+    def test_storage_class(self):
+        self.set_http_response(status_code=200)
+        b = Bucket(self.service_connection, 'mybucket')
+        k = b.get_key('fookey')
+
+        # Mock out the bucket object - we really only care about calls
+        # to list.
+        k.bucket = mock.MagicMock()
+
+        # Default behavior doesn't call list
+        k.set_contents_from_string('test')
+        k.bucket.list.assert_not_called()
+
+        # Direct access calls list to get the real value if unset,
+        # and still defaults to STANDARD if unavailable.
+        sc_value = k.storage_class
+        self.assertEqual(sc_value, 'STANDARD')
+        k.bucket.list.assert_called_with(k.name.encode('utf-8'))
+        k.bucket.list.reset_mock()
+
+        # Setting manually doesn't call list
+        k.storage_class = 'GLACIER'
+        k.set_contents_from_string('test')
+        k.bucket.list.assert_not_called()
+
 
 def counter(fn):
     def _wrapper(*args, **kwargs):