Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Doc/library/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,9 @@ 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
Incorrect or mismatch style and fmt will raise a ``ValueError``. For
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mismatch -> mismatched

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
Loading