Skip to content

Commit eec6d11

Browse files
committed
fix: preserve agents-extras hint when memory backends are missing
`src/agents/extensions/memory/__init__.py` wraps each lazy import so the user gets a `pip install openai-agents[<extra>]` message when an extra is missing. The wrappers caught `ModuleNotFoundError`, but `redis_session.py`, `dapr_session.py`, and `mongodb_session.py` each have their own `try/except ImportError` that re-raises a plain `ImportError` (not `ModuleNotFoundError`). `ImportError` is the superclass, so the outer `except ModuleNotFoundError` clause never matched and the helpful hint was effectively dead code. Concretely, `from agents.extensions.memory import RedisSession` with `redis` missing produced `"RedisSession requires the 'redis' package. Install it with: pip install redis"` (the inner wrapper) instead of `"RedisSession requires the 'redis' extra. Install it with: pip install openai-agents[redis]"` (the outer wrapper). Widen the outer `except` clauses to `ImportError` so they catch both `ImportError` and `ModuleNotFoundError`. Add a parametrized regression test covering every extras-backed export plus both error subclasses.
1 parent 5645845 commit eec6d11

2 files changed

Lines changed: 104 additions & 9 deletions

File tree

src/agents/extensions/memory/__init__.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __getattr__(name: str) -> Any:
4242
from .encrypt_session import EncryptedSession # noqa: F401
4343

4444
return EncryptedSession
45-
except ModuleNotFoundError as e:
45+
except ImportError as e:
4646
raise ImportError(
4747
"EncryptedSession requires the 'cryptography' extra. "
4848
"Install it with: pip install openai-agents[encrypt]"
@@ -53,7 +53,7 @@ def __getattr__(name: str) -> Any:
5353
from .redis_session import RedisSession # noqa: F401
5454

5555
return RedisSession
56-
except ModuleNotFoundError as e:
56+
except ImportError as e:
5757
raise ImportError(
5858
"RedisSession requires the 'redis' extra. "
5959
"Install it with: pip install openai-agents[redis]"
@@ -64,7 +64,7 @@ def __getattr__(name: str) -> Any:
6464
from .sqlalchemy_session import SQLAlchemySession # noqa: F401
6565

6666
return SQLAlchemySession
67-
except ModuleNotFoundError as e:
67+
except ImportError as e:
6868
raise ImportError(
6969
"SQLAlchemySession requires the 'sqlalchemy' extra. "
7070
"Install it with: pip install openai-agents[sqlalchemy]"
@@ -75,23 +75,23 @@ def __getattr__(name: str) -> Any:
7575
from .advanced_sqlite_session import AdvancedSQLiteSession # noqa: F401
7676

7777
return AdvancedSQLiteSession
78-
except ModuleNotFoundError as e:
78+
except ImportError as e:
7979
raise ImportError(f"Failed to import AdvancedSQLiteSession: {e}") from e
8080

8181
if name == "AsyncSQLiteSession":
8282
try:
8383
from .async_sqlite_session import AsyncSQLiteSession # noqa: F401
8484

8585
return AsyncSQLiteSession
86-
except ModuleNotFoundError as e:
86+
except ImportError as e:
8787
raise ImportError(f"Failed to import AsyncSQLiteSession: {e}") from e
8888

8989
if name == "DaprSession":
9090
try:
9191
from .dapr_session import DaprSession # noqa: F401
9292

9393
return DaprSession
94-
except ModuleNotFoundError as e:
94+
except ImportError as e:
9595
raise ImportError(
9696
"DaprSession requires the 'dapr' extra. "
9797
"Install it with: pip install openai-agents[dapr]"
@@ -102,7 +102,7 @@ def __getattr__(name: str) -> Any:
102102
from .dapr_session import DAPR_CONSISTENCY_EVENTUAL # noqa: F401
103103

104104
return DAPR_CONSISTENCY_EVENTUAL
105-
except ModuleNotFoundError as e:
105+
except ImportError as e:
106106
raise ImportError(
107107
"DAPR_CONSISTENCY_EVENTUAL requires the 'dapr' extra. "
108108
"Install it with: pip install openai-agents[dapr]"
@@ -113,7 +113,7 @@ def __getattr__(name: str) -> Any:
113113
from .dapr_session import DAPR_CONSISTENCY_STRONG # noqa: F401
114114

115115
return DAPR_CONSISTENCY_STRONG
116-
except ModuleNotFoundError as e:
116+
except ImportError as e:
117117
raise ImportError(
118118
"DAPR_CONSISTENCY_STRONG requires the 'dapr' extra. "
119119
"Install it with: pip install openai-agents[dapr]"
@@ -124,7 +124,7 @@ def __getattr__(name: str) -> Any:
124124
from .mongodb_session import MongoDBSession # noqa: F401
125125

126126
return MongoDBSession
127-
except ModuleNotFoundError as e:
127+
except ImportError as e:
128128
raise ImportError(
129129
"MongoDBSession requires the 'mongodb' extra. "
130130
"Install it with: pip install openai-agents[mongodb]"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
import importlib.abc
5+
import sys
6+
from types import ModuleType
7+
8+
import pytest
9+
10+
# (symbol, broken_module, extra_name) tuples covering the extras-backed
11+
# lazy exports in `agents.extensions.memory.__init__`. Each entry asserts
12+
# that the package-level `__getattr__` produces a helpful
13+
# `pip install openai-agents[<extra>]` message even when the backing
14+
# module re-raises the dependency failure as a plain `ImportError`
15+
# (as `redis_session`, `dapr_session`, and `mongodb_session` do).
16+
_EXTRA_EXPORTS: tuple[tuple[str, str, str], ...] = (
17+
("RedisSession", "agents.extensions.memory.redis_session", "redis"),
18+
("DaprSession", "agents.extensions.memory.dapr_session", "dapr"),
19+
(
20+
"DAPR_CONSISTENCY_EVENTUAL",
21+
"agents.extensions.memory.dapr_session",
22+
"dapr",
23+
),
24+
(
25+
"DAPR_CONSISTENCY_STRONG",
26+
"agents.extensions.memory.dapr_session",
27+
"dapr",
28+
),
29+
("MongoDBSession", "agents.extensions.memory.mongodb_session", "mongodb"),
30+
("EncryptedSession", "agents.extensions.memory.encrypt_session", "encrypt"),
31+
(
32+
"SQLAlchemySession",
33+
"agents.extensions.memory.sqlalchemy_session",
34+
"sqlalchemy",
35+
),
36+
)
37+
38+
39+
class _BrokenMemoryModuleFinder(importlib.abc.MetaPathFinder):
40+
def __init__(self, broken_module: str, error_cls: type[ImportError]) -> None:
41+
self._broken_module = broken_module
42+
self._error_cls = error_cls
43+
44+
def find_spec(
45+
self,
46+
fullname: str,
47+
path: object | None,
48+
target: ModuleType | None = None,
49+
) -> None:
50+
if fullname == self._broken_module:
51+
raise self._error_cls("simulated dependency import failure")
52+
return None
53+
54+
55+
def _reset_memory_imports(
56+
monkeypatch: pytest.MonkeyPatch,
57+
memory_module: ModuleType,
58+
broken_module: str,
59+
symbol: str,
60+
) -> None:
61+
monkeypatch.delitem(sys.modules, broken_module, raising=False)
62+
short = broken_module.rsplit(".", 1)[-1]
63+
monkeypatch.delitem(memory_module.__dict__, short, raising=False)
64+
monkeypatch.delitem(memory_module.__dict__, symbol, raising=False)
65+
66+
67+
@pytest.mark.parametrize(
68+
("symbol", "broken_module", "extra"),
69+
_EXTRA_EXPORTS,
70+
)
71+
@pytest.mark.parametrize("error_cls", [ImportError, ModuleNotFoundError])
72+
def test_memory_extras_error_message_points_to_install_extra(
73+
monkeypatch: pytest.MonkeyPatch,
74+
symbol: str,
75+
broken_module: str,
76+
extra: str,
77+
error_cls: type[ImportError],
78+
) -> None:
79+
"""Lazy memory exports must surface the `openai-agents[<extra>]` hint
80+
regardless of whether the backing module raises `ImportError` or
81+
`ModuleNotFoundError`. Backing modules like `redis_session` re-raise
82+
`ImportError`, which used to bypass the outer wrapper's
83+
`except ModuleNotFoundError`."""
84+
85+
import agents.extensions.memory as memory_module
86+
87+
_reset_memory_imports(monkeypatch, memory_module, broken_module, symbol)
88+
finder = _BrokenMemoryModuleFinder(broken_module, error_cls)
89+
monkeypatch.setattr(sys, "meta_path", [finder, *sys.meta_path])
90+
91+
with pytest.raises(ImportError) as exc_info:
92+
getattr(memory_module, symbol)
93+
94+
assert f"openai-agents[{extra}]" in str(exc_info.value)
95+
assert exc_info.value.__cause__ is not None

0 commit comments

Comments
 (0)