Repository: googleapis/google-cloud-python (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']}>
```


Discussion:
@dhermes You added the explicit check to exclude empty lists in 58732669.  Do you recall the rationale?
@tseaver That's not where the logic was added, it just moved. ISTM that https://github.com/googleapis/google-cloud-python/commit/de03993053e2528d0c79969cf57c51751ced09cc is the correct place

```
$ export PHRASE='if value_is_list and len(value) == 0:'
$ git grep -l "${PHRASE}"
datastore/google/cloud/datastore/helpers.py
$ git blame datastore/google/cloud/datastore/helpers.py | grep "${PHRASE}"
58732669802 gcloud/datastore/helpers.py                 (Danny Hermes    2015-12-18 17:03:35 -0800 215)         if value_is_list and len(value) == 0:
$ git log -1 58732669802 --pretty=%P  # Find parent commit
4861e7e37d8f3d7f23c06eac9c13e7298526cd21
$ git checkout 4861e7e37d8f3d7f23c06eac9c13e7298526cd21
$ git grep -l "${PHRASE}"
$ git blame gcloud/datastore/batch.py | grep "${PHRASE}"
de03993053e (Tres Seaver  2015-01-14 20:31:49 -0500 239)         if value_is_list and len(value) == 0:
$ unset PHRASE
```

As for this issue, does #6285 actually fix it? That is, does the backend distinguish between empty and unset?
The backend very much does. If unset comes on the first one the column isn't even there. We don't send it. (which is the reason for removing the check so it would add the empty instance) It is also graphically different in the portal. not set is '-' where empty array is '[]'




Patch:
diff --git a/datastore/google/cloud/datastore/helpers.py b/datastore/google/cloud/datastore/helpers.py
index 964eb2a37204..38c4b0ddfdf6 100644
--- 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
index c73b2a80cc17..2a1080b628e5 100644
--- 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
index 9b854141e553..4b94b657e9d0 100644
--- 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
index 91a693455b7d..f4338812be54 100644
--- 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