Commits

Anonymous committed 34119c5

Refactoring and generalising the setting of log levels.

Renamed the plugin class.

Since there is now an implementation of a logging handler it handles both the log record instances
and the log text stream.

Factored out the funcarg into its own class rather than returning the
handler.

Factored out the log level context manager from the handler to it own class.

Store the handler on the item only since we can still get to it for the funcarg.

A reference to the handler was still on the function hence it would not be eligible for gc. Now all
refs are removed after the test.

Removed remembering and restoring the root logger level after each test as it seemed like a noop.

Extended the use of separate checks for log, stdout and stderr to all tests. In addition use the
py.test.raises checks for all tests.

Extended tests for setting log levels on any logger and also getting the log text from the funcarg.

Updated docs and made the README the authoritative one with the module docstring just a duplicate.

  • Participants
  • Parent commits 1ee921a

Comments (0)

Files changed (4)

 
 py.test plugin to capture log messages
 
+Installation
+------------
+
+The `pytest-capturelog`_ package may be installed with pip or easy_install::
+
+    pip install pytest-capturelog
+    easy_install pytest-capturelog
+
+.. _`pytest-capturelog`: http://pypi.python.org/pypi/pytest-capturelog/
+
 Usage
 -----
 
-Log messages are captured by default and for each failed test will be
-shown in the same manner as captured stdout and stderr.
+If the plugin is installed log messages are captured by default and for
+each failed test will be shown in the same manner as captured stdout and
+stderr.
 
 Running without options::
 
     text going to stderr
     ==================== 2 failed in 0.02 seconds =====================
 
-Inside tests it is possible to change the loglevel for the captured
+Inside tests it is possible to change the log level for the captured
 log messages.  This is supported by the ``capturelog`` funcarg::
 
     def test_foo(capturelog):
         capturelog.setLevel(logging.INFO)
         pass
 
-It is also possible to use the capturelog as a context manager to
-temporarily change the log level::
+By default the level is set on the handler used to capture the log
+messages, however as a convenience it is also possible to set the log
+level of any logger::
+
+    def test_foo(capturelog):
+        capturelog.setLevel(logging.CRITICAL, logger='root.baz')
+        pass
+
+It is also possible to use a context manager to temporarily change the
+log level::
 
     def test_bar(capturelog):
-        with capturelog(logging.INFO):
+        with capturelog.atLevel(logging.INFO):
             pass
 
-Lastly the LogRecord instances sent to the logger during the test run
-are also available on the function argument.  This is useful for when
-you want to assert on the contents of a message::
+Again, by default the level of the handler is affected but the level
+of any logger can be changed instead with::
+
+    def test_bar(capturelog):
+        with capturelog.atLevel(logging.CRITICAL, logger='root.baz'):
+            pass
+
+Lastly all the logs sent to the logger during the test run are made
+available on the funcarg in the form of both the LogRecord instances
+and the final log text.  This is useful for when you want to assert on
+the contents of a message::
 
     def test_baz(capturelog):
         func_under_test()
-        for record in capturelog.raw_records:
+        for record in capturelog.records():
             assert record.levelname != 'CRITICAL'
+        assert 'wally' not in capturelog.text()
 
 For all the available attributes of the log records see the
 ``logging.LogRecord`` class.
-
-Installation
-------------
-
-With pip::
-
-    pip install pytest-capturelog
-
-With easy install::
-
-    easy_install pytest-capturelog

pytest_capturelog.py

 Installation
 ------------
 
-You can install the `pytest-capturelog pypi`_ package
-with pip::
+The `pytest-capturelog`_ package may be installed with pip or easy_install::
 
     pip install pytest-capturelog
-
-or with easy install::
-
     easy_install pytest-capturelog
 
-.. _`pytest-capturelog pypi`: http://pypi.python.org/pypi/pytest-capturelog/
+.. _`pytest-capturelog`: http://pypi.python.org/pypi/pytest-capturelog/
 
 Usage
 -----
     text going to stderr
     ==================== 2 failed in 0.02 seconds =====================
 
-
-Inside tests it is possible to change the loglevel for the captured
+Inside tests it is possible to change the log level for the captured
 log messages.  This is supported by the ``capturelog`` funcarg::
 
     def test_foo(capturelog):
         capturelog.setLevel(logging.INFO)
         pass
 
-It is also possible to use the capturelog as a context manager to
-temporarily change the log level::
+By default the level is set on the handler used to capture the log
+messages, however as a convenience it is also possible to set the log
+level of any logger::
+
+    def test_foo(capturelog):
+        capturelog.setLevel(logging.CRITICAL, logger='root.baz')
+        pass
+
+It is also possible to use a context manager to temporarily change the
+log level::
 
     def test_bar(capturelog):
-        with capturelog(logging.INFO):
+        with capturelog.atLevel(logging.INFO):
             pass
 
-Lastly the LogRecord instances sent to the logger during the test run
-are also available on the function argument.  This is useful for when
-you want to assert on the contents of a message::
+Again, by default the level of the handler is affected but the level
+of any logger can be changed instead with::
+
+    def test_bar(capturelog):
+        with capturelog.atLevel(logging.CRITICAL, logger='root.baz'):
+            pass
+
+Lastly all the logs sent to the logger during the test run are made
+available on the funcarg in the form of both the LogRecord instances
+and the final log text.  This is useful for when you want to assert on
+the contents of a message::
 
     def test_baz(capturelog):
         func_under_test()
-        for record in capturelog.raw_records:
+        for record in capturelog.records():
             assert record.levelname != 'CRITICAL'
+        assert 'wally' not in capturelog.text()
 
 For all the available attributes of the log records see the
 ``logging.LogRecord`` class.
                     default=None,
                     help='log date format as used by the logging module')
 
+
 def pytest_configure(config):
     """Activate log capturing if appropriate."""
 
     if config.getvalue('capturelog'):
-        config.pluginmanager.register(Capturer(config), '_capturelog')
+        config.pluginmanager.register(CaptureLogPlugin(config), '_capturelog')
 
 
-class CapturelogHandler(logging.StreamHandler):
-    def __call__(self, level):
-        self.__tmp_level = level
-        return self
-
-    def __enter__(self):
-        self.__enter_level = self.level
-        self.level = self.__tmp_level
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        self.level = self.__enter_level
-
-    def emit(self, record):
-        """Keep the raw records in a buffer as well"""
-        try:
-            self.raw_records.append(record)
-        except AttributeError:
-            self.raw_records = [record]
-        logging.StreamHandler.emit(self, record)
-
-
-class Capturer(object):
+class CaptureLogPlugin(object):
     """Attaches to the logging module and captures log messages for each test."""
 
     def __init__(self, config):
-        """Creates a new capturer.
+        """Creates a new plugin to capture log messges.
 
         The formatter can be safely shared across all handlers so
         create a single one for the entire test session here.
     def pytest_runtest_setup(self, item):
         """Start capturing log messages for this test.
 
-        Creating a specific handler and stream for each test ensures
-        that we avoid multi threading issues.
+        Creating a specific handler for each test ensures that we
+        avoid multi threading issues.
 
         Attaching the handler and setting the level at the beginning
         of each test ensures that we are setup to capture log
         messages.
         """
 
-        # Create a handler and stream for this test.
-        item.capturelog_stream = py.io.TextIO()
-        item.capturelog_handler = CapturelogHandler(item.capturelog_stream)
+        # Create a handler for this test.
+        item.capturelog_handler = CaptureLogHandler()
         item.capturelog_handler.setFormatter(self.formatter)
-        item.function.capturelog_handler = item.capturelog_handler
-        item.capturelog_loglevel = logging.getLogger().level
 
         # Attach the handler to the root logger and ensure that the
         # root logger is set to log all levels.
         if call.when == 'call':
 
             # Detach the handler from the root logger to ensure no
-            # further access to the handler and stream.
+            # further access to the handler.
             root_logger = logging.getLogger()
             root_logger.removeHandler(item.capturelog_handler)
 
             if not report.passed:
                 longrepr = getattr(report, 'longrepr', None)
                 if hasattr(longrepr, 'addsection'):
-                    log = item.capturelog_stream.getvalue().strip()
+                    log = item.capturelog_handler.stream.getvalue().strip()
                     if log:
                         longrepr.addsection('Captured log', log)
 
-            # Release the handler and stream resources.
+            # Release the handler resources.
             item.capturelog_handler.close()
-            item.capturelog_stream.close()
             del item.capturelog_handler
-            del item.capturelog_stream
-
-            # Restore loglevel
-            root_logger.setLevel(item.capturelog_loglevel)
 
         return report
 
     def pytest_funcarg__capturelog(self, request):
-        """Returns the log handler configured for this logger
+        """Returns a funcarg to access and control log capturing."""
 
-        This can be used to modify the loglevel or format inside a
-        test.
+        return CaptureLogFuncArg(request._pyfuncitem.capturelog_handler)
+
+
+class CaptureLogHandler(logging.StreamHandler):
+    """A logging handler that stores log records and the log text."""
+
+    def __init__(self):
+        """Creates a new log handler."""
+
+        logging.StreamHandler.__init__(self)
+        self.stream = py.io.TextIO()
+        self.records  = []
+
+    def close(self):
+        """Close this log handler and its underlying stream."""
+
+        logging.StreamHandler.close(self)
+        self.stream.close()
+
+    def emit(self, record):
+        """Keep the log records in a list in addition to the log text."""
+
+        self.records.append(record)
+        logging.StreamHandler.emit(self, record)
+
+
+class CaptureLogFuncArg(object):
+    """Provides access and control of log capturing."""
+
+    def __init__(self, handler):
+        """Creates a new funcarg."""
+
+        self.handler = handler
+
+    def text(self):
+        """Returns the log text."""
+
+        return self.handler.stream.getvalue()
+
+    def records(self):
+        """Returns the list of log records."""
+
+        return self.handler.records
+
+    def setLevel(self, level, logger=None):
+        """Sets the level for capturing of logs.
+
+        By default, the level is set on the handler used to capture
+        logs. Specify a logger name to instead set the level of any
+        logger.
         """
-        return request.function.capturelog_handler
+
+        obj = logger and logging.getLogger(logger) or self.handler
+        obj.setLevel(level)
+
+    def atLevel(self, level, logger=None):
+        """Context manager that sets the level for capturing of logs.
+
+        By default, the level is set on the handler used to capture
+        logs. Specify a logger name to instead set the level of any
+        logger.
+        """
+
+        obj = logger and logging.getLogger(logger) or self.handler
+        return CaptureLogLevel(obj, level)
+
+
+class CaptureLogLevel(object):
+    """Context manager that sets the logging level of a handler or logger."""
+
+    def __init__(self, obj, level):
+        """Creates a new log level context manager."""
+
+        self.obj = obj
+        self.level = level
+
+    def __enter__(self):
+        """Adjust the log level."""
+
+        self.orig_level = self.obj.level
+        self.obj.setLevel(self.level)
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """Restore the log level."""
+
+        self.obj.setLevel(self.orig_level)
 import setuptools
 
 setuptools.setup(name='pytest-capturelog',
-                 version='0.5',
+                 version='0.6',
                  description='py.test plugin to capture log messages',
                  long_description=open('README').read().strip(),
                  author='Meme Dough',

test_pytest_capturelog.py

 def test_nothing_logged(testdir):
     testdir.makepyfile('''
         import sys
+        import logging
+
+        pytest_plugins = 'capturelog'
 
         def test_foo():
             sys.stdout.write('text going to stdout')
         ''')
     result = testdir.runpytest()
     assert result.ret == 1
-    assert result.stdout.fnmatch_lines([
-            '*- Captured stdout -*',
-            'text going to stdout',
-            '*- Captured stderr -*',
-            'text going to stderr'
-            ])
-    matching_lines = [line for line in result.outlines if '- Captured log -' in line]
-    assert not matching_lines
+    result.stdout.fnmatch_lines(['*- Captured stdout -*', 'text going to stdout'])
+    result.stdout.fnmatch_lines(['*- Captured stderr -*', 'text going to stderr'])
+    py.test.raises(AssertionError, result.stdout.fnmatch_lines, ['*- Captured log -*'])
 
 def test_messages_logged(testdir):
     testdir.makepyfile('''
         ''')
     result = testdir.runpytest()
     assert result.ret == 1
-    fnmatch = result.stdout.fnmatch_lines
-    assert fnmatch(['*- Captured log -*', '*text going to logger*'])
-    assert fnmatch(['*- Captured stdout -*', 'text going to stdout'])
-    assert fnmatch(['*- Captured stderr -*', 'text going to stderr'])
+    result.stdout.fnmatch_lines(['*- Captured log -*', '*text going to logger*'])
+    result.stdout.fnmatch_lines(['*- Captured stdout -*', 'text going to stdout'])
+    result.stdout.fnmatch_lines(['*- Captured stderr -*', 'text going to stderr'])
 
 def test_change_level(testdir):
     testdir.makepyfile('''
         def test_foo(capturelog):
             capturelog.setLevel(logging.INFO)
             log = logging.getLogger()
-            log.debug('DEBUG level')
-            log.info('INFO level')
+            log.debug('handler DEBUG level')
+            log.info('handler INFO level')
+
+            capturelog.setLevel(logging.CRITICAL, logger='root.baz')
+            log = logging.getLogger('root.baz')
+            log.warning('logger WARNING level')
+            log.critical('logger CRITICAL level')
+
             assert False
         ''')
     result = testdir.runpytest()
     assert result.ret == 1
-    fnmatch = result.stdout.fnmatch_lines
-    assert fnmatch(['*- Captured log -*', '*INFO level*'])
-    py.test.raises(AssertionError,
-                   fnmatch, ['*- Captured log -*', '*DEBUG level*'])
+    result.stdout.fnmatch_lines(['*- Captured log -*', '*handler INFO level*', '*logger CRITICAL level*'])
+    py.test.raises(AssertionError, result.stdout.fnmatch_lines, ['*- Captured log -*', '*handler DEBUG level*'])
+    py.test.raises(AssertionError, result.stdout.fnmatch_lines, ['*- Captured log -*', '*logger WARNING level*'])
 
 @py.test.mark.skipif('sys.version_info < (2,5)')
 def test_with_statement(testdir):
         pytest_plugins = 'capturelog'
 
         def test_foo(capturelog):
-            log = logging.getLogger()
-            with capturelog(logging.INFO):
-                log.debug('DEBUG level')
-                log.info('INFO level')
+            with capturelog.atLevel(logging.INFO):
+                log = logging.getLogger()
+                log.debug('handler DEBUG level')
+                log.info('handler INFO level')
+
+                with capturelog.atLevel(logging.CRITICAL, logger='root.baz'):
+                    log = logging.getLogger('root.baz')
+                    log.warning('logger WARNING level')
+                    log.critical('logger CRITICAL level')
+
             assert False
         ''')
     result = testdir.runpytest()
     assert result.ret == 1
-    fnmatch = result.stdout.fnmatch_lines
-    assert fnmatch(['*- Captured log -*', '*INFO level*'])
-    py.test.raises(AssertionError,
-                   fnmatch, ['*- Captured log -*', '*DEBUG level*'])
+    result.stdout.fnmatch_lines(['*- Captured log -*', '*handler INFO level*', '*logger CRITICAL level*'])
+    py.test.raises(AssertionError, result.stdout.fnmatch_lines, ['*- Captured log -*', '*handler DEBUG level*'])
+    py.test.raises(AssertionError, result.stdout.fnmatch_lines, ['*- Captured log -*', '*logger WARNING level*'])
 
-def test_raw_record(testdir):
+def test_log_access(testdir):
     testdir.makepyfile('''
         import sys
         import logging
 
         def test_foo(capturelog):
             logging.getLogger().info('boo %s', 'arg')
-            assert capturelog.raw_records[0].levelname == 'INFO'
-            assert capturelog.raw_records[0].msg == 'boo %s'
+            assert capturelog.records()[0].levelname == 'INFO'
+            assert capturelog.records()[0].msg == 'boo %s'
+            assert 'boo arg' in capturelog.text()
         ''')
     result = testdir.runpytest()
     assert result.ret == 0