Repository: GoogleCloudPlatform/google-auth-library-python
Pull request #99 (2016-12-14 18:45:43)

Fix cyclic import and add Signer.from_service_account_info


Patch:
diff --git a/google/auth/_service_account_info.py b/google/auth/_service_account_info.py
index b007abd..9891011 100644
--- a/google/auth/_service_account_info.py
+++ b/google/auth/_service_account_info.py
@@ -41,10 +41,7 @@ def from_dict(data, require=None):
         ValueError: if the data was in the wrong format, or if one of the
             required keys is missing.
     """
-    # Private key is always required.
-    keys_needed = set(('private_key',))
-    if require is not None:
-        keys_needed.update(require)
+    keys_needed = set(require if require is not None else [])
 
     missing = keys_needed.difference(six.iterkeys(data))
 
@@ -54,8 +51,7 @@ def from_dict(data, require=None):
             'fields {}.'.format(', '.join(missing)))
 
     # Create a signer.
-    signer = crypt.Signer.from_string(
-        data['private_key'], data.get('private_key_id'))
+    signer = crypt.Signer.from_service_account_info(data)
 
     return signer
 
diff --git a/google/auth/crypt.py b/google/auth/crypt.py
index 618afe5..c425045 100644
--- a/google/auth/crypt.py
+++ b/google/auth/crypt.py
@@ -38,6 +38,8 @@ To sign messages use :class:`Signer` with a private key::
     signature = signer.sign(message)
 
 """
+import io
+import json
 
 from pyasn1.codec.der import decoder
 from pyasn1_modules import pem
@@ -55,6 +57,8 @@ _PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
 _PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
                  '-----END PRIVATE KEY-----')
 _PKCS8_SPEC = PrivateKeyInfo()
+_JSON_FILE_PRIVATE_KEY = 'private_key'
+_JSON_FILE_PRIVATE_KEY_ID = 'private_key_id'
 
 
 def _bit_list_to_bytes(bit_list):
@@ -228,6 +232,30 @@ class Signer(object):
 
         return cls(private_key, key_id=key_id)
 
+    @classmethod
+    def from_service_account_info(cls, info):
+        """Creates a Signer instance instance from a dictionary containing
+        service account info in Google format.
+
+        Args:
+            info (Mapping[str, str]): The service account info in Google
+                format.
+
+        Returns:
+            Signer: The constructed signer.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        if _JSON_FILE_PRIVATE_KEY not in info:
+            raise ValueError(
+                'The private_key field was not found in the service account '
+                'info.')
+
+        return cls.from_string(
+            info[_JSON_FILE_PRIVATE_KEY],
+            info.get(_JSON_FILE_PRIVATE_KEY_ID))
+
     @classmethod
     def from_service_account_file(cls, filename):
         """Creates a Signer instance from a service account .json file
@@ -239,6 +267,7 @@ class Signer(object):
         Returns:
             Signer: The constructed signer.
         """
-        from google.auth import _service_account_info
-        _, signer = _service_account_info.from_filename(filename)
-        return signer
+        with io.open(filename, 'r', encoding='utf-8') as json_file:
+            data = json.load(json_file)
+
+        return cls.from_service_account_info(data)


Test patch:
diff --git a/tests/test__service_account_info.py b/tests/test__service_account_info.py
index 14f659a..4caea95 100644
--- a/tests/test__service_account_info.py
+++ b/tests/test__service_account_info.py
@@ -47,7 +47,7 @@ def test_from_dict_bad_private_key():
 
 def test_from_dict_bad_format():
     with pytest.raises(ValueError) as excinfo:
-        _service_account_info.from_dict({})
+        _service_account_info.from_dict({}, require=('meep',))
 
     assert excinfo.match(r'missing fields')
 
diff --git a/tests/test_crypt.py b/tests/test_crypt.py
index fd70f4b..9671230 100644
--- a/tests/test_crypt.py
+++ b/tests/test_crypt.py
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import os
 import json
+import os
 
 import mock
 from pyasn1_modules import pem
@@ -199,9 +199,23 @@ class TestSigner(object):
         with pytest.raises(ValueError):
             crypt.Signer.from_string(key_bytes)
 
+    def test_from_service_account_info(self):
+        signer = crypt.Signer.from_service_account_info(SERVICE_ACCOUNT_INFO)
+
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[
+            crypt._JSON_FILE_PRIVATE_KEY_ID]
+        assert isinstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_service_account_info_missing_key(self):
+        with pytest.raises(ValueError) as excinfo:
+            crypt.Signer.from_service_account_info({})
+
+        assert excinfo.match(crypt._JSON_FILE_PRIVATE_KEY)
+
     def test_from_service_account_file(self):
         signer = crypt.Signer.from_service_account_file(
             SERVICE_ACCOUNT_JSON_FILE)
 
-        assert signer.key_id == SERVICE_ACCOUNT_INFO['private_key_id']
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[
+            crypt._JSON_FILE_PRIVATE_KEY_ID]
         assert isinstance(signer._key, rsa.key.PrivateKey)