Skip to content

Commit 8fd4c37

Browse files
authored
feat: enable delete_blobs() to preserve generation (#840)
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
1 parent 3da8b58 commit 8fd4c37

File tree

2 files changed

+71
-0
lines changed

2 files changed

+71
-0
lines changed

google/cloud/storage/bucket.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,7 @@ def delete_blobs(
16981698
blobs,
16991699
on_error=None,
17001700
client=None,
1701+
preserve_generation=False,
17011702
timeout=_DEFAULT_TIMEOUT,
17021703
if_generation_match=None,
17031704
if_generation_not_match=None,
@@ -1709,6 +1710,10 @@ def delete_blobs(
17091710
17101711
Uses :meth:`delete_blob` to delete each individual blob.
17111712
1713+
By default, any generation information in the list of blobs is ignored, and the
1714+
live versions of all blobs are deleted. Set `preserve_generation` to True
1715+
if blob generation should instead be propagated from the list of blobs.
1716+
17121717
If :attr:`user_project` is set, bills the API request to that project.
17131718
17141719
:type blobs: list
@@ -1725,6 +1730,12 @@ def delete_blobs(
17251730
:param client: (Optional) The client to use. If not passed, falls back
17261731
to the ``client`` stored on the current bucket.
17271732
1733+
:type preserve_generation: bool
1734+
:param preserve_generation: (Optional) Deletes only the generation specified on the blob object,
1735+
instead of the live version, if set to True. Only :class:~google.cloud.storage.blob.Blob
1736+
objects can have their generation set in this way.
1737+
Default: False.
1738+
17281739
:type if_generation_match: list of long
17291740
:param if_generation_match:
17301741
(Optional) See :ref:`using-if-generation-match`
@@ -1787,11 +1798,15 @@ def delete_blobs(
17871798
for blob in blobs:
17881799
try:
17891800
blob_name = blob
1801+
generation = None
17901802
if not isinstance(blob_name, str):
17911803
blob_name = blob.name
1804+
generation = blob.generation if preserve_generation else None
1805+
17921806
self.delete_blob(
17931807
blob_name,
17941808
client=client,
1809+
generation=generation,
17951810
if_generation_match=next(if_generation_match, None),
17961811
if_generation_not_match=next(if_generation_not_match, None),
17971812
if_metageneration_match=next(if_metageneration_match, None),

tests/unit/test_bucket.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,7 @@ def test_delete_w_force_w_user_project_w_miss_on_blob(self):
14901490
bucket.delete_blob.assert_called_once_with(
14911491
blob_name,
14921492
client=client,
1493+
generation=None,
14931494
if_generation_match=None,
14941495
if_generation_not_match=None,
14951496
if_metageneration_match=None,
@@ -1649,6 +1650,7 @@ def test_delete_blobs_hit_w_explicit_client_w_timeout(self):
16491650
bucket.delete_blob.assert_called_once_with(
16501651
blob_name,
16511652
client=client,
1653+
generation=None,
16521654
if_generation_match=None,
16531655
if_generation_not_match=None,
16541656
if_metageneration_match=None,
@@ -1693,6 +1695,7 @@ def test_delete_blobs_w_generation_match_w_retry(self):
16931695
call_1 = mock.call(
16941696
blob_name,
16951697
client=None,
1698+
generation=None,
16961699
if_generation_match=generation_number,
16971700
if_generation_not_match=None,
16981701
if_metageneration_match=None,
@@ -1703,6 +1706,7 @@ def test_delete_blobs_w_generation_match_w_retry(self):
17031706
call_2 = mock.call(
17041707
blob_name2,
17051708
client=None,
1709+
generation=None,
17061710
if_generation_match=generation_number2,
17071711
if_generation_not_match=None,
17081712
if_metageneration_match=None,
@@ -1730,6 +1734,7 @@ def test_delete_blobs_w_generation_match_none(self):
17301734
call_1 = mock.call(
17311735
blob_name,
17321736
client=None,
1737+
generation=None,
17331738
if_generation_match=generation_number,
17341739
if_generation_not_match=None,
17351740
if_metageneration_match=None,
@@ -1740,6 +1745,7 @@ def test_delete_blobs_w_generation_match_none(self):
17401745
call_2 = mock.call(
17411746
blob_name2,
17421747
client=None,
1748+
generation=None,
17431749
if_generation_match=None,
17441750
if_generation_not_match=None,
17451751
if_metageneration_match=None,
@@ -1749,6 +1755,52 @@ def test_delete_blobs_w_generation_match_none(self):
17491755
)
17501756
bucket.delete_blob.assert_has_calls([call_1, call_2])
17511757

1758+
def test_delete_blobs_w_preserve_generation(self):
1759+
name = "name"
1760+
blob_name = "blob-name"
1761+
blob_name2 = "blob-name2"
1762+
generation_number = 1234567890
1763+
generation_number2 = 7890123456
1764+
client = mock.Mock(spec=[])
1765+
bucket = self._make_one(client=client, name=name)
1766+
blob = self._make_blob(bucket.name, blob_name)
1767+
blob.generation = generation_number
1768+
blob2 = self._make_blob(bucket.name, blob_name2)
1769+
blob2.generation = generation_number2
1770+
bucket.delete_blob = mock.Mock()
1771+
retry = mock.Mock(spec=[])
1772+
1773+
# Test generation is propagated from list of blob instances
1774+
bucket.delete_blobs(
1775+
[blob, blob2],
1776+
preserve_generation=True,
1777+
retry=retry,
1778+
)
1779+
1780+
call_1 = mock.call(
1781+
blob_name,
1782+
client=None,
1783+
generation=generation_number,
1784+
if_generation_match=None,
1785+
if_generation_not_match=None,
1786+
if_metageneration_match=None,
1787+
if_metageneration_not_match=None,
1788+
timeout=self._get_default_timeout(),
1789+
retry=retry,
1790+
)
1791+
call_2 = mock.call(
1792+
blob_name2,
1793+
client=None,
1794+
generation=generation_number2,
1795+
if_generation_match=None,
1796+
if_generation_not_match=None,
1797+
if_metageneration_match=None,
1798+
if_metageneration_not_match=None,
1799+
timeout=self._get_default_timeout(),
1800+
retry=retry,
1801+
)
1802+
bucket.delete_blob.assert_has_calls([call_1, call_2])
1803+
17521804
def test_delete_blobs_miss_wo_on_error(self):
17531805
from google.cloud.exceptions import NotFound
17541806

@@ -1766,6 +1818,7 @@ def test_delete_blobs_miss_wo_on_error(self):
17661818
call_1 = mock.call(
17671819
blob_name,
17681820
client=None,
1821+
generation=None,
17691822
if_generation_match=None,
17701823
if_generation_not_match=None,
17711824
if_metageneration_match=None,
@@ -1776,6 +1829,7 @@ def test_delete_blobs_miss_wo_on_error(self):
17761829
call_2 = mock.call(
17771830
blob_name2,
17781831
client=None,
1832+
generation=None,
17791833
if_generation_match=None,
17801834
if_generation_not_match=None,
17811835
if_metageneration_match=None,
@@ -1804,6 +1858,7 @@ def test_delete_blobs_miss_w_on_error(self):
18041858
call_1 = mock.call(
18051859
blob_name,
18061860
client=None,
1861+
generation=None,
18071862
if_generation_match=None,
18081863
if_generation_not_match=None,
18091864
if_metageneration_match=None,
@@ -1814,6 +1869,7 @@ def test_delete_blobs_miss_w_on_error(self):
18141869
call_2 = mock.call(
18151870
blob_name2,
18161871
client=None,
1872+
generation=None,
18171873
if_generation_match=None,
18181874
if_generation_not_match=None,
18191875
if_metageneration_match=None,

0 commit comments

Comments
 (0)