From b2ecee4086662bf62b873800f03bd8f67b6137c8 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:59:38 +0000 Subject: [PATCH 1/3] Fix atexit handler for async drivers during shutdown Use close=False when disposing engines in the atexit handler to avoid MissingGreenlet errors with async drivers (e.g., aiosqlite) that require an event loop context to close connections. The OS will clean up connections when the process exits. Co-Authored-By: Claude Opus 4.5 --- sqlalchemy_bind_manager/_bind_manager.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sqlalchemy_bind_manager/_bind_manager.py b/sqlalchemy_bind_manager/_bind_manager.py index ae89e59..2c0dec8 100644 --- a/sqlalchemy_bind_manager/_bind_manager.py +++ b/sqlalchemy_bind_manager/_bind_manager.py @@ -91,18 +91,24 @@ def __init__( self.__init_bind(DEFAULT_BIND_NAME, config) SQLAlchemyBindManager._instances.add(self) - def _dispose_sync(self) -> None: + def _dispose_sync(self, close: bool = True) -> None: """Dispose all engines synchronously. This method is safe to call from any context, including __del__ and atexit handlers. For async engines, it uses the underlying sync_engine to perform synchronous disposal. + + Args: + close: If True (default), also close all currently checked-in + connections. Set to False during interpreter shutdown to + avoid issues with async drivers (e.g., aiosqlite) that + require an event loop to close connections. """ for bind in self.__binds.values(): if isinstance(bind, SQLAlchemyAsyncBind): - bind.engine.sync_engine.dispose() + bind.engine.sync_engine.dispose(close=close) else: - bind.engine.dispose() + bind.engine.dispose(close=close) def __del__(self) -> None: self._dispose_sync() @@ -230,6 +236,10 @@ def _cleanup_all_managers() -> None: This ensures all SQLAlchemyBindManager instances have their engines disposed before the interpreter exits, even if __del__ hasn't been called yet due to reference cycles or other GC timing issues. + + Uses close=False to avoid issues with async drivers (e.g., aiosqlite) + that require an event loop context to close connections. The OS will + clean up connections when the process exits. """ for manager in list(SQLAlchemyBindManager._instances): - manager._dispose_sync() + manager._dispose_sync(close=False) From 45987993b62a1e7c7579b45fe79d716cdd235642 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:35:13 +0000 Subject: [PATCH 2/3] Rename _dispose_sync to dispose_engines and make it public Co-Authored-By: Claude Opus 4.5 --- sqlalchemy_bind_manager/_bind_manager.py | 20 +++++--------------- tests/test_sqlalchemy_bind_manager.py | 6 +++--- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/sqlalchemy_bind_manager/_bind_manager.py b/sqlalchemy_bind_manager/_bind_manager.py index 2c0dec8..a8cb57e 100644 --- a/sqlalchemy_bind_manager/_bind_manager.py +++ b/sqlalchemy_bind_manager/_bind_manager.py @@ -91,27 +91,21 @@ def __init__( self.__init_bind(DEFAULT_BIND_NAME, config) SQLAlchemyBindManager._instances.add(self) - def _dispose_sync(self, close: bool = True) -> None: + def dispose_engines(self) -> None: """Dispose all engines synchronously. This method is safe to call from any context, including __del__ and atexit handlers. For async engines, it uses the underlying sync_engine to perform synchronous disposal. - - Args: - close: If True (default), also close all currently checked-in - connections. Set to False during interpreter shutdown to - avoid issues with async drivers (e.g., aiosqlite) that - require an event loop to close connections. """ for bind in self.__binds.values(): if isinstance(bind, SQLAlchemyAsyncBind): - bind.engine.sync_engine.dispose(close=close) + bind.engine.sync_engine.dispose() else: - bind.engine.dispose(close=close) + bind.engine.dispose() def __del__(self) -> None: - self._dispose_sync() + self.dispose_engines() def __init_bind(self, name: str, config: SQLAlchemyConfig): if not isinstance(config, SQLAlchemyConfig): @@ -236,10 +230,6 @@ def _cleanup_all_managers() -> None: This ensures all SQLAlchemyBindManager instances have their engines disposed before the interpreter exits, even if __del__ hasn't been called yet due to reference cycles or other GC timing issues. - - Uses close=False to avoid issues with async drivers (e.g., aiosqlite) - that require an event loop context to close connections. The OS will - clean up connections when the process exits. """ for manager in list(SQLAlchemyBindManager._instances): - manager._dispose_sync(close=False) + manager.dispose_engines() diff --git a/tests/test_sqlalchemy_bind_manager.py b/tests/test_sqlalchemy_bind_manager.py index 80eb64f..27b582f 100644 --- a/tests/test_sqlalchemy_bind_manager.py +++ b/tests/test_sqlalchemy_bind_manager.py @@ -115,8 +115,8 @@ def test_atexit_cleanup_disposes_all_managers(multiple_config): with patch.object( sa_manager, - "_dispose_sync", - ) as mocked_dispose_sync: + "dispose_engines", + ) as mocked_dispose_engines: _cleanup_all_managers() - mocked_dispose_sync.assert_called_once() + mocked_dispose_engines.assert_called_once() From ac7af0436784a01b178f846299da7964ef796071 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:41:46 +0000 Subject: [PATCH 3/3] Document dispose_engines method and engine lifecycle Co-Authored-By: Claude Opus 4.5 --- docs/manager/config.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/manager/config.md b/docs/manager/config.md index 767ad6a..eb40290 100644 --- a/docs/manager/config.md +++ b/docs/manager/config.md @@ -88,3 +88,23 @@ async with sa_manager.get_session() as session: Note that async implementation has several differences from the sync one, make sure to check [SQLAlchemy asyncio documentation](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) + +## Bind engines lifecycle + +Engine disposal is handled automatically by `SQLAlchemyBindManager`. Engines are disposed when: + +* The manager instance is garbage collected +* The Python interpreter shuts down (via an `atexit` handler) + +In some scenarios, such as automated tests, you may need to manually dispose engines to release database connections between tests. The `dispose_engines()` method is available for this purpose: + +```python +sa_manager = SQLAlchemyBindManager(config) + +# ... use the manager ... + +# Manually dispose all engines +sa_manager.dispose_engines() +``` + +This method disposes all engines synchronously, including async engines (using their underlying sync engine).