diff --git a/changelog/14189.bugfix.rst b/changelog/14189.bugfix.rst new file mode 100644 index 00000000000..101c3c21a3e --- /dev/null +++ b/changelog/14189.bugfix.rst @@ -0,0 +1 @@ +Fixed nested :meth:`caplog.filtering() ` calls with the same filter removing the filter too early when the inner context exits. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 6f34c1b93fd..ce52ebb303d 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -581,15 +581,22 @@ def filtering(self, filter_: logging.Filter) -> Generator[None]: :meth:`handler` for the 'with' statement block, and removes that filter at the end of the block. + If the filter is already attached to the handler (e.g. from an outer + nested ``filtering()`` call), it is not added again, and will not be + removed when the inner block exits. + :param filter_: A custom :class:`logging.Filter` object. .. versionadded:: 7.5 """ - self.handler.addFilter(filter_) + already_present = filter_ in self.handler.filters + if not already_present: + self.handler.addFilter(filter_) try: yield finally: - self.handler.removeFilter(filter_) + if not already_present: + self.handler.removeFilter(filter_) @fixture diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 5f94cb8508a..133b656b0da 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -206,6 +206,27 @@ def filter(self, record: logging.LogRecord) -> bool: assert unfiltered_tuple == ("test_fixture", 20, "handler call") +def test_nested_filtering_same_filter(caplog: pytest.LogCaptureFixture) -> None: + """Nested ``caplog.filtering()`` with the same filter should not remove + the filter when the inner context exits (#14189).""" + + class NoCaptureFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + return False + + filter_instance = NoCaptureFilter() + with caplog.at_level(logging.INFO): + with caplog.filtering(filter_instance): + logger.info("outer before inner") + with caplog.filtering(filter_instance): + logger.info("inside inner") + logger.info("outer after inner") + logger.info("outside both") + + assert len(caplog.records) == 1 + assert caplog.records[0].message == "outside both" + + @pytest.mark.parametrize( "level_str,expected_disable_level", [