Skip to content

bpo-34844: logging.Formatter enhancement - Ensure styles and fmt matches in logging.Formatter #9703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Oct 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/library/logging.config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ otherwise, the context is used to determine what to instantiate.
(with defaults of ``None``) and these are used to construct a
:class:`~logging.Formatter` instance.

.. versionchanged:: 3.8
a ``validate`` key (with default of ``True``) can be added into
the ``formatters`` section of the configuring dict, this is to
validate the format.

* *filters* - the corresponding value will be a dict in which each key
is a filter id and each value is a dict describing how to configure
the corresponding Filter instance.
Expand Down
4 changes: 4 additions & 0 deletions Doc/library/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,10 @@ The useful mapping keys in a :class:`LogRecord` are given in the section on
.. versionchanged:: 3.2
The *style* parameter was added.

.. versionchanged:: 3.8
The *validate* parameter was added. Incorrect or mismatched style and fmt
will raise a ``ValueError``.
For example: ``logging.Formatter('%(asctime)s - %(message)s', style='{')``.

.. method:: format(record)

Expand Down
68 changes: 63 additions & 5 deletions Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
To use, simply 'import logging' and log away!
"""

import sys, os, time, io, traceback, warnings, weakref, collections.abc
import sys, os, time, io, re, traceback, warnings, weakref, collections.abc

from string import Template
from string import Formatter as StrFormatter


__all__ = ['BASIC_FORMAT', 'BufferingFormatter', 'CRITICAL', 'DEBUG', 'ERROR',
'FATAL', 'FileHandler', 'Filter', 'Formatter', 'Handler', 'INFO',
Expand Down Expand Up @@ -413,33 +415,71 @@ def makeLogRecord(dict):
rv.__dict__.update(dict)
return rv


#---------------------------------------------------------------------------
# Formatter classes and functions
#---------------------------------------------------------------------------
_str_formatter = StrFormatter()
del StrFormatter


class PercentStyle(object):

default_format = '%(message)s'
asctime_format = '%(asctime)s'
asctime_search = '%(asctime)'
validation_pattern = re.compile(r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]', re.I)

def __init__(self, fmt):
self._fmt = fmt or self.default_format

def usesTime(self):
return self._fmt.find(self.asctime_search) >= 0

def format(self, record):
def validate(self):
"""Validate the input format, ensure it matches the correct style"""
if not self.validation_pattern.search(self._fmt):
raise ValueError("Invalid format '%s' for '%s' style" % (self._fmt, self.default_format[0]))

def _format(self, record):
return self._fmt % record.__dict__

def format(self, record):
try:
return self._format(record)
except KeyError as e:
raise ValueError('Formatting field not found in record: %s' % e)


class StrFormatStyle(PercentStyle):
default_format = '{message}'
asctime_format = '{asctime}'
asctime_search = '{asctime'

def format(self, record):
fmt_spec = re.compile(r'^(.?[<>=^])?[+ -]?#?0?(\d+|{\w+})?[,_]?(\.(\d+|{\w+}))?[bcdefgnosx%]?$', re.I)
field_spec = re.compile(r'^(\d+|\w+)(\.\w+|\[[^]]+\])*$')

def _format(self, record):
return self._fmt.format(**record.__dict__)

def validate(self):
"""Validate the input format, ensure it is the correct string formatting style"""
fields = set()
try:
for _, fieldname, spec, conversion in _str_formatter.parse(self._fmt):
if fieldname:
if not self.field_spec.match(fieldname):
raise ValueError('invalid field name/expression: %r' % fieldname)
fields.add(fieldname)
if conversion and conversion not in 'rsa':
raise ValueError('invalid conversion: %r' % conversion)
if spec and not self.fmt_spec.match(spec):
raise ValueError('bad specifier: %r' % spec)
except ValueError as e:
raise ValueError('invalid format: %s' % e)
if not fields:
raise ValueError('invalid format: no fields')


class StringTemplateStyle(PercentStyle):
default_format = '${message}'
Expand All @@ -454,9 +494,24 @@ def usesTime(self):
fmt = self._fmt
return fmt.find('$asctime') >= 0 or fmt.find(self.asctime_format) >= 0

def format(self, record):
def validate(self):
pattern = Template.pattern
fields = set()
for m in pattern.finditer(self._fmt):
d = m.groupdict()
if d['named']:
fields.add(d['named'])
elif d['braced']:
fields.add(d['braced'])
elif m.group(0) == '$':
raise ValueError('invalid format: bare \'$\' not allowed')
if not fields:
raise ValueError('invalid format: no fields')

def _format(self, record):
return self._tpl.substitute(**record.__dict__)


BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"

_STYLES = {
Expand Down Expand Up @@ -510,7 +565,7 @@ class Formatter(object):

converter = time.localtime

def __init__(self, fmt=None, datefmt=None, style='%'):
def __init__(self, fmt=None, datefmt=None, style='%', validate=True):
"""
Initialize the formatter with specified format strings.

Expand All @@ -530,6 +585,9 @@ def __init__(self, fmt=None, datefmt=None, style='%'):
raise ValueError('Style must be one of: %s' % ','.join(
_STYLES.keys()))
self._style = _STYLES[style][0](fmt)
if validate:
self._style.validate()

self._fmt = self._style._fmt
self.datefmt = datefmt

Expand Down
10 changes: 9 additions & 1 deletion Lib/logging/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,11 +666,19 @@ def configure_formatter(self, config):
dfmt = config.get('datefmt', None)
style = config.get('style', '%')
cname = config.get('class', None)

if not cname:
c = logging.Formatter
else:
c = _resolve(cname)
result = c(fmt, dfmt, style)

# A TypeError would be raised if "validate" key is passed in with a formatter callable
# that does not accept "validate" as a parameter
if 'validate' in config: # if user hasn't mentioned it, the default will be fine
result = c(fmt, dfmt, style, config['validate'])
else:
result = c(fmt, dfmt, style)

return result

def configure_filter(self, config):
Expand Down
Loading