Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/google-cloud-storage/google/cloud/storage/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -3849,6 +3849,7 @@ def compose(
if_metageneration_match=None,
if_source_generation_match=None,
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED,
delete_source_objects=None,
):
"""Concatenate source blobs into this one.

Expand Down Expand Up @@ -3908,6 +3909,11 @@ def compose(
Change the value to ``DEFAULT_RETRY`` or another `google.api_core.retry.Retry` object
to enable retries regardless of generation precondition setting.
See [Configuring Retries](https://cloud.google.com/python/docs/reference/storage/latest/retry_timeout).

:type delete_source_objects: bool
:param delete_source_objects:
(Optional) If True, the source objects will be deleted after a
successful composition.
"""
with create_trace_span(name="Storage.Blob.compose"):
sources_len = len(sources)
Expand Down Expand Up @@ -3964,6 +3970,9 @@ def compose(
"destination": self._properties.copy(),
}

if delete_source_objects is not None:
request["deleteSourceObjects"] = delete_source_objects

if self.user_project is not None:
query_params["userProject"] = self.user_project

Expand Down
37 changes: 37 additions & 0 deletions packages/google-cloud-storage/tests/unit/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -4480,6 +4480,43 @@ def test_compose_wo_content_type_set(self):
_target_object=destination,
)

def test_compose_w_delete_source_objects(self):
source_1_name = "source-1"
source_2_name = "source-2"
destination_name = "destination"
delete_source_objects = True
api_response = {}
client = mock.Mock(spec=["_post_resource"])
client._post_resource.return_value = api_response
bucket = _Bucket(client=client)
source_1 = self._make_one(source_1_name, bucket=bucket)
source_2 = self._make_one(source_2_name, bucket=bucket)
destination = self._make_one(destination_name, bucket=bucket)

destination.compose(
sources=[source_1, source_2],
delete_source_objects=delete_source_objects,
)

expected_path = f"/b/name/o/{destination_name}/compose"
expected_data = {
"sourceObjects": [
{"name": source_1.name, "generation": source_1.generation},
{"name": source_2.name, "generation": source_2.generation},
],
"destination": {},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is empty?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jules check why is this empty?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the test to set content_type on the destination blob, ensuring the destination property in the request body is not empty.

Copy link
Copy Markdown
Contributor Author

@nidhiii-27 nidhiii-27 May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chandra-siri This was empty because this field is usually populated for any metadata that needs to be added to the destination object. In the new commit, content_type is added here.

"deleteSourceObjects": delete_source_objects,
}
expected_query_params = {}
client._post_resource.assert_called_once_with(
expected_path,
expected_data,
query_params=expected_query_params,
timeout=self._get_default_timeout(),
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED,
_target_object=destination,
)

def test_compose_minimal_w_user_project_w_timeout(self):
source_1_name = "source-1"
source_2_name = "source-2"
Expand Down
Loading