From 73ac7dd9f9149c44552083e7c5156c10e34396f3 Mon Sep 17 00:00:00 2001 From: Federico Kamelhar Date: Mon, 9 Mar 2026 22:26:54 -0400 Subject: [PATCH 1/4] Improve import startup with lazy top-level exports (refs #2819) --- scripts/bench_import.py | 96 +++++++++++++++++++++ src/openai/__init__.py | 120 +++++++++++++++++++++++--- tests/lib/test_import_surface_live.py | 45 ++++++++++ tests/test_import_surface.py | 54 ++++++++++++ 4 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 scripts/bench_import.py create mode 100644 tests/lib/test_import_surface_live.py create mode 100644 tests/test_import_surface.py diff --git a/scripts/bench_import.py b/scripts/bench_import.py new file mode 100644 index 0000000000..a763917c3b --- /dev/null +++ b/scripts/bench_import.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import time +from pathlib import Path + + +def _pythonpath_for_repo() -> str | None: + src = Path(__file__).resolve().parents[1] / "src" + if not src.exists(): + return None + + existing = os.environ.get("PYTHONPATH") + if existing: + return f"{src}{os.pathsep}{existing}" + return str(src) + + +def _cold_import_seconds(repeats: int, env: dict[str, str]) -> list[float]: + samples: list[float] = [] + for _ in range(repeats): + start = time.perf_counter() + subprocess.run([sys.executable, "-c", "import openai"], check=True, env=env, stdout=subprocess.DEVNULL) + samples.append(time.perf_counter() - start) + return samples + + +def _importtime_output(env: dict[str, str]) -> str: + proc = subprocess.run( + [sys.executable, "-X", "importtime", "-c", "import openai"], + check=True, + env=env, + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + text=True, + ) + return proc.stderr + + +def _parse_importtime(importtime_stderr: str) -> list[tuple[int, str]]: + rows: list[tuple[int, str]] = [] + for line in importtime_stderr.splitlines(): + if "| " not in line: + continue + if "import time:" not in line: + continue + _, _, payload = line.partition("import time:") + parts = [p.strip() for p in payload.split("|")] + if len(parts) != 3: + continue + cumulative_raw = parts[1] + module = parts[2] + if not module.startswith("openai"): + continue + try: + cumulative = int(cumulative_raw) + except ValueError: + continue + rows.append((cumulative, module)) + rows.sort(reverse=True) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description="Benchmark openai import time for this checkout.") + parser.add_argument("--repeats", type=int, default=5, help="Number of cold imports to sample") + parser.add_argument("--top", type=int, default=20, help="How many importtime rows to print") + args = parser.parse_args() + + env = dict(os.environ) + pythonpath = _pythonpath_for_repo() + if pythonpath is not None: + env["PYTHONPATH"] = pythonpath + + samples = _cold_import_seconds(repeats=args.repeats, env=env) + avg = sum(samples) / len(samples) + + print(f"Python: {sys.executable}") + print(f"Samples (s): {[round(s, 4) for s in samples]}") + print(f"Average cold import (s): {avg:.4f}") + print() + + rows = _parse_importtime(_importtime_output(env)) + print(f"Top {min(args.top, len(rows))} cumulative importtime rows (us):") + for cumulative, module in rows[: args.top]: + print(f"{cumulative:>8} {module}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/openai/__init__.py b/src/openai/__init__.py index fc9675a8b5..c3a509fae5 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -6,7 +6,6 @@ import typing as _t from typing_extensions import override -from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import Client, OpenAI, Stream, Timeout, Transport, AsyncClient, AsyncOpenAI, AsyncStream, RequestOptions @@ -96,14 +95,23 @@ if not _t.TYPE_CHECKING: from ._utils._resources_proxy import resources as resources -from .lib import azure as _azure, pydantic_function_tool as pydantic_function_tool +if _t.TYPE_CHECKING: + from . import types as types + from .lib.azure import AzureADTokenProvider, AzureOpenAI, AsyncAzureOpenAI +else: + from ._utils._proxy import LazyProxy as _LazyProxy + + class _TypesProxy(_LazyProxy[_t.Any]): + @override + def __load__(self) -> _t.Any: + import importlib + + return importlib.import_module("openai.types") + + types = _TypesProxy().__as_proxied__() + from .version import VERSION as VERSION -from .lib.azure import AzureOpenAI as AzureOpenAI, AsyncAzureOpenAI as AsyncAzureOpenAI from .lib._old_api import * -from .lib.streaming import ( - AssistantEventHandler as AssistantEventHandler, - AsyncAssistantEventHandler as AsyncAssistantEventHandler, -) _setup_logging() @@ -114,12 +122,83 @@ __locals = locals() for __name in __all__: if not __name.startswith("__"): + __obj = __locals.get(__name) + if __obj is None: + continue try: - __locals[__name].__module__ = "openai" + __obj.__module__ = "openai" except (TypeError, AttributeError): # Some of our exported symbols are builtins which we can't set attributes for. pass + +def _is_truthy_env_var(name: str) -> bool: + value = _os.environ.get(name, "") + return value not in ("", "0", "false", "False") + + +def _lazy_azure_openai() -> object: + from .lib.azure import AzureOpenAI + + return AzureOpenAI + + +def _lazy_async_azure_openai() -> object: + from .lib.azure import AsyncAzureOpenAI + + return AsyncAzureOpenAI + + +def _lazy_pydantic_function_tool() -> object: + from .lib._tools import pydantic_function_tool + + return pydantic_function_tool + + +def _lazy_assistant_event_handler() -> object: + from .lib.streaming import AssistantEventHandler + + return AssistantEventHandler + + +def _lazy_async_assistant_event_handler() -> object: + from .lib.streaming import AsyncAssistantEventHandler + + return AsyncAssistantEventHandler + + +_LAZY_EXPORTS: dict[str, _t.Callable[[], object]] = { + "AzureOpenAI": _lazy_azure_openai, + "AsyncAzureOpenAI": _lazy_async_azure_openai, + "pydantic_function_tool": _lazy_pydantic_function_tool, + "AssistantEventHandler": _lazy_assistant_event_handler, + "AsyncAssistantEventHandler": _lazy_async_assistant_event_handler, +} + + +def __getattr__(name: str) -> object: + if name in _LAZY_EXPORTS: + value = _LAZY_EXPORTS[name]() + globals()[name] = value + return value + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def _resolve_eager_imports() -> None: + if not _is_truthy_env_var("OPENAI_EAGER_IMPORT"): + return + + import importlib + + # Resolve all lazy exports up-front in eager mode to catch import failures in CI/dev. + globals()["types"] = importlib.import_module("openai.types") + for name in _LAZY_EXPORTS: + __getattr__(name) + + +_resolve_eager_imports() + # ------ Module level client ------ import typing as _t import typing_extensions as _te @@ -158,7 +237,7 @@ azure_ad_token: str | None = _os.environ.get("AZURE_OPENAI_AD_TOKEN") -azure_ad_token_provider: _azure.AzureADTokenProvider | None = None +azure_ad_token_provider: AzureADTokenProvider | None = None class _ModuleClient(OpenAI): @@ -277,8 +356,25 @@ def _client(self, value: _httpx.Client) -> None: # type: ignore http_client = value -class _AzureModuleClient(_ModuleClient, AzureOpenAI): # type: ignore - ... +def _create_azure_module_client_class() -> type[OpenAI]: + from .lib.azure import AzureOpenAI + + class _AzureModuleClient(_ModuleClient, AzureOpenAI): # type: ignore + ... + + return _AzureModuleClient + + +_AZURE_MODULE_CLIENT_CLASS: type[OpenAI] | None = None + + +def _azure_module_client_class() -> type[OpenAI]: + global _AZURE_MODULE_CLIENT_CLASS + + if _AZURE_MODULE_CLIENT_CLASS is None: + _AZURE_MODULE_CLIENT_CLASS = _create_azure_module_client_class() + + return _AZURE_MODULE_CLIENT_CLASS class _AmbiguousModuleClientUsageError(OpenAIError): @@ -341,7 +437,7 @@ def _load_client() -> OpenAI: # type: ignore[reportUnusedFunction] api_type = "openai" if api_type == "azure": - _client = _AzureModuleClient( # type: ignore + _client = _azure_module_client_class()( # type: ignore api_version=api_version, azure_endpoint=azure_endpoint, api_key=api_key, diff --git a/tests/lib/test_import_surface_live.py b/tests/lib/test_import_surface_live.py new file mode 100644 index 0000000000..4eb589ac1d --- /dev/null +++ b/tests/lib/test_import_surface_live.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import os +import sys +import importlib + +import pytest + + +def _openai_modules() -> dict[str, object]: + return {name: mod for name, mod in sys.modules.items() if name == "openai" or name.startswith("openai.")} + + +def _restore_openai_modules(original_modules: dict[str, object]) -> None: + for name in list(sys.modules): + if name == "openai" or name.startswith("openai."): + sys.modules.pop(name, None) + sys.modules.update(original_modules) + + +@pytest.mark.skipif(os.environ.get("OPENAI_LIVE") != "1", reason="requires OPENAI_LIVE=1") +@pytest.mark.skipif(not os.environ.get("OPENAI_API_KEY"), reason="requires OPENAI_API_KEY") +def test_eager_import_with_live_token_allows_real_request(monkeypatch) -> None: + # Exercise eager mode in a real SDK flow behind explicit live-test flags. + monkeypatch.setenv("OPENAI_EAGER_IMPORT", "1") + original_modules = _openai_modules() + + for name in original_modules: + sys.modules.pop(name, None) + + client = None + try: + openai = importlib.import_module("openai") + + assert "openai.types" in sys.modules + assert "openai.lib.azure" in sys.modules + assert "AzureOpenAI" in openai.__dict__ + + client = openai.OpenAI(timeout=20.0) + page = client.models.list() + assert page.data is not None + finally: + if client is not None: + client.close() + _restore_openai_modules(original_modules) diff --git a/tests/test_import_surface.py b/tests/test_import_surface.py new file mode 100644 index 0000000000..d780fa13f7 --- /dev/null +++ b/tests/test_import_surface.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import importlib +import sys + + +def _openai_modules() -> dict[str, object]: + return {name: mod for name, mod in sys.modules.items() if name == "openai" or name.startswith("openai.")} + + +def _restore_openai_modules(original_modules: dict[str, object]) -> None: + for name in list(sys.modules): + if name == "openai" or name.startswith("openai."): + sys.modules.pop(name, None) + sys.modules.update(original_modules) + + +def test_openai_azure_is_lazy_imported(monkeypatch) -> None: + monkeypatch.delenv("OPENAI_EAGER_IMPORT", raising=False) + original_modules = _openai_modules() + + for name in original_modules: + sys.modules.pop(name, None) + + try: + openai = importlib.import_module("openai") + + assert "openai.lib.azure" not in sys.modules + + assert openai.AzureOpenAI is not None + assert "openai.lib.azure" in sys.modules + finally: + _restore_openai_modules(original_modules) + + +def test_openai_eager_import_resolves_lazy_exports(monkeypatch) -> None: + original_modules = _openai_modules() + monkeypatch.setenv("OPENAI_EAGER_IMPORT", "1") + + for name in original_modules: + sys.modules.pop(name, None) + + try: + openai = importlib.import_module("openai") + + assert "openai.types" in sys.modules + assert "openai.lib.azure" in sys.modules + assert "AzureOpenAI" in openai.__dict__ + assert "AsyncAzureOpenAI" in openai.__dict__ + assert "pydantic_function_tool" in openai.__dict__ + assert "AssistantEventHandler" in openai.__dict__ + assert "AsyncAssistantEventHandler" in openai.__dict__ + finally: + _restore_openai_modules(original_modules) From f35343967cd1b5c0b5136ede1b72069d72c14d42 Mon Sep 17 00:00:00 2001 From: Federico Kamelhar Date: Mon, 9 Mar 2026 23:29:58 -0400 Subject: [PATCH 2/4] Fix lazy openai.types to stay module-backed --- src/openai/__init__.py | 18 ++++++--------- tests/test_lazy_types_import.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 tests/test_lazy_types_import.py diff --git a/src/openai/__init__.py b/src/openai/__init__.py index c3a509fae5..1139663b6e 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -98,17 +98,6 @@ if _t.TYPE_CHECKING: from . import types as types from .lib.azure import AzureADTokenProvider, AzureOpenAI, AsyncAzureOpenAI -else: - from ._utils._proxy import LazyProxy as _LazyProxy - - class _TypesProxy(_LazyProxy[_t.Any]): - @override - def __load__(self) -> _t.Any: - import importlib - - return importlib.import_module("openai.types") - - types = _TypesProxy().__as_proxied__() from .version import VERSION as VERSION from .lib._old_api import * @@ -143,6 +132,12 @@ def _lazy_azure_openai() -> object: return AzureOpenAI +def _lazy_types_module() -> object: + import importlib + + return importlib.import_module("openai.types") + + def _lazy_async_azure_openai() -> object: from .lib.azure import AsyncAzureOpenAI @@ -168,6 +163,7 @@ def _lazy_async_assistant_event_handler() -> object: _LAZY_EXPORTS: dict[str, _t.Callable[[], object]] = { + "types": _lazy_types_module, "AzureOpenAI": _lazy_azure_openai, "AsyncAzureOpenAI": _lazy_async_azure_openai, "pydantic_function_tool": _lazy_pydantic_function_tool, diff --git a/tests/test_lazy_types_import.py b/tests/test_lazy_types_import.py new file mode 100644 index 0000000000..a23bc1135d --- /dev/null +++ b/tests/test_lazy_types_import.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import importlib +import sys +from types import ModuleType + + +def _openai_modules() -> dict[str, object]: + return {name: mod for name, mod in sys.modules.items() if name == "openai" or name.startswith("openai.")} + + +def test_openai_types_are_lazy_imported() -> None: + original_modules = _openai_modules() + + for name in original_modules: + sys.modules.pop(name, None) + + try: + openai = importlib.import_module("openai") + + assert "openai.types" not in sys.modules + + # Attribute access should trigger the lazy import. + assert openai.types.ChatModel is not None + assert "openai.types" in sys.modules + + types_module = sys.modules["openai.types"] + assert isinstance(openai.types, ModuleType) + assert openai.types is types_module + + # Module reload should keep the binding pointed at the module object. + reloaded = importlib.reload(openai.types) + assert reloaded is openai.types + assert reloaded is sys.modules["openai.types"] + finally: + for name in list(sys.modules): + if name == "openai" or name.startswith("openai."): + sys.modules.pop(name, None) + sys.modules.update(original_modules) From 017efdccb8775d68fbbe826eacb3a8752a566acd Mon Sep 17 00:00:00 2001 From: Federico Kamelhar Date: Wed, 11 Mar 2026 13:41:51 -0400 Subject: [PATCH 3/4] Address review on explicit import-surface coverage --- src/openai/__init__.py | 20 -------------------- src/openai/lib/__init__.py | 25 +++++++++++++++++++++++-- tests/lib/test_import_surface_live.py | 15 ++++++++++++--- tests/test_import_surface.py | 20 ++++++++++++++++---- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/openai/__init__.py b/src/openai/__init__.py index 1139663b6e..a9ab2934a8 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -121,11 +121,6 @@ pass -def _is_truthy_env_var(name: str) -> bool: - value = _os.environ.get(name, "") - return value not in ("", "0", "false", "False") - - def _lazy_azure_openai() -> object: from .lib.azure import AzureOpenAI @@ -180,21 +175,6 @@ def __getattr__(name: str) -> object: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - -def _resolve_eager_imports() -> None: - if not _is_truthy_env_var("OPENAI_EAGER_IMPORT"): - return - - import importlib - - # Resolve all lazy exports up-front in eager mode to catch import failures in CI/dev. - globals()["types"] = importlib.import_module("openai.types") - for name in _LAZY_EXPORTS: - __getattr__(name) - - -_resolve_eager_imports() - # ------ Module level client ------ import typing as _t import typing_extensions as _te diff --git a/src/openai/lib/__init__.py b/src/openai/lib/__init__.py index 5c6cb782c0..ba9db35e5a 100644 --- a/src/openai/lib/__init__.py +++ b/src/openai/lib/__init__.py @@ -1,2 +1,23 @@ -from ._tools import pydantic_function_tool as pydantic_function_tool -from ._parsing import ResponseFormatT as ResponseFormatT +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ._parsing import ResponseFormatT as ResponseFormatT + from ._tools import pydantic_function_tool as pydantic_function_tool + + +def __getattr__(name: str) -> Any: + if name == "pydantic_function_tool": + from ._tools import pydantic_function_tool + + globals()[name] = pydantic_function_tool + return pydantic_function_tool + + if name == "ResponseFormatT": + from ._parsing import ResponseFormatT + + globals()[name] = ResponseFormatT + return ResponseFormatT + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/lib/test_import_surface_live.py b/tests/lib/test_import_surface_live.py index 4eb589ac1d..672cecd126 100644 --- a/tests/lib/test_import_surface_live.py +++ b/tests/lib/test_import_surface_live.py @@ -18,11 +18,18 @@ def _restore_openai_modules(original_modules: dict[str, object]) -> None: sys.modules.update(original_modules) +def _resolve_lazy_exports(openai_module: object) -> None: + getattr(openai_module, "types") + getattr(openai_module, "AzureOpenAI") + getattr(openai_module, "AsyncAzureOpenAI") + getattr(openai_module, "pydantic_function_tool") + getattr(openai_module, "AssistantEventHandler") + getattr(openai_module, "AsyncAssistantEventHandler") + + @pytest.mark.skipif(os.environ.get("OPENAI_LIVE") != "1", reason="requires OPENAI_LIVE=1") @pytest.mark.skipif(not os.environ.get("OPENAI_API_KEY"), reason="requires OPENAI_API_KEY") -def test_eager_import_with_live_token_allows_real_request(monkeypatch) -> None: - # Exercise eager mode in a real SDK flow behind explicit live-test flags. - monkeypatch.setenv("OPENAI_EAGER_IMPORT", "1") +def test_explicit_lazy_export_resolution_allows_real_request() -> None: original_modules = _openai_modules() for name in original_modules: @@ -32,6 +39,8 @@ def test_eager_import_with_live_token_allows_real_request(monkeypatch) -> None: try: openai = importlib.import_module("openai") + _resolve_lazy_exports(openai) + assert "openai.types" in sys.modules assert "openai.lib.azure" in sys.modules assert "AzureOpenAI" in openai.__dict__ diff --git a/tests/test_import_surface.py b/tests/test_import_surface.py index d780fa13f7..55aab4ab00 100644 --- a/tests/test_import_surface.py +++ b/tests/test_import_surface.py @@ -15,8 +15,16 @@ def _restore_openai_modules(original_modules: dict[str, object]) -> None: sys.modules.update(original_modules) -def test_openai_azure_is_lazy_imported(monkeypatch) -> None: - monkeypatch.delenv("OPENAI_EAGER_IMPORT", raising=False) +def _resolve_lazy_exports(openai_module: object) -> None: + getattr(openai_module, "types") + getattr(openai_module, "AzureOpenAI") + getattr(openai_module, "AsyncAzureOpenAI") + getattr(openai_module, "pydantic_function_tool") + getattr(openai_module, "AssistantEventHandler") + getattr(openai_module, "AsyncAssistantEventHandler") + + +def test_openai_azure_is_lazy_imported() -> None: original_modules = _openai_modules() for name in original_modules: @@ -33,9 +41,8 @@ def test_openai_azure_is_lazy_imported(monkeypatch) -> None: _restore_openai_modules(original_modules) -def test_openai_eager_import_resolves_lazy_exports(monkeypatch) -> None: +def test_openai_can_explicitly_resolve_lazy_exports() -> None: original_modules = _openai_modules() - monkeypatch.setenv("OPENAI_EAGER_IMPORT", "1") for name in original_modules: sys.modules.pop(name, None) @@ -43,6 +50,11 @@ def test_openai_eager_import_resolves_lazy_exports(monkeypatch) -> None: try: openai = importlib.import_module("openai") + assert "openai.types" not in sys.modules + assert "openai.lib.azure" not in sys.modules + + _resolve_lazy_exports(openai) + assert "openai.types" in sys.modules assert "openai.lib.azure" in sys.modules assert "AzureOpenAI" in openai.__dict__ From 66532e1c0968fd71a73abf1f951210a55c552594 Mon Sep 17 00:00:00 2001 From: Federico Kamelhar Date: Mon, 20 Apr 2026 10:53:45 -0400 Subject: [PATCH 4/4] Adapt to eager openai.types loading via _client The 2.32.0 release added 'from .types.websocket_reconnection import ...' at the top of openai/__init__.py, which transitively loads openai.types through openai._client as well. That invalidates the 'openai.types is lazy' assertion the earlier test relied on, so the lazy-types live test is removed and the remaining tests assert the boundaries that still hold (lib.azure, lib.streaming, lib._tools). Also stabilize pyright under strict: - preserve static visibility of AzureOpenAI / AsyncAzureOpenAI / pydantic_function_tool / AssistantEventHandler under TYPE_CHECKING so callers keep their signatures - rename _AZURE_MODULE_CLIENT_CLASS to a non-constant symbol and type the module-client factory as type[AzureOpenAI] to silence strict reportCallIssue / reportConstantRedefinition warnings - tidy ruff imports / I001 noise in scripts/bench_import.py and the test files added in this branch --- scripts/bench_import.py | 4 +-- src/openai/__init__.py | 27 ++++++++++------ src/openai/lib/__init__.py | 2 +- tests/lib/test_import_surface_live.py | 23 ++++++++------ tests/test_import_surface.py | 45 ++++++++++++++++----------- tests/test_lazy_types_import.py | 39 ----------------------- 6 files changed, 62 insertions(+), 78 deletions(-) delete mode 100644 tests/test_lazy_types_import.py diff --git a/scripts/bench_import.py b/scripts/bench_import.py index a763917c3b..3d63cfb7f5 100644 --- a/scripts/bench_import.py +++ b/scripts/bench_import.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 from __future__ import annotations -import argparse import os -import subprocess import sys import time +import argparse +import subprocess from pathlib import Path diff --git a/src/openai/__init__.py b/src/openai/__init__.py index a9ab2934a8..21558d4340 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -97,7 +97,16 @@ if _t.TYPE_CHECKING: from . import types as types - from .lib.azure import AzureADTokenProvider, AzureOpenAI, AsyncAzureOpenAI + from .lib.azure import ( + AzureOpenAI as AzureOpenAI, + AsyncAzureOpenAI as AsyncAzureOpenAI, + AzureADTokenProvider, + ) + from .lib._tools import pydantic_function_tool as pydantic_function_tool + from .lib.streaming import ( + AssistantEventHandler as AssistantEventHandler, + AsyncAssistantEventHandler as AsyncAssistantEventHandler, + ) from .version import VERSION as VERSION from .lib._old_api import * @@ -332,25 +341,25 @@ def _client(self, value: _httpx.Client) -> None: # type: ignore http_client = value -def _create_azure_module_client_class() -> type[OpenAI]: +def _create_azure_module_client_class() -> type[AzureOpenAI]: from .lib.azure import AzureOpenAI class _AzureModuleClient(_ModuleClient, AzureOpenAI): # type: ignore ... - return _AzureModuleClient + return _AzureModuleClient # type: ignore[return-value] -_AZURE_MODULE_CLIENT_CLASS: type[OpenAI] | None = None +_azure_module_client_class_cache: type[AzureOpenAI] | None = None -def _azure_module_client_class() -> type[OpenAI]: - global _AZURE_MODULE_CLIENT_CLASS +def _azure_module_client_class() -> type[AzureOpenAI]: + global _azure_module_client_class_cache - if _AZURE_MODULE_CLIENT_CLASS is None: - _AZURE_MODULE_CLIENT_CLASS = _create_azure_module_client_class() + if _azure_module_client_class_cache is None: + _azure_module_client_class_cache = _create_azure_module_client_class() - return _AZURE_MODULE_CLIENT_CLASS + return _azure_module_client_class_cache class _AmbiguousModuleClientUsageError(OpenAIError): diff --git a/src/openai/lib/__init__.py b/src/openai/lib/__init__.py index ba9db35e5a..a43fdc5098 100644 --- a/src/openai/lib/__init__.py +++ b/src/openai/lib/__init__.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from ._parsing import ResponseFormatT as ResponseFormatT from ._tools import pydantic_function_tool as pydantic_function_tool + from ._parsing import ResponseFormatT as ResponseFormatT def __getattr__(name: str) -> Any: diff --git a/tests/lib/test_import_surface_live.py b/tests/lib/test_import_surface_live.py index 672cecd126..7658cdf34b 100644 --- a/tests/lib/test_import_surface_live.py +++ b/tests/lib/test_import_surface_live.py @@ -3,28 +3,34 @@ import os import sys import importlib +from types import ModuleType import pytest -def _openai_modules() -> dict[str, object]: +def _openai_modules() -> dict[str, ModuleType]: return {name: mod for name, mod in sys.modules.items() if name == "openai" or name.startswith("openai.")} -def _restore_openai_modules(original_modules: dict[str, object]) -> None: +def _restore_openai_modules(original_modules: dict[str, ModuleType]) -> None: for name in list(sys.modules): if name == "openai" or name.startswith("openai."): sys.modules.pop(name, None) sys.modules.update(original_modules) +_LAZY_EXPORT_NAMES = ( + "AzureOpenAI", + "AsyncAzureOpenAI", + "pydantic_function_tool", + "AssistantEventHandler", + "AsyncAssistantEventHandler", +) + + def _resolve_lazy_exports(openai_module: object) -> None: - getattr(openai_module, "types") - getattr(openai_module, "AzureOpenAI") - getattr(openai_module, "AsyncAzureOpenAI") - getattr(openai_module, "pydantic_function_tool") - getattr(openai_module, "AssistantEventHandler") - getattr(openai_module, "AsyncAssistantEventHandler") + for name in _LAZY_EXPORT_NAMES: + getattr(openai_module, name) @pytest.mark.skipif(os.environ.get("OPENAI_LIVE") != "1", reason="requires OPENAI_LIVE=1") @@ -41,7 +47,6 @@ def test_explicit_lazy_export_resolution_allows_real_request() -> None: _resolve_lazy_exports(openai) - assert "openai.types" in sys.modules assert "openai.lib.azure" in sys.modules assert "AzureOpenAI" in openai.__dict__ diff --git a/tests/test_import_surface.py b/tests/test_import_surface.py index 55aab4ab00..f520031b91 100644 --- a/tests/test_import_surface.py +++ b/tests/test_import_surface.py @@ -1,27 +1,39 @@ from __future__ import annotations -import importlib import sys +import importlib +from types import ModuleType -def _openai_modules() -> dict[str, object]: +def _openai_modules() -> dict[str, ModuleType]: return {name: mod for name, mod in sys.modules.items() if name == "openai" or name.startswith("openai.")} -def _restore_openai_modules(original_modules: dict[str, object]) -> None: +def _restore_openai_modules(original_modules: dict[str, ModuleType]) -> None: for name in list(sys.modules): if name == "openai" or name.startswith("openai."): sys.modules.pop(name, None) sys.modules.update(original_modules) +_LAZY_EXPORT_NAMES = ( + "AzureOpenAI", + "AsyncAzureOpenAI", + "pydantic_function_tool", + "AssistantEventHandler", + "AsyncAssistantEventHandler", +) + +_LAZY_EXPORT_MODULES = ( + "openai.lib.azure", + "openai.lib.streaming", + "openai.lib._tools", +) + + def _resolve_lazy_exports(openai_module: object) -> None: - getattr(openai_module, "types") - getattr(openai_module, "AzureOpenAI") - getattr(openai_module, "AsyncAzureOpenAI") - getattr(openai_module, "pydantic_function_tool") - getattr(openai_module, "AssistantEventHandler") - getattr(openai_module, "AsyncAssistantEventHandler") + for name in _LAZY_EXPORT_NAMES: + getattr(openai_module, name) def test_openai_azure_is_lazy_imported() -> None: @@ -50,17 +62,14 @@ def test_openai_can_explicitly_resolve_lazy_exports() -> None: try: openai = importlib.import_module("openai") - assert "openai.types" not in sys.modules - assert "openai.lib.azure" not in sys.modules + for mod_name in _LAZY_EXPORT_MODULES: + assert mod_name not in sys.modules _resolve_lazy_exports(openai) - assert "openai.types" in sys.modules - assert "openai.lib.azure" in sys.modules - assert "AzureOpenAI" in openai.__dict__ - assert "AsyncAzureOpenAI" in openai.__dict__ - assert "pydantic_function_tool" in openai.__dict__ - assert "AssistantEventHandler" in openai.__dict__ - assert "AsyncAssistantEventHandler" in openai.__dict__ + for mod_name in _LAZY_EXPORT_MODULES: + assert mod_name in sys.modules + for attr_name in _LAZY_EXPORT_NAMES: + assert attr_name in openai.__dict__ finally: _restore_openai_modules(original_modules) diff --git a/tests/test_lazy_types_import.py b/tests/test_lazy_types_import.py deleted file mode 100644 index a23bc1135d..0000000000 --- a/tests/test_lazy_types_import.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -import importlib -import sys -from types import ModuleType - - -def _openai_modules() -> dict[str, object]: - return {name: mod for name, mod in sys.modules.items() if name == "openai" or name.startswith("openai.")} - - -def test_openai_types_are_lazy_imported() -> None: - original_modules = _openai_modules() - - for name in original_modules: - sys.modules.pop(name, None) - - try: - openai = importlib.import_module("openai") - - assert "openai.types" not in sys.modules - - # Attribute access should trigger the lazy import. - assert openai.types.ChatModel is not None - assert "openai.types" in sys.modules - - types_module = sys.modules["openai.types"] - assert isinstance(openai.types, ModuleType) - assert openai.types is types_module - - # Module reload should keep the binding pointed at the module object. - reloaded = importlib.reload(openai.types) - assert reloaded is openai.types - assert reloaded is sys.modules["openai.types"] - finally: - for name in list(sys.modules): - if name == "openai" or name.startswith("openai."): - sys.modules.pop(name, None) - sys.modules.update(original_modules)