Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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 defaults of ``True``) can be added into

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

defaults -> a default

the ``formatters`` section of the configuring dict, this is to
validate the correct format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

validate the correct format -> 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
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
Copy Markdown
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):
Comment thread
vsajip marked this conversation as resolved.
Resolved
"""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
19 changes: 18 additions & 1 deletion Lib/logging/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import sys
import threading
import traceback
from inspect import signature

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIRC importing inspect can be expensive, so consider only importing it if and where needed. If it is kept here, note that it needs to go in the above list in alphabetical order (I know other parts of logging don't follow that, but this module does). We may not even need it - see comment below.


from socketserver import ThreadingTCPServer, StreamRequestHandler

Expand Down Expand Up @@ -666,13 +667,29 @@ def configure_formatter(self, config):
dfmt = config.get('datefmt', None)
style = config.get('style', '%')
cname = config.get('class', None)
validate = config.get('validate', True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I might be overthinking this, but the logic in this area should perhaps be:

if not cname:
    c = logging.Formatter
else:
    c = _resolve(cname)  # this might actually resolve to logging.Formatter

if c is logging.Formatter:
    # we know c will accept the validate parameter
    use_validate = True
    validate = config.get('validate', True)
else:
    # we don't yet know if c will accept a validate parameter.
    use_validate = False
    # Here, we could go to some trouble to determine whether there's a validate
    # or kwargs parameter, as you've done. But we have to take care:  there are
    # a number of things to consider. We currently call c with fmt, dfmt, style
    # arguments. So we expect c to be a class which inherits from
    # logging.Formatter and either doesn't redefine __init__, or does redefine
    # it. In the first case, we'd be safe to set use_validate to True, but in
    # the second, we wouldn't, because we don't know if this a Formatter
    # subclass written after 3.8 was released - when the developer of it would
    # be expected to know about the validate parameter - or whether it was a
    # class designed before 3.8, when they wouldn't be expected to know about
    # it. If it's specified in the config, then they know about it and we can
    # assume it's a post-3.8 design, so we can pass the configured value;
    # otherwise, we have as yet no idea if the redefined __init__ will accept
    # validate. We could dig into its signature to see if we could figure it
    # out, but there's no guarantee it will be *our* validate parameter - even
    # if we can figure out that it's expected to be a bool, we have no idea
    # how a True value would be interpreted. So it's safest not to pass it.

    # In the case that c is a class which doesn't even inherit from Formatter,
    # we have no idea what its __init__ does, though we do know that it has to
    # take (fmt, dfmt, style) parameters, otherwise it wouldn't work now.
    # Thus:

    if issubclass(c, logging.Formatter):
        if '__init__' not in c.__dict__:  # not redefined
            use_validate = True
        elif 'validate' in config:  # user has specified it, so OK to use
            use_validate = True
        # else:
            # if we decided to implement signature checking, we would import
            # inspect here and introspect the signature. Note that you couldn't
            # rely on a parameter literally named 'kwargs' - someone could have
            # used **kw, for example. You'd have to rely on there being a
            # specific parameter p such that p.kind == inspect._VAR_KEYWORD.

if use_validate :
    result = c(fmt, dfmt, style)
else:
    result = c(fmt, dfmt, style, validate)

if not cname:
c = logging.Formatter
result = c(fmt, dfmt, style, validate)
else:
c = _resolve(cname)
result = c(fmt, dfmt, style)
if self._has_validate_param(config, c):
result = c(fmt, dfmt, style, validate)
else:
result = c(fmt, dfmt, style)
return result

def _has_validate_param(self, config, formatter_callable):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This may not be needed, see above.

"""Check if formatter callable has validate parameter"""
params = signature(formatter_callable).parameters
if not isinstance(formatter_callable, logging.Formatter) or \
not params.get('validate') or params.get("kwargs"):
if config.get('validate'):
raise TypeError('%r has not validate in parameters' % formatter_callable)

return False
return True

def configure_filter(self, config):
"""Configure a filter from a dictionary."""
if '()' in config:
Expand Down
Loading