From 69ef2160c1b1c1dcff157c07f29a7e8e325c0e9d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 23:35:32 +0000 Subject: [PATCH] Dispose DB engine in CLI to fix Python 3.13 ResourceWarning leak The `rcdb run` CLI test test_number_shortcut_equals_run (added in 876e2a5) fails only on Python 3.13: it asserts that `rcdb 1000` and `rcdb run 1000` produce identical captured output, but Python 3.13 emits `ResourceWarning: unclosed database` for the leaked SQLite connection at GC time, and Click's CliRunner folds stderr into the captured output, so the warning non-deterministically pollutes one of the two runs. Root cause: the CLI never disconnected its lazily-created RCDBProvider, and `disconnect()` only closed the ORM session, never disposing the engine - so the pooled DBAPI connection stayed open until garbage collection. - provider.disconnect(): also dispose the engine so the connection pool releases its DBAPI connections (null-safe). - app_context: add close() to tear down the provider. - cli/app: register ctx.call_on_close(close) so every command disposes the engine deterministically instead of relying on GC. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SeseguLYWWEBNUZc2Fq4j9 --- python/rcdb/app_context.py | 12 ++++++++++++ python/rcdb/cli/app.py | 6 ++++++ python/rcdb/provider.py | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/python/rcdb/app_context.py b/python/rcdb/app_context.py index 0e2cf7c..af79b71 100644 --- a/python/rcdb/app_context.py +++ b/python/rcdb/app_context.py @@ -19,6 +19,18 @@ def db(self) -> RCDBProvider: self._db_instance = RCDBProvider(self.connection_str) return self._db_instance + def close(self): + """Disconnect the database if one was opened. + + Safe to call when no connection was ever made. The CLI registers this on + the Click context so the engine is disposed deterministically when the + command finishes, instead of waiting for garbage collection (which on + Python 3.13+ leaks a ``ResourceWarning: unclosed database``). + """ + if self._db_instance is not None: + self._db_instance.disconnect() + self._db_instance = None + def require_connected_db(self) -> RCDBProvider: """Return the RCDBProvider, or fail with a clear CLI error if no connection was given. diff --git a/python/rcdb/cli/app.py b/python/rcdb/cli/app.py index ccfac5d..625b21c 100644 --- a/python/rcdb/cli/app.py +++ b/python/rcdb/cli/app.py @@ -60,6 +60,12 @@ def rcdb_cli(ctx, user_config, connection, config, verbose): for key, value in config: ctx.obj.set_config(key, value) + # Dispose the DB engine deterministically when the command finishes, rather + # than leaving it to garbage collection. On Python 3.13+ a lingering, GC'd + # SQLite connection emits "ResourceWarning: unclosed database" at an + # unpredictable time, which can leak into command output captured by tests. + ctx.call_on_close(ctx.obj.close) + # Bo commands given if ctx.invoked_subcommand is None: # There is a connection but no subcommand diff --git a/python/rcdb/provider.py b/python/rcdb/provider.py index 453c1df..97f2c61 100644 --- a/python/rcdb/provider.py +++ b/python/rcdb/provider.py @@ -150,9 +150,19 @@ def connect(self, connection_string="mysql+pymysql://rcdb@127.0.0.1/rcdb", check # Closes connection to data # ------------------------------------------------ def disconnect(self): - """Closes connection to database""" + """Closes connection to database. + + Closes the ORM session and disposes the SQLAlchemy engine so the + underlying connection pool releases its DBAPI connections. Without the + ``engine.dispose()`` the pooled SQLite/MySQL connection stays open until + the engine is garbage collected, which on Python 3.13+ surfaces as a + ``ResourceWarning: unclosed database`` at an unpredictable time. + """ self._is_connected = False - self.session.close() + if self.session is not None: + self.session.close() + if self.engine is not None: + self.engine.dispose() # ------------------------------------------------- # indicates ether the connection is open or not