Skip to content

Commit 70d19e7

Browse files
cojencotseavergcf-owl-bot[bot]
authored
fix: revise blob.compose query parameters if_generation_match (#454)
* revise blob.compose logic to match API usage * update tests * update system test * address comments * 🦉 Updates from OwlBot * revise logic for backwards compatibility * add tests * revise docstring * fix test * revise to DeprecationWarning * address comments and revise docstrings Co-authored-by: Tres Seaver <[email protected]> Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 0dbbb8a commit 70d19e7

File tree

3 files changed

+329
-69
lines changed

3 files changed

+329
-69
lines changed

google/cloud/storage/blob.py

+75-39
Original file line numberDiff line numberDiff line change
@@ -3198,6 +3198,7 @@ def compose(
31983198
timeout=_DEFAULT_TIMEOUT,
31993199
if_generation_match=None,
32003200
if_metageneration_match=None,
3201+
if_source_generation_match=None,
32013202
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED,
32023203
):
32033204
"""Concatenate source blobs into this one.
@@ -3218,73 +3219,98 @@ def compose(
32183219
(Optional) The amount of time, in seconds, to wait
32193220
for the server response. See: :ref:`configuring_timeouts`
32203221
3221-
:type if_generation_match: list of long
3222+
:type if_generation_match: long
32223223
:param if_generation_match:
3223-
(Optional) Make the operation conditional on whether the blob's
3224-
current generation matches the given value. Setting to 0 makes the
3225-
operation succeed only if there are no live versions of the blob.
3226-
The list must match ``sources`` item-to-item.
3224+
(Optional) Makes the operation conditional on whether the
3225+
destination object's current generation matches the given value.
3226+
Setting to 0 makes the operation succeed only if there are no live
3227+
versions of the object.
3228+
3229+
Note: In a previous version, this argument worked identically to the
3230+
``if_source_generation_match`` argument. For backwards-compatibility reasons,
3231+
if a list is passed in, this argument will behave like ``if_source_generation_match``
3232+
and also issue a DeprecationWarning.
32273233
3228-
:type if_metageneration_match: list of long
3234+
:type if_metageneration_match: long
32293235
:param if_metageneration_match:
3230-
(Optional) Make the operation conditional on whether the blob's
3231-
current metageneration matches the given value. The list must match
3232-
``sources`` item-to-item.
3236+
(Optional) Makes the operation conditional on whether the
3237+
destination object's current metageneration matches the given
3238+
value.
3239+
3240+
If a list of long is passed in, no match operation will be performed.
3241+
(Deprecated: type(list of long) is supported for backwards-compatability reasons only.)
3242+
3243+
:type if_source_generation_match: list of long
3244+
:param if_source_generation_match:
3245+
(Optional) Makes the operation conditional on whether the current generation
3246+
of each source blob matches the corresponding generation.
3247+
The list must match ``sources`` item-to-item.
32333248
32343249
:type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy
32353250
:param retry:
32363251
(Optional) How to retry the RPC. See: :ref:`configuring_retries`
32373252
32383253
Example:
3239-
Compose blobs using generation match preconditions.
3254+
Compose blobs using source generation match preconditions.
32403255
32413256
>>> from google.cloud import storage
32423257
>>> client = storage.Client()
32433258
>>> bucket = client.bucket("bucket-name")
32443259
32453260
>>> blobs = [bucket.blob("blob-name-1"), bucket.blob("blob-name-2")]
3246-
>>> if_generation_match = [None] * len(blobs)
3247-
>>> if_generation_match[0] = "123" # precondition for "blob-name-1"
3261+
>>> if_source_generation_match = [None] * len(blobs)
3262+
>>> if_source_generation_match[0] = "123" # precondition for "blob-name-1"
32483263
32493264
>>> composed_blob = bucket.blob("composed-name")
3250-
>>> composed_blob.compose(blobs, if_generation_match)
3265+
>>> composed_blob.compose(blobs, if_source_generation_match=if_source_generation_match)
32513266
"""
32523267
sources_len = len(sources)
3253-
if if_generation_match is not None and len(if_generation_match) != sources_len:
3254-
raise ValueError(
3255-
"'if_generation_match' length must be the same as 'sources' length"
3268+
client = self._require_client(client)
3269+
query_params = {}
3270+
3271+
if isinstance(if_generation_match, list):
3272+
warnings.warn(
3273+
"if_generation_match: type list is deprecated and supported for backwards-compatability reasons only."
3274+
"Use if_source_generation_match instead to match source objects generations.",
3275+
DeprecationWarning,
3276+
stacklevel=2,
32563277
)
32573278

3258-
if (
3259-
if_metageneration_match is not None
3260-
and len(if_metageneration_match) != sources_len
3261-
):
3262-
raise ValueError(
3263-
"'if_metageneration_match' length must be the same as 'sources' length"
3279+
if if_source_generation_match is not None:
3280+
raise ValueError(
3281+
"Use if_generation_match to match the generation of the destination object by passing in a generation number, instead of a list."
3282+
"Use if_source_generation_match to match source objects generations."
3283+
)
3284+
3285+
# if_generation_match: type list is deprecated. Instead use if_source_generation_match.
3286+
if_source_generation_match = if_generation_match
3287+
if_generation_match = None
3288+
3289+
if isinstance(if_metageneration_match, list):
3290+
warnings.warn(
3291+
"if_metageneration_match: type list is deprecated and supported for backwards-compatability reasons only."
3292+
"Note that the metageneration to be matched is that of the destination blob."
3293+
"Please pass in a single value (type long).",
3294+
DeprecationWarning,
3295+
stacklevel=2,
32643296
)
32653297

3266-
client = self._require_client(client)
3267-
query_params = {}
3298+
if_metageneration_match = None
32683299

3269-
if self.user_project is not None:
3270-
query_params["userProject"] = self.user_project
3300+
if if_source_generation_match is None:
3301+
if_source_generation_match = [None] * sources_len
3302+
if len(if_source_generation_match) != sources_len:
3303+
raise ValueError(
3304+
"'if_source_generation_match' length must be the same as 'sources' length"
3305+
)
32713306

32723307
source_objects = []
3273-
for index, source in enumerate(sources):
3274-
source_object = {"name": source.name}
3308+
for source, source_generation in zip(sources, if_source_generation_match):
3309+
source_object = {"name": source.name, "generation": source.generation}
32753310

32763311
preconditions = {}
3277-
if (
3278-
if_generation_match is not None
3279-
and if_generation_match[index] is not None
3280-
):
3281-
preconditions["ifGenerationMatch"] = if_generation_match[index]
3282-
3283-
if (
3284-
if_metageneration_match is not None
3285-
and if_metageneration_match[index] is not None
3286-
):
3287-
preconditions["ifMetagenerationMatch"] = if_metageneration_match[index]
3312+
if source_generation is not None:
3313+
preconditions["ifGenerationMatch"] = source_generation
32883314

32893315
if preconditions:
32903316
source_object["objectPreconditions"] = preconditions
@@ -3295,6 +3321,16 @@ def compose(
32953321
"sourceObjects": source_objects,
32963322
"destination": self._properties.copy(),
32973323
}
3324+
3325+
if self.user_project is not None:
3326+
query_params["userProject"] = self.user_project
3327+
3328+
_add_generation_match_parameters(
3329+
query_params,
3330+
if_generation_match=if_generation_match,
3331+
if_metageneration_match=if_metageneration_match,
3332+
)
3333+
32983334
api_response = client._post_resource(
32993335
"{}/compose".format(self.path),
33003336
request,

tests/system/test_system.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -1723,7 +1723,7 @@ def test_compose_replace_existing_blob(self):
17231723
composed = original.download_as_bytes()
17241724
self.assertEqual(composed, BEFORE + TO_APPEND)
17251725

1726-
def test_compose_with_generation_match(self):
1726+
def test_compose_with_generation_match_list(self):
17271727
BEFORE = b"AAA\n"
17281728
original = self.bucket.blob("original")
17291729
original.content_type = "text/plain"
@@ -1751,6 +1751,49 @@ def test_compose_with_generation_match(self):
17511751
composed = original.download_as_bytes()
17521752
self.assertEqual(composed, BEFORE + TO_APPEND)
17531753

1754+
def test_compose_with_generation_match_long(self):
1755+
BEFORE = b"AAA\n"
1756+
original = self.bucket.blob("original")
1757+
original.content_type = "text/plain"
1758+
original.upload_from_string(BEFORE)
1759+
self.case_blobs_to_delete.append(original)
1760+
1761+
TO_APPEND = b"BBB\n"
1762+
to_append = self.bucket.blob("to_append")
1763+
to_append.upload_from_string(TO_APPEND)
1764+
self.case_blobs_to_delete.append(to_append)
1765+
1766+
with self.assertRaises(google.api_core.exceptions.PreconditionFailed):
1767+
original.compose([original, to_append], if_generation_match=0)
1768+
1769+
original.compose([original, to_append], if_generation_match=original.generation)
1770+
1771+
composed = original.download_as_bytes()
1772+
self.assertEqual(composed, BEFORE + TO_APPEND)
1773+
1774+
def test_compose_with_source_generation_match(self):
1775+
BEFORE = b"AAA\n"
1776+
original = self.bucket.blob("original")
1777+
original.content_type = "text/plain"
1778+
original.upload_from_string(BEFORE)
1779+
self.case_blobs_to_delete.append(original)
1780+
1781+
TO_APPEND = b"BBB\n"
1782+
to_append = self.bucket.blob("to_append")
1783+
to_append.upload_from_string(TO_APPEND)
1784+
self.case_blobs_to_delete.append(to_append)
1785+
1786+
with self.assertRaises(google.api_core.exceptions.PreconditionFailed):
1787+
original.compose([original, to_append], if_source_generation_match=[6, 7])
1788+
1789+
original.compose(
1790+
[original, to_append],
1791+
if_source_generation_match=[original.generation, to_append.generation],
1792+
)
1793+
1794+
composed = original.download_as_bytes()
1795+
self.assertEqual(composed, BEFORE + TO_APPEND)
1796+
17541797
@unittest.skipUnless(USER_PROJECT, "USER_PROJECT not set in environment.")
17551798
def test_compose_with_user_project(self):
17561799
new_bucket_name = "compose-user-project" + unique_resource_id("-")

0 commit comments

Comments
 (0)