-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
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
Changes from 2 commits
66bbddf
0eaaf25
f4f6a60
d51e186
cc2cfbd
7c24e46
abd868d
c1359bb
921ba33
8484f29
4c35216
6eee298
aab0c46
59879e9
43c0b34
908c3ec
d73ab3a
ff05742
2664f85
74ca6a2
de1243f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,10 +23,13 @@ | |
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 contextlib import contextmanager | ||
|
||
|
||
__all__ = ['BASIC_FORMAT', 'BufferingFormatter', 'CRITICAL', 'DEBUG', 'ERROR', | ||
'FATAL', 'FileHandler', 'Filter', 'Formatter', 'Handler', 'INFO', | ||
'LogRecord', 'Logger', 'LoggerAdapter', 'NOTSET', 'NullHandler', | ||
|
@@ -413,10 +416,10 @@ def makeLogRecord(dict): | |
rv.__dict__.update(dict) | ||
return rv | ||
|
||
|
||
#--------------------------------------------------------------------------- | ||
# Formatter classes and functions | ||
#--------------------------------------------------------------------------- | ||
|
||
class PercentStyle(object): | ||
|
||
default_format = '%(message)s' | ||
|
@@ -426,19 +429,44 @@ class PercentStyle(object): | |
def __init__(self, fmt): | ||
self._fmt = fmt or self.default_format | ||
|
||
@contextmanager | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand the thinking behind this. Why do we need a context manager for this? Please explain, See other comments further below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used a context manager to avoid duplication, however I like your solution better. I'm going to make the change :) |
||
def _verifyFields(self): | ||
""" | ||
Verify if fields passed into XXXStyle. | ||
""" | ||
try: | ||
yield | ||
except KeyError as e: | ||
raise ValueError("Invalid formatting field %s" % e) | ||
|
||
def usesTime(self): | ||
return self._fmt.find(self.asctime_search) >= 0 | ||
|
||
def validate(self): | ||
vsajip marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._validateFormat(r'(\%\([^%()]+\)[\s\d-]*[\w]+)') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How did you arrive at this pattern? It doesn't seem to match the specification given here. It might be OK to match just the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, this does seem to match the example you have given. let me try and redo this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I have gotten quite a few things wrong in this pattern. I thought something like this would be valid |
||
|
||
def _validateFormat(self, expression): | ||
matches = re.search(expression, self._fmt) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if matches is None: | ||
raise ValueError('Invalid format %(fmt)s for %(style)s style' | ||
% {"fmt": self._fmt, "style": self.default_format[0]}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You really don't need to use named values in the format string in this context. I gave a specific example in the earlier comment beginning "This would only be needed in PercentStyle:" - did you miss it? Also, you're not using a compiled pattern, as suggested. |
||
|
||
def format(self, record): | ||
return self._fmt % record.__dict__ | ||
with self._verifyFields(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before this PR, each of the BEFORE: in each
AFTER: In each
and in PercentStyle only (the base class for the others), define:
|
||
return self._fmt % record.__dict__ | ||
|
||
|
||
class StrFormatStyle(PercentStyle): | ||
default_format = '{message}' | ||
asctime_format = '{asctime}' | ||
asctime_search = '{asctime' | ||
|
||
def format(self, record): | ||
return self._fmt.format(**record.__dict__) | ||
with self._verifyFields(): | ||
return self._fmt.format(**record.__dict__) | ||
|
||
def validate(self): | ||
self._validateFormat(r'(\{[^{}]+\})') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How did you arrive at this pattern? It matches There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems reasonable, I will exclude some special characters in the keys |
||
|
||
|
||
class StringTemplateStyle(PercentStyle): | ||
|
@@ -455,7 +483,13 @@ def usesTime(self): | |
return fmt.find('$asctime') >= 0 or fmt.find(self.asctime_format) >= 0 | ||
|
||
def format(self, record): | ||
return self._tpl.substitute(**record.__dict__) | ||
with self._verifyFields(): | ||
return self._tpl.substitute(**record.__dict__) | ||
|
||
def validate(self): | ||
with self._verifyFields(): | ||
self._validateFormat(r'(\$\{[^${}]+\})') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How did you arrive at this pattern? It matches |
||
|
||
|
||
BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s" | ||
|
||
|
@@ -530,6 +564,8 @@ 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) | ||
self._style.validate() | ||
vsajip marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
self._fmt = self._style._fmt | ||
self.datefmt = datefmt | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3163,7 +3163,13 @@ def verify_reverse(stuff): | |
], pat=r"^[\w.]+ -> (\w+): (\d+)$") | ||
|
||
def test_out_of_order(self): | ||
self.apply_config(self.out_of_order) | ||
self.assertRaises(ValueError, self.apply_config, self.out_of_order) | ||
|
||
def test_out_of_order_with_dollar_style(self): | ||
config = self.out_of_order.copy() | ||
config['formatters']['mySimpleFormatter']['format'] = "${asctime} (${name}) ${levelname}: ${message}" | ||
|
||
self.apply_config(config) | ||
handler = logging.getLogger('mymodule').handlers[0] | ||
self.assertIsInstance(handler.target, logging.Handler) | ||
self.assertIsInstance(handler.formatter._style, | ||
|
@@ -3491,50 +3497,55 @@ def test_percent(self): | |
f = logging.Formatter('${%(message)s}') | ||
self.assertEqual(f.format(r), '${Message with 2 placeholders}') | ||
f = logging.Formatter('%(random)s') | ||
self.assertRaises(KeyError, f.format, r) | ||
self.assertRaises(ValueError, f.format, r) | ||
self.assertFalse(f.usesTime()) | ||
f = logging.Formatter('%(asctime)s') | ||
self.assertTrue(f.usesTime()) | ||
f = logging.Formatter('%(asctime)-15s') | ||
self.assertTrue(f.usesTime()) | ||
f = logging.Formatter('asctime') | ||
self.assertFalse(f.usesTime()) | ||
f = logging.Formatter('%(asctime) - 15s') | ||
self.assertTrue(f.usesTime()) | ||
# Testing ValueError raised from mismatching format | ||
self.assertRaises(ValueError, logging.Formatter, '{asctime}') | ||
self.assertRaises(ValueError, logging.Formatter, '${message}') | ||
|
||
def test_braces(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add an extra test |
||
# Test {}-formatting | ||
r = self.get_record() | ||
f = logging.Formatter('$%{message}%$', style='{') | ||
self.assertEqual(f.format(r), '$%Message with 2 placeholders%$') | ||
f = logging.Formatter('{random}', style='{') | ||
self.assertRaises(KeyError, f.format, r) | ||
self.assertRaises(ValueError, f.format, r) | ||
f = logging.Formatter("{message}", style='{') | ||
self.assertFalse(f.usesTime()) | ||
f = logging.Formatter('{asctime}', style='{') | ||
self.assertTrue(f.usesTime()) | ||
f = logging.Formatter('{asctime!s:15}', style='{') | ||
self.assertTrue(f.usesTime()) | ||
f = logging.Formatter('{asctime:15}', style='{') | ||
self.assertTrue(f.usesTime()) | ||
f = logging.Formatter('asctime', style='{') | ||
self.assertFalse(f.usesTime()) | ||
# Testing ValueError raised from mismatching format | ||
self.assertRaises(ValueError, logging.Formatter, '%(asctime)s', style='{') | ||
|
||
def test_dollars(self): | ||
# Test $-formatting | ||
r = self.get_record() | ||
f = logging.Formatter('$message', style='$') | ||
f = logging.Formatter('${message}', style='$') | ||
self.assertEqual(f.format(r), 'Message with 2 placeholders') | ||
f = logging.Formatter('$$%${message}%$$', style='$') | ||
self.assertEqual(f.format(r), '$%Message with 2 placeholders%$') | ||
f = logging.Formatter('${random}', style='$') | ||
self.assertRaises(KeyError, f.format, r) | ||
self.assertRaises(ValueError, f.format, r) | ||
self.assertFalse(f.usesTime()) | ||
f = logging.Formatter('${asctime}', style='$') | ||
self.assertTrue(f.usesTime()) | ||
f = logging.Formatter('${asctime', style='$') | ||
f = logging.Formatter('${message}', style='$') | ||
self.assertFalse(f.usesTime()) | ||
f = logging.Formatter('$asctime', style='$') | ||
f = logging.Formatter('${asctime}--', style='$') | ||
self.assertTrue(f.usesTime()) | ||
f = logging.Formatter('asctime', style='$') | ||
self.assertFalse(f.usesTime()) | ||
# Testing ValueError raised from mismatching format | ||
self.assertRaises(ValueError, logging.Formatter, '{asctime}', style='$') | ||
self.assertRaises(ValueError, logging.Formatter, '%(asctime)s', style='$') | ||
|
||
vsajip marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def test_invalid_style(self): | ||
self.assertRaises(ValueError, logging.Formatter, None, None, 'x') | ||
|
@@ -4000,10 +4011,10 @@ def test_stream(self): | |
self.assertEqual(handler.stream, stream) | ||
|
||
def test_format(self): | ||
logging.basicConfig(format='foo') | ||
logging.basicConfig(format='%(asctime)s - %(message)s') | ||
|
||
formatter = logging.root.handlers[0].formatter | ||
self.assertEqual(formatter._style._fmt, 'foo') | ||
self.assertEqual(formatter._style._fmt, '%(asctime)s - %(message)s') | ||
|
||
def test_datefmt(self): | ||
logging.basicConfig(datefmt='bar') | ||
|
@@ -4032,11 +4043,11 @@ def test_incompatible(self): | |
handlers = [logging.StreamHandler()] | ||
stream = sys.stderr | ||
assertRaises(ValueError, logging.basicConfig, filename='test.log', | ||
stream=stream) | ||
stream=stream) | ||
assertRaises(ValueError, logging.basicConfig, filename='test.log', | ||
handlers=handlers) | ||
handlers=handlers) | ||
assertRaises(ValueError, logging.basicConfig, stream=stream, | ||
handlers=handlers) | ||
handlers=handlers) | ||
# Issue 23207: test for invalid kwargs | ||
assertRaises(ValueError, logging.basicConfig, loglevel=logging.INFO) | ||
# Should pop both filename and filemode even if filename is None | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is no longer needed - please remove.