feat: lazy imports google auth (requests/urllib3) - #17679
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a __lazy_modules__ set in packages/google-auth/google/auth/transport/__init__.py to support lazy loading of transport modules. The review feedback correctly identifies an incorrect module name (_aiohttp_requests instead of aiohttp_requests) that would prevent lazy importing in Python 3.15+.
parthea
left a comment
There was a problem hiding this comment.
LGTM but holding off on formal approval until we have tests. We should assert that on Python 3.15+ the modules are indeed absent from sys.modules until accessed, and that on pre-3.15 environments they fallback cleanly to standard eager imports
import sys
import pytest
# List of modules we expect to be lazy
LAZY_MODULES = [
"google.auth.transport.requests",
"google.auth.transport.urllib3",
"google.auth.transport.grpc",
]
def clean_sys_modules():
"""Helper to ensure we start with a clean slate for import testing."""
for mod in LAZY_MODULES:
sys.modules.pop(mod, None)
@pytest.mark.skipif(sys.version_info < (3, 15), reason="PEP 810 requires Python 3.15+")
def test_lazy_imports_on_python_315():
clean_sys_modules()
# 1. Import the transport package
import google.auth.transport
# 2. Assert that none of the lazy modules have been eagerly loaded into sys.modules
for mod in LAZY_MODULES:
assert mod not in sys.modules
# 3. Access an attribute to trigger reification
from google.auth.transport import requests
_ = requests.__name__ # Trigger first-use reification
# 4. Assert that the module has now been reified and loaded
assert "google.auth.transport.requests" in sys.modules
@pytest.mark.skipif(sys.version_info >= (3, 15), reason="Testing fallback behavior on < 3.15")
def test_fallback_eager_imports_pre_315():
clean_sys_modules()
# On older Python, __lazy_modules__ is safely ignored, meaning they should eager-load
import google.auth.transport
for mod in LAZY_MODULES:
assert mod in sys.modules
|
I see the tests are failing. We also need to add |
|
Disregard my last comment.
We should only add From https://docs.python.org/3.15/reference/simple_stmts.html#lazy-imports
|
6e6fe6e to
eef11dc
Compare
|
addressed all comments |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces lazy import support for Python 3.15+ by defining __lazy_modules__ in several transport modules (grpc.py, requests.py, and urllib3.py) and adding corresponding integration tests. However, the feedback highlights that the lazy loading mechanism is currently defeated in grpc.py due to inheriting from grpc.AuthMetadataPlugin at the module level, and in urllib3.py due to a module-level version check on urllib3.__version__.
parthea
left a comment
There was a problem hiding this comment.
Please can you address the feedback from GCA?
|
\gemini |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces lazy loading support for heavy transport modules (grpc, requests, and urllib3) to optimize import times, particularly targeting Python 3.15+ (PEP 810), with fallback behavior for older versions. Key changes include defining __lazy_modules__, decoupling classes from eager parent imports, and adding a test suite to verify the lazy loading behavior. Feedback focuses on optimizing a performance bottleneck in urllib3.py's dynamic attribute resolution, addressing a bypassed ImportError check in requests.py due to deferred imports, and adding explanatory comments in grpc.py to prevent accidental regressions.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for PEP 0810 (Explicit Lazy Imports) in Python 3.15+ across the grpc, requests, and urllib3 transports. This is achieved by defining lazy_modules, removing eager base class inheritance, and lazily resolving attributes (such as RequestMethods in AuthorizedHttp). A test suite is also added to verify this behavior. The reviewer suggests improving the getattr implementation in AuthorizedHttp by dynamically checking for attributes on the underlying RequestMethods class rather than hardcoding specific method names, and lazily importing version to avoid eagerly loading the packaging module.
|
I updated the PR title to |
4a8aa04 to
c010c58
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces lazy loading of heavy dependencies (such as requests and urllib3) at import time to improve performance. It implements a LazyBasesMeta metaclass and a HeapDummy class to dynamically resolve and swap base classes upon instantiation, subclassing, or attribute inspection. Feedback was provided on _resolve_bases in LazyBasesMeta, which is susceptible to infinite recursion and race conditions in multi-threaded environments if attribute lookups occur during base resolution. A thread-local tracking mechanism was suggested to prevent re-entrant recursion.
c9e5ed3 to
c39064b
Compare
There was a problem hiding this comment.
Do these changes really makes sense? IIRC, neither of these modules are loaded by default, they have to be imported explicitly. And they are both designed to fail fast by raising an exception if the libraries they depend on are missing. Does it even make sense to add lazy loading to something with these access patterns?
Even if there's a good argument there, I'd still be pretty skeptical about this kind of change. google-auth is a key foundational library, so I don't want to be adding complex metaclass indirection unless there are overwhelming benefits. Have you done any benchmarking to justify the change?
| _SessionBase = _helpers.HeapDummy | ||
|
|
||
|
|
||
| class _LazyBasesMeta(_helpers.LazyBasesMeta): |
There was a problem hiding this comment.
- 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)
There was a problem hiding this comment.
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.
| self._timeout = timeout | ||
| self.remaining_timeout = timeout | ||
| if timeout_error_type is None: | ||
| timeout_error_type = requests.exceptions.Timeout |
There was a problem hiding this comment.
Why was this changed? This would be a breaking change, that contradicts the docstring: If ``None``, a timeout error is never raised.
There was a problem hiding this comment.
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.
General Response / Benchmarking JustificationComment:
Response: Yes, it is justified. While transport modules are not loaded by default under clean conditions, they are implicitly imported in two major ways:
Here are the benchmark results using the import profiling tool ( Profile for
|
| Metric | Eager Imports (Normal) | Lazy Imports (PR Branch) | Change |
|---|---|---|---|
| P50 (Median) Time | 2,278.46 ms | 1,287.93 ms | -990.53 ms (-43.5%) |
| Mean Time | 2,293.88 ms | 1,292.16 ms | -1,001.72 ms (-43.7%) |
| Loaded Modules | 395 | 176 | -219 modules (-55.4%) |
| Loaded Source Lines | 140,500 | 65,018 | -75,482 lines (-53.7%) |
| Physical RSS RAM | 45.68 MB | 36.14 MB | -9.54 MB (-20.9%) |
By deferring these heavy third-party libraries, we save ~990 ms of latency on cold start (a 43.5% speedup) and reduce physical RAM usage by ~9.5 MB, avoiding parsing ~75,000 lines of Python code at startup.
In both of these cases, the classes are immediately imported and used though, right? You need the requests module to execute
This is a real example of where lazy loading is beneficial, but it seems like a separate issue. Aren't we already planning to add lazy loading to the client.py files separately, to avoid importing the rest clients when using grpc clients (and vice versa)?
I don't think benchmarking You could try benchmarking an import of |
@daniel-sanche Thank you for the detailed feedback. I ran the benchmarks across 1. Benchmark:
|
| Metric | main (Baseline) |
PR Branch (feature/lazy-imports-google-auth) |
Delta |
|---|---|---|---|
| P50 (Median) Latency | 65.49 ms |
76.32 ms |
+10.83 ms (runtime variance) |
| Loaded Modules | 17 |
17 |
0 (Identical) |
| Loaded Source Lines | 7,339 |
7,339 |
0 (Identical) |
| Physical RSS RAM | 2.94 MB |
2.94 MB |
0.00 MB |
Top-level google.auth does not load transports on either branch, so the baseline overhead is identical.
2. Macro-Scale Scenario Comparison
Here is the side-by-side comparison across different import and client initialization scenarios in clean subprocesses:
| Scenario | Metric | main (Eager Baseline) |
PR Branch (PEP 810 Lazy) | Improvement |
|---|---|---|---|---|
import google.auth.transport.requests |
Latency | 359.94 ms |
263.75 ms |
-96.19 ms (-26.7%) |
| Loaded Modules | 447 |
294 |
-153 modules (-34.2%) | |
| RAM Added | 35.11 MB |
28.70 MB |
-6.41 MB (-18.3%) | |
requests in sys.modules? |
True (eagerly loaded) |
False (deferred) |
Deferred | |
urllib3 in sys.modules? |
True (eagerly loaded) |
False (deferred) |
Deferred | |
from google.oauth2 import service_account |
Latency | 337.01 ms |
266.94 ms |
-70.07 ms (-20.8%) |
| Loaded Modules | 293 |
292 |
-1 module |
|
import REST Transport (SecretManager) |
Latency | 707.48 ms |
631.24 ms |
-76.24 ms (-10.8%) |
| Loaded Modules | 618 |
612 |
-6 modules |
|
Construct gRPC Client (SecretManager) |
Latency | 1,816.01 ms |
1,652.17 ms |
-163.84 ms (-9.0%) |
| Loaded Modules | 629 |
624 |
-5 modules |
3. Why Lazy Loading Transports is Beneficial
-
Defers Heavy 3rd-Party Dependencies Across Call Sites:
- Over 40+ generated REST transport modules in
google-cloud-python(google/cloud/*/transports/rest.py) importfrom google.auth.transport.requests import AuthorizedSessionat module level. - On
main, simply importinggoogle.auth.transport.requestseagerly loadsrequests,urllib3,certifi,idna, andcharset_normalizer(447 modules, 35.1 MB RAM). - On this PR branch,
requestsandurllib3remain completely absent fromsys.modulesuntil an actual network request is executed (294 modules, 28.7 MB RAM, saving 153 modules and 6.4 MB RSS).
- Over 40+ generated REST transport modules in
-
Complementary to GAPIC Lazy Loading:
- While GAPIC client-level lazy loading defers importing the REST transport when using gRPC, having
google-authtransport modules natively support PEP 810 ensures that any library, utility, or user script importinggoogle.auth.transport.requestsorAuthorizedSessiondirectly does not pay the eager import tax at startup.
- While GAPIC client-level lazy loading defers importing the REST transport when using gRPC, having
-
Compatibility & Reliability:
- On Python 3.15+, base class resolution is handled transparently on first instantiation/access.
- On Python < 3.15, standard eager loading executes with zero behavioral difference.
- All 59 transport tests pass on Python 3.15, and all 883 unit tests pass on Python 3.11.
Let me know your thoughts!
aed7d4e to
a0b573f
Compare
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.
a0b573f to
83d2a92
Compare
…core This PR implements PEP 0810 explicit lazy imports in google-cloud-core. On Python 3.15+, this defers loading of heavy inner modules and third-party dependencies (grpcio, cryptography, requests, and protobuf descriptor pools) to reduce serverless cold starts and memory footprints. On Python 3.14 and below, this falls back safely to eager execution with zero backwards-compatibility risk. ### Related Links - GAPIC Implementation PR: #17591 - google-api-core gapic_v1 PR: #17673 - google-api-core operations_v1 PR: #17724 - google-auth transport PR: #17679
…core This PR implements PEP 0810 explicit lazy imports in google-cloud-core. On Python 3.15+, this defers loading of heavy inner modules and third-party dependencies (grpcio, cryptography, requests, and protobuf descriptor pools) to reduce serverless cold starts and memory footprints. On Python 3.14 and below, this falls back safely to eager execution with zero backwards-compatibility risk. ### Related Links - GAPIC Implementation PR: #17591 - google-api-core gapic_v1 PR: #17673 - google-api-core operations_v1 PR: #17724 - google-auth transport PR: #17679
…core This PR implements PEP 0810 explicit lazy imports in google-cloud-core. On Python 3.15+, this defers loading of heavy inner modules and third-party dependencies (grpcio, cryptography, requests, and protobuf descriptor pools) to reduce serverless cold starts and memory footprints. On Python 3.14 and below, this falls back safely to eager execution with zero backwards-compatibility risk. ### Related Links - GAPIC Implementation PR: #17591 - google-api-core gapic_v1 PR: #17673 - google-api-core operations_v1 PR: #17724 - google-auth transport PR: #17679
…core This PR implements PEP 0810 explicit lazy imports in google-cloud-core. On Python 3.15+, this defers loading of heavy inner modules and third-party dependencies (grpcio, cryptography, requests, and protobuf descriptor pools) to reduce serverless cold starts and memory footprints. On Python 3.14 and below, this falls back safely to eager execution with zero backwards-compatibility risk. ### Related Links - GAPIC Implementation PR: #17591 - google-api-core gapic_v1 PR: #17673 - google-api-core operations_v1 PR: #17724 - google-auth transport PR: #17679
…core This PR implements PEP 0810 explicit lazy imports in google-cloud-core. On Python 3.15+, this defers loading of heavy inner modules and third-party dependencies (grpcio, cryptography, requests, and protobuf descriptor pools) to reduce serverless cold starts and memory footprints. On Python 3.14 and below, this falls back safely to eager execution with zero backwards-compatibility risk. ### Related Links - GAPIC Implementation PR: #17591 - google-api-core gapic_v1 PR: #17673 - google-api-core operations_v1 PR: #17724 - google-auth transport PR: #17679
|
I think the data just validates my concerns. I'm still seeing a lot of cost with no real benefit here: Benefits
We can safely assume anyone importing a rest client plans to use it, so we would be at best deferring costs, not removing them. So it looks like, best case, we defer ~200ms computation, and momentarily avoid ~7mb of memory Also, keep in mind this cost is only paid once, no matter how many libraries they import Costs
The metaclasses aren't gated by version, so wouldn't we have these costs on all python versions?
Tests can be tricky to validate this kind of monkey-patch code, because they generally run against the resolved classes, so they may miss things (i.e. issubclass() checks) I think we should close this, and focus on more straight-forward improvements |
This PR implements PEP 0810 explicit lazy imports in
google-authtransports (requestsandurllib3).On Python 3.15+, this defers loading of heavy third-party networking libraries (
requestsandurllib3) 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.Related Links