Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
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
4 changes: 4 additions & 0 deletions storage/google/cloud/storage/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ def _set_properties(self, value):
self._properties = value
# If the values are reset, the changes must as well.
self._changes = set()
if hasattr(self, '_label_removals'):
self._label_removals.clear()

This comment was marked as spam.

This comment was marked as spam.


def patch(self, client=None):
"""Sends all changed properties in a PATCH request.
Expand All @@ -142,6 +144,8 @@ def patch(self, client=None):
# to work properly w/ 'noAcl'.
update_properties = {key: self._properties[key]
for key in self._changes}

# Make the API call.
api_response = client._connection.api_request(
method='PATCH', path=self.path, data=update_properties,
query_params={'projection': 'full'}, _target_object=self)
Expand Down
31 changes: 31 additions & 0 deletions storage/google/cloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def __init__(self, client, name=None):
self._client = client
self._acl = BucketACL(self)
self._default_object_acl = DefaultObjectACL(self)
self._label_removals = set()

def __repr__(self):
return '<Bucket: %s>' % (self.name,)
Expand Down Expand Up @@ -199,6 +200,27 @@ def create(self, client=None):
data=properties, _target_object=self)
self._set_properties(api_response)

def patch(self, client=None):
"""Sends all changed properties in a PATCH request.

Updates the ``_properties`` with the response from the backend.

:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.
"""
# Special case: For buckets, it is possible that labels are being
# removed; this requires special handling.
if self._label_removals:
self._changes.add('labels')
self._properties.setdefault('labels', {})
for removed_label in self._label_removals:
self._properties['labels'][removed_label] = None

# Call the superclass method.
return super(Bucket, self).patch(client=client)

@property
def acl(self):
"""Create our ACL on demand."""
Expand Down Expand Up @@ -619,6 +641,15 @@ def labels(self, mapping):
:type mapping: :class:`dict`
:param mapping: Name-value pairs (string->string) labelling the bucket.
"""
# If any labels have been expressly removed, we need to track this
# so that a future .patch() call can do the correct thing.
existing = set([k for k in self.labels.keys()])
incoming = set([k for k in mapping.keys()])
self._label_removals = self._label_removals.union(
existing.difference(incoming),
)

# Actually update the labels on the object.
self._patch_property('labels', copy.deepcopy(mapping))

@property
Expand Down
2 changes: 1 addition & 1 deletion storage/tests/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def test_bucket_update_labels(self):

new_labels = {'another-label': 'another-value'}
bucket.labels = new_labels
bucket.update()
bucket.patch()
self.assertEqual(bucket.labels, new_labels)

bucket.labels = {}
Expand Down
27 changes: 27 additions & 0 deletions storage/tests/unit/test_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,33 @@ def test_labels_setter(self):
self.assertIsNot(bucket._properties['labels'], LABELS)
self.assertIn('labels', bucket._changes)

def test_labels_setter_with_removal(self):
# Make sure the bucket labels look correct and follow the expected
# public structure.
bucket = self._make_one(name='name')
self.assertEqual(bucket.labels, {})
bucket.labels = {'color': 'red', 'flavor': 'cherry'}
self.assertEqual(bucket.labels, {'color': 'red', 'flavor': 'cherry'})
bucket.labels = {'color': 'red'}
self.assertEqual(bucket.labels, {'color': 'red'})

# Make sure that a patch call correctly removes the flavor label.
client = mock.NonCallableMock(spec=('_connection',))
client._connection = mock.NonCallableMock(spec=('api_request',))
bucket.patch(client=client)
client._connection.api_request.assert_called()
_, _, kwargs = client._connection.api_request.mock_calls[0]
self.assertEqual(len(kwargs['data']['labels']), 2)
self.assertEqual(kwargs['data']['labels']['color'], 'red')
self.assertIsNone(kwargs['data']['labels']['flavor'])

# A second patch call should be a no-op for labels.
client._connection.api_request.reset_mock()
bucket.patch(client=client)
client._connection.api_request.assert_called()
_, _, kwargs = client._connection.api_request.mock_calls[0]
self.assertNotIn('labels', kwargs['data'])

def test_get_logging_w_prefix(self):
NAME = 'name'
LOG_BUCKET = 'logs'
Expand Down