Skip to content

Commit c010c58

Browse files
committed
feat(auth): PEP 0810 explicit lazy imports in google-auth transports
This PR implements PEP 0810 explicit lazy imports in google-auth transports (requests and urllib3). On Python 3.15+, this defers loading of heavy third-party networking libraries (requests and urllib3) to reduce serverless cold-start latency and peak memory usage. On Python 3.14 and below, this falls back safely to standard eager loading with zero backwards-compatibility risk.
1 parent 8ed6b71 commit c010c58

3 files changed

Lines changed: 184 additions & 31 deletions

File tree

packages/google-auth/google/auth/_helpers.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import json
2323
import logging
2424
import sys
25+
import threading
2526
from typing import Any, Dict, Mapping, Optional, Union
2627
import urllib
2728

@@ -534,3 +535,69 @@ def response_log(logger: logging.Logger, response: Any) -> None:
534535
if is_logging_enabled(logger):
535536
json_response = _parse_response(response)
536537
_response_log_base(logger, json_response)
538+
539+
540+
class LazyBasesMeta(type):
541+
"""Metaclass that allows dynamic lazy resolution of class bases.
542+
543+
This metaclass avoids eager loading of heavy dependencies during import
544+
time by allowing classes to inherit from a dummy class initially.
545+
The real base classes are dynamically resolved and swapped in when the
546+
class is first instantiated, subclassed, or has its attributes inspected.
547+
"""
548+
549+
_lock = threading.RLock()
550+
551+
def __new__(mcls, name, bases, attrs):
552+
# Automatically resolve lazy bases of any base classes at subclass definition time.
553+
for base in bases:
554+
if isinstance(base, LazyBasesMeta):
555+
type(base)._resolve_bases(base)
556+
557+
cls = super(LazyBasesMeta, mcls).__new__(mcls, name, bases, attrs)
558+
type.__setattr__(cls, "_lazy_bases_resolved", False)
559+
return cls
560+
561+
def __call__(cls, *args, **kwargs):
562+
type(cls)._resolve_bases(cls)
563+
return super(LazyBasesMeta, cls).__call__(*args, **kwargs)
564+
565+
def __getattribute__(cls, name):
566+
if name not in (
567+
"_lazy_bases_resolved",
568+
"_resolve_bases",
569+
"_perform_resolve_bases",
570+
):
571+
type(cls)._resolve_bases(cls)
572+
return super(LazyBasesMeta, cls).__getattribute__(name)
573+
574+
def _resolve_bases(cls):
575+
try:
576+
resolved = type.__getattribute__(cls, "_lazy_bases_resolved")
577+
except AttributeError:
578+
resolved = False
579+
580+
if not resolved:
581+
with type(cls)._lock:
582+
try:
583+
resolved = type.__getattribute__(cls, "_lazy_bases_resolved")
584+
except AttributeError:
585+
resolved = False
586+
587+
if not resolved:
588+
type(cls)._perform_resolve_bases(cls)
589+
type.__setattr__(cls, "_lazy_bases_resolved", True)
590+
591+
def _perform_resolve_bases(cls):
592+
pass
593+
594+
595+
class HeapDummy(object):
596+
"""A heap-allocated dummy class.
597+
598+
This class serves as a safe initial base class for classes using LazyBasesMeta.
599+
Inheriting from HeapDummy instead of built-in `object` ensures that Python's
600+
memory layout check passes when we swap bases to another heap-allocated class.
601+
"""
602+
603+
pass

packages/google-auth/google/auth/transport/requests.py

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,63 @@
1818

1919
import functools
2020
import http.client as http_client
21+
import importlib.util
2122
import logging
2223
import numbers
2324
import time
24-
from typing import Optional
25-
26-
try:
27-
import requests
28-
except ImportError as caught_exc: # pragma: NO COVER
25+
from typing import Optional, Set, TYPE_CHECKING
26+
27+
# PEP 0810: Explicit Lazy Imports
28+
# Python 3.15+ natively intercepts and defers these imports.
29+
# Developers can disable this behavior and force eager imports.
30+
# For more information, see:
31+
# https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports_filter
32+
# Older Python versions safely ignore this variable.
33+
__lazy_modules__: Set[str] = {
34+
"requests",
35+
"requests.adapters",
36+
"requests.exceptions",
37+
"requests.packages",
38+
"requests.packages.urllib3",
39+
"requests.packages.urllib3.util",
40+
"requests.packages.urllib3.util.ssl_",
41+
}
42+
43+
44+
if importlib.util.find_spec("requests") is None:
2945
raise ImportError(
30-
"The requests library is not installed from please install the requests package to use the requests transport."
31-
) from caught_exc
46+
"The requests library is not installed, please install the requests package to use the requests transport."
47+
)
48+
49+
import requests
3250
import requests.adapters # pylint: disable=ungrouped-imports
3351
import requests.exceptions # pylint: disable=ungrouped-imports
34-
from requests.packages.urllib3.util.ssl_ import ( # type: ignore
35-
create_urllib3_context,
36-
) # pylint: disable=ungrouped-imports
3752

38-
from google.auth import _helpers
39-
from google.auth import exceptions
40-
from google.auth import transport
53+
54+
from google.auth import _helpers, exceptions, transport
4155
from google.auth.transport import _mtls_helper
4256
import google.auth.transport._mtls_helper
4357
from google.oauth2 import service_account
4458

59+
if TYPE_CHECKING:
60+
_HTTPAdapterBase = requests.adapters.HTTPAdapter
61+
_SessionBase = requests.Session
62+
else:
63+
_HTTPAdapterBase = _helpers.HeapDummy
64+
_SessionBase = _helpers.HeapDummy
65+
66+
67+
class _LazyBasesMeta(_helpers.LazyBasesMeta):
68+
def _perform_resolve_bases(cls):
69+
current_bases = type.__getattribute__(cls, "__bases__")
70+
if current_bases == (_helpers.HeapDummy,):
71+
cls_name = type.__getattribute__(cls, "__name__")
72+
if cls_name in ("_MutualTlsAdapter", "_MutualTlsOffloadAdapter"):
73+
cls.__bases__ = (requests.adapters.HTTPAdapter,)
74+
elif cls_name == "AuthorizedSession":
75+
cls.__bases__ = (requests.Session,)
76+
77+
4578
_LOGGER = logging.getLogger(__name__)
4679

4780
_DEFAULT_TIMEOUT = 120 # in seconds
@@ -84,9 +117,11 @@ class TimeoutGuard(object):
84117
:class:`requests.exceptions.Timeout`.
85118
"""
86119

87-
def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
120+
def __init__(self, timeout, timeout_error_type=None):
88121
self._timeout = timeout
89122
self.remaining_timeout = timeout
123+
if timeout_error_type is None:
124+
timeout_error_type = requests.exceptions.Timeout
90125
self._timeout_error_type = timeout_error_type
91126

92127
def __enter__(self):
@@ -161,7 +196,7 @@ def __call__(
161196
body=None,
162197
headers=None,
163198
timeout=_DEFAULT_TIMEOUT,
164-
**kwargs
199+
**kwargs,
165200
):
166201
"""Make an HTTP request using requests.
167202
@@ -195,7 +230,7 @@ def __call__(
195230
raise new_exc from caught_exc
196231

197232

198-
class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
233+
class _MutualTlsAdapter(_HTTPAdapterBase, metaclass=_LazyBasesMeta):
199234
"""
200235
A TransportAdapter that enables mutual TLS.
201236
@@ -209,9 +244,13 @@ class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
209244
"""
210245

211246
def __init__(self, cert, key):
212-
import certifi
213247
import ssl
214248

249+
from google.auth.transport.requests import (
250+
create_urllib3_context,
251+
)
252+
import certifi
253+
215254
ctx_poolmanager = create_urllib3_context()
216255
ctx_poolmanager.load_verify_locations(cafile=certifi.where())
217256

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

263302

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

286325
def __init__(self, enterprise_cert_file_path):
326+
from google.auth.transport.requests import (
327+
create_urllib3_context,
328+
)
287329
import certifi
330+
288331
from google.auth.transport import _custom_tls_signer
289332

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

313356

314-
class AuthorizedSession(requests.Session):
357+
class AuthorizedSession(_SessionBase, metaclass=_LazyBasesMeta):
315358
"""A Requests Session class with credentials.
316359
317360
This class is used to perform requests to API endpoints that require
@@ -504,7 +547,7 @@ def request(
504547
headers=None,
505548
max_allowed_time=None,
506549
timeout=_DEFAULT_TIMEOUT,
507-
**kwargs
550+
**kwargs,
508551
):
509552
"""Implementation of Requests' request.
510553
@@ -566,7 +609,7 @@ def request(
566609
data=data,
567610
headers=request_headers,
568611
timeout=timeout,
569-
**kwargs
612+
**kwargs,
570613
)
571614
remaining_time = guard.remaining_timeout
572615

@@ -638,7 +681,7 @@ def request(
638681
max_allowed_time=remaining_time,
639682
timeout=timeout,
640683
_credential_refresh_attempt=_credential_refresh_attempt + 1,
641-
**kwargs
684+
**kwargs,
642685
)
643686

644687
return response
@@ -652,3 +695,13 @@ def close(self):
652695
if self._auth_request_session is not None:
653696
self._auth_request_session.close()
654697
super(AuthorizedSession, self).close()
698+
699+
700+
def __getattr__(name):
701+
if name == "create_urllib3_context":
702+
from requests.packages.urllib3.util.ssl_ import ( # type: ignore
703+
create_urllib3_context,
704+
)
705+
706+
return create_urllib3_context
707+
raise AttributeError(f"module {__name__} has no attribute {name}")

packages/google-auth/google/auth/transport/urllib3.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import http.client as http_client
2020
import logging
21+
from typing import Set, TYPE_CHECKING
2122
import warnings
2223

2324
# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
@@ -50,16 +51,47 @@
5051
) from caught_exc
5152

5253

53-
from google.auth import _helpers
54-
from google.auth import exceptions
55-
from google.auth import transport
54+
from google.auth import _helpers, exceptions, transport
5655
from google.auth.transport import _mtls_helper
5756
from google.oauth2 import service_account
5857

59-
if version.parse(urllib3.__version__) >= version.parse("2.0.0"): # pragma: NO COVER
60-
RequestMethods = urllib3._request_methods.RequestMethods # type: ignore
61-
else: # pragma: NO COVER
62-
RequestMethods = urllib3.request.RequestMethods # type: ignore
58+
if TYPE_CHECKING:
59+
try:
60+
from urllib3._request_methods import RequestMethods as _HttpBase
61+
except ImportError:
62+
try:
63+
from urllib3.request import RequestMethods as _HttpBase # type: ignore
64+
except ImportError:
65+
_HttpBase = object # type: ignore
66+
else:
67+
_HttpBase = _helpers.HeapDummy
68+
69+
70+
# PEP 0810: Explicit Lazy Imports
71+
# Python 3.15+ natively intercepts and defers these imports.
72+
# Developers can disable this behavior and force eager imports.
73+
# For more information, see:
74+
# https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports_filter
75+
# Older Python versions safely ignore this variable.
76+
__lazy_modules__: Set[str] = {
77+
"urllib3",
78+
"urllib3.exceptions",
79+
"certifi",
80+
"packaging",
81+
"packaging.version",
82+
}
83+
84+
85+
class _LazyBasesMeta(_helpers.LazyBasesMeta):
86+
def _perform_resolve_bases(cls):
87+
current_bases = type.__getattribute__(cls, "__bases__")
88+
if current_bases == (_helpers.HeapDummy,):
89+
if version.parse(urllib3.__version__) >= version.parse("2.0.0"):
90+
request_methods_class = urllib3._request_methods.RequestMethods # type: ignore
91+
else:
92+
request_methods_class = urllib3.request.RequestMethods # type: ignore
93+
cls.__bases__ = (request_methods_class,)
94+
6395

6496
_LOGGER = logging.getLogger(__name__)
6597

@@ -176,9 +208,10 @@ def _make_mutual_tls_http(cert, key):
176208
Raises:
177209
google.auth.exceptions.MutualTLSChannelError: If the cert or key is invalid.
178210
"""
179-
import certifi
180211
import ssl
181212

213+
import certifi
214+
182215
ctx = urllib3.util.ssl_.create_urllib3_context()
183216
ctx.load_verify_locations(cafile=certifi.where())
184217

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

205238

206-
class AuthorizedHttp(RequestMethods): # type: ignore
239+
class AuthorizedHttp(_HttpBase, metaclass=_LazyBasesMeta):
207240
"""A urllib3 HTTP class with credentials.
208241
209242
This class is used to perform requests to API endpoints that require

0 commit comments

Comments
 (0)