Kaydet (Commit) 18fb1fb9 authored tarafından BNMetrics's avatar BNMetrics Kaydeden (comit) Vinay Sajip

bpo-34844: logging.Formatter enhancement - Ensure style and format string…

bpo-34844: logging.Formatter enhancement - Ensure style and format string matches in logging.Formatter  (GH-9703)
üst e890421e
...@@ -226,6 +226,11 @@ otherwise, the context is used to determine what to instantiate. ...@@ -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 (with defaults of ``None``) and these are used to construct a
:class:`~logging.Formatter` instance. :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 * *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 is a filter id and each value is a dict describing how to configure
the corresponding Filter instance. the corresponding Filter instance.
......
...@@ -544,6 +544,10 @@ The useful mapping keys in a :class:`LogRecord` are given in the section on ...@@ -544,6 +544,10 @@ The useful mapping keys in a :class:`LogRecord` are given in the section on
.. versionchanged:: 3.2 .. versionchanged:: 3.2
The *style* parameter was added. 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) .. method:: format(record)
......
...@@ -23,9 +23,11 @@ Copyright (C) 2001-2017 Vinay Sajip. All Rights Reserved. ...@@ -23,9 +23,11 @@ Copyright (C) 2001-2017 Vinay Sajip. All Rights Reserved.
To use, simply 'import logging' and log away! 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 Template
from string import Formatter as StrFormatter
__all__ = ['BASIC_FORMAT', 'BufferingFormatter', 'CRITICAL', 'DEBUG', 'ERROR', __all__ = ['BASIC_FORMAT', 'BufferingFormatter', 'CRITICAL', 'DEBUG', 'ERROR',
'FATAL', 'FileHandler', 'Filter', 'Formatter', 'Handler', 'INFO', 'FATAL', 'FileHandler', 'Filter', 'Formatter', 'Handler', 'INFO',
...@@ -413,15 +415,20 @@ def makeLogRecord(dict): ...@@ -413,15 +415,20 @@ def makeLogRecord(dict):
rv.__dict__.update(dict) rv.__dict__.update(dict)
return rv return rv
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Formatter classes and functions # Formatter classes and functions
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
_str_formatter = StrFormatter()
del StrFormatter
class PercentStyle(object): class PercentStyle(object):
default_format = '%(message)s' default_format = '%(message)s'
asctime_format = '%(asctime)s' asctime_format = '%(asctime)s'
asctime_search = '%(asctime)' asctime_search = '%(asctime)'
validation_pattern = re.compile(r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]', re.I)
def __init__(self, fmt): def __init__(self, fmt):
self._fmt = fmt or self.default_format self._fmt = fmt or self.default_format
...@@ -429,17 +436,50 @@ class PercentStyle(object): ...@@ -429,17 +436,50 @@ class PercentStyle(object):
def usesTime(self): def usesTime(self):
return self._fmt.find(self.asctime_search) >= 0 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__ 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): class StrFormatStyle(PercentStyle):
default_format = '{message}' default_format = '{message}'
asctime_format = '{asctime}' asctime_format = '{asctime}'
asctime_search = '{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__) 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): class StringTemplateStyle(PercentStyle):
default_format = '${message}' default_format = '${message}'
...@@ -454,9 +494,24 @@ class StringTemplateStyle(PercentStyle): ...@@ -454,9 +494,24 @@ class StringTemplateStyle(PercentStyle):
fmt = self._fmt fmt = self._fmt
return fmt.find('$asctime') >= 0 or fmt.find(self.asctime_format) >= 0 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__) return self._tpl.substitute(**record.__dict__)
BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s" BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
_STYLES = { _STYLES = {
...@@ -510,7 +565,7 @@ class Formatter(object): ...@@ -510,7 +565,7 @@ class Formatter(object):
converter = time.localtime 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. Initialize the formatter with specified format strings.
...@@ -530,6 +585,9 @@ class Formatter(object): ...@@ -530,6 +585,9 @@ class Formatter(object):
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)
if validate:
self._style.validate()
self._fmt = self._style._fmt self._fmt = self._style._fmt
self.datefmt = datefmt self.datefmt = datefmt
......
...@@ -666,11 +666,19 @@ class DictConfigurator(BaseConfigurator): ...@@ -666,11 +666,19 @@ class DictConfigurator(BaseConfigurator):
dfmt = config.get('datefmt', None) dfmt = config.get('datefmt', None)
style = config.get('style', '%') style = config.get('style', '%')
cname = config.get('class', None) cname = config.get('class', None)
if not cname: if not cname:
c = logging.Formatter c = logging.Formatter
else: else:
c = _resolve(cname) 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 return result
def configure_filter(self, config): def configure_filter(self, config):
......
This diff is collapsed.
logging.Formatter enhancement - Ensure styles and fmt matches in
logging.Formatter - Added validate method in each format style class:
StrFormatStyle, PercentStyle, StringTemplateStyle. - This method is called
in the constructor of logging.Formatter class - Also re-raise the KeyError
in the format method of each style class, so it would a bit clear that it's
an error with the invalid format fields.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment