Repository: googleapis/google-cloud-python

Issue:
Datastore: empty lists are not reflected in 'client.put'
Related to https://github.com/googleapis/google-cloud-python/pull/3767

Currently, Sending an empty array results in the field not being set.

```python
from google.cloud import datastore

ds = datastore.Client()

key = ds.key('Test')
entity = datastore.Entity(key=key)
entity['children'] = []
ds.put(entity)
print(entity)
value = ds.get(entity.key)
print(value)

key = ds.key('Test')
entity = datastore.Entity(key=key)
entity['children'] = ['A']
ds.put(entity)
print(entity)
value = ds.get(entity.key)
print(value)
```

Output:

```
<Entity('Test', 5707090131681280) {'children': []}>
<Entity('Test', 5707090131681280) {}>
<Entity('Test', 5682274280407040) {'children': ['A']}>
<Entity('Test', 5682274280407040) {'children': ['A']}>
```
Datastore: empty lists are not reflected in 'client.put'
Related to https://github.com/googleapis/google-cloud-python/pull/3767

Currently, Sending an empty array results in the field not being set.

```python
from google.cloud import datastore

ds = datastore.Client()

key = ds.key('Test')
entity = datastore.Entity(key=key)
entity['children'] = []
ds.put(entity)
print(entity)
value = ds.get(entity.key)
print(value)

key = ds.key('Test')
entity = datastore.Entity(key=key)
entity['children'] = ['A']
ds.put(entity)
print(entity)
value = ds.get(entity.key)
print(value)
```

Output:

```
<Entity('Test', 5707090131681280) {'children': []}>
<Entity('Test', 5707090131681280) {}>
<Entity('Test', 5682274280407040) {'children': ['A']}>
<Entity('Test', 5682274280407040) {'children': ['A']}>
```


Discussion:

Resolving patch:
diff --git a/datastore/google/cloud/datastore/helpers.py b/datastore/google/cloud/datastore/helpers.py
--- a/datastore/google/cloud/datastore/helpers.py
+++ b/datastore/google/cloud/datastore/helpers.py
@@ -212,8 +212,6 @@ def entity_to_protobuf(entity):
 
     for name, value in entity.items():
         value_is_list = isinstance(value, list)
-        if value_is_list and len(value) == 0:
-            continue
 
         value_pb = _new_value_pb(entity_pb, name)
         # Set the appropriate value.
@@ -453,10 +451,14 @@ def _set_protobuf_value(value_pb, val):
         entity_pb = entity_to_protobuf(val)
         value_pb.entity_value.CopyFrom(entity_pb)
     elif attr == 'array_value':
-        l_pb = value_pb.array_value.values
-        for item in val:
-            i_pb = l_pb.add()
-            _set_protobuf_value(i_pb, item)
+        if len(val) == 0:
+            array_value = entity_pb2.ArrayValue(values=[])
+            value_pb.array_value.CopyFrom(array_value)
+        else:
+            l_pb = value_pb.array_value.values
+            for item in val:
+                i_pb = l_pb.add()
+                _set_protobuf_value(i_pb, item)
     elif attr == 'geo_point_value':
         value_pb.geo_point_value.CopyFrom(val)
     else:  # scalar, just assign


Test patch:
diff --git a/datastore/tests/system/test_system.py b/datastore/tests/system/test_system.py
--- a/datastore/tests/system/test_system.py
+++ b/datastore/tests/system/test_system.py
@@ -547,3 +547,15 @@ def test_failure_with_contention(self):
                 # transaction.
                 entity_in_txn[contention_prop_name] = u'inside'
                 txn.put(entity_in_txn)
+
+    def test_empty_array_put(self):
+        local_client = clone_client(Config.CLIENT)
+
+        key = local_client.key('EmptyArray', 1234)
+        local_client = datastore.Client()
+        entity = datastore.Entity(key=key)
+        entity['children'] = []
+        local_client.put(entity)
+        retrieved = local_client.get(entity.key)
+
+        self.assertEqual(entity['children'], retrieved['children'])
diff --git a/datastore/tests/unit/test_batch.py b/datastore/tests/unit/test_batch.py
--- a/datastore/tests/unit/test_batch.py
+++ b/datastore/tests/unit/test_batch.py
@@ -119,7 +119,7 @@ def test_put_entity_w_completed_key(self):
             'foo': 'bar',
             'baz': 'qux',
             'spam': [1, 2, 3],
-            'frotz': [],  # will be ignored
+            'frotz': [],
         }
         client = _Client(project)
         batch = self._make_one(client)
@@ -134,7 +134,7 @@ def test_put_entity_w_completed_key(self):
         self.assertEqual(mutated_entity.key, key._key)
 
         prop_dict = dict(_property_tuples(mutated_entity))
-        self.assertEqual(len(prop_dict), 3)
+        self.assertEqual(len(prop_dict), 4)
         self.assertFalse(prop_dict['foo'].exclude_from_indexes)
         self.assertTrue(prop_dict['baz'].exclude_from_indexes)
         self.assertFalse(prop_dict['spam'].exclude_from_indexes)
@@ -142,7 +142,7 @@ def test_put_entity_w_completed_key(self):
         self.assertTrue(spam_values[0].exclude_from_indexes)
         self.assertTrue(spam_values[1].exclude_from_indexes)
         self.assertTrue(spam_values[2].exclude_from_indexes)
-        self.assertFalse('frotz' in prop_dict)
+        self.assertTrue('frotz' in prop_dict)
 
     def test_delete_wrong_status(self):
         project = 'PROJECT'
diff --git a/datastore/tests/unit/test_helpers.py b/datastore/tests/unit/test_helpers.py
--- a/datastore/tests/unit/test_helpers.py
+++ b/datastore/tests/unit/test_helpers.py
@@ -294,7 +294,11 @@ def test_with_empty_list(self):
         entity['foo'] = []
         entity_pb = self._call_fut(entity)
 
-        self._compare_entity_proto(entity_pb, entity_pb2.Entity())
+        expected_pb = entity_pb2.Entity()
+        prop = expected_pb.properties.get_or_create('foo')
+        prop.array_value.CopyFrom(entity_pb2.ArrayValue(values=[]))
+
+        self._compare_entity_proto(entity_pb, expected_pb)
 
     def test_inverts_to_protobuf(self):
         from google.cloud.datastore_v1.proto import entity_pb2