diff --git a/packages/google-api-core/google/api_core/gapic_v1/__init__.py b/packages/google-api-core/google/api_core/gapic_v1/__init__.py index e5b7ad352ee6..9b71a6178079 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/__init__.py +++ b/packages/google-api-core/google/api_core/gapic_v1/__init__.py @@ -12,18 +12,51 @@ # See the License for the specific language governing permissions and # limitations under the License. -from google.api_core.gapic_v1 import client_info -from google.api_core.gapic_v1 import config -from google.api_core.gapic_v1 import config_async -from google.api_core.gapic_v1 import method -from google.api_core.gapic_v1 import method_async -from google.api_core.gapic_v1 import routing_header +import importlib.util +from typing import Set -__all__ = [ - "client_info", - "config", - "config_async", - "method", - "method_async", - "routing_header", -] +_has_grpc = importlib.util.find_spec("grpc") is not None + +# 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] = { + "google.api_core.gapic_v1.client_info", + "google.api_core.gapic_v1.routing_header", +} + +if _has_grpc: + __lazy_modules__.update( + { + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.config_async", + "google.api_core.gapic_v1.method", + "google.api_core.gapic_v1.method_async", + } + ) + +from google.api_core.gapic_v1 import client_info # noqa: E402 +from google.api_core.gapic_v1 import routing_header # noqa: E402 + +if _has_grpc: + from google.api_core.gapic_v1 import config # noqa: F401 + from google.api_core.gapic_v1 import config_async # noqa: F401 + from google.api_core.gapic_v1 import method # noqa: F401 + from google.api_core.gapic_v1 import method_async # noqa: F401 + + __all__ = [ + "client_info", + "routing_header", + "config", + "config_async", + "method", + "method_async", + ] +else: + __all__ = [ + "client_info", + "routing_header", + ] diff --git a/packages/google-api-core/tests/unit/gapic/test_lazy_imports.py b/packages/google-api-core/tests/unit/gapic/test_lazy_imports.py new file mode 100644 index 000000000000..597249610a5b --- /dev/null +++ b/packages/google-api-core/tests/unit/gapic/test_lazy_imports.py @@ -0,0 +1,97 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import subprocess +import sys + +import pytest + +SCRIPT_PYTHON_315 = """ +import sys +import google.api_core.gapic_v1 + +# The inner gapic_v1 modules should NOT be loaded yet +HEAVY_MODULES = [ + "google.api_core.gapic_v1.client_info", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.config_async", + "google.api_core.gapic_v1.method", + "google.api_core.gapic_v1.method_async", + "google.api_core.gapic_v1.routing_header", +] + +for mod in HEAVY_MODULES: + if mod in sys.modules: + print(f"FAILED: {mod} was eagerly loaded into sys.modules") + sys.exit(1) + +# Trigger reification +import google.api_core.gapic_v1.client_info +_ = google.api_core.gapic_v1.client_info.__name__ + +if "google.api_core.gapic_v1.client_info" not in sys.modules: + print("FAILED: google.api_core.gapic_v1.client_info was not lazily loaded upon access") + sys.exit(2) + +sys.exit(0) +""" + +SCRIPT_PRE_315 = """ +import sys +import google.api_core.gapic_v1 + +try: + import grpc + has_grpc = True +except ImportError: + has_grpc = False + +HEAVY_MODULES = [ + "google.api_core.gapic_v1.client_info", + "google.api_core.gapic_v1.routing_header", +] + +if has_grpc: + HEAVY_MODULES.extend([ + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.config_async", + "google.api_core.gapic_v1.method", + "google.api_core.gapic_v1.method_async", + ]) + +for mod in HEAVY_MODULES: + if mod not in sys.modules: + print(f"FAILED: {mod} was not eagerly loaded into sys.modules") + sys.exit(1) + +sys.exit(0) +""" + + +@pytest.mark.skipif(sys.version_info < (3, 15), reason="PEP 810 requires Python 3.15+") +def test_lazy_imports_on_python_315(): + result = subprocess.run( + [sys.executable, "-c", SCRIPT_PYTHON_315], capture_output=True, text=True + ) + assert result.returncode == 0, f"Subprocess failed: {result.stdout}" + + +@pytest.mark.skipif( + sys.version_info >= (3, 15), reason="Testing fallback behavior on < 3.15" +) +def test_fallback_eager_imports_pre_315(): + result = subprocess.run( + [sys.executable, "-c", SCRIPT_PRE_315], capture_output=True, text=True + ) + assert result.returncode == 0, f"Subprocess failed: {result.stdout}"