Repository: googleapis/google-cloud-python

Issue:
Vision raises TypeError: b'...' is not JSON serializable on Python 3
Simple reproducible case:

``` python
import io
import os

from google.cloud import vision

vision_client = vision.Client()

file_name = os.path.join(
    os.path.dirname(__file__),
    'resources/wakeupcat.jpg')

with io.open(file_name, 'rb') as image_file:
    image = vision_client.image(
        content=image_file.read())

labels = image.detect_labels()

print('Labels:')
for label in labels:
    print(label.description)
```

works fine on Python 2, fails on Python 3.



Discussion:
Stack trace:

```
Traceback (most recent call last):
  File "quickstart.py", line 51, in <module>
    run_quickstart()
  File "quickstart.py", line 38, in run_quickstart
    image = vision_client.image(
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/site-packages/google/cloud/vision/image.py", line 139, in detect_labels
    return self._detect_annotation(feature)
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/site-packages/google/cloud/vision/image.py", line 105, in _detect_annotation
    result = self.client.annotate(self, [feature])
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/site-packages/google/cloud/vision/client.py", line 106, in annotate
    data=data)
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/site-packages/google/cloud/connection.py", line 341, in api_request
    data = json.dumps(data)
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/json/__init__.py", line 230, in dumps
    return _default_encoder.encode(obj)
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/json/encoder.py", line 192, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/json/encoder.py", line 250, in iterencode
    return _iterencode(o, 0)
  File "/Users/jonwayne/.pyenv/versions/3.4.3/lib/python3.4/json/encoder.py", line 173, in default
    raise TypeError(repr(o) + " is not JSON serializable")
```

Same issue fixed by #2520?

Seems like it, vision probably just needs a similar patch.

Oh my blindness!

@daspecster Do the system tests for vision not cover this?

Zoinks! That bytes. I will get on this right now.

> That bytes

heheh.

@dhermes Also, sadly there are no system tests for this.
I made an issue to add system tests though.

https://github.com/GoogleCloudPlatform/google-cloud-python/issues/2526

Ah. That is no good. We should not have pushed to PyPI without system tests. Live and learn.

Agreed. I don't think we even brought it up anywhere.

@dhermes I don't think I have access to the system test project(`precise-truck`?) to add things like images and audio  to run the tests on.

@daspecster Get them working locally first and then we can cross that bridge?

Ok, will do.


Resolving patch:
diff --git a/vision/google/cloud/vision/image.py b/vision/google/cloud/vision/image.py
--- a/vision/google/cloud/vision/image.py
+++ b/vision/google/cloud/vision/image.py
@@ -18,6 +18,7 @@
 from base64 import b64encode
 
 from google.cloud._helpers import _to_bytes
+from google.cloud._helpers import _bytes_to_unicode
 from google.cloud.vision.entity import EntityAnnotation
 from google.cloud.vision.face import Face
 from google.cloud.vision.feature import Feature
@@ -47,7 +48,7 @@ def __init__(self, client, content=None, source_uri=None):
         if source_uri:
             self._source = source_uri
         else:
-            self._content = b64encode(_to_bytes(content))
+            self._content = _bytes_to_unicode(b64encode(_to_bytes(content)))
 
     def as_dict(self):
         """Generate dictionary structure for request.


Test patch:
diff --git a/vision/unit_tests/test_client.py b/vision/unit_tests/test_client.py
--- a/vision/unit_tests/test_client.py
+++ b/vision/unit_tests/test_client.py
@@ -12,20 +12,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
+import base64
 import unittest
 
 from google.cloud._helpers import _to_bytes
+from google.cloud._helpers import _bytes_to_unicode
 
-_IMAGE_CONTENT = _to_bytes('/9j/4QNURXhpZgAASUkq')
-_IMAGE_SOURCE = 'gs://some/image.jpg'
 
+IMAGE_CONTENT = _to_bytes('/9j/4QNURXhpZgAASUkq')
+IMAGE_SOURCE = 'gs://some/image.jpg'
+PROJECT = 'PROJECT'
+B64_IMAGE_CONTENT = _bytes_to_unicode(base64.b64encode(IMAGE_CONTENT))
 
-class TestClient(unittest.TestCase):
-    import base64
-    PROJECT = 'PROJECT'
-    B64_IMAGE_CONTENT = base64.b64encode(_IMAGE_CONTENT)
 
+class TestClient(unittest.TestCase):
     def _getTargetClass(self):
         from google.cloud.vision.client import Client
         return Client
@@ -35,8 +35,8 @@ def _makeOne(self, *args, **kw):
 
     def test_ctor(self):
         creds = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=creds)
-        self.assertEqual(client.project, self.PROJECT)
+        client = self._makeOne(project=PROJECT, credentials=creds)
+        self.assertEqual(client.project, PROJECT)
 
     def test_face_annotation(self):
         from google.cloud.vision.feature import Feature, FeatureTypes
@@ -47,7 +47,7 @@ def test_face_annotation(self):
             "requests": [
                 {
                     "image": {
-                        "content": self.B64_IMAGE_CONTENT
+                        "content": B64_IMAGE_CONTENT
                     },
                     "features": [
                         {
@@ -59,12 +59,12 @@ def test_face_annotation(self):
             ]
         }
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
         features = [Feature(feature_type=FeatureTypes.FACE_DETECTION,
                             max_results=3)]
-        image = client.image(content=_IMAGE_CONTENT)
+        image = client.image(content=IMAGE_CONTENT)
         response = client.annotate(image, features)
 
         self.assertEqual(REQUEST,
@@ -75,9 +75,9 @@ def test_image_with_client(self):
         from google.cloud.vision.image import Image
 
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT,
+        client = self._makeOne(project=PROJECT,
                                credentials=credentials)
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         self.assertIsInstance(image, Image)
 
     def test_face_detection_from_source(self):
@@ -85,15 +85,15 @@ def test_face_detection_from_source(self):
         from unit_tests._fixtures import FACE_DETECTION_RESPONSE
         RETURNED = FACE_DETECTION_RESPONSE
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         faces = image.detect_faces(limit=3)
         self.assertEqual(5, len(faces))
         self.assertIsInstance(faces[0], Face)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(_IMAGE_SOURCE,
+        self.assertEqual(IMAGE_SOURCE,
                          image_request['image']['source']['gcs_image_uri'])
         self.assertEqual(3, image_request['features'][0]['maxResults'])
 
@@ -102,15 +102,16 @@ def test_face_detection_from_content(self):
         from unit_tests._fixtures import FACE_DETECTION_RESPONSE
         RETURNED = FACE_DETECTION_RESPONSE
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(content=_IMAGE_CONTENT)
+        image = client.image(content=IMAGE_CONTENT)
         faces = image.detect_faces(limit=5)
         self.assertEqual(5, len(faces))
         self.assertIsInstance(faces[0], Face)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(self.B64_IMAGE_CONTENT,
+
+        self.assertEqual(B64_IMAGE_CONTENT,
                          image_request['image']['content'])
         self.assertEqual(5, image_request['features'][0]['maxResults'])
 
@@ -120,15 +121,15 @@ def test_label_detection_from_source(self):
             LABEL_DETECTION_RESPONSE as RETURNED)
 
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         labels = image.detect_labels(limit=3)
         self.assertEqual(3, len(labels))
         self.assertIsInstance(labels[0], EntityAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(_IMAGE_SOURCE,
+        self.assertEqual(IMAGE_SOURCE,
                          image_request['image']['source']['gcs_image_uri'])
         self.assertEqual(3, image_request['features'][0]['maxResults'])
         self.assertEqual('automobile', labels[0].description)
@@ -142,15 +143,15 @@ def test_landmark_detection_from_source(self):
             LANDMARK_DETECTION_RESPONSE as RETURNED)
 
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         landmarks = image.detect_landmarks(limit=3)
         self.assertEqual(2, len(landmarks))
         self.assertIsInstance(landmarks[0], EntityAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(_IMAGE_SOURCE,
+        self.assertEqual(IMAGE_SOURCE,
                          image_request['image']['source']['gcs_image_uri'])
         self.assertEqual(3, image_request['features'][0]['maxResults'])
         self.assertEqual(48.861013, landmarks[0].locations[0].latitude)
@@ -164,15 +165,15 @@ def test_landmark_detection_from_content(self):
             LANDMARK_DETECTION_RESPONSE as RETURNED)
 
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(content=_IMAGE_CONTENT)
+        image = client.image(content=IMAGE_CONTENT)
         landmarks = image.detect_landmarks(limit=5)
         self.assertEqual(2, len(landmarks))
         self.assertIsInstance(landmarks[0], EntityAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(self.B64_IMAGE_CONTENT,
+        self.assertEqual(B64_IMAGE_CONTENT,
                          image_request['image']['content'])
         self.assertEqual(5, image_request['features'][0]['maxResults'])
 
@@ -181,15 +182,15 @@ def test_logo_detection_from_source(self):
         from unit_tests._fixtures import LOGO_DETECTION_RESPONSE
         RETURNED = LOGO_DETECTION_RESPONSE
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         logos = image.detect_logos(limit=3)
         self.assertEqual(2, len(logos))
         self.assertIsInstance(logos[0], EntityAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(_IMAGE_SOURCE,
+        self.assertEqual(IMAGE_SOURCE,
                          image_request['image']['source']['gcs_image_uri'])
         self.assertEqual(3, image_request['features'][0]['maxResults'])
 
@@ -198,15 +199,15 @@ def test_logo_detection_from_content(self):
         from unit_tests._fixtures import LOGO_DETECTION_RESPONSE
         RETURNED = LOGO_DETECTION_RESPONSE
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(content=_IMAGE_CONTENT)
+        image = client.image(content=IMAGE_CONTENT)
         logos = image.detect_logos(limit=5)
         self.assertEqual(2, len(logos))
         self.assertIsInstance(logos[0], EntityAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(self.B64_IMAGE_CONTENT,
+        self.assertEqual(B64_IMAGE_CONTENT,
                          image_request['image']['content'])
         self.assertEqual(5, image_request['features'][0]['maxResults'])
 
@@ -216,15 +217,15 @@ def test_text_detection_from_source(self):
             TEXT_DETECTION_RESPONSE as RETURNED)
 
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         text = image.detect_text(limit=3)
         self.assertEqual(3, len(text))
         self.assertIsInstance(text[0], EntityAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(_IMAGE_SOURCE,
+        self.assertEqual(IMAGE_SOURCE,
                          image_request['image']['source']['gcs_image_uri'])
         self.assertEqual(3, image_request['features'][0]['maxResults'])
         self.assertEqual('en', text[0].locale)
@@ -238,14 +239,14 @@ def test_safe_search_detection_from_source(self):
 
         RETURNED = SAFE_SEARCH_DETECTION_RESPONSE
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         safe_search = image.detect_safe_search()
         self.assertIsInstance(safe_search, SafeSearchAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(_IMAGE_SOURCE,
+        self.assertEqual(IMAGE_SOURCE,
                          image_request['image']['source']['gcs_image_uri'])
         self.assertEqual('VERY_UNLIKELY', safe_search.adult)
         self.assertEqual('UNLIKELY', safe_search.spoof)
@@ -258,14 +259,14 @@ def test_image_properties_detection_from_source(self):
 
         RETURNED = IMAGE_PROPERTIES_RESPONSE
         credentials = _Credentials()
-        client = self._makeOne(project=self.PROJECT, credentials=credentials)
+        client = self._makeOne(project=PROJECT, credentials=credentials)
         client.connection = _Connection(RETURNED)
 
-        image = client.image(source_uri=_IMAGE_SOURCE)
+        image = client.image(source_uri=IMAGE_SOURCE)
         image_properties = image.detect_properties()
         self.assertIsInstance(image_properties, ImagePropertiesAnnotation)
         image_request = client.connection._requested[0]['data']['requests'][0]
-        self.assertEqual(_IMAGE_SOURCE,
+        self.assertEqual(IMAGE_SOURCE,
                          image_request['image']['source']['gcs_image_uri'])
         self.assertEqual(0.42258179, image_properties.colors[0].score)
         self.assertEqual(0.025376344,
@@ -289,14 +290,14 @@ def test_make_vision_request(self):
 
         feature = Feature(feature_type=FeatureTypes.FACE_DETECTION,
                           max_results=3)
-        vision_request = self._makeOne(_IMAGE_CONTENT, feature)
-        self.assertEqual(_IMAGE_CONTENT, vision_request.image)
+        vision_request = self._makeOne(IMAGE_CONTENT, feature)
+        self.assertEqual(IMAGE_CONTENT, vision_request.image)
         self.assertEqual(FeatureTypes.FACE_DETECTION,
                          vision_request.features[0].feature_type)
 
     def test_make_vision_request_with_bad_feature(self):
         with self.assertRaises(TypeError):
-            self._makeOne(_IMAGE_CONTENT, 'nonsensefeature')
+            self._makeOne(IMAGE_CONTENT, 'nonsensefeature')
 
 
 class _Credentials(object):
@@ -319,6 +320,8 @@ def __init__(self, *responses):
         self._requested = []
 
     def api_request(self, **kw):
+        import json
+        json.dumps(kw.get('data', ''))  # Simulate JSON encoding.
         self._requested.append(kw)
         response, self._responses = self._responses[0], self._responses[1:]
         return response
diff --git a/vision/unit_tests/test_image.py b/vision/unit_tests/test_image.py
--- a/vision/unit_tests/test_image.py
+++ b/vision/unit_tests/test_image.py
@@ -12,18 +12,19 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
 import base64
+import unittest
 
 from google.cloud._helpers import _to_bytes
+from google.cloud._helpers import _bytes_to_unicode
 
+IMAGE_SOURCE = 'gs://some/image.jpg'
+IMAGE_CONTENT = _to_bytes('/9j/4QNURXhpZgAASUkq')
+B64_IMAGE_CONTENT = _bytes_to_unicode(base64.b64encode(IMAGE_CONTENT))
+CLIENT_MOCK = {'source': ''}
 
-class TestVisionImage(unittest.TestCase):
-    _IMAGE_SOURCE = 'gs://some/image.jpg'
-    _IMAGE_CONTENT = _to_bytes('/9j/4QNURXhpZgAASUkq')
-    _B64_IMAGE_CONTENT = base64.b64encode(_IMAGE_CONTENT)
-    _CLIENT_MOCK = {'source': ''}
 
+class TestVisionImage(unittest.TestCase):
     def _getTargetClass(self):
         from google.cloud.vision.image import Image
         return Image
@@ -32,37 +33,37 @@ def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
     def test_image_source_type_content(self):
-        image = self._makeOne(self._CLIENT_MOCK, content=self._IMAGE_CONTENT)
+        image = self._makeOne(CLIENT_MOCK, content=IMAGE_CONTENT)
 
         _AS_DICT = {
-            'content': self._B64_IMAGE_CONTENT
+            'content': B64_IMAGE_CONTENT
         }
 
-        self.assertEqual(self._B64_IMAGE_CONTENT, image.content)
+        self.assertEqual(B64_IMAGE_CONTENT, image.content)
         self.assertEqual(None, image.source)
         self.assertEqual(_AS_DICT, image.as_dict())
 
     def test_image_source_type_google_cloud_storage(self):
-        image = self._makeOne(self._CLIENT_MOCK, source_uri=self._IMAGE_SOURCE)
+        image = self._makeOne(CLIENT_MOCK, source_uri=IMAGE_SOURCE)
 
         _AS_DICT = {
             'source': {
-                'gcs_image_uri': self._IMAGE_SOURCE
+                'gcs_image_uri': IMAGE_SOURCE
             }
         }
 
-        self.assertEqual(self._IMAGE_SOURCE, image.source)
+        self.assertEqual(IMAGE_SOURCE, image.source)
         self.assertEqual(None, image.content)
         self.assertEqual(_AS_DICT, image.as_dict())
 
     def test_cannot_set_both_source_and_content(self):
-        image = self._makeOne(self._CLIENT_MOCK, content=self._IMAGE_CONTENT)
+        image = self._makeOne(CLIENT_MOCK, content=IMAGE_CONTENT)
 
-        self.assertEqual(self._B64_IMAGE_CONTENT, image.content)
+        self.assertEqual(B64_IMAGE_CONTENT, image.content)
         with self.assertRaises(AttributeError):
-            image.source = self._IMAGE_SOURCE
+            image.source = IMAGE_SOURCE
 
-        image = self._makeOne(self._CLIENT_MOCK, source_uri=self._IMAGE_SOURCE)
-        self.assertEqual(self._IMAGE_SOURCE, image.source)
+        image = self._makeOne(CLIENT_MOCK, source_uri=IMAGE_SOURCE)
+        self.assertEqual(IMAGE_SOURCE, image.source)
         with self.assertRaises(AttributeError):
-            image.content = self._IMAGE_CONTENT
+            image.content = IMAGE_CONTENT