Skip to content

Handling of loggers with propagate=False #3697

Description

@bluefish6

Hi,

There is a 2-year old issue on the old repo and inspired by #3003 I thought it might be easier if I at least add some reference here:

eisensheng/pytest-catchlog#44

Activity

  1. bluefish6 commented on Jul 19, 2018

    @bluefish6
    Author

    I suggest to remove the auto-added label os:mac, as this is an issue on Ubuntu 16.04 as well.

  2. added
    type: enhancementnew feature or API change, should be merged into features branch
    plugin: loggingrelated to the logging builtin plugin
    on Oct 20, 2018
  3. dazza-codes commented on May 1, 2019

    @dazza-codes

    When using the caplog.records or caplog.record_tuples there is nothing captured for a logger that does not propagate up to the root logger. When testing the log output, is there any work-around for this or does a solution require mocking of logging handlers?

  4. nicoddemus commented on May 1, 2019

    @nicoddemus
    Member

    Not sure, we only capture what we get from the root logger... @Thisch might give some input here.

  5. dazza-codes commented on May 1, 2019

    @dazza-codes

    So the simplest work around in a pytest test is to enable the propagation during the test, only for any test that must check the presence or content of a log message (other tests might confirm that propagation is disabled by default). If I had time, a PR on the docs might help, but here is what I have time for now, e.g.

    def test_app_logger_content(caplog):
        logger = logging.getLogger('app')  # 'app' is already configured without propagation
        logger.propagate = True  # log capture only works for propagation to the root logger
        logger.info('foo')
        logger.error('err')
        assert caplog.record_tuples == [
            ('app', logging.INFO, 'foo'),
            ('app', logging.ERROR, 'err')
        ]
    
    def test_app_logger_does_not_propagate(caplog):
        logger = logging.getLogger('app')
        assert not logger.propagate
        logger.info('foo')
        logger.error('err')
        assert not caplog.records
  6. nicoddemus commented on May 1, 2019

    @nicoddemus
    Member

    If you have only a few loggers which don't propagate, probably you can use a session-scoped autouse fixture to enable propagation for those loggers.

  7. lig commented on May 29, 2019

    @lig

    I think this is an issue. Enabling propagating for logger results in a possible false positive result of a test.

    Say, I would like to ensure a message goes to exact logger I want it to go. There is no way at the moment to test that.

    It would be helpful to be able to tell to caplog what logger I'm interested exactly. So, it will capture that logger and everything that goes there.

  8. twmr commented on May 29, 2019

    @twmr
    Contributor

    @lig If we add support for specifying a logger in _pytest.logging.catching_logs you can use the following code to tell caplog the logger you're interested in

    import logging
    from _pytest.logging import catching_logs
    
    
    def test_app_logger_content_nocatchlog(caplog):
        logger = logging.getLogger('app')
        logger.propagate = False
        logger.info('foo')
        logger.error('err')
        assert ('app', logging.INFO, 'foo') not in caplog.record_tuples
        assert ('app', logging.ERROR, 'err') not in caplog.record_tuples
    
    
    def test_app_logger_content_catchlog(caplog):
        logger = logging.getLogger('app')
        logger.propagate = False
        caplog.set_level(logging.INFO)
    
        with catching_logs(caplog.handler, logger=logger):
            logger.info('foo')
            logger.error('err')
            assert caplog.record_tuples == [
                ('app', logging.INFO, 'foo'),
                ('app', logging.ERROR, 'err')
            ]
  9. lig commented on May 29, 2019

    @lig

    @Thisch looks really useful

    one thing. I'd assume this would work either

    def test_app_logger_content_catchlog(caplog):
        logger = logging.getLogger('app')
        logger.propagate = False
        caplog.set_level(logging.INFO)
    
        with catching_logs(caplog.handler, logger=logger):
            logger.info('foo')
            logger.error('err')
        assert caplog.record_tuples == [
            ('app', logging.INFO, 'foo'),
            ('app', logging.ERROR, 'err'),
        ]

    notice that caplog keeps records after catching_logs context manager has exited

  10. chrisjsewell commented on Nov 11, 2020

    @chrisjsewell

    Yeh it would be ideal if there was a proper replacement in pytest for unittest.TestCase.assertLogs, i.e. with pytest.logs(...
    Can you not just copy what that does, i.e. temporarily overriding the propagate and handler?

    class _AssertLogsContext(_BaseTestCaseContext):
        """A context manager used to implement TestCase.assertLogs()."""
    
        LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
    
        def __init__(self, test_case, logger_name, level):
            _BaseTestCaseContext.__init__(self, test_case)
            self.logger_name = logger_name
            if level:
                self.level = logging._nameToLevel.get(level, level)
            else:
                self.level = logging.INFO
            self.msg = None
    
        def __enter__(self):
            if isinstance(self.logger_name, logging.Logger):
                logger = self.logger = self.logger_name
            else:
                logger = self.logger = logging.getLogger(self.logger_name)
            formatter = logging.Formatter(self.LOGGING_FORMAT)
            handler = _CapturingHandler()
            handler.setFormatter(formatter)
            self.watcher = handler.watcher
            self.old_handlers = logger.handlers[:]
            self.old_level = logger.level
            self.old_propagate = logger.propagate
            logger.handlers = [handler]
            logger.setLevel(self.level)
            logger.propagate = False
            return handler.watcher
    
        def __exit__(self, exc_type, exc_value, tb):
            self.logger.handlers = self.old_handlers
            self.logger.propagate = self.old_propagate
            self.logger.setLevel(self.old_level)
            if exc_type is not None:
                # let unexpected exceptions pass through
                return False
            if len(self.watcher.records) == 0:
                self._raiseFailure(
                    "no logs of level {} or higher triggered on {}"
                    .format(logging.getLevelName(self.level), self.logger.name))
  11. deleted a comment from pytestbot on Dec 17, 2020
  12. chr1st1ank commented on Mar 3, 2021

    @chr1st1ank

    This is unfortunately still relevant and the workaround mentioned above by @lig and @Thisch, using the internal catching_logs() function is no longer available. The "logger" parameter was removed in 3862b0b.
    Are there any other ways to achieve the effect? I'm a bit puzzled how to fix our unit tests for a project where there is a logger which doesn't propagate to root.

  13. 19 remaining items

  14. added a commit that references this issue on Jun 23, 2026
  15. added a commit that references this issue on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    plugin: loggingrelated to the logging builtin plugintype: enhancementnew feature or API change, should be merged into features branch

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions