Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions packages/google-auth/google/auth/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import json
import logging
import sys
import threading
from typing import Any, Dict, Mapping, Optional, Union
import urllib

Expand Down Expand Up @@ -534,3 +535,82 @@ def response_log(logger: logging.Logger, response: Any) -> None:
if is_logging_enabled(logger):
json_response = _parse_response(response)
_response_log_base(logger, json_response)


class LazyBasesMeta(type):
"""Metaclass that allows dynamic lazy resolution of class bases.

This metaclass avoids eager loading of heavy dependencies during import
time by allowing classes to inherit from a dummy class initially.
The real base classes are dynamically resolved and swapped in when the
class is first instantiated, subclassed, or has its attributes inspected.
"""

_lock = threading.RLock()

def __new__(mcls, name, bases, attrs):
# Automatically resolve lazy bases of any base classes at subclass definition time.
for base in bases:
if isinstance(base, LazyBasesMeta):
type(base)._resolve_bases(base)

cls = super(LazyBasesMeta, mcls).__new__(mcls, name, bases, attrs)
type.__setattr__(cls, "_lazy_bases_resolved", False)
return cls

def __call__(cls, *args, **kwargs):
type(cls)._resolve_bases(cls)
return super(LazyBasesMeta, cls).__call__(*args, **kwargs)

def __getattribute__(cls, name):
if name not in (
"_lazy_bases_resolved",
"_resolve_bases",
"_perform_resolve_bases",
):
type(cls)._resolve_bases(cls)
return super(LazyBasesMeta, cls).__getattribute__(name)

def _resolve_bases(cls):
try:
resolved = type.__getattribute__(cls, "_lazy_bases_resolved")
except AttributeError:
resolved = False

if not resolved:
if not hasattr(LazyBasesMeta, "_local"):
LazyBasesMeta._local = threading.local()
if not hasattr(LazyBasesMeta._local, "resolving"):
LazyBasesMeta._local.resolving = set()

cls_id = id(cls)
if cls_id in LazyBasesMeta._local.resolving:
return

with type(cls)._lock:
try:
resolved = type.__getattribute__(cls, "_lazy_bases_resolved")
except AttributeError:
resolved = False

if not resolved:
LazyBasesMeta._local.resolving.add(cls_id)
try:
type(cls)._perform_resolve_bases(cls)
type.__setattr__(cls, "_lazy_bases_resolved", True)
finally:
LazyBasesMeta._local.resolving.remove(cls_id)

def _perform_resolve_bases(cls):
pass
Comment thread
parthea marked this conversation as resolved.


class HeapDummy(object):
"""A heap-allocated dummy class.

This class serves as a safe initial base class for classes using LazyBasesMeta.
Inheriting from HeapDummy instead of built-in `object` ensures that Python's
memory layout check passes when we swap bases to another heap-allocated class.
"""

pass
97 changes: 75 additions & 22 deletions packages/google-auth/google/auth/transport/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,63 @@

import functools
import http.client as http_client
import importlib.util
import logging
import numbers
import time
from typing import Optional

try:
import requests
except ImportError as caught_exc: # pragma: NO COVER
from typing import Optional, Set, TYPE_CHECKING

# PEP 0810: Explicit Lazy Imports
# Python 3.15+ natively intercepts and defers these imports.
# Developers can disable this behavior and force eager imports.
# For more information, see:
# https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports_filter
# Older Python versions safely ignore this variable.
__lazy_modules__: Set[str] = {
"requests",
"requests.adapters",
"requests.exceptions",
"requests.packages",
"requests.packages.urllib3",
"requests.packages.urllib3.util",
"requests.packages.urllib3.util.ssl_",
}


if importlib.util.find_spec("requests") is None:
raise ImportError(
"The requests library is not installed from please install the requests package to use the requests transport."
) from caught_exc
"The requests library is not installed, please install the requests package to use the requests transport."
)

import requests
import requests.adapters # pylint: disable=ungrouped-imports
import requests.exceptions # pylint: disable=ungrouped-imports
from requests.packages.urllib3.util.ssl_ import ( # type: ignore
create_urllib3_context,
) # pylint: disable=ungrouped-imports

from google.auth import _helpers
from google.auth import exceptions
from google.auth import transport

from google.auth import _helpers, exceptions, transport
from google.auth.transport import _mtls_helper
import google.auth.transport._mtls_helper
from google.oauth2 import service_account

if TYPE_CHECKING:
_HTTPAdapterBase = requests.adapters.HTTPAdapter
_SessionBase = requests.Session
else:
_HTTPAdapterBase = _helpers.HeapDummy
_SessionBase = _helpers.HeapDummy


class _LazyBasesMeta(_helpers.LazyBasesMeta):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Isn't this class duplicated in urllub3?
  • Does the name need to be so similar to the one on _helpers?
  • Why is this needed? (docstrings should be added if adding the class is justified)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While both classes share the name _LazyBasesMeta and inherit from _helpers.LazyBasesMeta, they are not duplicates because their base resolution logic is transport-specific:

In requests.py, it resolves to requests.adapters.HTTPAdapter or requests.Session.

In urllib3.py, it resolves to urllib3._request_methods.RequestMethods or urllib3.request.RequestMethods.
To prevent requests and urllib3 modules from loading at import time, we cannot inherit directly from their classes (like requests.Session or urllib3.request.RequestMethods) during class definition time. We use LazyBasesMeta to inherit from HeapDummy initially, and then dynamically swap the base classes at runtime on first instantiation, subclassing, or attribute access.

The name _LazyBasesMeta is a private, module-specific subclass of _helpers.LazyBasesMeta which highlights its inheritance. I have added class-level docstrings to both files explaining this base class swapping mechanism.

def _perform_resolve_bases(cls):
current_bases = type.__getattribute__(cls, "__bases__")
if current_bases == (_helpers.HeapDummy,):
cls_name = type.__getattribute__(cls, "__name__")
if cls_name in ("_MutualTlsAdapter", "_MutualTlsOffloadAdapter"):
cls.__bases__ = (requests.adapters.HTTPAdapter,)
elif cls_name == "AuthorizedSession":
cls.__bases__ = (requests.Session,)


_LOGGER = logging.getLogger(__name__)
Comment thread
hebaalazzeh marked this conversation as resolved.

_DEFAULT_TIMEOUT = 120 # in seconds
Expand Down Expand Up @@ -84,9 +117,11 @@ class TimeoutGuard(object):
:class:`requests.exceptions.Timeout`.
"""

def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
def __init__(self, timeout, timeout_error_type=None):
self._timeout = timeout
self.remaining_timeout = timeout
if timeout_error_type is None:
timeout_error_type = requests.exceptions.Timeout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed? This would be a breaking change, that contradicts the docstring: If ``None``, a timeout error is never raised.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring comment "If None, a timeout error is never raised" refers to the timeout argument (the first argument), not timeout_error_type (the second argument). If timeout itself is None, the guard returns early, maintaining identical behavior.

The signature default value was changed from requests.exceptions.Timeout to None for the timeout_error_type argument to prevent eager evaluation of requests.exceptions at module import time.

Inside the constructor, if timeout_error_type is None, it is resolved to requests.exceptions.Timeout. If timeout itself is None, the guard returns early, maintaining identical behavior. This is a standard Python pattern to defer default evaluations.

self._timeout_error_type = timeout_error_type

def __enter__(self):
Expand Down Expand Up @@ -161,7 +196,7 @@ def __call__(
body=None,
headers=None,
timeout=_DEFAULT_TIMEOUT,
**kwargs
**kwargs,
):
"""Make an HTTP request using requests.

Expand Down Expand Up @@ -195,7 +230,7 @@ def __call__(
raise new_exc from caught_exc


class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
class _MutualTlsAdapter(_HTTPAdapterBase, metaclass=_LazyBasesMeta):
"""
A TransportAdapter that enables mutual TLS.

Expand All @@ -209,9 +244,13 @@ class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
"""

def __init__(self, cert, key):
import certifi
import ssl

from google.auth.transport.requests import (
create_urllib3_context,
)
import certifi

ctx_poolmanager = create_urllib3_context()
Comment thread
hebaalazzeh marked this conversation as resolved.
ctx_poolmanager.load_verify_locations(cafile=certifi.where())

Expand Down Expand Up @@ -261,7 +300,7 @@ def proxy_manager_for(self, *args, **kwargs):
return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)


class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
class _MutualTlsOffloadAdapter(_HTTPAdapterBase, metaclass=_LazyBasesMeta):
"""
A TransportAdapter that enables mutual TLS and offloads the client side
signing operation to the signing library.
Expand All @@ -284,7 +323,11 @@ class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
"""

def __init__(self, enterprise_cert_file_path):
from google.auth.transport.requests import (
create_urllib3_context,
)
import certifi

from google.auth.transport import _custom_tls_signer

self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
Expand All @@ -311,7 +354,7 @@ def proxy_manager_for(self, *args, **kwargs):
return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)


class AuthorizedSession(requests.Session):
class AuthorizedSession(_SessionBase, metaclass=_LazyBasesMeta):
"""A Requests Session class with credentials.

This class is used to perform requests to API endpoints that require
Expand Down Expand Up @@ -504,7 +547,7 @@ def request(
headers=None,
max_allowed_time=None,
timeout=_DEFAULT_TIMEOUT,
**kwargs
**kwargs,
):
"""Implementation of Requests' request.

Expand Down Expand Up @@ -566,7 +609,7 @@ def request(
data=data,
headers=request_headers,
timeout=timeout,
**kwargs
**kwargs,
)
remaining_time = guard.remaining_timeout

Expand Down Expand Up @@ -638,7 +681,7 @@ def request(
max_allowed_time=remaining_time,
timeout=timeout,
_credential_refresh_attempt=_credential_refresh_attempt + 1,
**kwargs
**kwargs,
)

return response
Expand All @@ -652,3 +695,13 @@ def close(self):
if self._auth_request_session is not None:
self._auth_request_session.close()
super(AuthorizedSession, self).close()


def __getattr__(name):
if name == "create_urllib3_context":
from requests.packages.urllib3.util.ssl_ import ( # type: ignore
create_urllib3_context,
)

return create_urllib3_context
raise AttributeError(f"module {__name__} has no attribute {name}")
51 changes: 42 additions & 9 deletions packages/google-auth/google/auth/transport/urllib3.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import http.client as http_client
import logging
from typing import Set, TYPE_CHECKING
import warnings

# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
Expand Down Expand Up @@ -50,16 +51,47 @@
) from caught_exc


from google.auth import _helpers
from google.auth import exceptions
from google.auth import transport
from google.auth import _helpers, exceptions, transport
from google.auth.transport import _mtls_helper
from google.oauth2 import service_account

if version.parse(urllib3.__version__) >= version.parse("2.0.0"): # pragma: NO COVER
RequestMethods = urllib3._request_methods.RequestMethods # type: ignore
else: # pragma: NO COVER
RequestMethods = urllib3.request.RequestMethods # type: ignore
if TYPE_CHECKING:
try:
from urllib3._request_methods import RequestMethods as _HttpBase
except ImportError:
try:
from urllib3.request import RequestMethods as _HttpBase # type: ignore
except ImportError:
_HttpBase = object # type: ignore
else:
_HttpBase = _helpers.HeapDummy


# PEP 0810: Explicit Lazy Imports
# Python 3.15+ natively intercepts and defers these imports.
# Developers can disable this behavior and force eager imports.
# For more information, see:
# https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports_filter
# Older Python versions safely ignore this variable.
__lazy_modules__: Set[str] = {
"urllib3",
"urllib3.exceptions",
"certifi",
"packaging",
"packaging.version",
}


class _LazyBasesMeta(_helpers.LazyBasesMeta):
def _perform_resolve_bases(cls):
current_bases = type.__getattribute__(cls, "__bases__")
if current_bases == (_helpers.HeapDummy,):
if version.parse(urllib3.__version__) >= version.parse("2.0.0"):
request_methods_class = urllib3._request_methods.RequestMethods # type: ignore
else:
request_methods_class = urllib3.request.RequestMethods # type: ignore
cls.__bases__ = (request_methods_class,)


_LOGGER = logging.getLogger(__name__)
Comment thread
hebaalazzeh marked this conversation as resolved.

Expand Down Expand Up @@ -176,9 +208,10 @@ def _make_mutual_tls_http(cert, key):
Raises:
google.auth.exceptions.MutualTLSChannelError: If the cert or key is invalid.
"""
import certifi
import ssl

import certifi

ctx = urllib3.util.ssl_.create_urllib3_context()
ctx.load_verify_locations(cafile=certifi.where())

Expand All @@ -203,7 +236,7 @@ def _make_mutual_tls_http(cert, key):
return http


class AuthorizedHttp(RequestMethods): # type: ignore
class AuthorizedHttp(_HttpBase, metaclass=_LazyBasesMeta):
"""A urllib3 HTTP class with credentials.

This class is used to perform requests to API endpoints that require
Expand Down
Loading