[Python-checkins] bpo-40884: Added defaults parameter for logging.Formatter (GH-20668)

Bar Harel webhook-mailer at python.org
Thu Jun 18 10:19:03 EDT 2020


https://github.com/python/cpython/commit/8f192d12af82c4dc40730bf59814f6a68f68f950
commit: 8f192d12af82c4dc40730bf59814f6a68f68f950
branch: master
author: Bar Harel <bzvi7919 at gmail.com>
committer: GitHub <noreply at github.com>
date: 2020-06-18T07:18:58-07:00
summary:

bpo-40884: Added defaults parameter for logging.Formatter (GH-20668)



Docs and tests are underway.

Automerge-Triggered-By: @vsajip

files:
A Misc/NEWS.d/next/Library/2020-06-06-02-42-26.bpo-40884.n7fOwS.rst
M Doc/library/logging.rst
M Lib/logging/__init__.py
M Lib/test/test_logging.py

diff --git a/Doc/library/logging.rst b/Doc/library/logging.rst
index 7267f812cc192..3ff67f76cc3c5 100644
--- a/Doc/library/logging.rst
+++ b/Doc/library/logging.rst
@@ -529,7 +529,8 @@ The useful mapping keys in a :class:`LogRecord` are given in the section on
 :ref:`logrecord-attributes`.
 
 
-.. class:: Formatter(fmt=None, datefmt=None, style='%', validate=True)
+.. class:: Formatter(fmt=None, datefmt=None, style='%', validate=True, *,
+   defaults=None)
 
    Returns a new instance of the :class:`Formatter` class.  The instance is
    initialized with a format string for the message as a whole, as well as a
@@ -545,6 +546,10 @@ The useful mapping keys in a :class:`LogRecord` are given in the section on
    :ref:`formatting-styles` for more information on using {- and $-formatting
    for log messages.
 
+   The *defaults* parameter can be a dictionary with default values to use in
+   custom fields. For example:
+   ``logging.Formatter('%(ip)s %(message)s', defaults={"ip": None})``
+
    .. versionchanged:: 3.2
       The *style* parameter was added.
 
@@ -553,6 +558,9 @@ The useful mapping keys in a :class:`LogRecord` are given in the section on
       will raise a ``ValueError``.
       For example: ``logging.Formatter('%(asctime)s - %(message)s', style='{')``.
 
+   .. versionchanged:: 3.10
+      The *defaults* parameter was added.
+
    .. method:: format(record)
 
       The record's attribute dictionary is used as the operand to a string
diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py
index 1c446fd421650..94361ca75f4f3 100644
--- a/Lib/logging/__init__.py
+++ b/Lib/logging/__init__.py
@@ -411,8 +411,9 @@ class PercentStyle(object):
     asctime_search = '%(asctime)'
     validation_pattern = re.compile(r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]', re.I)
 
-    def __init__(self, fmt):
+    def __init__(self, fmt, *, defaults=None):
         self._fmt = fmt or self.default_format
+        self._defaults = defaults
 
     def usesTime(self):
         return self._fmt.find(self.asctime_search) >= 0
@@ -423,7 +424,11 @@ def validate(self):
             raise ValueError("Invalid format '%s' for '%s' style" % (self._fmt, self.default_format[0]))
 
     def _format(self, record):
-        return self._fmt % record.__dict__
+        if defaults := self._defaults:
+            values = defaults | record.__dict__
+        else:
+            values = record.__dict__
+        return self._fmt % values
 
     def format(self, record):
         try:
@@ -441,7 +446,11 @@ class StrFormatStyle(PercentStyle):
     field_spec = re.compile(r'^(\d+|\w+)(\.\w+|\[[^]]+\])*$')
 
     def _format(self, record):
-        return self._fmt.format(**record.__dict__)
+        if defaults := self._defaults:
+            values = defaults | record.__dict__
+        else:
+            values = record.__dict__
+        return self._fmt.format(**values)
 
     def validate(self):
         """Validate the input format, ensure it is the correct string formatting style"""
@@ -467,8 +476,8 @@ class StringTemplateStyle(PercentStyle):
     asctime_format = '${asctime}'
     asctime_search = '${asctime}'
 
-    def __init__(self, fmt):
-        self._fmt = fmt or self.default_format
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
         self._tpl = Template(self._fmt)
 
     def usesTime(self):
@@ -490,7 +499,11 @@ def validate(self):
             raise ValueError('invalid format: no fields')
 
     def _format(self, record):
-        return self._tpl.substitute(**record.__dict__)
+        if defaults := self._defaults:
+            values = defaults | record.__dict__
+        else:
+            values = record.__dict__
+        return self._tpl.substitute(**values)
 
 
 BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
@@ -546,7 +559,8 @@ class Formatter(object):
 
     converter = time.localtime
 
-    def __init__(self, fmt=None, datefmt=None, style='%', validate=True):
+    def __init__(self, fmt=None, datefmt=None, style='%', validate=True, *,
+                 defaults=None):
         """
         Initialize the formatter with specified format strings.
 
@@ -565,7 +579,7 @@ def __init__(self, fmt=None, datefmt=None, style='%', validate=True):
         if style not in _STYLES:
             raise ValueError('Style must be one of: %s' % ','.join(
                              _STYLES.keys()))
-        self._style = _STYLES[style][0](fmt)
+        self._style = _STYLES[style][0](fmt, defaults=defaults)
         if validate:
             self._style.validate()
 
diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py
index e719d264a9191..2ae00b6e3b4e9 100644
--- a/Lib/test/test_logging.py
+++ b/Lib/test/test_logging.py
@@ -3710,6 +3710,9 @@ def setUp(self):
             'args': (2, 'placeholders'),
         }
         self.variants = {
+            'custom': {
+                'custom': 1234
+            }
         }
 
     def get_record(self, name=None):
@@ -3926,6 +3929,26 @@ def test_format_validate(self):
         )
         self.assertRaises(ValueError, logging.Formatter, '${asctime', style='$')
 
+    def test_defaults_parameter(self):
+        fmts = ['%(custom)s %(message)s', '{custom} {message}', '$custom $message']
+        styles = ['%', '{', '$']
+        for fmt, style in zip(fmts, styles):
+            f = logging.Formatter(fmt, style=style, defaults={'custom': 'Default'})
+            r = self.get_record()
+            self.assertEqual(f.format(r), 'Default Message with 2 placeholders')
+            r = self.get_record("custom")
+            self.assertEqual(f.format(r), '1234 Message with 2 placeholders')
+
+            # Without default
+            f = logging.Formatter(fmt, style=style)
+            r = self.get_record()
+            self.assertRaises(ValueError, f.format, r)
+
+            # Non-existing default is ignored
+            f = logging.Formatter(fmt, style=style, defaults={'Non-existing': 'Default'})
+            r = self.get_record("custom")
+            self.assertEqual(f.format(r), '1234 Message with 2 placeholders')
+
     def test_invalid_style(self):
         self.assertRaises(ValueError, logging.Formatter, None, None, 'x')
 
diff --git a/Misc/NEWS.d/next/Library/2020-06-06-02-42-26.bpo-40884.n7fOwS.rst b/Misc/NEWS.d/next/Library/2020-06-06-02-42-26.bpo-40884.n7fOwS.rst
new file mode 100644
index 0000000000000..64990e8023fba
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-06-06-02-42-26.bpo-40884.n7fOwS.rst
@@ -0,0 +1,3 @@
+Added a `defaults` parameter to :class:`logging.Formatter`, to allow
+specifying default values for custom fields. Patch by Asaf Alon and Bar
+Harel.



More information about the Python-checkins mailing list