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

Docs and tests are underway.

Automerge-Triggered-By: @vsajip
This commit is contained in:
Bar Harel 2020-06-18 17:18:58 +03:00 committed by GitHub
parent ddbeb2f3e0
commit 8f192d12af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 57 additions and 9 deletions

View File

@ -529,7 +529,8 @@ The useful mapping keys in a :class:`LogRecord` are given in the section on
:ref:`logrecord-attributes`. :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 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 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 :ref:`formatting-styles` for more information on using {- and $-formatting
for log messages. 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 .. versionchanged:: 3.2
The *style* parameter was added. 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``. will raise a ``ValueError``.
For example: ``logging.Formatter('%(asctime)s - %(message)s', style='{')``. For example: ``logging.Formatter('%(asctime)s - %(message)s', style='{')``.
.. versionchanged:: 3.10
The *defaults* parameter was added.
.. method:: format(record) .. method:: format(record)
The record's attribute dictionary is used as the operand to a string The record's attribute dictionary is used as the operand to a string

View File

@ -411,8 +411,9 @@ class PercentStyle(object):
asctime_search = '%(asctime)' asctime_search = '%(asctime)'
validation_pattern = re.compile(r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]', re.I) 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._fmt = fmt or self.default_format
self._defaults = defaults
def usesTime(self): def usesTime(self):
return self._fmt.find(self.asctime_search) >= 0 return self._fmt.find(self.asctime_search) >= 0
@ -423,7 +424,11 @@ class PercentStyle(object):
raise ValueError("Invalid format '%s' for '%s' style" % (self._fmt, self.default_format[0])) raise ValueError("Invalid format '%s' for '%s' style" % (self._fmt, self.default_format[0]))
def _format(self, record): 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): def format(self, record):
try: try:
@ -441,7 +446,11 @@ class StrFormatStyle(PercentStyle):
field_spec = re.compile(r'^(\d+|\w+)(\.\w+|\[[^]]+\])*$') field_spec = re.compile(r'^(\d+|\w+)(\.\w+|\[[^]]+\])*$')
def _format(self, record): 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): def validate(self):
"""Validate the input format, ensure it is the correct string formatting style""" """Validate the input format, ensure it is the correct string formatting style"""
@ -467,8 +476,8 @@ class StringTemplateStyle(PercentStyle):
asctime_format = '${asctime}' asctime_format = '${asctime}'
asctime_search = '${asctime}' asctime_search = '${asctime}'
def __init__(self, fmt): def __init__(self, *args, **kwargs):
self._fmt = fmt or self.default_format super().__init__(*args, **kwargs)
self._tpl = Template(self._fmt) self._tpl = Template(self._fmt)
def usesTime(self): def usesTime(self):
@ -490,7 +499,11 @@ class StringTemplateStyle(PercentStyle):
raise ValueError('invalid format: no fields') raise ValueError('invalid format: no fields')
def _format(self, record): 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" BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
@ -546,7 +559,8 @@ class Formatter(object):
converter = time.localtime 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. Initialize the formatter with specified format strings.
@ -565,7 +579,7 @@ class Formatter(object):
if style not in _STYLES: if style not in _STYLES:
raise ValueError('Style must be one of: %s' % ','.join( raise ValueError('Style must be one of: %s' % ','.join(
_STYLES.keys())) _STYLES.keys()))
self._style = _STYLES[style][0](fmt) self._style = _STYLES[style][0](fmt, defaults=defaults)
if validate: if validate:
self._style.validate() self._style.validate()

View File

@ -3710,6 +3710,9 @@ class FormatterTest(unittest.TestCase):
'args': (2, 'placeholders'), 'args': (2, 'placeholders'),
} }
self.variants = { self.variants = {
'custom': {
'custom': 1234
}
} }
def get_record(self, name=None): def get_record(self, name=None):
@ -3926,6 +3929,26 @@ class FormatterTest(unittest.TestCase):
) )
self.assertRaises(ValueError, logging.Formatter, '${asctime', style='$') 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): def test_invalid_style(self):
self.assertRaises(ValueError, logging.Formatter, None, None, 'x') self.assertRaises(ValueError, logging.Formatter, None, None, 'x')

View File

@ -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.