Skip to content

Commit f03bf10

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 f03bf10

3 files changed

Lines changed: 167 additions & 28 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: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,65 @@
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
3452
from requests.packages.urllib3.util.ssl_ import ( # type: ignore
3553
create_urllib3_context,
3654
) # pylint: disable=ungrouped-imports
3755

38-
from google.auth import _helpers
39-
from google.auth import exceptions
40-
from google.auth import transport
56+
from google.auth import _helpers, exceptions, transport
4157
from google.auth.transport import _mtls_helper
4258
import google.auth.transport._mtls_helper
4359
from google.oauth2 import service_account
4460

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

4782
_DEFAULT_TIMEOUT = 120 # in seconds
@@ -84,9 +119,11 @@ class TimeoutGuard(object):
84119
:class:`requests.exceptions.Timeout`.
85120
"""
86121

87-
def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
122+
def __init__(self, timeout, timeout_error_type=None):
88123
self._timeout = timeout
89124
self.remaining_timeout = timeout
125+
if timeout_error_type is None:
126+
timeout_error_type = requests.exceptions.Timeout
90127
self._timeout_error_type = timeout_error_type
91128

92129
def __enter__(self):
@@ -161,7 +198,7 @@ def __call__(
161198
body=None,
162199
headers=None,
163200
timeout=_DEFAULT_TIMEOUT,
164-
**kwargs
201+
**kwargs,
165202
):
166203
"""Make an HTTP request using requests.
167204
@@ -195,7 +232,7 @@ def __call__(
195232
raise new_exc from caught_exc
196233

197234

198-
class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
235+
class _MutualTlsAdapter(_HTTPAdapterBase, metaclass=_LazyBasesMeta):
199236
"""
200237
A TransportAdapter that enables mutual TLS.
201238
@@ -209,9 +246,10 @@ class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
209246
"""
210247

211248
def __init__(self, cert, key):
212-
import certifi
213249
import ssl
214250

251+
import certifi
252+
215253
ctx_poolmanager = create_urllib3_context()
216254
ctx_poolmanager.load_verify_locations(cafile=certifi.where())
217255

@@ -261,7 +299,7 @@ def proxy_manager_for(self, *args, **kwargs):
261299
return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
262300

263301

264-
class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
302+
class _MutualTlsOffloadAdapter(_HTTPAdapterBase, metaclass=_LazyBasesMeta):
265303
"""
266304
A TransportAdapter that enables mutual TLS and offloads the client side
267305
signing operation to the signing library.
@@ -285,6 +323,7 @@ class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
285323

286324
def __init__(self, enterprise_cert_file_path):
287325
import certifi
326+
288327
from google.auth.transport import _custom_tls_signer
289328

290329
self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
@@ -311,7 +350,7 @@ def proxy_manager_for(self, *args, **kwargs):
311350
return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)
312351

313352

314-
class AuthorizedSession(requests.Session):
353+
class AuthorizedSession(_SessionBase, metaclass=_LazyBasesMeta):
315354
"""A Requests Session class with credentials.
316355
317356
This class is used to perform requests to API endpoints that require
@@ -504,7 +543,7 @@ def request(
504543
headers=None,
505544
max_allowed_time=None,
506545
timeout=_DEFAULT_TIMEOUT,
507-
**kwargs
546+
**kwargs,
508547
):
509548
"""Implementation of Requests' request.
510549
@@ -566,7 +605,7 @@ def request(
566605
data=data,
567606
headers=request_headers,
568607
timeout=timeout,
569-
**kwargs
608+
**kwargs,
570609
)
571610
remaining_time = guard.remaining_timeout
572611

@@ -638,7 +677,7 @@ def request(
638677
max_allowed_time=remaining_time,
639678
timeout=timeout,
640679
_credential_refresh_attempt=_credential_refresh_attempt + 1,
641-
**kwargs
680+
**kwargs,
642681
)
643682

644683
return response

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)