Skip to content

Commit 4a975af

Browse files
authored
Improve logging helper neo4j.debug.watch() (#946)
The helper did not handle logging level correctly. For example ``` neo4j.debug.watch("neo4j", out=sys.stdout) neo4j.debug.watch("neo4j", out=sys.stderr, level=logging.WARNING) ``` would've caused the logging level of the "neo4j" logger to end up being `WARNING` even though the first call (implicitly) requested `DEBUG`. The fix will make sure to set the logger's level to the most verbose requested level and use a level filter on the Handlers registered for each call to filter on the level accordingly.
1 parent f028ded commit 4a975af

File tree

2 files changed

+97
-23
lines changed

2 files changed

+97
-23
lines changed

src/neo4j/debug.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,14 @@ def watch(
166166
self.stop()
167167
handler = StreamHandler(out)
168168
handler.setFormatter(self.formatter)
169+
handler.setLevel(level)
169170
if self._task_info:
170171
handler.addFilter(TaskIdFilter())
171172
for logger in self. _loggers:
172173
self._handlers[logger.name] = handler
173174
logger.addHandler(handler)
174-
logger.setLevel(level)
175+
if logger.getEffectiveLevel() > level:
176+
logger.setLevel(level)
175177

176178
def stop(self) -> None:
177179
"""Disable logging for all loggers."""

tests/unit/common/test_debug.py

+94-22
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,24 @@ def __call__(self, *args: str) -> t.Sequence[t.Any]:
4343

4444

4545
@pytest.fixture
46-
def logger_mocker(mocker) -> _TSetupMockProtocol:
46+
def logger_mocker(mocker) -> t.Generator[_TSetupMockProtocol, None, None]:
47+
original_levels = {}
48+
4749
def setup_mock(*logger_names):
50+
nonlocal original_levels
51+
4852
loggers = [logging.getLogger(name) for name in logger_names]
4953
for logger in loggers:
50-
logger.addHandler = mocker.Mock()
51-
logger.removeHandler = mocker.Mock()
52-
logger.setLevel = mocker.Mock()
54+
original_levels[logger] = logger.level
55+
mocker.patch.object(logger, "addHandler")
56+
mocker.patch.object(logger, "removeHandler")
57+
mocker.spy(logger, "setLevel")
5358
return loggers
5459

55-
return setup_mock
60+
yield setup_mock
61+
62+
for logger, level in original_levels.items():
63+
logger.setLevel(level)
5664

5765

5866
def test_watch_returns_watcher(logger_mocker) -> None:
@@ -106,22 +114,21 @@ def test_watcher_context_manager(mocker) -> None:
106114
watcher.stop.assert_called_once()
107115

108116

109-
@pytest.mark.parametrize(
110-
("default_level", "level", "expected_level"),
111-
(
112-
(None, None, logging.DEBUG),
113-
(logging.WARNING, None, logging.WARNING),
114-
(logging.WARNING, logging.DEBUG, logging.DEBUG),
115-
(logging.DEBUG, logging.WARNING, logging.WARNING),
116-
(1, None, 1),
117-
(None, 1, 1),
118-
)
117+
WATCH_ARGS = (
118+
#default_level, level, expected_level
119+
(None, None, logging.DEBUG),
120+
(logging.WARNING, None, logging.WARNING),
121+
(logging.WARNING, logging.DEBUG, logging.DEBUG),
122+
(logging.DEBUG, logging.WARNING, logging.WARNING),
123+
(logging.INFO, None, logging.INFO),
124+
(logging.INFO, logging.DEBUG, logging.DEBUG),
125+
(logging.DEBUG, logging.INFO, logging.INFO),
126+
(1, None, 1),
127+
(None, 1, 1),
119128
)
120-
def test_watcher_level(
121-
logger_mocker, default_level, level, expected_level
122-
) -> None:
123-
logger_name = "neo4j"
124-
logger = logger_mocker(logger_name)[0]
129+
130+
131+
def _setup_watch(logger_name, default_level, level):
125132
kwargs = {}
126133
if default_level is not None:
127134
kwargs["default_level"] = default_level
@@ -131,9 +138,73 @@ def test_watcher_level(
131138
kwargs["level"] = level
132139
watcher.watch(**kwargs)
133140

141+
142+
@pytest.mark.parametrize(
143+
("default_level", "level", "expected_level"),
144+
WATCH_ARGS
145+
)
146+
@pytest.mark.parametrize(
147+
"effective_level",
148+
(logging.DEBUG, logging.WARNING, logging.INFO, logging.ERROR)
149+
)
150+
def test_watcher_level(
151+
logger_mocker, default_level, level, expected_level, effective_level,
152+
) -> None:
153+
logger_name = "neo4j"
154+
logger = logger_mocker(logger_name)[0]
155+
logger.setLevel(effective_level)
156+
logger.setLevel.reset_mock()
157+
_setup_watch(logger_name, default_level, level)
158+
134159
(handler,), _ = logger.addHandler.call_args
135-
assert handler.level == logging.NOTSET
136-
logger.setLevel.assert_called_once_with(expected_level)
160+
assert handler.level == expected_level
161+
if effective_level <= expected_level:
162+
logger.setLevel.assert_not_called()
163+
else:
164+
logger.setLevel.assert_called_once_with(expected_level)
165+
166+
167+
@pytest.mark.parametrize(
168+
("default_level1", "level1", "expected_level1"),
169+
WATCH_ARGS
170+
)
171+
@pytest.mark.parametrize(
172+
("default_level2", "level2", "expected_level2"),
173+
WATCH_ARGS
174+
)
175+
@pytest.mark.parametrize(
176+
"effective_level",
177+
(logging.DEBUG, logging.WARNING, logging.INFO, logging.ERROR)
178+
)
179+
def test_watcher_level_multiple_watchers(
180+
logger_mocker, default_level1, level1, expected_level1,
181+
default_level2, level2, expected_level2,
182+
effective_level,
183+
) -> None:
184+
logger_name = "neo4j"
185+
logger = logger_mocker(logger_name)[0]
186+
logger.setLevel(effective_level)
187+
logger.setLevel.reset_mock()
188+
_setup_watch(logger_name, default_level1, level1)
189+
_setup_watch(logger_name, default_level2, level2)
190+
191+
assert logger.addHandler.call_count == 2
192+
(handler1,), _ = logger.addHandler.call_args_list[0]
193+
(handler2,), _ = logger.addHandler.call_args_list[1]
194+
195+
assert handler1.level == expected_level1
196+
assert handler2.level == expected_level2
197+
198+
expected_logger_level = min(expected_level1, expected_level2)
199+
if effective_level <= expected_logger_level:
200+
logger.setLevel.assert_not_called()
201+
else:
202+
if effective_level > expected_level1 > expected_level2:
203+
assert logger.setLevel.call_count == 2
204+
else:
205+
assert logger.setLevel.call_count == 1
206+
(level,), _ = logger.setLevel.call_args_list[-1]
207+
assert level == expected_logger_level
137208

138209

139210
custom_log_out = io.StringIO()
@@ -220,6 +291,7 @@ def _assert_task_injection(
220291
handler_cls_mock = mocker.patch("neo4j.debug.StreamHandler", autospec=True)
221292
handler_mock = handler_cls_mock.return_value
222293
logger_name = "neo4j"
294+
logger_mocker(logger_name)[0]
223295
watcher = neo4j_debug.Watcher(logger_name, colour=colour,
224296
thread_info=thread, task_info=task)
225297
record_mock = mocker.Mock(spec=logging.LogRecord)

0 commit comments

Comments
 (0)