Repository: googleapis/python-storage

Issue:
Storage: Possible metadata regression on blobs in 1.24.0
#### Environment details

* Debian 10
* google-cloud-storage version: 1.24.1

#### Steps to reproduce

It's documented that metadata that isn't set will return a NoneType. Naturally, when wanting to unset any metadata, you expect to be able to pass in `None`. This worked up until 1.24.0 which broke this with googleapis/google-cloud-python#9796.

I think you can still technically set the metadata to an empty dictionary and get the same functionality, but it's a bit counter intuitive, when no metadata is described as being a `NoneType`.

#### Code example

```python
storage_client = storage.Client(...)
blob = storage_client.get_bucket('abc').get_blob('abc')
blob.metadata = None
blob.patch()
```

#### Stack trace

```
AttributeError: 'NoneType' object has no attribute 'items'
  File "django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "django/views/generic/base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "rest_framework/views.py", line 505, in dispatch
    response = self.handle_exception(exc)
  File "rest_framework/views.py", line 465, in handle_exception
    self.raise_uncaught_exception(exc)
  File "rest_framework/views.py", line 476, in raise_uncaught_exception
    raise exc
  File "rest_framework/views.py", line 502, in dispatch
    response = handler(request, *args, **kwargs)
  File "api/media/views.py", line 96, in put
    product.media.set_primary(name)
  File "media/api.py", line 134, in set_primary
    blob.metadata = None
  File "/usr/local/lib/python3.7/site-packages/google/cloud/storage/blob.py", line 1917, in metadata
    value = {k: str(v) for k, v in value.items()}
```

Discussion:
IlyaFaer: @frankyn, we already discussed this changes here: https://github.com/googleapis/google-cloud-python/pull/10108

Patch:
diff --git a/google/cloud/storage/blob.py b/google/cloud/storage/blob.py
index 93beced..dcf7d03 100644
--- a/google/cloud/storage/blob.py
+++ b/google/cloud/storage/blob.py
@@ -1986,9 +1986,10 @@ class Blob(_PropertyMixin):
         See https://cloud.google.com/storage/docs/json_api/v1/objects
 
         :type value: dict
-        :param value: (Optional) The blob metadata to set.
+        :param value: The blob metadata to set.
         """
-        value = {k: str(v) for k, v in value.items()}
+        if value is not None:
+            value = {k: str(v) for k, v in value.items()}
         self._patch_property("metadata", value)
 
     @property


Test patch:
diff --git a/tests/unit/test_blob.py b/tests/unit/test_blob.py
index 1c2b1e9..5246d37 100644
--- a/tests/unit/test_blob.py
+++ b/tests/unit/test_blob.py
@@ -1215,6 +1215,16 @@ class Test_Blob(unittest.TestCase):
         expected = {"name": name}
         self.assertEqual(object_metadata, expected)
 
+    def test__set_metadata_to_none(self):
+        name = u"blob-name"
+        blob = self._make_one(name, bucket=None)
+        blob.storage_class = "NEARLINE"
+        blob.cache_control = "max-age=3600"
+
+        with mock.patch("google.cloud.storage.blob.Blob._patch_property") as patch_prop:
+            blob.metadata = None
+            patch_prop.assert_called_once_with("metadata", None)
+
     def test__get_upload_arguments(self):
         name = u"blob-name"
         key = b"[pXw@,p@@AfBfrR3x-2b2SCHR,.?YwRO"