Skip to content

Commit b7b27aa

Browse files
Refactor xCDAT logging for consistency and safety (#811)
1 parent 08a340a commit b7b27aa

File tree

3 files changed

+156
-11
lines changed

3 files changed

+156
-11
lines changed

tests/test_logger.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import logging
2+
3+
import pytest
4+
5+
from xcdat._logger import _setup_custom_logger, _setup_xcdat_logger
6+
7+
8+
@pytest.fixture(autouse=True)
9+
def reset_logging():
10+
# Reset logging before each test.
11+
logging.shutdown()
12+
13+
for handler in logging.root.handlers[:]:
14+
logging.root.removeHandler(handler)
15+
16+
# Reset xcdat logger specifically
17+
xcdat_logger = logging.getLogger("xcdat")
18+
for handler in xcdat_logger.handlers[:]:
19+
xcdat_logger.removeHandler(handler)
20+
21+
xcdat_logger.setLevel(logging.NOTSET)
22+
xcdat_logger.propagate = True
23+
24+
yield
25+
26+
# Cleanup after test.
27+
logging.shutdown()
28+
29+
30+
class TestSetupXCDATLogger:
31+
def test_logger_creation(self):
32+
logger = _setup_xcdat_logger()
33+
34+
assert isinstance(logger, logging.Logger)
35+
assert logger.name == "xcdat"
36+
37+
def test_logger_level(self):
38+
logger = _setup_xcdat_logger(level=logging.DEBUG)
39+
40+
assert logger.level == logging.DEBUG
41+
42+
def test_logger_force(self):
43+
logger = _setup_xcdat_logger()
44+
handler_ids_before = [id(h) for h in logger.handlers]
45+
46+
# Force reconfiguration.
47+
logger = _setup_xcdat_logger(force=True)
48+
49+
handler_ids_after = [id(h) for h in logger.handlers]
50+
51+
# Clean reset with one handler.
52+
assert len(logger.handlers) == 1
53+
# The handler should be a new instance, not the same object.
54+
assert handler_ids_before != handler_ids_after
55+
56+
def test_logger_format(self):
57+
logger = _setup_xcdat_logger()
58+
handler = logger.handlers[0]
59+
60+
assert isinstance(handler.formatter, logging.Formatter)
61+
62+
# Use the public API to check the formatter's output
63+
test_record = logging.LogRecord(
64+
name="xcdat",
65+
level=logging.INFO,
66+
pathname=__file__,
67+
lineno=123,
68+
msg="Test message",
69+
args=(),
70+
exc_info=None,
71+
func="test_func",
72+
)
73+
formatted = handler.formatter.format(test_record)
74+
assert "Test message" in formatted
75+
assert "[INFO]" in formatted
76+
assert "test_func" in formatted
77+
assert str(123) in formatted
78+
79+
def test_logger_propagation(self):
80+
logger = _setup_xcdat_logger()
81+
82+
assert logger.propagate is False
83+
84+
def test_logger_no_duplicate_handlers(self):
85+
logger1 = _setup_xcdat_logger()
86+
num_handlers_before = len(logger1.handlers)
87+
88+
logger2 = _setup_xcdat_logger()
89+
num_handlers_after = len(logger2.handlers)
90+
91+
assert num_handlers_after == num_handlers_before
92+
93+
94+
class TestSetupCustomLogger:
95+
def test_custom_logger_creation(self):
96+
logger = _setup_custom_logger("test_logger")
97+
98+
assert isinstance(logger, logging.Logger)
99+
assert logger.name == "test_logger"
100+
101+
def test_custom_logger_propagation(self):
102+
logger = _setup_custom_logger("test_logger", propagate=False)
103+
assert logger.propagate is False
104+
105+
logger = _setup_custom_logger("test_logger", propagate=True)
106+
assert logger.propagate is True
107+
108+
def test_custom_logger_inherits_from_xcdat(self):
109+
_setup_xcdat_logger()
110+
logger = _setup_custom_logger("xcdat.submodule")
111+
112+
assert logger.parent is not None and logger.parent.name == "xcdat"

xcdat/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Top-level package for xcdat."""
22

33
from xcdat import tutorial # noqa: F401
4+
from xcdat._logger import _setup_xcdat_logger
45
from xcdat.axis import ( # noqa: F401
56
center_times,
67
get_coords_by_name,
@@ -25,4 +26,7 @@
2526
from xcdat.temporal import TemporalAccessor # noqa: F401
2627
from xcdat.utils import compare_datasets # noqa: F401
2728

29+
# Initialize xCDAT logger once when the package is imported
30+
_setup_xcdat_logger()
31+
2832
__version__ = "0.10.1"

xcdat/_logger.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,49 @@
11
"""Logger module for setting up a logger."""
22

33
import logging
4-
import logging.handlers
54

6-
# Logging module setup
7-
log_format = (
5+
LOG_FORMAT = (
86
"%(asctime)s [%(levelname)s]: %(filename)s(%(funcName)s:%(lineno)s) >> %(message)s"
97
)
10-
logging.basicConfig(format=log_format, filemode="w", level=logging.INFO)
11-
12-
# Console handler setup
13-
console_handler = logging.StreamHandler()
14-
console_handler.setLevel(logging.INFO)
15-
logFormatter = logging.Formatter(log_format)
16-
console_handler.setFormatter(logFormatter)
17-
logging.getLogger().addHandler(console_handler)
8+
LOG_LEVEL = logging.INFO
9+
10+
11+
def _setup_xcdat_logger(level: int = LOG_LEVEL, force: bool = False) -> logging.Logger:
12+
"""Configures and returns the xCDAT package logger.
13+
14+
Parameters
15+
----------
16+
level : int
17+
Logging level for xCDAT. Should be a logging level constant (e.g.,
18+
logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
19+
logging.CRITICAL).
20+
force : bool, optional
21+
If True, clears existing handlers before adding a new one. If False
22+
(default), only adds a handler if none exist. Defaults to False.
23+
24+
Returns
25+
-------
26+
logging.Logger
27+
The xCDAT package logger.
28+
"""
29+
logger = logging.getLogger("xcdat")
30+
31+
if force:
32+
for handler in logger.handlers[:]:
33+
logger.removeHandler(handler)
34+
35+
if not logger.handlers:
36+
handler = logging.StreamHandler()
37+
handler.setFormatter(logging.Formatter(LOG_FORMAT))
38+
logger.addHandler(handler)
39+
40+
# Update level on logger and all handlers
41+
logger.setLevel(level)
42+
for handler in logger.handlers:
43+
handler.setLevel(level)
44+
45+
logger.propagate = False
46+
return logger
1847

1948

2049
def _setup_custom_logger(name, propagate=True) -> logging.Logger:

0 commit comments

Comments
 (0)