Repository: googleapis/google-auth-library-python

Issue:
Failure with Python 3.9 due to use of deprecated collections.Mapping
#### Environment details

  - OS: Linux
  - Python version: 3.9 alpha 2
  - pip version: 19.3.1
  - `google-auth` version: 1.10.0

#### Steps to reproduce

  1. Use Python 3.9
  2. Attempt to decode an ID Token:
    google.oauth2.id_token.verify_oauth2_token()...

Expected results:
decoded ID token

Actual results:
AttributeError: module 'collections' has no attribute 'Mapping'
see full crash at:
AttributeError: module 'collections' has no attribute 'Mapping'

Further details:
collections.Mapping was moved to collections.abc.Mapping in Python 3.3 but mapped back to collections up to Python 3.8. In Python 3.9 the legacy mapping is removed so we break. See:
https://docs.python.org/3/library/collections.html#


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9762d0c..70032ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@
 
 [1]: https://pypi.org/project/google-auth/#history
 
+### [1.10.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.10.0...v1.10.1) (2020-01-10)
+
+
+### Bug Fixes
+
+* **google.auth.compute_engine.metadata:** add retry to google.auth.compute_engine._metadata.get() ([#398](https://www.github.com/googleapis/google-auth-library-python/issues/398)) ([af29c1a](https://www.github.com/googleapis/google-auth-library-python/commit/af29c1a9fd9282b38867961e4053f74f018a3815)), closes [#211](https://www.github.com/googleapis/google-auth-library-python/issues/211) [#323](https://www.github.com/googleapis/google-auth-library-python/issues/323) [#323](https://www.github.com/googleapis/google-auth-library-python/issues/323) [#211](https://www.github.com/googleapis/google-auth-library-python/issues/211)
+* always pass body of type bytes to `google.auth.transport.Request` ([#421](https://www.github.com/googleapis/google-auth-library-python/issues/421)) ([a57a770](https://www.github.com/googleapis/google-auth-library-python/commit/a57a7708cfea635b5030f8c7ba10c967715f9a87)), closes [#318](https://www.github.com/googleapis/google-auth-library-python/issues/318)
+
 ## [1.10.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.9.0...v1.10.0) (2019-12-18)
 
 
diff --git a/google/auth/compute_engine/_metadata.py b/google/auth/compute_engine/_metadata.py
index f4fae72..30cd3d4 100644
--- a/google/auth/compute_engine/_metadata.py
+++ b/google/auth/compute_engine/_metadata.py
@@ -99,7 +99,7 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
     return False
 
 
-def get(request, path, root=_METADATA_ROOT, recursive=False):
+def get(request, path, root=_METADATA_ROOT, recursive=False, retry_count=5):
     """Fetch a resource from the metadata server.
 
     Args:
@@ -111,6 +111,8 @@ def get(request, path, root=_METADATA_ROOT, recursive=False):
         recursive (bool): Whether to do a recursive query of metadata. See
             https://cloud.google.com/compute/docs/metadata#aggcontents for more
             details.
+        retry_count (int): How many times to attempt connecting to metadata
+            server using above timeout.
 
     Returns:
         Union[Mapping, str]: If the metadata server returns JSON, a mapping of
@@ -129,7 +131,24 @@ def get(request, path, root=_METADATA_ROOT, recursive=False):
 
     url = _helpers.update_query(base_url, query_params)
 
-    response = request(url=url, method="GET", headers=_METADATA_HEADERS)
+    retries = 0
+    while retries < retry_count:
+        try:
+            response = request(url=url, method="GET", headers=_METADATA_HEADERS)
+            break
+
+        except exceptions.TransportError:
+            _LOGGER.info(
+                "Compute Engine Metadata server unavailable on" "attempt %s of %s",
+                retries + 1,
+                retry_count,
+            )
+            retries += 1
+    else:
+        raise exceptions.TransportError(
+            "Failed to retrieve {} from the Google Compute Engine"
+            "metadata service. Compute Engine Metadata server unavailable".format(url)
+        )
 
     if response.status == http_client.OK:
         content = _helpers.from_bytes(response.data)
diff --git a/google/auth/iam.py b/google/auth/iam.py
index a438726..0ab5b55 100644
--- a/google/auth/iam.py
+++ b/google/auth/iam.py
@@ -70,7 +70,9 @@ class Signer(crypt.Signer):
         method = "POST"
         url = _SIGN_BLOB_URI.format(self._service_account_email)
         headers = {}
-        body = json.dumps({"bytesToSign": base64.b64encode(message).decode("utf-8")})
+        body = json.dumps(
+            {"bytesToSign": base64.b64encode(message).decode("utf-8")}
+        ).encode("utf-8")
 
         self._credentials.before_request(self._request, method, url, headers)
         response = self._request(url=url, method=method, body=body, headers=headers)
diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py
index 70fa5dc..bc7031e 100644
--- a/google/auth/impersonated_credentials.py
+++ b/google/auth/impersonated_credentials.py
@@ -84,7 +84,7 @@ def _make_iam_token_request(request, principal, headers, body):
     """
     iam_endpoint = _IAM_ENDPOINT.format(principal)
 
-    body = json.dumps(body)
+    body = json.dumps(body).encode("utf-8")
 
     response = request(url=iam_endpoint, method="POST", headers=headers, body=body)
 
diff --git a/google/auth/jwt.py b/google/auth/jwt.py
index a30c575..361c456 100644
--- a/google/auth/jwt.py
+++ b/google/auth/jwt.py
@@ -40,7 +40,10 @@ You can also skip verification::
 
 """
 
-import collections
+try:
+    from collections.abc import Mapping
+except ImportError: # Python 2.7 compatibility
+    from collections import Mapping
 import copy
 import datetime
 import json
@@ -215,7 +218,7 @@ def decode(token, certs=None, verify=True, audience=None):
 
     # If certs is specified as a dictionary of key IDs to certificates, then
     # use the certificate identified by the key ID in the token header.
-    if isinstance(certs, collections.Mapping):
+    if isinstance(certs, Mapping):
         key_id = header.get("kid")
         if key_id:
             if key_id not in certs:
diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py
index 4cf7a7f..4ba31a8 100644
--- a/google/oauth2/_client.py
+++ b/google/oauth2/_client.py
@@ -95,7 +95,7 @@ def _token_endpoint_request(request, token_uri, body):
         google.auth.exceptions.RefreshError: If the token endpoint returned
             an error.
     """
-    body = urllib.parse.urlencode(body)
+    body = urllib.parse.urlencode(body).encode("utf-8")
     headers = {"content-type": _URLENCODED_CONTENT_TYPE}
 
     retry = 0
diff --git a/setup.py b/setup.py
index 954ce17..1b5eac4 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@ DEPENDENCIES = (
 with io.open("README.rst", "r") as fh:
     long_description = fh.read()
 
-version = "1.10.0"
+version = "1.10.1"
 
 setup(
     name="google-auth",


Test patch:
diff --git a/tests/compute_engine/test__metadata.py b/tests/compute_engine/test__metadata.py
index bd06b74..0898e1f 100644
--- a/tests/compute_engine/test__metadata.py
+++ b/tests/compute_engine/test__metadata.py
@@ -30,14 +30,17 @@ from google.auth.compute_engine import _metadata
 PATH = "instance/service-accounts/default"
 
 
-def make_request(data, status=http_client.OK, headers=None):
+def make_request(data, status=http_client.OK, headers=None, retry=False):
     response = mock.create_autospec(transport.Response, instance=True)
     response.status = status
     response.data = _helpers.to_bytes(data)
     response.headers = headers or {}
 
     request = mock.create_autospec(transport.Request)
-    request.return_value = response
+    if retry:
+        request.side_effect = [exceptions.TransportError(), response]
+    else:
+        request.return_value = response
 
     return request
 
@@ -55,6 +58,20 @@ def test_ping_success():
     )
 
 
+def test_ping_success_retry():
+    request = make_request("", headers=_metadata._METADATA_HEADERS, retry=True)
+
+    assert _metadata.ping(request)
+
+    request.assert_called_with(
+        method="GET",
+        url=_metadata._METADATA_IP_ROOT,
+        headers=_metadata._METADATA_HEADERS,
+        timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
+    )
+    assert request.call_count == 2
+
+
 def test_ping_failure_bad_flavor():
     request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"})
 
@@ -105,6 +122,25 @@ def test_get_success_json():
     assert result[key] == value
 
 
+def test_get_success_retry():
+    key, value = "foo", "bar"
+
+    data = json.dumps({key: value})
+    request = make_request(
+        data, headers={"content-type": "application/json"}, retry=True
+    )
+
+    result = _metadata.get(request, PATH)
+
+    request.assert_called_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert request.call_count == 2
+    assert result[key] == value
+
+
 def test_get_success_text():
     data = "foobar"
     request = make_request(data, headers={"content-type": "text/plain"})
@@ -154,6 +190,23 @@ def test_get_failure():
     )
 
 
+def test_get_failure_connection_failed():
+    request = make_request("")
+    request.side_effect = exceptions.TransportError()
+
+    with pytest.raises(exceptions.TransportError) as excinfo:
+        _metadata.get(request, PATH)
+
+    assert excinfo.match(r"Compute Engine Metadata server unavailable")
+
+    request.assert_called_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert request.call_count == 5
+
+
 def test_get_failure_bad_json():
     request = make_request("{", headers={"content-type": "application/json"})
 
diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py
index 9cf59eb..052390a 100644
--- a/tests/oauth2/test__client.py
+++ b/tests/oauth2/test__client.py
@@ -96,7 +96,7 @@ def test__token_endpoint_request():
         method="POST",
         url="http://example.com",
         headers={"content-type": "application/x-www-form-urlencoded"},
-        body="test=params",
+        body="test=params".encode("utf-8"),
     )
 
     # Check result
@@ -131,7 +131,7 @@ def test__token_endpoint_request_internal_failure_error():
 
 
 def verify_request_params(request, params):
-    request_body = request.call_args[1]["body"]
+    request_body = request.call_args[1]["body"].decode("utf-8")
     request_params = urllib.parse.parse_qs(request_body)
 
     for key, value in six.iteritems(params):