diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b68702d..b034603f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ So please make sure to **always** properly configure your applications. [#401](https://github.com/hynek/structlog/pull/401) - `structlog.PrintLogger` -- that is used by default -- now uses `print()` for printing, making it a better citizen for interactive terminal applications. [#399](https://github.com/hynek/structlog/pull/399) +- `structlog.testing.capture_logs` now works for already initialized bound loggers. + [#408](https://github.com/hynek/structlog/pull/412) ### Fixed diff --git a/src/structlog/testing.py b/src/structlog/testing.py index 1b62392f..075ab449 100644 --- a/src/structlog/testing.py +++ b/src/structlog/testing.py @@ -59,12 +59,22 @@ def capture_logs() -> Generator[List[EventDict], None, None]: .. versionadded:: 20.1.0 """ cap = LogCapture() - old_processors = get_config()["processors"] + # Modify `_Configuration.default_processors` set via `configure` but always + # keep the list instance intact to not break references held by bound + # loggers. + processors = get_config()["processors"] + old_processors = processors.copy() try: - configure(processors=[cap]) + # clear processors list and use LogCapture for testing + processors.clear() + processors.append(cap) + configure(processors=processors) yield cap.entries finally: - configure(processors=old_processors) + # remove LogCapture and restore original processors + processors.clear() + processors.extend(old_processors) + configure(processors=processors) class ReturnLogger: diff --git a/tests/test_testing.py b/tests/test_testing.py index ddbf70dd..99b4a75c 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -10,6 +10,7 @@ CapturedCall, CapturingLogger, CapturingLoggerFactory, + LogCapture, ReturnLogger, ReturnLoggerFactory, ) @@ -46,11 +47,16 @@ def test_restores_processors_on_success(self): exit. """ orig_procs = self.get_active_procs() + assert len(orig_procs) > 1 with testing.capture_logs(): - assert orig_procs is not self.get_active_procs() + modified_procs = self.get_active_procs() + assert len(modified_procs) == 1 + assert isinstance(modified_procs[0], LogCapture) - assert orig_procs is self.get_active_procs() + restored_procs = self.get_active_procs() + assert orig_procs is restored_procs + assert len(restored_procs) > 1 def test_restores_processors_on_error(self): """ @@ -64,6 +70,26 @@ def test_restores_processors_on_error(self): assert orig_procs is self.get_active_procs() + def test_captures_bound_logers(self): + """ + Even logs from already bound loggers are captured and their processors + restored on exit. + """ + logger = get_logger("bound").bind(foo="bar") + logger.info("ensure logger is bound") + + with testing.capture_logs() as logs: + logger.info("hello", answer=42) + + assert logs == [ + { + "event": "hello", + "answer": 42, + "foo": "bar", + "log_level": "info", + } + ] + class TestReturnLogger: # @pytest.mark.parametrize("method", stdlib_log_methods)