diff options
author | BNMetrics <luna@bnmetrics.com> | 2018-10-15 18:41:36 (GMT) |
---|---|---|
committer | Vinay Sajip <vinay_sajip@yahoo.co.uk> | 2018-10-15 18:41:36 (GMT) |
commit | 18fb1fb943b7dbd7f8a76017ee2a67ef13effb85 (patch) | |
tree | eb8236a053b1f1b5d46374d6271a53f2136fc1cd /Lib/logging | |
parent | e890421e334ccf0c000c6b29c4a521d86cd12f47 (diff) | |
download | cpython-18fb1fb943b7dbd7f8a76017ee2a67ef13effb85.zip cpython-18fb1fb943b7dbd7f8a76017ee2a67ef13effb85.tar.gz cpython-18fb1fb943b7dbd7f8a76017ee2a67ef13effb85.tar.bz2 |
bpo-34844: logging.Formatter enhancement - Ensure style and format string matches in logging.Formatter (GH-9703)
Diffstat (limited to 'Lib/logging')
-rw-r--r-- | Lib/logging/__init__.py | 68 | ||||
-rw-r--r-- | Lib/logging/config.py | 10 |
2 files changed, 72 insertions, 6 deletions
diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 7aeff45..58afcd2 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -23,9 +23,11 @@ Copyright (C) 2001-2017 Vinay Sajip. All Rights Reserved. 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', @@ -413,15 +415,20 @@ 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 @@ -429,17 +436,50 @@ class PercentStyle(object): 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}' @@ -454,9 +494,24 @@ class StringTemplateStyle(PercentStyle): 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 = { @@ -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. @@ -530,6 +585,9 @@ class Formatter(object): 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 diff --git a/Lib/logging/config.py b/Lib/logging/config.py index fa1a398..cfd9311 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -666,11 +666,19 @@ class DictConfigurator(BaseConfigurator): 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): |