Repository: boto/boto3
Issue: S3 object property "size" not available

This bug appears in 1.2.2 version.

> pip freeze | grep boto3
> boto3==1.2.2
> 
> python
> 
> > > import boto3
> > > s3 = boto3.resource('s3')
> > > object_summary = s3.ObjectSummary('bucket_name','key')
> > > object_summary.size
> > > Traceback (most recent call last):
> > > File "", line 1, in 
> > > File "/usr/local/lib/python2.7/site-packages/boto3/resources/factory.py", line 212, in property_loader
> > > '{0} has no load method'.format(self.class.name))
> > > boto3.exceptions.ResourceLoadException: s3.ObjectSummary has no load method


Changed files:
tests/unit/s3/test_inject.py, boto3/s3/inject.py, tests/functional/test_s3.py, boto3/session.py

diff --git a/boto3/s3/inject.py b/boto3/s3/inject.py
index 02a7a68d..23f1c8c9 100644
--- a/boto3/s3/inject.py
+++ b/boto3/s3/inject.py
@@ -34,6 +34,10 @@ def inject_object_methods(class_attributes, **kwargs):
         class_attributes, 'download_file', object_download_file)
 
 
+def inject_object_summary_methods(class_attributes, **kwargs):
+    utils.inject_attribute(class_attributes, 'load', object_summary_load)
+
+
 def bucket_load(self, *args, **kwargs):
     """Calls s3.Client.list_buckets() to update the attributes of the Bucket resource."""
     # The docstring above is phrased this way to match what the autogenerated
@@ -51,6 +55,15 @@ def bucket_load(self, *args, **kwargs):
                           'ListBuckets')
 
 
+def object_summary_load(self, *args, **kwargs):
+    """Calls s3.Client.head_object to update the attributes of the ObjectSummary resource."""
+    response = self.meta.client.head_object(
+        Bucket=self.bucket_name, Key=self.key)
+    if 'ContentLength' in response:
+        response['Size'] = response.pop('ContentLength')
+    self.meta.data = response
+
+
 def upload_file(self, Filename, Bucket, Key, ExtraArgs=None,
                 Callback=None, Config=None):
     """Upload a file to an S3 object.
diff --git a/boto3/session.py b/boto3/session.py
index fbc70737..554958e1 100644
--- a/boto3/session.py
+++ b/boto3/session.py
@@ -394,6 +394,10 @@ class Session(object):
             'creating-resource-class.s3.Object',
             boto3.utils.lazy_call(
                 'boto3.s3.inject.inject_object_methods'))
+        self._session.register(
+            'creating-resource-class.s3.ObjectSummary',
+            boto3.utils.lazy_call(
+                'boto3.s3.inject.inject_object_summary_methods'))
 
         # DynamoDb customizations
         self._session.register(
diff --git a/tests/functional/test_s3.py b/tests/functional/test_s3.py
index 31c565af..03afe472 100644
--- a/tests/functional/test_s3.py
+++ b/tests/functional/test_s3.py
@@ -12,6 +12,8 @@
 # language governing permissions and limitations under the License.
 from tests import unittest
 
+from botocore.stub import Stubber
+
 import boto3.session
 
 
@@ -43,3 +45,44 @@ class TestS3MethodInjection(unittest.TestCase):
                         'upload_file was not injected onto S3 object')
         self.assertTrue(hasattr(obj, 'download_file'),
                         'download_file was not injected onto S3 object')
+
+
+class TestS3ObjectSummary(unittest.TestCase):
+    def setUp(self):
+        self.session = boto3.session.Session(
+            aws_access_key_id='foo', aws_secret_access_key='bar',
+            region_name='us-west-2')
+        self.s3 = self.session.resource('s3')
+        self.obj_summary = self.s3.ObjectSummary('my_bucket', 'my_key')
+        self.obj_summary_size = 12
+        self.stubber = Stubber(self.s3.meta.client)
+        self.stubber.activate()
+        self.stubber.add_response(
+            method='head_object',
+            service_response={
+                'ContentLength': self.obj_summary_size, 'ETag': 'my-etag',
+                'ContentType': 'binary'
+            },
+            expected_params={
+                'Bucket': 'my_bucket',
+                'Key': 'my_key'
+            }
+        )
+
+    def tearDown(self):
+        self.stubber.deactivate()
+
+    def test_has_load(self):
+        self.assertTrue(hasattr(self.obj_summary, 'load'),
+                        'load() was not injected onto ObjectSummary resource.')
+
+    def test_autoloads_correctly(self):
+        # In HeadObject the parameter returned is ContentLength, this
+        # should get mapped to Size of ListObject since the resource uses
+        # the shape returned to by ListObjects.
+        self.assertEqual(self.obj_summary.size, self.obj_summary_size)
+
+    def test_cannot_access_other_non_related_parameters(self):
+        # Even though an HeadObject was used to load this, it should
+        # only expose the attributes from its shape defined in ListObjects.
+        self.assertFalse(hasattr(self.obj_summary, 'content_length'))
diff --git a/tests/unit/s3/test_inject.py b/tests/unit/s3/test_inject.py
index 666a2381..964dcd48 100644
--- a/tests/unit/s3/test_inject.py
+++ b/tests/unit/s3/test_inject.py
@@ -111,3 +111,24 @@ class TestObjectTransferMethods(unittest.TestCase):
         self.obj.meta.client.download_file.assert_called_with(
             Bucket=self.obj.bucket_name, Key=self.obj.key, Filename='foo',
             ExtraArgs=None, Callback=None, Config=None)
+
+
+class TestObejctSummaryLoad(unittest.TestCase):
+    def setUp(self):
+        self.client = mock.Mock()
+        self.resource = mock.Mock()
+        self.resource.meta.client = self.client
+        self.head_object_response = {
+            'ContentLength': 5, 'ETag': 'my-etag'
+        }
+        self.client.head_object.return_value = self.head_object_response
+
+    def test_object_summary_load(self):
+        inject.object_summary_load(self.resource)
+        self.assertEqual(
+            self.resource.meta.data, {'Size': 5, 'ETag': 'my-etag'})
+
+    def test_can_handle_missing_content_length(self):
+        self.head_object_response.pop('ContentLength')
+        inject.object_summary_load(self.resource)
+        self.assertEqual(self.resource.meta.data, {'ETag': 'my-etag'})