Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/openai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,27 @@
from .lib.azure import AzureOpenAI as AzureOpenAI, AsyncAzureOpenAI as AsyncAzureOpenAI
from .lib.bedrock import BedrockOpenAI as BedrockOpenAI, AsyncBedrockOpenAI as AsyncBedrockOpenAI
from .lib._old_api import *
from .lib.streaming import (
AssistantEventHandler as AssistantEventHandler,
AsyncAssistantEventHandler as AsyncAssistantEventHandler,
)

if _t.TYPE_CHECKING:
from .lib.streaming import (
AssistantEventHandler as AssistantEventHandler,
AsyncAssistantEventHandler as AsyncAssistantEventHandler,
)
else:
# `openai.lib.streaming` reaches `openai.types.beta`, which is 318 modules
# and about a third of the cost of `import openai`, for the Assistants API.
# Deferring the module keeps the names available on `openai` while leaving
# them -- and their annotations -- fully resolvable once anything asks.
_STREAMING_EXPORTS = ("AssistantEventHandler", "AsyncAssistantEventHandler")

def __getattr__(__name: str) -> _t.Any:
if __name in _STREAMING_EXPORTS:
Comment on lines +124 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep lazy handler exports visible to introspection

On a fresh import openai, these public classes are now absent from the module dictionary, and dir(openai) therefore omits them until some separate direct attribute lookup happens. This makes standard discovery paths such as inspect.getmembers(openai) fail to find either documented handler, whereas they were discoverable before this change. Add a module __dir__ that includes _STREAMING_EXPORTS so introspection remains compatible without eagerly importing the beta types.

AGENTS.md reference: AGENTS.md:L5-L8

Useful? React with 👍 / 👎.

import importlib

value = getattr(importlib.import_module("openai.lib.streaming"), __name)
globals()[__name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {__name!r}")

_setup_logging()

Expand Down
53 changes: 53 additions & 0 deletions tests/lib/test_streaming_lazy_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

import sys
import typing
import subprocess

import pytest

import openai


def _modules_after_import_openai() -> set[str]:
"""Return the module names loaded by a bare `import openai` in a fresh interpreter."""
output = subprocess.run(
[
sys.executable,
"-c",
"import sys\nimport openai\nprint('\\n'.join(sys.modules))\n",
],
check=True,
capture_output=True,
text=True,
).stdout
return set(output.split())


def test_import_openai_does_not_load_beta_types() -> None:
# `openai.lib.streaming` is only needed by callers using the Assistants API,
# and it reaches `openai.types.beta`, so importing the package must not pull
# the namespace in.
modules = _modules_after_import_openai()

assert "openai" in modules
assert not [module for module in modules if module.startswith("openai.types.beta")]


def test_assistant_event_handlers_are_still_exported() -> None:
assert openai.AssistantEventHandler.__name__ == "AssistantEventHandler"
assert openai.AsyncAssistantEventHandler.__name__ == "AsyncAssistantEventHandler"


def test_handler_annotations_stay_resolvable() -> None:
# Deferring the module must not make the handlers' postponed annotations
# unresolvable: `get_type_hints` has to keep working for annotation-aware
# integrations and documentation tooling.
for name in ("on_event", "on_run_step_delta", "on_tool_call_delta"):
hints = typing.get_type_hints(getattr(openai.AssistantEventHandler, name))
assert "return" in hints


def test_unknown_attribute_still_raises_attribute_error() -> None:
with pytest.raises(AttributeError, match="definitely_not_an_export"):
openai.definitely_not_an_export # type: ignore[attr-defined] # noqa: B018