Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/14189.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Nested usage of :meth:`caplog.filtering <pytest.LogCaptureFixture.filtering>` no longer removes filters early if they were already present.
7 changes: 5 additions & 2 deletions src/_pytest/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 52 additions & 40 deletions testing/logging/test_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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*")
Expand All @@ -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):
Expand All @@ -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*")
Expand All @@ -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):
Expand All @@ -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)

Expand Down Expand Up @@ -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",
[
Expand Down Expand Up @@ -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):
Expand All @@ -358,23 +383,19 @@ 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
assert result.ret == 0


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):
Expand All @@ -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):
Expand All @@ -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*"])
Expand All @@ -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

Expand All @@ -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*")
Expand Down
Loading