Skip to content

Commit 0b3b9a9

Browse files
authored
Merge pull request #796 from amas0/logger-updates
Enable/disable logging helpers
2 parents d1aeceb + 21a0d22 commit 0b3b9a9

File tree

6 files changed

+155
-11
lines changed

6 files changed

+155
-11
lines changed

cmdstanpy/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def _cleanup_tmpdir() -> None:
4242
set_make_env,
4343
show_versions,
4444
write_stan_json,
45+
enable_logging,
46+
disable_logging,
4547
)
4648

4749
__all__ = [
@@ -63,4 +65,6 @@ def _cleanup_tmpdir() -> None:
6365
'show_versions',
6466
'rebuild_cmdstan',
6567
'cmdstan_version',
68+
"enable_logging",
69+
"disable_logging",
6670
]

cmdstanpy/utils/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
windows_short_path,
2929
)
3030
from .json import write_stan_json
31-
from .logging import get_logger
31+
from .logging import get_logger, enable_logging, disable_logging
3232
from .stancsv import (
3333
check_sampler_csv,
3434
parse_rdump_value,
@@ -144,4 +144,6 @@ def show_versions(output: bool = True) -> str:
144144
'windows_short_path',
145145
'wrap_url_progress_hook',
146146
'write_stan_json',
147+
'enable_logging',
148+
'disable_logging',
147149
]

cmdstanpy/utils/logging.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,62 @@
11
"""
22
CmdStanPy logging
33
"""
4+
45
import functools
56
import logging
7+
import types
8+
from contextlib import AbstractContextManager
9+
from typing import Optional, Type
610

711

812
@functools.lru_cache(maxsize=None)
913
def get_logger() -> logging.Logger:
1014
"""cmdstanpy logger"""
11-
logger = logging.getLogger('cmdstanpy')
12-
if len(logger.handlers) == 0:
15+
logger = logging.getLogger("cmdstanpy")
16+
if not logger.hasHandlers():
1317
# send all messages to handlers
1418
logger.setLevel(logging.DEBUG)
1519
# add a default handler to the logger to INFO and higher
1620
handler = logging.StreamHandler()
1721
handler.setLevel(logging.INFO)
1822
handler.setFormatter(
1923
logging.Formatter(
20-
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
24+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
2125
"%H:%M:%S",
2226
)
2327
)
2428
logger.addHandler(handler)
2529
return logger
30+
31+
32+
class ToggleLogging(AbstractContextManager):
33+
def __init__(self, disable: bool) -> None:
34+
self.disable = disable
35+
self.logger = get_logger()
36+
self.prev_state = self.logger.disabled
37+
self.logger.disabled = self.disable
38+
39+
def __repr__(self) -> str:
40+
return ""
41+
42+
def __enter__(self) -> "ToggleLogging":
43+
self.logger.disabled = self.disable
44+
return self
45+
46+
def __exit__(
47+
self,
48+
exc_type: Optional[Type[BaseException]],
49+
exc_value: Optional[BaseException],
50+
traceback: Optional[types.TracebackType],
51+
) -> None:
52+
self.logger.disabled = self.prev_state
53+
54+
55+
def enable_logging() -> ToggleLogging:
56+
"""Enable cmdstanpy logging. Can be used as a context manager"""
57+
return ToggleLogging(disable=False)
58+
59+
60+
def disable_logging() -> ToggleLogging:
61+
"""Disable cmdstanpy logging. Can be used as a context manager"""
62+
return ToggleLogging(disable=True)

docsrc/api.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,13 @@ write_stan_json
139139
===============
140140

141141
.. autofunction:: cmdstanpy.write_stan_json
142+
143+
enable_logging
144+
===============
145+
146+
.. autofunction:: cmdstanpy.enable_logging
147+
148+
disable_logging
149+
===============
150+
151+
.. autofunction:: cmdstanpy.disable_logging

docsrc/users-guide/outputs.rst

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,47 @@ You may notice CmdStanPy can produce a lot of output when it is running:
6060
6161
fit = model.sample(data=data_file, show_progress=False)
6262
63-
This output is managed through the built-in :mod:`logging` module. For example, it can be disabled entirely:
63+
This output is managed through the built-in :mod:`logging` module. For
64+
convenience, CmdStanPy provides the module-level :meth:`cmdstanpy.enable_logging()`
65+
and :meth:`cmdstanpy.disable_logging()` functions to simplify logging management.
66+
67+
For example, it can be disabled entirely:
6468

6569
.. ipython:: python
6670
67-
import logging
68-
cmdstanpy_logger = logging.getLogger("cmdstanpy")
69-
cmdstanpy_logger.disabled = True
71+
import cmdstanpy
72+
cmdstanpy.disable_logging()
7073
# look, no output!
7174
fit = model.sample(data=data_file, show_progress=False)
7275
73-
Or one can remove the logging handler that CmdStanPy installs by default and install their own for more
74-
fine-grained control. For example, the following code sends all logs (including the ``DEBUG`` logs, which are hidden by default),
75-
to a file.
76+
We can re-enable this by calling ``enable_logging()``:
77+
78+
.. ipython:: python
79+
80+
cmdstanpy.enable_logging()
81+
fit = model.sample(data=data_file, show_progress=False)
82+
83+
84+
These functions also work as context managers for more local control:
85+
86+
.. ipython:: python
87+
88+
with cmdstanpy.disable_logging():
89+
fit = model.sample(data=data_file, show_progress=False)
90+
91+
92+
For more fine-grained control, one can interact with the underlying ``logging``
93+
library directly. For example, the following code installs a custom handler
94+
that sends all logs (including the ``DEBUG`` logs, which are hidden by
95+
default), to a file.
7696

7797
DEBUG logging is useful primarily to developers or when trying to hunt down an issue.
7898

7999
.. ipython:: python
80100
101+
import logging
102+
cmdstanpy_logger = logging.getLogger("cmdstanpy")
103+
81104
cmdstanpy_logger.disabled = False
82105
# remove all existing handlers
83106
cmdstanpy_logger.handlers = []
@@ -93,6 +116,7 @@ DEBUG logging is useful primarily to developers or when trying to hunt down an i
93116
)
94117
cmdstanpy_logger.addHandler(handler)
95118
119+
96120
Now, if we run the model and check the contents of the file, we will see all the possible logging.
97121

98122
.. ipython:: python

test/test_logging.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Logging control tests"""
2+
3+
import logging
4+
import cmdstanpy
5+
6+
7+
def test_disable_logging(caplog):
8+
logger = cmdstanpy.utils.logging.get_logger()
9+
10+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
11+
logger.info("before")
12+
assert any("before" in m for m in caplog.messages)
13+
14+
caplog.clear()
15+
cmdstanpy.disable_logging()
16+
17+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
18+
logger.info("after")
19+
20+
assert not caplog.messages
21+
logger.disabled = False
22+
23+
24+
def test_disable_logging_context_manager(caplog):
25+
logger = cmdstanpy.utils.logging.get_logger()
26+
27+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
28+
logger.info("before")
29+
assert any("before" in m for m in caplog.messages)
30+
31+
caplog.clear()
32+
with cmdstanpy.disable_logging():
33+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
34+
logger.info("inside context manager")
35+
36+
assert not caplog.messages
37+
38+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
39+
logger.info("after")
40+
41+
assert any("after" in m for m in caplog.messages)
42+
logger.disabled = False
43+
44+
45+
def test_disable_logging_context_manager_nested(caplog):
46+
logger = cmdstanpy.utils.logging.get_logger()
47+
48+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
49+
logger.info("before")
50+
assert any("before" in m for m in caplog.messages)
51+
52+
caplog.clear()
53+
with cmdstanpy.disable_logging():
54+
with cmdstanpy.enable_logging():
55+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
56+
logger.info("inside context manager")
57+
58+
assert any("inside context manager" in m for m in caplog.messages)
59+
60+
caplog.clear()
61+
with cmdstanpy.enable_logging():
62+
with cmdstanpy.disable_logging():
63+
with caplog.at_level(logging.INFO, logger="cmdstanpy"):
64+
logger.info("inside context manager")
65+
66+
assert not caplog.messages
67+
logger.disabled = False

0 commit comments

Comments
 (0)