diff --git a/pyrit/memory/azure_sql_memory.py b/pyrit/memory/azure_sql_memory.py index c9f349c0d..6782cddc4 100644 --- a/pyrit/memory/azure_sql_memory.py +++ b/pyrit/memory/azure_sql_memory.py @@ -640,7 +640,15 @@ def dispose_engine(self) -> None: """ if self.engine: self.engine.dispose() - logger.info("Engine disposed successfully.") + # During interpreter shutdown, logging handler streams may already be closed, + # causing the framework to print "Logging error" to stderr (GH-1520). + # Temporarily suppress logging errors for this teardown message. + previous_raise = logging.raiseExceptions + logging.raiseExceptions = False + try: + logger.info("Engine disposed successfully.") + finally: + logging.raiseExceptions = previous_raise def get_all_embeddings(self) -> Sequence[EmbeddingDataEntry]: """ diff --git a/pyrit/memory/sqlite_memory.py b/pyrit/memory/sqlite_memory.py index d59a6571d..d5a521700 100644 --- a/pyrit/memory/sqlite_memory.py +++ b/pyrit/memory/sqlite_memory.py @@ -346,7 +346,15 @@ def dispose_engine(self) -> None: """ if self.engine: self.engine.dispose() - logger.info("Engine disposed and all connections closed.") + # During interpreter shutdown, logging handler streams may already be closed, + # causing the framework to print "Logging error" to stderr (GH-1520). + # Temporarily suppress logging errors for this teardown message. + previous_raise = logging.raiseExceptions + logging.raiseExceptions = False + try: + logger.info("Engine disposed and all connections closed.") + finally: + logging.raiseExceptions = previous_raise def export_conversations( self, diff --git a/tests/unit/memory/test_sqlite_memory.py b/tests/unit/memory/test_sqlite_memory.py index 5e6d3b168..d174d58a1 100644 --- a/tests/unit/memory/test_sqlite_memory.py +++ b/tests/unit/memory/test_sqlite_memory.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import io +import logging import os import uuid from collections.abc import Sequence @@ -668,3 +670,25 @@ def test_get_conversation_stats_batches_multiple_conversations(sqlite_instance): assert result[conv_ids[0]].message_count == 1 assert result[conv_ids[1]].message_count == 2 assert result[conv_ids[2]].message_count == 3 + + +def test_dispose_engine_tolerates_closed_log_stream(sqlite_instance, capsys): + """Verify dispose_engine does not raise or emit 'Logging error' when streams are closed (GH-1520).""" + pyrit_logger = logging.getLogger("pyrit") + prev_level = pyrit_logger.level + pyrit_logger.setLevel(logging.INFO) + + stream = io.StringIO() + handler = logging.StreamHandler(stream) + root = logging.getLogger() + root.addHandler(handler) + + try: + stream.close() + sqlite_instance.dispose_engine() + finally: + root.removeHandler(handler) + pyrit_logger.setLevel(prev_level) + + captured = capsys.readouterr() + assert "Logging error" not in captured.err