Skip to content

Commit 16e346d

Browse files
authored
Improve logging helper neo4j.debug.watch() (#1032)
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. Backport of #946
1 parent ba18132 commit 16e346d

File tree

2 files changed

+96
-33
lines changed

2 files changed

+96
-33
lines changed

neo4j/debug.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@ def watch(self, level=DEBUG, out=stderr):
8787
self.stop()
8888
handler = StreamHandler(out)
8989
handler.setFormatter(self.formatter)
90+
handler.setLevel(level)
9091
for logger in self. loggers:
9192
self.handlers[logger.name] = handler
9293
logger.addHandler(handler)
93-
logger.setLevel(level)
94+
if logger.getEffectiveLevel() > level:
95+
logger.setLevel(level)
9496

9597
def stop(self):
9698
"""Disable logging for all loggers."""

tests/unit/test_debug.py

Lines changed: 93 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,47 +26,55 @@
2626

2727

2828
@pytest.fixture
29-
def add_handler_mocker(mocker):
29+
def logger_mocker(mocker):
30+
original_levels = {}
31+
3032
def setup_mock(*logger_names):
33+
nonlocal original_levels
34+
3135
loggers = [logging.getLogger(name) for name in logger_names]
3236
for logger in loggers:
33-
logger.addHandler = mocker.Mock()
34-
logger.removeHandler = mocker.Mock()
35-
logger.setLevel = mocker.Mock()
37+
original_levels[logger] = logger.level
38+
mocker.patch.object(logger, "addHandler")
39+
mocker.patch.object(logger, "removeHandler")
40+
mocker.spy(logger, "setLevel")
3641
return loggers
3742

38-
return setup_mock
43+
yield setup_mock
44+
45+
for logger, level in original_levels.items():
46+
logger.setLevel(level)
3947

4048

41-
def test_watch_returns_watcher(add_handler_mocker):
49+
def test_watch_returns_watcher(logger_mocker):
4250
logger_name = "neo4j"
43-
add_handler_mocker(logger_name)
51+
logger_mocker(logger_name)
4452
watcher = neo4j_debug.watch(logger_name)
4553
assert isinstance(watcher, neo4j_debug.Watcher)
4654

4755

4856
@pytest.mark.parametrize("logger_names",
4957
(("neo4j",), ("foobar",), ("neo4j", "foobar")))
50-
def test_watch_enables_logging(logger_names, add_handler_mocker):
51-
loggers = add_handler_mocker(*logger_names)
58+
def test_watch_enables_logging(logger_names, logger_mocker):
59+
loggers = logger_mocker(*logger_names)
5260
neo4j_debug.watch(*logger_names)
5361
for logger in loggers:
5462
logger.addHandler.assert_called_once()
5563

5664

57-
def test_watcher_watch_adds_logger(add_handler_mocker):
65+
def test_watcher_watch_adds_logger(logger_mocker):
5866
logger_name = "neo4j"
59-
logger = add_handler_mocker(logger_name)[0]
67+
logger = logger_mocker(logger_name)[0]
6068
watcher = neo4j_debug.Watcher(logger_name)
6169

6270
logger.addHandler.assert_not_called()
6371
watcher.watch()
6472
logger.addHandler.assert_called_once()
6573

6674

67-
def test_watcher_stop_removes_logger(add_handler_mocker):
75+
def test_watcher_stop_removes_logger(logger_mocker):
6876
logger_name = "neo4j"
69-
logger = add_handler_mocker(logger_name)[0]
77+
logger = logger_mocker(logger_name)[0]
7078
watcher = neo4j_debug.Watcher(logger_name)
7179

7280
watcher.watch()
@@ -89,31 +97,84 @@ def test_watcher_context_manager(mocker):
8997
watcher.stop.assert_called_once()
9098

9199

92-
@pytest.mark.parametrize(
93-
("level", "expected_level"),
94-
(
95-
(None, logging.DEBUG),
96-
(logging.DEBUG, logging.DEBUG),
97-
(logging.WARNING, logging.WARNING),
98-
(1, 1),
99-
)
100+
WATCH_ARGS = (
101+
# level, expected_level
102+
(None, logging.DEBUG),
103+
(logging.DEBUG, logging.DEBUG),
104+
(logging.WARNING, logging.WARNING),
105+
(1, 1),
100106
)
101-
def test_watcher_level(add_handler_mocker, level, expected_level):
102-
logger_name = "neo4j"
103-
logger = add_handler_mocker(logger_name)[0]
107+
108+
109+
def _setup_watch(logger_name, level):
104110
watcher = neo4j_debug.Watcher(logger_name)
105111
kwargs = {}
106112
if level is not None:
107113
kwargs["level"] = level
108114
watcher.watch(**kwargs)
109115

116+
117+
@pytest.mark.parametrize(("level", "expected_level"), WATCH_ARGS)
118+
@pytest.mark.parametrize(
119+
"effective_level",
120+
(logging.DEBUG, logging.WARNING, logging.INFO, logging.ERROR)
121+
)
122+
def test_watcher_level(
123+
logger_mocker, level, expected_level, effective_level,
124+
):
125+
logger_name = "neo4j"
126+
logger = logger_mocker(logger_name)[0]
127+
logger.setLevel(effective_level)
128+
logger.setLevel.reset_mock()
129+
_setup_watch(logger_name, level)
130+
110131
(handler,), _ = logger.addHandler.call_args
111-
assert handler.level == logging.NOTSET
112-
logger.setLevel.assert_called_once_with(expected_level)
132+
assert handler.level == expected_level
133+
if effective_level <= expected_level:
134+
logger.setLevel.assert_not_called()
135+
else:
136+
logger.setLevel.assert_called_once_with(expected_level)
137+
138+
139+
@pytest.mark.parametrize(("level1", "expected_level1"), WATCH_ARGS)
140+
@pytest.mark.parametrize(("level2", "expected_level2"), WATCH_ARGS)
141+
@pytest.mark.parametrize(
142+
"effective_level",
143+
(logging.DEBUG, logging.WARNING, logging.INFO, logging.ERROR)
144+
)
145+
def test_watcher_level_multiple_watchers(
146+
logger_mocker, level1, expected_level1, level2, expected_level2,
147+
effective_level,
148+
):
149+
logger_name = "neo4j"
150+
logger = logger_mocker(logger_name)[0]
151+
logger.setLevel(effective_level)
152+
logger.setLevel.reset_mock()
153+
_setup_watch(logger_name, level1)
154+
_setup_watch(logger_name, level2)
155+
156+
assert logger.addHandler.call_count == 2
157+
(handler1,), _ = logger.addHandler.call_args_list[0]
158+
(handler2,), _ = logger.addHandler.call_args_list[1]
159+
160+
assert handler1.level == expected_level1
161+
assert handler2.level == expected_level2
162+
163+
expected_logger_level = min(expected_level1, expected_level2)
164+
if effective_level <= expected_logger_level:
165+
logger.setLevel.assert_not_called()
166+
else:
167+
if effective_level > expected_level1 > expected_level2:
168+
assert logger.setLevel.call_count == 2
169+
else:
170+
assert logger.setLevel.call_count == 1
171+
(level,), _ = logger.setLevel.call_args_list[-1]
172+
assert level == expected_logger_level
113173

114174

115175
custom_log_out = io.StringIO()
116176

177+
117178
@pytest.mark.parametrize(
118179
("out", "expected_out"),
119180
(
@@ -123,9 +184,9 @@ def test_watcher_level(add_handler_mocker, level, expected_level):
123184
(custom_log_out, custom_log_out),
124185
)
125186
)
126-
def test_watcher_out(add_handler_mocker, out, expected_out):
187+
def test_watcher_out(logger_mocker, out, expected_out):
127188
logger_name = "neo4j"
128-
logger = add_handler_mocker(logger_name)[0]
189+
logger = logger_mocker(logger_name)[0]
129190
watcher = neo4j_debug.Watcher(logger_name)
130191
kwargs = {}
131192
if out is not None:
@@ -137,9 +198,9 @@ def test_watcher_out(add_handler_mocker, out, expected_out):
137198
assert handler.stream == expected_out
138199

139200

140-
def test_watcher_colour(add_handler_mocker):
201+
def test_watcher_colour(logger_mocker):
141202
logger_name = "neo4j"
142-
logger = add_handler_mocker(logger_name)[0]
203+
logger = logger_mocker(logger_name)[0]
143204
watcher = neo4j_debug.Watcher(logger_name)
144205
watcher.watch()
145206

@@ -148,9 +209,9 @@ def test_watcher_colour(add_handler_mocker):
148209
assert isinstance(handler.formatter, neo4j_debug.ColourFormatter)
149210

150211

151-
def test_watcher_format(add_handler_mocker):
212+
def test_watcher_format(logger_mocker):
152213
logger_name = "neo4j"
153-
logger = add_handler_mocker(logger_name)[0]
214+
logger = logger_mocker(logger_name)[0]
154215
watcher = neo4j_debug.Watcher(logger_name)
155216
watcher.watch()
156217

0 commit comments

Comments
 (0)