From fd73e41001e334e4a81184bc24c843a4afaaf832 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Tue, 29 Sep 2026 23:55:16 +0530 Subject: [PATCH] fix: preserve configured logging levels Signed-off-by: Shubham Padkonde --- cmdstanpy/utils/logging.py | 5 +++-- docsrc/users-guide/outputs.rst | 10 ++++++++++ test/test_logging.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/cmdstanpy/utils/logging.py b/cmdstanpy/utils/logging.py index e2149d75..5584a873 100644 --- a/cmdstanpy/utils/logging.py +++ b/cmdstanpy/utils/logging.py @@ -16,8 +16,9 @@ def get_logger() -> logging.Logger: """cmdstanpy logger""" logger = logging.getLogger("cmdstanpy") if not logger.hasHandlers(): - # send all messages to handlers - logger.setLevel(logging.DEBUG) + # Send all messages to handlers unless the user selected a level. + if logger.level == logging.NOTSET: + logger.setLevel(logging.DEBUG) # add a default handler to the logger to WARNING and higher handler = logging.StreamHandler() handler.setLevel(logging.WARNING) diff --git a/docsrc/users-guide/outputs.rst b/docsrc/users-guide/outputs.rst index 148a24a0..5e293201 100644 --- a/docsrc/users-guide/outputs.rst +++ b/docsrc/users-guide/outputs.rst @@ -97,6 +97,16 @@ These functions also work as context managers for more local control: fit = model.sample(data=data_file, show_progress=False) +An explicitly configured level is preserved when CmdStanPy initializes its +logger. For example, to show errors but suppress warnings, set the level before +using a model: + +.. code-block:: python + + import logging + + logging.getLogger("cmdstanpy").setLevel(logging.ERROR) + For more fine-grained control, one can interact with the underlying ``logging`` library directly. For example, the following code installs a custom handler that sends all logs (including the ``DEBUG`` logs, which are hidden by diff --git a/test/test_logging.py b/test/test_logging.py index f20388e6..b7bc054b 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -1,12 +1,45 @@ """Logging control tests""" import logging +import subprocess +import sys import pytest import cmdstanpy +@pytest.mark.parametrize( + "level", [logging.NOTSET, logging.DEBUG, logging.WARNING, logging.ERROR] +) +def test_logger_initialization_preserves_level(level: int) -> None: + # A fresh process avoids pytest's logging handlers and the cached logger. + result = subprocess.run( + [ + sys.executable, + "-c", + f""" +import logging +import cmdstanpy + +logger = logging.getLogger('cmdstanpy') +logger.setLevel({level}) +logger = cmdstanpy.utils.get_logger() +print(logger.level) +logger.warning('warning message') +logger.error('error message') +""", + ], + capture_output=True, + text=True, + check=True, + ) + expected_level = logging.DEBUG if level == logging.NOTSET else level + assert int(result.stdout) == expected_level + assert ('warning message' in result.stderr) == (level <= logging.WARNING) + assert 'error message' in result.stderr + + def test_disable_logging(caplog: pytest.LogCaptureFixture) -> None: logger = cmdstanpy.utils.logging.get_logger()