diff --git a/changelog/7431.feature.rst b/changelog/7431.feature.rst new file mode 100644 index 00000000000..ca13ec38aec --- /dev/null +++ b/changelog/7431.feature.rst @@ -0,0 +1 @@ +``--suppress-logger`` CLI option added to suppress output from individual loggers diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 3b046c95441..8e3720e9a53 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -280,6 +280,13 @@ def add_option_ini(option, dest, default=None, type=None, **kwargs): default=None, help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.", ) + group.addoption( + "--suppress-logger", + action="append", + default=[], + dest="suppress_logger", + help="Suppress loggers by name", + ) _HandlerType = TypeVar("_HandlerType", bound=logging.Handler) @@ -575,6 +582,17 @@ def __init__(self, config: Config) -> None: get_option_ini(config, "log_auto_indent"), ) self.log_cli_handler.setFormatter(log_cli_formatter) + if config.option.suppress_logger: + self._suppress_loggers() + + def _suppress_loggers(self) -> None: + logger_names = set(self._config.option.suppress_logger) + for name in logger_names: + # disable propagation for the given logger preventing of event passing to ancestor handlers + # adding a NullHandler to prevent logging.LastResort handler from logging warnings to stderr + logger = logging.getLogger(name) + logger.addHandler(logging.NullHandler()) + logger.propagate = False def _create_formatter(self, log_format, log_date_format, auto_indent): # Color option doesn't exist if terminal plugin is disabled. diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 7545d016d52..9448bb8e400 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1152,3 +1152,60 @@ def test_log_file_cli_subdirectories_are_successfully_created(testdir): result = testdir.runpytest("--log-file=foo/bar/logf.log") assert "logf.log" in os.listdir(expected) assert result.ret == ExitCode.OK + + +def test_suppress_loggers(testdir): + testdir.makepyfile( + """ + import logging + import os + suppressed_log = logging.getLogger('suppressed') + other_log = logging.getLogger('other') + normal_log = logging.getLogger('normal') + def test_logger_propagation(caplog): + with caplog.at_level(logging.DEBUG): + suppressed_log.warning("no log; no stderr") + other_log.info("no log") + normal_log.debug("Unsuppressed!") + print(os.linesep) + assert caplog.record_tuples == [('normal', 10, 'Unsuppressed!')] + """ + ) + result = testdir.runpytest( + "--suppress-logger=suppressed", "--suppress-logger=other", "-s" + ) + assert result.ret == ExitCode.OK + assert not result.stderr.lines + + +def test_suppress_loggers_help(testdir): + result = testdir.runpytest("-h") + result.stdout.fnmatch_lines( + [" --suppress-logger=SUPPRESS_LOGGER", "*Suppress loggers by name*"] + ) + + +def test_log_suppressing_works_with_log_cli(testdir): + testdir.makepyfile( + """ + import logging + log = logging.getLogger('mylog') + suppressed_log = logging.getLogger('suppressed') + def test_log_cli_works(caplog): + log.info("hello world") + suppressed_log.warning("Hello World") + """ + ) + result = testdir.runpytest( + "--log-cli-level=DEBUG", + "--suppress-logger=suppressed", + "--suppress-logger=mylog", + ) + assert result.ret == ExitCode.OK + result.stdout.no_fnmatch_line( + "INFO mylog:test_log_suppressing_works_with_log_cli.py:5 hello world" + ) + result.stdout.no_fnmatch_line( + "WARNING suppressed:test_log_suppressing_works_with_log_cli.py:6 Hello World" + ) + assert not result.stderr.lines