Repository: googleapis/google-cloud-python

Issue:
BigQuery: harden 'ArrayQueryParameter.from_api_repr' against missing 'parameterValue'.
Closes #7309.


Discussion:

Resolving patch:
diff --git a/storage/google/cloud/storage/_helpers.py b/storage/google/cloud/storage/_helpers.py
--- a/storage/google/cloud/storage/_helpers.py
+++ b/storage/google/cloud/storage/_helpers.py
@@ -147,6 +147,22 @@ def patch(self, client=None):
             query_params={'projection': 'full'}, _target_object=self)
         self._set_properties(api_response)
 
+    def update(self, client=None):
+        """Sends all properties in a PUT 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.
+        """
+        client = self._require_client(client)
+        api_response = client._connection.api_request(
+            method='PUT', path=self.path, data=self._properties,
+            query_params={'projection': 'full'}, _target_object=self)
+        self._set_properties(api_response)
+
 
 def _scalar_property(fieldname):
     """Create a property descriptor around the :class:`_PropertyMixin` helpers.
diff --git a/storage/google/cloud/storage/bucket.py b/storage/google/cloud/storage/bucket.py
--- a/storage/google/cloud/storage/bucket.py
+++ b/storage/google/cloud/storage/bucket.py
@@ -556,6 +556,7 @@ def cors(self):
            >>> policies[1]['maxAgeSeconds'] = 3600
            >>> del policies[0]
            >>> bucket.cors = policies
+           >>> bucket.update()
 
         :setter: Set CORS policies for this bucket.
         :getter: Gets the CORS policies for this bucket.
@@ -595,6 +596,7 @@ def labels(self):
            >>> labels['new_key'] = 'some-label'
            >>> del labels['old_key']
            >>> bucket.labels = labels
+           >>> bucket.update()
 
         :setter: Set labels for this bucket.
         :getter: Gets the labels for this bucket.
@@ -663,6 +665,7 @@ def lifecycle_rules(self):
            >>> rules[1]['rule']['action']['type'] = 'Delete'
            >>> del rules[0]
            >>> bucket.lifecycle_rules = rules
+           >>> bucket.update()
 
         :setter: Set lifestyle rules for this bucket.
         :getter: Gets the lifestyle rules for this bucket.


Test patch:
diff --git a/storage/tests/system.py b/storage/tests/system.py
--- a/storage/tests/system.py
+++ b/storage/tests/system.py
@@ -114,6 +114,26 @@ def test_list_buckets(self):
                            if bucket.name in buckets_to_create]
         self.assertEqual(len(created_buckets), len(buckets_to_create))
 
+    def test_bucket_update_labels(self):
+        bucket_name = 'update-labels' + unique_resource_id('-')
+        bucket = retry_429(Config.CLIENT.create_bucket)(bucket_name)
+        self.case_buckets_to_delete.append(bucket_name)
+        self.assertTrue(bucket.exists())
+
+        updated_labels = {'test-label': 'label-value'}
+        bucket.labels = updated_labels
+        bucket.update()
+        self.assertEqual(bucket.labels, updated_labels)
+
+        new_labels = {'another-label': 'another-value'}
+        bucket.labels = new_labels
+        bucket.update()
+        self.assertEqual(bucket.labels, new_labels)
+
+        bucket.labels = {}
+        bucket.update()
+        self.assertEqual(bucket.labels, {})
+
 
 class TestStorageFiles(unittest.TestCase):
 
diff --git a/storage/tests/unit/test__helpers.py b/storage/tests/unit/test__helpers.py
--- a/storage/tests/unit/test__helpers.py
+++ b/storage/tests/unit/test__helpers.py
@@ -95,6 +95,26 @@ def test_patch(self):
         # Make sure changes get reset by patch().
         self.assertEqual(derived._changes, set())
 
+    def test_update(self):
+        connection = _Connection({'foo': 'Foo'})
+        client = _Client(connection)
+        derived = self._derivedClass('/path')()
+        # Make sure changes is non-empty, so we can observe a change.
+        BAR = object()
+        BAZ = object()
+        derived._properties = {'bar': BAR, 'baz': BAZ}
+        derived._changes = set(['bar'])  # Update sends 'baz' anyway.
+        derived.update(client=client)
+        self.assertEqual(derived._properties, {'foo': 'Foo'})
+        kw = connection._requested
+        self.assertEqual(len(kw), 1)
+        self.assertEqual(kw[0]['method'], 'PUT')
+        self.assertEqual(kw[0]['path'], '/path')
+        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(kw[0]['data'], {'bar': BAR, 'baz': BAZ})
+        # Make sure changes get reset by patch().
+        self.assertEqual(derived._changes, set())
+
 
 class Test__scalar_property(unittest.TestCase):