refactor: split 'with_quota_project' into separate base class (#561)

Co-authored-by: Tres Seaver <tseaver@palladion.com>
diff --git a/google/auth/app_engine.py b/google/auth/app_engine.py
index fae00d0..f1d2128 100644
--- a/google/auth/app_engine.py
+++ b/google/auth/app_engine.py
@@ -77,7 +77,9 @@
     return app_identity.get_application_id()
 
 
-class Credentials(credentials.Scoped, credentials.Signing, credentials.Credentials):
+class Credentials(
+    credentials.Scoped, credentials.Signing, credentials.CredentialsWithQuotaProject
+):
     """App Engine standard environment credentials.
 
     These credentials use the App Engine App Identity API to obtain access
@@ -145,7 +147,7 @@
             quota_project_id=self.quota_project_id,
         )
 
-    @_helpers.copy_docstring(credentials.Credentials)
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
     def with_quota_project(self, quota_project_id):
         return self.__class__(
             scopes=self._scopes,
diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py
index e6da238..b7fca18 100644
--- a/google/auth/compute_engine/credentials.py
+++ b/google/auth/compute_engine/credentials.py
@@ -32,7 +32,7 @@
 from google.oauth2 import _client
 
 
-class Credentials(credentials.ReadOnlyScoped, credentials.Credentials):
+class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
     """Compute Engine Credentials.
 
     These credentials use the Google Compute Engine metadata server to obtain
@@ -118,7 +118,7 @@
         """False: Compute Engine credentials can not be scoped."""
         return False
 
-    @_helpers.copy_docstring(credentials.Credentials)
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
     def with_quota_project(self, quota_project_id):
         return self.__class__(
             service_account_email=self._service_account_email,
@@ -130,7 +130,7 @@
 _DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
 
 
-class IDTokenCredentials(credentials.Credentials, credentials.Signing):
+class IDTokenCredentials(credentials.CredentialsWithQuotaProject, credentials.Signing):
     """Open ID Connect ID Token-based service account credentials.
 
     These credentials relies on the default service account of a GCE instance.
@@ -254,7 +254,7 @@
                 quota_project_id=self._quota_project_id,
             )
 
-    @_helpers.copy_docstring(credentials.Credentials)
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
     def with_quota_project(self, quota_project_id):
 
         # since the signer is already instantiated,
diff --git a/google/auth/credentials.py b/google/auth/credentials.py
index 3f389b1..5ea36a0 100644
--- a/google/auth/credentials.py
+++ b/google/auth/credentials.py
@@ -133,6 +133,10 @@
             self.refresh(request)
         self.apply(headers)
 
+
+class CredentialsWithQuotaProject(Credentials):
+    """Abstract base for credentials supporting ``with_quota_project`` factory"""
+
     def with_quota_project(self, quota_project_id):
         """Returns a copy of these credentials with a modified quota project
 
@@ -143,7 +147,7 @@
         Returns:
             google.oauth2.credentials.Credentials: A new credentials instance.
         """
-        raise NotImplementedError("This class does not support quota project.")
+        raise NotImplementedError("This credential does not support quota project.")
 
 
 class AnonymousCredentials(Credentials):
@@ -182,9 +186,6 @@
     def before_request(self, request, method, url, headers):
         """Anonymous credentials do nothing to the request."""
 
-    def with_quota_project(self, quota_project_id):
-        raise ValueError("Anonymous credentials don't support quota project.")
-
 
 @six.add_metaclass(abc.ABCMeta)
 class ReadOnlyScoped(object):
diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py
index dbcb291..d2c5ded 100644
--- a/google/auth/impersonated_credentials.py
+++ b/google/auth/impersonated_credentials.py
@@ -115,7 +115,7 @@
         six.raise_from(new_exc, caught_exc)
 
 
-class Credentials(credentials.Credentials, credentials.Signing):
+class Credentials(credentials.CredentialsWithQuotaProject, credentials.Signing):
     """This module defines impersonated credentials which are essentially
     impersonated identities.
 
@@ -293,7 +293,7 @@
     def signer(self):
         return self
 
-    @_helpers.copy_docstring(credentials.Credentials)
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
     def with_quota_project(self, quota_project_id):
         return self.__class__(
             self._source_credentials,
@@ -305,7 +305,7 @@
         )
 
 
-class IDTokenCredentials(credentials.Credentials):
+class IDTokenCredentials(credentials.CredentialsWithQuotaProject):
     """Open ID Connect ID Token-based service account credentials.
 
     """
@@ -359,7 +359,7 @@
             quota_project_id=self._quota_project_id,
         )
 
-    @_helpers.copy_docstring(credentials.Credentials)
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
     def with_quota_project(self, quota_project_id):
         return self.__class__(
             target_credentials=self._target_credentials,
diff --git a/google/auth/jwt.py b/google/auth/jwt.py
index 35ae034..a4f04f5 100644
--- a/google/auth/jwt.py
+++ b/google/auth/jwt.py
@@ -288,7 +288,9 @@
     return payload
 
 
-class Credentials(google.auth.credentials.Signing, google.auth.credentials.Credentials):
+class Credentials(
+    google.auth.credentials.Signing, google.auth.credentials.CredentialsWithQuotaProject
+):
     """Credentials that use a JWT as the bearer token.
 
     These credentials require an "audience" claim. This claim identifies the
@@ -493,7 +495,7 @@
             quota_project_id=self._quota_project_id,
         )
 
-    @_helpers.copy_docstring(google.auth.credentials.Credentials)
+    @_helpers.copy_docstring(google.auth.credentials.CredentialsWithQuotaProject)
     def with_quota_project(self, quota_project_id):
         return self.__class__(
             self._signer,
@@ -554,7 +556,7 @@
 
 
 class OnDemandCredentials(
-    google.auth.credentials.Signing, google.auth.credentials.Credentials
+    google.auth.credentials.Signing, google.auth.credentials.CredentialsWithQuotaProject
 ):
     """On-demand JWT credentials.
 
@@ -721,7 +723,7 @@
             quota_project_id=self._quota_project_id,
         )
 
-    @_helpers.copy_docstring(google.auth.credentials.Credentials)
+    @_helpers.copy_docstring(google.auth.credentials.CredentialsWithQuotaProject)
     def with_quota_project(self, quota_project_id):
 
         return self.__class__(