diff --git a/changelog/14189.bugfix.rst b/changelog/14189.bugfix.rst new file mode 100644 index 00000000000..cdb0680e35a --- /dev/null +++ b/changelog/14189.bugfix.rst @@ -0,0 +1 @@ +Nested usage of :meth:`caplog.filtering ` no longer removes filters early if they were already present. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 6f34c1b93fd..48960bcf4bc 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -585,11 +585,14 @@ def filtering(self, filter_: logging.Filter) -> Generator[None]: .. 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..630c910c04b 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -4,6 +4,7 @@ from collections.abc import Iterator import logging +from typing import cast from _pytest.logging import caplog_records_key from _pytest.pytester import Pytester @@ -69,8 +70,7 @@ def test_change_level_undo(pytester: Pytester) -> None: Tests the logging output themselves (affected both by logger and handler levels). """ - pytester.makepyfile( - """ + pytester.makepyfile(""" import logging def test1(caplog): @@ -83,8 +83,7 @@ def test2(caplog): # using + operator here so fnmatch_lines doesn't match the code in the traceback logging.info('log from ' + 'test2') assert 0 - """ - ) + """) result = pytester.runpytest() result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"]) result.stdout.no_fnmatch_line("*log from test2*") @@ -95,8 +94,7 @@ def test_change_disabled_level_undo(pytester: Pytester) -> None: Tests the logging output themselves (affected by disabled logging level). """ - pytester.makepyfile( - """ + pytester.makepyfile(""" import logging def test1(caplog): @@ -112,8 +110,7 @@ def test2(caplog): # isn't reset to ``CRITICAL`` after test1. logging.warning('log from ' + 'test2') assert 0 - """ - ) + """) result = pytester.runpytest() result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"]) result.stdout.no_fnmatch_line("*log from test2*") @@ -124,8 +121,7 @@ def test_change_level_undoes_handler_level(pytester: Pytester) -> None: Issue #7569. Tests the handler level specifically. """ - pytester.makepyfile( - """ + pytester.makepyfile(""" import logging def test1(caplog): @@ -141,8 +137,7 @@ def test3(caplog): assert caplog.handler.level == 0 caplog.set_level(43) assert caplog.handler.level == 43 - """ - ) + """) result = pytester.runpytest() result.assert_outcomes(passed=3) @@ -206,6 +201,37 @@ def filter(self, record: logging.LogRecord) -> bool: assert unfiltered_tuple == ("test_fixture", 20, "handler call") +def test_with_statement_nested_filtering(caplog: pytest.LogCaptureFixture) -> None: + def no_capture_filter(log_record: logging.LogRecord) -> bool: + return False + + with caplog.filtering(cast(logging.Filter, no_capture_filter)): + logger.warning("Will not be captured") + with caplog.filtering(cast(logging.Filter, no_capture_filter)): + logger.warning("Will also not be captured") + logger.warning("Should not be captured either") + + assert caplog.records == [] + + +def test_with_statement_filtering_already_present( + caplog: pytest.LogCaptureFixture, +) -> None: + def no_capture_filter(log_record: logging.LogRecord) -> bool: + return False + + caplog.handler.addFilter(no_capture_filter) + try: + with caplog.filtering(cast(logging.Filter, no_capture_filter)): + logger.warning("Should not be captured") + + # After context manager, filter should STILL be present because it was already there + logger.warning("Should still not be captured") + assert caplog.records == [] + finally: + caplog.handler.removeFilter(no_capture_filter) + + @pytest.mark.parametrize( "level_str,expected_disable_level", [ @@ -346,8 +372,7 @@ def test_clear_for_call_stage( def test_ini_controls_global_log_level(pytester: Pytester) -> None: - pytester.makepyfile( - """ + pytester.makepyfile(""" import pytest import logging def test_log_level_override(request, caplog): @@ -358,14 +383,11 @@ def test_log_level_override(request, caplog): logger.error("ERROR message will be shown") assert 'WARNING' not in caplog.text assert 'ERROR' in caplog.text - """ - ) - pytester.makeini( - """ + """) + pytester.makeini(""" [pytest] log_level=ERROR - """ - ) + """) result = pytester.runpytest() # make sure that we get a '0' exit code for the testsuite @@ -373,8 +395,7 @@ def test_log_level_override(request, caplog): def test_can_override_global_log_level(pytester: Pytester) -> None: - pytester.makepyfile( - """ + pytester.makepyfile(""" import pytest import logging def test_log_level_override(request, caplog): @@ -398,22 +419,18 @@ def test_log_level_override(request, caplog): logger.info("INFO message will be shown") assert "message won't be shown" not in caplog.text - """ - ) - pytester.makeini( - """ + """) + pytester.makeini(""" [pytest] log_level=WARNING - """ - ) + """) result = pytester.runpytest() assert result.ret == 0 def test_captures_despite_exception(pytester: Pytester) -> None: - pytester.makepyfile( - """ + pytester.makepyfile(""" import pytest import logging def test_log_level_override(request, caplog): @@ -426,14 +443,11 @@ def test_log_level_override(request, caplog): with caplog.at_level(logging.DEBUG, logger.name): logger.debug("DEBUG message " + "won't be shown") raise Exception() - """ - ) - pytester.makeini( - """ + """) + pytester.makeini(""" [pytest] log_level=WARNING - """ - ) + """) result = pytester.runpytest() result.stdout.fnmatch_lines(["*ERROR message will be shown*"]) @@ -449,8 +463,7 @@ def test_log_report_captures_according_to_config_option_upon_failure( (2) The `DEBUG` message does NOT appear in the `Captured log call` report. (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`. """ - pytester.makepyfile( - """ + pytester.makepyfile(""" import pytest import logging @@ -471,8 +484,7 @@ def test_that_fails(request, caplog): raise Exception('caplog failed to ' + 'capture DEBUG') assert False - """ - ) + """) result = pytester.runpytest("--log-level=INFO") result.stdout.no_fnmatch_line("*Exception: caplog failed to capture DEBUG*")