Repository: googleapis/google-cloud-python

Issue:
Should Bucket.copy_blob be moved over to Blob?
/cc @tseaver 



Discussion:
My inclination is to treat copy and rename operations as more closely associated with "containers" than with "items".

So then we should move `Blob.rename` to `Bucket`?

@tseaver Bump

I'd be fine with having `Blob.rename` move to `Bucket.rename_blob`.


Resolving patch:
diff --git a/gcloud/storage/blob.py b/gcloud/storage/blob.py
--- a/gcloud/storage/blob.py
+++ b/gcloud/storage/blob.py
@@ -227,32 +227,6 @@ def exists(self, client=None):
         except NotFound:
             return False
 
-    def rename(self, new_name, client=None):
-        """Renames this blob using copy and delete operations.
-
-        Effectively, copies blob to the same bucket with a new name, then
-        deletes the blob.
-
-        .. warning::
-          This method will first duplicate the data and then delete the
-          old blob.  This means that with very large objects renaming
-          could be a very (temporarily) costly or a very slow operation.
-
-        :type new_name: string
-        :param new_name: The new name for this blob.
-
-        :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
-        :param client: Optional. The client to use.  If not passed, falls back
-                       to the ``client`` stored on the blob's bucket.
-
-        :rtype: :class:`Blob`
-        :returns: The newly-copied blob.
-        """
-        new_blob = self.bucket.copy_blob(self, self.bucket, new_name,
-                                         client=client)
-        self.delete(client=client)
-        return new_blob
-
     def delete(self, client=None):
         """Deletes a blob from Cloud Storage.
 
diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py
--- a/gcloud/storage/bucket.py
+++ b/gcloud/storage/bucket.py
@@ -434,7 +434,7 @@ def copy_blob(self, blob, destination_bucket, new_name=None,
                   client=None):
         """Copy the given blob to the given bucket, optionally with a new name.
 
-        :type blob: string or :class:`gcloud.storage.blob.Blob`
+        :type blob: :class:`gcloud.storage.blob.Blob`
         :param blob: The blob to be copied.
 
         :type destination_bucket: :class:`gcloud.storage.bucket.Bucket`
@@ -461,6 +461,35 @@ def copy_blob(self, blob, destination_bucket, new_name=None,
         new_blob._set_properties(copy_result)
         return new_blob
 
+    def rename_blob(self, blob, new_name, client=None):
+        """Rename the given blob using copy and delete operations.
+
+        Effectively, copies blob to the same bucket with a new name, then
+        deletes the blob.
+
+        .. warning::
+
+          This method will first duplicate the data and then delete the
+          old blob.  This means that with very large objects renaming
+          could be a very (temporarily) costly or a very slow operation.
+
+        :type blob: :class:`gcloud.storage.blob.Blob`
+        :param blob: The blob to be renamed.
+
+        :type new_name: string
+        :param new_name: The new name for this blob.
+
+        :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
+        :param client: Optional. The client to use.  If not passed, falls back
+                       to the ``client`` stored on the current bucket.
+
+        :rtype: :class:`Blob`
+        :returns: The newly-renamed blob.
+        """
+        new_blob = self.copy_blob(blob, self, new_name, client=client)
+        blob.delete(client=client)
+        return new_blob
+
     @property
     def cors(self):
         """Retrieve CORS policies configured for this bucket.


Test patch:
diff --git a/gcloud/storage/test_blob.py b/gcloud/storage/test_blob.py
--- a/gcloud/storage/test_blob.py
+++ b/gcloud/storage/test_blob.py
@@ -232,21 +232,6 @@ def test_exists_hit(self):
         bucket._blobs[BLOB_NAME] = 1
         self.assertTrue(blob.exists())
 
-    def test_rename(self):
-        BLOB_NAME = 'blob-name'
-        NEW_NAME = 'new-name'
-        connection = _Connection()
-        client = _Client(connection)
-        bucket = _Bucket(client=client)
-        blob = self._makeOne(BLOB_NAME, bucket=bucket)
-        bucket._blobs[BLOB_NAME] = 1
-        new_blob = blob.rename(NEW_NAME)
-        self.assertEqual(blob.name, BLOB_NAME)
-        self.assertEqual(new_blob.name, NEW_NAME)
-        self.assertFalse(BLOB_NAME in bucket._blobs)
-        self.assertEqual(bucket._deleted, [(BLOB_NAME, None)])
-        self.assertTrue(NEW_NAME in bucket._blobs)
-
     def test_delete(self):
         from six.moves.http_client import NOT_FOUND
         BLOB_NAME = 'blob-name'
@@ -1058,11 +1043,6 @@ def __init__(self, client=None):
         self._copied = []
         self._deleted = []
 
-    def copy_blob(self, blob, destination_bucket, new_name, client=None):
-        self._copied.append((blob, destination_bucket, new_name, client))
-        destination_bucket._blobs[new_name] = self._blobs[blob.name]
-        return blob.__class__(new_name, bucket=destination_bucket)
-
     def delete_blob(self, blob_name, client=None):
         del self._blobs[blob_name]
         self._deleted.append((blob_name, client))
diff --git a/gcloud/storage/test_bucket.py b/gcloud/storage/test_bucket.py
--- a/gcloud/storage/test_bucket.py
+++ b/gcloud/storage/test_bucket.py
@@ -519,6 +519,32 @@ class _Blob(object):
         self.assertEqual(kw['method'], 'POST')
         self.assertEqual(kw['path'], COPY_PATH)
 
+    def test_rename_blob(self):
+        BUCKET_NAME = 'BUCKET_NAME'
+        BLOB_NAME = 'blob-name'
+        NEW_BLOB_NAME = 'new-blob-name'
+
+        DATA = {'name': NEW_BLOB_NAME}
+        connection = _Connection(DATA)
+        client = _Client(connection)
+        bucket = self._makeOne(client=client, name=BUCKET_NAME)
+
+        class _Blob(object):
+
+            def __init__(self, name, bucket_name):
+                self.name = name
+                self.path = '/b/%s/o/%s' % (bucket_name, name)
+                self._deleted = []
+
+            def delete(self, client=None):
+                self._deleted.append(client)
+
+        blob = _Blob(BLOB_NAME, BUCKET_NAME)
+        renamed_blob = bucket.rename_blob(blob, NEW_BLOB_NAME, client=client)
+        self.assertTrue(renamed_blob.bucket is bucket)
+        self.assertEqual(renamed_blob.name, NEW_BLOB_NAME)
+        self.assertEqual(blob._deleted, [client])
+
     def test_etag(self):
         ETAG = 'ETAG'
         properties = {'etag': ETAG}