diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 74905ff4c..6df06f7b2 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter from _pytest.compat import final -from _pytest.compat import importlib_metadata +from _pytest.compat import importlib_metadata # type: ignore[attr-defined] from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import absolutepath diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index eab8b7142..707ececb1 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -463,7 +463,7 @@ class LogCaptureFixture: ) -> int: """Enable the desired logging level if the level was disabled. - Only enables logging levels equal too and higher than the requested ``level``. + Only enables logging levels greater than or equal to the requested ``level``. Does nothing if the desired ``level`` wasn't disabled. @@ -478,16 +478,17 @@ class LogCaptureFixture: original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined] if isinstance(level, str): + # Try to translate the level string to an int for `logging.disable()` level = logging.getLevelName(level) if not isinstance(level, int): + # The level provided was not valid, so just un-disable all logging. logging.disable(logging.NOTSET) - return original_disable_level - if logger_obj.isEnabledFor(level): - return original_disable_level - - disable_level = max(level - 10, logging.NOTSET) - logging.disable(disable_level) + elif not logger_obj.isEnabledFor(level): + # Each level is `10` away from other levels. + # https://docs.python.org/3/library/logging.html#logging-levels + disable_level = max(level - 10, logging.NOTSET) + logging.disable(disable_level) return original_disable_level diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 49ff70847..7ead6b558 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -9,6 +9,19 @@ logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") +@pytest.fixture +def cleanup_disabled_logging(): + """Simple fixture that ensures that a test doesn't disable logging. + + This is necessary because ``logging.disable()`` is global, so a test disabling logging + and not cleaning up after will break every test that runs after it. + + This behavior was moved to a fixture so that logging will be un-disabled even if the test fails an assertion. + """ + yield + logging.disable(logging.NOTSET) + + def test_fixture_help(pytester: Pytester) -> None: result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines(["*caplog*"]) @@ -29,7 +42,7 @@ def test_change_level(caplog): assert "CRITICAL" in caplog.text -def test_change_level_logging_disabled(caplog): +def test_change_level_logging_disabled(caplog, cleanup_disabled_logging): logging.disable(logging.CRITICAL) assert logging.root.manager.disable == logging.CRITICAL caplog.set_level(logging.WARNING) @@ -45,9 +58,6 @@ def test_change_level_logging_disabled(caplog): assert "SUB_WARNING" not in caplog.text assert "SUB_CRITICAL" in caplog.text - # logging.disable needs to be reset because it's global and causes future tests will break. - logging.disable(logging.NOTSET) - def test_change_level_undo(pytester: Pytester) -> None: """Ensure that 'set_level' is undone after the end of the test. @@ -75,7 +85,9 @@ def test_change_level_undo(pytester: Pytester) -> None: result.stdout.no_fnmatch_line("*log from test2*") -def test_change_disabled_level_undo(pytester: Pytester) -> None: +def test_change_disabled_level_undo( + pytester: Pytester, cleanup_disabled_logging +) -> None: """Ensure that 'force_enable_logging' in 'set_level' is undone after the end of the test. Tests the logging output themselves (affected by disabled logging level). @@ -103,9 +115,6 @@ def test_change_disabled_level_undo(pytester: Pytester) -> None: result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"]) result.stdout.no_fnmatch_line("*log from test2*") - # logging.disable needs to be reset because it's global and causes future tests will break. - logging.disable(logging.NOTSET) - def test_change_level_undos_handler_level(pytester: Pytester) -> None: """Ensure that 'set_level' is undone after the end of the test (handler). @@ -150,7 +159,7 @@ def test_with_statement(caplog): assert "CRITICAL" in caplog.text -def test_with_statement_logging_disabled(caplog): +def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging): logging.disable(logging.CRITICAL) assert logging.root.manager.disable == logging.CRITICAL with caplog.at_level(logging.WARNING): @@ -175,9 +184,6 @@ def test_with_statement_logging_disabled(caplog): assert "SUB_CRITICAL" in caplog.text assert logging.root.manager.disable == logging.CRITICAL - # logging.disable needs to be reset because it's global and causes future tests will break. - logging.disable(logging.NOTSET) - def test_log_access(caplog): caplog.set_level(logging.INFO)