Skip to content

Commit ffc2494

Browse files
donggyun112copybara-github
authored andcommitted
fix: install greenlet with the db extra and name it when missing
Merge #7327 DatabaseSessionService.__init__ now raises ImportError naming 'sqlalchemy[asyncio]' (instead of 'sqlalchemy') when SQLAlchemy or its asyncio extra fails to import. PiperOrigin-RevId: 991319378
1 parent 4e48bb0 commit ffc2494

4 files changed

Lines changed: 59 additions & 9 deletions

File tree

‎pyproject.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ optional-dependencies.all = [
131131
"redis>=4.2",
132132
"rouge-score>=0.1.2",
133133
"slack-bolt>=1.22",
134-
"sqlalchemy>=2,<3",
134+
"sqlalchemy[asyncio]>=2,<3",
135135
"sqlalchemy-spanner>=1.14",
136136
"tabulate>=0.9",
137137
"toolbox-adk>=1,<2",
@@ -159,7 +159,7 @@ optional-dependencies.daytona = [
159159
"daytona>=0.191", # For DaytonaEnvironment remote sandbox.
160160
]
161161
optional-dependencies.db = [
162-
"sqlalchemy>=2,<3",
162+
"sqlalchemy[asyncio]>=2,<3",
163163
"sqlalchemy-spanner>=1.14",
164164
]
165165
optional-dependencies.dev = [
@@ -349,7 +349,7 @@ optional-dependencies.test = [
349349
"python-multipart>=0.0.9",
350350
"rouge-score>=0.1.2",
351351
"slack-bolt>=1.22",
352-
"sqlalchemy>=2,<3",
352+
"sqlalchemy[asyncio]>=2,<3",
353353
"sqlalchemy-spanner>=1.14",
354354
"tabulate>=0.9",
355355
"tomli>=2,<3; python_version<'3.11'",

‎src/google/adk/sessions/database_session_service.py‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from google.adk.platform import time as platform_time
3232
from google.adk.platform import uuid as platform_uuid
3333

34+
_sqlalchemy_import_error: ImportError | None = None
3435
try:
3536
from sqlalchemy import delete
3637
from sqlalchemy import event
@@ -49,8 +50,9 @@
4950
from sqlalchemy.ext.asyncio import AsyncSession as DatabaseSessionFactory
5051
from sqlalchemy.ext.asyncio import create_async_engine
5152
from sqlalchemy.pool import StaticPool
52-
except ImportError:
53-
pass
53+
except ImportError as e:
54+
# Re-raised by __init__, so the module still imports without the db extra.
55+
_sqlalchemy_import_error = e
5456
from typing_extensions import override
5557

5658
from . import _session_util
@@ -352,12 +354,15 @@ def __init__(
352354
ValueError: If neither or both db_url and db_engine are provided, or if
353355
engine creation fails.
354356
"""
355-
try:
356-
import sqlalchemy # noqa: F401
357-
except ImportError as e:
357+
# Re-importing cannot tell whether the imports above failed: without
358+
# greenlet, SQLAlchemy 2.1 raises on the first import of its asyncio
359+
# extension and lets later ones succeed.
360+
if _sqlalchemy_import_error is not None:
358361
from ..utils._dependency import missing_extra
359362

360-
raise missing_extra("sqlalchemy", "db") from e
363+
raise missing_extra(
364+
"sqlalchemy[asyncio]", "db"
365+
) from _sqlalchemy_import_error
361366

362367
if (db_url is None) == (db_engine is None):
363368
raise ValueError(

‎tests/unittests/test_optional_dependencies.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ def test_database_session_service_fails_on_creation():
146146
assert "sqlalchemy" in str(exc_info.value)
147147

148148

149+
def test_database_session_service_names_its_extra_when_asyncio_failed_to_load():
150+
"""Without greenlet, SQLAlchemy 2.1 fails only the first asyncio import.
151+
152+
The module is loaded while that import fails, and the service is created
153+
after it would succeed again, as happens in a real environment.
154+
"""
155+
module_name = "google.adk.sessions.database_session_service"
156+
original = sys.modules.pop(module_name, None)
157+
try:
158+
with mock.patch.dict("sys.modules", {"sqlalchemy.ext.asyncio": None}):
159+
from google.adk.sessions import DatabaseSessionService
160+
161+
with pytest.raises(
162+
ImportError, match=r"sqlalchemy\[asyncio\].*google-adk\[db\]"
163+
):
164+
DatabaseSessionService(db_url="sqlite+aiosqlite:///:memory:")
165+
finally:
166+
sys.modules.pop(module_name, None)
167+
if original is not None:
168+
sys.modules[module_name] = original
169+
170+
149171
def test_vertex_ai_session_service_fails_on_creation():
150172
"""Verify that creating VertexAiSessionService without extra fails using mocks."""
151173
try:

‎tests/unittests/test_release_dependencies.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,29 @@ def test_extras_that_reach_aiohttp_indirectly_declare_it(
306306
assert 'aiohttp' in names, f'The {extra!r} extra needs aiohttp: {reason}.'
307307

308308

309+
@pytest.mark.parametrize('extra', ['db', 'all', 'test'])
310+
def test_sqlalchemy_extras_request_the_asyncio_extra(
311+
pyproject: dict, extra: str
312+
) -> None:
313+
"""DatabaseSessionService needs greenlet, which SQLAlchemy 2.1 made optional.
314+
315+
SQLAlchemy 2.0 installed greenlet on common platforms by default. From 2.1 it
316+
comes only with ``sqlalchemy[asyncio]``, and without it
317+
``sqlalchemy.ext.asyncio`` fails to import.
318+
"""
319+
wanted = canonicalize_name('sqlalchemy')
320+
requirements = [
321+
Requirement(entry)
322+
for entry in pyproject['project']['optional-dependencies'][extra]
323+
]
324+
sqlalchemy = [r for r in requirements if canonicalize_name(r.name) == wanted]
325+
assert sqlalchemy, f'The {extra!r} extra must declare sqlalchemy.'
326+
assert all('asyncio' in r.extras for r in sqlalchemy), (
327+
f'The {extra!r} extra must require sqlalchemy[asyncio]; plain sqlalchemy'
328+
' 2.1+ installs without greenlet, so DatabaseSessionService cannot start.'
329+
)
330+
331+
309332
def test_main_deps_require_lazy_mcp_google_genai_release(
310333
pyproject: dict,
311334
) -> None:

0 commit comments

Comments
 (0)