diff --git a/google/genai/__init__.py b/google/genai/__init__.py index 9c98f6ef6..e9d37f6bb 100644 --- a/google/genai/__init__.py +++ b/google/genai/__init__.py @@ -15,7 +15,9 @@ """Google Gen AI SDK""" -from . import interactions +import importlib +from typing import Any + from . import types from . import version from .client import Client @@ -23,4 +25,12 @@ __version__ = version.__version__ -__all__ = ['Client'] +__all__ = ['Client', 'interactions', 'types'] + + +def __getattr__(name: str) -> Any: + if name == 'interactions': + module = importlib.import_module('.interactions', __name__) + globals()[name] = module + return module + raise AttributeError(f'module {__name__!r} has no attribute {name!r}') diff --git a/google/genai/client.py b/google/genai/client.py index b3d869db2..25a2f3dd2 100644 --- a/google/genai/client.py +++ b/google/genai/client.py @@ -13,13 +13,17 @@ # limitations under the License. # +from __future__ import annotations + import asyncio import os -from typing import Any, Optional, Union +from typing import Any, Optional, TYPE_CHECKING, Union +import warnings import google.auth import pydantic +from . import _common from ._api_client import BaseApiClient from ._base_url import get_base_url from ._replay_api_client import ReplayApiClient @@ -35,22 +39,17 @@ from .tunings import AsyncTunings, Tunings from .types import HttpOptions, HttpOptionsDict, HttpRetryOptions -import warnings - -from . import _common - -from ._gaos.google_genai import ( - AsyncGeminiNextGenAgents, - AsyncGeminiNextGenInteractions, - AsyncGeminiNextGenWebhooks, - GeminiNextGenAgents, - GeminiNextGenInteractions, - GeminiNextGenWebhooks, - build_google_genai_async_client, - build_google_genai_client, -) -from ._gaos.sdk import AsyncGenAI as AsyncGeminiNextGenAPI -from ._gaos.sdk import GenAI as GeminiNextGenAPI +if TYPE_CHECKING: + from ._gaos.google_genai import ( + AsyncGeminiNextGenAgents, + AsyncGeminiNextGenInteractions, + AsyncGeminiNextGenWebhooks, + GeminiNextGenAgents, + GeminiNextGenInteractions, + GeminiNextGenWebhooks, + ) + from ._gaos.sdk import AsyncGenAI as AsyncGeminiNextGenAPI + from ._gaos.sdk import GenAI as GeminiNextGenAPI _agent_experimental_warned = False @@ -77,6 +76,8 @@ def __init__(self, api_client: BaseApiClient): @property def _nextgen_client(self) -> AsyncGeminiNextGenAPI: + from ._gaos.google_genai import build_google_genai_async_client + if self._nextgen_client_instance is None: self._nextgen_client_instance = build_google_genai_async_client( self._api_client @@ -85,18 +86,24 @@ def _nextgen_client(self) -> AsyncGeminiNextGenAPI: @property def interactions(self) -> AsyncGeminiNextGenInteractions: + from ._gaos.google_genai import AsyncGeminiNextGenInteractions + if self._interactions is None: self._interactions = AsyncGeminiNextGenInteractions(self._api_client) return self._interactions @property def webhooks(self) -> AsyncGeminiNextGenWebhooks: + from ._gaos.google_genai import AsyncGeminiNextGenWebhooks + if self._webhooks is None: self._webhooks = AsyncGeminiNextGenWebhooks(self._api_client) return self._webhooks @property def agents(self) -> AsyncGeminiNextGenAgents: + from ._gaos.google_genai import AsyncGeminiNextGenAgents + global _agent_experimental_warned if not _agent_experimental_warned: _agent_experimental_warned = True @@ -400,6 +407,8 @@ def _get_api_client( @property def _nextgen_client(self) -> GeminiNextGenAPI: + from ._gaos.google_genai import build_google_genai_client + if self._nextgen_client_instance is None: self._nextgen_client_instance = build_google_genai_client( self._api_client @@ -408,18 +417,24 @@ def _nextgen_client(self) -> GeminiNextGenAPI: @property def interactions(self) -> GeminiNextGenInteractions: + from ._gaos.google_genai import GeminiNextGenInteractions + if self._interactions is None: self._interactions = GeminiNextGenInteractions(self._api_client) return self._interactions @property def webhooks(self) -> GeminiNextGenWebhooks: + from ._gaos.google_genai import GeminiNextGenWebhooks + if self._webhooks is None: self._webhooks = GeminiNextGenWebhooks(self._api_client) return self._webhooks @property def agents(self) -> GeminiNextGenAgents: + from ._gaos.google_genai import GeminiNextGenAgents + global _agent_experimental_warned if not _agent_experimental_warned: _agent_experimental_warned = True diff --git a/google/genai/tests/client/test_lazy_imports.py b/google/genai/tests/client/test_lazy_imports.py new file mode 100644 index 000000000..a194e313b --- /dev/null +++ b/google/genai/tests/client/test_lazy_imports.py @@ -0,0 +1,130 @@ +# Copyright 2025 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. +# +# pylint: disable=protected-access + + +"""Tests for the deferred import of the NextGen backends and interactions. + +The client exposes the interactions/webhooks/agents/nextgen backends through +properties that import the heavy ``_gaos`` backend only on first access, and the +top-level package exposes ``interactions`` as a lazy module attribute. These +tests exercise those deferred-import paths: each accessor builds the correct +backend, memoizes it, and (for agents) emits the experimental warning. +""" + +import sys + +import pytest + +from ... import Client +from ... import client as client_module +from ..._gaos.google_genai import AsyncGeminiNextGenAgents +from ..._gaos.google_genai import AsyncGeminiNextGenInteractions +from ..._gaos.google_genai import AsyncGeminiNextGenWebhooks +from ..._gaos.google_genai import GeminiNextGenAgents +from ..._gaos.google_genai import GeminiNextGenInteractions +from ..._gaos.google_genai import GeminiNextGenWebhooks +from ..._gaos.sdk import AsyncGenAI +from ..._gaos.sdk import GenAI + +_GENAI_PACKAGE = Client.__module__.rsplit('.', 1)[0] + + +@pytest.fixture(autouse=True) +def _fresh_env(monkeypatch): + monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key') + for var in ( + 'GOOGLE_CLOUD_PROJECT', + 'GEMINI_API_KEY', + 'GOOGLE_CLOUD_LOCATION', + ): + monkeypatch.delenv(var, raising=False) + # ``agents`` warns once per process via a module global; reset it so every + # test observes deterministic warning behavior regardless of test order. + monkeypatch.setattr(client_module, '_agent_experimental_warned', False) + + +def test_sync_nextgen_client_is_lazy_and_cached(): + client = Client() + nextgen = client._nextgen_client + assert isinstance(nextgen, GenAI) + assert client._nextgen_client is nextgen + + +def test_sync_interactions_is_lazy_and_cached(): + client = Client() + interactions = client.interactions + assert isinstance(interactions, GeminiNextGenInteractions) + assert client.interactions is interactions + + +def test_sync_webhooks_is_lazy_and_cached(): + client = Client() + webhooks = client.webhooks + assert isinstance(webhooks, GeminiNextGenWebhooks) + assert client.webhooks is webhooks + + +def test_sync_agents_is_lazy_cached_and_warns(): + client = Client() + with pytest.warns(UserWarning, match='experimental'): + agents = client.agents + assert isinstance(agents, GeminiNextGenAgents) + assert client.agents is agents + + +def test_async_nextgen_client_is_lazy_and_cached(): + client = Client() + nextgen = client.aio._nextgen_client + assert isinstance(nextgen, AsyncGenAI) + assert client.aio._nextgen_client is nextgen + + +def test_async_interactions_is_lazy_and_cached(): + client = Client() + interactions = client.aio.interactions + assert isinstance(interactions, AsyncGeminiNextGenInteractions) + assert client.aio.interactions is interactions + + +def test_async_webhooks_is_lazy_and_cached(): + client = Client() + webhooks = client.aio.webhooks + assert isinstance(webhooks, AsyncGeminiNextGenWebhooks) + assert client.aio.webhooks is webhooks + + +def test_async_agents_is_lazy_cached_and_warns(): + client = Client() + with pytest.warns(UserWarning, match='experimental'): + agents = client.aio.agents + assert isinstance(agents, AsyncGeminiNextGenAgents) + assert client.aio.agents is agents + + +@pytest.mark.skipif( + "config.getoption('--private')", + reason=( + 'Package-level lazy interactions attribute is a public-SDK-only' + ' feature.' + ), +) +def test_package_lazily_exposes_interactions_submodule(): + package = sys.modules[_GENAI_PACKAGE] + # Drop any memoized attribute so the lazy ``__getattr__`` path runs. + package.__dict__.pop('interactions', None) + interactions = package.interactions + assert interactions.__name__ == f'{_GENAI_PACKAGE}.interactions' + assert interactions is sys.modules[f'{_GENAI_PACKAGE}.interactions']