Skip to content

Commit

Permalink
feat: enable delete_blobs() to preserve generation (#840)
Browse files Browse the repository at this point in the history
This adds a flag `preserve_generation` to the method `bucket.delete_blobs()`
- allows preserving and propagating blob generations when set to True (default False)
- better ensures backwards compatibility with both `delete_blobs()` and `bucket.delete(force=True)`

Fixes #814
  • Loading branch information
cojenco committed Aug 23, 2022
1 parent 3da8b58 commit 8fd4c37
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 0 deletions.
15 changes: 15 additions & 0 deletions google/cloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,7 @@ def delete_blobs(
blobs,
on_error=None,
client=None,
preserve_generation=False,
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
Expand All @@ -1709,6 +1710,10 @@ def delete_blobs(
Uses :meth:`delete_blob` to delete each individual blob.
By default, any generation information in the list of blobs is ignored, and the
live versions of all blobs are deleted. Set `preserve_generation` to True
if blob generation should instead be propagated from the list of blobs.
If :attr:`user_project` is set, bills the API request to that project.
:type blobs: list
Expand All @@ -1725,6 +1730,12 @@ def delete_blobs(
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the current bucket.
:type preserve_generation: bool
:param preserve_generation: (Optional) Deletes only the generation specified on the blob object,
instead of the live version, if set to True. Only :class:~google.cloud.storage.blob.Blob
objects can have their generation set in this way.
Default: False.
:type if_generation_match: list of long
:param if_generation_match:
(Optional) See :ref:`using-if-generation-match`
Expand Down Expand Up @@ -1787,11 +1798,15 @@ def delete_blobs(
for blob in blobs:
try:
blob_name = blob
generation = None
if not isinstance(blob_name, str):
blob_name = blob.name
generation = blob.generation if preserve_generation else None

self.delete_blob(
blob_name,
client=client,
generation=generation,
if_generation_match=next(if_generation_match, None),
if_generation_not_match=next(if_generation_not_match, None),
if_metageneration_match=next(if_metageneration_match, None),
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/test_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,7 @@ def test_delete_w_force_w_user_project_w_miss_on_blob(self):
bucket.delete_blob.assert_called_once_with(
blob_name,
client=client,
generation=None,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
Expand Down Expand Up @@ -1649,6 +1650,7 @@ def test_delete_blobs_hit_w_explicit_client_w_timeout(self):
bucket.delete_blob.assert_called_once_with(
blob_name,
client=client,
generation=None,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
Expand Down Expand Up @@ -1693,6 +1695,7 @@ def test_delete_blobs_w_generation_match_w_retry(self):
call_1 = mock.call(
blob_name,
client=None,
generation=None,
if_generation_match=generation_number,
if_generation_not_match=None,
if_metageneration_match=None,
Expand All @@ -1703,6 +1706,7 @@ def test_delete_blobs_w_generation_match_w_retry(self):
call_2 = mock.call(
blob_name2,
client=None,
generation=None,
if_generation_match=generation_number2,
if_generation_not_match=None,
if_metageneration_match=None,
Expand Down Expand Up @@ -1730,6 +1734,7 @@ def test_delete_blobs_w_generation_match_none(self):
call_1 = mock.call(
blob_name,
client=None,
generation=None,
if_generation_match=generation_number,
if_generation_not_match=None,
if_metageneration_match=None,
Expand All @@ -1740,6 +1745,7 @@ def test_delete_blobs_w_generation_match_none(self):
call_2 = mock.call(
blob_name2,
client=None,
generation=None,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
Expand All @@ -1749,6 +1755,52 @@ def test_delete_blobs_w_generation_match_none(self):
)
bucket.delete_blob.assert_has_calls([call_1, call_2])

def test_delete_blobs_w_preserve_generation(self):
name = "name"
blob_name = "blob-name"
blob_name2 = "blob-name2"
generation_number = 1234567890
generation_number2 = 7890123456
client = mock.Mock(spec=[])
bucket = self._make_one(client=client, name=name)
blob = self._make_blob(bucket.name, blob_name)
blob.generation = generation_number
blob2 = self._make_blob(bucket.name, blob_name2)
blob2.generation = generation_number2
bucket.delete_blob = mock.Mock()
retry = mock.Mock(spec=[])

# Test generation is propagated from list of blob instances
bucket.delete_blobs(
[blob, blob2],
preserve_generation=True,
retry=retry,
)

call_1 = mock.call(
blob_name,
client=None,
generation=generation_number,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
timeout=self._get_default_timeout(),
retry=retry,
)
call_2 = mock.call(
blob_name2,
client=None,
generation=generation_number2,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
timeout=self._get_default_timeout(),
retry=retry,
)
bucket.delete_blob.assert_has_calls([call_1, call_2])

def test_delete_blobs_miss_wo_on_error(self):
from google.cloud.exceptions import NotFound

Expand All @@ -1766,6 +1818,7 @@ def test_delete_blobs_miss_wo_on_error(self):
call_1 = mock.call(
blob_name,
client=None,
generation=None,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
Expand All @@ -1776,6 +1829,7 @@ def test_delete_blobs_miss_wo_on_error(self):
call_2 = mock.call(
blob_name2,
client=None,
generation=None,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
Expand Down Expand Up @@ -1804,6 +1858,7 @@ def test_delete_blobs_miss_w_on_error(self):
call_1 = mock.call(
blob_name,
client=None,
generation=None,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
Expand All @@ -1814,6 +1869,7 @@ def test_delete_blobs_miss_w_on_error(self):
call_2 = mock.call(
blob_name2,
client=None,
generation=None,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
Expand Down

0 comments on commit 8fd4c37

Please sign in to comment.