Skip to content

Commit

Permalink
Provide a synchronous sessionmaker even with async engines
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Jan 16, 2024
1 parent 7688208 commit 1138cc3
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 15 deletions.
8 changes: 4 additions & 4 deletions docs/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ Events with asynchronous engines/sessions
-----------------------------------------

SQLAlchemy doesn't support asynchronous events yet, and sessionmakers producing async
sessions cannot currently be used as a target for event listeners. As a workaround, you can
register an event listener on the :class:`~sqlalchemy.orm.Session` class and then check
in the listener itself if the ``session`` argument matches the async session's
``sync_session`` attribute in the current context::
sessions cannot currently be used as a target for event listeners. To work around this,
a synchronous ``sessionmaker`` resource is provided by this component even for async
engines. To add listeners, simply use this session maker as the target for the
listeners::

from asphalt.core import NoCurrentContext, get_resource, inject, resource
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down
3 changes: 3 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
**UNRELEASED**

- Dropped support for Python 3.7
- Changed component startup to always provide a ``sessionmaker`` resource, even with
async engines. This session maker is configured as the ``sync_session_class`` for the
async sessionmaker, and can be used to add session event listeners.

**5.0.1** (2023-04-03)

Expand Down
8 changes: 7 additions & 1 deletion src/asphalt/sqlalchemy/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class SQLAlchemyComponent(Component):
For asynchronous engines, the following resources are provided:
* :class:`~sqlalchemy.ext.asyncio.AsyncEngine`
* :class:`~sqlalchemy.orm.session.sessionmaker`
* :class:`~sqlalchemy.ext.asyncio.async_sessionmaker`
* :class:`~sqlalchemy.ext.asyncio.AsyncSession`
Expand Down Expand Up @@ -163,8 +164,12 @@ def __init__(
apply_sqlite_hacks(self.engine)

if isinstance(self.engine, AsyncEngine):
# This is needed for listening to ORM events when async sessions are used
self._sessionmaker = sessionmaker()
self._async_sessionmaker = async_sessionmaker(
bind=self._async_bind, **session_args
bind=self._async_bind,
sync_session_class=self._sessionmaker,
**session_args,
)
else:
self._sessionmaker = sessionmaker(bind=self._bind, **session_args)
Expand Down Expand Up @@ -215,6 +220,7 @@ async def start(self, ctx: Context) -> AsyncGenerator[None, Exception | None]:

bind = self._async_bind
ctx.add_resource(self.engine, self.resource_name)
ctx.add_resource(self._sessionmaker, self.resource_name)
ctx.add_resource(self._async_sessionmaker, self.resource_name)
ctx.add_resource_factory(
self.create_async_session,
Expand Down
41 changes: 31 additions & 10 deletions tests/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from contextlib import ExitStack
from pathlib import Path
from threading import Thread, current_thread
from typing import Any

import pytest
from asphalt.core import NoCurrentContext, current_context
Expand All @@ -30,28 +31,48 @@
pytestmark = pytest.mark.anyio


async def test_component_start_sync() -> None:
@pytest.mark.parametrize(
"component_opts, args",
[
pytest.param({}, ()),
pytest.param({"resource_name": "alternate"}, ("alternate",)),
],
)
async def test_component_start_sync(
component_opts: dict[str, Any], args: tuple[Any]
) -> None:
"""Test that the component creates all the expected (synchronous) resources."""
url = URL.create("sqlite", database=":memory:")
component = SQLAlchemyComponent(url=url)
component = SQLAlchemyComponent(url=url, **component_opts)
async with Context() as ctx:
await component.start(ctx)

ctx.require_resource(Engine)
ctx.require_resource(sessionmaker)
ctx.require_resource(Session)
ctx.require_resource(Engine, *args)
ctx.require_resource(sessionmaker, *args)
ctx.require_resource(Session, *args)


async def test_component_start_async() -> None:
@pytest.mark.parametrize(
"component_opts, args",
[
pytest.param({}, ()),
pytest.param({"resource_name": "alternate"}, ("alternate",)),
],
)
async def test_component_start_async(
component_opts: dict[str, Any], args: tuple[Any]
) -> None:
"""Test that the component creates all the expected (asynchronous) resources."""
url = URL.create("sqlite+aiosqlite", database=":memory:")
component = SQLAlchemyComponent(url=url)
component = SQLAlchemyComponent(url=url, **component_opts)
async with Context() as ctx:
await component.start(ctx)

ctx.require_resource(AsyncEngine)
ctx.require_resource(async_sessionmaker)
ctx.require_resource(AsyncSession)
ctx.require_resource(AsyncEngine, *args)
async_session_class = ctx.require_resource(async_sessionmaker, *args)
ctx.require_resource(AsyncSession, *args)
sync_session_class = ctx.require_resource(sessionmaker, *args)
assert async_session_class.kw["sync_session_class"] is sync_session_class


@pytest.mark.parametrize(
Expand Down

0 comments on commit 1138cc3

Please sign in to comment.