Repository: getlogbook/logbook

Issue:
Exception in LoggingCompatRecord for mapping keys
Example:
```
logger = logging.getLogger("test")
logger.setLevel("DEBUG")
logger.addHandler(RedirectLoggingHandler())

with logbook.StderrHandler():
    logger.debug("test map %(name)s", {"name": "mapname"})     
    # raise exception in LoggingCompatRecord:
   #Traceback (most recent call last):
    #   File "D:\bin\Python34\lib\site-packages\logbook\base.py", line 515, in message
    #    return self._format_message(self.msg, *self.args, **self.kwargs)
    #  File "D:\bin\Python34\lib\site-packages\logbook\compat.py", line 66, in _format_message
    #    return msg % tuple(args)
    # TypeError: format requires a mapping
```

The quote from "logging/\_\_init\_\_.py"
>
        #
        # The following statement allows passing of a dictionary as a sole
        # argument, so that you can do something like
        #  logging.debug("a %(a)d b %(b)s", {'a':1, 'b':2})
        # Suggested by Stefan Behnel.
        # Note that without the test for args[0], we get a problem because
        # during formatting, we test to see if the arg is present using
        # 'if self.args:'. If the event being logged is e.g. 'Value is %d'
        # and if the passed arg fails 'if self.args:' then no formatting
        # is done. For example, logger.warning('Value is %d', 0) would log
        # 'Value is %d' instead of 'Value is 0'.
        # For the use case of passing a dictionary, this should not be a
        # problem.
        # Issue #21172: a request was made to relax the isinstance check
        # to hasattr(args[0], '__getitem__'). However, the docs on string
        # formatting still seem to suggest a mapping object is required.
        # Thus, while not removing the isinstance check, it does now look
        # for collections.Mapping rather than, as before, dict.
        if (args and len(args) == 1 and isinstance(args[0], collections.Mapping)
            and args[0]):
            args = args[0]


Discussion:

Patch:
diff --git a/logbook/compat.py b/logbook/compat.py
index c3896db..b65ac00 100644
--- a/logbook/compat.py
+++ b/logbook/compat.py
@@ -9,12 +9,13 @@
     :copyright: (c) 2010 by Armin Ronacher, Georg Brandl.
     :license: BSD, see LICENSE for more details.
 """
-import sys
+import collections
 import logging
+import sys
 import warnings
-import logbook
 from datetime import date, datetime
 
+import logbook
 from logbook.helpers import u, string_types, iteritems
 
 _epoch_ord = date(1970, 1, 1).toordinal()
@@ -63,8 +64,12 @@ class redirected_logging(object):
 class LoggingCompatRecord(logbook.LogRecord):
 
     def _format_message(self, msg, *args, **kwargs):
-        assert not kwargs
-        return msg % tuple(args)
+        if kwargs:
+            assert not args
+            return msg % kwargs
+        else:
+            assert not kwargs
+            return msg % tuple(args)
 
 
 class RedirectLoggingHandler(logging.Handler):
@@ -124,10 +129,17 @@ class RedirectLoggingHandler(logging.Handler):
 
     def convert_record(self, old_record):
         """Converts an old logging record into a logbook log record."""
+        args = old_record.args
+        kwargs = None
+
+        # Logging allows passing a mapping object, in which case args will be a mapping.
+        if isinstance(args, collections.Mapping):
+            kwargs = args
+            args = None
         record = LoggingCompatRecord(old_record.name,
                                      self.convert_level(old_record.levelno),
-                                     old_record.msg, old_record.args,
-                                     None, old_record.exc_info,
+                                     old_record.msg, args,
+                                     kwargs, old_record.exc_info,
                                      self.find_extra(old_record),
                                      self.find_caller(old_record))
         record.time = self.convert_time(old_record.created)


Test patch:
diff --git a/tests/test_logging_compat.py b/tests/test_logging_compat.py
index 48dfebe..31fdd40 100644
--- a/tests/test_logging_compat.py
+++ b/tests/test_logging_compat.py
@@ -36,8 +36,11 @@ def test_basic_compat(request, set_root_logger_level):
                 logger.warn('This is from the old %s', 'system')
                 logger.error('This is from the old system')
                 logger.critical('This is from the old system')
+                logger.error('This is a %(what)s %(where)s', {'what': 'mapping', 'where': 'test'})
         assert ('WARNING: %s: This is from the old system' %
                 name) in captured.getvalue()
+        assert ('ERROR: %s: This is a mapping test' %
+                name) in captured.getvalue()
     if set_root_logger_level:
         assert handler.records[0].level == logbook.DEBUG
     else: