Repository: googleapis/google-cloud-python

Issue:
Firestore: surprising DocumentReference.get behavior for non-iterable field_path argument
Hi there! :smile:

I've been trying to get a specific field on a document efficiently by specifying a field path in `get`, but I encountered some surprising behavior. The docs point out that `field_paths` is an optional _iterable_, but if you pass a single field argument like this:

```doc.get('foo')```

no fields are returned (`to_dict` on the resulting snapshot produces `{}`). If, however, you pass an invalid field path:

```doc.get('.foo')```

you'll get a validation error, which makes it seem like passing a _valid_ single field path would work. Only ```doc.get(['foo'])``` produces the expected result (a snapshot with only field `foo` present). This is an easy mistake to make! Maybe we could validate that users pass in an iterable?

This is from `google-cloud-firestore` version 0.29.0.


Discussion:
The underlying issue here is that Python's `str` type *is* iterable.  In effect, `doc.get(`foo`)` is the same as `doc.get(['f', 'o', 'o'])`, which is why the snapshot is empty (none of the requested field paths are in it).
@schmidt-sebastian, @jadekler Should `google-cloud-firestore` be adding special-case handling for the case that the user passes a single field name to `doc.get`?
`DocumentSnapshot.get()` should accept a single field path, and not an array of field paths. 

https://github.com/googleapis/nodejs-firestore/blob/master/types/firestore.d.ts#L693
@schmidt-sebastian Sorry for the confusion:  this issue is about `DocumentRefererence.get`, which currently requires an array of field names.  My question was whether to allow passing a single field name to that method.
`DocumentRefererence.get()` takes no arguments and returns a DocumentSnapshot via the BatchGetDocuments RPC.
At least in [Python](https://github.com/googleapis/google-cloud-python/blob/d6bfb08ed3c5332d4bc91139c544c30d86019602/firestore/google/cloud/firestore_v1beta1/document.py#L395), `DocumentReference.get` takes an optional list of `field_paths` (not names, that was my mistake) and returns a snapshot.  If `field_paths` is passed, only the matching fields are included in the snapshot, via the `mask` argument of [`BatchGetDocumentsRequest`](https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#google.firestore.v1beta1.BatchGetDocumentsRequest).
We are currently trying to figure out how to expose Field Mask in the Server SDKs in a consistent way for all SDKs. The expected state for these masks is that they are not exposed right now outside of Query API. As such, I cannot provide any guidance on how this API should behave.

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1beta1/document.py b/firestore/google/cloud/firestore_v1beta1/document.py
--- a/firestore/google/cloud/firestore_v1beta1/document.py
+++ b/firestore/google/cloud/firestore_v1beta1/document.py
@@ -14,9 +14,10 @@
 
 """Classes for representing documents for the Google Cloud Firestore API."""
 
-
 import copy
 
+import six
+
 from google.cloud.firestore_v1beta1 import _helpers
 
 
@@ -418,6 +419,9 @@ def get(self, field_paths=None, transaction=None):
                 `update_time`, and `create_time` attributes will all be
                 `None` and `exists` will be `False`.
         """
+        if isinstance(field_paths, six.string_types):
+            raise ValueError(
+                "'field_paths' must be a sequence of paths, not a string.")
         snapshot_generator = self._client.get_all(
             [self], field_paths=field_paths, transaction=transaction)
         return _consume_single_get(snapshot_generator)


Test patch:
diff --git a/firestore/tests/unit/test_document.py b/firestore/tests/unit/test_document.py
--- a/firestore/tests/unit/test_document.py
+++ b/firestore/tests/unit/test_document.py
@@ -449,6 +449,13 @@ def test_delete_with_option(self):
         )
         self._delete_helper(last_update_time=timestamp_pb)
 
+    def test_get_w_single_field_path(self):
+        client = mock.Mock(spec=[])
+
+        document = self._make_one('yellow', 'mellow', client=client)
+        with self.assertRaises(ValueError):
+            document.get('foo')
+
     def test_get_success(self):
         # Create a minimal fake client with a dummy response.
         response_iterator = iter([mock.sentinel.snapshot])